main.go raw
1 package main
2
3 import (
4 "common/jsbridge/sw"
5 )
6
7 // App Shell domain — Service Worker lifecycle, static asset caching, SSE version monitoring.
8 // Thin outer shell: delegates all app-level messages to the Subscription Router.
9
10 const cacheName = "smesh"
11
12 var appFiles = []string{
13 // Main app — absolute paths so cache.addAll resolves correctly from SW location.
14 // Note: /index.html omitted — Go FileServer 301-redirects it to /, which
15 // causes cache.addAll to fail (Cache API rejects redirect responses).
16 "/",
17 "/$entry.mjs",
18 "/smesh3.mjs",
19 "/common_crypto_secp256k1.mjs",
20 "/common_crypto_sha256.mjs",
21 "/common_helpers.mjs",
22 "/common_jsbridge_dom.mjs",
23 "/common_jsbridge_localstorage.mjs",
24 "/common_jsbridge_signer.mjs",
25 "/common_nostr.mjs",
26 "/$runtime/index.mjs",
27 "/$runtime/runtime.mjs",
28 "/$runtime/goroutine.mjs",
29 "/$runtime/channel.mjs",
30 "/$runtime/builtin.mjs",
31 "/$runtime/types.mjs",
32 "/$runtime/sync.mjs",
33 "/$runtime/dom.mjs",
34 "/$runtime/localstorage.mjs",
35 "/$runtime/signer.mjs",
36 "/$wasm/secp256k1.mjs",
37 "/$wasm/secp256k1.wasm",
38 "/smesh-loader.svg",
39 // Service worker (shell SW).
40 "/$sw/$entry.mjs",
41 "/$sw/sw.mjs",
42 "/$sw/common_jsbridge_sw.mjs",
43 "/$sw/common_jsbridge_bc.mjs",
44 "/$sw/common_jsbridge_subtle.mjs",
45 "/$sw/common_crypto_secp256k1.mjs",
46 "/$sw/common_crypto_sha256.mjs",
47 "/$sw/common_helpers.mjs",
48 "/$sw/$runtime/index.mjs",
49 "/$sw/$runtime/runtime.mjs",
50 "/$sw/$runtime/goroutine.mjs",
51 "/$sw/$runtime/channel.mjs",
52 "/$sw/$runtime/builtin.mjs",
53 "/$sw/$runtime/types.mjs",
54 "/$sw/$runtime/sync.mjs",
55 "/$sw/$runtime/sw.mjs",
56 "/$sw/$runtime/subtle.mjs",
57 "/$sw/$runtime/crypto.mjs",
58 "/$sw/$runtime/ws.mjs",
59 "/$sw/$runtime/bc.mjs",
60 }
61
62 var currentVersion string
63 var refreshing bool
64
65 // File categories for smart reload.
66 const (
67 catAppCode = 0 // full page reload
68 catSWCode = 1 // SW re-register via FORCE_UPDATE_SW
69 catStatic = 2 // silent cache update only
70 )
71
72 func main() {
73 initSharedState()
74 sw.OnInstall(onInstall)
75 sw.OnActivate(onActivate)
76 sw.OnFetch(onFetch)
77 sw.OnMessage(onMessage)
78 // Connect bus+SSE from main() so they survive SW thread eviction.
79 // onActivate only fires once per lifecycle; the browser can evict
80 // and restart the thread at any time, losing all in-memory state.
81 connectSSE()
82 connectBus()
83 }
84
85 func onInstall(event sw.Event) {
86 sw.WaitUntil(event, func(done func()) {
87 sw.SkipWaiting()
88 done()
89 })
90 }
91
92 func onActivate(event sw.Event) {
93 sw.WaitUntil(event, func(done func()) {
94 // Delete old caches left from previous cache names.
95 // caches.match() searches ALL caches — stale entries poison fetches.
96 sw.CacheDelete("sm3sh", func() {
97 sw.ClaimClients(func() {
98 done()
99 })
100 })
101 })
102 }
103
104 func onFetch(event sw.Event) {
105 url := sw.GetRequestURL(event)
106 origin := sw.Origin()
107 // Only intercept same-origin requests.
108 if len(url) < len(origin) || url[:len(origin)] != origin {
109 return
110 }
111 path := sw.GetRequestPath(event)
112 if path == "/__sse" || path == "/__version" {
113 return
114 }
115 // SW module files, satellite SW dirs, and fonts: pass through to network.
116 if (len(path) > 4 && path[:4] == "/$sw") || (len(path) > 6 && path[:7] == "/fonts/") {
117 return
118 }
119 sw.RespondWithCacheFirst(event)
120 }
121
122 func onMessage(event sw.Event) {
123 data := sw.GetMessageData(event)
124 clientID := sw.GetMessageClientID(event)
125
126 // Simple string messages — App Shell handles directly.
127 if data == "activate-update" {
128 fullRefresh()
129 return
130 }
131 if data == "CLAIM" {
132 // Re-claim after hard refresh (onActivate doesn't re-fire).
133 sw.ClaimClients(func() {})
134 return
135 }
136
137 // JSON array messages — parse and route.
138 w := newMW(data)
139 msgType := w.str()
140
141 switch msgType {
142 case "SKIP_WAITING":
143 sw.SkipWaiting()
144 default:
145 routeMessage(clientID, &w, msgType)
146 }
147 }
148
149 func connectSSE() {
150 sw.SSEConnect("/__sse", func(data string) {
151 v := jsonFieldRaw(data, "v")
152 if v == "" {
153 // Old-format SSE (plain number) — treat entire data as version.
154 v = data
155 }
156 if v == "" {
157 return
158 }
159 if currentVersion == "" {
160 currentVersion = v
161 // First connect: populate cache silently — page is already loading.
162 populateCache()
163 return
164 }
165 if v != currentVersion && !refreshing {
166 currentVersion = v
167 refreshing = true
168 raw := jsonFieldRaw(data, "files")
169 if raw == "" {
170 fullRefresh()
171 } else {
172 w := mw{s: raw, i: 0}
173 files := w.strs()
174 if len(files) == 0 {
175 fullRefresh()
176 } else {
177 smartRefresh(files)
178 }
179 }
180 }
181 })
182 }
183
184 func categorize(path string) int {
185 if len(path) > 4 && path[:4] == "/$sw" {
186 return catSWCode
187 }
188 if hasSuffix(path, ".svg") || hasSuffix(path, ".css") ||
189 hasSuffix(path, ".woff2") || hasSuffix(path, ".html") {
190 return catStatic
191 }
192 return catAppCode
193 }
194
195 func hasSuffix(s, suffix string) bool {
196 return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
197 }
198
199 func smartRefresh(files []string) {
200 needReload := false
201 needSWUpdate := false
202 var cacheFiles []string
203
204 for _, f := range files {
205 switch categorize(f) {
206 case catAppCode:
207 needReload = true
208 cacheFiles = append(cacheFiles, f)
209 case catSWCode:
210 needSWUpdate = true
211 // SW files pass through to network, no cache entry.
212 case catStatic:
213 cacheFiles = append(cacheFiles, f)
214 }
215 }
216
217 after := func() {
218 if needReload {
219 doNavigate()
220 } else if needSWUpdate {
221 broadcastToClients(`["FORCE_UPDATE_SW","$sw-relay"]`)
222 }
223 }
224
225 if len(cacheFiles) > 0 {
226 refreshChanged(cacheFiles, after)
227 } else {
228 after()
229 }
230 }
231
232 func refreshChanged(files []string, done func()) {
233 once := false
234 finish := func() {
235 if once {
236 return
237 }
238 once = true
239 refreshing = false
240 done()
241 }
242 sw.SetTimeout(5000, finish)
243
244 sw.CacheOpen(cacheName, func(cache sw.Cache) {
245 urls := make([]string, len(files))
246 for i, f := range files {
247 urls[i] = f + "?v=" + currentVersion
248 }
249 pending := 0
250 fetchesDone := false
251 sw.FetchAll(urls, func(idx int, resp sw.Response, ok bool) {
252 if ok && sw.ResponseOK(resp) {
253 pending++
254 sw.CachePut(cache, files[idx], resp, func() {
255 pending--
256 if fetchesDone && pending == 0 {
257 finish()
258 }
259 })
260 }
261 }, func() {
262 fetchesDone = true
263 if pending == 0 {
264 finish()
265 }
266 })
267 })
268 }
269
270 func populateCache() {
271 cacheAll(func() {})
272 }
273
274 func fullRefresh() {
275 cacheAll(doNavigate)
276 }
277
278 func cacheAll(done func()) {
279 once := false
280 finish := func() {
281 if once {
282 return
283 }
284 once = true
285 refreshing = false
286 done()
287 }
288 sw.SetTimeout(8000, finish)
289
290 sw.CacheOpen(cacheName, func(cache sw.Cache) {
291 urls := make([]string, len(appFiles))
292 for i, f := range appFiles {
293 urls[i] = f + "?v=" + currentVersion
294 }
295 pending := 0
296 fetchesDone := false
297 sw.FetchAll(urls, func(idx int, resp sw.Response, ok bool) {
298 if ok && sw.ResponseOK(resp) {
299 pending++
300 sw.CachePut(cache, appFiles[idx], resp, func() {
301 pending--
302 if fetchesDone && pending == 0 {
303 finish()
304 }
305 })
306 }
307 }, func() {
308 fetchesDone = true
309 if pending == 0 {
310 finish()
311 }
312 })
313 })
314 }
315
316 func doNavigate() {
317 sw.MatchClients(func(client sw.Client) {
318 sw.Navigate(client, "")
319 })
320 }
321