rivetkit 3.0.0-preview.3 → 3.0.0-preview.5

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.
Files changed (68) hide show
  1. package/dist/browser/client.js +49 -28
  2. package/dist/browser/client.js.map +1 -1
  3. package/dist/browser/inspector/client.js +1 -1
  4. package/dist/browser/inspector/client.js.map +1 -1
  5. package/dist/tsup/{chunk-KCWRKXTY.js → chunk-AW7C2XT5.js} +2 -2
  6. package/dist/tsup/{chunk-JWI6YJEA.js → chunk-BAXQGOBL.js} +2 -2
  7. package/dist/tsup/{chunk-4WDX5ZI7.cjs → chunk-CFGRIV62.cjs} +2 -2
  8. package/dist/tsup/{chunk-4WDX5ZI7.cjs.map → chunk-CFGRIV62.cjs.map} +1 -1
  9. package/dist/tsup/{chunk-7KES6QNE.cjs → chunk-CLY73DEM.cjs} +3 -3
  10. package/dist/tsup/{chunk-7KES6QNE.cjs.map → chunk-CLY73DEM.cjs.map} +1 -1
  11. package/dist/tsup/{chunk-KMYBA4P5.cjs → chunk-EON5X6B7.cjs} +3 -3
  12. package/dist/tsup/{chunk-KMYBA4P5.cjs.map → chunk-EON5X6B7.cjs.map} +1 -1
  13. package/dist/tsup/{chunk-2VXPVVNW.js → chunk-EOUH7MP7.js} +54 -33
  14. package/dist/tsup/chunk-EOUH7MP7.js.map +1 -0
  15. package/dist/tsup/{chunk-BCX5V7NI.cjs → chunk-GJUWI47D.cjs} +191 -170
  16. package/dist/tsup/chunk-GJUWI47D.cjs.map +1 -0
  17. package/dist/tsup/{chunk-PJ4PORWE.cjs → chunk-KEHBPXZE.cjs} +3 -3
  18. package/dist/tsup/{chunk-PJ4PORWE.cjs.map → chunk-KEHBPXZE.cjs.map} +1 -1
  19. package/dist/tsup/{chunk-HDQQVBOG.js → chunk-KJM23DKG.js} +2 -2
  20. package/dist/tsup/{chunk-JXO5QVDX.js → chunk-KKSIGBUN.js} +2 -2
  21. package/dist/tsup/{chunk-JXO5QVDX.js.map → chunk-KKSIGBUN.js.map} +1 -1
  22. package/dist/tsup/{chunk-HRBZ4VW5.cjs → chunk-MVDTFTCY.cjs} +7 -7
  23. package/dist/tsup/{chunk-HRBZ4VW5.cjs.map → chunk-MVDTFTCY.cjs.map} +1 -1
  24. package/dist/tsup/{chunk-B6U5V6WE.cjs → chunk-PHFXQQGE.cjs} +4 -4
  25. package/dist/tsup/{chunk-B6U5V6WE.cjs.map → chunk-PHFXQQGE.cjs.map} +1 -1
  26. package/dist/tsup/{chunk-A447S5YG.js → chunk-SJUGJ5DT.js} +2 -2
  27. package/dist/tsup/{chunk-AGQNNAZ2.js → chunk-W4CGRSYM.js} +3 -3
  28. package/dist/tsup/{chunk-IJGFSNGA.js → chunk-W5DDGJBO.js} +4 -4
  29. package/dist/tsup/{chunk-7Y4GHUAT.cjs → chunk-ZOFNA4SR.cjs} +10 -10
  30. package/dist/tsup/{chunk-7Y4GHUAT.cjs.map → chunk-ZOFNA4SR.cjs.map} +1 -1
  31. package/dist/tsup/client/mod.cjs +5 -5
  32. package/dist/tsup/client/mod.js +4 -4
  33. package/dist/tsup/common/log.cjs +2 -2
  34. package/dist/tsup/common/log.js +1 -1
  35. package/dist/tsup/common/websocket.cjs +3 -3
  36. package/dist/tsup/common/websocket.js +2 -2
  37. package/dist/tsup/db/drizzle.cjs +2 -2
  38. package/dist/tsup/db/drizzle.js +1 -1
  39. package/dist/tsup/db/mod.cjs +3 -3
  40. package/dist/tsup/db/mod.js +2 -2
  41. package/dist/tsup/inspector/mod.cjs +5 -5
  42. package/dist/tsup/inspector/mod.js +4 -4
  43. package/dist/tsup/inspector/workflow.cjs +4 -4
  44. package/dist/tsup/inspector/workflow.js +3 -3
  45. package/dist/tsup/mod.cjs +94 -90
  46. package/dist/tsup/mod.cjs.map +1 -1
  47. package/dist/tsup/mod.js +15 -11
  48. package/dist/tsup/mod.js.map +1 -1
  49. package/dist/tsup/test/mod.cjs +6 -6
  50. package/dist/tsup/test/mod.js +4 -4
  51. package/dist/tsup/utils.cjs +2 -2
  52. package/dist/tsup/utils.js +1 -1
  53. package/dist/tsup/workflow/mod.cjs +7 -7
  54. package/dist/tsup/workflow/mod.cjs.map +1 -1
  55. package/dist/tsup/workflow/mod.js +4 -4
  56. package/package.json +9 -9
  57. package/src/client/actor-conn.ts +59 -33
  58. package/src/client/lifecycle-errors.ts +13 -0
  59. package/src/registry/config/index.ts +2 -1
  60. package/src/registry/index.ts +16 -3
  61. package/dist/tsup/chunk-2VXPVVNW.js.map +0 -1
  62. package/dist/tsup/chunk-BCX5V7NI.cjs.map +0 -1
  63. /package/dist/tsup/{chunk-KCWRKXTY.js.map → chunk-AW7C2XT5.js.map} +0 -0
  64. /package/dist/tsup/{chunk-JWI6YJEA.js.map → chunk-BAXQGOBL.js.map} +0 -0
  65. /package/dist/tsup/{chunk-HDQQVBOG.js.map → chunk-KJM23DKG.js.map} +0 -0
  66. /package/dist/tsup/{chunk-A447S5YG.js.map → chunk-SJUGJ5DT.js.map} +0 -0
  67. /package/dist/tsup/{chunk-AGQNNAZ2.js.map → chunk-W4CGRSYM.js.map} +0 -0
  68. /package/dist/tsup/{chunk-IJGFSNGA.js.map → chunk-W5DDGJBO.js.map} +0 -0
