@rivetkit/engine-runner 25.7.1-rc.1
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/.turbo/turbo-build.log +23 -0
- package/.turbo/turbo-check-types.log +5 -0
- package/.turbo/turbo-test.log +5537 -0
- package/benches/actor-lifecycle.bench.ts +190 -0
- package/benches/utils.ts +143 -0
- package/dist/mod.cjs +2044 -0
- package/dist/mod.cjs.map +1 -0
- package/dist/mod.d.cts +67 -0
- package/dist/mod.d.ts +67 -0
- package/dist/mod.js +2044 -0
- package/dist/mod.js.map +1 -0
- package/package.json +38 -0
- package/src/log.ts +11 -0
- package/src/mod.ts +1354 -0
- package/src/tunnel.ts +841 -0
- package/src/utils.ts +31 -0
- package/src/websocket-tunnel-adapter.ts +486 -0
- package/src/websocket.ts +43 -0
- package/tests/lifecycle.test.ts +596 -0
- package/tsconfig.json +13 -0
- package/tsup.config.ts +4 -0
- package/turbo.json +4 -0
- package/vitest.config.ts +17 -0
package/src/utils.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function unreachable(x: never): never {
|
|
2
|
+
throw `Unreachable: ${x}`;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface BackoffOptions {
|
|
6
|
+
initialDelay?: number;
|
|
7
|
+
maxDelay?: number;
|
|
8
|
+
multiplier?: number;
|
|
9
|
+
jitter?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function calculateBackoff(
|
|
13
|
+
attempt: number,
|
|
14
|
+
options: BackoffOptions = {},
|
|
15
|
+
): number {
|
|
16
|
+
const {
|
|
17
|
+
initialDelay = 1000,
|
|
18
|
+
maxDelay = 30000,
|
|
19
|
+
multiplier = 2,
|
|
20
|
+
jitter = true,
|
|
21
|
+
} = options;
|
|
22
|
+
|
|
23
|
+
let delay = Math.min(initialDelay * Math.pow(multiplier, attempt), maxDelay);
|
|
24
|
+
|
|
25
|
+
if (jitter) {
|
|
26
|
+
// Add random jitter between 0% and 25% of the delay
|
|
27
|
+
delay = delay * (1 + Math.random() * 0.25);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return Math.floor(delay);
|
|
31
|
+
}
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
// WebSocket-like adapter for tunneled connections
|
|
2
|
+
// Implements a subset of the WebSocket interface for compatibility with runner code
|
|
3
|
+
|
|
4
|
+
import { logger } from "./log";
|
|
5
|
+
|
|
6
|
+
export class WebSocketTunnelAdapter {
|
|
7
|
+
#webSocketId: string;
|
|
8
|
+
#readyState: number = 0; // CONNECTING
|
|
9
|
+
#eventListeners: Map<string, Set<(event: any) => void>> = new Map();
|
|
10
|
+
#onopen: ((this: any, ev: any) => any) | null = null;
|
|
11
|
+
#onclose: ((this: any, ev: any) => any) | null = null;
|
|
12
|
+
#onerror: ((this: any, ev: any) => any) | null = null;
|
|
13
|
+
#onmessage: ((this: any, ev: any) => any) | null = null;
|
|
14
|
+
#bufferedAmount = 0;
|
|
15
|
+
#binaryType: "nodebuffer" | "arraybuffer" | "blob" = "nodebuffer";
|
|
16
|
+
#extensions = "";
|
|
17
|
+
#protocol = "";
|
|
18
|
+
#url = "";
|
|
19
|
+
#sendCallback: (data: ArrayBuffer | string, isBinary: boolean) => void;
|
|
20
|
+
#closeCallback: (code?: number, reason?: string) => void;
|
|
21
|
+
|
|
22
|
+
// Event buffering for events fired before listeners are attached
|
|
23
|
+
#bufferedEvents: Array<{
|
|
24
|
+
type: string;
|
|
25
|
+
event: any;
|
|
26
|
+
}> = [];
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
webSocketId: string,
|
|
30
|
+
sendCallback: (data: ArrayBuffer | string, isBinary: boolean) => void,
|
|
31
|
+
closeCallback: (code?: number, reason?: string) => void
|
|
32
|
+
) {
|
|
33
|
+
this.#webSocketId = webSocketId;
|
|
34
|
+
this.#sendCallback = sendCallback;
|
|
35
|
+
this.#closeCallback = closeCallback;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get readyState(): number {
|
|
39
|
+
return this.#readyState;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get bufferedAmount(): number {
|
|
43
|
+
return this.#bufferedAmount;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get binaryType(): string {
|
|
47
|
+
return this.#binaryType;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
set binaryType(value: string) {
|
|
51
|
+
if (value === "nodebuffer" || value === "arraybuffer" || value === "blob") {
|
|
52
|
+
this.#binaryType = value;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get extensions(): string {
|
|
57
|
+
return this.#extensions;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get protocol(): string {
|
|
61
|
+
return this.#protocol;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get url(): string {
|
|
65
|
+
return this.#url;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get onopen(): ((this: any, ev: any) => any) | null {
|
|
69
|
+
return this.#onopen;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
set onopen(value: ((this: any, ev: any) => any) | null) {
|
|
73
|
+
this.#onopen = value;
|
|
74
|
+
// Flush any buffered open events when onopen is set
|
|
75
|
+
if (value) {
|
|
76
|
+
this.#flushBufferedEvents("open");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
get onclose(): ((this: any, ev: any) => any) | null {
|
|
81
|
+
return this.#onclose;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
set onclose(value: ((this: any, ev: any) => any) | null) {
|
|
85
|
+
this.#onclose = value;
|
|
86
|
+
// Flush any buffered close events when onclose is set
|
|
87
|
+
if (value) {
|
|
88
|
+
this.#flushBufferedEvents("close");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get onerror(): ((this: any, ev: any) => any) | null {
|
|
93
|
+
return this.#onerror;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
set onerror(value: ((this: any, ev: any) => any) | null) {
|
|
97
|
+
this.#onerror = value;
|
|
98
|
+
// Flush any buffered error events when onerror is set
|
|
99
|
+
if (value) {
|
|
100
|
+
this.#flushBufferedEvents("error");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
get onmessage(): ((this: any, ev: any) => any) | null {
|
|
105
|
+
return this.#onmessage;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
set onmessage(value: ((this: any, ev: any) => any) | null) {
|
|
109
|
+
this.#onmessage = value;
|
|
110
|
+
// Flush any buffered message events when onmessage is set
|
|
111
|
+
if (value) {
|
|
112
|
+
this.#flushBufferedEvents("message");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
send(data: string | ArrayBuffer | ArrayBufferView | Blob | Buffer): void {
|
|
117
|
+
if (this.#readyState !== 1) { // OPEN
|
|
118
|
+
throw new Error("WebSocket is not open");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let isBinary = false;
|
|
122
|
+
let messageData: string | ArrayBuffer;
|
|
123
|
+
|
|
124
|
+
if (typeof data === "string") {
|
|
125
|
+
messageData = data;
|
|
126
|
+
} else if (data instanceof ArrayBuffer) {
|
|
127
|
+
isBinary = true;
|
|
128
|
+
messageData = data;
|
|
129
|
+
} else if (ArrayBuffer.isView(data)) {
|
|
130
|
+
isBinary = true;
|
|
131
|
+
// Convert ArrayBufferView to ArrayBuffer
|
|
132
|
+
const view = data as ArrayBufferView;
|
|
133
|
+
// Check if it's a SharedArrayBuffer
|
|
134
|
+
if (view.buffer instanceof SharedArrayBuffer) {
|
|
135
|
+
// Copy SharedArrayBuffer to regular ArrayBuffer
|
|
136
|
+
const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
|
|
137
|
+
messageData = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as unknown as ArrayBuffer;
|
|
138
|
+
} else {
|
|
139
|
+
messageData = view.buffer.slice(
|
|
140
|
+
view.byteOffset,
|
|
141
|
+
view.byteOffset + view.byteLength
|
|
142
|
+
) as ArrayBuffer;
|
|
143
|
+
}
|
|
144
|
+
} else if (data instanceof Blob) {
|
|
145
|
+
throw new Error("Blob sending not implemented in tunnel adapter");
|
|
146
|
+
} else if (typeof Buffer !== 'undefined' && Buffer.isBuffer(data)) {
|
|
147
|
+
isBinary = true;
|
|
148
|
+
// Convert Buffer to ArrayBuffer
|
|
149
|
+
const buf = data as Buffer;
|
|
150
|
+
// Check if it's a SharedArrayBuffer
|
|
151
|
+
if (buf.buffer instanceof SharedArrayBuffer) {
|
|
152
|
+
// Copy SharedArrayBuffer to regular ArrayBuffer
|
|
153
|
+
const bytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
154
|
+
messageData = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as unknown as ArrayBuffer;
|
|
155
|
+
} else {
|
|
156
|
+
messageData = buf.buffer.slice(
|
|
157
|
+
buf.byteOffset,
|
|
158
|
+
buf.byteOffset + buf.byteLength
|
|
159
|
+
) as ArrayBuffer;
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
throw new Error("Invalid data type");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Send through tunnel
|
|
166
|
+
this.#sendCallback(messageData, isBinary);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
close(code?: number, reason?: string): void {
|
|
170
|
+
if (
|
|
171
|
+
this.#readyState === 2 || // CLOSING
|
|
172
|
+
this.#readyState === 3 // CLOSED
|
|
173
|
+
) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this.#readyState = 2; // CLOSING
|
|
178
|
+
|
|
179
|
+
// Send close through tunnel
|
|
180
|
+
this.#closeCallback(code, reason);
|
|
181
|
+
|
|
182
|
+
// Update state and fire event
|
|
183
|
+
this.#readyState = 3; // CLOSED
|
|
184
|
+
|
|
185
|
+
const closeEvent = {
|
|
186
|
+
wasClean: true,
|
|
187
|
+
code: code || 1000,
|
|
188
|
+
reason: reason || "",
|
|
189
|
+
type: "close",
|
|
190
|
+
target: this,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
this.#fireEvent("close", closeEvent);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
addEventListener(
|
|
197
|
+
type: string,
|
|
198
|
+
listener: (event: any) => void,
|
|
199
|
+
options?: boolean | any
|
|
200
|
+
): void {
|
|
201
|
+
if (typeof listener === "function") {
|
|
202
|
+
let listeners = this.#eventListeners.get(type);
|
|
203
|
+
if (!listeners) {
|
|
204
|
+
listeners = new Set();
|
|
205
|
+
this.#eventListeners.set(type, listeners);
|
|
206
|
+
}
|
|
207
|
+
listeners.add(listener);
|
|
208
|
+
|
|
209
|
+
// Flush any buffered events for this type
|
|
210
|
+
this.#flushBufferedEvents(type);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
removeEventListener(
|
|
215
|
+
type: string,
|
|
216
|
+
listener: (event: any) => void,
|
|
217
|
+
options?: boolean | any
|
|
218
|
+
): void {
|
|
219
|
+
if (typeof listener === "function") {
|
|
220
|
+
const listeners = this.#eventListeners.get(type);
|
|
221
|
+
if (listeners) {
|
|
222
|
+
listeners.delete(listener);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
dispatchEvent(event: any): boolean {
|
|
228
|
+
// Simple implementation
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
#fireEvent(type: string, event: any): void {
|
|
233
|
+
// Call all registered event listeners
|
|
234
|
+
const listeners = this.#eventListeners.get(type);
|
|
235
|
+
let hasListeners = false;
|
|
236
|
+
|
|
237
|
+
if (listeners && listeners.size > 0) {
|
|
238
|
+
hasListeners = true;
|
|
239
|
+
for (const listener of listeners) {
|
|
240
|
+
try {
|
|
241
|
+
listener.call(this, event);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
logger()?.error({ msg: "error in websocket event listener", error, type });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Call the onX property if set
|
|
249
|
+
switch (type) {
|
|
250
|
+
case "open":
|
|
251
|
+
if (this.#onopen) {
|
|
252
|
+
hasListeners = true;
|
|
253
|
+
try {
|
|
254
|
+
this.#onopen.call(this, event);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
logger()?.error({ msg: "error in onopen handler", error });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
break;
|
|
260
|
+
case "close":
|
|
261
|
+
if (this.#onclose) {
|
|
262
|
+
hasListeners = true;
|
|
263
|
+
try {
|
|
264
|
+
this.#onclose.call(this, event);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
logger()?.error({ msg: "error in onclose handler", error });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
case "error":
|
|
271
|
+
if (this.#onerror) {
|
|
272
|
+
hasListeners = true;
|
|
273
|
+
try {
|
|
274
|
+
this.#onerror.call(this, event);
|
|
275
|
+
} catch (error) {
|
|
276
|
+
logger()?.error({ msg: "error in onerror handler", error });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
break;
|
|
280
|
+
case "message":
|
|
281
|
+
if (this.#onmessage) {
|
|
282
|
+
hasListeners = true;
|
|
283
|
+
try {
|
|
284
|
+
this.#onmessage.call(this, event);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
logger()?.error({ msg: "error in onmessage handler", error });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Buffer the event if no listeners are registered
|
|
293
|
+
if (!hasListeners) {
|
|
294
|
+
this.#bufferedEvents.push({ type, event });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
#flushBufferedEvents(type: string): void {
|
|
299
|
+
const eventsToFlush = this.#bufferedEvents.filter(
|
|
300
|
+
(buffered) => buffered.type === type
|
|
301
|
+
);
|
|
302
|
+
this.#bufferedEvents = this.#bufferedEvents.filter(
|
|
303
|
+
(buffered) => buffered.type !== type
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
for (const { event } of eventsToFlush) {
|
|
307
|
+
// Re-fire the event, which will now have listeners
|
|
308
|
+
const listeners = this.#eventListeners.get(type);
|
|
309
|
+
if (listeners) {
|
|
310
|
+
for (const listener of listeners) {
|
|
311
|
+
try {
|
|
312
|
+
listener.call(this, event);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
logger()?.error({
|
|
315
|
+
msg: "error in websocket event listener",
|
|
316
|
+
error,
|
|
317
|
+
type,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Also call the onX handler if it exists
|
|
324
|
+
switch (type) {
|
|
325
|
+
case "open":
|
|
326
|
+
if (this.#onopen) {
|
|
327
|
+
try {
|
|
328
|
+
this.#onopen.call(this, event);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
logger()?.error({ msg: "error in onopen handler", error });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
break;
|
|
334
|
+
case "close":
|
|
335
|
+
if (this.#onclose) {
|
|
336
|
+
try {
|
|
337
|
+
this.#onclose.call(this, event);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
logger()?.error({ msg: "error in onclose handler", error });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
break;
|
|
343
|
+
case "error":
|
|
344
|
+
if (this.#onerror) {
|
|
345
|
+
try {
|
|
346
|
+
this.#onerror.call(this, event);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
logger()?.error({ msg: "error in onerror handler", error });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
break;
|
|
352
|
+
case "message":
|
|
353
|
+
if (this.#onmessage) {
|
|
354
|
+
try {
|
|
355
|
+
this.#onmessage.call(this, event);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
logger()?.error({ msg: "error in onmessage handler", error });
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Internal methods called by the Tunnel class
|
|
366
|
+
_handleOpen(): void {
|
|
367
|
+
if (this.#readyState !== 0) { // CONNECTING
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
this.#readyState = 1; // OPEN
|
|
372
|
+
|
|
373
|
+
const event = {
|
|
374
|
+
type: "open",
|
|
375
|
+
target: this,
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
this.#fireEvent("open", event);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
_handleMessage(data: string | Uint8Array, isBinary: boolean): void {
|
|
382
|
+
if (this.#readyState !== 1) { // OPEN
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
let messageData: any;
|
|
387
|
+
|
|
388
|
+
if (isBinary) {
|
|
389
|
+
// Handle binary data based on binaryType
|
|
390
|
+
if (this.#binaryType === "nodebuffer") {
|
|
391
|
+
// Convert to Buffer for Node.js compatibility
|
|
392
|
+
messageData = Buffer.from(data as Uint8Array);
|
|
393
|
+
} else if (this.#binaryType === "arraybuffer") {
|
|
394
|
+
// Convert to ArrayBuffer
|
|
395
|
+
if (data instanceof Uint8Array) {
|
|
396
|
+
messageData = data.buffer.slice(
|
|
397
|
+
data.byteOffset,
|
|
398
|
+
data.byteOffset + data.byteLength
|
|
399
|
+
);
|
|
400
|
+
} else {
|
|
401
|
+
messageData = data;
|
|
402
|
+
}
|
|
403
|
+
} else {
|
|
404
|
+
// Blob type - not commonly used in Node.js
|
|
405
|
+
throw new Error("Blob binaryType not supported in tunnel adapter");
|
|
406
|
+
}
|
|
407
|
+
} else {
|
|
408
|
+
messageData = data;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const event = {
|
|
412
|
+
data: messageData,
|
|
413
|
+
type: "message",
|
|
414
|
+
target: this,
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
this.#fireEvent("message", event);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
_handleClose(code?: number, reason?: string): void {
|
|
421
|
+
if (this.#readyState === 3) { // CLOSED
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
this.#readyState = 3; // CLOSED
|
|
426
|
+
|
|
427
|
+
const event = {
|
|
428
|
+
wasClean: true,
|
|
429
|
+
code: code || 1000,
|
|
430
|
+
reason: reason || "",
|
|
431
|
+
type: "close",
|
|
432
|
+
target: this,
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
this.#fireEvent("close", event);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
_handleError(error: Error): void {
|
|
439
|
+
const event = {
|
|
440
|
+
type: "error",
|
|
441
|
+
target: this,
|
|
442
|
+
error,
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
this.#fireEvent("error", event);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// WebSocket constants for compatibility
|
|
449
|
+
static readonly CONNECTING = 0;
|
|
450
|
+
static readonly OPEN = 1;
|
|
451
|
+
static readonly CLOSING = 2;
|
|
452
|
+
static readonly CLOSED = 3;
|
|
453
|
+
|
|
454
|
+
// Instance constants
|
|
455
|
+
readonly CONNECTING = 0;
|
|
456
|
+
readonly OPEN = 1;
|
|
457
|
+
readonly CLOSING = 2;
|
|
458
|
+
readonly CLOSED = 3;
|
|
459
|
+
|
|
460
|
+
// Additional methods for compatibility
|
|
461
|
+
ping(data?: any, mask?: boolean, cb?: (err: Error) => void): void {
|
|
462
|
+
// Not implemented for tunnel - could be added if needed
|
|
463
|
+
if (cb) cb(new Error("Ping not supported in tunnel adapter"));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
pong(data?: any, mask?: boolean, cb?: (err: Error) => void): void {
|
|
467
|
+
// Not implemented for tunnel - could be added if needed
|
|
468
|
+
if (cb) cb(new Error("Pong not supported in tunnel adapter"));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
terminate(): void {
|
|
472
|
+
// Immediate close without close frame
|
|
473
|
+
this.#readyState = 3; // CLOSED
|
|
474
|
+
this.#closeCallback(1006, "Abnormal Closure");
|
|
475
|
+
|
|
476
|
+
const event = {
|
|
477
|
+
wasClean: false,
|
|
478
|
+
code: 1006,
|
|
479
|
+
reason: "Abnormal Closure",
|
|
480
|
+
type: "close",
|
|
481
|
+
target: this,
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
this.#fireEvent("close", event);
|
|
485
|
+
}
|
|
486
|
+
}
|
package/src/websocket.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { logger } from "./log";
|
|
2
|
+
|
|
3
|
+
// Global singleton promise that will be reused for subsequent calls
|
|
4
|
+
let webSocketPromise: Promise<typeof WebSocket> | null = null;
|
|
5
|
+
|
|
6
|
+
export async function importWebSocket(): Promise<typeof WebSocket> {
|
|
7
|
+
// Return existing promise if we already started loading
|
|
8
|
+
if (webSocketPromise !== null) {
|
|
9
|
+
return webSocketPromise;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Create and store the promise
|
|
13
|
+
webSocketPromise = (async () => {
|
|
14
|
+
let _WebSocket: typeof WebSocket;
|
|
15
|
+
|
|
16
|
+
if (typeof WebSocket !== "undefined") {
|
|
17
|
+
// Native
|
|
18
|
+
_WebSocket = WebSocket as unknown as typeof WebSocket;
|
|
19
|
+
logger()?.debug({ msg: "using native websocket" });
|
|
20
|
+
} else {
|
|
21
|
+
// Node.js package
|
|
22
|
+
try {
|
|
23
|
+
const ws = await import("ws");
|
|
24
|
+
_WebSocket = ws.default as unknown as typeof WebSocket;
|
|
25
|
+
logger()?.debug({ msg: "using websocket from npm" });
|
|
26
|
+
} catch {
|
|
27
|
+
// WS not available
|
|
28
|
+
_WebSocket = class MockWebSocket {
|
|
29
|
+
constructor() {
|
|
30
|
+
throw new Error(
|
|
31
|
+
'WebSocket support requires installing the "ws" peer dependency.',
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
} as unknown as typeof WebSocket;
|
|
35
|
+
logger()?.debug({ msg: "using mock websocket" });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return _WebSocket;
|
|
40
|
+
})();
|
|
41
|
+
|
|
42
|
+
return webSocketPromise;
|
|
43
|
+
}
|