@rivetkit/engine-runner 25.7.1-rc.1 → 25.7.2-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/tunnel.ts CHANGED
@@ -1,10 +1,10 @@
1
- import WebSocket from "ws";
2
- import * as tunnel from "@rivetkit/engine-tunnel-protocol";
1
+ import * as protocol from "@rivetkit/engine-runner-protocol";
2
+ import type { RequestId, MessageId } from "@rivetkit/engine-runner-protocol";
3
3
  import { WebSocketTunnelAdapter } from "./websocket-tunnel-adapter";
4
- import { calculateBackoff } from "./utils";
5
4
  import type { Runner, ActorInstance } from "./mod";
6
5
  import { v4 as uuidv4 } from "uuid";
7
6
  import { logger } from "./log";
7
+ import { unreachable } from "./utils";
8
8
 
9
9
  const GC_INTERVAL = 60000; // 60 seconds
10
10
  const MESSAGE_ACK_TIMEOUT = 5000; // 5 seconds
@@ -16,71 +16,34 @@ interface PendingRequest {
16
16
  actorId?: string;
17
17
  }
18
18
 
19
- interface TunnelCallbacks {
20
- onConnected(): void;
21
- onDisconnected(): void;
22
- }
23
-
24
19
  interface PendingMessage {
25
20
  sentAt: number;
26
21
  requestIdStr: string;
27
22
  }
28
23
 
