;; Two names for the two things a `.catch` can mean.
;;
;; This tree had ~22 copies of `(.catch p (fn [_] nil))`, and read literally
;; every one of them says the same thing: "whatever went wrong, forget it".
;; They do not mean the same thing. Some of them are the protocol
;; (PROTOCOL.md §3: any decryption failure MUST drop silently — a peer that
;; cannot open a frame is indistinguishable from a peer that never got one,
;; and logging it would be a side channel as well as a flood). The rest are
;; ours: a store that would not open, an OrbitDB append that rejected, a code
;; chunk that would not load. Those are bugs, and swallowing them is how a boot
;; failure becomes "the app just sits there".
;;
;; The boot path is where that distinction has actually cost something. Both
;; production outages in this repo were promise chains whose failure had no
;; voice: the room never appeared and the console was clean. So the two cases
;; get two names, and picking one is a decision the next reader can see.
;;
;; drop-silently the failure is EXPECTED and carries no information —
;; an AEAD tag that did not verify, a peer that went away
;; mid-send, a browser that refuses persistent storage.
;; tolerate the failure is OURS. The app continues without this, but
;; someone should be told, and `label` is what they will read.
;; It logs at console.ERROR, which is the level `init`'s
;; "boot failed" already used and the level the rest of these
;; sites should always have had: a swallowed defect that warns
;; is a defect nobody reads.
;;
;; …and two names for the two things a promise stored in a MUTABLE FIELD can
;; mean. Those were left out of the first pass on purpose, because their only
;; users are lib/mailbox and lib/media and neither had been touched yet:
;;
;; locked! a promise-chain MUTEX. `peer.queue`, `peer.mediaQueue` and
;; mailbox's `_enqueueChain` are all the same trick: the field
;; holds the tail of a chain, and appending to it is what
;; makes two calls run strictly one after the other. Nothing
;; in the code said so — it read as three unrelated
;; `(unchecked-set o "q" (.then (unchecked-get o "q") …))`
;; lines, and test/vectors/media-effects.json's `signalMutex`
;; scenario exists precisely because replacing one of them
;; with a `p/let` would look like a simplification and would
;; silently interleave two signal handlers.
;; single-flight! at most one call in flight; every caller that arrives while
;; one is running gets that same promise. TS wrote it as
;; `this.#inflight ??= f().finally(() => this.#inflight = null)`.
;;
;; Deliberately promesa-free, and deliberately not `p/catch`: this namespace is
;; reachable from `sueta.main`, which is the :main module — first paint — and
;; the whole point of the :room split is that the lobby pays for none of the
;; room stack. `.catch`/`.then`/`.finally` are on both js/Promise and promesa's
;; PromiseImpl, so both callers get the same four words. It requires NOTHING
;; (test/module-split.test.mjs asserts the empty require list), which is what
;; keeps it safe to use from either side of the split.
(ns sueta.fx)
(defn drop-silently
"`p`, with an expected failure turned into nil. No log: the failure is not
news. Use ONLY where a rejection genuinely carries no information."
[^js p]
(.catch p (fn [_] nil)))
(defn tolerate
"`p`, with OUR failure turned into nil and a console error under `label`.
Use where the app can continue but the rejection is a defect, not weather."
[label ^js p]
(.catch p (fn [e] (js/console.error label e) nil)))
(defn locked!
"Run `(f)` after whatever chain `o[k]` already holds, and store the new tail
back in `o[k]`. Returns that tail.
THE MUTEX, named. Two `locked!` calls on the same field never overlap: the
second starts only once the first has fully settled, because it is chained
onto it rather than started beside it. `p/let` gives the same guarantee
WITHIN one call and none at all BETWEEN two, which is the substitution this
name exists to make visible.
`(f)` is not wrapped in a catch, deliberately: every caller here already ends
its own body in `drop-silently`/`tolerate`, and adding one would change a
rejecting body from 'poisons the chain' to 'is swallowed' — a behaviour
change disguised as a helper."
[o k f]
(let [tail (.then ^js (unchecked-get o k) (fn [_] (f)))]
(unchecked-set o k tail)
tail))
(defn single-flight!
"`(f)`, unless one is already in flight — in which case that promise, and no
second call. `cell` is an atom holding the live promise or nil.
TS's `this.#inflight ??= f().finally(() => (this.#inflight = null))`. The
`.finally` clears the cell whichever way it settled, so a failure does not
latch the caller out; the next call mints afresh."
[cell f]
(or @cell
(let [p (.finally ^js (f) (fn [] (reset! cell nil)))]
(reset! cell p)
p)))