@rivetkit/engine-runner 2.0.21

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.
@@ -0,0 +1,538 @@
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 (
52
+ value === "nodebuffer" ||
53
+ value === "arraybuffer" ||
54
+ value === "blob"
55
+ ) {
56
+ this.#binaryType = value;
57
+ }
58
+ }
59
+
60
+ get extensions(): string {
61
+ return this.#extensions;
62
+ }
63
+
64
+ get protocol(): string {
65
+ return this.#protocol;
66
+ }
67
+
68
+ get url(): string {
69
+ return this.#url;
70
+ }
71
+
72
+ get onopen(): ((this: any, ev: any) => any) | null {
73
+ return this.#onopen;
74
+ }
75
+
76
+ set onopen(value: ((this: any, ev: any) => any) | null) {
77
+ this.#onopen = value;
78
+ // Flush any buffered open events when onopen is set
79
+ if (value) {
80
+ this.#flushBufferedEvents("open");
81
+ }
82
+ }
83
+
84
+ get onclose(): ((this: any, ev: any) => any) | null {
85
+ return this.#onclose;
86
+ }
87
+
88
+ set onclose(value: ((this: any, ev: any) => any) | null) {
89
+ this.#onclose = value;
90
+ // Flush any buffered close events when onclose is set
91
+ if (value) {
92
+ this.#flushBufferedEvents("close");
93
+ }
94
+ }
95
+
96
+ get onerror(): ((this: any, ev: any) => any) | null {
97
+ return this.#onerror;
98
+ }
99
+
100
+ set onerror(value: ((this: any, ev: any) => any) | null) {
101
+ this.#onerror = value;
102
+ // Flush any buffered error events when onerror is set
103
+ if (value) {
104
+ this.#flushBufferedEvents("error");
105
+ }
106
+ }
107
+
108
+ get onmessage(): ((this: any, ev: any) => any) | null {
109
+ return this.#onmessage;
110
+ }
111
+
112
+ set onmessage(value: ((this: any, ev: any) => any) | null) {
113
+ this.#onmessage = value;
114
+ // Flush any buffered message events when onmessage is set
115
+ if (value) {
116
+ this.#flushBufferedEvents("message");
117
+ }
118
+ }
119
+
120
+ send(data: string | ArrayBuffer | ArrayBufferView | Blob | Buffer): void {
121
+ if (this.#readyState !== 1) {
122
+ // OPEN
123
+ throw new Error("WebSocket is not open");
124
+ }
125
+
126
+ let isBinary = false;
127
+ let messageData: string | ArrayBuffer;
128
+
129
+ if (typeof data === "string") {
130
+ messageData = data;
131
+ } else if (data instanceof ArrayBuffer) {
132
+ isBinary = true;
133
+ messageData = data;
134
+ } else if (ArrayBuffer.isView(data)) {
135
+ isBinary = true;
136
+ // Convert ArrayBufferView to ArrayBuffer
137
+ const view = data as ArrayBufferView;
138
+ // Check if it's a SharedArrayBuffer
139
+ if (view.buffer instanceof SharedArrayBuffer) {
140
+ // Copy SharedArrayBuffer to regular ArrayBuffer
141
+ const bytes = new Uint8Array(
142
+ view.buffer,
143
+ view.byteOffset,
144
+ view.byteLength,
145
+ );
146
+ messageData = bytes.buffer.slice(
147
+ bytes.byteOffset,
148
+ bytes.byteOffset + bytes.byteLength,
149
+ ) as unknown as ArrayBuffer;
150
+ } else {
151
+ messageData = view.buffer.slice(
152
+ view.byteOffset,
153
+ view.byteOffset + view.byteLength,
154
+ ) as ArrayBuffer;
155
+ }
156
+ } else if (data instanceof Blob) {
157
+ throw new Error("Blob sending not implemented in tunnel adapter");
158
+ } else if (typeof Buffer !== "undefined" && Buffer.isBuffer(data)) {
159
+ isBinary = true;
160
+ // Convert Buffer to ArrayBuffer
161
+ const buf = data as Buffer;
162
+ // Check if it's a SharedArrayBuffer
163
+ if (buf.buffer instanceof SharedArrayBuffer) {
164
+ // Copy SharedArrayBuffer to regular ArrayBuffer
165
+ const bytes = new Uint8Array(
166
+ buf.buffer,
167
+ buf.byteOffset,
168
+ buf.byteLength,
169
+ );
170
+ messageData = bytes.buffer.slice(
171
+ bytes.byteOffset,
172
+ bytes.byteOffset + bytes.byteLength,
173
+ ) as unknown as ArrayBuffer;
174
+ } else {
175
+ messageData = buf.buffer.slice(
176
+ buf.byteOffset,
177
+ buf.byteOffset + buf.byteLength,
178
+ ) as ArrayBuffer;
179
+ }
180
+ } else {
181
+ throw new Error("Invalid data type");
182
+ }
183
+
184
+ // Send through tunnel
185
+ this.#sendCallback(messageData, isBinary);
186
+ }
187
+
188
+ close(code?: number, reason?: string): void {
189
+ if (
190
+ this.#readyState === 2 || // CLOSING
191
+ this.#readyState === 3 // CLOSED
192
+ ) {
193
+ return;
194
+ }
195
+
196
+ this.#readyState = 2; // CLOSING
197
+
198
+ // Send close through tunnel
199
+ this.#closeCallback(code, reason);
200
+
201
+ // Update state and fire event
202
+ this.#readyState = 3; // CLOSED
203
+
204
+ const closeEvent = {
205
+ wasClean: true,
206
+ code: code || 1000,
207
+ reason: reason || "",
208
+ type: "close",
209
+ target: this,
210
+ };
211
+
212
+ this.#fireEvent("close", closeEvent);
213
+ }
214
+
215
+ addEventListener(
216
+ type: string,
217
+ listener: (event: any) => void,
218
+ options?: boolean | any,
219
+ ): void {
220
+ if (typeof listener === "function") {
221
+ let listeners = this.#eventListeners.get(type);
222
+ if (!listeners) {
223
+ listeners = new Set();
224
+ this.#eventListeners.set(type, listeners);
225
+ }
226
+ listeners.add(listener);
227
+
228
+ // Flush any buffered events for this type
229
+ this.#flushBufferedEvents(type);
230
+ }
231
+ }
232
+
233
+ removeEventListener(
234
+ type: string,
235
+ listener: (event: any) => void,
236
+ options?: boolean | any,
237
+ ): void {
238
+ if (typeof listener === "function") {
239
+ const listeners = this.#eventListeners.get(type);
240
+ if (listeners) {
241
+ listeners.delete(listener);
242
+ }
243
+ }
244
+ }
245
+
246
+ dispatchEvent(event: any): boolean {
247
+ // Simple implementation
248
+ return true;
249
+ }
250
+
251
+ #fireEvent(type: string, event: any): void {
252
+ // Call all registered event listeners
253
+ const listeners = this.#eventListeners.get(type);
254
+ let hasListeners = false;
255
+
256
+ if (listeners && listeners.size > 0) {
257
+ hasListeners = true;
258
+ for (const listener of listeners) {
259
+ try {
260
+ listener.call(this, event);
261
+ } catch (error) {
262
+ logger()?.error({
263
+ msg: "error in websocket event listener",
264
+ error,
265
+ type,
266
+ });
267
+ }
268
+ }
269
+ }
270
+
271
+ // Call the onX property if set
272
+ switch (type) {
273
+ case "open":
274
+ if (this.#onopen) {
275
+ hasListeners = true;
276
+ try {
277
+ this.#onopen.call(this, event);
278
+ } catch (error) {
279
+ logger()?.error({
280
+ msg: "error in onopen handler",
281
+ error,
282
+ });
283
+ }
284
+ }
285
+ break;
286
+ case "close":
287
+ if (this.#onclose) {
288
+ hasListeners = true;
289
+ try {
290
+ this.#onclose.call(this, event);
291
+ } catch (error) {
292
+ logger()?.error({
293
+ msg: "error in onclose handler",
294
+ error,
295
+ });
296
+ }
297
+ }
298
+ break;
299
+ case "error":
300
+ if (this.#onerror) {
301
+ hasListeners = true;
302
+ try {
303
+ this.#onerror.call(this, event);
304
+ } catch (error) {
305
+ logger()?.error({
306
+ msg: "error in onerror handler",
307
+ error,
308
+ });
309
+ }
310
+ }
311
+ break;
312
+ case "message":
313
+ if (this.#onmessage) {
314
+ hasListeners = true;
315
+ try {
316
+ this.#onmessage.call(this, event);
317
+ } catch (error) {
318
+ logger()?.error({
319
+ msg: "error in onmessage handler",
320
+ error,
321
+ });
322
+ }
323
+ }
324
+ break;
325
+ }
326
+
327
+ // Buffer the event if no listeners are registered
328
+ if (!hasListeners) {
329
+ this.#bufferedEvents.push({ type, event });
330
+ }
331
+ }
332
+
333
+ #flushBufferedEvents(type: string): void {
334
+ const eventsToFlush = this.#bufferedEvents.filter(
335
+ (buffered) => buffered.type === type,
336
+ );
337
+ this.#bufferedEvents = this.#bufferedEvents.filter(
338
+ (buffered) => buffered.type !== type,
339
+ );
340
+
341
+ for (const { event } of eventsToFlush) {
342
+ // Re-fire the event, which will now have listeners
343
+ const listeners = this.#eventListeners.get(type);
344
+ if (listeners) {
345
+ for (const listener of listeners) {
346
+ try {
347
+ listener.call(this, event);
348
+ } catch (error) {
349
+ logger()?.error({
350
+ msg: "error in websocket event listener",
351
+ error,
352
+ type,
353
+ });
354
+ }
355
+ }
356
+ }
357
+
358
+ // Also call the onX handler if it exists
359
+ switch (type) {
360
+ case "open":
361
+ if (this.#onopen) {
362
+ try {
363
+ this.#onopen.call(this, event);
364
+ } catch (error) {
365
+ logger()?.error({
366
+ msg: "error in onopen handler",
367
+ error,
368
+ });
369
+ }
370
+ }
371
+ break;
372
+ case "close":
373
+ if (this.#onclose) {
374
+ try {
375
+ this.#onclose.call(this, event);
376
+ } catch (error) {
377
+ logger()?.error({
378
+ msg: "error in onclose handler",
379
+ error,
380
+ });
381
+ }
382
+ }
383
+ break;
384
+ case "error":
385
+ if (this.#onerror) {
386
+ try {
387
+ this.#onerror.call(this, event);
388
+ } catch (error) {
389
+ logger()?.error({
390
+ msg: "error in onerror handler",
391
+ error,
392
+ });
393
+ }
394
+ }
395
+ break;
396
+ case "message":
397
+ if (this.#onmessage) {
398
+ try {
399
+ this.#onmessage.call(this, event);
400
+ } catch (error) {
401
+ logger()?.error({
402
+ msg: "error in onmessage handler",
403
+ error,
404
+ });
405
+ }
406
+ }
407
+ break;
408
+ }
409
+ }
410
+ }
411
+
412
+ // Internal methods called by the Tunnel class
413
+ _handleOpen(): void {
414
+ if (this.#readyState !== 0) {
415
+ // CONNECTING
416
+ return;
417
+ }
418
+
419
+ this.#readyState = 1; // OPEN
420
+
421
+ const event = {
422
+ type: "open",
423
+ target: this,
424
+ };
425
+
426
+ this.#fireEvent("open", event);
427
+ }
428
+
429
+ _handleMessage(data: string | Uint8Array, isBinary: boolean): void {
430
+ if (this.#readyState !== 1) {
431
+ // OPEN
432
+ return;
433
+ }
434
+
435
+ let messageData: any;
436
+
437
+ if (isBinary) {
438
+ // Handle binary data based on binaryType
439
+ if (this.#binaryType === "nodebuffer") {
440
+ // Convert to Buffer for Node.js compatibility
441
+ messageData = Buffer.from(data as Uint8Array);
442
+ } else if (this.#binaryType === "arraybuffer") {
443
+ // Convert to ArrayBuffer
444
+ if (data instanceof Uint8Array) {
445
+ messageData = data.buffer.slice(
446
+ data.byteOffset,
447
+ data.byteOffset + data.byteLength,
448
+ );
449
+ } else {
450
+ messageData = data;
451
+ }
452
+ } else {
453
+ // Blob type - not commonly used in Node.js
454
+ throw new Error(
455
+ "Blob binaryType not supported in tunnel adapter",
456
+ );
457
+ }
458
+ } else {
459
+ messageData = data;
460
+ }
461
+
462
+ const event = {
463
+ data: messageData,
464
+ type: "message",
465
+ target: this,
466
+ };
467
+
468
+ this.#fireEvent("message", event);
469
+ }
470
+
471
+ _handleClose(code?: number, reason?: string): void {
472
+ if (this.#readyState === 3) {
473
+ // CLOSED
474
+ return;
475
+ }
476
+
477
+ this.#readyState = 3; // CLOSED
478
+
479
+ const event = {
480
+ wasClean: true,
481
+ code: code || 1000,
482
+ reason: reason || "",
483
+ type: "close",
484
+ target: this,
485
+ };
486
+
487
+ this.#fireEvent("close", event);
488
+ }
489
+
490
+ _handleError(error: Error): void {
491
+ const event = {
492
+ type: "error",
493
+ target: this,
494
+ error,
495
+ };
496
+
497
+ this.#fireEvent("error", event);
498
+ }
499
+
500
+ // WebSocket constants for compatibility
501
+ static readonly CONNECTING = 0;
502
+ static readonly OPEN = 1;
503
+ static readonly CLOSING = 2;
504
+ static readonly CLOSED = 3;
505
+
506
+ // Instance constants
507
+ readonly CONNECTING = 0;
508
+ readonly OPEN = 1;
509
+ readonly CLOSING = 2;
510
+ readonly CLOSED = 3;
511
+
512
+ // Additional methods for compatibility
513
+ ping(data?: any, mask?: boolean, cb?: (err: Error) => void): void {
514
+ // Not implemented for tunnel - could be added if needed
515
+ if (cb) cb(new Error("Ping not supported in tunnel adapter"));
516
+ }
517
+
518
+ pong(data?: any, mask?: boolean, cb?: (err: Error) => void): void {
519
+ // Not implemented for tunnel - could be added if needed
520
+ if (cb) cb(new Error("Pong not supported in tunnel adapter"));
521
+ }
522
+
523
+ terminate(): void {
524
+ // Immediate close without close frame
525
+ this.#readyState = 3; // CLOSED
526
+ this.#closeCallback(1006, "Abnormal Closure");
527
+
528
+ const event = {
529
+ wasClean: false,
530
+ code: 1006,
531
+ reason: "Abnormal Closure",
532
+ type: "close",
533
+ target: this,
534
+ };
535
+
536
+ this.#fireEvent("close", event);
537
+ }
538
+ }
@@ -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
+ }