@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/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
+ }
@@ -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
+ }