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 | ;; ported-from: src/app/migrate.ts
;;
;; One-shot import of v1 device-local history into the v2 replicated log.
;;
;; v1 stored the last 200 messages per room in IndexedDB under the v1 roomId
;; (derived with the v1 app salt). On the first v2 open of a room we read that
;; record, publish its text/threads/reactions as `legacy` log ops (batched), and
;; mark the room migrated. Ids become "v1:<peerId>:<seq>" so two members racing
;; to import the same snapshot fold into no-ops on each other. Legacy image
;; blobs are not migrated (they were ephemeral by design in v1).
(ns sueta.app.migrate
(:require [sueta.app.history :as history]
[sueta.config :as config]
[ardegazu.rooms.js :as j]
[ardegazu.rooms.lib.crypto :as rc]
[shadow.cljs.modern :refer (js-await)]))
(def ^:private V1-SALT-SUFFIX "/v1")
(def ^:private BATCH 60)
;; Wire/storage validators use the JS forms the TypeScript used, not the Clojure
;; predicates: `array?` compiles to `instance? js/Array` for a :browser build,
;; which is realm-sensitive, while TS wrote `Array.isArray`. In a validator that
;; difference IS the wire contract.
(defn- ^boolean arr? [x] (js/Array.isArray x))
;; JS `typeof x === "…"`, not Clojure predicates: the values are remote input
(defn- ^boolean s? [x] (identical? "string" (js* "typeof ~{}" x)))
(defn- ^boolean n? [x] (identical? "number" (js* "typeof ~{}" x)))
(defn migrate-legacy-history
"Publishes this device's v1 snapshot as `legacy` ops; resolves to the count."
[secret app-salt-v2 log store]
(-> (js/Promise.resolve nil)
(.then
(fn [_]
(let [salt-v1 (.replace ^string app-salt-v2 (js/RegExp. "\\/v2$") V1-SALT-SUFFIX)]
(-> (rc/create secret salt-v1)
(.then (fn [rc-v1] (unchecked-get rc-v1 "roomId")))
(.catch (fn [_] nil))
(.then
(fn [room-id-v1]
(if (nil? room-id-v1)
0
(js-await [rec (history/load-history room-id-v1)]
(let [msgs-in (when (j/truthy? rec) (unchecked-get rec "msgs"))]
(if-not (and (j/truthy? rec) (arr? msgs-in) (pos? (.-length ^js msgs-in)))
0
(let [legacy-mine (unchecked-get store "legacyMine")
mine-ids (unchecked-get rec "mineIds")]
;; "mine" styling is device-local knowledge — feed it
;; to the projector regardless of who ends up
;; publishing the legacy ops first
(doseq [id (array-seq (if (arr? mine-ids) mine-ids (array)))]
(when (s? id) (.add ^js legacy-mine id)))
(let [flag-key (volatile! "")
already (try
(vreset! flag-key (config/ns-key (str "migrated:" room-id-v1)))
(j/truthy? (js/localStorage.getItem @flag-key))
(catch :default _ false))]
(if already
0 ; this device already published
;; belt-and-braces for devices where the flag
;; can't persist (private mode, quota): if the
;; replayed log already folded any of this
;; snapshot's ids, someone (maybe us, last boot)
;; already published it — don't append again
(let [probe (.find ^js msgs-in
(fn [m] (s? (when (j/truthy? m) (unchecked-get m "id")))))]
(if (and (j/truthy? probe)
(.hasMessage ^js store (str "v1:" (unchecked-get probe "id"))))
0
(let [msgs (array)]
(doseq [m (array-seq msgs-in)]
(let [id (when (j/truthy? m) (unchecked-get m "id"))
ts (when (j/truthy? m) (unchecked-get m "ts"))
text (when (j/truthy? m) (unchecked-get m "text"))
thread (when (j/truthy? m) (unchecked-get m "thread"))
rx (when (j/truthy? m) (unchecked-get m "rx"))
from (when (j/truthy? m) (unchecked-get m "from"))
nm (when (j/truthy? m) (unchecked-get m "name"))]
(when (and (s? id) (n? ts)
(s? text) (j/truthy? text))
;; a LegacyMsg: {lid, from, name, ts,
;; text?, thread?, rx?} — SEVEN keys
;; at most, built in wire order
(let [lm (j/ordered "lid" id
"from" (js* "String(~{})" (j/nn from ""))
"name" (.slice (js* "String(~{})" (j/nn nm "?")) 0 40)
"ts" ts
"text" (.slice ^string text 0 8000))]
(when (s? thread) (unchecked-set lm "thread" thread))
(when (arr? rx) (unchecked-set lm "rx" rx))
(.push msgs lm)))))
(if (zero? (.-length msgs))
0
(let [published (volatile! 0)]
(-> ((fn step [i]
(if (>= i (.-length msgs))
(js/Promise.resolve nil)
(let [batch (.slice msgs i (+ i BATCH))]
(js-await [_ (.append ^js log
(j/ordered "t" "legacy"
"ts" (js/Date.now)
"msgs" batch))]
(do (vswap! published + (.-length batch))
(step (+ i BATCH)))))))
0)
(.then (fn [_]
(try
(when (j/truthy? @flag-key)
(js/localStorage.setItem @flag-key (str (js/Date.now))))
(catch :default _ nil))
@published)))))))))))))))))))))))
|