main.mx raw
1 package main
2
3 import (
4 "git.smesh.lol/musiquay/web/common/helpers"
5 "git.smesh.lol/musiquay/web/common/jsbridge/idb"
6 "git.smesh.lol/musiquay/web/common/jsbridge/sw"
7 "git.smesh.lol/musiquay/web/common/mw"
8 )
9
10 // Service worker: relay pool, subscription router, IDB event cache,
11 // DM decryption relay, MLS dispatch, cache lifecycle.
12 // WASM target. No goroutines. All async operations are explicit callback
13 // chains registered through the jsbridge. JS calls exported WASM functions;
14 // WASM calls back via registered imports.
15
16 const cacheName = "musiquay"
17
18 var staticFiles []string
19
20 // --- Subscription router ---
21
22 // --- Entry point ---
23
24 func initSwGlobals() {
25 staticFiles = []string{
26 "/",
27 "/index.html",
28 "/style.css",
29 "/musiquay-logo.svg",
30 "/sw-register.js",
31 // Critical .mjs modules - pre-cache so the network-first path can't fall
32 // through to the 503 "offline" Response on transient network failures
33 // during SW activation. Without these, a failed network fetch + empty
34 // cache returns a non-JS Response that fails module import with the
35 // "error loading dynamically imported module" error.
36 "/wasm-host.mjs",
37 "/bridge-common.mjs",
38 "/$runtime/dom.mjs",
39 "/$runtime/localstorage.mjs",
40 "/$runtime/ws.mjs",
41 "/$runtime/markdown.mjs",
42 "/$runtime/builtin.mjs",
43 "/$runtime/aead.mjs",
44 "/$runtime/idb.mjs",
45 "/$runtime/poly1305.mjs",
46 "/$runtime/sw.mjs",
47 }
48 }
49
50 func main() {
51 initSwGlobals()
52
53 idb.Open(func() {})
54 sw.OnInstall(onInstall)
55 sw.OnActivate(onActivate)
56 sw.OnFetch(onFetch)
57 sw.OnMessage(onMessage)
58 connectSSE()
59 }
60
61 // --- Lifecycle ---
62
63 func onInstall(event sw.Event) {
64 sw.WaitUntil(event, func(done func()) {
65 sw.CacheOpen(cacheName, func(cache sw.Cache) {
66 sw.CacheFromManifests(cache, staticFiles, func() {
67 sw.SkipWaiting()
68 done()
69 })
70 })
71 })
72 }
73
74 func onActivate(event sw.Event) {
75 sw.WaitUntil(event, func(done func()) {
76 // Delete both old cache name and current cache to evict any stale WASM
77 // binaries cached from previous sessions (e.g. a broken relay-proxy.wasm).
78 // onInstall will repopulate only the files in staticFiles.
79 sw.CacheDelete("sm3sh", func() {
80 sw.CacheDelete(cacheName, func() {
81 sw.CacheOpen(cacheName, func(cache sw.Cache) {
82 sw.CacheFromManifests(cache, staticFiles, func() {
83 sw.ClaimClients(func() {
84 done()
85 })
86 })
87 })
88 })
89 })
90 })
91 }
92
93 func onFetch(event sw.Event) {
94 url := sw.GetRequestURL(event)
95 origin := sw.Origin()
96 if len(url) < len(origin) || url[:len(origin)] != origin {
97 return
98 }
99 path := sw.GetRequestPath(event)
100 if path == "/__sse" || path == "/__version" {
101 return
102 }
103 if isNavigationPath(path) {
104 return
105 }
106 if isJSPath(path) {
107 sw.RespondWithNetworkFirst(event)
108 } else {
109 sw.RespondWithCacheFirst(event)
110 }
111 }
112
113 func isNavigationPath(path string) (ok bool) {
114 if path == "/" || path == "/index.html" {
115 return true
116 }
117 if len(path) > 2 && path[:2] == "/p" {
118 return true
119 }
120 if len(path) > 2 && path[:2] == "/t" {
121 return true
122 }
123 if len(path) > 3 && path[:4] == "/msg" {
124 return true
125 }
126 dot := false
127 for i := len(path) - 1; i >= 0 && path[i] != '/'; i-- {
128 if path[i] == '.' {
129 dot = true
130 break
131 }
132 }
133 return !dot
134 }
135
136 func isJSPath(path string) (ok bool) {
137 n := len(path)
138 if n > 4 && path[n-4:] == ".mjs" {
139 return true
140 }
141 if n > 3 && path[n-3:] == ".js" {
142 return true
143 }
144 return false
145 }
146
147 // --- Message dispatch ---
148
149 func onMessage(event sw.Event) {
150 data := sw.GetMessageData(event)
151 clientID := sw.GetMessageClientID(event)
152
153 switch data {
154 case "activate-update":
155 sw.CacheOpen(cacheName, func(cache sw.Cache) {
156 sw.CacheFromManifests(cache, staticFiles, func() {
157 sw.MatchClients(func(client sw.Client) {
158 sw.PostMessage(client, "reload")
159 })
160 })
161 })
162 return
163 case "CLAIM":
164 sw.ClaimClients(func() {})
165 return
166 }
167
168 w := mw.New(data)
169 msgType := w.Str()
170
171 switch msgType {
172 case "SKIP_WAITING":
173 sw.SkipWaiting()
174 case "DIAG":
175 sendToClient(clientID, "[\"DIAG\",\"\"]")
176 default:
177 routeMessage(clientID, &w, msgType)
178 }
179 }
180
181 func routeMessage(clientID string, w *mw.MW, msgType string) {
182 sw.Log("UNROUTED MSG type=" | msgType)
183 }
184
185 // --- Client messaging ---
186
187 func sendToClient(clientID, msg string) {
188 sw.GetClientByID(clientID, func(c sw.Client, ok bool) {
189 if ok {
190 sw.PostMessageJSON(c, msg)
191 } else {
192 evictDeadClient(clientID)
193 }
194 })
195 }
196
197 func evictDeadClient(_ string) {}
198
199 // --- SSE version check ---
200
201 // swVersion remembers the last version the server reported. It is a sovereign
202 // self-mutating type because the SSE callback runs long after connectSSE
203 // returns: the value cannot live in that frame, and package globals are
204 // immutable outside init().
205 type swVersion struct {
206 v string
207 }
208
209 // note records v and reports whether it is a change from a previously seen,
210 // non-empty version. The first value only seeds the state.
211 func (s *swVersion) note(v string) (changed bool) {
212 if v == "" {
213 return false
214 }
215 first := s.v == ""
216 changed = !first && v != s.v
217 s.v = v
218 return changed
219 }
220
221 var currentVersion swVersion
222
223 func connectSSE() {
224 sw.SSEConnect("/__version", func(data string) {
225 v := helpers.JsonGetString(data, "v")
226 if v == "" {
227 v = data
228 }
229 if !currentVersion.note(v) {
230 return
231 }
232 sw.CacheDelete("sm3sh", func() {
233 sw.CacheDelete(cacheName, func() {
234 sw.CacheOpen(cacheName, func(cache sw.Cache) {
235 sw.CacheFromManifests(cache, staticFiles, func() {
236 sw.MatchClients(func(c sw.Client) {
237 sw.PostMessage(c, "reload")
238 })
239 })
240 })
241 })
242 })
243 })
244 }
245
246 // --- JSON helpers ---
247
248 func jstr(s string) (sv string) { return helpers.JsonString(s) }
249
250