@scelar/nodepod 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/LICENSE +43 -0
  2. package/README.md +240 -0
  3. package/dist/child_process-BJOMsZje.js +8233 -0
  4. package/dist/child_process-BJOMsZje.js.map +1 -0
  5. package/dist/child_process-Cj8vOcuc.cjs +7434 -0
  6. package/dist/child_process-Cj8vOcuc.cjs.map +1 -0
  7. package/dist/index-Cb1Cgdnd.js +35308 -0
  8. package/dist/index-Cb1Cgdnd.js.map +1 -0
  9. package/dist/index-DsMGS-xc.cjs +37195 -0
  10. package/dist/index-DsMGS-xc.cjs.map +1 -0
  11. package/dist/index.cjs +65 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.mjs +59 -0
  14. package/dist/index.mjs.map +1 -0
  15. package/package.json +95 -0
  16. package/src/__tests__/smoke.test.ts +11 -0
  17. package/src/constants/cdn-urls.ts +18 -0
  18. package/src/constants/config.ts +236 -0
  19. package/src/cross-origin.ts +26 -0
  20. package/src/engine-factory.ts +176 -0
  21. package/src/engine-types.ts +56 -0
  22. package/src/helpers/byte-encoding.ts +39 -0
  23. package/src/helpers/digest.ts +9 -0
  24. package/src/helpers/event-loop.ts +96 -0
  25. package/src/helpers/wasm-cache.ts +133 -0
  26. package/src/iframe-sandbox.ts +141 -0
  27. package/src/index.ts +192 -0
  28. package/src/isolation-helpers.ts +148 -0
  29. package/src/memory-volume.ts +941 -0
  30. package/src/module-transformer.ts +368 -0
  31. package/src/packages/archive-extractor.ts +248 -0
  32. package/src/packages/browser-bundler.ts +284 -0
  33. package/src/packages/installer.ts +396 -0
  34. package/src/packages/registry-client.ts +131 -0
  35. package/src/packages/version-resolver.ts +411 -0
  36. package/src/polyfills/assert.ts +384 -0
  37. package/src/polyfills/async_hooks.ts +144 -0
  38. package/src/polyfills/buffer.ts +628 -0
  39. package/src/polyfills/child_process.ts +2288 -0
  40. package/src/polyfills/chokidar.ts +336 -0
  41. package/src/polyfills/cluster.ts +106 -0
  42. package/src/polyfills/console.ts +136 -0
  43. package/src/polyfills/constants.ts +123 -0
  44. package/src/polyfills/crypto.ts +885 -0
  45. package/src/polyfills/dgram.ts +87 -0
  46. package/src/polyfills/diagnostics_channel.ts +76 -0
  47. package/src/polyfills/dns.ts +134 -0
  48. package/src/polyfills/domain.ts +68 -0
  49. package/src/polyfills/esbuild.ts +854 -0
  50. package/src/polyfills/events.ts +276 -0
  51. package/src/polyfills/fs.ts +2888 -0
  52. package/src/polyfills/fsevents.ts +79 -0
  53. package/src/polyfills/http.ts +1449 -0
  54. package/src/polyfills/http2.ts +199 -0
  55. package/src/polyfills/https.ts +76 -0
  56. package/src/polyfills/inspector.ts +62 -0
  57. package/src/polyfills/lightningcss.ts +105 -0
  58. package/src/polyfills/module.ts +191 -0
  59. package/src/polyfills/net.ts +353 -0
  60. package/src/polyfills/os.ts +238 -0
  61. package/src/polyfills/path.ts +206 -0
  62. package/src/polyfills/perf_hooks.ts +102 -0
  63. package/src/polyfills/process.ts +690 -0
  64. package/src/polyfills/punycode.ts +159 -0
  65. package/src/polyfills/querystring.ts +93 -0
  66. package/src/polyfills/quic.ts +118 -0
  67. package/src/polyfills/readdirp.ts +229 -0
  68. package/src/polyfills/readline.ts +692 -0
  69. package/src/polyfills/repl.ts +134 -0
  70. package/src/polyfills/rollup.ts +119 -0
  71. package/src/polyfills/sea.ts +33 -0
  72. package/src/polyfills/sqlite.ts +78 -0
  73. package/src/polyfills/stream.ts +1620 -0
  74. package/src/polyfills/string_decoder.ts +25 -0
  75. package/src/polyfills/tailwindcss-oxide.ts +309 -0
  76. package/src/polyfills/test.ts +197 -0
  77. package/src/polyfills/timers.ts +32 -0
  78. package/src/polyfills/tls.ts +105 -0
  79. package/src/polyfills/trace_events.ts +50 -0
  80. package/src/polyfills/tty.ts +71 -0
  81. package/src/polyfills/url.ts +174 -0
  82. package/src/polyfills/util.ts +559 -0
  83. package/src/polyfills/v8.ts +126 -0
  84. package/src/polyfills/vm.ts +132 -0
  85. package/src/polyfills/volume-registry.ts +15 -0
  86. package/src/polyfills/wasi.ts +44 -0
  87. package/src/polyfills/worker_threads.ts +326 -0
  88. package/src/polyfills/ws.ts +595 -0
  89. package/src/polyfills/zlib.ts +881 -0
  90. package/src/request-proxy.ts +716 -0
  91. package/src/script-engine.ts +3375 -0
  92. package/src/sdk/nodepod-fs.ts +93 -0
  93. package/src/sdk/nodepod-process.ts +86 -0
  94. package/src/sdk/nodepod-terminal.ts +350 -0
  95. package/src/sdk/nodepod.ts +509 -0
  96. package/src/sdk/types.ts +70 -0
  97. package/src/shell/commands/bun.ts +121 -0
  98. package/src/shell/commands/directory.ts +297 -0
  99. package/src/shell/commands/file-ops.ts +525 -0
  100. package/src/shell/commands/git.ts +2142 -0
  101. package/src/shell/commands/node.ts +80 -0
  102. package/src/shell/commands/npm.ts +198 -0
  103. package/src/shell/commands/pm-types.ts +45 -0
  104. package/src/shell/commands/pnpm.ts +82 -0
  105. package/src/shell/commands/search.ts +264 -0
  106. package/src/shell/commands/shell-env.ts +352 -0
  107. package/src/shell/commands/text-processing.ts +1152 -0
  108. package/src/shell/commands/yarn.ts +84 -0
  109. package/src/shell/shell-builtins.ts +19 -0
  110. package/src/shell/shell-helpers.ts +250 -0
  111. package/src/shell/shell-interpreter.ts +514 -0
  112. package/src/shell/shell-parser.ts +429 -0
  113. package/src/shell/shell-types.ts +85 -0
  114. package/src/syntax-transforms.ts +561 -0
  115. package/src/threading/engine-worker.ts +64 -0
  116. package/src/threading/inline-worker.ts +372 -0
  117. package/src/threading/offload-types.ts +112 -0
  118. package/src/threading/offload-worker.ts +383 -0
  119. package/src/threading/offload.ts +271 -0
  120. package/src/threading/process-context.ts +92 -0
  121. package/src/threading/process-handle.ts +275 -0
  122. package/src/threading/process-manager.ts +956 -0
  123. package/src/threading/process-worker-entry.ts +854 -0
  124. package/src/threading/shared-vfs.ts +352 -0
  125. package/src/threading/sync-channel.ts +135 -0
  126. package/src/threading/task-queue.ts +177 -0
  127. package/src/threading/vfs-bridge.ts +231 -0
  128. package/src/threading/worker-pool.ts +233 -0
  129. package/src/threading/worker-protocol.ts +358 -0
  130. package/src/threading/worker-vfs.ts +218 -0
  131. package/src/types/externals.d.ts +38 -0
  132. package/src/types/fs-streams.ts +142 -0
  133. package/src/types/manifest.ts +17 -0
  134. package/src/worker-sandbox.ts +90 -0