29
24
  export class Tunnel {
30
- #pegboardTunnelUrl: string;
31
-
32
25
  #runner: Runner;
33
26
 
34
- #tunnelWs?: WebSocket;
35
- #shutdown = false;
36
- #reconnectTimeout?: NodeJS.Timeout;
37
- #reconnectAttempt = 0;
38
-
39
27
  #actorPendingRequests: Map<string, PendingRequest> = new Map();
40
28
  #actorWebSockets: Map<string, WebSocketTunnelAdapter> = new Map();
41
29
 
42
30
  #pendingMessages: Map<string, PendingMessage> = new Map();
43
31
  #gcInterval?: NodeJS.Timeout;
44
32
 
45
- #callbacks: TunnelCallbacks;
46
-
47
- constructor(
48
- runner: Runner,
49
- pegboardTunnelUrl: string,
50
- callbacks: TunnelCallbacks,
51
- ) {
52
- this.#pegboardTunnelUrl = pegboardTunnelUrl;
33
+ constructor(runner: Runner) {
53
34
  this.#runner = runner;
54
- this.#callbacks = callbacks;
55
35
  }
56
36
 
57
37
  start(): void {
58
- if (this.#tunnelWs?.readyState === WebSocket.OPEN) {
59
- return;
60
- }
61
-
62
- this.#connect();
63
38
  this.#startGarbageCollector();
64
39
  }
65
40
 
66
41
  shutdown() {
67
- this.#shutdown = true;
68
-
69
- if (this.#reconnectTimeout) {
70
- clearTimeout(this.#reconnectTimeout);
71
- this.#reconnectTimeout = undefined;
72
- }
73
-
74
42
  if (this.#gcInterval) {
75
43
  clearInterval(this.#gcInterval);
76
44
  this.#gcInterval = undefined;
77
45
  }
78
46
 
79
- if (this.#tunnelWs) {
80
- this.#tunnelWs.close();
81
- this.#tunnelWs = undefined;
82
- }
83
-
84
47
  // TODO: Should we use unregisterActor instead
85
48
 
86
49
  // Reject all pending requests
@@ -96,8 +59,12 @@ export class Tunnel {
96
59
  this.#actorWebSockets.clear();
97
60
  }
98
61
 
99
- #sendMessage(requestId: tunnel.RequestId, messageKind: tunnel.MessageKind) {
100
- if (!this.#tunnelWs || this.#tunnelWs.readyState !== WebSocket.OPEN) {
62
+ #sendMessage(
63
+ requestId: RequestId,
64
+ messageKind: protocol.ToServerTunnelMessageKind,
65
+ ) {
66
+ // TODO: Switch this with runner WS
67
+ if (!this.#runner.__webSocketReady()) {
101
68
  console.warn("Cannot send tunnel message, WebSocket not connected");
102
69
  return;
103
70
  }
@@ -112,29 +79,32 @@ export class Tunnel {
112
79
  });
113
80
 
114
81
  // Send message
115
- const message: tunnel.RunnerMessage = {
116
- requestId,
117
- messageId,
118
- messageKind,
82
+ const message: protocol.ToServer = {
83
+ tag: "ToServerTunnelMessage",
84
+ val: {
85
+ requestId,
86
+ messageId,
87
+ messageKind,
88
+ },
119
89
  };
120
-
121
- const encoded = tunnel.encodeRunnerMessage(message);
122
- this.#tunnelWs.send(encoded);
90
+ this.#runner.__sendToServer(message);
123
91
  }
124
92
 
125
- #sendAck(requestId: tunnel.RequestId, messageId: tunnel.MessageId) {
126
- if (!this.#tunnelWs || this.#tunnelWs.readyState !== WebSocket.OPEN) {
93
+ #sendAck(requestId: RequestId, messageId: MessageId) {
94
+ if (!this.#runner.__webSocketReady()) {
127
95
  return;
128
96
  }
129
97
 
130
- const message: tunnel.RunnerMessage = {
131
- requestId,
132
- messageId,
133
- messageKind: { tag: "Ack", val: null },
98
+ const message: protocol.ToServer = {
99
+ tag: "ToServerTunnelMessage",
100
+ val: {
101
+ requestId,
102
+ messageId,
103
+ messageKind: { tag: "TunnelAck", val: null },
104
+ },
134
105
  };
135
106
 
136
- const encoded = tunnel.encodeRunnerMessage(message);
137
- this.#tunnelWs.send(encoded);
107
+ this.#runner.__sendToServer(message);
138
108
  }
139
109
 
140
110
  #startGarbageCollector() {
@@ -240,80 +210,8 @@ export class Tunnel {
240
210
  return fetchHandler;
241
211
  }
242
212
 
243
- #connect() {
244
- if (this.#shutdown) return;
245
-
246
- try {
247
- this.#tunnelWs = new WebSocket(this.#pegboardTunnelUrl, {
248
- headers: {
249
- "x-rivet-target": "tunnel",
250
- },
251
- });
252
-
253
- this.#tunnelWs.binaryType = "arraybuffer";
254
-
255
- this.#tunnelWs.addEventListener("open", () => {
256
- this.#reconnectAttempt = 0;
257
-
258
- if (this.#reconnectTimeout) {
259
- clearTimeout(this.#reconnectTimeout);
260
- this.#reconnectTimeout = undefined;
261
- }
262
-
263
- this.#callbacks.onConnected();
264
- });
265
-
266
- this.#tunnelWs.addEventListener("message", async (event) => {
267
- try {
268
- await this.#handleMessage(event.data as ArrayBuffer);
269
- } catch (error) {
270
- logger()?.error({
271
- msg: "error handling tunnel message",
272
- error,
273
- });
274
- }
275
- });
276
-
277
- this.#tunnelWs.addEventListener("error", (event) => {
278
- logger()?.error({ msg: "tunnel websocket error", event });
279
- });
280
-
281
- this.#tunnelWs.addEventListener("close", () => {
282
- this.#callbacks.onDisconnected();
283
-
284
- if (!this.#shutdown) {
285
- this.#scheduleReconnect();
286
- }
287
- });
288
- } catch (error) {
289
- logger()?.error({ msg: "failed to connect tunnel", error });
290
- if (!this.#shutdown) {
291
- this.#scheduleReconnect();
292
- }
293
- }
294
- }
295
-
296
- #scheduleReconnect() {
297
- if (this.#shutdown) return;
298
-
299
- const delay = calculateBackoff(this.#reconnectAttempt, {
300
- initialDelay: 1000,
301
- maxDelay: 30000,
302
- multiplier: 2,
303
- jitter: true,
304
- });
305
-
306
- this.#reconnectAttempt++;
307
-
308
- this.#reconnectTimeout = setTimeout(() => {
309
- this.#connect();
310
- }, delay);
311
- }
312
-
313
- async #handleMessage(data: ArrayBuffer) {
314
- const message = tunnel.decodeRunnerMessage(new Uint8Array(data));
315
-
316
- if (message.messageKind.tag === "Ack") {
213
+ async handleTunnelMessage(message: protocol.ToClientTunnelMessage) {
214
+ if (message.messageKind.tag === "TunnelAck") {
317
215
  // Mark pending message as acknowledged and remove it
318
216
  const msgIdStr = bufferToString(message.messageId);
319
217
  const pending = this.#pendingMessages.get(msgIdStr);
@@ -323,79 +221,48 @@ export class Tunnel {
323
221
  } else {
324
222
  this.#sendAck(message.requestId, message.messageId);
325
223
  switch (message.messageKind.tag) {
326
- case "ToServerRequestStart":
224
+ case "ToClientRequestStart":
327
225
  await this.#handleRequestStart(
328
226
  message.requestId,
329
227
  message.messageKind.val,
330
228
  );
331
229
  break;
332
- case "ToServerRequestChunk":
230
+ case "ToClientRequestChunk":
333
231
  await this.#handleRequestChunk(
334
232
  message.requestId,
335
233
  message.messageKind.val,
336
234
  );
337
235
  break;
338
- case "ToServerRequestAbort":
236
+ case "ToClientRequestAbort":
339
237
  await this.#handleRequestAbort(message.requestId);
340
238
  break;
341
- case "ToServerWebSocketOpen":
342
- await this.#handleWebSocketOpen(
343
- message.requestId,
344
- message.messageKind.val,
345
- );
346
- break;
347
- case "ToServerWebSocketMessage":
348
- await this.#handleWebSocketMessage(
349
- message.requestId,
350
- message.messageKind.val,
351
- );
352
- break;
353
- case "ToServerWebSocketClose":
354
- await this.#handleWebSocketClose(
355
- message.requestId,
356
- message.messageKind.val,
357
- );
358
- break;
359
- case "ToClientResponseStart":
360
- this.#handleResponseStart(
361
- message.requestId,
362
- message.messageKind.val,
363
- );
364
- break;
365
- case "ToClientResponseChunk":
366
- this.#handleResponseChunk(
367
- message.requestId,
368
- message.messageKind.val,
369
- );
370
- break;
371
- case "ToClientResponseAbort":
372
- this.#handleResponseAbort(message.requestId);
373
- break;
374
239
  case "ToClientWebSocketOpen":
