chat / client / src / sueta / app / migrate.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
 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)))))))))))))))))))))))

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