@rivetkit/engine-runner 25.8.0 → 25.8.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rivetkit/engine-runner",
3
- "version": "25.8.0",
3
+ "version": "25.8.3",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "import": {
@@ -16,7 +16,7 @@
16
16
  "uuid": "^12.0.0",
17
17
  "pino": "^9.9.5",
18
18
  "ws": "^8.18.3",
19
- "@rivetkit/engine-runner-protocol": "25.8.0"
19
+ "@rivetkit/engine-runner-protocol": "25.8.3"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/node": "^22.18.1",
package/src/log.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { Logger } from "pino";
1
+ import type { Logger } from "pino";
2
2
 
3
- let LOGGER: Logger | undefined = undefined;
3
+ let LOGGER: Logger | undefined;
4
4
 
5
5
  export function setLogger(logger: Logger) {
6
6
  LOGGER = logger;
package/src/mod.ts CHANGED
@@ -1,17 +1,18 @@
1
- import type WebSocket from "ws";
2
- import { importWebSocket } from "./websocket.js";
3
1
  import * as protocol from "@rivetkit/engine-runner-protocol";
4
- import { unreachable, calculateBackoff } from "./utils";
2
+ import type { Logger } from "pino";
3
+ import type WebSocket from "ws";
4
+ import { logger, setLogger } from "./log.js";
5
5
  import { Tunnel } from "./tunnel";
6
+ import { calculateBackoff, unreachable } from "./utils";
7
+ import { importWebSocket } from "./websocket.js";
6
8
  import type { WebSocketTunnelAdapter } from "./websocket-tunnel-adapter";
7
- import type { Logger } from "pino";
8
- import { setLogger, logger } from "./log.js";
9
9
 
10
10
  const KV_EXPIRE: number = 30_000;
11
11
  const PROTOCOL_VERSION: number = 1;
12
12
 
13
13
  /** Warn once the backlog significantly exceeds the server's ack batch size. */
14
14
  const EVENT_BACKLOG_WARN_THRESHOLD = 10_000;
15
+ const SIGNAL_HANDLERS: (() => void)[] = [];
15
16
 
16
17
  export interface ActorInstance {
17
18
  actorId: string;
@@ -44,8 +45,17 @@ export interface RunnerConfig {
44
45
  onConnected: () => void;
45
46
  onDisconnected: () => void;
46
47
  onShutdown: () => void;
47
- fetch: (actorId: string, request: Request) => Promise<Response>;
48
- websocket?: (actorId: string, ws: any, request: Request) => Promise<void>;
48
+ fetch: (
49
+ runner: Runner,
50
+ actorId: string,
51
+ request: Request,
52
+ ) => Promise<Response>;
53
+ websocket?: (
54
+ runner: Runner,
55
+ actorId: string,
56
+ ws: any,
57
+ request: Request,
58
+ ) => Promise<void>;
49
59
  onActorStart: (
50
60
  actorId: string,
51
61
  generation: number,
@@ -107,14 +117,12 @@ export class Runner {
107
117
  #kvCleanupInterval?: NodeJS.Timeout;
108
118
 
109
119
  // Tunnel for HTTP/WebSocket forwarding
110
- #tunnel: Tunnel;
120
+ #tunnel: Tunnel | undefined;
111
121
 
112
122
  constructor(config: RunnerConfig) {
113
123
  this.#config = config;
114
124
  if (this.#config.logger) setLogger(this.#config.logger);
115
125
 
116
- this.#tunnel = new Tunnel(this);
117
-
118
126
  // Start cleaning up old unsent KV requests every 15 seconds
119
127
  this.#kvCleanupInterval = setInterval(() => {
120
128
  this.#cleanupOldKvRequests();
@@ -134,11 +142,21 @@ export class Runner {
134
142
  }
135
143
 
136
144
  async stopActor(actorId: string, generation?: number) {
145
+ const actor = this.getActor(actorId, generation);
146
+ if (!actor) return;
147
+
148
+ this.#sendActorIntent(actorId, actor.generation, "stop");
149
+
150
+ // NOTE: We do NOT remove the actor from this.#actors here
151
+ // The server will send a StopActor command if it wants to fully stop
152
+ }
153
+
154
+ async forceStopActor(actorId: string, generation?: number) {
137
155
  const actor = this.#removeActor(actorId, generation);
138
156
  if (!actor) return;
139
157
 
140
158
  // Unregister actor from tunnel
141
- this.#tunnel.unregisterActor(actor);
159
+ this.#tunnel?.unregisterActor(actor);
142
160
 
143
161
  // If onActorStop times out, Pegboard will handle this timeout with ACTOR_STOP_THRESHOLD_DURATION_MS
144
162
  try {
@@ -152,6 +170,7 @@ export class Runner {
152
170
  this.#config.onActorStop(actorId, actor.generation).catch((err) => {
153
171
  logger()?.error({
154
172
  msg: "error in onactorstop for actor",
173
+ runnerId: this.runnerId,
155
174
  actorId,
156
175
  err,
157
176
  });
@@ -159,25 +178,31 @@ export class Runner {
159
178
  }
160
179
 
161
180
  #stopAllActors() {
162
- logger()?.info(
163
- "stopping all actors due to runner lost threshold exceeded",
164
- );
181
+ logger()?.info({
182
+ msg: "stopping all actors due to runner lost threshold exceeded",
183
+ runnerId: this.runnerId,
184
+ });
165
185
 
166
186
  const actorIds = Array.from(this.#actors.keys());
167
187
  for (const actorId of actorIds) {
168
- this.stopActor(actorId);
188
+ this.forceStopActor(actorId);
169
189
  }
170
190
  }
171
191
 
172
192
  getActor(actorId: string, generation?: number): ActorInstance | undefined {
173
193
  const actor = this.#actors.get(actorId);
174
194
  if (!actor) {
175
- logger()?.error({ msg: "actor not found", actorId });
195
+ logger()?.error({
196
+ msg: "actor not found",
197
+ runnerId: this.runnerId,
198
+ actorId,
199
+ });
176
200
  return undefined;
177
201
  }
178
202
  if (generation !== undefined && actor.generation !== generation) {
179
203
  logger()?.error({
180
204
  msg: "actor generation mismatch",
205
+ runnerId: this.runnerId,
181
206
  actorId,
182
207
  generation,
183
208
  });
@@ -202,12 +227,17 @@ export class Runner {
202
227
  ): ActorInstance | undefined {
203
228
  const actor = this.#actors.get(actorId);
204
229
  if (!actor) {
205
- logger()?.error({ msg: "actor not found for removal", actorId });
230
+ logger()?.error({
231
+ msg: "actor not found for removal",
232
+ runnerId: this.runnerId,
233
+ actorId,
234
+ });
206
235
  return undefined;
207
236
  }
208
237
  if (generation !== undefined && actor.generation !== generation) {
209
238
  logger()?.error({
210
239
  msg: "actor generation mismatch",
240
+ runnerId: this.runnerId,
211
241
  actorId,
212
242
  generation,
213
243
  });
@@ -225,6 +255,7 @@ export class Runner {
225
255
  } catch (err) {
226
256
  logger()?.error({
227
257
  msg: "error closing websocket for actor",
258
+ runnerId: this.runnerId,
228
259
  actorId,
229
260
  err,
230
261
  });
@@ -241,8 +272,9 @@ export class Runner {
241
272
  if (this.#started) throw new Error("Cannot call runner.start twice");
242
273
  this.#started = true;
243
274
 
244
- logger()?.info("starting runner");
275
+ logger()?.info({ msg: "starting runner" });
245
276
 
277
+ this.#tunnel = new Tunnel(this);
246
278
  this.#tunnel.start();
247
279
 
248
280
  try {
@@ -253,14 +285,46 @@ export class Runner {
253
285
  }
254
286
 
255
287
  if (!this.#config.noAutoShutdown) {
256
- process.on("SIGTERM", this.shutdown.bind(this, false, true));
257
- process.on("SIGINT", this.shutdown.bind(this, false, true));
288
+ if (!SIGNAL_HANDLERS.length) {
289
+ process.on("SIGTERM", () => {
290
+ logger()?.debug("received SIGTERM");
291
+
292
+ for (const handler of SIGNAL_HANDLERS) {
293
+ handler();
294
+ }
295
+
296
+ process.exit(0);
297
+ });
298
+ process.on("SIGINT", () => {
299
+ logger()?.debug("received SIGINT");
300
+
301
+ for (const handler of SIGNAL_HANDLERS) {
302
+ handler();
303
+ }
304
+
305
+ process.exit(0);
306
+ });
307
+
308
+ logger()?.debug({
309
+ msg: "added SIGTERM listeners",
310
+ });
311
+ }
312
+
313
+ SIGNAL_HANDLERS.push(() => {
314
+ const weak = new WeakRef(this);
315
+ weak.deref()?.shutdown(false, false);
316
+ });
258
317
  }
259
318
  }
260
319
 
261
320
  // MARK: Shutdown
262
321
  async shutdown(immediate: boolean, exit: boolean = false) {
263
- logger()?.info({ msg: "starting shutdown...", immediate });
322
+ logger()?.info({
323
+ msg: "starting shutdown",
324
+ runnerId: this.runnerId,
325
+ immediate,
326
+ exit,
327
+ });
264
328
  this.#shutdown = true;
265
329
 
266
330
  // Clear reconnect timeout
@@ -315,6 +379,7 @@ export class Runner {
315
379
  try {
316
380
  logger()?.info({
317
381
  msg: "sending stopping message",
382
+ runnerId: this.runnerId,
318
383
  readyState: pegboardWebSocket.readyState,
319
384
  });
320
385
 
@@ -342,6 +407,7 @@ export class Runner {
342
407
  pegboardWebSocket.addEventListener("close", (ev) => {
343
408
  logger()?.info({
344
409
  msg: "connection closed",
410
+ runnerId: this.runnerId,
345
411
  code: ev.code,
346
412
  reason: ev.reason.toString(),
347
413
  });
@@ -351,28 +417,39 @@ export class Runner {
351
417
 
352
418
  // TODO: Wait for all actors to stop before closing ws
353
419
 
354
- logger()?.info("closing WebSocket");
420
+ logger()?.info({
421
+ msg: "closing WebSocket",
422
+ runnerId: this.runnerId,
423
+ });
355
424
  pegboardWebSocket.close(1000, "Stopping");
356
425
 
357
426
  await closePromise;
358
427
 
359
- logger()?.info("websocket shutdown completed");
428
+ logger()?.info({
429
+ msg: "websocket shutdown completed",
430
+ runnerId: this.runnerId,
431
+ });
360
432
  } catch (error) {
361
433
  logger()?.error({
362
434
  msg: "error during websocket shutdown:",
435
+ runnerId: this.runnerId,
363
436
  error,
364
437
  });
365
438
  pegboardWebSocket.close();
366
439
  }
367
440
  }
368
441
  } else {
369
- logger()?.warn("no runner WebSocket to shutdown or already closed");
442
+ logger()?.warn({
443
+ msg: "no runner WebSocket to shutdown or already closed",
444
+ runnerId: this.runnerId,
445
+ readyState: this.#pegboardWebSocket?.readyState,
446
+ });
370
447
  }
371
448
 
372
449
  // Close tunnel
373
450
  if (this.#tunnel) {
374
451
  this.#tunnel.shutdown();
375
- logger()?.info("tunnel shutdown completed");
452
+ this.#tunnel = undefined;
376
453
  }
377
454
 
378
455
  if (exit) process.exit(0);
@@ -400,7 +477,7 @@ export class Runner {
400
477
  this.#pegboardWebSocket = ws;
401
478
 
402
479
  ws.addEventListener("open", () => {
403
- logger()?.info("Connected");
480
+ logger()?.info({ msg: "Connected" });
404
481
 
405
482
  // Reset reconnect attempt counter on successful connection
406
483
  this.#reconnectAttempt = 0;
@@ -457,7 +534,10 @@ export class Runner {
457
534
  });
458
535
  } else {
459
536
  clearInterval(pingLoop);
460
- logger()?.info("WebSocket not open, stopping ping loop");
537
+ logger()?.info({
538
+ msg: "WebSocket not open, stopping ping loop",
539
+ runnerId: this.runnerId,
540
+ });
461
541
  }
462
542
  }, pingInterval);
463
543
  this.#pingLoop = pingLoop;
@@ -469,7 +549,10 @@ export class Runner {
469
549
  this.#sendCommandAcknowledgment();
470
550
  } else {
471
551
  clearInterval(ackLoop);
472
- logger()?.info("WebSocket not open, stopping ack loop");
552
+ logger()?.info({
553
+ msg: "WebSocket not open, stopping ack loop",
554
+ runnerId: this.runnerId,
555
+ });
473
556
  }
474
557
  }, ackInterval);
475
558
  this.#ackInterval = ackLoop;
@@ -492,7 +575,7 @@ export class Runner {
492
575
  if (message.tag === "ToClientInit") {
493
576
  const init = message.val;
494
577
 
495
- if (this.runnerId != init.runnerId) {
578
+ if (this.runnerId !== init.runnerId) {
496
579
  this.runnerId = init.runnerId;
497
580
 
498
581
  // Clear history if runner id changed
@@ -526,7 +609,7 @@ export class Runner {
526
609
  } else if (message.tag === "ToClientTunnelMessage") {
527
610
  this.#tunnel?.handleTunnelMessage(message.val);
528
611
  } else if (message.tag === "ToClientClose") {
529
- this.#tunnel.shutdown();
612
+ this.#tunnel?.shutdown();
530
613
  ws.close(1000, "manual closure");
531
614
  } else {
532
615
  unreachable(message);
@@ -534,7 +617,10 @@ export class Runner {
534
617
  });
535
618
 
536
619
  ws.addEventListener("error", (ev) => {
537
- logger()?.error(`WebSocket error: ${ev.error}`);
620
+ logger()?.error({
621
+ msg: `WebSocket error: ${ev.error}`,
622
+ runnerId: this.runnerId,
623
+ });
538
624
 
539
625
  if (!this.#shutdown) {
540
626
  // Start runner lost timeout if we have a threshold and are not shutting down
@@ -545,6 +631,7 @@ export class Runner {
545
631
  ) {
546
632
  logger()?.info({
547
633
  msg: "starting runner lost timeout",
634
+ runnerId: this.runnerId,
548
635
  seconds: this.#runnerLostThreshold / 1000,
549
636
  });
550
637
  this.#runnerLostTimeout = setTimeout(() => {
@@ -560,14 +647,18 @@ export class Runner {
560
647
  ws.addEventListener("close", async (ev) => {
561
648
  logger()?.info({
562
649
  msg: "connection closed",
650
+ runnerId: this.runnerId,
563
651
  code: ev.code,
564
652
  reason: ev.reason.toString(),
565
653
  });
566
654
 
567
655
  this.#config.onDisconnected();
568
656
 
569
- if (ev.reason.toString() == "ws.eviction") {
570
- logger()?.info("runner evicted");
657
+ if (ev.reason.toString().startsWith("ws.eviction")) {
658
+ logger()?.info({
659
+ msg: "runner evicted",
660
+ runnerId: this.runnerId,
661
+ });
571
662
 
572
663
  await this.shutdown(true);
573
664
  }
@@ -593,6 +684,7 @@ export class Runner {
593
684
  ) {
594
685
  logger()?.info({
595
686
  msg: "starting runner lost timeout",
687
+ runnerId: this.runnerId,
596
688
  seconds: this.#runnerLostThreshold / 1000,
597
689
  });
598
690
  this.#runnerLostTimeout = setTimeout(() => {
@@ -609,11 +701,16 @@ export class Runner {
609
701
  #handleCommands(commands: protocol.ToClientCommands) {
610
702
  logger()?.info({
611
703
  msg: "received commands",
704
+ runnerId: this.runnerId,
612
705
  commandCount: commands.length,
613
706
  });
614
707
 
615
708
  for (const commandWrapper of commands) {
616
- logger()?.info({ msg: "received command", commandWrapper });
709
+ logger()?.info({
710
+ msg: "received command",
711
+ runnerId: this.runnerId,
712
+ commandWrapper,
713
+ });
617
714
  if (commandWrapper.inner.tag === "CommandStartActor") {
618
715
  this.#handleCommandStartActor(commandWrapper);
619
716
  } else if (commandWrapper.inner.tag === "CommandStopActor") {
@@ -638,6 +735,7 @@ export class Runner {
638
735
  if (prunedCount > 0) {
639
736
  logger()?.info({
640
737
  msg: "pruned acknowledged events",
738
+ runnerId: this.runnerId,
641
739
  lastAckedIdx: lastAckedIdx.toString(),
642
740
  prunedCount,
643
741
  });
@@ -659,6 +757,7 @@ export class Runner {
659
757
  this.#eventBacklogWarned = true;
660
758
  logger()?.warn({
661
759
  msg: "unacknowledged event backlog exceeds threshold",
760
+ runnerId: this.runnerId,
662
761
  backlogSize: this.#eventHistory.length,
663
762
  threshold: EVENT_BACKLOG_WARN_THRESHOLD,
664
763
  });
@@ -699,13 +798,14 @@ export class Runner {
699
798
  .catch((err) => {
700
799
  logger()?.error({
701
800
  msg: "error in onactorstart for actor",
801
+ runnerId: this.runnerId,
702
802
  actorId,
703
803
  err,
704
804
  });
705
805
 
706
806
  // TODO: Mark as crashed
707
807
  // Send stopped state update if start failed
708
- this.stopActor(actorId, generation);
808
+ this.forceStopActor(actorId, generation);
709
809
  });
710
810
  }
711
811
 
@@ -716,7 +816,7 @@ export class Runner {
716
816
  const actorId = stopCommand.actorId;
717
817
  const generation = stopCommand.generation;
718
818
 
719
- this.stopActor(actorId, generation);
819
+ this.forceStopActor(actorId, generation);
720
820
  }
721
821
 
722
822
  #sendActorIntent(
@@ -725,7 +825,10 @@ export class Runner {
725
825
  intentType: "sleep" | "stop",
726
826
  ) {
727
827
  if (this.#shutdown) {
728
- logger()?.warn("Runner is shut down, cannot send actor intent");
828
+ logger()?.warn({
829
+ msg: "Runner is shut down, cannot send actor intent",
830
+ runnerId: this.runnerId,
831
+ });
729
832
  return;
730
833
  }
731
834
  let actorIntent: protocol.ActorIntent;
@@ -760,6 +863,7 @@ export class Runner {
760
863
 
761
864
  logger()?.info({
762
865
  msg: "sending event to server",
866
+ runnerId: this.runnerId,
763
867
  index: eventWrapper.index,
764
868
  tag: eventWrapper.inner.tag,
765
869
  val: eventWrapper.inner.val,
@@ -777,9 +881,10 @@ export class Runner {
777
881
  stateType: "running" | "stopped",
778
882
  ) {
779
883
  if (this.#shutdown) {
780
- logger()?.warn(
781
- "Runner is shut down, cannot send actor state update",
782
- );
884
+ logger()?.warn({
885
+ msg: "Runner is shut down, cannot send actor state update",
886
+ runnerId: this.runnerId,
887
+ });
783
888
  return;
784
889
  }
785
890
  let actorState: protocol.ActorState;
@@ -817,6 +922,7 @@ export class Runner {
817
922
 
818
923
  logger()?.info({
819
924
  msg: "sending event to server",
925
+ runnerId: this.runnerId,
820
926
  index: eventWrapper.index,
821
927
  tag: eventWrapper.inner.tag,
822
928
  val: eventWrapper.inner.val,
@@ -830,9 +936,10 @@ export class Runner {
830
936
 
831
937
  #sendCommandAcknowledgment() {
832
938
  if (this.#shutdown) {
833
- logger()?.warn(
834
- "Runner is shut down, cannot send command acknowledgment",
835
- );
939
+ logger()?.warn({
940
+ msg: "Runner is shut down, cannot send command acknowledgment",
941
+ runnerId: this.runnerId,
942
+ });
836
943
  return;
837
944
  }
838
945
 
@@ -857,11 +964,7 @@ export class Runner {
857
964
 
858
965
  if (!request) {
859
966
  const msg = "received kv response for unknown request id";
860
- if (logger()) {
861
- logger()?.error({ msg, requestId });
862
- } else {
863
- logger()?.error({ msg, requestId });
864
- }
967
+ logger()?.error({ msg, runnerId: this.runnerId, requestId });
865
968
  return;
866
969
  }
867
970
 
@@ -1266,9 +1369,10 @@ export class Runner {
1266
1369
 
1267
1370
  __sendToServer(message: protocol.ToServer) {
1268
1371
  if (this.#shutdown) {
1269
- logger()?.warn(
1270
- "Runner is shut down, cannot send message to server",
1271
- );
1372
+ logger()?.warn({
1373
+ msg: "Runner is shut down, cannot send message to server",
1374
+ runnerId: this.runnerId,
1375
+ });
1272
1376
  return;
1273
1377
  }
1274
1378
 
@@ -1279,16 +1383,17 @@ export class Runner {
1279
1383
  ) {
1280
1384
  this.#pegboardWebSocket.send(encoded);
1281
1385
  } else {
1282
- logger()?.error(
1283
- "WebSocket not available or not open for sending data",
1284
- );
1386
+ logger()?.error({
1387
+ msg: "WebSocket not available or not open for sending data",
1388
+ runnerId: this.runnerId,
1389
+ });
1285
1390
  }
1286
1391
  }
1287
1392
 
1288
1393
  getServerlessInitPacket(): string | undefined {
1289
1394
  if (!this.runnerId) return undefined;
1290
1395
 
1291
- let data = protocol.encodeToServerlessServer({
1396
+ const data = protocol.encodeToServerlessServer({
1292
1397
  tag: "ToServerlessServerInit",
1293
1398
  val: {
1294
1399
  runnerId: this.runnerId,
@@ -1305,7 +1410,10 @@ export class Runner {
1305
1410
 
1306
1411
  #scheduleReconnect() {
1307
1412
  if (this.#shutdown) {
1308
- logger()?.debug("Runner is shut down, not attempting reconnect");
1413
+ logger()?.debug({
1414
+ msg: "Runner is shut down, not attempting reconnect",
1415
+ runnerId: this.runnerId,
1416
+ });
1309
1417
  return;
1310
1418
  }
1311
1419
 
@@ -1316,16 +1424,18 @@ export class Runner {
1316
1424
  jitter: true,
1317
1425
  });
1318
1426
 
1319
- logger()?.debug(
1320
- `Scheduling reconnect attempt ${this.#reconnectAttempt + 1} in ${delay}ms`,
1321
- );
1427
+ logger()?.debug({
1428
+ msg: `Scheduling reconnect attempt ${this.#reconnectAttempt + 1} in ${delay}ms`,
1429
+ runnerId: this.runnerId,
1430
+ });
1322
1431
 
1323
1432
  this.#reconnectTimeout = setTimeout(async () => {
1324
1433
  if (!this.#shutdown) {
1325
1434
  this.#reconnectAttempt++;
1326
- logger()?.debug(
1327
- `Attempting to reconnect (attempt ${this.#reconnectAttempt})...`,
1328
- );
1435
+ logger()?.debug({
1436
+ msg: `Attempting to reconnect (attempt ${this.#reconnectAttempt})...`,
1437
+ runnerId: this.runnerId,
1438
+ });
1329
1439
  await this.#openPegboardWebSocket();
1330
1440
  }
1331
1441
  }, delay);
package/src/tunnel.ts CHANGED
@@ -1,10 +1,10 @@
1
- import * as protocol from "@rivetkit/engine-runner-protocol";
2
- import type { RequestId, MessageId } from "@rivetkit/engine-runner-protocol";
3
- import { WebSocketTunnelAdapter } from "./websocket-tunnel-adapter";
4
- import type { Runner, ActorInstance } from "./mod";
1
+ import type * as protocol from "@rivetkit/engine-runner-protocol";
2
+ import type { MessageId, RequestId } from "@rivetkit/engine-runner-protocol";
5
3
  import { v4 as uuidv4 } from "uuid";
6
4
  import { logger } from "./log";
5
+ import type { ActorInstance, Runner } from "./mod";
7
6
  import { unreachable } from "./utils";
7
+ import { WebSocketTunnelAdapter } from "./websocket-tunnel-adapter";
8
8
 
9
9
  const GC_INTERVAL = 60000; // 60 seconds
10
10
  const MESSAGE_ACK_TIMEOUT = 5000; // 5 seconds
@@ -48,8 +48,6 @@ export class Tunnel {
48
48
  this.#gcInterval = undefined;
49
49
  }
50
50
 
51
- // TODO: Should we use unregisterActor instead
52
-
53
51
  // Reject all pending requests
54
52
  for (const [_, request] of this.#actorPendingRequests) {
55
53
  request.reject(new Error("Tunnel shutting down"));
@@ -212,7 +210,11 @@ export class Tunnel {
212
210
  return new Response("Actor not found", { status: 404 });
213
211
  }
214
212
 
215
- const fetchHandler = this.#runner.config.fetch(actorId, request);
213
+ const fetchHandler = this.#runner.config.fetch(
214
+ this.#runner,
215
+ actorId,
216
+ request,
217
+ );
216
218
 
217
219
  if (!fetchHandler) {
218
220
  return new Response("Not Implemented", { status: 501 });
@@ -541,7 +543,12 @@ export class Tunnel {
541
543
  });
542
544
 
543
545
  // Call websocket handler
544
- await websocketHandler(open.actorId, adapter, request);
546
+ await websocketHandler(
547
+ this.#runner,
548
+ open.actorId,
549
+ adapter,
550
+ request,
551
+ );
545
552
  } catch (error) {
546
553
  logger()?.error({ msg: "error handling websocket open", error });
547
554
  // Send close on error
package/src/utils.ts CHANGED
@@ -20,7 +20,7 @@ export function calculateBackoff(
20
20
  jitter = true,
21
21
  } = options;
22
22
 
23
- let delay = Math.min(initialDelay * Math.pow(multiplier, attempt), maxDelay);
23
+ let delay = Math.min(initialDelay * multiplier ** attempt, maxDelay);
24
24
 
25
25
  if (jitter) {
26
26
  // Add random jitter between 0% and 25% of the delay
@@ -18,7 +18,7 @@ export class WebSocketTunnelAdapter {
18
18
  #url = "";
19
19
  #sendCallback: (data: ArrayBuffer | string, isBinary: boolean) => void;
20
20
  #closeCallback: (code?: number, reason?: string) => void;
21
-
21
+
22
22
  // Event buffering for events fired before listeners are attached
23
23
  #bufferedEvents: Array<{
24
24
  type: string;
@@ -28,7 +28,7 @@ export class WebSocketTunnelAdapter {
28
28
  constructor(
29
29
  webSocketId: string,
30
30
  sendCallback: (data: ArrayBuffer | string, isBinary: boolean) => void,
31
- closeCallback: (code?: number, reason?: string) => void
31
+ closeCallback: (code?: number, reason?: string) => void,
32
32
  ) {
33
33
  this.#webSocketId = webSocketId;
34
34
  this.#sendCallback = sendCallback;
@@ -48,7 +48,11 @@ export class WebSocketTunnelAdapter {
48
48
  }
49
49
 
50
50
  set binaryType(value: string) {
51
- if (value === "nodebuffer" || value === "arraybuffer" || value === "blob") {
51
+ if (
52
+ value === "nodebuffer" ||
53
+ value === "arraybuffer" ||
54
+ value === "blob"
55
+ ) {
52
56
  this.#binaryType = value;
53
57
  }
54
58
  }
@@ -114,7 +118,8 @@ export class WebSocketTunnelAdapter {
114
118
  }
115
119
 
116
120
  send(data: string | ArrayBuffer | ArrayBufferView | Blob | Buffer): void {
117
- if (this.#readyState !== 1) { // OPEN
121
+ if (this.#readyState !== 1) {
122
+ // OPEN
118
123
  throw new Error("WebSocket is not open");
119
124
  }
120
125
 
@@ -133,29 +138,43 @@ export class WebSocketTunnelAdapter {
133
138
  // Check if it's a SharedArrayBuffer
134
139
  if (view.buffer instanceof SharedArrayBuffer) {
135
140
  // 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;
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;
138
150
  } else {
139
151
  messageData = view.buffer.slice(
140
152
  view.byteOffset,
141
- view.byteOffset + view.byteLength
153
+ view.byteOffset + view.byteLength,
142
154
  ) as ArrayBuffer;
143
155
  }
144
156
  } else if (data instanceof Blob) {
145
157
  throw new Error("Blob sending not implemented in tunnel adapter");
146
- } else if (typeof Buffer !== 'undefined' && Buffer.isBuffer(data)) {
158
+ } else if (typeof Buffer !== "undefined" && Buffer.isBuffer(data)) {
147
159
  isBinary = true;
148
160
  // Convert Buffer to ArrayBuffer
149
161
  const buf = data as Buffer;
150
162
  // Check if it's a SharedArrayBuffer
151
163
  if (buf.buffer instanceof SharedArrayBuffer) {
152
164
  // 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;
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;
155
174
  } else {
156
175
  messageData = buf.buffer.slice(
157
176
  buf.byteOffset,
158
- buf.byteOffset + buf.byteLength
177
+ buf.byteOffset + buf.byteLength,
159
178
  ) as ArrayBuffer;
160
179
  }
161
180
  } else {
@@ -169,7 +188,7 @@ export class WebSocketTunnelAdapter {
169
188
  close(code?: number, reason?: string): void {
170
189
  if (
171
190
  this.#readyState === 2 || // CLOSING
172
- this.#readyState === 3 // CLOSED
191
+ this.#readyState === 3 // CLOSED
173
192
  ) {
174
193
  return;
175
194
  }
@@ -181,7 +200,7 @@ export class WebSocketTunnelAdapter {
181
200
 
182
201
  // Update state and fire event
183
202
  this.#readyState = 3; // CLOSED
184
-
203
+
185
204
  const closeEvent = {
186
205
  wasClean: true,
187
206
  code: code || 1000,
@@ -189,14 +208,14 @@ export class WebSocketTunnelAdapter {
189
208
  type: "close",
190
209
  target: this,
191
210
  };
192
-
211
+
193
212
  this.#fireEvent("close", closeEvent);
194
213
  }
195
214
 
196
215
  addEventListener(
197
216
  type: string,
198
217
  listener: (event: any) => void,
199
- options?: boolean | any
218
+ options?: boolean | any,
200
219
  ): void {
201
220
  if (typeof listener === "function") {
202
221
  let listeners = this.#eventListeners.get(type);
@@ -214,7 +233,7 @@ export class WebSocketTunnelAdapter {
214
233
  removeEventListener(
215
234
  type: string,
216
235
  listener: (event: any) => void,
217
- options?: boolean | any
236
+ options?: boolean | any,
218
237
  ): void {
219
238
  if (typeof listener === "function") {
220
239
  const listeners = this.#eventListeners.get(type);
@@ -237,12 +256,16 @@ export class WebSocketTunnelAdapter {
237
256
  if (listeners && listeners.size > 0) {
238
257
  hasListeners = true;
239
258
  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
- }
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
+ });
245
267
  }
268
+ }
246
269
  }
247
270
 
248
271
  // Call the onX property if set
@@ -253,7 +276,10 @@ export class WebSocketTunnelAdapter {
253
276
  try {
254
277
  this.#onopen.call(this, event);
255
278
  } catch (error) {
256
- logger()?.error({ msg: "error in onopen handler", error });
279
+ logger()?.error({
280
+ msg: "error in onopen handler",
281
+ error,
282
+ });
257
283
  }
258
284
  }
259
285
  break;
@@ -263,7 +289,10 @@ export class WebSocketTunnelAdapter {
263
289
  try {
264
290
  this.#onclose.call(this, event);
265
291
  } catch (error) {
266
- logger()?.error({ msg: "error in onclose handler", error });
292
+ logger()?.error({
293
+ msg: "error in onclose handler",
294
+ error,
295
+ });
267
296
  }
268
297
  }
269
298
  break;
@@ -273,7 +302,10 @@ export class WebSocketTunnelAdapter {
273
302
  try {
274
303
  this.#onerror.call(this, event);
275
304
  } catch (error) {
276
- logger()?.error({ msg: "error in onerror handler", error });
305
+ logger()?.error({
306
+ msg: "error in onerror handler",
307
+ error,
308
+ });
277
309
  }
278
310
  }
279
311
  break;
@@ -283,7 +315,10 @@ export class WebSocketTunnelAdapter {
283
315
  try {
284
316
  this.#onmessage.call(this, event);
285
317
  } catch (error) {
286
- logger()?.error({ msg: "error in onmessage handler", error });
318
+ logger()?.error({
319
+ msg: "error in onmessage handler",
320
+ error,
321
+ });
287
322
  }
288
323
  }
289
324
  break;
@@ -297,10 +332,10 @@ export class WebSocketTunnelAdapter {
297
332
 
298
333
  #flushBufferedEvents(type: string): void {
299
334
  const eventsToFlush = this.#bufferedEvents.filter(
300
- (buffered) => buffered.type === type
335
+ (buffered) => buffered.type === type,
301
336
  );
302
337
  this.#bufferedEvents = this.#bufferedEvents.filter(
303
- (buffered) => buffered.type !== type
338
+ (buffered) => buffered.type !== type,
304
339
  );
305
340
 
306
341
  for (const { event } of eventsToFlush) {
@@ -327,7 +362,10 @@ export class WebSocketTunnelAdapter {
327
362
  try {
328
363
  this.#onopen.call(this, event);
329
364
  } catch (error) {
330
- logger()?.error({ msg: "error in onopen handler", error });
365
+ logger()?.error({
366
+ msg: "error in onopen handler",
367
+ error,
368
+ });
331
369
  }
332
370
  }
333
371
  break;
@@ -336,7 +374,10 @@ export class WebSocketTunnelAdapter {
336
374
  try {
337
375
  this.#onclose.call(this, event);
338
376
  } catch (error) {
339
- logger()?.error({ msg: "error in onclose handler", error });
377
+ logger()?.error({
378
+ msg: "error in onclose handler",
379
+ error,
380
+ });
340
381
  }
341
382
  }
342
383
  break;
@@ -345,7 +386,10 @@ export class WebSocketTunnelAdapter {
345
386
  try {
346
387
  this.#onerror.call(this, event);
347
388
  } catch (error) {
348
- logger()?.error({ msg: "error in onerror handler", error });
389
+ logger()?.error({
390
+ msg: "error in onerror handler",
391
+ error,
392
+ });
349
393
  }
350
394
  }
351
395
  break;
@@ -354,7 +398,10 @@ export class WebSocketTunnelAdapter {
354
398
  try {
355
399
  this.#onmessage.call(this, event);
356
400
  } catch (error) {
357
- logger()?.error({ msg: "error in onmessage handler", error });
401
+ logger()?.error({
402
+ msg: "error in onmessage handler",
403
+ error,
404
+ });
358
405
  }
359
406
  }
360
407
  break;
@@ -364,27 +411,29 @@ export class WebSocketTunnelAdapter {
364
411
 
365
412
  // Internal methods called by the Tunnel class
366
413
  _handleOpen(): void {
367
- if (this.#readyState !== 0) { // CONNECTING
414
+ if (this.#readyState !== 0) {
415
+ // CONNECTING
368
416
  return;
369
417
  }
370
418
 
371
419
  this.#readyState = 1; // OPEN
372
-
420
+
373
421
  const event = {
374
422
  type: "open",
375
423
  target: this,
376
424
  };
377
-
425
+
378
426
  this.#fireEvent("open", event);
379
427
  }
380
428
 
381
429
  _handleMessage(data: string | Uint8Array, isBinary: boolean): void {
382
- if (this.#readyState !== 1) { // OPEN
430
+ if (this.#readyState !== 1) {
431
+ // OPEN
383
432
  return;
384
433
  }
385
434
 
386
435
  let messageData: any;
387
-
436
+
388
437
  if (isBinary) {
389
438
  // Handle binary data based on binaryType
390
439
  if (this.#binaryType === "nodebuffer") {
@@ -395,14 +444,16 @@ export class WebSocketTunnelAdapter {
395
444
  if (data instanceof Uint8Array) {
396
445
  messageData = data.buffer.slice(
397
446
  data.byteOffset,
398
- data.byteOffset + data.byteLength
447
+ data.byteOffset + data.byteLength,
399
448
  );
400
449
  } else {
401
450
  messageData = data;
402
451
  }
403
452
  } else {
404
453
  // Blob type - not commonly used in Node.js
405
- throw new Error("Blob binaryType not supported in tunnel adapter");
454
+ throw new Error(
455
+ "Blob binaryType not supported in tunnel adapter",
456
+ );
406
457
  }
407
458
  } else {
408
459
  messageData = data;
@@ -418,7 +469,8 @@ export class WebSocketTunnelAdapter {
418
469
  }
419
470
 
420
471
  _handleClose(code?: number, reason?: string): void {
421
- if (this.#readyState === 3) { // CLOSED
472
+ if (this.#readyState === 3) {
473
+ // CLOSED
422
474
  return;
423
475
  }
424
476
 
@@ -472,7 +524,7 @@ export class WebSocketTunnelAdapter {
472
524
  // Immediate close without close frame
473
525
  this.#readyState = 3; // CLOSED
474
526
  this.#closeCallback(1006, "Abnormal Closure");
475
-
527
+
476
528
  const event = {
477
529
  wasClean: false,
478
530
  code: 1006,
@@ -480,7 +532,7 @@ export class WebSocketTunnelAdapter {
480
532
  type: "close",
481
533
  target: this,
482
534
  };
483
-
535
+
484
536
  this.#fireEvent("close", event);
485
537
  }
486
538
  }
package/tsconfig.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "extends": "../../../tsconfig.base.json",
2
+ "extends": "../../../../tsconfig.base.json",
3
3
  "compilerOptions": {
4
4
  "types": ["node"],
5
5
  "paths": {
@@ -7,7 +7,5 @@
7
7
  }
8
8
  },
9
9
  "include": ["src/**/*", "tests/**/*", "benches/**/*"],
10
- "exclude": [
11
- "node_modules"
12
- ]
10
+ "exclude": ["node_modules"]
13
11
  }
package/tsup.config.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import { defineConfig } from "tsup";
2
- import defaultConfig from "../../../tsup.base.ts";
2
+ import defaultConfig from "../../../../tsup.base.ts";
3
3
 
4
4
  export default defineConfig(defaultConfig);
package/turbo.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "$schema": "https://turbo.build/schema.json",
3
- "extends": ["//"]
2
+ "$schema": "https://turbo.build/schema.json",
3
+ "extends": ["//"]
4
4
  }
package/vitest.config.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { resolve } from "path";
1
+ import { resolve } from "node:path";
2
2
  import { defineConfig } from "vitest/config";
3
- import defaultConfig from "../../../vitest.base.ts";
3
+ import defaultConfig from "../../../../vitest.base.ts";
4
4
 
5
5
  export default defineConfig({
6
6
  ...defaultConfig,
@@ -14,4 +14,3 @@ export default defineConfig({
14
14
  include: ["tests/**/*.test.ts"],
15
15
  },
16
16
  });
17
-