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)