social-kit / src / ardegazu / social / macros.clj
  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
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
;; The named JavaScript boundary of the social kit — compile-time only.
;;
;; Everything below is a MACRO. This namespace ships zero bytes: there is no
;; runtime namespace to load, nothing lands in the module graph, and `./join`
;; (the dependency-free subpath every app pulls at first paint) pays exactly
;; nothing for it. Each macro expands into the same primitive the transliterated
;; port wrote by hand, so the emitted JavaScript is what it always was — what
;; changes is that the four places JS semantics leak into this codebase now have
;; names, and a reader can see at a glance which line is talking to JavaScript
;; and which line is Clojure.
;;
;; The rule the rest of the kit follows: BELOW these macros, values are JS —
;; `undefined` is distinct from `null`, `0` and `""` are falsy, key order is a
;; wire fact. ABOVE them, values are Clojure — `nil` is the only absence and
;; ordinary predicates are safe. Every namespace decodes at its entry points and
;; encodes at its exits, and nothing in between reaches for `unchecked-get`.
(ns ardegazu.social.macros)

(defmacro truthy?
  "JavaScript's `!!x`, which is NOT `some?` and NOT `boolean`.

  Load-bearing wherever a value came from JavaScript and `0` or `\"\"` has to
  read as absent: a friend's empty `name` must not beat their petname, an empty
  `joinLink` must not go on a beacon. Keeping it spelled out means the sites
  that genuinely need JS falsiness are greppable, instead of being an accident
  of whichever predicate happened to get typed."
  [x]
  (list 'js* "!!(~{})" x))

(defmacro oget
  "Read a string key off a JS object, following a path.

  (oget self \"_o\" \"pubsub\") is (unchecked-get (unchecked-get self \"_o\") \"pubsub\").

  String keys, never `(.-pubsub ^js o)`: this kit compiles with :advanced, and a
  property access that inferred externs fail to protect is renamed to something
  short and the app dies at runtime with a green build. `unchecked-get` with a
  literal string cannot be renamed at all, so the safety is structural rather
  than dependent on inference succeeding.

  TRAP: `^js` does NOT survive this macro. `(.deposit ^js (oget st \"mbx\") …)`
  reads as if the target were tagged, but the reader attached the tag to the
  `(oget …)` FORM, and macroexpansion replaces that form. Carrying `(meta
  &form)` onto the expansion was tried and does not restore it — unchecked-get
  has its own inferred type and that is what shadow goes on. Bind first
  instead:

    (let [^js mbx (oget st \"mbx\")] (.deposit mbx bytes))

  The failure is quiet, because the tag only matters for method names absent
  from Closure's default externs. A whole file of untagged `.get`/`.set`/
  `.has`/`.slice` on Maps and Arrays compiles without a murmur; the one call to
  something of our own is the only :infer-warning, which is how the single
  instance of this was found — by scripts/compile-check.mjs, on its second run."
  [o & ks]
  (reduce (fn [x k] (list 'cljs.core/unchecked-get x k)) o ks))

(defmacro obj
  "A JS object literal whose key order is guaranteed at ANY width.

  `#js {}` and the `js-obj` MACRO both route their pairs through `(into {})`:
  eight pairs is a PersistentArrayMap and insertion order survives, nine is a
  PersistentHashMap and the emitted literal comes out in HASH order, at every
  optimization level, with no warning. This kit was live on that cliff — the
  shipped 2.1.0 dist emitted friends.cljs's ten-pair records as
  {xs,addedTs,petTs,x,name,lastApp,pub,pet,state,lastSeenTs}.

  This expands to sequential `unchecked-set` calls instead, so the order in the
  source is the order in the object no matter how many pairs there are. Use it
  for anything whose key order is, or could become, a wire or storage fact."
  [& kvs]
  (when-not (even? (count kvs))
    (throw (IllegalArgumentException. "obj needs an even number of forms")))
  (let [o (gensym "obj")]
    `(let [~o (cljs.core/js-obj)]
       ~@(for [[k v] (partition 2 kvs)]
           (list 'cljs.core/unchecked-set o k v))
       ~o)))

(defmacro obj-when
  "`obj`, with each pair dropped when its value is nil.

  The additive-wire-key shape: a beacon carries `br` only when the peer has
  brains to advertise, a friend record carries `note` only when there is one,
  and an absent key is not the same as a present nil — old readers key off
  absence. Written as one form instead of a build-then-patch sequence, so the
  full shape and the conditions are visible together."
  [& kvs]
  (when-not (even? (count kvs))
    (throw (IllegalArgumentException. "obj-when needs an even number of forms")))
  (let [o (gensym "obj")]
    `(let [~o (cljs.core/js-obj)]
       ~@(for [[k v] (partition 2 kvs)]
           (let [tmp (gensym "v")]
             `(let [~tmp ~v]
                (when-not (nil? ~tmp) (cljs.core/unchecked-set ~o ~k ~tmp)))))
       ~o)))

(defmacro awaits
  "Sequential promise bindings, flattened — the kit's one async idiom.

  (awaits [a (p1) b (f a)] body…)
    expands to
  (js-await [a (p1)] (js-await [b (f a)] body…))

  `js-await` is pure `.then` sugar: it expands to `(-> thenable (.then (fn [n]
  body)))`, verified against shadow-cljs 3.3.6's shadow/cljs/modern.cljc, and it
  binds exactly one name. Sixteen sequential awaits could therefore only ever be
  sixteen nested closures, which is how presence.cljs's `start` came to be seven
  levels deep and `sync-self` eighty lines of staircase. This is the same chain
  with one binding vector, and it expands to that `.then` ladder VERBATIM — the
  emitted JavaScript is byte-for-byte what hand-nesting produced.

  Deliberately NOT promesa, although promesa is what chat adopted and what
  dev/docs/CLJS.md calls canon. promesa's ClojureScript runtime is not
  js/Promise: `p/let` returns its own `promesa.impl.promise/PromiseImpl`, which
  is thenable and assimilated by `await` and `Promise.all` but is NOT
  `instanceof Promise`, and an unhandled rejection on one does not fire
  window.onunhandledrejection. In chat that is contained — promesa is gated
  below `sueta.room`, and every chain there ends in a catch of its own. Here it
  would not be: `attachSocial`, `agent.start`, `sendInvite`, `pairChannel` and
  the channel `seal`/`open` closures are the PUBLIC API of a kit that nine apps,
  the bot fleet and a hand-authored .d.ts consume as `Promise<T>`. Swapping the
  class those return is not a refactor of this repo, it is a behaviour change in
  nine others, and the only thing it buys over this macro is a dependency.

  No `(catch e …)` clause, unlike `js-await`: attach `(.catch …)` to the whole
  form so it is visible which rungs of the ladder it covers."
  [bindings & body]
  (when-not (vector? bindings)
    (throw (IllegalArgumentException. "awaits needs a binding vector")))
  (when-not (even? (count bindings))
    (throw (IllegalArgumentException. "awaits needs an even number of binding forms")))
  (if (empty? bindings)
    `(do ~@body)
    (let [[n v & more] bindings]
      `(-> ~v (.then (fn [~n] (awaits ~(vec more) ~@body)))))))

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/social-kit.git