wallet-kit / src / ardegazu / wallet / 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
;; The named JavaScript boundary of the wallet kit — compile-time only.
;;
;; Everything below is a MACRO, so this namespace ships zero bytes: there is no
;; runtime namespace to load and nothing lands in the module graph. The set is
;; social-kit's, kept deliberately identical rather than "improved" — the two
;; kits are read side by side and a reader who knows one should not have to
;; re-learn the other. Each macro expands to the same primitive a hand-written
;; port would have typed, so the emitted JavaScript is unchanged; what they buy
;; is that the places where JS semantics leak into this codebase have names.
;;
;; 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. In THIS kit almost everything is below the
;; boundary: the four artifacts are JavaScript objects whose key order is the
;; wire, and the ledger hands its records straight to app code and to a
;; hand-written .d.ts.
(ns ardegazu.wallet.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 — an empty memo must not go on the wire as a present one.
  Keeping it spelled out means the sites that genuinely need JS falsiness are
  greppable, instead of being an accident of whichever predicate got typed."
  [x]
  (list 'js* "!!(~{})" x))

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

  (oget rc \"po\" \"cur\") is (unchecked-get (unchecked-get rc \"po\") \"cur\").

  String keys, never `(.-cur ^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. Every field name in this file's
  neighbourhood is a WIRE name — renaming one is not a size win, it is a
  protocol break.

  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. Bind first instead:

    (let [^js po (oget rc \"po\")] (.hasOwnProperty po \"sig\"))"
  [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. social-kit shipped a dist with exactly
  that bug — its ten-pair friend records came out reordered.

  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.

  In this kit that is not a precaution, it is the format: `wpo` is THIRTEEN
  pairs, and the JSON a payer hands to a bank is `JSON.stringify(order)` in
  insertion order. Use `obj` for every artifact, every nested record and
  anything that could become one — never `#js {}`, never `js-obj`.

  (Only the SIGNATURE is taken over `canon()`, which sorts; the transmitted
  bytes are insertion-ordered. Both facts are pinned by the golden vectors.)"
  [& 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: an absent key is not the same as a present null,
  and 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.

  Nothing in the four v1 artifacts uses it — their key sets are EXACT and
  verify rejects a stray key (see shapes.cljs). It is here for the ledger's
  storage envelope, whose sections are additive by design."
  [& 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, so a chain of six would otherwise be six nested
  closures. This is the same chain with one binding vector, expanding to that
  `.then` ladder VERBATIM.

  Deliberately NOT promesa, for the same reason social-kit is not: a `p/let`
  result is thenable but not `instanceof Promise` and its rejections do not
  reach the global handler, and every promise this kit returns crosses into a
  consumer that a hand-authored .d.ts has told to expect `Promise<T>`.

  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/wallet-kit.git