package main import ( "common/jsbridge/sw" ) // App Shell domain — Service Worker lifecycle, static asset caching, SSE version monitoring. // Thin outer shell: delegates all app-level messages to the Subscription Router. const cacheName = "smesh" var appFiles = []string{ // Main app — absolute paths so cache.addAll resolves correctly from SW location. // Note: /index.html omitted — Go FileServer 301-redirects it to /, which // causes cache.addAll to fail (Cache API rejects redirect responses). "/", "/$entry.mjs", "/smesh3.mjs", "/common_crypto_secp256k1.mjs", "/common_crypto_sha256.mjs", "/common_helpers.mjs", "/common_jsbridge_dom.mjs", "/common_jsbridge_localstorage.mjs", "/common_jsbridge_signer.mjs", "/common_nostr.mjs", "/$runtime/index.mjs", "/$runtime/runtime.mjs", "/$runtime/goroutine.mjs", "/$runtime/channel.mjs", "/$runtime/builtin.mjs", "/$runtime/types.mjs", "/$runtime/sync.mjs", "/$runtime/dom.mjs", "/$runtime/localstorage.mjs", "/$runtime/signer.mjs", "/$wasm/secp256k1.mjs", "/$wasm/secp256k1.wasm", "/smesh-loader.svg", // Service worker (shell SW). "/$sw/$entry.mjs", "/$sw/sw.mjs", "/$sw/common_jsbridge_sw.mjs", "/$sw/common_jsbridge_bc.mjs", "/$sw/common_jsbridge_subtle.mjs", "/$sw/common_crypto_secp256k1.mjs", "/$sw/common_crypto_sha256.mjs", "/$sw/common_helpers.mjs", "/$sw/$runtime/index.mjs", "/$sw/$runtime/runtime.mjs", "/$sw/$runtime/goroutine.mjs", "/$sw/$runtime/channel.mjs", "/$sw/$runtime/builtin.mjs", "/$sw/$runtime/types.mjs", "/$sw/$runtime/sync.mjs", "/$sw/$runtime/sw.mjs", "/$sw/$runtime/subtle.mjs", "/$sw/$runtime/crypto.mjs", "/$sw/$runtime/ws.mjs", "/$sw/$runtime/bc.mjs", } var currentVersion string var refreshing bool // File categories for smart reload. const ( catAppCode = 0 // full page reload catSWCode = 1 // SW re-register via FORCE_UPDATE_SW catStatic = 2 // silent cache update only ) func main() { initSharedState() sw.OnInstall(onInstall) sw.OnActivate(onActivate) sw.OnFetch(onFetch) sw.OnMessage(onMessage) // Connect bus+SSE from main() so they survive SW thread eviction. // onActivate only fires once per lifecycle; the browser can evict // and restart the thread at any time, losing all in-memory state. connectSSE() connectBus() } func onInstall(event sw.Event) { sw.WaitUntil(event, func(done func()) { sw.SkipWaiting() done() }) } func onActivate(event sw.Event) { sw.WaitUntil(event, func(done func()) { // Delete old caches left from previous cache names. // caches.match() searches ALL caches — stale entries poison fetches. sw.CacheDelete("sm3sh", func() { sw.ClaimClients(func() { done() }) }) }) } func onFetch(event sw.Event) { url := sw.GetRequestURL(event) origin := sw.Origin() // Only intercept same-origin requests. if len(url) < len(origin) || url[:len(origin)] != origin { return } path := sw.GetRequestPath(event) if path == "/__sse" || path == "/__version" { return } // SW module files, satellite SW dirs, and fonts: pass through to network. if (len(path) > 4 && path[:4] == "/$sw") || (len(path) > 6 && path[:7] == "/fonts/") { return } sw.RespondWithCacheFirst(event) } func onMessage(event sw.Event) { data := sw.GetMessageData(event) clientID := sw.GetMessageClientID(event) // Simple string messages — App Shell handles directly. if data == "activate-update" { fullRefresh() return } if data == "CLAIM" { // Re-claim after hard refresh (onActivate doesn't re-fire). sw.ClaimClients(func() {}) return } // JSON array messages — parse and route. w := newMW(data) msgType := w.str() switch msgType { case "SKIP_WAITING": sw.SkipWaiting() default: routeMessage(clientID, &w, msgType) } } func connectSSE() { sw.SSEConnect("/__sse", func(data string) { v := jsonFieldRaw(data, "v") if v == "" { // Old-format SSE (plain number) — treat entire data as version. v = data } if v == "" { return } if currentVersion == "" { currentVersion = v // First connect: populate cache silently — page is already loading. populateCache() return } if v != currentVersion && !refreshing { currentVersion = v refreshing = true raw := jsonFieldRaw(data, "files") if raw == "" { fullRefresh() } else { w := mw{s: raw, i: 0} files := w.strs() if len(files) == 0 { fullRefresh() } else { smartRefresh(files) } } } }) } func categorize(path string) int { if len(path) > 4 && path[:4] == "/$sw" { return catSWCode } if hasSuffix(path, ".svg") || hasSuffix(path, ".css") || hasSuffix(path, ".woff2") || hasSuffix(path, ".html") { return catStatic } return catAppCode } func hasSuffix(s, suffix string) bool { return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix } func smartRefresh(files []string) { needReload := false needSWUpdate := false var cacheFiles []string for _, f := range files { switch categorize(f) { case catAppCode: needReload = true cacheFiles = append(cacheFiles, f) case catSWCode: needSWUpdate = true // SW files pass through to network, no cache entry. case catStatic: cacheFiles = append(cacheFiles, f) } } after := func() { if needReload { doNavigate() } else if needSWUpdate { broadcastToClients(`["FORCE_UPDATE_SW","$sw-relay"]`) } } if len(cacheFiles) > 0 { refreshChanged(cacheFiles, after) } else { after() } } func refreshChanged(files []string, done func()) { once := false finish := func() { if once { return } once = true refreshing = false done() } sw.SetTimeout(5000, finish) sw.CacheOpen(cacheName, func(cache sw.Cache) { urls := make([]string, len(files)) for i, f := range files { urls[i] = f + "?v=" + currentVersion } pending := 0 fetchesDone := false sw.FetchAll(urls, func(idx int, resp sw.Response, ok bool) { if ok && sw.ResponseOK(resp) { pending++ sw.CachePut(cache, files[idx], resp, func() { pending-- if fetchesDone && pending == 0 { finish() } }) } }, func() { fetchesDone = true if pending == 0 { finish() } }) }) } func populateCache() { cacheAll(func() {}) } func fullRefresh() { cacheAll(doNavigate) } func cacheAll(done func()) { once := false finish := func() { if once { return } once = true refreshing = false done() } sw.SetTimeout(8000, finish) sw.CacheOpen(cacheName, func(cache sw.Cache) { urls := make([]string, len(appFiles)) for i, f := range appFiles { urls[i] = f + "?v=" + currentVersion } pending := 0 fetchesDone := false sw.FetchAll(urls, func(idx int, resp sw.Response, ok bool) { if ok && sw.ResponseOK(resp) { pending++ sw.CachePut(cache, appFiles[idx], resp, func() { pending-- if fetchesDone && pending == 0 { finish() } }) } }, func() { fetchesDone = true if pending == 0 { finish() } }) }) } func doNavigate() { sw.MatchClients(func(client sw.Client) { sw.Navigate(client, "") }) }