chat / client / src / sueta / fx.cljs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
;; 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)))

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/chat.git