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