main.mx raw
1 package main
2
3 import (
4 "smesh.lol/web/common/helpers"
5 "smesh.lol/web/common/jsbridge/idb"
6 "smesh.lol/web/common/jsbridge/sw"
7 )
8
9 // Unified service worker: lifecycle, caching, subscription routing, relay proxy.
10 // Single binary. No BroadcastChannel. No MV3 lifecycle damage.
11
12 const cacheName = "smesh"
13
14 // Static assets not generated by moxiejs. Generated files are
15 // discovered at install time via $manifest.json.
16 var staticFiles = []string{
17 "/",
18 "/index.html",
19 "/style.css",
20 "/smesh-logo.svg",
21 "/sw-register.js",
22 }
23
24 var currentVersion string
25
26 func main() {
27 initRouter()
28 initRelayProxy()
29 initSharedState()
30 idb.Open(func() {})
31 sw.OnInstall(onInstall)
32 sw.OnActivate(onActivate)
33 sw.OnFetch(onFetch)
34 sw.OnMessage(onMessage)
35 connectSSE()
36 }
37
38 func onInstall(event sw.Event) {
39 sw.WaitUntil(event, func(done func()) {
40 sw.CacheOpen(cacheName, func(cache sw.Cache) {
41 sw.CacheFromManifests(cache, staticFiles, func() {
42 sw.SkipWaiting()
43 done()
44 })
45 })
46 })
47 }
48
49 func onActivate(event sw.Event) {
50 sw.WaitUntil(event, func(done func()) {
51 sw.CacheDelete("sm3sh", func() {
52 sw.ClaimClients(func() {
53 done()
54 })
55 })
56 })
57 }
58
59 func onFetch(event sw.Event) {
60 url := sw.GetRequestURL(event)
61 origin := sw.Origin()
62 if len(url) < len(origin) || url[:len(origin)] != origin {
63 return
64 }
65 path := sw.GetRequestPath(event)
66 if path == "/__sse" || path == "/__version" {
67 return
68 }
69 // HTML navigation requests (root, SPA routes) always go to network
70 // so browser reload works. Static assets (JS, CSS, SVG, images) use cache-first.
71 if isNavigationPath(path) {
72 return // fall through to network
73 }
74 sw.RespondWithCacheFirst(event)
75 }
76
77 func isNavigationPath(path string) bool {
78 if path == "/" || path == "/index.html" {
79 return true
80 }
81 // SPA routes: /p/, /t/, /msg, /msg/
82 if len(path) > 2 && path[:2] == "/p" {
83 return true
84 }
85 if len(path) > 2 && path[:2] == "/t" {
86 return true
87 }
88 if len(path) > 3 && path[:4] == "/msg" {
89 return true
90 }
91 // Anything without a file extension is likely a route, not an asset.
92 dot := false
93 for i := len(path) - 1; i >= 0 && path[i] != '/'; i-- {
94 if path[i] == '.' {
95 dot = true
96 break
97 }
98 }
99 return !dot
100 }
101
102 func onMessage(event sw.Event) {
103 data := sw.GetMessageData(event)
104 clientID := sw.GetMessageClientID(event)
105
106 if data == "activate-update" {
107 refreshAndReload()
108 return
109 }
110 if data == "CLAIM" {
111 sw.ClaimClients(func() {})
112 return
113 }
114
115 w := newMW(data)
116 msgType := w.str()
117
118 switch msgType {
119 case "SKIP_WAITING":
120 sw.SkipWaiting()
121 case "DIAG":
122 diagInfo := "subs=" + helpers.Itoa(int64(len(clientSubs))) + " proxy=" + helpers.Itoa(int64(len(proxySubs)))
123 sendToClient(clientID, "[\"DIAG\",\""+diagInfo+"\"]")
124 default:
125 routeMessage(clientID, &w, msgType)
126 }
127 }
128
129 func connectSSE() {
130 sw.SSEConnect("/__version", func(data string) {
131 v := jsonFieldRaw(data, "v")
132 if v == "" {
133 v = data
134 }
135 if v == "" {
136 return
137 }
138 if currentVersion == "" {
139 currentVersion = v
140 return
141 }
142 if v != currentVersion {
143 currentVersion = v
144 notifyUpdate()
145 }
146 })
147 }
148
149 func notifyUpdate() {
150 sw.MatchClients(func(client sw.Client) {
151 sw.PostMessage(client, "update-available")
152 })
153 }
154
155 func refreshAndReload() {
156 sw.CacheOpen(cacheName, func(cache sw.Cache) {
157 sw.CacheFromManifests(cache, staticFiles, func() {
158 sw.MatchClients(func(client sw.Client) {
159 sw.Navigate(client, "")
160 })
161 })
162 })
163 }
164