@rivetkit/engine-runner 2.0.24-rc.1 → 2.0.24

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.
@@ -1,10 +1,15 @@
1
1
  // WebSocket-like adapter for tunneled connections
2
2
  // Implements a subset of the WebSocket interface for compatibility with runner code
3
3
 
4
+ import type { Logger } from "pino";
4
5
  import { logger } from "./log";
6
+ import type { Tunnel } from "./tunnel";
7
+ import { wrappingAddU16, wrappingLteU16, wrappingSubU16 } from "./utils";
8
+
9
+ export const HIBERNATABLE_SYMBOL = Symbol("hibernatable");
5
10
 
6
11
  export class WebSocketTunnelAdapter {
7
- #webSocketId: string;
12
+ // MARK: - WebSocket Compat Variables
8
13
  #readyState: number = 0; // CONNECTING
9
14
  #eventListeners: Map<string, Set<(event: any) => void>> = new Map();
10
15
  #onopen: ((this: any, ev: any) => any) | null = null;
@@ -16,38 +21,340 @@ export class WebSocketTunnelAdapter {
16
21
  #extensions = "";
17
22
  #protocol = "";
18
23
  #url = "";
24
+
25
+ // mARK: - Internal State
26
+ #tunnel: Tunnel;
27
+ #actorId: string;
28
+ #requestId: string;
29
+ #hibernatable: boolean;
30
+ #serverMessageIndex: number;
31
+
32
+ get [HIBERNATABLE_SYMBOL](): boolean {
33
+ return this.#hibernatable;
34
+ }
35
+
36
+ /**
37
+ * Called when sending a message from this WebSocket.
38
+ *
39
+ * Used to send a tunnel message from Tunnel.
40
+ */
19
41
  #sendCallback: (data: ArrayBuffer | string, isBinary: boolean) => void;
20
- #closeCallback: (code?: number, reason?: string, retry?: boolean) => void;
21
- #canHibernate: boolean = false;
22
42
 
23
- // Event buffering for events fired before listeners are attached
24
- #bufferedEvents: Array<{
25
- type: string;
26
- event: any;
27
- }> = [];
43
+ /**
44
+ * Called when closing this WebSocket.
45
+ *
46
+ * Used to send a tunnel message from Tunnel
47
+ */
48
+ #closeCallback: (
49
+ code?: number,
50
+ reason?: string,
51
+ hibernate?: boolean,
52
+ ) => void;
53
+
54
+ get #log(): Logger | undefined {
55
+ return this.#tunnel.log;
56
+ }
28
57
 
29
58
  constructor(
30
- webSocketId: string,
59
+ tunnel: Tunnel,
60
+ actorId: string,
61
+ requestId: string,
62
+ serverMessageIndex: number,
63
+ hibernatable: boolean,
64
+ isRestoringHibernatable: boolean,
65
+ /** @experimental */
66
+ public readonly request: Request,
31
67
  sendCallback: (data: ArrayBuffer | string, isBinary: boolean) => void,
32
- closeCallback: (
33
- code?: number,
34
- reason?: string,
35
- retry?: boolean,
36
- ) => void,
68
+ closeCallback: (code?: number, reason?: string) => void,
37
69
  ) {
38
- this.#webSocketId = webSocketId;
70
+ this.#tunnel = tunnel;
71
+ this.#actorId = actorId;
72
+ this.#requestId = requestId;
73
+ this.#hibernatable = hibernatable;
74
+ this.#serverMessageIndex = serverMessageIndex;
39
75
  this.#sendCallback = sendCallback;
40
76
  this.#closeCallback = closeCallback;
41
- }
42
77
 
43
- get readyState(): number {
44
- return this.#readyState;
78
+ // For restored WebSockets, immediately set to OPEN state
79
+ if (isRestoringHibernatable) {
80
+ this.#log?.debug({
81
+ msg: "setting WebSocket to OPEN state for restored connection",
82
+ actorId: this.#actorId,
83
+ requestId: this.#requestId,
84
+ hibernatable: this.#hibernatable,
85
+ });
86
+ this.#readyState = 1; // OPEN
87
+ }
45
88
  }
46
89
 
