package main import ( "smesh.lol/web/common/helpers" "smesh.lol/web/common/jsbridge/idb" "smesh.lol/web/common/jsbridge/sw" ) // Unified service worker: lifecycle, caching, subscription routing, relay proxy. // Single binary. No BroadcastChannel. No MV3 lifecycle damage. const cacheName = "smesh" // Static assets not generated by moxiejs. Generated files are // discovered at install time via $manifest.json. var staticFiles = []string{ "/", "/index.html", "/style.css", "/smesh-logo.svg", "/sw-register.js", } var currentVersion string func main() { initRouter() initRelayProxy() initSharedState() idb.Open(func() {}) sw.OnInstall(onInstall) sw.OnActivate(onActivate) sw.OnFetch(onFetch) sw.OnMessage(onMessage) connectSSE() } func onInstall(event sw.Event) { sw.WaitUntil(event, func(done func()) { sw.CacheOpen(cacheName, func(cache sw.Cache) { sw.CacheFromManifests(cache, staticFiles, func() { sw.SkipWaiting() done() }) }) }) } func onActivate(event sw.Event) { sw.WaitUntil(event, func(done func()) { sw.CacheDelete("sm3sh", func() { sw.ClaimClients(func() { done() }) }) }) } func onFetch(event sw.Event) { url := sw.GetRequestURL(event) origin := sw.Origin() if len(url) < len(origin) || url[:len(origin)] != origin { return } path := sw.GetRequestPath(event) if path == "/__sse" || path == "/__version" { return } // HTML navigation requests (root, SPA routes) always go to network // so browser reload works. Static assets (JS, CSS, SVG, images) use cache-first. if isNavigationPath(path) { return // fall through to network } sw.RespondWithCacheFirst(event) } func isNavigationPath(path string) bool { if path == "/" || path == "/index.html" { return true } // SPA routes: /p/, /t/, /msg, /msg/ if len(path) > 2 && path[:2] == "/p" { return true } if len(path) > 2 && path[:2] == "/t" { return true } if len(path) > 3 && path[:4] == "/msg" { return true } // Anything without a file extension is likely a route, not an asset. dot := false for i := len(path) - 1; i >= 0 && path[i] != '/'; i-- { if path[i] == '.' { dot = true break } } return !dot } func onMessage(event sw.Event) { data := sw.GetMessageData(event) clientID := sw.GetMessageClientID(event) if data == "activate-update" { refreshAndReload() return } if data == "CLAIM" { sw.ClaimClients(func() {}) return } w := newMW(data) msgType := w.str() switch msgType { case "SKIP_WAITING": sw.SkipWaiting() case "DIAG": diagInfo := "subs=" + helpers.Itoa(int64(len(clientSubs))) + " proxy=" + helpers.Itoa(int64(len(proxySubs))) sendToClient(clientID, "[\"DIAG\",\""+diagInfo+"\"]") default: routeMessage(clientID, &w, msgType) } } func connectSSE() { sw.SSEConnect("/__version", func(data string) { v := jsonFieldRaw(data, "v") if v == "" { v = data } if v == "" { return } if currentVersion == "" { currentVersion = v return } if v != currentVersion { currentVersion = v notifyUpdate() } }) } func notifyUpdate() { sw.MatchClients(func(client sw.Client) { sw.PostMessage(client, "update-available") }) } func refreshAndReload() { sw.CacheOpen(cacheName, func(cache sw.Cache) { sw.CacheFromManifests(cache, staticFiles, func() { sw.MatchClients(func(client sw.Client) { sw.Navigate(client, "") }) }) }) }