375
- this.#handleWebSocketOpenResponse(
240
+ await this.#handleWebSocketOpen(
376
241
  message.requestId,
377
242
  message.messageKind.val,
378
243
  );
379
244
  break;
380
245
  case "ToClientWebSocketMessage":
381
- this.#handleWebSocketMessageResponse(
246
+ await this.#handleWebSocketMessage(
382
247
  message.requestId,
383
248
  message.messageKind.val,
384
249
  );
385
250
  break;
386
251
  case "ToClientWebSocketClose":
387
- this.#handleWebSocketCloseResponse(
252
+ await this.#handleWebSocketClose(
388
253
  message.requestId,
389
254
  message.messageKind.val,
390
255
  );
391
256
  break;
257
+ default:
258
+ unreachable(message.messageKind);
392
259
  }
393
260
  }
394
261
  }
395
262
 
396
263
  async #handleRequestStart(
397
264
  requestId: ArrayBuffer,
398
- req: tunnel.ToServerRequestStart,
265
+ req: protocol.ToClientRequestStart,
399
266
  ) {
400
267
  // Track this request for the actor
401
268
  const requestIdStr = bufferToString(requestId);
@@ -471,7 +338,7 @@ export class Tunnel {
471
338
 
472
339
  async #handleRequestChunk(
473
340
  requestId: ArrayBuffer,
474
- chunk: tunnel.ToServerRequestChunk,
341
+ chunk: protocol.ToClientRequestChunk,
475
342
  ) {
476
343
  const requestIdStr = bufferToString(requestId);
477
344
  const pending = this.#actorPendingRequests.get(requestIdStr);
@@ -516,9 +383,9 @@ export class Tunnel {
516
383
 
517
384
  // Send as non-streaming response
518
385
  this.#sendMessage(requestId, {
519
- tag: "ToClientResponseStart",
386
+ tag: "ToServerResponseStart",
520
387
  val: {
521
- status: response.status as tunnel.u16,
388
+ status: response.status as protocol.u16,
522
389
  headers,
523
390
  body: body || null,
524
391
  stream: false,
@@ -535,9 +402,9 @@ export class Tunnel {
535
402
  headers.set("content-type", "text/plain");
536
403
 
537
404
  this.#sendMessage(requestId, {
538
- tag: "ToClientResponseStart",
405
+ tag: "ToServerResponseStart",
539
406
  val: {
540
- status: status as tunnel.u16,
407
+ status: status as protocol.u16,
541
408
  headers,
542
409
  body: new TextEncoder().encode(message).buffer as ArrayBuffer,
543
410
  stream: false,
@@ -547,7 +414,7 @@ export class Tunnel {
547
414
 
548
415
  async #handleWebSocketOpen(
549
416
  requestId: ArrayBuffer,
550
- open: tunnel.ToServerWebSocketOpen,
417
+ open: protocol.ToClientWebSocketOpen,
551
418
  ) {
552
419
  const webSocketId = bufferToString(requestId);
553
420
  // Validate actor exists
@@ -559,7 +426,7 @@ export class Tunnel {
559
426
  });
560
427
  // Send close immediately
561
428
  this.#sendMessage(requestId, {
562
- tag: "ToClientWebSocketClose",
429
+ tag: "ToServerWebSocketClose",
563
430
  val: {
564
431
  code: 1011,
565
432
  reason: "Actor not found",
@@ -577,7 +444,7 @@ export class Tunnel {
577
444
  });
578
445
  // Send close immediately
579
446
  this.#sendMessage(requestId, {
580
- tag: "ToClientWebSocketClose",
447
+ tag: "ToServerWebSocketClose",
581
448
  val: {
582
449
  code: 1011,
583
450
  reason: "Not Implemented",
@@ -604,7 +471,7 @@ export class Tunnel {
604
471
  : data;
605
472
 
606
473
  this.#sendMessage(requestId, {
607
- tag: "ToClientWebSocketMessage",
474
+ tag: "ToServerWebSocketMessage",
608
475
  val: {
609
476
  data: dataBuffer,
610
477
  binary: isBinary,
@@ -614,7 +481,7 @@ export class Tunnel {
614
481
  (code?: number, reason?: string) => {
615
482
  // Send close through tunnel
616
483
  this.#sendMessage(requestId, {
617
- tag: "ToClientWebSocketClose",
484
+ tag: "ToServerWebSocketClose",
618
485
  val: {
619
486
  code: code || null,
620
487
  reason: reason || null,
@@ -636,7 +503,7 @@ export class Tunnel {
636
503
 
637
504
  // Send open confirmation
638
505
  this.#sendMessage(requestId, {
639
- tag: "ToClientWebSocketOpen",
506
+ tag: "ToServerWebSocketOpen",
640
507
  val: null,
641
508
  });
642
509
 
@@ -669,7 +536,7 @@ export class Tunnel {
669
536
  logger()?.error({ msg: "error handling websocket open", error });
670
537
  // Send close on error
671
538
  this.#sendMessage(requestId, {
672
- tag: "ToClientWebSocketClose",
539
+ tag: "ToServerWebSocketClose",
673
540
  val: {
674
541
  code: 1011,
675
542
  reason: "Server Error",
@@ -687,7 +554,7 @@ export class Tunnel {
687
554
 
688
555
  async #handleWebSocketMessage(
689
556
  requestId: ArrayBuffer,
690
- msg: tunnel.ToServerWebSocketMessage,
557
+ msg: protocol.ToServerWebSocketMessage,
691
558
  ) {
692
559
  const webSocketId = bufferToString(requestId);
693
560
  const adapter = this.#actorWebSockets.get(webSocketId);
@@ -702,119 +569,7 @@ export class Tunnel {
702
569
 
703
570
  async #handleWebSocketClose(
704
571
  requestId: ArrayBuffer,
705
- close: tunnel.ToServerWebSocketClose,
706
- ) {
707
- const webSocketId = bufferToString(requestId);
708
- const adapter = this.#actorWebSockets.get(webSocketId);
709
- if (adapter) {
710
- adapter._handleClose(
711
- close.code || undefined,
712
- close.reason || undefined,
713
- );
714
- this.#actorWebSockets.delete(webSocketId);
715
- }
716
- }
717
-
718
- #handleResponseStart(
719
- requestId: ArrayBuffer,
720
- resp: tunnel.ToClientResponseStart,
721
- ) {
722
- const requestIdStr = bufferToString(requestId);
723
- const pending = this.#actorPendingRequests.get(requestIdStr);
724
- if (!pending) {
725
- logger()?.warn({
726
- msg: "received response for unknown request",
727
- requestId: requestIdStr,
728
- });
729
- return;
730
- }
731
-
732
- // Convert headers map to Headers object
733
- const headers = new Headers();
734
- for (const [key, value] of resp.headers) {
735
- headers.append(key, value);
736
- }
737
-
738
- if (resp.stream) {
739
- // Create streaming response
740
- const stream = new ReadableStream<Uint8Array>({
741
- start: (controller) => {
742
- pending.streamController = controller;
743
- },
744
- });
745
-
746
- const response = new Response(stream, {
747
- status: resp.status,
748
- headers,
749
- });
750
-
751
- pending.resolve(response);
752
- } else {
753
- // Non-streaming response
754
- const body = resp.body ? new Uint8Array(resp.body) : null;
755
- const response = new Response(body, {
756
- status: resp.status,
757
- headers,
758
- });
759
-
760
- pending.resolve(response);
761
- this.#actorPendingRequests.delete(requestIdStr);
762
- }
763
- }
764
-
765
- #handleResponseChunk(
766
- requestId: ArrayBuffer,
767
- chunk: tunnel.ToClientResponseChunk,
768
- ) {
769
- const requestIdStr = bufferToString(requestId);
770
- const pending = this.#actorPendingRequests.get(requestIdStr);
771
- if (pending?.streamController) {
772
- pending.streamController.enqueue(new Uint8Array(chunk.body));
773
- if (chunk.finish) {
774
- pending.streamController.close();
775
- this.#actorPendingRequests.delete(requestIdStr);
776
- }
777
- }
778
- }
779
-
780
- #handleResponseAbort(requestId: ArrayBuffer) {
781
- const requestIdStr = bufferToString(requestId);
782
- const pending = this.#actorPendingRequests.get(requestIdStr);
783
- if (pending?.streamController) {
784
- pending.streamController.error(new Error("Response aborted"));
785
- }
786
- this.#actorPendingRequests.delete(requestIdStr);
787
- }
788
-
789
- #handleWebSocketOpenResponse(
790
- requestId: ArrayBuffer,
791
- open: tunnel.ToClientWebSocketOpen,
792
- ) {
793
- const webSocketId = bufferToString(requestId);
794
- const adapter = this.#actorWebSockets.get(webSocketId);
795
- if (adapter) {
796
- adapter._handleOpen();
797
- }
798
- }
799
-
800
- #handleWebSocketMessageResponse(
801
- requestId: ArrayBuffer,
802
- msg: tunnel.ToClientWebSocketMessage,
803
- ) {
804
- const webSocketId = bufferToString(requestId);
805
- const adapter = this.#actorWebSockets.get(webSocketId);
806
- if (adapter) {
807
- const data = msg.binary
808
- ? new Uint8Array(msg.data)
809
- : new TextDecoder().decode(new Uint8Array(msg.data));
810
-
811
- adapter._handleMessage(data, msg.binary);
812
- }
813
- }
814
-
815
- #handleWebSocketCloseResponse(
816
- requestId: ArrayBuffer,
817
- close: tunnel.ToClientWebSocketClose,
572
+ close: protocol.ToServerWebSocketClose,
818
573
  ) {
819
574
  const webSocketId = bufferToString(requestId);
820
575
  const adapter = this.#actorWebSockets.get(webSocketId);