dev / templates / app-cljs-rooms / client / src / __NAME__ / main.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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
;; Bootstrap: PWA update banner + lobby, then lazy-load the room stack
;; (__NAME__.room). __NAME__.i18n is required first among the text-bearing
;; namespaces — its load resolves the language and hydrates the static markup
;; before anything renders.
(ns __NAME__.main
  (:require [__NAME__.i18n :as i18n :refer (t)]
            [__NAME__.app.rooms :as rooms]
            [__NAME__.config :as config]
            [ardegazu.rooms.js :as j]
            [ardegazu.rooms.lib.crypto :as rc]
            [shadow.cljs.modern :refer (js-await)]
            [shadow.lazy :as lazy]))

(def ^:private room-loadable (lazy/loadable __NAME__.room/boot-room))

(def VERSION config/APP-VERSION)

(defn- fetch-live-version
  "What version is live on the server right now? (bypasses the SW precache)"
  []
  (-> (js/fetch (str "./version.json?cb=" (js/Date.now)) (j/ordered "cache" "no-store"))
      (.then (fn [res] (.json ^js res)))
      (.then (fn [body]
               (let [v (when (j/truthy? body) (unchecked-get body "version"))]
                 (if (identical? "number" (js* "typeof ~{}" v)) v nil))))
      (.catch (fn [_] nil))))

(defn- show-update-banner! [update-sw]
  (if (j/truthy? (js/document.getElementById "upd-banner"))
    (js/Promise.resolve nil)
    (js-await [live (fetch-live-version)]
      (let [b (js/document.createElement "button")]
        (set! (.-id b) "upd-banner")
        (set! (.-className b) "update-banner")
        (set! (.-textContent b)
              (if (and (j/truthy? live) (not (identical? live VERSION)))
                (t "update.ready-v" {"live" live "v" VERSION})
                (t "update.ready")))
        (.addEventListener
         b "click"
         (fn []
           (set! (.-textContent b) (t "update.updating"))
           ;; Own the reload: the SW only signals "waiting", and controllerchange
           ;; may never fire on a page that had no controller.
           (let [reloaded (volatile! false)
                 reload (fn []
                          (when-not @reloaded
                            (vreset! reloaded true)
                            (.reload js/location))
                          js/undefined)
                 sw (.-serviceWorker js/navigator)]
             (when (j/truthy? sw)
               (.addEventListener ^js sw "controllerchange" reload (j/ordered "once" true)))
             (js/setTimeout reload 4000) ; fallback if controllerchange never fires
             (update-sw))
           js/undefined))
        (.appendChild js/document.body b)
        js/undefined))))

(defn- register-sw!
  "The suite's PROMPT-mode update contract (workbox generateSW with
   skipWaiting:false): the new worker waits for a SKIP_WAITING message, which is
   what the banner's `update-sw` sends. Release builds only — the dev server
   ships no sw.js, and a dev-origin SW squats. Never flip this to auto-update:
   the version.json banner IS the contract for a versioned app."
  []
  (when (and (not ^boolean goog.DEBUG) (j/truthy? (.-serviceWorker js/navigator)))
    (-> (.register (.-serviceWorker js/navigator) "./sw.js")
        (.then
         (fn [reg]
           (let [update-sw (fn []
                             (let [waiting (.-waiting ^js reg)]
                               (when (j/truthy? waiting)
                                 (.postMessage ^js waiting (j/ordered "type" "SKIP_WAITING"))))
                             js/undefined)
                 offer (fn []
                         (when (and (j/truthy? (.-waiting ^js reg))
                                    (j/truthy? (.-controller (.-serviceWorker js/navigator))))
                           (show-update-banner! update-sw))
                         js/undefined)]
             ;; a worker already waiting when we registered
             (offer)
             (.addEventListener ^js reg "updatefound"
                                (fn []
                                  (let [installing (.-installing ^js reg)]
                                    (when (j/truthy? installing)
                                      (.addEventListener ^js installing "statechange"
                                                         (fn []
                                                           (when (identical? "installed"
                                                                             (.-state ^js installing))
                                                             (offer))
                                                           js/undefined))))
                                  js/undefined))
             ;; look for updates every 5 minutes and whenever the app comes back
             (js/setInterval (fn [] (.catch (.update ^js reg) (fn [_] nil))) 300000)
             (.addEventListener js/document "visibilitychange"
                                (fn []
                                  (when (identical? "visible" (.-visibilityState js/document))
                                    (.catch (.update ^js reg) (fn [_] nil)))
                                  js/undefined))
             js/undefined)))
        (.catch (fn [_] nil))))
  js/undefined)

(def ^:private SECRET-RE (js/RegExp. "^[A-Za-z0-9_-]{43}$"))

(defn- boot []
  (register-sw!)
  ;; OrbitDB quirk: a decrypt failure in the heads-exchange pipe surfaces as an
  ;; unhandled rejection (orphaned mid-pipe promise). Log-and-continue.
  (.addEventListener js/window "unhandledrejection"
                     (fn [^js e]
                       (let [reason (.-reason e)
                             msg (js* "String(~{})" (j/nn (when (j/truthy? reason)
                                                            (unchecked-get reason "message"))
                                                          ""))]
                         (when (.includes ^string msg "Could not decrypt") (.preventDefault e)))
                       js/undefined))

  ;; fragment: #<secret>[&relay] — never sent to any server
  (let [frag (.slice (.-hash js/location) 1)
        parts (.split ^string frag "&")
        secret-part (aget parts 0)
        force-relay (.includes (.slice parts 1) "relay")
        root (js/document.getElementById "app")]
    (js-await [secret (if (.test SECRET-RE secret-part)
                        (js/Promise.resolve secret-part)
                        ;; no room link → lobby: pick one of your rooms or start
                        ;; a new one
                        (rooms/lobby root rc/new-room-secret))]
      (do
        (.replaceState js/history nil "" (str "#" secret (if force-relay "&relay" "")))
        ;; opening a different room link while running → clean re-boot into it
        (.addEventListener js/window "hashchange" (fn [] (.reload js/location) js/undefined))
        ;; the p2p stack is the heavy chunk — load it only once we're entering a
        ;; room
        (js-await [boot-room (lazy/load room-loadable)]
          (boot-room root secret force-relay))))))

(defn init []
  (-> (boot)
      (.catch (fn [e] (js/console.error "boot failed" e) nil))))

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