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