chat / client / src / sueta / wire.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
;; The wire/storage boundary — the one place a declared shape becomes a JS
;; object, or is read back out of one.
;;
;; WHY IT IS A NAMESPACE. Every JSON shape this app persists or sends is a
;; contract: PROTOCOL.md fixes the key ORDER of the wire ones, and the storage
;; ones are what an existing user's browser already holds. Before this file,
;; each shape was spelled out at its call site as a `j/ordered "a" x "b" y …`
;; and re-read field by field with `unchecked-get` somewhere else, so the
;; contract existed only as the sum of two lists of string literals that had to
;; be kept in step by eye.
;;
;; A shape is now an ordered vector of [clojure-key wire-key] pairs. `encode`
;; walks it, doing exactly the sequential `unchecked-set` that
;; ardegazu.rooms.js/ordered does — so the bytes do not move, and neither does
;; the eight-pair `#js {}` hash-order cliff, which this construct never had.
;; What changes is that key order is a VALUE: printable, diffable, and testable
;; without serialising anything.
;;
;; `decode` goes the other way and returns a Clojure map — never a half-JS one.
;; It drops absent fields (JS `undefined` and `null` are both nil to CLJS), and
;; it does NOT validate: each caller states its own drop-vs-clamp rule next to
;; the rule it implements, instead of the policy emerging from a compound `or`
;; two hundred lines from the clamp.
;;
;; ABOVE THIS FILE, nil is the only absence, so plain Clojure predicates are
;; safe. The JS forms live here and stay here: `arr?` is `Array.isArray`, NOT
;; `array?` — the latter compiles to `instance? js/Array`, which is
;; realm-sensitive, and in a validator that difference IS the wire contract.
(ns sueta.wire
  (:require [ardegazu.rooms.js :as j]))

;; ---- the JS forms ----------------------------------------------------------

(defn ^boolean arr? [x] (js/Array.isArray x))
(defn ^boolean str? [x] (identical? "string" (js* "typeof ~{}" x)))
(defn ^boolean num? [x] (identical? "number" (js* "typeof ~{}" x)))

(defn ^boolean finite-num? [x] (and (num? x) (js/Number.isFinite x)))

(defn non-empty
  "The string, or nil if it is absent, not a string, or \"\" — the JS truthiness
   test the originals wrote as a bare `if (s)` on a string-or-missing field.
   Returning the value rather than a boolean is what lets it feed `encode`
   directly, where nil means 'omit this key'."
  [x]
  (when (and (str? x) (pos? (.-length ^string x))) x))

(defn oget
  "TS's `o?.k`: nil for a nullish holder. `j/truthy?` is load-bearing here — it
   is what makes a null/undefined holder safe to read through, exactly as the
   original's `x && x.k` did. CLJS already reads `undefined` as nil for `nil?`
   and `some?`, so the result needs no further normalising."
  [o k]
  (when (j/truthy? o)
    (unchecked-get o k)))

;; ---- shapes ----------------------------------------------------------------

(defn encode
  "A fresh JS object carrying `shape`'s fields, set in declaration order.
   A nil value is omitted — that is how an optional field stays optional."
  [shape m]
  (let [o (js-obj)]
    (doseq [[k wire] shape]
      (when-some [v (get m k)] (unchecked-set o wire v)))
    o))

(defn encode-all [shape ms]
  (into-array (map #(encode shape %) ms)))

(defn decode
  "`shape`'s fields off a JS object, as a Clojure map; nil for a nullish input.
   Absent fields are absent from the map, so `nil` is the only absence above."
  [shape o]
  (when (j/truthy? o)
    (reduce (fn [m [k wire]]
              (let [v (unchecked-get o wire)]
                (if (some? v) (assoc m k v) m)))
            {}
            shape)))

(defn decode-all [shape xs]
  (map #(decode shape %) xs))

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