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);