@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.
- package/LICENSE +43 -0
- package/README.md +240 -0
- package/dist/child_process-BJOMsZje.js +8233 -0
- package/dist/child_process-BJOMsZje.js.map +1 -0
- package/dist/child_process-Cj8vOcuc.cjs +7434 -0
- package/dist/child_process-Cj8vOcuc.cjs.map +1 -0
- package/dist/index-Cb1Cgdnd.js +35308 -0
- package/dist/index-Cb1Cgdnd.js.map +1 -0
- package/dist/index-DsMGS-xc.cjs +37195 -0
- package/dist/index-DsMGS-xc.cjs.map +1 -0
- package/dist/index.cjs +65 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.mjs +59 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +95 -0
- package/src/__tests__/smoke.test.ts +11 -0
- package/src/constants/cdn-urls.ts +18 -0
- package/src/constants/config.ts +236 -0
- package/src/cross-origin.ts +26 -0
- package/src/engine-factory.ts +176 -0
- package/src/engine-types.ts +56 -0
- package/src/helpers/byte-encoding.ts +39 -0
- package/src/helpers/digest.ts +9 -0
- package/src/helpers/event-loop.ts +96 -0
- package/src/helpers/wasm-cache.ts +133 -0
- package/src/iframe-sandbox.ts +141 -0
- package/src/index.ts +192 -0
- package/src/isolation-helpers.ts +148 -0
- package/src/memory-volume.ts +941 -0
- package/src/module-transformer.ts +368 -0
- package/src/packages/archive-extractor.ts +248 -0
- package/src/packages/browser-bundler.ts +284 -0
- package/src/packages/installer.ts +396 -0
- package/src/packages/registry-client.ts +131 -0
- package/src/packages/version-resolver.ts +411 -0
- package/src/polyfills/assert.ts +384 -0
- package/src/polyfills/async_hooks.ts +144 -0
- package/src/polyfills/buffer.ts +628 -0
- package/src/polyfills/child_process.ts +2288 -0
- package/src/polyfills/chokidar.ts +336 -0
- package/src/polyfills/cluster.ts +106 -0
- package/src/polyfills/console.ts +136 -0
- package/src/polyfills/constants.ts +123 -0
- package/src/polyfills/crypto.ts +885 -0
- package/src/polyfills/dgram.ts +87 -0
- package/src/polyfills/diagnostics_channel.ts +76 -0
- package/src/polyfills/dns.ts +134 -0
- package/src/polyfills/domain.ts +68 -0
- package/src/polyfills/esbuild.ts +854 -0
- package/src/polyfills/events.ts +276 -0
- package/src/polyfills/fs.ts +2888 -0
- package/src/polyfills/fsevents.ts +79 -0
- package/src/polyfills/http.ts +1449 -0
- package/src/polyfills/http2.ts +199 -0
- package/src/polyfills/https.ts +76 -0
- package/src/polyfills/inspector.ts +62 -0
- package/src/polyfills/lightningcss.ts +105 -0
- package/src/polyfills/module.ts +191 -0
- package/src/polyfills/net.ts +353 -0
- package/src/polyfills/os.ts +238 -0
- package/src/polyfills/path.ts +206 -0
- package/src/polyfills/perf_hooks.ts +102 -0
- package/src/polyfills/process.ts +690 -0
- package/src/polyfills/punycode.ts +159 -0
- package/src/polyfills/querystring.ts +93 -0
- package/src/polyfills/quic.ts +118 -0
- package/src/polyfills/readdirp.ts +229 -0
- package/src/polyfills/readline.ts +692 -0
- package/src/polyfills/repl.ts +134 -0
- package/src/polyfills/rollup.ts +119 -0
- package/src/polyfills/sea.ts +33 -0
- package/src/polyfills/sqlite.ts +78 -0
- package/src/polyfills/stream.ts +1620 -0
- package/src/polyfills/string_decoder.ts +25 -0
- package/src/polyfills/tailwindcss-oxide.ts +309 -0
- package/src/polyfills/test.ts +197 -0
- package/src/polyfills/timers.ts +32 -0
- package/src/polyfills/tls.ts +105 -0
- package/src/polyfills/trace_events.ts +50 -0
- package/src/polyfills/tty.ts +71 -0
- package/src/polyfills/url.ts +174 -0
- package/src/polyfills/util.ts +559 -0
- package/src/polyfills/v8.ts +126 -0
- package/src/polyfills/vm.ts +132 -0
- package/src/polyfills/volume-registry.ts +15 -0
- package/src/polyfills/wasi.ts +44 -0
- package/src/polyfills/worker_threads.ts +326 -0
- package/src/polyfills/ws.ts +595 -0
- package/src/polyfills/zlib.ts +881 -0
- package/src/request-proxy.ts +716 -0
- package/src/script-engine.ts +3375 -0
- package/src/sdk/nodepod-fs.ts +93 -0
- package/src/sdk/nodepod-process.ts +86 -0
- package/src/sdk/nodepod-terminal.ts +350 -0
- package/src/sdk/nodepod.ts +509 -0
- package/src/sdk/types.ts +70 -0
- package/src/shell/commands/bun.ts +121 -0
- package/src/shell/commands/directory.ts +297 -0
- package/src/shell/commands/file-ops.ts +525 -0
- package/src/shell/commands/git.ts +2142 -0
- package/src/shell/commands/node.ts +80 -0
- package/src/shell/commands/npm.ts +198 -0
- package/src/shell/commands/pm-types.ts +45 -0
- package/src/shell/commands/pnpm.ts +82 -0
- package/src/shell/commands/search.ts +264 -0
- package/src/shell/commands/shell-env.ts +352 -0
- package/src/shell/commands/text-processing.ts +1152 -0
- package/src/shell/commands/yarn.ts +84 -0
- package/src/shell/shell-builtins.ts +19 -0
- package/src/shell/shell-helpers.ts +250 -0
- package/src/shell/shell-interpreter.ts +514 -0
- package/src/shell/shell-parser.ts +429 -0
- package/src/shell/shell-types.ts +85 -0
- package/src/syntax-transforms.ts +561 -0
- package/src/threading/engine-worker.ts +64 -0
- package/src/threading/inline-worker.ts +372 -0
- package/src/threading/offload-types.ts +112 -0
- package/src/threading/offload-worker.ts +383 -0
- package/src/threading/offload.ts +271 -0
- package/src/threading/process-context.ts +92 -0
- package/src/threading/process-handle.ts +275 -0
- package/src/threading/process-manager.ts +956 -0
- package/src/threading/process-worker-entry.ts +854 -0
- package/src/threading/shared-vfs.ts +352 -0
- package/src/threading/sync-channel.ts +135 -0
- package/src/threading/task-queue.ts +177 -0
- package/src/threading/vfs-bridge.ts +231 -0
- package/src/threading/worker-pool.ts +233 -0
- package/src/threading/worker-protocol.ts +358 -0
- package/src/threading/worker-vfs.ts +218 -0
- package/src/types/externals.d.ts +38 -0
- package/src/types/fs-streams.ts +142 -0
- package/src/types/manifest.ts +17 -0
- package/src/worker-sandbox.ts +90 -0
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
// ws-compatible WebSocket polyfill wrapping browser native WebSocket
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import { encodeFrame, decodeFrame } from "./http";
|
|
5
|
+
import { Buffer } from "./buffer";
|
|
6
|
+
import { createHash } from "./crypto";
|
|
7
|
+
import type { TcpSocket } from "./net";
|
|
8
|
+
|
|
9
|
+
// polyfill for environments missing CloseEvent / MessageEvent
|
|
10
|
+
const SafeCloseEvent: typeof CloseEvent =
|
|
11
|
+
typeof CloseEvent !== 'undefined'
|
|
12
|
+
? CloseEvent
|
|
13
|
+
: (class SyntheticClose extends Event {
|
|
14
|
+
code: number;
|
|
15
|
+
reason: string;
|
|
16
|
+
wasClean: boolean;
|
|
17
|
+
constructor(kind: string, init?: { code?: number; reason?: string; wasClean?: boolean }) {
|
|
18
|
+
super(kind);
|
|
19
|
+
this.code = init?.code ?? 1000;
|
|
20
|
+
this.reason = init?.reason ?? '';
|
|
21
|
+
this.wasClean = init?.wasClean ?? true;
|
|
22
|
+
}
|
|
23
|
+
} as unknown as typeof CloseEvent);
|
|
24
|
+
|
|
25
|
+
const SafeMessageEvent: typeof MessageEvent =
|
|
26
|
+
typeof MessageEvent !== 'undefined'
|
|
27
|
+
? MessageEvent
|
|
28
|
+
: (class SyntheticMessage extends Event {
|
|
29
|
+
data: unknown;
|
|
30
|
+
constructor(kind: string, init?: { data?: unknown }) {
|
|
31
|
+
super(kind);
|
|
32
|
+
this.data = init?.data;
|
|
33
|
+
}
|
|
34
|
+
} as unknown as typeof MessageEvent);
|
|
35
|
+
|
|
36
|
+
// in-process server <-> client messaging via BroadcastChannel
|
|
37
|
+
let internalChannel: BroadcastChannel | null = null;
|
|
38
|
+
try {
|
|
39
|
+
internalChannel = new BroadcastChannel('nodepod-ws-bridge');
|
|
40
|
+
} catch { /* not available */ }
|
|
41
|
+
|
|
42
|
+
const activeServers = new Map<string, WebSocketServer>();
|
|
43
|
+
let nextClientId = 0;
|
|
44
|
+
|
|
45
|
+
type Handler = (...args: unknown[]) => void;
|
|
46
|
+
|
|
47
|
+
interface TinyEmitter {
|
|
48
|
+
_listeners: Map<string, Set<Handler>>;
|
|
49
|
+
on(evt: string, fn: Handler): this;
|
|
50
|
+
off(evt: string, fn: Handler): this;
|
|
51
|
+
emit(evt: string, ...args: unknown[]): void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface TinyEmitterConstructor {
|
|
55
|
+
new (): TinyEmitter;
|
|
56
|
+
(this: any): void;
|
|
57
|
+
prototype: any;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const TinyEmitter = function TinyEmitter(this: any) {
|
|
61
|
+
if (!this) return;
|
|
62
|
+
this._listeners = new Map<string, Set<Handler>>();
|
|
63
|
+
} as unknown as TinyEmitterConstructor;
|
|
64
|
+
|
|
65
|
+
TinyEmitter.prototype.on = function on(this: any, evt: string, fn: Handler): any {
|
|
66
|
+
if (!this._listeners.has(evt)) this._listeners.set(evt, new Set());
|
|
67
|
+
this._listeners.get(evt)!.add(fn);
|
|
68
|
+
return this;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
TinyEmitter.prototype.off = function off(this: any, evt: string, fn: Handler): any {
|
|
72
|
+
this._listeners.get(evt)?.delete(fn);
|
|
73
|
+
return this;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
TinyEmitter.prototype.emit = function emit(this: any, evt: string, ...args: unknown[]): void {
|
|
77
|
+
const s = this._listeners.get(evt);
|
|
78
|
+
if (!s) return;
|
|
79
|
+
for (const fn of s) {
|
|
80
|
+
try { fn(...args); } catch { /* swallow handler errors */ }
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const CONNECTING = 0;
|
|
85
|
+
export const OPEN = 1;
|
|
86
|
+
export const CLOSING = 2;
|
|
87
|
+
export const CLOSED = 3;
|
|
88
|
+
|
|
89
|
+
export interface WebSocket extends TinyEmitter {
|
|
90
|
+
readonly CONNECTING: number;
|
|
91
|
+
readonly OPEN: number;
|
|
92
|
+
readonly CLOSING: number;
|
|
93
|
+
readonly CLOSED: number;
|
|
94
|
+
readyState: number;
|
|
95
|
+
url: string;
|
|
96
|
+
protocol: string;
|
|
97
|
+
extensions: string;
|
|
98
|
+
bufferedAmount: number;
|
|
99
|
+
binaryType: 'blob' | 'arraybuffer';
|
|
100
|
+
_uid: string;
|
|
101
|
+
_boundServer: WebSocketServer | null;
|
|
102
|
+
_native: globalThis.WebSocket | null;
|
|
103
|
+
_tcpSocket: TcpSocket | null;
|
|
104
|
+
_tcpInboundBuf: Uint8Array;
|
|
105
|
+
onopen: ((ev: Event) => void) | null;
|
|
106
|
+
onclose: ((ev: CloseEvent) => void) | null;
|
|
107
|
+
onerror: ((ev: Event) => void) | null;
|
|
108
|
+
onmessage: ((ev: MessageEvent) => void) | null;
|
|
109
|
+
_open(): void;
|
|
110
|
+
_openNative(): void;
|
|
111
|
+
send(payload: string | ArrayBuffer | Uint8Array): void;
|
|
112
|
+
close(code?: number, reason?: string): void;
|
|
113
|
+
ping(): void;
|
|
114
|
+
pong(): void;
|
|
115
|
+
terminate(): void;
|
|
116
|
+
_bindServer(srv: WebSocketServer): void;
|
|
117
|
+
_deliverMessage(data: unknown): void;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface WebSocketConstructor {
|
|
121
|
+
new (address: string, protocols?: string | string[]): WebSocket;
|
|
122
|
+
(this: any, address: string, protocols?: string | string[]): void;
|
|
123
|
+
prototype: any;
|
|
124
|
+
readonly CONNECTING: number;
|
|
125
|
+
readonly OPEN: number;
|
|
126
|
+
readonly CLOSING: number;
|
|
127
|
+
readonly CLOSED: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const WebSocket = function WebSocket(this: any, address: string, protocols?: string | string[]) {
|
|
131
|
+
if (!this) return;
|
|
132
|
+
TinyEmitter.call(this);
|
|
133
|
+
this.CONNECTING = CONNECTING;
|
|
134
|
+
this.OPEN = OPEN;
|
|
135
|
+
this.CLOSING = CLOSING;
|
|
136
|
+
this.CLOSED = CLOSED;
|
|
137
|
+
this.readyState = CONNECTING;
|
|
138
|
+
this.url = address;
|
|
139
|
+
this.protocol = '';
|
|
140
|
+
this.extensions = '';
|
|
141
|
+
this.bufferedAmount = 0;
|
|
142
|
+
this.binaryType = 'blob';
|
|
143
|
+
this._uid = `ws-${++nextClientId}`;
|
|
144
|
+
this._boundServer = null;
|
|
145
|
+
this._native = null;
|
|
146
|
+
this._tcpSocket = null;
|
|
147
|
+
this._tcpInboundBuf = new Uint8Array(0);
|
|
148
|
+
this.onopen = null;
|
|
149
|
+
this.onclose = null;
|
|
150
|
+
this.onerror = null;
|
|
151
|
+
this.onmessage = null;
|
|
152
|
+
if (protocols) this.protocol = Array.isArray(protocols) ? protocols[0] : protocols;
|
|
153
|
+
const self = this;
|
|
154
|
+
setTimeout(() => self._open(), 0);
|
|
155
|
+
} as unknown as WebSocketConstructor;
|
|
156
|
+
|
|
157
|
+
Object.setPrototypeOf(WebSocket.prototype, TinyEmitter.prototype);
|
|
158
|
+
|
|
159
|
+
(WebSocket as any).CONNECTING = CONNECTING;
|
|
160
|
+
(WebSocket as any).OPEN = OPEN;
|
|
161
|
+
(WebSocket as any).CLOSING = CLOSING;
|
|
162
|
+
(WebSocket as any).CLOSED = CLOSED;
|
|
163
|
+
|
|
164
|
+
WebSocket.prototype._open = function _open(this: any): void {
|
|
165
|
+
// Internal loopback connection (server-side socket)
|
|
166
|
+
if (this.url.startsWith('internal://')) {
|
|
167
|
+
this.readyState = OPEN;
|
|
168
|
+
this.emit('open');
|
|
169
|
+
this.onopen?.(new Event('open'));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Real remote connection -- delegate to browser's native WebSocket
|
|
174
|
+
if (this.url.startsWith('ws://') || this.url.startsWith('wss://')) {
|
|
175
|
+
this._openNative();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// BroadcastChannel-based in-process connection
|
|
180
|
+
if (!internalChannel) {
|
|
181
|
+
const self = this;
|
|
182
|
+
setTimeout(() => {
|
|
183
|
+
self.readyState = OPEN;
|
|
184
|
+
self.emit('open');
|
|
185
|
+
self.onopen?.(new Event('open'));
|
|
186
|
+
}, 0);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
internalChannel.postMessage({ kind: 'connect', uid: this._uid, url: this.url });
|
|
191
|
+
|
|
192
|
+
const chan = internalChannel;
|
|
193
|
+
const self = this;
|
|
194
|
+
const onMsg = (ev: MessageEvent) => {
|
|
195
|
+
const d = ev.data;
|
|
196
|
+
if (d.targetUid !== self._uid) return;
|
|
197
|
+
|
|
198
|
+
if (d.kind === 'connected') {
|
|
199
|
+
self.readyState = OPEN;
|
|
200
|
+
self.emit('open');
|
|
201
|
+
self.onopen?.(new Event('open'));
|
|
202
|
+
} else if (d.kind === 'payload') {
|
|
203
|
+
const me = new SafeMessageEvent('message', { data: d.body });
|
|
204
|
+
self.emit('message', me);
|
|
205
|
+
self.onmessage?.(me);
|
|
206
|
+
} else if (d.kind === 'closed') {
|
|
207
|
+
self.readyState = CLOSED;
|
|
208
|
+
const ce = new SafeCloseEvent('close', { code: d.code || 1000, reason: d.reason || '', wasClean: true });
|
|
209
|
+
self.emit('close', ce);
|
|
210
|
+
self.onclose?.(ce);
|
|
211
|
+
chan.removeEventListener('message', onMsg);
|
|
212
|
+
} else if (d.kind === 'fault') {
|
|
213
|
+
const ee = new Event('error');
|
|
214
|
+
self.emit('error', ee);
|
|
215
|
+
self.onerror?.(ee);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
chan.addEventListener('message', onMsg);
|
|
219
|
+
|
|
220
|
+
// If nobody responds within 100 ms, consider the socket "open" anyway
|
|
221
|
+
setTimeout(() => {
|
|
222
|
+
if (self.readyState === CONNECTING) {
|
|
223
|
+
self.readyState = OPEN;
|
|
224
|
+
self.emit('open');
|
|
225
|
+
self.onopen?.(new Event('open'));
|
|
226
|
+
}
|
|
227
|
+
}, 100);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
WebSocket.prototype._openNative = function _openNative(this: any): void {
|
|
231
|
+
const inBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';
|
|
232
|
+
const NativeImpl = inBrowser && typeof globalThis.WebSocket === 'function' && globalThis.WebSocket !== (WebSocket as unknown)
|
|
233
|
+
? globalThis.WebSocket
|
|
234
|
+
: null;
|
|
235
|
+
|
|
236
|
+
if (!NativeImpl) {
|
|
237
|
+
const self = this;
|
|
238
|
+
setTimeout(() => {
|
|
239
|
+
self.readyState = OPEN;
|
|
240
|
+
self.emit('open');
|
|
241
|
+
self.onopen?.(new Event('open'));
|
|
242
|
+
}, 0);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
this._native = new NativeImpl(this.url);
|
|
248
|
+
this._native.binaryType = this.binaryType === 'arraybuffer' ? 'arraybuffer' : 'blob';
|
|
249
|
+
} catch {
|
|
250
|
+
this.readyState = CLOSED;
|
|
251
|
+
const errEvt = new Event('error');
|
|
252
|
+
this.emit('error', errEvt);
|
|
253
|
+
this.onerror?.(errEvt);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const self = this;
|
|
258
|
+
this._native.onopen = () => {
|
|
259
|
+
self.readyState = OPEN;
|
|
260
|
+
self.emit('open');
|
|
261
|
+
self.onopen?.(new Event('open'));
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
this._native.onmessage = (raw: globalThis.MessageEvent) => {
|
|
265
|
+
const me = new SafeMessageEvent('message', { data: raw.data });
|
|
266
|
+
self.emit('message', me);
|
|
267
|
+
self.onmessage?.(me);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
this._native.onclose = (raw: globalThis.CloseEvent) => {
|
|
271
|
+
self.readyState = CLOSED;
|
|
272
|
+
self._native = null;
|
|
273
|
+
const ce = new SafeCloseEvent('close', { code: raw.code, reason: raw.reason, wasClean: raw.wasClean });
|
|
274
|
+
self.emit('close', ce);
|
|
275
|
+
self.onclose?.(ce);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
this._native.onerror = () => {
|
|
279
|
+
const errEvt = new Event('error');
|
|
280
|
+
self.emit('error', errEvt);
|
|
281
|
+
self.onerror?.(errEvt);
|
|
282
|
+
};
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
WebSocket.prototype.send = function send(this: any, payload: string | ArrayBuffer | Uint8Array): void {
|
|
286
|
+
if (this.readyState !== OPEN) throw new Error('WebSocket is not open');
|
|
287
|
+
|
|
288
|
+
if (this._native) { this._native.send(payload); return; }
|
|
289
|
+
|
|
290
|
+
// TcpSocket-backed (from handleUpgrade) — write real WS frames
|
|
291
|
+
if (this._tcpSocket) {
|
|
292
|
+
let data: Uint8Array;
|
|
293
|
+
let op: number;
|
|
294
|
+
if (typeof payload === 'string') {
|
|
295
|
+
data = new TextEncoder().encode(payload);
|
|
296
|
+
op = 0x01; // text frame
|
|
297
|
+
} else if (payload instanceof ArrayBuffer) {
|
|
298
|
+
data = new Uint8Array(payload);
|
|
299
|
+
op = 0x02; // binary frame
|
|
300
|
+
} else {
|
|
301
|
+
data = payload;
|
|
302
|
+
op = 0x02; // binary frame
|
|
303
|
+
}
|
|
304
|
+
// Server frames are NOT masked
|
|
305
|
+
const frame = encodeFrame(op, data, false);
|
|
306
|
+
this._tcpSocket.write(Buffer.from(frame));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (this._boundServer) { this._boundServer._injectClientPayload(this, payload); return; }
|
|
311
|
+
|
|
312
|
+
if (internalChannel) {
|
|
313
|
+
internalChannel.postMessage({ kind: 'payload', uid: this._uid, url: this.url, body: payload });
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
WebSocket.prototype.close = function close(this: any, code?: number, reason?: string): void {
|
|
318
|
+
if (this.readyState === CLOSED || this.readyState === CLOSING) return;
|
|
319
|
+
this.readyState = CLOSING;
|
|
320
|
+
|
|
321
|
+
if (this._native) { this._native.close(code, reason); return; }
|
|
322
|
+
|
|
323
|
+
// TcpSocket-backed — send close frame
|
|
324
|
+
if (this._tcpSocket) {
|
|
325
|
+
const c = code ?? 1000;
|
|
326
|
+
const closeBuf = new Uint8Array(2);
|
|
327
|
+
closeBuf[0] = (c >> 8) & 0xff;
|
|
328
|
+
closeBuf[1] = c & 0xff;
|
|
329
|
+
const frame = encodeFrame(0x08, closeBuf, false);
|
|
330
|
+
try { this._tcpSocket.write(Buffer.from(frame)); } catch { /* socket may be dead */ }
|
|
331
|
+
const self = this;
|
|
332
|
+
setTimeout(() => {
|
|
333
|
+
self.readyState = CLOSED;
|
|
334
|
+
const ce = new SafeCloseEvent('close', { code: c, reason: reason || '', wasClean: true });
|
|
335
|
+
self.emit('close', ce);
|
|
336
|
+
self.onclose?.(ce);
|
|
337
|
+
self._tcpSocket = null;
|
|
338
|
+
}, 0);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (internalChannel) {
|
|
343
|
+
internalChannel.postMessage({ kind: 'disconnect', uid: this._uid, url: this.url, code, reason });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const self = this;
|
|
347
|
+
setTimeout(() => {
|
|
348
|
+
self.readyState = CLOSED;
|
|
349
|
+
const ce = new SafeCloseEvent('close', { code: code || 1000, reason: reason || '', wasClean: true });
|
|
350
|
+
self.emit('close', ce);
|
|
351
|
+
self.onclose?.(ce);
|
|
352
|
+
}, 0);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
WebSocket.prototype.ping = function ping(): void { /* no-op in browser */ };
|
|
356
|
+
WebSocket.prototype.pong = function pong(): void { /* no-op in browser */ };
|
|
357
|
+
|
|
358
|
+
WebSocket.prototype.terminate = function terminate(this: any): void {
|
|
359
|
+
if (this._native) { this._native.close(); this._native = null; }
|
|
360
|
+
if (this._tcpSocket) { try { this._tcpSocket.destroy(); } catch { /* */ } this._tcpSocket = null; }
|
|
361
|
+
this.readyState = CLOSED;
|
|
362
|
+
const ce = new SafeCloseEvent('close', { code: 1006, reason: 'Terminated', wasClean: false });
|
|
363
|
+
this.emit('close', ce);
|
|
364
|
+
this.onclose?.(ce);
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
WebSocket.prototype._bindServer = function _bindServer(this: any, srv: WebSocketServer): void { this._boundServer = srv; };
|
|
368
|
+
|
|
369
|
+
WebSocket.prototype._deliverMessage = function _deliverMessage(this: any, data: unknown): void {
|
|
370
|
+
const me = new SafeMessageEvent('message', { data });
|
|
371
|
+
this.emit('message', me);
|
|
372
|
+
this.onmessage?.(me);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// Sec-WebSocket-Accept key computation (RFC 6455)
|
|
376
|
+
const WS_GUID = '258EAFA5-E914-47DA-95CA-5AB5DC76CB76';
|
|
377
|
+
|
|
378
|
+
function _computeAcceptKey(wsKey: string): string {
|
|
379
|
+
try {
|
|
380
|
+
const hash = createHash('sha1');
|
|
381
|
+
hash.update(wsKey + WS_GUID);
|
|
382
|
+
return hash.digest('base64') as string;
|
|
383
|
+
} catch {
|
|
384
|
+
// bridge only checks for "HTTP/1.1 101", so a placeholder works
|
|
385
|
+
return btoa(wsKey + WS_GUID).slice(0, 28);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export interface ServerConfig {
|
|
390
|
+
host?: string;
|
|
391
|
+
port?: number;
|
|
392
|
+
server?: unknown;
|
|
393
|
+
noServer?: boolean;
|
|
394
|
+
path?: string;
|
|
395
|
+
clientTracking?: boolean;
|
|
396
|
+
perMessageDeflate?: boolean | object;
|
|
397
|
+
maxPayload?: number;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export interface WebSocketServer extends TinyEmitter {
|
|
401
|
+
clients: Set<WebSocket>;
|
|
402
|
+
options: ServerConfig;
|
|
403
|
+
_route: string;
|
|
404
|
+
_channelCb: ((ev: MessageEvent) => void) | null;
|
|
405
|
+
_listen(): void;
|
|
406
|
+
_injectClientPayload(source: WebSocket, data: unknown): void;
|
|
407
|
+
handleUpgrade(req: unknown, socket: unknown, head: unknown, done: (ws: WebSocket, req: unknown) => void): void;
|
|
408
|
+
close(done?: () => void): void;
|
|
409
|
+
address(): { port: number; family: string; address: string } | null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
interface WebSocketServerConstructor {
|
|
413
|
+
new (opts?: ServerConfig): WebSocketServer;
|
|
414
|
+
(this: any, opts?: ServerConfig): void;
|
|
415
|
+
prototype: any;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export const WebSocketServer = function WebSocketServer(this: any, opts: ServerConfig = {}) {
|
|
419
|
+
if (!this) return;
|
|
420
|
+
TinyEmitter.call(this);
|
|
421
|
+
this.clients = new Set<WebSocket>();
|
|
422
|
+
this.options = opts;
|
|
423
|
+
this._route = opts.path || '/';
|
|
424
|
+
this._channelCb = null;
|
|
425
|
+
|
|
426
|
+
if (!opts.noServer) this._listen();
|
|
427
|
+
activeServers.set(this._route, this);
|
|
428
|
+
} as unknown as WebSocketServerConstructor;
|
|
429
|
+
|
|
430
|
+
Object.setPrototypeOf(WebSocketServer.prototype, TinyEmitter.prototype);
|
|
431
|
+
|
|
432
|
+
WebSocketServer.prototype._listen = function _listen(this: any): void {
|
|
433
|
+
if (!internalChannel) return;
|
|
434
|
+
const chan = internalChannel;
|
|
435
|
+
const self = this;
|
|
436
|
+
|
|
437
|
+
this._channelCb = (ev: MessageEvent) => {
|
|
438
|
+
const d = ev.data;
|
|
439
|
+
|
|
440
|
+
if (d.kind === 'connect') {
|
|
441
|
+
const sock = new WebSocket('internal://' + self._route);
|
|
442
|
+
sock._bindServer(self);
|
|
443
|
+
(sock as unknown as { _uid: string })._uid = d.uid;
|
|
444
|
+
self.clients.add(sock);
|
|
445
|
+
chan.postMessage({ kind: 'connected', targetUid: d.uid });
|
|
446
|
+
self.emit('connection', sock, { url: d.url });
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (d.kind === 'payload') {
|
|
450
|
+
for (const c of self.clients) {
|
|
451
|
+
if ((c as unknown as { _uid: string })._uid === d.uid) {
|
|
452
|
+
c._deliverMessage(d.body);
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (d.kind === 'disconnect') {
|
|
459
|
+
for (const c of self.clients) {
|
|
460
|
+
if ((c as unknown as { _uid: string })._uid === d.uid) {
|
|
461
|
+
c.close(d.code, d.reason);
|
|
462
|
+
self.clients.delete(c);
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
chan.addEventListener('message', this._channelCb);
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
WebSocketServer.prototype._injectClientPayload = function _injectClientPayload(source: WebSocket, data: unknown): void {
|
|
472
|
+
const me = new SafeMessageEvent('message', { data });
|
|
473
|
+
source.emit('message', me);
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
WebSocketServer.prototype.handleUpgrade = function handleUpgrade(
|
|
477
|
+
this: any,
|
|
478
|
+
req: unknown,
|
|
479
|
+
socket: unknown,
|
|
480
|
+
head: unknown,
|
|
481
|
+
done: (ws: WebSocket, req: unknown) => void,
|
|
482
|
+
): void {
|
|
483
|
+
const sock = new WebSocket('internal://' + this._route);
|
|
484
|
+
sock._bindServer(this);
|
|
485
|
+
if (this.options.clientTracking !== false) this.clients.add(sock);
|
|
486
|
+
|
|
487
|
+
// Check if socket is a real TcpSocket (from http.Server.dispatchUpgrade)
|
|
488
|
+
const tcp = socket as TcpSocket | null;
|
|
489
|
+
const isTcp = tcp && typeof tcp.write === 'function' && typeof tcp._feedData === 'function';
|
|
490
|
+
if (isTcp) {
|
|
491
|
+
// Wire the ws.WebSocket to the TcpSocket for frame-level I/O
|
|
492
|
+
sock._tcpSocket = tcp;
|
|
493
|
+
|
|
494
|
+
// Compute Sec-WebSocket-Accept
|
|
495
|
+
const reqHeaders = (req as { headers?: Record<string, string> })?.headers || {};
|
|
496
|
+
const wsKey = reqHeaders['sec-websocket-key'] || '';
|
|
497
|
+
const acceptKey = _computeAcceptKey(wsKey);
|
|
498
|
+
|
|
499
|
+
// Write HTTP 101 Switching Protocols response to the TcpSocket
|
|
500
|
+
// This triggers handshakeDone in the request-proxy bridge
|
|
501
|
+
const handshake = `HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${acceptKey}\r\n\r\n`;
|
|
502
|
+
tcp.write(Buffer.from(handshake));
|
|
503
|
+
|
|
504
|
+
// Listen for incoming data from the TcpSocket (client→server frames)
|
|
505
|
+
tcp.on('data', (chunk: unknown) => {
|
|
506
|
+
const raw = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk as ArrayBuffer);
|
|
507
|
+
const merged = new Uint8Array(sock._tcpInboundBuf.length + raw.length);
|
|
508
|
+
merged.set(sock._tcpInboundBuf, 0);
|
|
509
|
+
merged.set(raw, sock._tcpInboundBuf.length);
|
|
510
|
+
sock._tcpInboundBuf = merged;
|
|
511
|
+
|
|
512
|
+
while (sock._tcpInboundBuf.length >= 2) {
|
|
513
|
+
const frame = decodeFrame(sock._tcpInboundBuf);
|
|
514
|
+
if (!frame) break;
|
|
515
|
+
sock._tcpInboundBuf = sock._tcpInboundBuf.slice(frame.consumed);
|
|
516
|
+
|
|
517
|
+
switch (frame.op) {
|
|
518
|
+
case 0x01: { // text
|
|
519
|
+
const text = new TextDecoder().decode(frame.data);
|
|
520
|
+
const me = new SafeMessageEvent('message', { data: text });
|
|
521
|
+
sock.emit('message', me);
|
|
522
|
+
sock.onmessage?.(me);
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
case 0x02: { // binary
|
|
526
|
+
const me = new SafeMessageEvent('message', { data: frame.data.buffer });
|
|
527
|
+
sock.emit('message', me);
|
|
528
|
+
sock.onmessage?.(me);
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
case 0x08: { // close
|
|
532
|
+
const code = frame.data.length >= 2
|
|
533
|
+
? (frame.data[0] << 8) | frame.data[1]
|
|
534
|
+
: 1000;
|
|
535
|
+
sock.readyState = CLOSED;
|
|
536
|
+
const ce = new SafeCloseEvent('close', { code, reason: '', wasClean: true });
|
|
537
|
+
sock.emit('close', ce);
|
|
538
|
+
sock.onclose?.(ce);
|
|
539
|
+
sock._tcpSocket = null;
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
case 0x09: { // ping — respond with pong
|
|
543
|
+
const pong = encodeFrame(0x0a, frame.data, false);
|
|
544
|
+
tcp.write(Buffer.from(pong));
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
case 0x0a: { // pong
|
|
548
|
+
sock.emit('pong', frame.data);
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
tcp.on('close', () => {
|
|
556
|
+
if (sock.readyState !== CLOSED) {
|
|
557
|
+
sock.readyState = CLOSED;
|
|
558
|
+
const ce = new SafeCloseEvent('close', { code: 1006, reason: 'Connection lost', wasClean: false });
|
|
559
|
+
sock.emit('close', ce);
|
|
560
|
+
sock.onclose?.(ce);
|
|
561
|
+
}
|
|
562
|
+
sock._tcpSocket = null;
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const self = this;
|
|
567
|
+
setTimeout(() => {
|
|
568
|
+
done(sock, req);
|
|
569
|
+
self.emit('connection', sock, req);
|
|
570
|
+
}, 0);
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
WebSocketServer.prototype.close = function close(this: any, done?: () => void): void {
|
|
574
|
+
for (const c of this.clients) c.close(1001, 'Server closing');
|
|
575
|
+
this.clients.clear();
|
|
576
|
+
activeServers.delete(this._route);
|
|
577
|
+
if (this._channelCb && internalChannel) {
|
|
578
|
+
internalChannel.removeEventListener('message', this._channelCb);
|
|
579
|
+
this._channelCb = null;
|
|
580
|
+
}
|
|
581
|
+
this.emit('close');
|
|
582
|
+
if (done) setTimeout(done, 0);
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
WebSocketServer.prototype.address = function address(this: any): { port: number; family: string; address: string } | null {
|
|
586
|
+
return { port: this.options.port || 0, family: 'IPv4', address: this.options.host || '0.0.0.0' };
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
export const Server = WebSocketServer;
|
|
590
|
+
|
|
591
|
+
export const createWebSocketStream = (): never => {
|
|
592
|
+
throw new Error('createWebSocketStream is not available in the browser');
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
export default WebSocket;
|