refactor(relay-server): enable dynamic portal frontend configuration and remove static bootstrap injection

- Update Dockerfile to set configurable environment variables for portal UI URL, frontend pattern, and bootstrap URIs instead of hardcoded domain - Simplify Makefile by removing BOOTSTRAPS variable and static injection logic from WASM build process - Add dynamic manifest.json generation and unified /frontend/ handlers in relay server for flexible frontend serving - Remove port-setting script from HTML to support wildcard domain patterns This refactor improves configurability and removes hardcoded values, allowing the portal to adapt to different environments without rebuilds.

lemon-mint committed Nov 7, 2025 at 01:38 UTC 6c8daebde2709faec2595fe42f870808a84d001a
8 files changed +376 -196
Dockerfile
+3 -1
@@ -30,7 +30,9 @@ COPY --from=builder /src/dist /app/dist
30
31 # Set default environment variables
32 ENV STATIC_DIR=/app/dist
33 -ENV PORTAL_DOMAIN=localhost
33 +ENV PORTAL_UI_URL=http://localhost:4017
34 +ENV POSTAL_FRONTEND_URL=http://*.localhost:4017
35 +ENV BOOTSTRAP_URIS=ws://localhost:4017/relay,wss://some.app:21762/relay
36
37 # Expose ports
38 # 4017: relay server and portal frontend
Makefile
+2 -11
@@ -17,20 +17,12 @@ build-protoc:
17 portal/core/proto/rdsec/rdsec.proto \
18 portal/core/proto/rdverb/rdverb.proto
19
20 -BOOTSTRAPS ?= ""
21 -
20 # Build WASM artifacts with wasm-opt optimization and generate manifest
21 build-wasm:
22 @echo "[wasm] building webclient WASM..."
23 @mkdir -p dist
24
27 - # Prepare optional link flags for bootstrap injection
28 - @WASM_LDFLAGS=""; \
29 - if [ -n "$(BOOTSTRAPS)" ]; then \
30 - WASM_LDFLAGS="-X main.bootstrapServersCSV=$(BOOTSTRAPS)"; \
31 - echo "[wasm] injecting bootstraps: $(BOOTSTRAPS)"; \
32 - fi; \
33 - GOOS=js GOARCH=wasm go build -trimpath -ldflags "-s -w $$WASM_LDFLAGS" -o dist/portal.wasm ./cmd/webclient
25 + GOOS=js GOARCH=wasm go build -trimpath -ldflags "-s -w" -o dist/portal.wasm ./cmd/webclient
26
27 @echo "[wasm] optimizing with wasm-opt..."
28 @if command -v wasm-opt >/dev/null 2>&1; then \
@@ -48,8 +40,7 @@ build-wasm:
40 echo "[wasm] cleaning old hash files..."; \
41 find dist -name '[0-9a-f]*.wasm' ! -name "$$WASM_HASH.wasm" -type f -delete 2>/dev/null || true; \
42 cp dist/portal.wasm dist/$$WASM_HASH.wasm; \
51 - echo "{\"wasmFile\":\"$$WASM_HASH.wasm\",\"hash\":\"$$WASM_HASH\"}" > dist/manifest.json; \
52 - echo "[wasm] manifest created"
43 + echo "[wasm] content-addressed WASM: $$WASM_HASH.wasm"
44
45 @echo "[wasm] copying additional resources..."
46 @cp cmd/webclient/wasm_exec.js dist/wasm_exec.js
cmd/relay-server/frontend.go
+203 -17
@@ -5,6 +5,8 @@ import (
5 "compress/gzip"
6 "crypto/sha256"
7 "encoding/hex"
8 + "encoding/json"
9 + "fmt"
10 "io"
11 "net/http"
12 "os"
@@ -20,8 +22,17 @@ import (
22 // staticDir is the directory where static files are located
23 var staticDir = "./dist"
24
23 -// portalDomain is the domain for portal frontend.
24 -var portalDomain = "localhost"
25 +// portalHost is the host for portal frontend.
26 +var portalHost = "localhost"
27 +
28 +// portalUIURL is the base URL for portal frontend
29 +var portalUIURL = "http://localhost:4017"
30 +
31 +// portalFrontendPattern is the wildcard pattern for portal frontend URLs (e.g., *.localhost:4017)
32 +var portalFrontendPattern = ""
33 +
34 +// bootstrapURIs stores the relay bootstrap server URIs
35 +var bootstrapURIs = "ws://localhost:4017/relay"
36
37 // wasmCache stores pre-compressed WASM files in memory
38 type wasmCacheEntry struct {
@@ -145,6 +156,19 @@ func createPortalMux() *http.ServeMux {
156 servePortalStaticFile(w, r, path)
157 })
158
159 + // Static file handler for /frontend/ (for unified caching)
160 + mux.HandleFunc("/frontend/", func(w http.ResponseWriter, r *http.Request) {
161 + path := strings.TrimPrefix(r.URL.Path, "/frontend/")
162 +
163 + // Special handling for manifest.json - generate dynamically
164 + if path == "manifest.json" {
165 + serveDynamicManifest(w, r)
166 + return
167 + }
168 +
169 + servePortalStaticFile(w, r, path)
170 + })
171 +
172 // Root handler for portal frontend
173 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
174 if r.URL.Path == "/" {
@@ -287,9 +311,7 @@ func servePortalStatic(w http.ResponseWriter, r *http.Request) {
311 return
312
313 case "service-worker.js":
290 - w.Header().Set("Cache-Control", "no-cache, must-revalidate")
291 - w.Header().Set("Content-Type", "application/javascript")
292 - serveStaticFileWithFallback(w, r, path, "application/javascript")
314 + serveDynamicServiceWorker(w, r)
315 return
316
317 case "wasm_exec.js":
@@ -417,22 +439,30 @@ func getContentType(ext string) string {
439 }
440 }
441
420 -// isPortalSubdomain checks if the host is a portal subdomain (*.{portalDomain})
442 +// isPortalSubdomain checks if the host matches the portal frontend pattern
443 func isPortalSubdomain(host string) bool {
422 - if portalDomain == "" {
423 - return false
444 + // If we have a frontend pattern, use it
445 + if portalFrontendPattern != "" {
446 + return matchesWildcardPattern(host, portalFrontendPattern)
447 }
425 - // Trim port from request host
426 - if i := strings.Index(host, ":"); i >= 0 {
427 - host = host[:i]
448 +
449 + // Fallback to checking if it ends with .{portalHost}
450 + if portalHost == "" {
451 + return false
452 }
429 - // Allow optional ":port" in portalDomain for local dev
430 - base := portalDomain
431 - if j := strings.Index(base, ":"); j >= 0 {
432 - base = base[:j]
453 + return strings.HasSuffix(host, "."+portalHost)
454 +}
455 +
456 +// matchesWildcardPattern checks if a host matches a wildcard pattern (e.g., *.localhost:4017)
457 +func matchesWildcardPattern(host, pattern string) bool {
458 + // Handle wildcard pattern (e.g., *.localhost:4017)
459 + if strings.HasPrefix(pattern, "*.") {
460 + suffix := strings.TrimPrefix(pattern, "*")
461 + return strings.HasSuffix(host, suffix)
462 }
434 - // Check if it ends with .{base}
435 - return strings.HasSuffix(host, "."+base)
463 +
464 + // Exact match
465 + return host == pattern
466 }
467
468 // isHexString checks if a string contains only hexadecimal characters
@@ -444,3 +474,159 @@ func isHexString(s string) bool {
474 }
475 return true
476 }
477 +
478 +// serveDynamicManifest generates and serves manifest.json dynamically
479 +func serveDynamicManifest(w http.ResponseWriter, r *http.Request) {
480 + // Find the content-addressed WASM file
481 + wasmCacheMu.RLock()
482 + var wasmHash string
483 + var wasmFile string
484 + for filename, entry := range wasmCache {
485 + wasmHash = entry.hash
486 + wasmFile = filename
487 + break // Use the first (and should be only) WASM file
488 + }
489 + wasmCacheMu.RUnlock()
490 +
491 + // Fallback: scan directory if cache is empty
492 + if wasmHash == "" {
493 + entries, err := os.ReadDir(staticDir)
494 + if err == nil {
495 + for _, entry := range entries {
496 + if entry.IsDir() {
497 + continue
498 + }
499 + name := entry.Name()
500 + // Look for content-addressed WASM files: <64-char-hex>.wasm
501 + if strings.HasSuffix(name, ".wasm") && len(name) == 69 {
502 + hash := strings.TrimSuffix(name, ".wasm")
503 + if isHexString(hash) && len(hash) == 64 {
504 + wasmHash = hash
505 + wasmFile = name
506 + break
507 + }
508 + }
509 + }
510 + }
511 + }
512 +
513 + // Generate WASM URL
514 + wasmURL := portalUIURL + "/frontend/" + wasmFile
515 +
516 + // Create manifest structure
517 + manifest := map[string]string{
518 + "wasmFile": wasmFile,
519 + "wasmUrl": wasmURL,
520 + "hash": wasmHash,
521 + "bootstraps": bootstrapURIs,
522 + }
523 +
524 + // Set headers for no caching
525 + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
526 + w.Header().Set("Pragma", "no-cache")
527 + w.Header().Set("Expires", "0")
528 + w.Header().Set("Content-Type", "application/json")
529 +
530 + // Encode and send
531 + w.WriteHeader(http.StatusOK)
532 + if err := json.NewEncoder(w).Encode(manifest); err != nil {
533 + log.Error().Err(err).Msg("Failed to encode manifest")
534 + }
535 +
536 + log.Debug().
537 + Str("wasmFile", wasmFile).
538 + Str("wasmUrl", wasmURL).
539 + Str("hash", wasmHash).
540 + Str("bootstraps", bootstrapURIs).
541 + Msg("Served dynamic manifest")
542 +}
543 +
544 +// serveDynamicServiceWorker serves service-worker.js with injected manifest and config
545 +func serveDynamicServiceWorker(w http.ResponseWriter, r *http.Request) {
546 + // Read the service-worker.js template
547 + fullPath := filepath.Join(staticDir, "service-worker.js")
548 + content, err := os.ReadFile(fullPath)
549 + if err != nil {
550 + log.Error().Err(err).Msg("Failed to read service-worker.js")
551 + http.NotFound(w, r)
552 + return
553 + }
554 +
555 + // Find the content-addressed WASM file
556 + wasmCacheMu.RLock()
557 + var wasmHash string
558 + var wasmFile string
559 + for filename, entry := range wasmCache {
560 + wasmHash = entry.hash
561 + wasmFile = filename
562 + break
563 + }
564 + wasmCacheMu.RUnlock()
565 +
566 + // Fallback: scan directory if cache is empty
567 + if wasmHash == "" {
568 + entries, err := os.ReadDir(staticDir)
569 + if err == nil {
570 + for _, entry := range entries {
571 + if entry.IsDir() {
572 + continue
573 + }
574 + name := entry.Name()
575 + if strings.HasSuffix(name, ".wasm") && len(name) == 69 {
576 + hash := strings.TrimSuffix(name, ".wasm")
577 + if isHexString(hash) && len(hash) == 64 {
578 + wasmHash = hash
579 + wasmFile = name
580 + break
581 + }
582 + }
583 + }
584 + }
585 + }
586 +
587 + // Generate WASM URL
588 + wasmURL := portalUIURL + "/frontend/" + wasmFile
589 +
590 + // Create manifest object
591 + manifestData := map[string]string{
592 + "wasmFile": wasmFile,
593 + "wasmUrl": wasmURL,
594 + "hash": wasmHash,
595 + "bootstraps": bootstrapURIs,
596 + }
597 +
598 + // Convert manifest to JSON string
599 + manifestJSON, err := json.Marshal(manifestData)
600 + if err != nil {
601 + log.Error().Err(err).Msg("Failed to marshal manifest for service worker")
602 + http.Error(w, "Internal server error", http.StatusInternalServerError)
603 + return
604 + }
605 +
606 + // Replace placeholders
607 + result := string(content)
608 + result = strings.ReplaceAll(result, "<PORTAL_UI_URL>", portalUIURL)
609 + result = strings.ReplaceAll(result, "\"<WASM_MANIFEST>\"", string(manifestJSON))
610 +
611 + // Inject __BOOTSTRAP_SERVERS__ as a global variable in service worker
612 + bootstrapServersLine := fmt.Sprintf("self.__BOOTSTRAP_SERVERS__ = %q;\n", bootstrapURIs)
613 +
614 + // Insert after the wasmManifest line (after line that sets wasmManifest)
615 + manifestLine := "let wasmManifest = JSON.parse(wasmManifestString);"
616 + result = strings.Replace(result, manifestLine, manifestLine+"\n"+bootstrapServersLine, 1)
617 +
618 + // Set headers for no caching
619 + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
620 + w.Header().Set("Pragma", "no-cache")
621 + w.Header().Set("Expires", "0")
622 + w.Header().Set("Content-Type", "application/javascript")
623 +
624 + // Send response
625 + w.WriteHeader(http.StatusOK)
626 + w.Write([]byte(result))
627 +
628 + log.Debug().
629 + Str("portalUIURL", portalUIURL).
630 + Str("wasmHash", wasmHash).
631 + Msg("Served dynamic service-worker.js")
632 +}
cmd/relay-server/main.go
+57 -13
@@ -18,11 +18,11 @@ import (
18 )
19
20 var (
21 - flagBootstraps []string
22 - flagALPN string
23 - flagPort int
24 - flagStaticDir string
25 - flagPortalDomain string
21 + flagBootstraps []string
22 + flagALPN string
23 + flagPort int
24 + flagStaticDir string
25 + flagPortalHost string
26 )
27
28 func main() {
@@ -33,16 +33,29 @@ func main() {
33 if defaultStaticDir == "" {
34 defaultStaticDir = "./dist"
35 }
36 - defaultPortalDomain := os.Getenv("PORTAL_DOMAIN")
37 - if defaultPortalDomain == "" {
38 - defaultPortalDomain = "localhost"
36 + // Parse PORTAL_UI_URL or POSTAL_FRONTEND_URL to extract portal host
37 + defaultPortalHost := os.Getenv("PORTAL_UI_URL")
38 + if defaultPortalHost == "" {
39 + defaultPortalHost = os.Getenv("POSTAL_FRONTEND_URL")
40 + }
41 + if defaultPortalHost != "" {
42 + // Extract host from URL (supports wildcard patterns like http://*.localhost:4017)
43 + defaultPortalHost = strings.TrimPrefix(defaultPortalHost, "http://")
44 + defaultPortalHost = strings.TrimPrefix(defaultPortalHost, "https://")
45 + defaultPortalHost = strings.TrimPrefix(defaultPortalHost, "*.")
46 + } else {
47 + defaultPortalHost = "localhost:4017"
48 + }
49 + defaultBootstraps := os.Getenv("BOOTSTRAP_URIS")
50 + if defaultBootstraps == "" {
51 + defaultBootstraps = "ws://localhost:4017/relay"
52 }
53 var flagBootstrapsCSV string
41 - flag.StringVar(&flagBootstrapsCSV, "bootstraps", "ws://localhost:4017/relay", "bootstrap addresses (comma-separated)")
54 + flag.StringVar(&flagBootstrapsCSV, "bootstraps", defaultBootstraps, "bootstrap addresses (comma-separated)")
55 flag.StringVar(&flagALPN, "alpn", "http/1.1", "ALPN identifier for this service")
56 flag.IntVar(&flagPort, "port", 4017, "admin UI and HTTP proxy port")
57 flag.StringVar(&flagStaticDir, "static-dir", defaultStaticDir, "static files directory for portal frontend (env: STATIC_DIR)")
45 - flag.StringVar(&flagPortalDomain, "portal-domain", defaultPortalDomain, "portal domain for frontend serving (env: PORTAL_DOMAIN)")
58 + flag.StringVar(&flagPortalHost, "portal-host", defaultPortalHost, "portal host for frontend serving (env: PORTAL_HOST)")
59
60 flag.Parse()
61
@@ -65,12 +78,43 @@ func runServer() error {
78 ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
79 defer stop()
80
68 - // Set static directory and portal domain
81 + // Set static directory and portal host
82 staticDir = flagStaticDir
70 - portalDomain = flagPortalDomain
83 + portalHost = flagPortalHost
84 +
85 + // Set portal UI URL from environment or construct from portal host
86 + portalUIURL = os.Getenv("PORTAL_UI_URL")
87 + if portalUIURL == "" {
88 + portalUIURL = os.Getenv("POSTAL_FRONTEND_URL")
89 + }
90 + if portalUIURL == "" {
91 + portalUIURL = "http://" + portalHost
92 + }
93 + // Trim trailing slashes
94 + portalUIURL = strings.TrimSuffix(portalUIURL, "/")
95 +
96 + // Set portal frontend pattern from POSTAL_FRONTEND_URL
97 + postalFrontendURL := os.Getenv("POSTAL_FRONTEND_URL")
98 + if postalFrontendURL != "" {
99 + // Extract host pattern from URL (e.g., http://*.localhost:4017 -> *.localhost:4017)
100 + postalFrontendURL = strings.TrimPrefix(postalFrontendURL, "http://")
101 + postalFrontendURL = strings.TrimPrefix(postalFrontendURL, "https://")
102 + portalFrontendPattern = postalFrontendURL
103 + }
104 +
105 + // Set bootstrap URIs from environment
106 + bootstrapURIs = os.Getenv("BOOTSTRAP_URIS")
107 + if bootstrapURIs == "" {
108 + // Use flagBootstraps as fallback
109 + bootstrapURIs = strings.Join(flagBootstraps, ",")
110 + }
111 +
112 log.Info().
113 Str("static_dir", staticDir).
73 - Str("portal_domain", portalDomain).
114 + Str("portal_host", portalHost).
115 + Str("portal_ui_url", portalUIURL).
116 + Str("portal_frontend_pattern", portalFrontendPattern).
117 + Str("bootstrap_uris", bootstrapURIs).
118 Msg("[server] frontend configuration")
119
120 cred := sdk.NewCredential()
cmd/relay-server/static/index.html
-15
@@ -90,21 +90,6 @@
90 });
91 } catch (_) { }
92 })();
93 - (function () {
94 - try {
95 - var port = window.location.port;
96 - if (!port) return; // default ports (80/443)
97 - document.querySelectorAll('a.card[href^="//"]').forEach(function (a) {
98 - try {
99 - var u = new URL(a.href);
100 - if (!u.port) { // only if not already set
101 - u.port = port;
102 - a.href = u.toString();
103 - }
104 - } catch (_) { }
105 - });
106 - } catch (_) { }
107 - })();
93 </script>
94 </body>
95
cmd/relay-server/view.go
+15 -2
@@ -51,12 +51,25 @@ func serveHTTP(_ context.Context, addr string, serv *portal.RelayServer, nodeID
51 serveAsset(adminMux, "/favicon.png", "static/favicon/favicon.png", "image/png")
52 serveAsset(adminMux, "/favicon.svg", "static/favicon/favicon.svg", "image/svg+xml")
53
54 - // Static assets for admin UI
54 + // Static assets for admin UI (embedded files)
55 adminMux.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) {
56 path := strings.TrimPrefix(r.URL.Path, "/static/")
57 serveAdminStatic(w, r, path)
58 })
59
60 + // Portal frontend files (for unified caching)
61 + adminMux.HandleFunc("/frontend/", func(w http.ResponseWriter, r *http.Request) {
62 + path := strings.TrimPrefix(r.URL.Path, "/frontend/")
63 +
64 + // Special handling for manifest.json - generate dynamically
65 + if path == "manifest.json" {
66 + serveDynamicManifest(w, r)
67 + return
68 + }
69 +
70 + servePortalStaticFile(w, r, path)
71 + })
72 +
73 adminMux.HandleFunc("/relay", func(w http.ResponseWriter, r *http.Request) {
74 if r.Method != http.MethodGet {
75 w.Header().Set("Allow", http.MethodGet)
@@ -235,7 +248,7 @@ func convertLeaseEntriesToRows(serv *portal.RelayServer) []leaseRow {
248 dnsLabel = dnsLabel[:8] + "..."
249 }
250
238 - link := fmt.Sprintf("//%s.%s/", lease.Name, portalDomain)
251 + link := fmt.Sprintf("//%s.%s/", lease.Name, portalHost)
252
253 row := leaseRow{
254 Peer: identityID,
cmd/webclient/main_js.go
+42 -7
@@ -28,12 +28,45 @@ import (
28 )
29
30 var (
31 - // Default bootstrap server, can be overridden at build time with -ldflags
32 - // Example: make build-wasm BOOTSTRAPS=wss://portal.gosuda.org/relay
33 - bootstrapServersCSV string = "ws://localhost:4017/relay"
34 - rdClient *sdk.RDClient
31 + rdClient *sdk.RDClient
32 )
33
34 +// getBootstrapServers retrieves bootstrap servers from global JavaScript variable
35 +func getBootstrapServers() []string {
36 + // Try to get bootstrap servers from window.__BOOTSTRAP_SERVERS__
37 + bootstrapsValue := js.Global().Get("__BOOTSTRAP_SERVERS__")
38 +
39 + if bootstrapsValue.IsUndefined() || bootstrapsValue.IsNull() {
40 + log.Warn().Msg("__BOOTSTRAP_SERVERS__ not found in global scope, using default")
41 + return []string{"ws://localhost:4017/relay"}
42 + }
43 +
44 + // Handle string (comma-separated)
45 + if bootstrapsValue.Type() == js.TypeString {
46 + bootstrapsStr := bootstrapsValue.String()
47 + if bootstrapsStr == "" {
48 + return []string{"ws://localhost:4017/relay"}
49 + }
50 + servers := strings.Split(bootstrapsStr, ",")
51 + for i := range servers {
52 + servers[i] = strings.TrimSpace(servers[i])
53 + }
54 + return servers
55 + }
56 +
57 + // Handle array
58 + if bootstrapsValue.Type() == js.TypeObject && bootstrapsValue.Length() > 0 {
59 + servers := make([]string, bootstrapsValue.Length())
60 + for i := 0; i < bootstrapsValue.Length(); i++ {
61 + servers[i] = bootstrapsValue.Index(i).String()
62 + }
63 + return servers
64 + }
65 +
66 + log.Warn().Msg("Invalid __BOOTSTRAP_SERVERS__ format, using default")
67 + return []string{"ws://localhost:4017/relay"}
68 +}
69 +
70 var rdDialer = func(ctx context.Context, network, address string) (net.Conn, error) {
71 originalAddr := address
72 address = strings.TrimSuffix(address, ":80")
@@ -355,7 +388,7 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
388
389 r = r.Clone(context.Background())
390
358 - // Decode hostname properly for Korean/multi-language domains
391 + // Decode hostname properly for IDN domains
392 decodedHost := getLeaseID(r.URL.Hostname())
393 r.URL.Host = decodedHost
394 r.URL.Scheme = "http"
@@ -575,8 +608,10 @@ func main() {
608 log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339})
609 var err error
610
578 - var bootstrapServerList = strings.Split(bootstrapServersCSV, ",")
579 - log.Info().Strs("servers", bootstrapServerList).Msg("Initializing RDClient with bootstrap servers")
611 + // Get bootstrap servers from global JavaScript variable
612 + bootstrapServerList := getBootstrapServers()
613 +
614 + log.Info().Strs("servers", bootstrapServerList).Msg("Initializing RDClient with bootstrap servers from global variable")
615
616 rdClient, err = sdk.NewClient(
617 sdk.WithBootstrapServers(bootstrapServerList),
cmd/webclient/service-worker.js
+54 -130
@@ -1,32 +1,21 @@
1 -const BASE_PATH = new URL('./', self.location).pathname;
2 -// const wasm_exec_URL = "https://cdn.jsdelivr.net/gh/golang/go@go1.25.3/lib/wasm/wasm_exec.js";
3 -const wasm_exec_URL = BASE_PATH + "wasm_exec.js";
4 -const manifest_URL = BASE_PATH + "manifest.json";
5 -
1 +//const wasm_exec_URL = "https://cdn.jsdelivr.net/gh/golang/go@go1.25.3/lib/wasm/wasm_exec.js";
2 +let BASE_PATH = "<PORTAL_UI_URL>";
3 +let wasmManifestString = '"<WASM_MANIFEST>"';
4 +let wasmManifest = JSON.parse(wasmManifestString);
5 +
6 +let wasm_exec_URL = BASE_PATH + "/frontend/wasm_exec.js";
7 +if (new URL(BASE_PATH).protocol === "http:") {
8 + wasm_exec_URL = "/frontend/wasm_exec.js";
9 +}
10 importScripts(wasm_exec_URL);
11
12 let loading = false;
13 let initError = null;
10 -let wasmManifest = null;
11 -let currentCacheVersion = null;
14 let _lastReload = Date.now();
15
16 // Fetch manifest to get current WASM filename
17 async function fetchManifest() {
16 - if (wasmManifest) return wasmManifest;
17 -
18 - try {
19 - const response = await fetch(manifest_URL);
20 - if (!response.ok) {
21 - throw new Error(`Failed to fetch manifest: ${response.status}`);
22 - }
23 - wasmManifest = await response.json();
24 - currentCacheVersion = `WASM_Cache_${wasmManifest.hash}`;
25 - return wasmManifest;
26 - } catch (error) {
27 - console.error("[SW] Failed to fetch manifest:", error);
28 - throw error;
29 - }
18 + return wasmManifest;
19 }
20
21 // Send error to all clients
@@ -69,27 +58,23 @@ async function runWASM() {
58
59 try {
60 const manifest = await fetchManifest();
72 - // Use content-addressed path under scope: <BASE_PATH>/static/<sha256>.wasm
73 - const wasm_URL = `${BASE_PATH}static/${manifest.wasmFile}`;
61 + // Use unified cache path from manifest (full URL)
62 + let wasm_URL;
63 + if (manifest.wasmUrl && new URL(manifest.wasmUrl).protocol !== "http:") {
64 + wasm_URL = manifest.wasmUrl;
65 + } else {
66 + wasm_URL = `/frontend/${manifest.wasmFile}`;
67 + }
68
69 const go = new Go();
70
77 - const cache = await caches.open(currentCacheVersion);
78 -
79 - let wasm_file;
80 - const cache_wasm = await cache.match(wasm_URL);
81 -
82 - if (cache_wasm) {
83 - wasm_file = await cache_wasm.arrayBuffer();
84 - } else {
85 - const response = await fetch(wasm_URL);
86 - if (!response.ok) {
87 - throw new Error(
88 - `Failed to fetch WASM: ${response.status} ${response.statusText}`
89 - );
90 - }
91 - wasm_file = await response.arrayBuffer();
71 + const response = await fetch(wasm_URL);
72 + if (!response.ok) {
73 + throw new Error(
74 + `Failed to fetch WASM: ${response.status} ${response.statusText}`
75 + );
76 }
77 + const wasm_file = await response.arrayBuffer();
78
79 const instance = await WebAssembly.instantiate(wasm_file, go.importObject);
80
@@ -99,10 +84,12 @@ async function runWASM() {
84 loading = false;
85 };
86
102 - go.run(instance.instance).then(onExit).catch((error) => {
103 - console.error("[SW] Go Program Error:", error);
104 - onExit();
105 - });
87 + go.run(instance.instance)
88 + .then(onExit)
89 + .catch((error) => {
90 + console.error("[SW] Go Program Error:", error);
91 + onExit();
92 + });
93 } catch (error) {
94 console.error("[SW] WASM initialization failed:", error);
95 throw new Error(`WASM Initialization: ${error.message}`);
@@ -110,19 +97,7 @@ async function runWASM() {
97 }
98
99 self.addEventListener("install", (e) => {
113 - async function LoadCache() {
114 - try {
115 - const manifest = await fetchManifest();
116 - // Use content-addressed path: /static/<sha256>.wasm
117 - const wasm_URL = `/static/${manifest.wasmFile}`;
118 - const cache = await caches.open(currentCacheVersion);
119 - await cache.addAll([wasm_URL, wasm_exec_URL, manifest_URL]);
120 - } catch (error) {
121 - console.error("[SW] Cache loading failed:", error);
122 - throw new Error(`Cache Loading: ${error.message}`);
123 - }
124 - }
125 - e.waitUntil(LoadCache());
100 + e.waitUntil(init());
101 self.skipWaiting();
102 });
103
@@ -130,19 +105,6 @@ self.addEventListener("activate", (e) => {
105 e.waitUntil(
106 (async () => {
107 try {
133 - // Delete old caches
134 - const cacheNames = await caches.keys();
135 - await Promise.all(
136 - cacheNames.map((cacheName) => {
137 - if (
138 - cacheName !== currentCacheVersion &&
139 - cacheName.startsWith("WASM_Cache_")
140 - ) {
141 - return caches.delete(cacheName);
142 - }
143 - })
144 - );
145 -
108 // Claim clients first to take control immediately
109 await self.clients.claim();
110
@@ -164,15 +126,18 @@ self.addEventListener("activate", (e) => {
126
127 self.addEventListener("message", (event) => {
128 if (event.data && event.data.type === "CLAIM_CLIENTS") {
167 - self.clients.claim().then(() => {
168 - self.clients.matchAll().then((clients) => {
169 - clients.forEach((client) => {
170 - client.postMessage({ type: "CLAIMED" });
129 + self.clients
130 + .claim()
131 + .then(() => {
132 + self.clients.matchAll().then((clients) => {
133 + clients.forEach((client) => {
134 + client.postMessage({ type: "CLAIMED" });
135 + });
136 });
137 + })
138 + .catch((error) => {
139 + console.error("[SW] Manual clients.claim() failed:", error);
140 });
173 - }).catch((error) => {
174 - console.error("[SW] Manual clients.claim() failed:", error);
175 - });
141 }
142 });
143
@@ -213,70 +178,29 @@ self.addEventListener("fetch", (e) => {
178 return;
179 }
180
216 - // Serve portal.mp4 from cache or fetch from origin
217 - if (url.pathname === BASE_PATH + "portal.mp4") {
218 - e.respondWith(
219 - (async () => {
220 - try {
221 - await fetchManifest();
222 - // Try to get from cache first
223 - const cache = await caches.open(currentCacheVersion);
224 - const cachedResponse = await cache.match(BASE_PATH + "portal.mp4");
225 - if (cachedResponse) {
226 - return cachedResponse;
227 - }
228 -
229 - // Fetch from network and cache it
230 - const response = await fetch(e.request);
231 - if (response.ok) {
232 - cache.put(BASE_PATH + "portal.mp4", response.clone());
233 - }
234 - return response;
235 - } catch (error) {
236 - console.error("[SW] Failed to fetch portal.mp4:", error);
237 - return new Response("Not Found", { status: 404 });
238 - }
239 - })()
240 - );
241 - return;
242 - }
243 -
181 e.respondWith(
182 (async () => {
183 if (typeof __go_jshttp === "undefined" && !loading) {
184 try {
185 await init();
186 } catch (error) {
250 - console.error('[SW] Init failed:', error);
251 - return new Response("WASM initialization failed. Please refresh the page.", {
252 - status: 503,
253 - statusText: 'Service Unavailable'
254 - });
187 + console.error("[SW] Init failed:", error);
188 + return new Response(
189 + "WASM initialization failed. Please refresh the page.",
190 + {
191 + status: 503,
192 + statusText: "Service Unavailable",
193 + }
194 + );
195 }
196
197 let waitCount = 0;
198 while (loading && waitCount < 50) {
259 - if (typeof __go_jshttp !== "undefined") {
260 - break;
261 - }
262 - await new Promise(resolve => setTimeout(resolve, 100));
263 - waitCount++;
264 - }
265 - }
266 -
267 - if (typeof __go_jshttp === "undefined") {
268 - if (Date.now() - _lastReload > 1000) {
269 - _lastReload = Date.now();
270 - loading = false;
271 - return new Response(
272 - "<html><head><meta http-equiv='refresh' content='0'></head><body><h1>Initializing WASM... Please wait.</h1></body></html>",
273 - { status: 503, headers: { "Content-Type": "text/html" } }
274 - );
275 - } else {
276 - return new Response(
277 - "<html><head><meta http-equiv='refresh' content='0'></head><body><h1>Sorry, Service Worker failed to process the request. Please refresh the page.</h1></body></html>",
278 - { status: 503, headers: { "Content-Type": "text/html" } }
279 - );
199 + if (typeof __go_jshttp !== "undefined") {
200 + break;
201 + }
202 + await new Promise((resolve) => setTimeout(resolve, 100));
203 + waitCount++;
204 }
205 }
206
@@ -284,7 +208,7 @@ self.addEventListener("fetch", (e) => {
208 const resp = await __go_jshttp(e.request);
209 return resp;
210 } catch (error) {
287 - console.error('[SW] Request handling error:', error);
211 + console.error("[SW] Request handling error:", error);
212 __go_jshttp = undefined;
213 await init();
214 const resp = await __go_jshttp(e.request);