90
+ // MARK: - Lifecycle
47
91
  get bufferedAmount(): number {
48
92
  return this.#bufferedAmount;
49
93
  }
50
94
 
95
+ _handleOpen(requestId: ArrayBuffer): void {
96
+ if (this.#readyState !== 0) {
97
+ // CONNECTING
98
+ return;
99
+ }
100
+
101
+ this.#readyState = 1; // OPEN
102
+
103
+ const event = {
104
+ type: "open",
105
+ rivetRequestId: requestId,
106
+ target: this,
107
+ };
108
+
109
+ this.#fireEvent("open", event);
110
+ }
111
+
112
+ _handleMessage(
113
+ requestId: ArrayBuffer,
114
+ data: string | Uint8Array,
115
+ serverMessageIndex: number,
116
+ isBinary: boolean,
117
+ ): boolean {
118
+ if (this.#readyState !== 1) {
119
+ this.#log?.warn({
120
+ msg: "WebSocket message ignored - not in OPEN state",
121
+ requestId: this.#requestId,
122
+ actorId: this.#actorId,
123
+ currentReadyState: this.#readyState,
124
+ expectedReadyState: 1,
125
+ serverMessageIndex: serverMessageIndex,
126
+ hibernatable: this.#hibernatable,
127
+ });
128
+ return true;
129
+ }
130
+
131
+ // Validate message index
132
+ if (this.#hibernatable) {
133
+ const previousIndex = this.#serverMessageIndex;
134
+
135
+ // Ignore duplicate old messages
136
+ //
137
+ // This should only happen if something goes wrong
138
+ // between persisting the previous index and acking the
139
+ // message index to the gateway. If the ack is never
140
+ // received by the gateway (due to a crash or network
141
+ // issue), the gateway will resend all messages from
142
+ // the last ack on reconnect.
143
+ if (wrappingLteU16(serverMessageIndex, previousIndex)) {
144
+ this.#log?.info({
145
+ msg: "received duplicate hibernating websocket message, this indicates the actor failed to ack the message index before restarting",
146
+ requestId,
147
+ actorId: this.#actorId,
148
+ previousIndex,
149
+ expectedIndex: wrappingAddU16(previousIndex, 1),
150
+ receivedIndex: serverMessageIndex,
151
+ });
152
+
153
+ return true;
154
+ }
155
+
156
+ // Close message if skipped message in sequence
157
+ //
158
+ // There is no scenario where this should ever happen
159
+ const expectedIndex = wrappingAddU16(previousIndex, 1);
160
+ if (serverMessageIndex !== expectedIndex) {
161
+ const closeReason = "ws.message_index_skip";
162
+
163
+ this.#log?.warn({
164
+ msg: "hibernatable websocket message index out of sequence, closing connection",
165
+ requestId,
166
+ actorId: this.#actorId,
167
+ previousIndex,
168
+ expectedIndex,
169
+ receivedIndex: serverMessageIndex,
170
+ closeReason,
171
+ gap: wrappingSubU16(
172
+ wrappingSubU16(serverMessageIndex, previousIndex),
173
+ 1,
174
+ ),
175
+ });
176
+
177
+ // Close the WebSocket and skip processing
178
+ this.close(1008, closeReason);
179
+
180
+ return true;
181
+ }
182
+
183
+ // Update to the next index
184
+ this.#serverMessageIndex = serverMessageIndex;
185
+ }
186
+
187
+ // Dispatch event
188
+ let messageData: any;
189
+ if (isBinary) {
190
+ // Handle binary data based on binaryType
191
+ if (this.#binaryType === "nodebuffer") {
192
+ // Convert to Buffer for Node.js compatibility
193
+ messageData = Buffer.from(data as Uint8Array);
194
+ } else if (this.#binaryType === "arraybuffer") {
195
+ // Convert to ArrayBuffer
196
+ if (data instanceof Uint8Array) {
197
+ messageData = data.buffer.slice(
198
+ data.byteOffset,
199
+ data.byteOffset + data.byteLength,
200
+ );
201
+ } else {
202
+ messageData = data;
203
+ }
204
+ } else {
205
+ // Blob type - not commonly used in Node.js
206
+ throw new Error(
207
+ "Blob binaryType not supported in tunnel adapter",
208
+ );
209
+ }
210
+ } else {
211
+ messageData = data;
212
+ }
213
+
214
+ const event = {
215
+ type: "message",
216
+ data: messageData,
217
+ rivetRequestId: requestId,
218
+ rivetMessageIndex: serverMessageIndex,
219
+ target: this,
220
+ };
221
+
222
+ this.#fireEvent("message", event);
223
+
224
+ return false;
225
+ }
226
+
227
+ _handleClose(
228
+ _requestId: ArrayBuffer,
229
+ code?: number,
230
+ reason?: string,
231
+ ): void {
232
+ this.#closeInner(code, reason, true);
233
+ }
234
+
235
+ _handleError(error: Error): void {
236
+ const event = {
237
+ type: "error",
238
+ target: this,
239
+ error,
240
+ };
241
+
242
+ this.#fireEvent("error", event);
243
+ }
244
+
245
+ _closeWithoutCallback(code?: number, reason?: string): void {
246
+ this.#closeInner(code, reason, false);
247
+ }
248
+
249
+ #fireEvent(type: string, event: any): void {
250
+ // Call all registered event listeners
251
+ const listeners = this.#eventListeners.get(type);
252
+
253
+ if (listeners && listeners.size > 0) {
254
+ for (const listener of listeners) {
255
+ try {
256
+ listener.call(this, event);
257
+ } catch (error) {
258
+ logger()?.error({
259
+ msg: "error in websocket event listener",
260
+ error,
261
+ type,
262
+ });
263
+ }
264
+ }
265
+ }
266
+
267
+ // Call the onX property if set
268
+ switch (type) {
269
+ case "open":
270
+ if (this.#onopen) {
271
+ try {
272
+ this.#onopen.call(this, event);
273
+ } catch (error) {
274
+ logger()?.error({
275
+ msg: "error in onopen handler",
276
+ error,
277
+ });
278
+ }
279
+ }
280
+ break;
281
+ case "close":
282
+ if (this.#onclose) {
283
+ try {
284
+ this.#onclose.call(this, event);
285
+ } catch (error) {
286
+ logger()?.error({
287
+ msg: "error in onclose handler",
288
+ error,
289
+ });
290
+ }
291
+ }
292
+ break;
293
+ case "error":
294
+ if (this.#onerror) {
295
+ try {
296
+ this.#onerror.call(this, event);
297
+ } catch (error) {
298
+ logger()?.error({
299
+ msg: "error in onerror handler",
300
+ error,
301
+ });
302
+ }
303
+ }
304
+ break;
305
+ case "message":
306
+ if (this.#onmessage) {
307
+ try {
308
+ this.#onmessage.call(this, event);
309
+ } catch (error) {
310
+ logger()?.error({
311
+ msg: "error in onmessage handler",
312
+ error,
313
+ });
314
+ }
315
+ }
316
+ break;
317
+ }
318
+ }
319
+
320
+ #closeInner(
321
+ code: number | undefined,
322
+ reason: string | undefined,
323
+ callback: boolean,
324
+ ): void {
325
+ if (
326
+ this.#readyState === 2 || // CLOSING
327
+ this.#readyState === 3 // CLOSED
328
+ ) {
329
+ return;
330
+ }
331
+
332
+ this.#readyState = 2; // CLOSING
333
+
334
+ // Send close through tunnel
335
+ if (callback) {
336
+ this.#closeCallback(code, reason);
337
+ }
338
+
339
+ // Update state and fire event
340
+ this.#readyState = 3; // CLOSED
341
+
342
+ const closeEvent = {
343
+ wasClean: true,
344
+ code: code || 1000,
345
+ reason: reason || "",
346
+ type: "close",
347
+ target: this,
348
+ };
349
+
350
+ this.#fireEvent("close", closeEvent);
351
+ }
352
+
353
+ // MARK: - WebSocket Compatible API
354
+ get readyState(): number {
355
+ return this.#readyState;
356
+ }
357
+
51
358
  get binaryType(): string {
52
359
  return this.#binaryType;
53
360
  }
