refactor(webclient): reformat index.html for consistent indentation and quotes

- Changed indentation from 4 spaces to 2 spaces throughout HTML, CSS, and JS sections - Standardized quotes to double quotes in JS and self-closing tags in HTML - Commented out 'loading = false' in init() finally block to prevent resetting flag - Applied formatting for better code consistency and readability

lemon-mint committed Nov 5, 2025 at 17:55 UTC 82a679fe8929a1a5e4d951969336233ad2f3be2f
5 files changed +1010 -841
cmd/webclient/index.html
+353 -319
@@ -1,361 +1,395 @@
1 <!DOCTYPE html>
2 <html>
3 -
4 -<head>
5 - <meta charset="UTF-8">
6 - <meta name="viewport" content="width=device-width, initial-scale=1.0">
3 + <head>
4 + <meta charset="UTF-8" />
5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 <title>Portal Proxy Gateway</title>
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" />
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 + />
13 <style>
11 - body {
12 - margin: 0;
13 - padding: 0;
14 - font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
15 - background: #000;
16 - }
17 -
18 - #loading-screen {
19 - position: fixed;
20 - top: 0;
21 - left: 0;
22 - width: 100%;
23 - height: 100%;
24 - background: #000;
25 - display: flex;
26 - flex-direction: column;
27 - justify-content: center;
28 - align-items: center;
29 - z-index: 9999;
30 - transition: opacity 0.5s ease-out;
31 - }
32 -
33 - #loading-screen.hide {
34 - opacity: 0;
35 - pointer-events: none;
36 - }
37 -
38 - #loading-logo {
39 - position: relative;
40 - max-width: 500px;
41 - width: 90%;
42 - margin-bottom: 40px;
43 - /* 컨테이너 쿼리를 위한 설정 */
44 - container-type: inline-size;
45 - }
46 -
47 - #loading-logo video {
48 - width: 100%;
49 - height: auto;
50 - display: block;
51 - }
52 -
53 - .logo-overlay {
54 - position: absolute;
55 - top: 0;
56 - left: 0;
57 - width: 100%;
58 - height: 100%;
59 - display: flex;
60 - flex-direction: column;
61 - justify-content: space-between;
62 - align-items: center;
63 - padding: 10% 1%;
64 - box-sizing: border-box;
65 - pointer-events: none;
66 - }
67 -
68 - .logo-title {
69 - font-family: 'Pretendard', sans-serif;
70 - /* 51.5px at 500px container = 51.5/500*100 = 10.3cqw */
71 - font-size: 10.3cqw;
72 - font-weight: 600;
73 - color: #ffffff;
74 - letter-spacing: 0.20em;
75 - text-align: center;
76 - text-shadow: 0 0 20px rgba(255, 255, 255, 0.5);
77 - margin: 0;
78 - }
79 -
80 - .logo-subtitle {
81 - font-family: 'Pretendard', sans-serif;
82 - /* 18px at 500px container = 18/500*100 = 3.6cqw */
83 - font-size: 3.6cqw;
84 - font-weight: 600;
85 - color: #ffffff;
86 - letter-spacing: -0.05em;
87 - text-align: center;
88 - text-shadow: 0 0 15px rgba(255, 255, 255, 0.4);
89 - margin: 0;
90 - }
91 -
92 - .loading-bar-container {
93 - width: 300px;
94 - max-width: 80%;
95 - height: 4px;
96 - background: rgba(255, 255, 255, 0.1);
97 - border-radius: 2px;
98 - overflow: hidden;
99 - position: relative;
100 - }
101 -
102 - .loading-bar {
103 - height: 100%;
104 - background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
105 - border-radius: 2px;
106 - animation: loading 1.5s ease-in-out infinite;
107 - }
108 -
109 - @keyframes loading {
110 - 0% {
111 - width: 0%;
112 - margin-left: 0%;
113 - }
114 -
115 - 50% {
116 - width: 50%;
117 - margin-left: 25%;
118 - }
119 -
120 - 100% {
121 - width: 0%;
122 - margin-left: 100%;
123 - }
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%;
117 }
118
126 - .loading-text {
127 - color: rgba(255, 255, 255, 0.7);
128 - margin-top: 20px;
129 - font-size: 14px;
130 - text-align: center;
131 - transition: color 0.3s ease;
119 + 50% {
120 + width: 50%;
121 + margin-left: 25%;
122 }
123
134 - .loading-text.error {
135 - color: #ef4444;
124 + 100% {
125 + width: 0%;
126 + margin-left: 100%;
127 }
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 + }
141 </style>
138 -</head>
142 + </head>
143
140 -<body>
144 + <body>
145 <!-- Loading Screen -->
146 <div id="loading-screen">
143 - <div id="loading-logo">
144 - <video autoplay loop muted playsinline>
145 - <source src="/portal.mp4" type="video/mp4">
146 - </video>
147 - <div class="logo-overlay">
148 - <h1 class="logo-title">PORTAL</h1>
149 - <p class="logo-subtitle">LOCAL TO WEB. INSTANT ACCESS.</p>
150 - </div>
151 - </div>
152 - <div class="loading-bar-container" id="loading-bar-container">
153 - <div class="loading-bar"></div>
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>
154 </div>
155 - <div class="loading-text" id="loading-text">Initializing Portal Network...</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>
162 </div>
163
164 <script>
159 - // In-app browser detection and redirect handler
160 - (function () {
161 - const userAgent = navigator.userAgent.toLowerCase();
162 - const finalUrl = window.location.href;
163 -
164 - // Detect various in-app browsers
165 - const isKakao = /kakaotalk/i.test(userAgent);
166 - const isNaver = /naver/i.test(userAgent);
167 - const isFacebook = /fb|fbav|fban/i.test(userAgent);
168 - const isInstagram = /instagram/i.test(userAgent);
169 - const isLine = /line/i.test(userAgent);
170 - const isInAppBrowser = isKakao || isNaver || isFacebook || isInstagram || isLine;
171 - const isAndroid = /android/.test(userAgent);
172 - const isIOS = /ipad|iphone|ipod/.test(userAgent);
173 -
174 - if (!isInAppBrowser) {
175 - return; // Not in-app browser, proceed normally
176 - }
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 + }
184
178 - // Handle redirection to external browser
179 - if (isAndroid) {
180 - if (isKakao) {
181 - location.href = 'kakaotalk://web/openExternal?url=' + encodeURIComponent(finalUrl);
182 - setTimeout(() => {
183 - location.href = 'kakaotalk://inappbrowser/close';
184 - }, 10);
185 - } else {
186 - // Use Intent to open in Chrome or default browser
187 - const intentUrl = 'intent://' + finalUrl.replace(/^https?:\/\//, '') + '#Intent;scheme=https;action=android.intent.action.VIEW;end';
188 - location.href = intentUrl;
189 - }
190 - return; // Stop execution
191 - } else if (isIOS) {
192 - if (isKakao) {
193 - location.href = 'kakaotalk://web/openExternal?url=' + encodeURIComponent(finalUrl);
194 - // Set up auto-close listener for iOS KakaoTalk
195 - document.addEventListener("visibilitychange", () => {
196 - if (document.visibilityState == "visible") {
197 - location.href = 'kakaoweb://closeBrowser';
198 - }
199 - });
200 - } else {
201 - // For other iOS in-app browsers, show message in loading screen
202 - updateLoadingText('⚠️ Please open in external browser (Safari)');
203 - document.getElementById('loading-text').classList.add('error');
204 - }
205 - return; // Stop execution
206 - }
207 - })();
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 + })();
222 </script>
223 <script>
210 - // Update loading text
211 - function updateLoadingText(message, isError = false) {
212 - const loadingText = document.getElementById('loading-text');
213 - if (loadingText) {
214 - loadingText.textContent = message;
215 - if (isError) {
216 - loadingText.classList.add('error');
217 - } else {
218 - loadingText.classList.remove('error');
219 - }
220 - }
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";
246 }
247
223 - // Show error in loading text
224 - function showError(error, context = '') {
225 - let errorMessage = '';
226 - if (typeof error === 'string') {
227 - errorMessage = error;
228 - } else if (error instanceof Error) {
229 - errorMessage = `${error.message}`;
230 - } else {
231 - errorMessage = 'An error occurred';
232 - }
233 -
234 - if (context) {
235 - errorMessage = `⚠️ ${context}: ${errorMessage}`;
236 - } else {
237 - errorMessage = `⚠️ ${errorMessage}`;
238 - }
239 -
240 - updateLoadingText(errorMessage, true);
241 - console.error(`[Portal Error - ${context}]`, error);
248 + if (context) {
249 + errorMessage = `⚠️ ${context}: ${errorMessage}`;
250 + } else {
251 + errorMessage = `⚠️ ${errorMessage}`;
252 }
253
244 - // Global error handler
245 - window.addEventListener('error', (event) => {
246 - showError(event.error || event.message, 'Error');
247 - event.preventDefault();
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 + }
277 });
278 + }
279
250 - // Unhandled promise rejection handler
251 - window.addEventListener('unhandledrejection', (event) => {
252 - showError(event.reason, 'Promise Error');
253 - event.preventDefault();
254 - });
280 + async function registerServiceWorker() {
281 + let retryCount = 0;
282 + const maxRetries = 30; // 3초 (30 × 100ms)
283
256 - // Listen for errors from Service Worker
257 - if ('serviceWorker' in navigator) {
258 - navigator.serviceWorker.addEventListener('message', (event) => {
259 - if (event.data && event.data.type === 'SW_ERROR') {
260 - const error = event.data.error;
261 - showError(error.message, 'Service Worker Error');
262 - }
284 + const checkWASMReady = async () => {
285 + try {
286 + const resp = await fetch("/e8c2c70c-ec4a-40b2-b8af-d5638264f831", {
287 + cache: "no-store",
288 });
264 - }
265 -
266 - async function registerServiceWorker() {
267 - let retryCount = 0;
268 - const maxRetries = 30; // 3초 (30 × 100ms)
269 -
270 - const checkWASMReady = async () => {
271 - try {
272 - const resp = await fetch('/e8c2c70c-ec4a-40b2-b8af-d5638264f831', {
273 - cache: 'no-store'
274 - });
275 - const text = await resp.text();
276 -
277 - if (text === 'ACK-e8c2c70c-ec4a-40b2-b8af-d5638264f831') {
278 - updateLoadingText('Portal Network Ready!');
279 - setTimeout(() => {
280 - window.location.reload();
281 - }, 500);
282 - } else if (text === 'NAK-e8c2c70c-ec4a-40b2-b8af-d5638264f831') {
283 - retryCount++;
284 - if (retryCount > maxRetries) {
285 - throw new Error(`WASM initialization timeout after ${maxRetries} retries`);
286 - }
287 - updateLoadingText(`Initializing WASM... (${retryCount}/${maxRetries})`);
288 - setTimeout(checkWASMReady, 100);
289 - } else {
290 - if (text.includes('<!DOCTYPE') || text.includes('<html>')) {
291 - throw new Error('Service Worker not active - please refresh');
292 - } else {
293 - throw new Error(`Unexpected response: ${text.substring(0, 50)}`);
294 - }
295 - }
296 - } catch (error) {
297 - showError(error, 'Connection');
298 - }
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 + if (navigator.serviceWorker.controller) {
324 + console.log("[Portal] Service Worker already active");
325 + resolve();
326 + return;
327 }
328
301 - const waitForController = () => {
302 - return new Promise((resolve, reject) => {
303 - if (navigator.serviceWorker.controller) {
304 - console.log('[Portal] Service Worker already active');
305 - resolve();
306 - return;
307 - }
308 -
309 - let timeoutId;
310 - const onControllerChange = () => {
311 - console.log('[Portal] Service Worker activated');
312 - clearTimeout(timeoutId);
313 - navigator.serviceWorker.removeEventListener('controllerchange', onControllerChange);
314 - resolve();
315 - };
316 -
317 - navigator.serviceWorker.addEventListener('controllerchange', onControllerChange);
318 -
319 - timeoutId = setTimeout(() => {
320 - navigator.serviceWorker.removeEventListener('controllerchange', onControllerChange);
321 - reject(new Error('Service Worker activation timeout'));
322 - }, 5000);
323 - });
329 + let timeoutId;
330 + const onControllerChange = () => {
331 + console.log("[Portal] Service Worker activated");
332 + clearTimeout(timeoutId);
333 + navigator.serviceWorker.removeEventListener(
334 + "controllerchange",
335 + onControllerChange
336 + );
337 + resolve();
338 };
339
326 - try {
327 - if (!('serviceWorker' in navigator)) {
328 - throw new Error('Service Worker not supported in this browser');
329 - }
330 -
331 - updateLoadingText('Registering Service Worker...');
332 -
333 - const registration = await navigator.serviceWorker.register('/service-worker.js', {
334 - scope: '/'
335 - });
340 + navigator.serviceWorker.addEventListener(
341 + "controllerchange",
342 + onControllerChange
343 + );
344 +
345 + timeoutId = setTimeout(() => {
346 + navigator.serviceWorker.removeEventListener(
347 + "controllerchange",
348 + onControllerChange
349 + );
350 + reject(new Error("Service Worker activation timeout"));
351 + setTimeout(() => {
352 + location.reload();
353 + }, 500);
354 + }, 3000);
355 + });
356 + };
357 +
358 + try {
359 + if (!("serviceWorker" in navigator)) {
360 + throw new Error("Service Worker not supported in this browser");
361 + }
362 +
363 + updateLoadingText("Registering Service Worker...");
364 +
365 + const registration = await navigator.serviceWorker.register(
366 + "/service-worker.js",
367 + {
368 + scope: "/",
369 + }
370 + );
371
337 - console.log('[Portal] Service Worker registered');
372 + console.log("[Portal] Service Worker registered");
373
339 - updateLoadingText('Activating Service Worker...');
374 + updateLoadingText("Activating Service Worker...");
375
341 - await navigator.serviceWorker.ready;
342 - console.log('[Portal] Service Worker ready');
376 + await navigator.serviceWorker.ready;
377 + console.log("[Portal] Service Worker ready");
378
344 - updateLoadingText('Waiting for activation...');
379 + updateLoadingText("Waiting for activation...");
380
346 - await waitForController();
381 + await waitForController();
382
348 - updateLoadingText('Connecting to Portal Network...');
349 - console.log('[Portal] Checking WASM initialization...');
383 + updateLoadingText("Connecting to Portal Network...");
384 + console.log("[Portal] Checking WASM initialization...");
385
351 - setTimeout(checkWASMReady, 100);
352 - } catch (error) {
353 - showError(error, 'Initialization');
354 - }
386 + setTimeout(checkWASMReady, 100);
387 + } catch (error) {
388 + showError(error, "Initialization");
389 }
390 + }
391
357 - registerServiceWorker();
392 + registerServiceWorker();
393 </script>
359 -</body>
360 -
361 -</html>
\ No newline at end of file
394 + </body>
395 +</html>
cmd/webclient/main_js.go
+27
@@ -372,6 +372,12 @@ func (p *Proxy) handleWebSocketPolyfill(w http.ResponseWriter, r *http.Request)
372 return
373 }
374
375 + if strings.HasPrefix(path, "/sw-cgi/websocket/disconnect/") && r.Method == http.MethodPost {
376 + connID := strings.TrimPrefix(path, "/sw-cgi/websocket/disconnect/")
377 + p.handleDisconnect(w, r, connID)
378 + return
379 + }
380 +
381 http.Error(w, "Not found", http.StatusNotFound)
382 }
383
@@ -508,6 +514,27 @@ func (p *Proxy) handleSend(w http.ResponseWriter, r *http.Request, connID string
514 w.WriteHeader(http.StatusOK)
515 }
516
517 +func (p *Proxy) handleDisconnect(w http.ResponseWriter, r *http.Request, connID string) {
518 + log.Info().Str("connId", connID).Msg("Handling disconnect request")
519 +
520 + wsConn, ok := p.wsManager.GetConnection(connID)
521 + if !ok {
522 + // Connection already removed or doesn't exist - this is OK
523 + log.Debug().Str("connId", connID).Msg("Connection not found (already disconnected)")
524 + w.WriteHeader(http.StatusOK)
525 + return
526 + }
527 +
528 + // Close the WebSocket connection
529 + wsConn.Close()
530 +
531 + // Remove from manager
532 + p.wsManager.RemoveConnection(connID)
533 +
534 + log.Info().Str("connId", connID).Msg("WebSocket connection disconnected successfully")
535 + w.WriteHeader(http.StatusOK)
536 +}
537 +
538 func main() {
539 log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339})
540 var err error
cmd/webclient/polyfill.js
+411 -342
@@ -1,355 +1,424 @@
1 -(function() {
2 - 'use strict';
3 -
4 - // Capture reference to current script for later removal
5 - const currentScript = document.currentScript;
6 -
7 - // Save original WebSocket
8 - const NativeWebSocket = window.WebSocket;
9 -
10 - // WebSocket polyfill using HTTP
11 - class WebSocketPolyfill {
12 - constructor(url, protocols) {
13 - this.url = url;
14 - this.protocols = protocols;
15 - this.readyState = WebSocket.CONNECTING;
16 - this.bufferedAmount = 0;
17 - this.extensions = '';
18 - this.protocol = '';
19 - this.binaryType = 'blob';
20 -
21 - // Event handlers
22 - this.onopen = null;
23 - this.onmessage = null;
24 - this.onerror = null;
25 - this.onclose = null;
26 -
27 - // Internal state
28 - this._connId = null;
29 - this._sendQueue = [];
30 - this._isSending = false;
31 - this._pollInterval = null;
32 - this._isClosed = false;
33 -
34 - // Initialize connection
35 - this._connect();
36 - }
37 -
38 - async _connect() {
39 - try {
40 - // Send connect request
41 - const response = await fetch('/sw-cgi/websocket/connect', {
42 - method: 'POST',
43 - headers: {
44 - 'Content-Type': 'application/json',
45 - },
46 - body: JSON.stringify({
47 - url: this.url,
48 - protocols: Array.isArray(this.protocols) ? this.protocols : (this.protocols ? [this.protocols] : [])
49 - })
50 - });
51 -
52 - if (!response.ok) {
53 - throw new Error(`Connection failed: ${response.status} ${response.statusText}`);
54 - }
55 -
56 - const result = await response.json();
57 - this._connId = result.connId;
58 - this.protocol = result.protocol || '';
59 -
60 - // Update state
61 - this.readyState = WebSocket.OPEN;
62 -
63 - // Fire onopen event
64 - if (this.onopen) {
65 - this.onopen(new Event('open'));
66 - }
67 - this.dispatchEvent(new Event('open'));
68 -
69 - // Start polling for messages
70 - this._startPolling();
71 -
72 - } catch (error) {
73 - this._handleError(error);
74 - }
75 - }
76 -
77 - async _startPolling() {
78 - // Long polling: continuously fetch messages
79 - // Server will wait up to 5 seconds before responding
80 - while (!this._isClosed) {
81 - try {
82 - const response = await fetch(`/sw-cgi/websocket/poll/${this._connId}`, {
83 - method: 'GET'
84 - });
85 -
86 - if (!response.ok) {
87 - throw new Error(`Poll failed: ${response.status}`);
88 - }
89 -
90 - const result = await response.json();
91 -
92 - if (result.messages && Array.isArray(result.messages)) {
93 - for (const message of result.messages) {
94 - this._handleMessage(message);
95 - }
96 - }
97 -
98 - } catch (error) {
99 - if (!this._isClosed) {
100 - console.error('Polling error:', error);
101 - this._handleError(error);
102 - }
103 - break;
104 - }
105 - }
106 - }
107 -
108 - _handleMessage(message) {
109 - if (this.readyState !== WebSocket.OPEN) return;
110 -
111 - if (message.type === 'close') {
112 - this._handleClose(message.code || 1000, message.reason || '');
113 - return;
114 - }
115 -
116 - // Decode data from base64
117 - let data;
118 - try {
119 - const binaryString = atob(message.data);
120 - const bytes = new Uint8Array(binaryString.length);
121 - for (let i = 0; i < binaryString.length; i++) {
122 - bytes[i] = binaryString.charCodeAt(i);
123 - }
124 -
125 - // Use messageType from server to determine if text or binary
126 - if (message.messageType === 'text') {
127 - // Decode as text
128 - const decoder = new TextDecoder('utf-8');
129 - data = decoder.decode(bytes);
130 - } else {
131 - // Binary message - respect binaryType setting
132 - if (this.binaryType === 'blob') {
133 - data = new Blob([bytes]);
134 - } else {
135 - data = bytes.buffer;
136 - }
137 - }
138 - } catch (e) {
139 - console.error('Failed to decode message:', e);
140 - return;
141 - }
142 -
143 - // Create MessageEvent
144 - const event = new MessageEvent('message', {
145 - data: data,
146 - origin: new URL(this.url).origin
147 - });
148 -
149 - if (this.onmessage) {
150 - this.onmessage(event);
151 - }
152 - this.dispatchEvent(event);
153 - }
154 -
155 - _handleError(error) {
156 - console.error('WebSocket error:', error);
157 -
158 - const event = new Event('error');
159 - event.error = error;
160 -
161 - if (this.onerror) {
162 - this.onerror(event);
163 - }
164 - this.dispatchEvent(event);
165 -
166 - // Close connection after error
167 - this._handleClose(1006, error.message);
1 +(function () {
2 + "use strict";
3 +
4 + // Capture reference to current script for later removal
5 + const currentScript = document.currentScript;
6 +
7 + // Save original WebSocket
8 + const NativeWebSocket = window.WebSocket;
9 +
10 + // WebSocket polyfill using HTTP
11 + class WebSocketPolyfill {
12 + constructor(url, protocols) {
13 + this.url = url;
14 + this.protocols = protocols;
15 + this.readyState = WebSocket.CONNECTING;
16 + this.bufferedAmount = 0;
17 + this.extensions = "";
18 + this.protocol = "";
19 + this.binaryType = "blob";
20 +
21 + // Event handlers
22 + this.onopen = null;
23 + this.onmessage = null;
24 + this.onerror = null;
25 + this.onclose = null;
26 +
27 + // Internal state
28 + this._connId = null;
29 + this._sendQueue = [];
30 + this._isSending = false;
31 + this._pollInterval = null;
32 + this._isClosed = false;
33 +
34 + // Initialize connection
35 + this._connect();
36 + }
37 +
38 + async _connect() {
39 + console.log("[WebSocket Polyfill] Connecting to:", this.url);
40 + try {
41 + // Send connect request
42 + const response = await fetch("/sw-cgi/websocket/connect", {
43 + method: "POST",
44 + headers: {
45 + "Content-Type": "application/json",
46 + },
47 + body: JSON.stringify({
48 + url: this.url,
49 + protocols: Array.isArray(this.protocols)
50 + ? this.protocols
51 + : this.protocols
52 + ? [this.protocols]
53 + : [],
54 + }),
55 + });
56 +
57 + if (!response.ok) {
58 + throw new Error(
59 + `Connection failed: ${response.status} ${response.statusText}`
60 + );
61 }
169 -
170 - _handleClose(code, reason) {
171 - if (this._isClosed) return;
172 -
173 - this._isClosed = true;
174 - this.readyState = WebSocket.CLOSED;
175 -
176 - // Create CloseEvent
177 - const event = new CloseEvent('close', {
178 - code: code,
179 - reason: reason,
180 - wasClean: code === 1000
181 - });
182 -
183 - if (this.onclose) {
184 - this.onclose(event);
185 - }
186 - this.dispatchEvent(event);
62 +
63 + const result = await response.json();
64 + this._connId = result.connId;
65 + this.protocol = result.protocol || "";
66 +
67 + console.log(
68 + "[WebSocket Polyfill] Connected successfully, connId:",
69 + this._connId
70 + );
71 +
72 + // Update state
73 + this.readyState = WebSocket.OPEN;
74 +
75 + // Fire onopen event
76 + console.log("[WebSocket Polyfill] Dispatching open event");
77 + if (this.onopen) {
78 + this.onopen(new Event("open"));
79 }
188 -
189 - async send(data) {
190 - if (this.readyState !== WebSocket.OPEN) {
191 - throw new Error('WebSocket is not open');
192 - }
193 -
194 - // Add to queue
195 - this._sendQueue.push(data);
196 -
197 - // Process queue
198 - this._processSendQueue();
199 - }
200 -
201 - async _processSendQueue() {
202 - // Ensure only one send operation at a time
203 - if (this._isSending || this._sendQueue.length === 0) {
204 - return;
205 - }
206 -
207 - this._isSending = true;
208 -
209 - while (this._sendQueue.length > 0 && !this._isClosed) {
210 - const data = this._sendQueue.shift();
211 -
212 - try {
213 - let payload;
214 -
215 - if (typeof data === 'string') {
216 - payload = JSON.stringify({ type: 'text', data: data });
217 - } else if (data instanceof ArrayBuffer) {
218 - // Convert ArrayBuffer to base64
219 - const bytes = new Uint8Array(data);
220 - const base64 = btoa(String.fromCharCode(...bytes));
221 - payload = JSON.stringify({ type: 'binary', data: base64 });
222 - } else if (data instanceof Blob) {
223 - // Convert Blob to base64
224 - const arrayBuffer = await data.arrayBuffer();
225 - const bytes = new Uint8Array(arrayBuffer);
226 - const base64 = btoa(String.fromCharCode(...bytes));
227 - payload = JSON.stringify({ type: 'binary', data: base64 });
228 - } else {
229 - throw new Error('Unsupported data type');
230 - }
231 -
232 - const response = await fetch(`/sw-cgi/websocket/send/${this._connId}`, {
233 - method: 'POST',
234 - headers: {
235 - 'Content-Type': 'application/json',
236 - },
237 - body: payload
238 - });
239 -
240 - if (!response.ok) {
241 - throw new Error(`Send failed: ${response.status}`);
242 - }
243 -
244 - } catch (error) {
245 - console.error('Failed to send message:', error);
246 - this._handleError(error);
247 - break;
248 - }
249 - }
250 -
251 - this._isSending = false;
252 - }
253 -
254 - close(code = 1000, reason = '') {
255 - if (this._isClosed || this.readyState === WebSocket.CLOSING) {
256 - return;
257 - }
258 -
259 - this.readyState = WebSocket.CLOSING;
260 -
261 - // Send close request
262 - if (this._connId) {
263 - fetch(`/sw-cgi/websocket/send/${this._connId}`, {
264 - method: 'POST',
265 - headers: {
266 - 'Content-Type': 'application/json',
267 - },
268 - body: JSON.stringify({ type: 'close', code: code, reason: reason })
269 - }).catch(err => {
270 - console.error('Failed to send close frame:', err);
271 - });
272 - }
273 -
274 - // Handle close locally
275 - this._handleClose(code, reason);
276 - }
277 -
278 - // EventTarget implementation
279 - addEventListener(type, listener) {
280 - if (!this._listeners) {
281 - this._listeners = {};
80 + this.dispatchEvent(new Event("open"));
81 +
82 + // Start polling for messages
83 + this._startPolling();
84 + } catch (error) {
85 + this._handleError(error);
86 + }
87 + }
88 +
89 + async _startPolling() {
90 + console.log("[WebSocket Polyfill] Starting polling loop");
91 + // Long polling: continuously fetch messages
92 + // Server will wait up to 5 seconds before responding
93 + while (!this._isClosed) {
94 + try {
95 + const response = await fetch(
96 + `/sw-cgi/websocket/poll/${this._connId}`,
97 + {
98 + method: "GET",
99 }
283 - if (!this._listeners[type]) {
284 - this._listeners[type] = [];
100 + );
101 +
102 + if (!response.ok) {
103 + this._handleClose(1006, "abnormal closure");
104 + throw new Error(`Poll failed: ${response.status}`);
105 + }
106 +
107 + const result = await response.json();
108 +
109 + if (result.messages && Array.isArray(result.messages)) {
110 + for (const message of result.messages) {
111 + this._handleMessage(message);
112 + // Stop processing messages after close
113 + if (this._isClosed) {
114 + break;
115 + }
116 }
286 - this._listeners[type].push(listener);
117 + }
118 +
119 + // Exit polling loop if connection was closed
120 + if (this._isClosed) {
121 + break;
122 + }
123 + } catch (error) {
124 + if (!this._isClosed) {
125 + console.error("Polling error:", error);
126 + this._handleError(error);
127 + }
128 + break;
129 }
288 -
289 - removeEventListener(type, listener) {
290 - if (!this._listeners || !this._listeners[type]) {
291 - return;
292 - }
293 - const index = this._listeners[type].indexOf(listener);
294 - if (index !== -1) {
295 - this._listeners[type].splice(index, 1);
296 - }
130 + }
131 + }
132 +
133 + _handleMessage(message) {
134 + if (this.readyState !== WebSocket.OPEN) return;
135 +
136 + if (message.type === "close") {
137 + console.log(
138 + "[WebSocket Polyfill] Received close message, code:",
139 + message.code,
140 + "reason:",
141 + message.reason
142 + );
143 + this._handleClose(message.code || 1000, message.reason || "");
144 + return;
145 + }
146 +
147 + // Decode data from base64
148 + let data;
149 + try {
150 + const binaryString = atob(message.data);
151 + const bytes = new Uint8Array(binaryString.length);
152 + for (let i = 0; i < binaryString.length; i++) {
153 + bytes[i] = binaryString.charCodeAt(i);
154 }
298 -
299 - dispatchEvent(event) {
300 - if (!this._listeners || !this._listeners[event.type]) {
301 - return true;
302 - }
303 - this._listeners[event.type].forEach(listener => {
304 - listener.call(this, event);
305 - });
306 - return true;
155 +
156 + // Use messageType from server to determine if text or binary
157 + if (message.messageType === "text") {
158 + // Decode as text
159 + const decoder = new TextDecoder("utf-8");
160 + data = decoder.decode(bytes);
161 + } else {
162 + // Binary message - respect binaryType setting
163 + if (this.binaryType === "blob") {
164 + data = new Blob([bytes]);
165 + } else {
166 + data = bytes.buffer;
167 + }
168 }
169 + } catch (e) {
170 + console.error("Failed to decode message:", e);
171 + return;
172 + }
173 +
174 + // Create MessageEvent
175 + const event = new MessageEvent("message", {
176 + data: data,
177 + origin: new URL(this.url).origin,
178 + });
179 +
180 + if (this.onmessage) {
181 + this.onmessage(event);
182 + }
183 + this.dispatchEvent(event);
184 + }
185 +
186 + _handleError(error) {
187 + console.error("[WebSocket Polyfill] Error occurred:", error);
188 +
189 + const event = new Event("error");
190 + event.error = error;
191 +
192 + if (this.onerror) {
193 + this.onerror(event);
194 + }
195 + this.dispatchEvent(event);
196 +
197 + // Close connection after error
198 + this._handleClose(1006, error.message);
199 }
309 -
310 - // Check if URL is same-origin
311 - function isSameOrigin(url) {
200 +
201 + _handleClose(code, reason) {
202 + if (this._isClosed) return;
203 +
204 + console.log(
205 + "[WebSocket Polyfill] Closing connection, code:",
206 + code,
207 + "reason:",
208 + reason
209 + );
210 +
211 + this._isClosed = true;
212 + this.readyState = WebSocket.CLOSED;
213 +
214 + // Notify server about disconnection
215 + if (this._connId) {
216 + console.log(
217 + "[WebSocket Polyfill] Notifying server about disconnection"
218 + );
219 + fetch(`/sw-cgi/websocket/disconnect/${this._connId}`, {
220 + method: "POST",
221 + }).catch((err) => {
222 + console.error("Failed to notify server about disconnection:", err);
223 + });
224 + }
225 +
226 + // Create CloseEvent
227 + const event = new CloseEvent("close", {
228 + code: code,
229 + reason: reason,
230 + wasClean: code === 1000,
231 + });
232 +
233 + console.log(
234 + "[WebSocket Polyfill] Dispatching close event, wasClean:",
235 + code === 1000
236 + );
237 + if (this.onclose) {
238 + this.onclose(event);
239 + }
240 + this.dispatchEvent(event);
241 + }
242 +
243 + async send(data) {
244 + if (this.readyState !== WebSocket.OPEN) {
245 + throw new Error("WebSocket is not open");
246 + }
247 +
248 + // Add to queue
249 + this._sendQueue.push(data);
250 +
251 + // Process queue
252 + this._processSendQueue();
253 + }
254 +
255 + async _processSendQueue() {
256 + // Ensure only one send operation at a time
257 + if (this._isSending || this._sendQueue.length === 0) {
258 + return;
259 + }
260 +
261 + this._isSending = true;
262 +
263 + while (this._sendQueue.length > 0 && !this._isClosed) {
264 + const data = this._sendQueue.shift();
265 +
266 try {
313 - const wsUrl = new URL(url, window.location.href);
314 - const currentOrigin = window.location.origin;
315 -
316 - // Convert ws:// to http:// and wss:// to https:// for comparison
317 - let wsOrigin = wsUrl.origin;
318 - if (wsUrl.protocol === 'ws:') {
319 - wsOrigin = wsOrigin.replace('ws:', 'http:');
320 - } else if (wsUrl.protocol === 'wss:') {
321 - wsOrigin = wsOrigin.replace('wss:', 'https:');
267 + let payload;
268 +
269 + if (typeof data === "string") {
270 + payload = JSON.stringify({ type: "text", data: data });
271 + } else if (data instanceof ArrayBuffer) {
272 + // Convert ArrayBuffer to base64
273 + const bytes = new Uint8Array(data);
274 + const base64 = btoa(String.fromCharCode(...bytes));
275 + payload = JSON.stringify({ type: "binary", data: base64 });
276 + } else if (data instanceof Blob) {
277 + // Convert Blob to base64
278 + const arrayBuffer = await data.arrayBuffer();
279 + const bytes = new Uint8Array(arrayBuffer);
280 + const base64 = btoa(String.fromCharCode(...bytes));
281 + payload = JSON.stringify({ type: "binary", data: base64 });
282 + } else {
283 + throw new Error("Unsupported data type");
284 + }
285 +
286 + const response = await fetch(
287 + `/sw-cgi/websocket/send/${this._connId}`,
288 + {
289 + method: "POST",
290 + headers: {
291 + "Content-Type": "application/json",
292 + },
293 + body: payload,
294 }
323 -
324 - return wsOrigin === currentOrigin;
325 - } catch (e) {
326 - return false;
295 + );
296 +
297 + if (!response.ok) {
298 + throw new Error(`Send failed: ${response.status}`);
299 + }
300 + } catch (error) {
301 + console.error("Failed to send message:", error);
302 + this._handleError(error);
303 + break;
304 }
305 + }
306 +
307 + this._isSending = false;
308 }
329 -
330 - // Replace WebSocket with polyfill
331 - window.WebSocket = function(url, protocols) {
332 - // Use polyfill for same-origin, native for cross-origin
333 - if (isSameOrigin(url)) {
334 - console.log('[WebSocket Polyfill] Using HTTP-based polyfill for same-origin connection:', url);
335 - return new WebSocketPolyfill(url, protocols);
336 - } else {
337 - console.log('[WebSocket Polyfill] Using native WebSocket for cross-origin connection:', url);
338 - return new NativeWebSocket(url, protocols);
339 - }
340 - };
341 -
342 - // Copy static properties
343 - window.WebSocket.CONNECTING = NativeWebSocket.CONNECTING;
344 - window.WebSocket.OPEN = NativeWebSocket.OPEN;
345 - window.WebSocket.CLOSING = NativeWebSocket.CLOSING;
346 - window.WebSocket.CLOSED = NativeWebSocket.CLOSED;
347 -
348 - console.log('[WebSocket Polyfill] Initialized');
349 -
350 - // Remove the polyfill script tag after initialization
351 - if (currentScript && currentScript.parentNode) {
352 - currentScript.parentNode.removeChild(currentScript);
353 - console.log('[WebSocket Polyfill] Script tag removed');
309 +
310 + close(code = 1000, reason = "") {
311 + if (this._isClosed || this.readyState === WebSocket.CLOSING) {
312 + return;
313 + }
314 +
315 + console.log(
316 + "[WebSocket Polyfill] Client initiated close, code:",
317 + code,
318 + "reason:",
319 + reason
320 + );
321 +
322 + this.readyState = WebSocket.CLOSING;
323 +
324 + // Send close request
325 + if (this._connId) {
326 + fetch(`/sw-cgi/websocket/send/${this._connId}`, {
327 + method: "POST",
328 + headers: {
329 + "Content-Type": "application/json",
330 + },
331 + body: JSON.stringify({ type: "close", code: code, reason: reason }),
332 + }).catch((err) => {
333 + console.error("Failed to send close frame:", err);
334 + });
335 + }
336 +
337 + // Handle close locally
338 + this._handleClose(code, reason);
339 + }
340 +
341 + // EventTarget implementation
342 + addEventListener(type, listener) {
343 + if (!this._listeners) {
344 + this._listeners = {};
345 + }
346 + if (!this._listeners[type]) {
347 + this._listeners[type] = [];
348 + }
349 + this._listeners[type].push(listener);
350 + }
351 +
352 + removeEventListener(type, listener) {
353 + if (!this._listeners || !this._listeners[type]) {
354 + return;
355 + }
356 + const index = this._listeners[type].indexOf(listener);
357 + if (index !== -1) {
358 + this._listeners[type].splice(index, 1);
359 + }
360 + }
361 +
362 + dispatchEvent(event) {
363 + if (!this._listeners || !this._listeners[event.type]) {
364 + return true;
365 + }
366 + this._listeners[event.type].forEach((listener) => {
367 + listener.call(this, event);
368 + });
369 + return true;
370 + }
371 + }
372 +
373 + // Check if URL is same-origin
374 + function isSameOrigin(url) {
375 + try {
376 + const wsUrl = new URL(url, window.location.href);
377 + const currentOrigin = window.location.origin;
378 +
379 + // Convert ws:// to http:// and wss:// to https:// for comparison
380 + let wsOrigin = wsUrl.origin;
381 + if (wsUrl.protocol === "ws:") {
382 + wsOrigin = wsOrigin.replace("ws:", "http:");
383 + } else if (wsUrl.protocol === "wss:") {
384 + wsOrigin = wsOrigin.replace("wss:", "https:");
385 + }
386 +
387 + return wsOrigin === currentOrigin;
388 + } catch (e) {
389 + return false;
390 + }
391 + }
392 +
393 + // Replace WebSocket with polyfill
394 + window.WebSocket = function (url, protocols) {
395 + // Use polyfill for same-origin, native for cross-origin
396 + if (isSameOrigin(url)) {
397 + console.log(
398 + "[WebSocket Polyfill] Using HTTP-based polyfill for same-origin connection:",
399 + url
400 + );
401 + return new WebSocketPolyfill(url, protocols);
402 + } else {
403 + console.log(
404 + "[WebSocket Polyfill] Using native WebSocket for cross-origin connection:",
405 + url
406 + );
407 + return new NativeWebSocket(url, protocols);
408 }
355 -})();
\ No newline at end of file
409 + };
410 +
411 + // Copy static properties
412 + window.WebSocket.CONNECTING = NativeWebSocket.CONNECTING;
413 + window.WebSocket.OPEN = NativeWebSocket.OPEN;
414 + window.WebSocket.CLOSING = NativeWebSocket.CLOSING;
415 + window.WebSocket.CLOSED = NativeWebSocket.CLOSED;
416 +
417 + console.log("[WebSocket Polyfill] Initialized");
418 +
419 + // Remove the polyfill script tag after initialization
420 + if (currentScript && currentScript.parentNode) {
421 + currentScript.parentNode.removeChild(currentScript);
422 + console.log("[WebSocket Polyfill] Script tag removed");
423 + }
424 +})();
cmd/webclient/service-worker.js
+217 -178
@@ -8,220 +8,259 @@ let loading = false;
8 let initError = null;
9 let wasmManifest = null;
10 let currentCacheVersion = null;
11 +let _lastReload = Date.now();
12
13 // Fetch manifest to get current WASM filename
14 async function fetchManifest() {
14 - if (wasmManifest) return wasmManifest;
15 + if (wasmManifest) return wasmManifest;
16
16 - try {
17 - const response = await fetch(manifest_URL);
18 - if (!response.ok) {
19 - throw new Error(`Failed to fetch manifest: ${response.status}`);
20 - }
21 - wasmManifest = await response.json();
22 - currentCacheVersion = `WASM_Cache_${wasmManifest.hash}`;
23 - return wasmManifest;
24 - } catch (error) {
25 - console.error('[SW] Failed to fetch manifest:', error);
26 - throw error;
17 + try {
18 + const response = await fetch(manifest_URL);
19 + if (!response.ok) {
20 + throw new Error(`Failed to fetch manifest: ${response.status}`);
21 }
22 + wasmManifest = await response.json();
23 + currentCacheVersion = `WASM_Cache_${wasmManifest.hash}`;
24 + return wasmManifest;
25 + } catch (error) {
26 + console.error("[SW] Failed to fetch manifest:", error);
27 + throw error;
28 + }
29 }
30
31 // Send error to all clients
32 async function notifyClientsOfError(error) {
32 - const clients = await self.clients.matchAll();
33 - const errorMessage = {
34 - type: 'SW_ERROR',
35 - error: {
36 - name: error.name,
37 - message: error.message,
38 - stack: error.stack
39 - }
40 - };
33 + const clients = await self.clients.matchAll();
34 + const errorMessage = {
35 + type: "SW_ERROR",
36 + error: {
37 + name: error.name,
38 + message: error.message,
39 + stack: error.stack,
40 + },
41 + };
42
42 - for (const client of clients) {
43 - client.postMessage(errorMessage);
44 - }
43 + for (const client of clients) {
44 + client.postMessage(errorMessage);
45 + }
46 }
47
48 async function init() {
48 - if (loading) return;
49 - loading = true;
50 - try {
51 - await runWASM();
52 - initError = null;
53 - } catch (error) {
54 - console.error("[SW] Error initializing WASM:", error);
55 - initError = error;
56 - await notifyClientsOfError(error);
57 - throw error; // Re-throw to prevent further processing
58 - } finally {
59 - loading = false;
60 - }
49 + if (loading) return;
50 + loading = true;
51 + try {
52 + await runWASM();
53 + initError = null;
54 + } catch (error) {
55 + console.error("[SW] Error initializing WASM:", error);
56 + initError = error;
57 + await notifyClientsOfError(error);
58 + throw error; // Re-throw to prevent further processing
59 + } finally {
60 + // loading = false;
61 + }
62 }
63
64 async function runWASM() {
64 - if (typeof __go_jshttp !== 'undefined') {
65 - return;
66 - }
65 + if (typeof __go_jshttp !== "undefined") {
66 + return;
67 + }
68
68 - try {
69 - const manifest = await fetchManifest();
70 - // Use content-addressed path: /static/<sha256>.wasm
71 - const wasm_URL = `/static/${manifest.wasmFile}`;
69 + try {
70 + const manifest = await fetchManifest();
71 + // Use content-addressed path: /static/<sha256>.wasm
72 + const wasm_URL = `/static/${manifest.wasmFile}`;
73
73 - const go = new Go();
74 + const go = new Go();
75
75 - const cache = await caches.open(currentCacheVersion);
76 + const cache = await caches.open(currentCacheVersion);
77
77 - let wasm_file;
78 - const cache_wasm = await cache.match(wasm_URL);
78 + let wasm_file;
79 + const cache_wasm = await cache.match(wasm_URL);
80
80 - if (cache_wasm) {
81 - wasm_file = await cache_wasm.arrayBuffer();
82 - } else {
83 - const response = await fetch(wasm_URL);
84 - if (!response.ok) {
85 - throw new Error(`Failed to fetch WASM: ${response.status} ${response.statusText}`);
86 - }
87 - wasm_file = await response.arrayBuffer();
88 - }
81 + if (cache_wasm) {
82 + wasm_file = await cache_wasm.arrayBuffer();
83 + } else {
84 + const response = await fetch(wasm_URL);
85 + if (!response.ok) {
86 + throw new Error(
87 + `Failed to fetch WASM: ${response.status} ${response.statusText}`
88 + );
89 + }
90 + wasm_file = await response.arrayBuffer();
91 + }
92
90 - const instance = await WebAssembly.instantiate(wasm_file, go.importObject);
93 + const instance = await WebAssembly.instantiate(wasm_file, go.importObject);
94
92 - go.run(instance.instance);
95 + const onExit = () => {
96 + console.log("[SW] Go Program Exited");
97 + __go_jshttp = undefined;
98 + loading = false;
99 + };
100
94 - } catch (error) {
95 - console.error('[SW] WASM initialization failed:', error);
96 - throw new Error(`WASM Initialization: ${error.message}`);
97 - }
101 + go.run(instance.instance).then(onExit).catch((error) => {
102 + console.error("[SW] Go Program Error:", error);
103 + onExit();
104 + });
105 + } catch (error) {
106 + console.error("[SW] WASM initialization failed:", error);
107 + throw new Error(`WASM Initialization: ${error.message}`);
108 + }
109 }
110
100 -self.addEventListener('install', (e) => {
101 - self.skipWaiting();
102 - async function LoadCache() {
103 - try {
104 - const manifest = await fetchManifest();
105 - // Use content-addressed path: /static/<sha256>.wasm
106 - const wasm_URL = `/static/${manifest.wasmFile}`;
107 - const cache = await caches.open(currentCacheVersion);
108 -
109 - await cache.addAll([
110 - wasm_URL,
111 - wasm_exec_URL,
112 - manifest_URL,
113 - ]);
114 - } catch (error) {
115 - console.error('[SW] Cache loading failed:', error);
116 - throw new Error(`Cache Loading: ${error.message}`);
117 - }
111 +self.addEventListener("install", (e) => {
112 + console.log("[SW] Install event");
113 + async function LoadCache() {
114 + console.log("[SW] Loading cache");
115 + try {
116 + const manifest = await fetchManifest();
117 + // Use content-addressed path: /static/<sha256>.wasm
118 + const wasm_URL = `/static/${manifest.wasmFile}`;
119 + const cache = await caches.open(currentCacheVersion);
120 +
121 + await cache.addAll([wasm_URL, wasm_exec_URL, manifest_URL]);
122 + } catch (error) {
123 + console.error("[SW] Cache loading failed:", error);
124 + throw new Error(`Cache Loading: ${error.message}`);
125 }
119 - e.waitUntil(LoadCache());
126 + }
127 + e.waitUntil(LoadCache());
128 + self.skipWaiting();
129 });
130
122 -self.addEventListener('activate', (e) => {
123 - e.waitUntil((async () => {
124 - try {
125 - // Delete old caches
126 - const manifest = await fetchManifest();
127 - const cacheNames = await caches.keys();
128 - await Promise.all(
129 - cacheNames.map(cacheName => {
130 - if (cacheName !== currentCacheVersion && cacheName.startsWith('WASM_Cache_')) {
131 - console.log('[SW] Deleting old cache:', cacheName);
132 - return caches.delete(cacheName);
133 - }
134 - })
135 - );
136 -
137 - // Claim clients first to take control immediately
138 - await self.clients.claim();
139 -
140 - // Then initialize WASM in background (don't block activation)
141 - init().catch(error => {
142 - console.error('[SW] WASM initialization failed after activation:', error);
143 - notifyClientsOfError(error);
144 - });
131 +self.addEventListener("activate", (e) => {
132 + e.waitUntil(
133 + (async () => {
134 + try {
135 + // Delete old caches
136 + const manifest = await fetchManifest();
137 + const cacheNames = await caches.keys();
138 + await Promise.all(
139 + cacheNames.map((cacheName) => {
140 + if (
141 + cacheName !== currentCacheVersion &&
142 + cacheName.startsWith("WASM_Cache_")
143 + ) {
144 + console.log("[SW] Deleting old cache:", cacheName);
145 + return caches.delete(cacheName);
146 + }
147 + })
148 + );
149
146 - } catch (error) {
147 - console.error('[SW] Activation failed:', error);
148 - await notifyClientsOfError(error);
149 - }
150 - })());
150 + // Claim clients first to take control immediately
151 + await self.clients.claim();
152 +
153 + // Then initialize WASM in background (don't block activation)
154 + init().catch((error) => {
155 + console.error(
156 + "[SW] WASM initialization failed after activation:",
157 + error
158 + );
159 + notifyClientsOfError(error);
160 + });
161 + } catch (error) {
162 + console.error("[SW] Activation failed:", error);
163 + await notifyClientsOfError(error);
164 + }
165 + })()
166 + );
167 });
168
153 -self.addEventListener('fetch', (e) => {
154 - const url = new URL(e.request.url);
169 +self.addEventListener("fetch", (e) => {
170 + const url = new URL(e.request.url);
171
156 - // Skip non-origin requests
157 - if (url.origin !== self.location.origin) {
158 - e.respondWith(fetch(e.request));
159 - return;
160 - }
172 + // Skip non-origin requests
173 + if (url.origin !== self.location.origin) {
174 + e.respondWith(fetch(e.request));
175 + return;
176 + }
177
162 - // Health check endpoint - check WASM status
163 - if (url.pathname === '/e8c2c70c-ec4a-40b2-b8af-d5638264f831') {
164 - e.respondWith((async () => {
165 -
166 - // Try to initialize if not ready
167 - if (typeof __go_jshttp === 'undefined' && !loading) {
168 - try {
169 - await init();
170 - } catch (error) {
171 - console.error('[SW] Health check init failed:', error);
172 - }
173 - }
178 + // Health check endpoint - check WASM status
179 + if (url.pathname === "/e8c2c70c-ec4a-40b2-b8af-d5638264f831") {
180 + e.respondWith(
181 + (async () => {
182 + // Try to initialize if not ready
183 + if (typeof __go_jshttp === "undefined" && !loading) {
184 + try {
185 + await init();
186 + } catch (error) {
187 + console.error("[SW] Health check init failed:", error);
188 + }
189 + }
190
175 - // Return status based on WASM availability
176 - if (typeof __go_jshttp !== 'undefined') {
177 - return new Response("ACK-e8c2c70c-ec4a-40b2-b8af-d5638264f831", { status: 200 });
178 - } else {
179 - return new Response("NAK-e8c2c70c-ec4a-40b2-b8af-d5638264f831", { status: 503 });
180 - }
181 - })());
182 - return;
183 - }
191 + // Return status based on WASM availability
192 + if (typeof __go_jshttp !== "undefined") {
193 + return new Response("ACK-e8c2c70c-ec4a-40b2-b8af-d5638264f831", {
194 + status: 200,
195 + });
196 + } else {
197 + return new Response("NAK-e8c2c70c-ec4a-40b2-b8af-d5638264f831", {
198 + status: 503,
199 + });
200 + }
201 + })()
202 + );
203 + return;
204 + }
205
185 - // Serve portal.mp4 from cache or fetch from origin
186 - if (url.pathname === '/portal.mp4') {
187 - e.respondWith((async () => {
188 - try {
189 - await fetchManifest();
190 - // Try to get from cache first
191 - const cache = await caches.open(currentCacheVersion);
192 - const cachedResponse = await cache.match('/portal.mp4');
193 - if (cachedResponse) {
194 - return cachedResponse;
195 - }
196 -
197 - // Fetch from network and cache it
198 - const response = await fetch(e.request);
199 - if (response.ok) {
200 - cache.put('/portal.mp4', response.clone());
201 - }
202 - return response;
203 - } catch (error) {
204 - console.error('Failed to fetch portal.mp4:', error);
205 - return new Response('Not Found', { status: 404 });
206 - }
207 - })());
208 - return;
209 - }
206 + // Serve portal.mp4 from cache or fetch from origin
207 + if (url.pathname === "/portal.mp4") {
208 + console.log("[SW] Fetching portal.mp4");
209 + e.respondWith(
210 + (async () => {
211 + try {
212 + await fetchManifest();
213 + // Try to get from cache first
214 + const cache = await caches.open(currentCacheVersion);
215 + const cachedResponse = await cache.match("/portal.mp4");
216 + if (cachedResponse) {
217 + console.log("[SW] Found portal.mp4 in cache");
218 + return cachedResponse;
219 + }
220
211 - if (__go_jshttp) {
212 - e.respondWith((async () => {
213 - try {
214 - const resp = await __go_jshttp(e.request);
215 - return resp;
216 - } catch {
217 - __go_jshttp = undefined;
218 - await init();
219 - const resp = await __go_jshttp(e.request);
220 - return resp;
221 - }
222 - })());
223 - return;
221 + // Fetch from network and cache it
222 + const response = await fetch(e.request);
223 + if (response.ok) {
224 + console.log("[SW] Caching portal.mp4");
225 + cache.put("/portal.mp4", response.clone());
226 + }
227 + return response;
228 + } catch (error) {
229 + console.error("[SW] Failed to fetch portal.mp4:", error);
230 + return new Response("Not Found", { status: 404 });
231 + }
232 + })()
233 + );
234 + return;
235 + }
236 +
237 + if (typeof __go_jshttp === "undefined") {
238 + if (Date.now() - _lastReload > 1000) {
239 + _lastReload = Date.now();
240 + loading = false;
241 + } else {
242 + e.respondWith(
243 + new Response(
244 + "Sorry, Service Worker failed to process the request. Please refresh the page.",
245 + { status: 500 }
246 + )
247 + );
248 + return;
249 }
250 + }
251
226 - e.respondWith(new Response("Sorry, Service Worker failed to process the request. Please refresh the page."));
227 -});
\ No newline at end of file
252 + e.respondWith(
253 + (async () => {
254 + try {
255 + const resp = await __go_jshttp(e.request);
256 + return resp;
257 + } catch {
258 + __go_jshttp = undefined;
259 + await init();
260 + const resp = await __go_jshttp(e.request);
261 + return resp;
262 + }
263 + })()
264 + );
265 + return;
266 +});
sdk/sdk.go
+2 -2
@@ -58,7 +58,7 @@ type RDClientConfig struct {
58 BootstrapServers []string
59 Dialer func(context.Context, string) (io.ReadWriteCloser, error)
60 HealthCheckInterval time.Duration // Interval for health checks (default: 10 seconds)
61 - ReconnectMaxRetries int // Maximum reconnection attempts (default: 3, 0 = infinite)
61 + ReconnectMaxRetries int // Maximum reconnection attempts (default: 0 = infinite)
62 ReconnectInterval time.Duration // Interval between reconnection attempts (default: 5 seconds)
63 }
64
@@ -196,7 +196,7 @@ func NewClient(opt ...Option) (*RDClient, error) {
196 config := &RDClientConfig{
197 Dialer: webSocketDialer(),
198 HealthCheckInterval: 10 * time.Second,
199 - ReconnectMaxRetries: 9,
199 + ReconnectMaxRetries: 0,
200 ReconnectInterval: 5 * time.Second,
201 }
202