fix: for enjoy safari
Hee Sung Son committed
Nov 7, 2025 at 22:53 UTC
875fb6044b2b8f0348a88ce1994bd89c6a994aeb
3 files changed
+389
-392
cmd/relay-server/frontend.go
+4
-5
@@ -7,7 +7,6 @@ import (
7
"encoding/hex"
8
"encoding/json"
9
"fmt"
10
- "io"
10
"net/http"
11
"os"
12
"path/filepath"
@@ -389,8 +388,8 @@ func serveStaticFile(w http.ResponseWriter, r *http.Request, path string, conten
388
}
389
}
390
392
- w.WriteHeader(http.StatusOK)
393
- io.Copy(w, file)
391
+ // Use http.ServeContent for proper Range request support (required for Safari video playback)
392
+ http.ServeContent(w, r, path, fileInfo.ModTime(), file)
393
394
log.Debug().
395
Str("path", path).
@@ -433,8 +432,8 @@ func serveStaticFileWithFallback(w http.ResponseWriter, r *http.Request, path st
432
}
433
}
434
436
- w.WriteHeader(http.StatusOK)
437
- io.Copy(w, file)
435
+ // Use http.ServeContent for proper Range request support (required for Safari video playback)
436
+ http.ServeContent(w, r, path, fileInfo.ModTime(), file)
437
438
log.Debug().
439
Str("path", path).
cmd/webclient/index.html
+384
-386
@@ -1,430 +1,428 @@
1
<!DOCTYPE html>
2
<html>
3
- <head>
3
+
4
+<head>
5
<meta charset="UTF-8" />
6
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
<title>Portal Proxy Gateway</title>
7
- <link
8
- rel="stylesheet"
9
- as="style"
10
- crossorigin
11
- href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css"
12
- />
8
+ <link rel="stylesheet" as="style" crossorigin
9
+ href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css" />
10
<style>
14
- body {
15
- margin: 0;
16
- padding: 0;
17
- font-family: "Pretendard", -apple-system, BlinkMacSystemFont, "Segoe UI",
18
- Roboto, sans-serif;
19
- background: #000;
20
- }
21
-
22
- #loading-screen {
23
- position: fixed;
24
- top: 0;
25
- left: 0;
26
- width: 100%;
27
- height: 100%;
28
- background: #000;
29
- display: flex;
30
- flex-direction: column;
31
- justify-content: center;
32
- align-items: center;
33
- z-index: 9999;
34
- transition: opacity 0.5s ease-out;
35
- }
36
-
37
- #loading-screen.hide {
38
- opacity: 0;
39
- pointer-events: none;
40
- }
41
-
42
- #loading-logo {
43
- position: relative;
44
- max-width: 500px;
45
- width: 90%;
46
- margin-bottom: 40px;
47
- /* 컨테이너 쿼리를 위한 설정 */
48
- container-type: inline-size;
49
- }
50
-
51
- #loading-logo video {
52
- width: 100%;
53
- height: auto;
54
- display: block;
55
- }
56
-
57
- .logo-overlay {
58
- position: absolute;
59
- top: 0;
60
- left: 0;
61
- width: 100%;
62
- height: 100%;
63
- display: flex;
64
- flex-direction: column;
65
- justify-content: space-between;
66
- align-items: center;
67
- padding: 10% 1%;
68
- box-sizing: border-box;
69
- pointer-events: none;
70
- }
71
-
72
- .logo-title {
73
- font-family: "Pretendard", sans-serif;
74
- /* 51.5px at 500px container = 51.5/500*100 = 10.3cqw */
75
- font-size: 10.3cqw;
76
- font-weight: 600;
77
- color: #ffffff;
78
- letter-spacing: 0.2em;
79
- text-align: center;
80
- text-shadow: 0 0 20px rgba(255, 255, 255, 0.5);
81
- margin: 0;
82
- }
83
-
84
- .logo-subtitle {
85
- font-family: "Pretendard", sans-serif;
86
- /* 18px at 500px container = 18/500*100 = 3.6cqw */
87
- font-size: 3.6cqw;
88
- font-weight: 600;
89
- color: #ffffff;
90
- letter-spacing: -0.05em;
91
- text-align: center;
92
- text-shadow: 0 0 15px rgba(255, 255, 255, 0.4);
93
- margin: 0;
94
- }
95
-
96
- .loading-bar-container {
97
- width: 300px;
98
- max-width: 80%;
99
- height: 4px;
100
- background: rgba(255, 255, 255, 0.1);
101
- border-radius: 2px;
102
- overflow: hidden;
103
- position: relative;
104
- }
105
-
106
- .loading-bar {
107
- height: 100%;
108
- background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
109
- border-radius: 2px;
110
- animation: loading 1.5s ease-in-out infinite;
111
- }
112
-
113
- @keyframes loading {
114
- 0% {
115
- width: 0%;
116
- margin-left: 0%;
11
+ body {
12
+ margin: 0;
13
+ padding: 0;
14
+ font-family: "Pretendard", -apple-system, BlinkMacSystemFont, "Segoe UI",
15
+ Roboto, sans-serif;
16
+ background: #000;
17
+ }
18
+
19
+ #loading-screen {
20
+ position: fixed;
21
+ top: 0;
22
+ left: 0;
23
+ width: 100%;
24
+ height: 100%;
25
+ background: #000;
26
+ display: flex;
27
+ flex-direction: column;
28
+ justify-content: center;
29
+ align-items: center;
30
+ z-index: 9999;
31
+ transition: opacity 0.5s ease-out;
32
+ }
33
+
34
+ #loading-screen.hide {
35
+ opacity: 0;
36
+ pointer-events: none;
37
+ }
38
+
39
+ #loading-logo {
40
+ position: relative;
41
+ max-width: 500px;
42
+ width: 90%;
43
+ margin-bottom: 40px;
44
+ /* 컨테이너 쿼리를 위한 설정 */
45
+ container-type: inline-size;
46
+ }
47
+
48
+ #loading-logo video {
49
+ width: 100%;
50
+ height: auto;
51
+ display: block;
52
+ }
53
+
54
+ .logo-overlay {
55
+ position: absolute;
56
+ top: 0;
57
+ left: 0;
58
+ width: 100%;
59
+ height: 100%;
60
+ display: flex;
61
+ flex-direction: column;
62
+ justify-content: space-between;
63
+ align-items: center;
64
+ padding: 10% 1%;
65
+ box-sizing: border-box;
66
+ pointer-events: none;
67
+ }
68
+
69
+ .logo-title {
70
+ font-family: "Pretendard", sans-serif;
71
+ /* 51.5px at 500px container = 51.5/500*100 = 10.3cqw */
72
+ font-size: 10.3cqw;
73
+ font-weight: 600;
74
+ color: #ffffff;
75
+ letter-spacing: 0.2em;
76
+ text-align: center;
77
+ text-shadow: 0 0 20px rgba(255, 255, 255, 0.5);
78
+ margin: 0;
79
+ }
80
+
81
+ .logo-subtitle {
82
+ font-family: "Pretendard", sans-serif;
83
+ /* 18px at 500px container = 18/500*100 = 3.6cqw */
84
+ font-size: 3.6cqw;
85
+ font-weight: 600;
86
+ color: #ffffff;
87
+ letter-spacing: -0.05em;
88
+ text-align: center;
89
+ text-shadow: 0 0 15px rgba(255, 255, 255, 0.4);
90
+ margin: 0;
91
}
92
119
- 50% {
120
- width: 50%;
121
- margin-left: 25%;
93
+ .loading-bar-container {
94
+ width: 300px;
95
+ max-width: 80%;
96
+ height: 4px;
97
+ background: rgba(255, 255, 255, 0.1);
98
+ border-radius: 2px;
99
+ overflow: hidden;
100
+ position: relative;
101
}
102
124
- 100% {
125
- width: 0%;
126
- margin-left: 100%;
103
+ .loading-bar {
104
+ height: 100%;
105
+ background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
106
+ border-radius: 2px;
107
+ animation: loading 1.5s ease-in-out infinite;
108
+ }
109
+
110
+ @keyframes loading {
111
+ 0% {
112
+ width: 0%;
113
+ margin-left: 0%;
114
+ }
115
+
116
+ 50% {
117
+ width: 50%;
118
+ margin-left: 25%;
119
+ }
120
+
121
+ 100% {
122
+ width: 0%;
123
+ margin-left: 100%;
124
+ }
125
+ }
126
+
127
+ .loading-text {
128
+ color: rgba(255, 255, 255, 0.7);
129
+ margin-top: 20px;
130
+ font-size: 14px;
131
+ text-align: center;
132
+ transition: color 0.3s ease;
133
+ }
134
+
135
+ .loading-text.error {
136
+ color: #ef4444;
137
}
128
- }
129
-
130
- .loading-text {
131
- color: rgba(255, 255, 255, 0.7);
132
- margin-top: 20px;
133
- font-size: 14px;
134
- text-align: center;
135
- transition: color 0.3s ease;
136
- }
137
-
138
- .loading-text.error {
139
- color: #ef4444;
140
- }
138
</style>
142
- </head>
139
+</head>
140
144
- <body>
141
+<body>
142
<!-- Loading Screen -->
143
<div id="loading-screen">
147
- <div id="loading-logo">
148
- <video autoplay loop muted playsinline>
149
- <source src="/portal.mp4" type="video/mp4" />
150
- </video>
151
- <div class="logo-overlay">
152
- <h1 class="logo-title">PORTAL</h1>
153
- <p class="logo-subtitle">LOCAL TO WEB. INSTANT ACCESS.</p>
144
+ <div id="loading-logo">
145
+ <video autoplay loop muted playsinline>
146
+ <source src="/portal.mp4" type="video/mp4" />
147
+ </video>
148
+ <div class="logo-overlay">
149
+ <h1 class="logo-title">PORTAL</h1>
150
+ <p class="logo-subtitle">LOCAL TO WEB. INSTANT ACCESS.</p>
151
+ </div>
152
+ </div>
153
+ <div class="loading-bar-container" id="loading-bar-container">
154
+ <div class="loading-bar"></div>
155
+ </div>
156
+ <div class="loading-text" id="loading-text">
157
+ Initializing Portal Network...
158
</div>
155
- </div>
156
- <div class="loading-bar-container" id="loading-bar-container">
157
- <div class="loading-bar"></div>
158
- </div>
159
- <div class="loading-text" id="loading-text">
160
- Initializing Portal Network...
161
- </div>
159
</div>
160
161
<script>
165
- // In-app browser detection and redirect handler
166
- (function () {
167
- const userAgent = navigator.userAgent.toLowerCase();
168
- const finalUrl = window.location.href;
169
-
170
- // Detect various in-app browsers
171
- const isKakao = /kakaotalk/i.test(userAgent);
172
- const isNaver = /naver/i.test(userAgent);
173
- const isFacebook = /fb|fbav|fban/i.test(userAgent);
174
- const isInstagram = /instagram/i.test(userAgent);
175
- const isLine = /line/i.test(userAgent);
176
- const isInAppBrowser =
177
- isKakao || isNaver || isFacebook || isInstagram || isLine;
178
- const isAndroid = /android/.test(userAgent);
179
- const isIOS = /ipad|iphone|ipod/.test(userAgent);
180
-
181
- if (!isInAppBrowser) {
182
- return; // Not in-app browser, proceed normally
183
- }
162
+ // In-app browser detection and redirect handler
163
+ (function () {
164
+ const userAgent = navigator.userAgent.toLowerCase();
165
+ const finalUrl = window.location.href;
166
+
167
+ // Detect various in-app browsers
168
+ const isKakao = /kakaotalk/i.test(userAgent);
169
+ const isNaver = /naver/i.test(userAgent);
170
+ const isFacebook = /fb|fbav|fban/i.test(userAgent);
171
+ const isInstagram = /instagram/i.test(userAgent);
172
+ const isLine = /line/i.test(userAgent);
173
+ const isInAppBrowser =
174
+ isKakao || isNaver || isFacebook || isInstagram || isLine;
175
+ const isAndroid = /android/.test(userAgent);
176
+ const isIOS = /ipad|iphone|ipod/.test(userAgent);
177
+
178
+ if (!isInAppBrowser) {
179
+ return; // Not in-app browser, proceed normally
180
+ }
181
185
- // Handle redirection to external browser
186
- if (isAndroid) {
187
- if (isKakao) {
188
- location.href =
189
- "kakaotalk://web/openExternal?url=" +
190
- encodeURIComponent(finalUrl);
191
- setTimeout(() => {
192
- location.href = "kakaotalk://inappbrowser/close";
193
- }, 10);
194
- } else {
195
- // Use Intent to open in Chrome or default browser
196
- const intentUrl =
197
- "intent://" +
198
- finalUrl.replace(/^https?:\/\//, "") +
199
- "#Intent;scheme=https;action=android.intent.action.VIEW;end";
200
- location.href = intentUrl;
201
- }
202
- return; // Stop execution
203
- } else if (isIOS) {
204
- if (isKakao) {
205
- location.href =
206
- "kakaotalk://web/openExternal?url=" +
207
- encodeURIComponent(finalUrl);
208
- // Set up auto-close listener for iOS KakaoTalk
209
- document.addEventListener("visibilitychange", () => {
210
- if (document.visibilityState == "visible") {
211
- location.href = "kakaoweb://closeBrowser";
212
- }
213
- });
214
- } else {
215
- // For other iOS in-app browsers, show message in loading screen
216
- updateLoadingText("⚠️ Please open in external browser (Safari)");
217
- document.getElementById("loading-text").classList.add("error");
218
- }
219
- return; // Stop execution
220
- }
221
- })();
182
+ // Handle redirection to external browser
183
+ if (isAndroid) {
184
+ if (isKakao) {
185
+ location.href =
186
+ "kakaotalk://web/openExternal?url=" +
187
+ encodeURIComponent(finalUrl);
188
+ setTimeout(() => {
189
+ location.href = "kakaotalk://inappbrowser/close";
190
+ }, 10);
191
+ } else {
192
+ // Use Intent to open in Chrome or default browser
193
+ const intentUrl =
194
+ "intent://" +
195
+ finalUrl.replace(/^https?:\/\//, "") +
196
+ "#Intent;scheme=https;action=android.intent.action.VIEW;end";
197
+ location.href = intentUrl;
198
+ }
199
+ return; // Stop execution
200
+ } else if (isIOS) {
201
+ if (isKakao) {
202
+ location.href =
203
+ "kakaotalk://web/openExternal?url=" +
204
+ encodeURIComponent(finalUrl);
205
+ // Set up auto-close listener for iOS KakaoTalk
206
+ document.addEventListener("visibilitychange", () => {
207
+ if (document.visibilityState == "visible") {
208
+ location.href = "kakaoweb://closeBrowser";
209
+ }
210
+ });
211
+ } else {
212
+ // For other iOS in-app browsers, show message in loading screen
213
+ // updateLoadingText("⚠️ Please open in external browser (Safari)");
214
+ // document.getElementById("loading-text").classList.add("error");
215
+ }
216
+ return; // Stop execution
217
+ }
218
+ })();
219
</script>
220
<script>
224
- // Update loading text
225
- function updateLoadingText(message, isError = false) {
226
- const loadingText = document.getElementById("loading-text");
227
- if (loadingText) {
228
- loadingText.textContent = message;
229
- if (isError) {
230
- loadingText.classList.add("error");
231
- } else {
232
- loadingText.classList.remove("error");
233
- }
234
- }
235
- }
236
-
237
- // Show error in loading text
238
- function showError(error, context = "") {
239
- let errorMessage = "";
240
- if (typeof error === "string") {
241
- errorMessage = error;
242
- } else if (error instanceof Error) {
243
- errorMessage = `${error.message}`;
244
- } else {
245
- errorMessage = "An error occurred";
221
+ // Update loading text
222
+ function updateLoadingText(message, isError = false) {
223
+ const loadingText = document.getElementById("loading-text");
224
+ if (loadingText) {
225
+ loadingText.textContent = message;
226
+ if (isError) {
227
+ loadingText.classList.add("error");
228
+ } else {
229
+ loadingText.classList.remove("error");
230
+ }
231
+ }
232
}
233
248
- if (context) {
249
- errorMessage = `⚠️ ${context}: ${errorMessage}`;
250
- } else {
251
- errorMessage = `⚠️ ${errorMessage}`;
234
+ // Show error in loading text
235
+ function showError(error, context = "") {
236
+ let errorMessage = "";
237
+ if (typeof error === "string") {
238
+ errorMessage = error;
239
+ } else if (error instanceof Error) {
240
+ errorMessage = `${error.message}`;
241
+ } else {
242
+ errorMessage = "An error occurred";
243
+ }
244
+
245
+ if (context) {
246
+ errorMessage = `⚠️ ${context}: ${errorMessage}`;
247
+ } else {
248
+ errorMessage = `⚠️ ${errorMessage}`;
249
+ }
250
+
251
+ updateLoadingText(errorMessage, true);
252
+ console.error(`[Portal Error - ${context}]`, error);
253
}
254
254
- updateLoadingText(errorMessage, true);
255
- console.error(`[Portal Error - ${context}]`, error);
256
- }
257
-
258
- // Global error handler
259
- window.addEventListener("error", (event) => {
260
- showError(event.error || event.message, "Error");
261
- event.preventDefault();
262
- });
263
-
264
- // Unhandled promise rejection handler
265
- window.addEventListener("unhandledrejection", (event) => {
266
- showError(event.reason, "Promise Error");
267
- event.preventDefault();
268
- });
269
-
270
- // Listen for errors from Service Worker
271
- if ("serviceWorker" in navigator) {
272
- navigator.serviceWorker.addEventListener("message", (event) => {
273
- if (event.data && event.data.type === "SW_ERROR") {
274
- const error = event.data.error;
275
- showError(error.message, "Service Worker Error");
276
- }
255
+ // Global error handler
256
+ window.addEventListener("error", (event) => {
257
+ showError(event.error || event.message, "Error");
258
+ event.preventDefault();
259
});
278
- }
260
280
- async function registerServiceWorker() {
281
- let retryCount = 0;
282
- const maxRetries = 30; // 3초 (30 × 100ms)
261
+ // Unhandled promise rejection handler
262
+ window.addEventListener("unhandledrejection", (event) => {
263
+ showError(event.reason, "Promise Error");
264
+ event.preventDefault();
265
+ });
266
284
- const checkWASMReady = async () => {
285
- try {
286
- const resp = await fetch("/e8c2c70c-ec4a-40b2-b8af-d5638264f831", {
287
- cache: "no-store",
267
+ // Listen for errors from Service Worker
268
+ if ("serviceWorker" in navigator) {
269
+ navigator.serviceWorker.addEventListener("message", (event) => {
270
+ if (event.data && event.data.type === "SW_ERROR") {
271
+ const error = event.data.error;
272
+ showError(error.message, "Service Worker Error");
273
+ }
274
});
289
- const text = await resp.text();
290
-
291
- if (text === "ACK-e8c2c70c-ec4a-40b2-b8af-d5638264f831") {
292
- updateLoadingText("Portal Network Ready!");
293
- setTimeout(() => {
294
- window.location.reload();
295
- }, 500);
296
- } else if (text === "NAK-e8c2c70c-ec4a-40b2-b8af-d5638264f831") {
297
- retryCount++;
298
- if (retryCount > maxRetries) {
299
- throw new Error(
300
- `WASM initialization timeout after ${maxRetries} retries`
301
- );
302
- }
303
- updateLoadingText(
304
- `Initializing WASM... (${retryCount}/${maxRetries})`
305
- );
306
- setTimeout(checkWASMReady, 100);
307
- } else {
308
- if (text.includes("<!DOCTYPE") || text.includes("<html>")) {
309
- throw new Error("Service Worker not active - please refresh");
310
- } else {
311
- throw new Error(
312
- `Unexpected response: ${text.substring(0, 50)}`
313
- );
314
- }
315
- }
316
- } catch (error) {
317
- showError(error, "Connection");
318
- }
319
- };
320
-
321
- const waitForController = () => {
322
- return new Promise((resolve, reject) => {
323
- const checkController = () => {
324
- if (navigator.serviceWorker.controller) {
325
- resolve();
326
- return true;
327
- }
328
- return false;
329
- };
330
-
331
- if (checkController()) {
332
- return;
333
- }
275
+ }
276
335
- let timeoutId;
336
- let pollIntervalId;
277
+ async function registerServiceWorker() {
278
+ let retryCount = 0;
279
+ const maxRetries = 30; // 3초 (30 × 100ms)
280
+
281
+ const checkWASMReady = async () => {
282
+ try {
283
+ const resp = await fetch("/e8c2c70c-ec4a-40b2-b8af-d5638264f831", {
284
+ cache: "no-store",
285
+ });
286
+ const text = await resp.text();
287
+
288
+ if (text === "ACK-e8c2c70c-ec4a-40b2-b8af-d5638264f831") {
289
+ updateLoadingText("Portal Network Ready!");
290
+ setTimeout(() => {
291
+ window.location.reload();
292
+ }, 500);
293
+ } else if (text === "NAK-e8c2c70c-ec4a-40b2-b8af-d5638264f831") {
294
+ retryCount++;
295
+ if (retryCount > maxRetries) {
296
+ throw new Error(
297
+ `WASM initialization timeout after ${maxRetries} retries`
298
+ );
299
+ }
300
+ updateLoadingText(
301
+ `Initializing WASM... (${retryCount}/${maxRetries})`
302
+ );
303
+ setTimeout(checkWASMReady, 100);
304
+ } else {
305
+ if (text.includes("<!DOCTYPE") || text.includes("<html>")) {
306
+ throw new Error("Service Worker not active - please refresh");
307
+ } else {
308
+ throw new Error(
309
+ `Unexpected response: ${text.substring(0, 50)}`
310
+ );
311
+ }
312
+ }
313
+ } catch (error) {
314
+ showError(error, "Connection");
315
+ }
316
+ };
317
338
- const onControllerChange = () => {
339
- if (checkController()) {
340
- clearTimeout(timeoutId);
341
- clearInterval(pollIntervalId);
342
- navigator.serviceWorker.removeEventListener(
343
- "controllerchange",
344
- onControllerChange
345
- );
346
- }
318
+ const waitForController = () => {
319
+ return new Promise((resolve, reject) => {
320
+ const checkController = () => {
321
+ if (navigator.serviceWorker.controller) {
322
+ resolve();
323
+ return true;
324
+ }
325
+ return false;
326
+ };
327
+
328
+ if (checkController()) {
329
+ return;
330
+ }
331
+
332
+ let timeoutId;
333
+ let pollIntervalId;
334
+
335
+ const onControllerChange = () => {
336
+ if (checkController()) {
337
+ clearTimeout(timeoutId);
338
+ clearInterval(pollIntervalId);
339
+ navigator.serviceWorker.removeEventListener(
340
+ "controllerchange",
341
+ onControllerChange
342
+ );
343
+ }
344
+ };
345
+
346
+ navigator.serviceWorker.addEventListener(
347
+ "controllerchange",
348
+ onControllerChange
349
+ );
350
+
351
+ pollIntervalId = setInterval(() => {
352
+ if (checkController()) {
353
+ clearTimeout(timeoutId);
354
+ clearInterval(pollIntervalId);
355
+ navigator.serviceWorker.removeEventListener(
356
+ "controllerchange",
357
+ onControllerChange
358
+ );
359
+ }
360
+ }, 100);
361
+
362
+ timeoutId = setTimeout(() => {
363
+ clearInterval(pollIntervalId);
364
+ navigator.serviceWorker.removeEventListener(
365
+ "controllerchange",
366
+ onControllerChange
367
+ );
368
+
369
+ if (navigator.serviceWorker.controller) {
370
+ resolve();
371
+ } else {
372
+ reject(new Error("Service Worker activation timeout"));
373
+ setTimeout(() => {
374
+ location.reload();
375
+ }, 500);
376
+ }
377
+ }, 3000);
378
+ });
379
};
380
349
- navigator.serviceWorker.addEventListener(
350
- "controllerchange",
351
- onControllerChange
352
- );
353
-
354
- pollIntervalId = setInterval(() => {
355
- if (checkController()) {
356
- clearTimeout(timeoutId);
357
- clearInterval(pollIntervalId);
358
- navigator.serviceWorker.removeEventListener(
359
- "controllerchange",
360
- onControllerChange
381
+ try {
382
+ if (!("serviceWorker" in navigator)) {
383
+ throw new Error("Service Worker not supported in this browser");
384
+ }
385
+
386
+ updateLoadingText("Registering Service Worker...");
387
+
388
+ const registration = await navigator.serviceWorker.register(
389
+ "/service-worker.js",
390
+ {
391
+ scope: "/",
392
+ updateViaCache: "none",
393
+ }
394
);
362
- }
363
- }, 100);
364
-
365
- timeoutId = setTimeout(() => {
366
- clearInterval(pollIntervalId);
367
- navigator.serviceWorker.removeEventListener(
368
- "controllerchange",
369
- onControllerChange
370
- );
371
-
372
- if (navigator.serviceWorker.controller) {
373
- resolve();
374
- } else {
375
- reject(new Error("Service Worker activation timeout"));
376
- setTimeout(() => {
377
- location.reload();
378
- }, 500);
379
- }
380
- }, 3000);
381
- });
382
- };
383
-
384
- try {
385
- if (!("serviceWorker" in navigator)) {
386
- throw new Error("Service Worker not supported in this browser");
387
- }
388
-
389
- updateLoadingText("Registering Service Worker...");
390
-
391
- const registration = await navigator.serviceWorker.register(
392
- "/service-worker.js",
393
- {
394
- scope: "/",
395
- updateViaCache: "none",
396
- }
397
- );
395
399
- if (!navigator.serviceWorker.controller && registration.active) {
400
- registration.active.postMessage({ type: "CLAIM_CLIENTS" });
401
- }
396
+ if (!navigator.serviceWorker.controller && registration.active) {
397
+ registration.active.postMessage({ type: "CLAIM_CLIENTS" });
398
+ }
399
403
- updateLoadingText("Activating Service Worker...");
400
+ updateLoadingText("Activating Service Worker...");
401
405
- await navigator.serviceWorker.ready;
402
+ await navigator.serviceWorker.ready;
403
407
- if (!navigator.serviceWorker.controller) {
408
- updateLoadingText("Waiting for activation...");
409
- try {
410
- await Promise.race([
411
- waitForController(),
412
- new Promise((resolve) => setTimeout(resolve, 500))
413
- ]);
414
- } catch (error) {
415
- // Ignore timeout, proceed anyway
416
- }
417
- }
404
+ if (!navigator.serviceWorker.controller) {
405
+ updateLoadingText("Waiting for activation...");
406
+ try {
407
+ await Promise.race([
408
+ waitForController(),
409
+ new Promise((resolve) => setTimeout(resolve, 500))
410
+ ]);
411
+ } catch (error) {
412
+ // Ignore timeout, proceed anyway
413
+ }
414
+ }
415
419
- updateLoadingText("Connecting to Portal Network...");
416
+ updateLoadingText("Connecting to Portal Network...");
417
421
- setTimeout(checkWASMReady, 100);
422
- } catch (error) {
423
- showError(error, "Initialization");
418
+ setTimeout(checkWASMReady, 100);
419
+ } catch (error) {
420
+ showError(error, "Initialization");
421
+ }
422
}
425
- }
423
427
- registerServiceWorker();
424
+ registerServiceWorker();
425
</script>
429
- </body>
430
-</html>
426
+</body>
427
+
428
+</html>
\ No newline at end of file
cmd/webclient/streamjs/stream_js.go
+1
-1
@@ -110,7 +110,7 @@ func NewReadableStream(r io.ReadCloser) *ReadableStream {
110
underlyingSource.Set("start", onStart)
111
underlyingSource.Set("pull", onPull)
112
underlyingSource.Set("cancel", onCancel)
113
- underlyingSource.Set("type", "bytes")
113
+ // underlyingSource.Set("type", "bytes") // Safari does not support ReadableByteStreamController
114
115
// 9. JS ReadableStream 인스턴스 생성
116
stream := _ReadableStream.New(underlyingSource)