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