main.mx raw

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