@@ -50,9 +50,7 @@ import {
50
50
  import { ACTOR_CONNS_SYMBOL, type ClientRaw } from "./client";
51
51
  import * as errors from "./errors";
52
52
  import { isRetryableLifecycleReconnectSignal } from "./lifecycle-errors";
53
- import { retryAfterMs } from "./retry-budget";
54
53
  import { logger } from "./log";
55
- import { isInvalidToken } from "./token-provider";
56
54
  import { outboundTelemetryHeaders } from "./outbound-telemetry";
57
55
  import {
58
56
  createQueueSender,
@@ -61,6 +59,8 @@ import {
61
59
  type QueueSendResult,
62
60
  type QueueSendWaitOptions,
63
61
  } from "./queue";
62
+ import { retryAfterMs } from "./retry-budget";
63
+ import { isInvalidToken } from "./token-provider";
64
64
  import {
65
65
  type WebSocketMessage as ConnMessage,
66
66
  messageLength,
@@ -160,6 +160,8 @@ export class ActorConnRaw {
160
160
  #abortController = new AbortController();
161
161
 
162
162
  #connStatus: ActorConnStatus = "idle";
163
+ /** The opening failure that stopped retries. Later actions must fail instead of queueing. */
164
+ #connectionOpenError?: errors.ActorError;
163
165
 
164
166
  #actorId?: string;
165
167
  #connId?: string;
@@ -356,6 +358,8 @@ export class ActorConnRaw {
356
358
  `Invalid action call: expected an options object { name, args }, got ${typeof opts}. Use conn.actionName(...args) for the shorthand API.`,
357
359
  );
358
360
  }
361
+ if (this.#disposed) throw new errors.ActorConnDisposed();
362
+ if (this.#connectionOpenError) throw this.#connectionOpenError;
359
363
  logger().debug({ msg: "action", name: opts.name, args: opts.args });
360
364
 
361
365
  // If we have an active connection, use the websockactionId
@@ -466,6 +470,7 @@ export class ActorConnRaw {
466
470
  }
467
471
 
468
472
  #connectWithRetry() {
473
+ this.#connectionOpenError = undefined;
469
474
  this.#setConnStatus("connecting");
470
475
 
471
476
  // Attempt to reconnect indefinitely
@@ -523,6 +528,11 @@ export class ActorConnRaw {
523
528
  // Wait for result
524
529
  await this.#waitForOpen(this.#onOpenPromise.promise);
525
530
  } catch (error) {
531
+ this.#abandonCurrentSocket(
532
+ isConnectionOpenTimeout(error)
533
+ ? "Connect timeout"
534
+ : "Connection opening failed",
535
+ );
526
536
  if (this.#shouldRetryConnectionOpenError(error)) {
527
537
  throw error;
528
538
  }
@@ -537,7 +547,7 @@ export class ActorConnRaw {
537
547
  { error: stringifyError(error) },
538
548
  );
539
549
 
540
- this.#clearQueuedMessages();
550
+ this.#connectionOpenError = actorError;
541
551
  this.#rejectPendingPromises(actorError, false);
542
552
  this.#dispatchActorError(actorError);
543
553
  this.#setConnStatus("idle");
@@ -573,29 +583,24 @@ export class ActorConnRaw {
573
583
  });
574
584
  try {
575
585
  await Promise.race([open, timedOut]);
576
- } catch (error) {
577
- if (isConnectionOpenTimeout(error)) {
578
- this.#abandonCurrentSocket();
579
- }
580
- throw error;
581
586
  } finally {
582
587
  clearTimeout(timer);
583
588
  }
584
589
  }
585
590
 
586
- #abandonCurrentSocket() {
591
+ #abandonCurrentSocket(reason: string) {
587
592
  const ws = this.#websocket;
588
593
  if (!ws) return;
589
594
  this.#websocket = undefined;
590
595
  this.#abandonedSockets.add(ws);
591
- logger().warn({
592
- msg: "connection did not open in time, retrying",
596
+ logger().debug({
597
+ msg: "closing failed connection attempt",
593
598
  connId: this.#connId,
594
599
  readyState: ws.readyState,
595
- timeoutMs: CONNECT_TIMEOUT_MS,
600
+ reason,
596
601
  });
597
602
  try {
598
- ws.close(1000, "Connect timeout");
603
+ ws.close(1000, reason);
599
604
  } catch (err) {
600
605
  logger().debug({
601
606
  msg: "failed to close abandoned websocket",
@@ -611,6 +616,13 @@ export class ActorConnRaw {
611
616
  if (isConnectionOpenTimeout(error)) {
612
617
  return true;
613
618
  }
619
+ if (
620
+ error instanceof errors.ActorError &&
621
+ error.group === "client" &&
622
+ error.code === "connection_open_interrupted"
623
+ ) {
624
+ return true;
625
+ }
614
626
  if (
615
627
  error instanceof errors.ActorError &&
616
628
  isInvalidToken(error.group, error.code) &&
@@ -692,16 +704,18 @@ export class ActorConnRaw {
692
704
  });
693
705
  this.#websocket = ws;
694
706
  ws.addEventListener("open", () => {
695
- if (this.#abandonedSockets.has(ws)) return;
707
+ if (this.#websocket !== ws || this.#abandonedSockets.has(ws))
708
+ return;
696
709
  logger().debug({
697
710
  msg: "client websocket open",
698
711
  connId: this.#connId,
699
712
  });
700
713
  });
701
714
  ws.addEventListener("message", async (ev: { data: ConnMessage }) => {
702
- if (this.#abandonedSockets.has(ws)) return;
715
+ if (this.#websocket !== ws || this.#abandonedSockets.has(ws))
716
+ return;
703
717
  try {
704
- await this.#handleOnMessage(ev.data);
718
+ await this.#handleOnMessage(ev.data, ws);
705
719
  } catch (err) {
706
720
  logger().error({
707
721
  msg: "error in websocket message handler",
@@ -710,7 +724,8 @@ export class ActorConnRaw {
710
724
  }
711
725
  });
712
726
  ws.addEventListener("close", async (ev: Event | CloseEventLike) => {
713
- if (this.#abandonedSockets.has(ws)) return;
727
+ if (this.#websocket !== ws || this.#abandonedSockets.has(ws))
728
+ return;
714
729
  try {
715
730
  await this.#handleOnClose(ev);
716
731
  } catch (err) {
@@ -721,7 +736,8 @@ export class ActorConnRaw {
721
736
  }
722
737
  });
723
738
  ws.addEventListener("error", () => {
724
- if (this.#abandonedSockets.has(ws)) return;
739
+ if (this.#websocket !== ws || this.#abandonedSockets.has(ws))
740
+ return;
725
741
  try {
726
742
  this.#handleOnError();
727
743
  } catch (err) {
@@ -803,7 +819,7 @@ export class ActorConnRaw {
803
819
  }
804
820
 
805
821
  /** Called by the onmessage event from drivers. */
806
- async #handleOnMessage(data: any) {
822
+ async #handleOnMessage(data: any, socket: UniversalWebSocket) {
807
823
  logger().trace({
808
824
  msg: "received message",
809
825
  dataType: typeof data,
@@ -812,6 +828,10 @@ export class ActorConnRaw {
812
828
  });
813
829
 
814
830
  const response = await this.#parseMessage(data as ConnMessage);
831
+ // Decoding can yield while this socket closes and another connects. Apply
832
+ // the message only while its socket still owns the connection.
833
+ if (this.#websocket !== socket || this.#abandonedSockets.has(socket))
834
+ return;
815
835
  logger().trace(
816
836
  getLogMessage()
817
837
  ? {
@@ -897,23 +917,19 @@ export class ActorConnRaw {
897
917
  this.#actorResolutionState,
898
918
  this.#driver,
899
919
  );
920
+ if (
921
+ this.#websocket !== socket ||
922
+ this.#abandonedSockets.has(socket)
923
+ ) {
924
+ return;
925
+ }
900
926
  if (schedulingError) {
901
927
  errorToThrow = schedulingError;
902
928
  }
903
929
  }
904
930
 
905
- // If we have an onOpenPromise, reject it with the error
906
- if (this.#onOpenPromise) {
907
- this.#onOpenPromise.reject(errorToThrow);
908
- }
909
-
910
931
  this.#invalidateActorIfStale(group, code);
911
-
912
- // Reject any in-flight requests
913
- for (const [id, inFlight] of this.#actionsInFlight.entries()) {
914
- inFlight.reject(errorToThrow);
915
- this.#actionsInFlight.delete(id);
916
- }
932
+ this.#rejectPendingPromises(errorToThrow, false);
917
933
 
918
934
  this.#dispatchActorError(errorToThrow);
919
935
  }
@@ -1047,9 +1063,17 @@ export class ActorConnRaw {
1047
1063
  this.#invalidateActorIfStale(group, code);
1048
1064
  } else {
1049
1065
  // Default error for non-structured close reasons
1050
- error = new Error(
1051
- `${wasClean ? "Connection closed" : "Connection lost"} (code: ${closeEvent.code}, reason: ${reason})`,
1052
- );
1066
+ const message = `${wasClean ? "Connection closed" : "Connection lost"} (code: ${closeEvent.code}, reason: ${reason})`;
1067
+ // A transport loss during the handshake can outlast an Engine
1068
+ // restart. Retry opening without replaying actions with unknown outcomes.
1069
+ error =
1070
+ !wasConnected && closeEvent.code === 1006
1071
+ ? new errors.ActorError(
1072
+ "client",
1073
+ "connection_open_interrupted",
1074
+ message,
1075
+ )
1076
+ : new Error(message);
1053
1077
  }
1054
1078
 
1055
1079
  this.#rejectPendingPromises(error, false);
@@ -1071,6 +1095,8 @@ export class ActorConnRaw {
1071
1095
  }
1072
1096
 
1073
1097
  #rejectPendingPromises(error: Error, suppressUnhandled: boolean) {
1098
+ // Rejected actions must not survive in the queue and execute after a reconnect.
1099
+ this.#clearQueuedMessages();
1074
1100
  if (this.#onOpenPromise) {
1075
1101
  if (suppressUnhandled) {
1076
1102
  this.#onOpenPromise.promise.catch(() => {});
@@ -77,6 +77,19 @@ function classifyActorError(
77
77
  );
78
78
  }
79
79
 
80
+ if (
81
+ (error.group === "ups" && error.code === "consumer_closed") ||
82
+ (error.group === "depot" && error.code === "request_deadline_exceeded")
83
+ ) {
84
+ // The connection can recover, but an interrupted action may already have run.
85
+ return buildLifecycleBoundaryInfo(
86
+ "reconnect_only",
87
+ "actor_error",
88
+ error.message,
89
+ { group: error.group, code: error.code },
90
+ );
91
+ }
92
+
80
93
  if (
81
94
  error.group === "guard" &&
82
95
  isRetryableGuardGatewayHttpError(error.code)
@@ -291,7 +291,8 @@ export const RegistryConfigSchema = z
291
291
  * after calling `CoreRegistry::shutdown()`. Defaults to the
292
292
  * engine-provided actor stop threshold once the node connects. A
293
293
  * signal-driven shutdown of a managed local development engine waits
294
- * one second by default so a file watcher can restart promptly.
294
+ * four seconds by default so actors can acknowledge stopping before
295
+ * a file watcher restarts the process.
295
296
  *
296
297
  * Set a longer period when a complete graceful node drain is required.
297
298
  */
@@ -29,6 +29,13 @@ import type {
29
29
  type ShutdownSignal = "SIGINT" | "SIGTERM";
30
30
 
31
31
  const REGISTRY_READY_TIMEOUT_MS = 30_000;
32
+ // A managed Engine restart can wait for the old process to release storage and
33
+ // for its worker lease to expire before the replacement can register.
34
+ const LOCAL_ENGINE_READY_TIMEOUT_MS = 60_000;
35
+ // Core gives local actors three seconds to drain and acknowledge their stopped
36
+ // reports. Leave time to close the node connection after that, while staying
37
+ // below the five-second force-kill ceiling used by tsx watch.
38
+ const LOCAL_DEV_SHUTDOWN_GRACE_PERIOD_MS = 4_000;
32
39
 
33
40
  // Serverless is disabled for v3; keep the implementation for a later release.
34
41
  function rejectServerless(): void {
@@ -789,7 +796,7 @@ export class Registry<A extends RegistryActors> {
789
796
  let gracePeriodMs = config.shutdown?.gracePeriodMs;
790
797
  if (gracePeriodMs === undefined) {
791
798
  gracePeriodMs = localDevSignal
792
- ? 1_000
799
+ ? LOCAL_DEV_SHUTDOWN_GRACE_PERIOD_MS
793
800
  : ((await this.#actorStopThresholdMs(
794
801
  modeAPromise ?? modeBPromise,
795
802
  )) ?? 30 * 60 * 1000);
@@ -942,16 +949,22 @@ export class Registry<A extends RegistryActors> {
942
949
  throw new Error("registry envoy startup did not initialize");
943
950
  }
944
951
 
952
+ const readyTimeoutMs =
953
+ config.engineSpawn &&
954
+ config.endpoint !== undefined &&
955
+ isLocalEngineEndpoint(config.endpoint)
956
+ ? LOCAL_ENGINE_READY_TIMEOUT_MS
957
+ : REGISTRY_READY_TIMEOUT_MS;
945
958
  let timeout: ReturnType<typeof setTimeout> | undefined;
946
959
  const timeoutPromise = new Promise<never>((_resolve, reject) => {
947
960
  timeout = setTimeout(
948
961
  () =>
949
962
  reject(
950
963
  new Error(
951
- `RivetKit registry did not register with the Engine within ${REGISTRY_READY_TIMEOUT_MS}ms`,
964
+ `RivetKit registry did not register with the Engine within ${readyTimeoutMs}ms`,
952
965
  ),
953
966
  ),
954
- REGISTRY_READY_TIMEOUT_MS,
967
+ readyTimeoutMs,
955
968
  );
956
969
  timeout.unref?.();
957
970
  });