@@ -0,0 +1,716 @@
1
+ // bridges Service Worker HTTP requests to virtual servers.
2
+ // intercepts browser fetches via SW and routes them to the http polyfill's server registry.
3
+
4
+ import type { CompletedResponse } from "./polyfills/http";
5
+ import {
6
+ Server,
7
+ setServerListenCallback,
8
+ setServerCloseCallback,
9
+ getServer,
10
+ encodeFrame,
11
+ decodeFrame,
12
+ } from "./polyfills/http";
13
+ import { EventEmitter } from "./polyfills/events";
14
+ import { Buffer } from "./polyfills/buffer";
15
+ import { bytesToBase64 } from "./helpers/byte-encoding";
16
+ import { TIMEOUTS, WS_OPCODE } from "./constants/config";
17
+ import { createHash } from "./polyfills/crypto";
18
+
19
+ const _enc = new TextEncoder();
20
+
21
+ export interface IVirtualServer {
22
+ listening: boolean;
23
+ address(): { port: number; address: string; family: string } | null;
24
+ dispatchRequest(
25
+ method: string,
26
+ url: string,
27
+ headers: Record<string, string>,
28
+ body?: Buffer | string,
29
+ ): Promise<CompletedResponse>;
30
+ }
31
+
32
+ export interface RegisteredServer {
33
+ server: Server | IVirtualServer;
34
+ port: number;
35
+ hostname: string;
36
+ }
37
+
38
+ export interface ProxyOptions {
39
+ baseUrl?: string;
40
+ onServerReady?: (port: number, url: string) => void;
41
+ }
42
+
43
+ export interface ServiceWorkerConfig {
44
+ swUrl?: string;
45
+ }
46
+
47
+ export { CompletedResponse };
48
+
49
+ export class RequestProxy extends EventEmitter {
50
+ static DEBUG = false;
51
+ private registry = new Map<number, RegisteredServer>();
52
+ private baseUrl: string;
53
+ private opts: ProxyOptions;
54
+ private channel: MessageChannel | null = null;
55
+ private swReady = false;
56
+ private heartbeat: ReturnType<typeof setInterval> | null = null;
57
+ private _processManager: any | null = null;
58
+ private _workerWsConns = new Map<string, { pid: number }>();
59
+ private _previewScript: string | null = null;
60
+
61
+ constructor(opts: ProxyOptions = {}) {
62
+ super();
63
+ this.opts = opts;
64
+ this.baseUrl =
65
+ typeof location !== "undefined"
66
+ ? opts.baseUrl || `${location.protocol}//${location.host}`
67
+ : opts.baseUrl || "http://localhost";
68
+
69
+ setServerListenCallback((port, srv) => this.register(srv, port));
70
+ setServerCloseCallback((port) => this.unregister(port));
71
+ }
72
+
73
+ setProcessManager(pm: any): void {
74
+ this._processManager = pm;
75
+ pm.on("ws-frame", (msg: any) => {
76
+ this._handleWorkerWsFrame(msg);
77
+ });
78
+ }
79
+
80
+ register(
81
+ server: Server | IVirtualServer,
82
+ port: number,
83
+ hostname = "0.0.0.0",
84
+ ): void {
85
+ this.registry.set(port, { server, port, hostname });
86
+ const url = this.serverUrl(port);
87
+ this.emit("server-ready", port, url);
88
+ this.opts.onServerReady?.(port, url);
89
+ this.notifySW("server-registered", { port, hostname });
90
+ }
91
+
92
+ unregister(port: number): void {
93
+ this.registry.delete(port);
94
+ this.notifySW("server-unregistered", { port });
95
+ }
96
+
97
+ // Sends a script to the Service Worker that gets injected into every HTML
98
+ // response served to preview iframes. Runs before any page content.
99
+ setPreviewScript(script: string | null): void {
100
+ this._previewScript = script;
101
+ this._sendPreviewScriptToSW();
102
+ }
103
+
104
+ setWatermark(enabled: boolean): void {
105
+ if (
106
+ typeof navigator !== "undefined" &&
107
+ navigator.serviceWorker?.controller
108
+ ) {
109
+ navigator.serviceWorker.controller.postMessage({
110
+ type: "set-watermark",
111
+ enabled,
112
+ });
113
+ }
114
+ }
115
+
116
+ private _sendPreviewScriptToSW(): void {
117
+ if (
118
+ typeof navigator !== "undefined" &&
119
+ navigator.serviceWorker?.controller
120
+ ) {
121
+ navigator.serviceWorker.controller.postMessage({
122
+ type: "set-preview-script",
123
+ script: this._previewScript,
124
+ });
125
+ }
126
+ }
127
+
128
+ serverUrl(port: number): string {
129
+ return `${this.baseUrl}/__virtual__/${port}`;
130
+ }
131
+
132
+ activePorts(): number[] {
133
+ return [...this.registry.keys()];
134
+ }
135
+
136
+ async handleRequest(
137
+ port: number,
138
+ method: string,
139
+ url: string,
140
+ headers: Record<string, string>,
141
+ body?: ArrayBuffer,
142
+ ): Promise<CompletedResponse> {
143
+ const entry = this.registry.get(port);
144
+ if (!entry) {
145
+ return {
146
+ statusCode: 503,
147
+ statusMessage: "Service Unavailable",
148
+ headers: { "Content-Type": "text/plain" },
149
+ body: Buffer.from(`No server on port ${port}`),
150
+ };
151
+ }
152
+ try {
153
+ const buf = body ? Buffer.from(new Uint8Array(body)) : undefined;
154
+ return await entry.server.dispatchRequest(method, url, headers, buf);
155
+ } catch (err) {
156
+ return {
157
+ statusCode: 500,
158
+ statusMessage: "Internal Server Error",
159
+ headers: { "Content-Type": "text/plain" },
160
+ body: Buffer.from(
161
+ err instanceof Error ? err.message : "Internal Server Error",
162
+ ),
163
+ };
164
+ }
165
+ }
166
+
167
+ async initServiceWorker(config?: ServiceWorkerConfig): Promise<void> {
168
+ if (!("serviceWorker" in navigator))
169
+ throw new Error("Service Workers not supported");
170
+
171
+ const swPath = config?.swUrl ?? "/__sw__.js";
172
+ // unregister old SWs and re-register with cache-busting to ensure latest __sw__.js
173
+ const existingRegs = await navigator.serviceWorker.getRegistrations();
174
+ for (const r of existingRegs) {
175
+ await r.unregister();
176
+ }
177
+ await new Promise(r => setTimeout(r, 100));
178
+
179
+ const controllerReady = new Promise<void>((res) => {
180
+ navigator.serviceWorker.addEventListener(
181
+ "controllerchange",
182
+ () => res(),
183
+ { once: true },
184
+ );
185
+ });
186
+
187
+ const swUrl = `${swPath}?v=${Date.now()}`;
188
+ const reg = await navigator.serviceWorker.register(swUrl, { scope: "/", updateViaCache: "none" });
189
+
190
+ const sw = reg.installing || reg.waiting || reg.active;
191
+ if (!sw) throw new Error("Service Worker registration failed");
192
+
193
+ await new Promise<void>((resolve) => {
194
+ if (sw.state === "activated") return resolve();
195
+ const check = () => {
196
+ if (sw.state === "activated") {
197
+ sw.removeEventListener("statechange", check);
198
+ resolve();
199
+ }
200
+ };
201
+ sw.addEventListener("statechange", check);
202
+ });
203
+
204
+ this.channel = new MessageChannel();
205
+ this.channel.port1.onmessage = this.onSWMessage.bind(this);
206
+ sw.postMessage({ type: "init", port: this.channel.port2 }, [
207
+ this.channel.port2,
208
+ ]);
209
+
210
+ await controllerReady;
211
+
212
+ const reinit = () => {
213
+ if (navigator.serviceWorker.controller) {
214
+ this.channel = new MessageChannel();
215
+ this.channel.port1.onmessage = this.onSWMessage.bind(this);
216
+ navigator.serviceWorker.controller.postMessage(
217
+ { type: "init", port: this.channel.port2 },
218
+ [this.channel.port2],
219
+ );
220
+ // Resend preview script to the new SW controller
221
+ if (this._previewScript !== null) {
222
+ this._sendPreviewScriptToSW();
223
+ }
224
+ }
225
+ };
226
+ navigator.serviceWorker.addEventListener("controllerchange", reinit);
227
+ navigator.serviceWorker.addEventListener("message", (ev) => {
228
+ if (ev.data?.type === "sw-needs-init") reinit();
229
+ });
230
+
231
+ this.heartbeat = setInterval(() => {
232
+ this.channel?.port1.postMessage({ type: "keepalive" });
233
+ }, TIMEOUTS.SW_HEARTBEAT);
234
+
235
+ this.swReady = true;
236
+ this.emit("sw-ready");
237
+
238
+ this._startWsBridge();
239
+ }
240
+
241
+ // strip /__preview__/{port} prefix from SW URLs if present
242
+ private _normalizeSwUrl(url: string, headers: Record<string, string>): string | null {
243
+ // Strip /__preview__/{port} prefix (fixes RSC HMR .rsc requests etc.)
244
+ const ppMatch = url.match(/^\/__preview__\/\d+(.*)?$/);
245
+ if (ppMatch) {
246
+ let stripped = ppMatch[1] || "/";
247
+ if (stripped[0] !== "/") stripped = "/" + stripped;
248
+ const qIdx = url.indexOf("?");
249
+ if (qIdx >= 0 && !stripped.includes("?")) {
250
+ stripped += url.slice(qIdx);
251
+ }
252
+ return stripped;
253
+ }
254
+ return url;
255
+ }
256
+
257
+ private async onSWMessage(event: MessageEvent): Promise<void> {
258
+ const { type, id, data } = event.data;
259
+ RequestProxy.DEBUG &&
260
+ console.log("[RequestProxy] SW:", type, id, data?.url);
261
+
262
+ if (type === "request") {
263
+ const { port, method, headers, body, streaming, originalUrl } = data;
264
+ let url: string = data.url;
265
+
266
+ const normalized = this._normalizeSwUrl(url, headers);
267
+ if (normalized !== null && normalized !== url) {
268
+ url = normalized;
269
+ }
270
+
271
+ try {
272
+ if (streaming) {
273
+ await this.handleStreaming(id, port, method, url, headers, body);
274
+ } else {
275
+ const resp = await this.handleRequest(
276
+ port,
277
+ method,
278
+ url,
279
+ headers,
280
+ body,
281
+ );
282
+ // 404 + original URL = try fetching from the real network as fallback
283
+ // (handles cross-origin resources like Google Fonts, CDN assets, etc.)
284
+ if (resp.statusCode === 404 && originalUrl) {
285
+ try {
286
+ const origUrl = new URL(originalUrl);
287
+ const isLocalhost = origUrl.hostname === "localhost" ||
288
+ origUrl.hostname === "127.0.0.1" ||
289
+ origUrl.hostname === "0.0.0.0";
290
+ if (!isLocalhost) {
291
+ const fallbackResp = await fetch(originalUrl);
292
+ const fallbackBody = await fallbackResp.arrayBuffer();
293
+ const fallbackHeaders: Record<string, string> = {};
294
+ fallbackResp.headers.forEach((v, k) => {
295
+ fallbackHeaders[k] = v;
296
+ });
297
+ const fallbackB64 = fallbackBody.byteLength > 0
298
+ ? bytesToBase64(new Uint8Array(fallbackBody))
299
+ : "";
300
+ this.channel?.port1.postMessage({
301
+ type: "response",
302
+ id,
303
+ data: {
304
+ statusCode: fallbackResp.status,
305
+ statusMessage: fallbackResp.statusText || "OK",
306
+ headers: fallbackHeaders,
307
+ bodyBase64: fallbackB64,
308
+ },
309
+ });
310
+ return;
311
+ }
312
+ } catch (fallbackErr) {
313
+ }
314
+ }
315
+
316
+ let bodyB64 = "";
317
+ if (resp.body?.length) {
318
+ const bytes =
319
+ resp.body instanceof Uint8Array ? resp.body : new Uint8Array(0);
320
+ bodyB64 = bytesToBase64(bytes);
321
+ }
322
+ this.channel?.port1.postMessage({
323
+ type: "response",
324
+ id,
325
+ data: {
326
+ statusCode: resp.statusCode,
327
+ statusMessage: resp.statusMessage,
328
+ headers: resp.headers,
329
+ bodyBase64: bodyB64,
330
+ },
331
+ });
332
+ }
333
+ } catch (err) {
334
+ this.channel?.port1.postMessage({
335
+ type: "response",
336
+ id,
337
+ error: err instanceof Error ? err.message : "Unknown error",
338
+ });
339
+ }
340
+ }
341
+ }
342
+
343
+ private async handleStreaming(
344
+ id: number,
345
+ port: number,
346
+ method: string,
347
+ url: string,
348
+ headers: Record<string, string>,
349
+ body?: ArrayBuffer,
350
+ ): Promise<void> {
351
+ const entry = this.registry.get(port);
352
+ if (!entry) {
353
+ this.channel?.port1.postMessage({
354
+ type: "stream-start",
355
+ id,
356
+ data: {
357
+ statusCode: 503,
358
+ statusMessage: "Service Unavailable",
359
+ headers: {},
360
+ },
361
+ });
362
+ this.channel?.port1.postMessage({ type: "stream-end", id });
363
+ return;
364
+ }
365
+
366
+ const srv = entry.server as any;
367
+ if (typeof srv.handleStreamingRequest === "function") {
368
+ const buf = body ? Buffer.from(new Uint8Array(body)) : undefined;
369
+ await srv.handleStreamingRequest(
370
+ method,
371
+ url,
372
+ headers,
373
+ buf,
374
+ (
375
+ statusCode: number,
376
+ statusMessage: string,
377
+ h: Record<string, string>,
378
+ ) => {
379
+ this.channel?.port1.postMessage({
380
+ type: "stream-start",
381
+ id,
382
+ data: { statusCode, statusMessage, headers: h },
383
+ });
384
+ },
385
+ (chunk: string | Uint8Array) => {
386
+ const bytes = typeof chunk === "string" ? _enc.encode(chunk) : chunk;
387
+ this.channel?.port1.postMessage({
388
+ type: "stream-chunk",
389
+ id,
390
+ data: { chunkBase64: bytesToBase64(bytes) },
391
+ });
392
+ },
393
+ () => {
394
+ this.channel?.port1.postMessage({ type: "stream-end", id });
395
+ },
396
+ );
397
+ } else {
398
+ const buf = body ? Buffer.from(new Uint8Array(body)) : undefined;
399
+ const resp = await entry.server.dispatchRequest(
400
+ method,
401
+ url,
402
+ headers,
403
+ buf,
404
+ );
405
+ this.channel?.port1.postMessage({
406
+ type: "stream-start",
407
+ id,
408
+ data: {
409
+ statusCode: resp.statusCode,
410
+ statusMessage: resp.statusMessage,
411
+ headers: resp.headers,
412
+ },
413
+ });
414
+ if (resp.body?.length) {
415
+ const bytes =
416
+ resp.body instanceof Uint8Array ? resp.body : new Uint8Array(0);
417
+ this.channel?.port1.postMessage({
418
+ type: "stream-chunk",
419
+ id,
420
+ data: { chunkBase64: bytesToBase64(bytes) },
421
+ });
422
+ }
423
+ this.channel?.port1.postMessage({ type: "stream-end", id });
424
+ }
425
+ }
426
+
427
+ // ---- WebSocket bridge ----
428
+
429
+ private _wsBridge: BroadcastChannel | null = null;
430
+ private _wsConns = new Map<
431
+ string,
432
+ { socket: import("./polyfills/net").TcpSocket; cleanup: () => void }
433
+ >();
434
+
435
+ // listens on BroadcastChannel "nodepod-ws" for connect/send/close from preview
436
+ // iframes, dispatches WS upgrade events on the virtual server, relays frames.
437
+ private _startWsBridge(): void {
438
+ if (typeof BroadcastChannel === "undefined") return;
439
+ if (this._wsBridge) return;
440
+
441
+ this._wsBridge = new BroadcastChannel("nodepod-ws");
442
+ this._wsBridge.onmessage = (ev: MessageEvent) => {
443
+ const d = ev.data;
444
+ if (!d || !d.kind) return;
445
+
446
+ if (d.kind === "ws-connect") {
447
+ this._handleWsConnect(d.uid, d.port, d.path, d.protocols);
448
+ } else if (d.kind === "ws-send") {
449
+ this._handleWsSend(d.uid, d.data, d.type);
450
+ } else if (d.kind === "ws-close") {
451
+ this._handleWsClose(d.uid, d.code, d.reason);
452
+ }
453
+ };
454
+ }
455
+
456
+ private _handleWsConnect(
457
+ uid: string,
458
+ port: number,
459
+ path: string,
460
+ protocols?: string,
461
+ ): void {
462
+ const server = getServer(port);
463
+
464
+ const wsKey = btoa(
465
+ String.fromCharCode(...crypto.getRandomValues(new Uint8Array(16))),
466
+ );
467
+
468
+ const headers: Record<string, string> = {
469
+ upgrade: "websocket",
470
+ connection: "Upgrade",
471
+ "sec-websocket-key": wsKey,
472
+ "sec-websocket-version": "13",
473
+ host: `localhost:${port}`,
474
+ };
475
+ if (protocols) headers["sec-websocket-protocol"] = protocols;
476
+
477
+ // no local server -- try routing through ProcessManager (worker mode)
478
+ if (!server) {
479
+ if (this._processManager) {
480
+ const pid = this._processManager.dispatchWsUpgrade(port, uid, path || "/", headers);
481
+ if (pid >= 0) {
482
+ this._workerWsConns.set(uid, { pid });
483
+ return;
484
+ }
485
+ }
486
+ this._wsBridge?.postMessage({
487
+ kind: "ws-error",
488
+ uid,
489
+ message: `No server on port ${port}`,
490
+ });
491
+ return;
492
+ }
493
+
494
+ const { socket } = server.dispatchUpgrade(path || "/", headers);
495
+ const bridge = this._wsBridge!;
496
+
497
+ let outboundBuf = new Uint8Array(0);
498
+ let handshakeDone = false;
499
+
500
+ // intercept socket.write to decode WS frames from server and relay to iframe
501
+ socket.write = ((
502
+ chunk: Uint8Array | string,
503
+ encOrCb?: BufferEncoding | ((err?: Error | null) => void),
504
+ cb?: (err?: Error | null) => void,
505
+ ): boolean => {
506
+ const raw =
507
+ typeof chunk === "string" ? Buffer.from(chunk) : new Uint8Array(chunk);
508
+ const fn = typeof encOrCb === "function" ? encOrCb : cb;
509
+
510
+ if (!handshakeDone) {
511
+ const text = new TextDecoder().decode(raw);
512
+ if (text.startsWith("HTTP/1.1 101")) {
513
+ handshakeDone = true;
514
+ bridge.postMessage({ kind: "ws-open", uid });
515
+ if (fn) queueMicrotask(() => fn(null));
516
+ return true;
517
+ }
518
+ }
519
+
520
+ const merged = new Uint8Array(outboundBuf.length + raw.length);
521
+ merged.set(outboundBuf, 0);
522
+ merged.set(raw, outboundBuf.length);
523
+ outboundBuf = merged;
524
+
525
+ while (outboundBuf.length >= 2) {
526
+ const frame = decodeFrame(outboundBuf);
527
+ if (!frame) break;
528
+ outboundBuf = outboundBuf.slice(frame.consumed);
529
+
530
+ switch (frame.op) {
531
+ case WS_OPCODE.TEXT: {
532
+ const text = new TextDecoder().decode(frame.data);
533
+ bridge.postMessage({
534
+ kind: "ws-message",
535
+ uid,
536
+ data: text,
537
+ type: "text",
538
+ });
539
+ break;
540
+ }
541
+ case WS_OPCODE.BINARY:
542
+ bridge.postMessage({
543
+ kind: "ws-message",
544
+ uid,
545
+ data: Array.from(frame.data),
546
+ type: "binary",
547
+ });
548
+ break;
549
+ case WS_OPCODE.CLOSE: {
550
+ const code =
551
+ frame.data.length >= 2
552
+ ? (frame.data[0] << 8) | frame.data[1]
553
+ : 1000;
554
+ bridge.postMessage({ kind: "ws-closed", uid, code });
555
+ break;
556
+ }
557
+ case WS_OPCODE.PING:
558
+ socket._feedData(
559
+ Buffer.from(encodeFrame(WS_OPCODE.PONG, frame.data, true)),
560
+ );
561
+ break;
562
+ }
563
+ }
564
+
565
+ if (fn) queueMicrotask(() => fn(null));
566
+ return true;
567
+ }) as any;
568
+
569
+ const cleanup = () => {
570
+ outboundBuf = new Uint8Array(0);
571
+ try { socket.destroy(); } catch { /* */ }
572
+ };
573
+ this._wsConns.set(uid, { socket, cleanup });
574
+ }
575
+
576
+ private _handleWorkerWsFrame(msg: any): void {
577
+ const bridge = this._wsBridge;
578
+ if (!bridge) return;
579
+ const uid = msg.uid;
580
+
581
+ switch (msg.kind) {
582
+ case "open":
583
+ bridge.postMessage({ kind: "ws-open", uid });
584
+ break;
585
+ case "text":
586
+ bridge.postMessage({ kind: "ws-message", uid, data: msg.data, type: "text" });
587
+ break;
588
+ case "binary":
589
+ bridge.postMessage({ kind: "ws-message", uid, data: msg.bytes, type: "binary" });
590
+ break;
591
+ case "close":
592
+ bridge.postMessage({ kind: "ws-closed", uid, code: msg.code || 1000 });
593
+ this._workerWsConns.delete(uid);
594
+ break;
595
+ case "error":
596
+ bridge.postMessage({ kind: "ws-error", uid, message: msg.message });
597
+ this._workerWsConns.delete(uid);
598
+ break;
599
+ }
600
+ }
601
+
602
+ private _handleWsSend(
603
+ uid: string,
604
+ data: unknown,
605
+ type?: string,
606
+ ): void {
607
+ const workerConn = this._workerWsConns.get(uid);
608
+ if (workerConn && this._processManager) {
609
+ let payload: Uint8Array;
610
+ let op: number;
611
+ if (type === "binary" && Array.isArray(data)) {
612
+ payload = new Uint8Array(data);
613
+ op = WS_OPCODE.BINARY;
614
+ } else {
615
+ payload = new TextEncoder().encode(String(data));
616
+ op = WS_OPCODE.TEXT;
617
+ }
618
+ const frame = encodeFrame(op, payload, true);
619
+ this._processManager.dispatchWsData(workerConn.pid, uid, Array.from(new Uint8Array(frame)));
620
+ return;
621
+ }
622
+
623
+ const conn = this._wsConns.get(uid);
624
+ if (!conn) return;
625
+
626
+ let payload: Uint8Array;
627
+ let op: number;
628
+ if (type === "binary" && Array.isArray(data)) {
629
+ payload = new Uint8Array(data);
630
+ op = WS_OPCODE.BINARY;
631
+ } else {
632
+ payload = new TextEncoder().encode(String(data));
633
+ op = WS_OPCODE.TEXT;
634
+ }
635
+ const frame = encodeFrame(op, payload, true);
636
+ conn.socket._feedData(Buffer.from(frame));
637
+ }
638
+
639
+ private _handleWsClose(uid: string, code?: number, reason?: string): void {
640
+ const workerConn = this._workerWsConns.get(uid);
641
+ if (workerConn && this._processManager) {
642
+ this._processManager.dispatchWsClose(workerConn.pid, uid, code ?? 1000);
643
+ this._workerWsConns.delete(uid);
644
+ return;
645
+ }
646
+
647
+ const conn = this._wsConns.get(uid);
648
+ if (!conn) return;
649
+
650
+ const codeBuf = new Uint8Array(2);
651
+ codeBuf[0] = ((code ?? 1000) >> 8) & 0xff;
652
+ codeBuf[1] = (code ?? 1000) & 0xff;
653
+ const frame = encodeFrame(WS_OPCODE.CLOSE, codeBuf, true);
654
+ try { conn.socket._feedData(Buffer.from(frame)); } catch { /* */ }
655
+
656
+ conn.cleanup();
657
+ this._wsConns.delete(uid);
658
+ }
659
+
660
+ private notifySW(type: string, data: unknown): void {
661
+ if (this.swReady && this.channel)
662
+ this.channel.port1.postMessage({ type, data });
663
+ }
664
+
665
+ createFetchHandler(): (req: Request) => Promise<Response> {
666
+ return async (req: Request): Promise<Response> => {
667
+ const parsed = new URL(req.url);
668
+ const match = parsed.pathname.match(/^\/__virtual__\/(\d+)(\/.*)?$/);
669
+ if (!match) throw new Error("Not a virtual server request");
670
+
671
+ const port = parseInt(match[1], 10);
672
+ const path = match[2] || "/";
673
+ const hdrs: Record<string, string> = {};
674
+ req.headers.forEach((v, k) => {
675
+ hdrs[k] = v;
676
+ });
677
+ let reqBody: ArrayBuffer | undefined;
678
+ if (req.method !== "GET" && req.method !== "HEAD")
679
+ reqBody = await req.arrayBuffer();
680
+
681
+ const resp = await this.handleRequest(
682
+ port,
683
+ req.method,
684
+ path + parsed.search,
685
+ hdrs,
686
+ reqBody,
687
+ );
688
+ let body: BodyInit | null = null;
689
+ if (resp.body instanceof Uint8Array) {
690
+ body = new Uint8Array(resp.body.buffer as ArrayBuffer, resp.body.byteOffset, resp.body.byteLength) as Uint8Array<ArrayBuffer>;
691
+ } else if (typeof resp.body === "string") {
692
+ body = resp.body;
693
+ }
694
+ return new Response(body, {
695
+ status: resp.statusCode,
696
+ statusText: resp.statusMessage,
697
+ headers: resp.headers,
698
+ });
699
+ };
700
+ }
701
+ }
702
+
703
+ // ── Singleton ──
704
+
705
+ let instance: RequestProxy | null = null;
706
+
707
+ export function getProxyInstance(opts?: ProxyOptions): RequestProxy {
708
+ if (!instance) instance = new RequestProxy(opts);
709
+ return instance;
710
+ }
711
+
712
+ export function resetProxy(): void {
713
+ instance = null;
714
+ }
715
+
716
+ export default RequestProxy;