@@ -74,26 +381,12 @@ export class WebSocketTunnelAdapter {
74
381
  return this.#url;
75
382
  }
76
383
 
77
- /** @experimental */
78
- get canHibernate(): boolean {
79
- return this.#canHibernate;
80
- }
81
-
82
- /** @experimental */
83
- set canHibernate(value: boolean) {
84
- this.#canHibernate = value;
85
- }
86
-
87
384
  get onopen(): ((this: any, ev: any) => any) | null {
88
385
  return this.#onopen;
89
386
  }
90
387
 
91
388
  set onopen(value: ((this: any, ev: any) => any) | null) {
92
389
  this.#onopen = value;
93
- // Flush any buffered open events when onopen is set
94
- if (value) {
95
- this.#flushBufferedEvents("open");
96
- }
97
390
  }
98
391
 
99
392
  get onclose(): ((this: any, ev: any) => any) | null {
@@ -102,10 +395,6 @@ export class WebSocketTunnelAdapter {
102
395
 
103
396
  set onclose(value: ((this: any, ev: any) => any) | null) {
104
397
  this.#onclose = value;
105
- // Flush any buffered close events when onclose is set
106
- if (value) {
107
- this.#flushBufferedEvents("close");
108
- }
109
398
  }
110
399
 
111
400
  get onerror(): ((this: any, ev: any) => any) | null {
@@ -114,10 +403,6 @@ export class WebSocketTunnelAdapter {
114
403
 
115
404
  set onerror(value: ((this: any, ev: any) => any) | null) {
116
405
  this.#onerror = value;
117
- // Flush any buffered error events when onerror is set
118
- if (value) {
119
- this.#flushBufferedEvents("error");
120
- }
121
406
  }
122
407
 
123
408
  get onmessage(): ((this: any, ev: any) => any) | null {
@@ -126,16 +411,21 @@ export class WebSocketTunnelAdapter {
126
411
 
127
412
  set onmessage(value: ((this: any, ev: any) => any) | null) {
128
413
  this.#onmessage = value;
129
- // Flush any buffered message events when onmessage is set
130
- if (value) {
131
- this.#flushBufferedEvents("message");
132
- }
133
414
  }
134
415
 
135
416
  send(data: string | ArrayBuffer | ArrayBufferView | Blob | Buffer): void {
136
- if (this.#readyState !== 1) {
137
- // OPEN
138
- throw new Error("WebSocket is not open");
417
+ // Handle different ready states
418
+ if (this.#readyState === 0) {
419
+ // CONNECTING
420
+ throw new DOMException(
421
+ "WebSocket is still in CONNECTING state",
422
+ "InvalidStateError",
423
+ );
424
+ }
425
+
426
+ if (this.#readyState === 2 || this.#readyState === 3) {
427
+ // CLOSING or CLOSED - silently ignore
428
+ return;
139
429
  }
140
430
 
141
431
  let isBinary = false;
@@ -201,49 +491,7 @@ export class WebSocketTunnelAdapter {
201
491
  }
202
492
 
203
493
  close(code?: number, reason?: string): void {
204
- this.closeInner(code, reason, false, true);
205
- }
206
-
207
- __closeWithRetry(code?: number, reason?: string): void {
208
- this.closeInner(code, reason, true, true);
209
- }
210
-
211
- __closeWithoutCallback(code?: number, reason?: string): void {
212
- this.closeInner(code, reason, false, false);
213
- }
214
-
215
- closeInner(
216
- code: number | undefined,
217
- reason: string | undefined,
218
- retry: boolean,
219
- callback: boolean,
220
- ): void {
221
- if (
222
- this.#readyState === 2 || // CLOSING
223
- this.#readyState === 3 // CLOSED
224
- ) {
225
- return;
226
- }
227
-
228
- this.#readyState = 2; // CLOSING
229
-
230
- // Send close through tunnel
231
- if (callback) {
232
- this.#closeCallback(code, reason, retry);
233
- }
234
-
235
- // Update state and fire event
236
- this.#readyState = 3; // CLOSED
237
-
238
- const closeEvent = {
239
- wasClean: true,
240
- code: code || 1000,
241
- reason: reason || "",
242
- type: "close",
243
- target: this,
244
- };
245
-
246
- this.#fireEvent("close", closeEvent);
494
+ this.#closeInner(code, reason, true);
247
495
  }
248
496
 
249
497
  addEventListener(
@@ -258,9 +506,6 @@ export class WebSocketTunnelAdapter {
258
506
  this.#eventListeners.set(type, listeners);
259
507
  }
260
508
  listeners.add(listener);
261
-
262
- // Flush any buffered events for this type
263
- this.#flushBufferedEvents(type);
264
509
  }
265
510
  }
266
511
 
@@ -278,278 +523,15 @@ export class WebSocketTunnelAdapter {
278
523
  }
279
524
 
280
525
  dispatchEvent(event: any): boolean {
281
- // Simple implementation
526
+ // TODO:
282
527
  return true;
283
528
  }
284
529
 
285
- #fireEvent(type: string, event: any): void {
286
- // Call all registered event listeners
287
- const listeners = this.#eventListeners.get(type);
288
- let hasListeners = false;
289
-
290
- if (listeners && listeners.size > 0) {
291
- hasListeners = true;
292
- for (const listener of listeners) {
293
- try {
294
- listener.call(this, event);
295
- } catch (error) {
296
- logger()?.error({
297
- msg: "error in websocket event listener",
298
- error,
299
- type,
300
- });
301
- }
302
- }
303
- }
304
-
305
- // Call the onX property if set
306
- switch (type) {
307
- case "open":
308
- if (this.#onopen) {
309
- hasListeners = true;
310
- try {
311
- this.#onopen.call(this, event);
312
- } catch (error) {
313
- logger()?.error({
314
- msg: "error in onopen handler",
315
- error,
316
- });
317
- }
318
- }
319
- break;
320
- case "close":
321
- if (this.#onclose) {
322
- hasListeners = true;
323
- try {
324
- this.#onclose.call(this, event);
325
- } catch (error) {
326
- logger()?.error({
327
- msg: "error in onclose handler",
328
- error,
329
- });
330
- }
331
- }
332
- break;
333
- case "error":
334
- if (this.#onerror) {
335
- hasListeners = true;
336
- try {
337
- this.#onerror.call(this, event);
338
- } catch (error) {
339
- logger()?.error({
340
- msg: "error in onerror handler",
341
- error,
342
- });
343
- }
344
- }
345
- break;
346
- case "message":
347
- if (this.#onmessage) {
348
- hasListeners = true;
349
- try {
350
- this.#onmessage.call(this, event);
351
- } catch (error) {
352
- logger()?.error({
353
- msg: "error in onmessage handler",
354
- error,
355
- });
356
- }
357
- }
358
- break;
359
- }
360
-
361
- // Buffer the event if no listeners are registered
362
- if (!hasListeners) {
363
- this.#bufferedEvents.push({ type, event });
364
- }
365
- }
366
-
367
- #flushBufferedEvents(type: string): void {
368
- const eventsToFlush = this.#bufferedEvents.filter(
369
- (buffered) => buffered.type === type,
370
- );
371
- this.#bufferedEvents = this.#bufferedEvents.filter(
372
- (buffered) => buffered.type !== type,
373
- );
374
-
375
- for (const { event } of eventsToFlush) {
376
- // Re-fire the event, which will now have listeners
377
- const listeners = this.#eventListeners.get(type);
378
- if (listeners) {
379
- for (const listener of listeners) {
380
- try {
381
- listener.call(this, event);
382
- } catch (error) {
383
- logger()?.error({
384
- msg: "error in websocket event listener",
385
- error,
386
- type,
387
- });
388
- }
389
- }
390
- }
391
-
392
- // Also call the onX handler if it exists
393
- switch (type) {
394
- case "open":
395
- if (this.#onopen) {
396
- try {
397
- this.#onopen.call(this, event);
398
- } catch (error) {
399
- logger()?.error({
400
- msg: "error in onopen handler",
401
- error,
402
- });
403
- }
404
- }
405
- break;
406
- case "close":
407
- if (this.#onclose) {
408
- try {
409
- this.#onclose.call(this, event);
410
- } catch (error) {
411
- logger()?.error({
412
- msg: "error in onclose handler",
413
- error,
414
- });
415
- }
416
- }
417
- break;
418
- case "error":
419
- if (this.#onerror) {
420
- try {
421
- this.#onerror.call(this, event);
422
- } catch (error) {
423
- logger()?.error({
424
- msg: "error in onerror handler",
425
- error,
426
- });
427
- }
428
- }
429
- break;
430
- case "message":
431
- if (this.#onmessage) {
432
- try {
433
- this.#onmessage.call(this, event);
434
- } catch (error) {
435
- logger()?.error({
436
- msg: "error in onmessage handler",
437
- error,
438
- });
439
- }
440
- }
441
- break;
442
- }
443
- }
444
- }
445
-
446
- // Internal methods called by the Tunnel class
447
- _handleOpen(requestId: ArrayBuffer): void {
448
- if (this.#readyState !== 0) {
449
- // CONNECTING
450
- return;
451
- }
452
-
453
- this.#readyState = 1; // OPEN
454
-
455
- const event = {
456
- type: "open",
457
- rivetRequestId: requestId,
458
- target: this,
459
- };
460
-
461
- this.#fireEvent("open", event);
462
- }
463
-
464
- /// Returns false if the message was sent off.
465
- _handleMessage(
466
- requestId: ArrayBuffer,
467
- data: string | Uint8Array,
468
- index: number,
469
- isBinary: boolean,
470
- ): boolean {
471
- if (this.#readyState !== 1) {
472
- // OPEN
473
- return true;
474
- }
475
-
476
- let messageData: any;
477
-
478
- if (isBinary) {
479
- // Handle binary data based on binaryType
480
- if (this.#binaryType === "nodebuffer") {
481
- // Convert to Buffer for Node.js compatibility
482
- messageData = Buffer.from(data as Uint8Array);
483
- } else if (this.#binaryType === "arraybuffer") {
484
- // Convert to ArrayBuffer
485
- if (data instanceof Uint8Array) {
486
- messageData = data.buffer.slice(
487
- data.byteOffset,
488
- data.byteOffset + data.byteLength,
489
- );
490
- } else {
491
- messageData = data;
492
- }
493
- } else {
494
- // Blob type - not commonly used in Node.js
495
- throw new Error(
496
- "Blob binaryType not supported in tunnel adapter",
497
- );
498
- }
499
- } else {
500
- messageData = data;
501
- }
502
-
503
- const event = {
504
- type: "message",
505
- data: messageData,
506
- rivetRequestId: requestId,
507
- rivetMessageIndex: index,
508
- target: this,
509
- };
510
-
511
- this.#fireEvent("message", event);
512
-
513
- return false;
514
- }
515
-
516
- _handleClose(requestId: ArrayBuffer, code?: number, reason?: string): void {
517
- if (this.#readyState === 3) {
518
- // CLOSED
519
- return;
520
- }
521
-
522
- this.#readyState = 3; // CLOSED
523
-
524
- const event = {
525
- type: "close",
526
- wasClean: true,
527
- code: code || 1000,
528
- reason: reason || "",
529
- rivetRequestId: requestId,
530
- target: this,
531
- };
532
-
533
- this.#fireEvent("close", event);
534
- }
535
-
536
- _handleError(error: Error): void {
537
- const event = {
538
- type: "error",
539
- target: this,
540
- error,
541
- };
542
-
543
- this.#fireEvent("error", event);
544
- }
545
-
546
- // WebSocket constants for compatibility
547
530
  static readonly CONNECTING = 0;
548
531
  static readonly OPEN = 1;
549
532
  static readonly CLOSING = 2;
550
533
  static readonly CLOSED = 3;
551
534
 
552
- // Instance constants
553
535
  readonly CONNECTING = 0;
554
536
  readonly OPEN = 1;
555
537
  readonly CLOSING = 2;
@@ -566,6 +548,7 @@ export class WebSocketTunnelAdapter {
566
548
  if (cb) cb(new Error("Pong not supported in tunnel adapter"));
567
549
  }
568
550
 
551
+ /** @experimental */
569
552
  terminate(): void {
570
553
  // Immediate close without close frame
571
554
  this.#readyState = 3; // CLOSED