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)))))))
|