rivetkit 2.3.30 → 3.0.0-preview.2

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 (135) hide show
  1. package/dist/browser/client.d.ts +58 -9
  2. package/dist/browser/client.js +193 -20
  3. package/dist/browser/client.js.map +1 -1
  4. package/dist/browser/inspector/client.js +1 -2
  5. package/dist/browser/inspector/client.js.map +1 -1
  6. package/dist/tsup/{chunk-P2PHHJMH.cjs → chunk-25MUHNWE.cjs} +326 -166
  7. package/dist/tsup/chunk-25MUHNWE.cjs.map +1 -0
  8. package/dist/tsup/{chunk-5SN6GAYX.cjs → chunk-3SPYZMME.cjs} +3 -3
  9. package/dist/tsup/{chunk-5SN6GAYX.cjs.map → chunk-3SPYZMME.cjs.map} +1 -1
  10. package/dist/tsup/{chunk-4SNS3PYZ.js → chunk-3URL73AV.js} +2 -2
  11. package/dist/tsup/{chunk-EKAQ7HG2.js → chunk-442XBCAF.js} +3 -3
  12. package/dist/tsup/{chunk-CORYIKDC.js → chunk-5F7FWAJI.js} +10 -11
  13. package/dist/tsup/chunk-5F7FWAJI.js.map +1 -0
  14. package/dist/tsup/{chunk-I472KTIC.js → chunk-6IGGYL2C.js} +185 -25
  15. package/dist/tsup/chunk-6IGGYL2C.js.map +1 -0
  16. package/dist/tsup/{chunk-JI6GZ2C2.js → chunk-A2CF6PFH.js} +18 -1
  17. package/dist/tsup/chunk-A2CF6PFH.js.map +1 -0
  18. package/dist/tsup/{chunk-OUB7SGSA.js → chunk-ANNVZW6B.js} +2 -2
  19. package/dist/tsup/{chunk-ZIIQQHC3.cjs → chunk-BCG7PTBS.cjs} +4 -4
  20. package/dist/tsup/{chunk-ZIIQQHC3.cjs.map → chunk-BCG7PTBS.cjs.map} +1 -1
  21. package/dist/tsup/{chunk-OJFVABRH.cjs → chunk-CJ3MX5MI.cjs} +7 -7
  22. package/dist/tsup/{chunk-OJFVABRH.cjs.map → chunk-CJ3MX5MI.cjs.map} +1 -1
  23. package/dist/tsup/{chunk-GEUBEODU.cjs → chunk-EPPV7WB2.cjs} +3 -3
  24. package/dist/tsup/{chunk-GEUBEODU.cjs.map → chunk-EPPV7WB2.cjs.map} +1 -1
  25. package/dist/tsup/{chunk-MXV26ZBB.js → chunk-OLZ4KAFL.js} +4 -4
  26. package/dist/tsup/{chunk-5UUOECX2.cjs → chunk-QDRZMCX5.cjs} +10 -10
  27. package/dist/tsup/{chunk-5UUOECX2.cjs.map → chunk-QDRZMCX5.cjs.map} +1 -1
  28. package/dist/tsup/{chunk-MYWZCDML.cjs → chunk-RTWH3AX3.cjs} +19 -2
  29. package/dist/tsup/chunk-RTWH3AX3.cjs.map +1 -0
  30. package/dist/tsup/{chunk-H3SGQEHZ.js → chunk-WGKL7HMH.js} +2 -2
  31. package/dist/tsup/{chunk-6LOYTFPZ.js → chunk-WGQHG26S.js} +2 -2
  32. package/dist/tsup/{chunk-2BIRGEIU.cjs → chunk-XSOHGU4G.cjs} +10 -11
  33. package/dist/tsup/chunk-XSOHGU4G.cjs.map +1 -0
  34. package/dist/tsup/{chunk-ABPZG7QV.cjs → chunk-YZVVID47.cjs} +3 -3
  35. package/dist/tsup/{chunk-ABPZG7QV.cjs.map → chunk-YZVVID47.cjs.map} +1 -1
  36. package/dist/tsup/client/mod.cjs +6 -6
  37. package/dist/tsup/client/mod.d.cts +15 -3
  38. package/dist/tsup/client/mod.d.ts +15 -3
  39. package/dist/tsup/client/mod.js +5 -5
  40. package/dist/tsup/common/log.cjs +2 -2
  41. package/dist/tsup/common/log.js +1 -1
  42. package/dist/tsup/common/websocket.cjs +3 -3
  43. package/dist/tsup/common/websocket.js +2 -2
  44. package/dist/tsup/{config-osdJqQwX.d.cts → config-8NgsxIcT.d.cts} +43 -12
  45. package/dist/tsup/{config-ey2I2zz5.d.ts → config-B2wlj9wx.d.cts} +7 -1
  46. package/dist/tsup/{config-ey2I2zz5.d.cts → config-B2wlj9wx.d.ts} +7 -1
  47. package/dist/tsup/{config-Cv6pjCt-.d.ts → config-DJ3KW-fe.d.ts} +43 -12
  48. package/dist/tsup/{context-CoVbKBuk.d.ts → context-BL8kUGjq.d.ts} +2 -2
  49. package/dist/tsup/{context-DpjrM7n7.d.cts → context-_RoE_YHL.d.cts} +2 -2
  50. package/dist/tsup/db/drizzle.cjs +2 -2
  51. package/dist/tsup/db/drizzle.d.cts +1 -1
  52. package/dist/tsup/db/drizzle.d.ts +1 -1
  53. package/dist/tsup/db/drizzle.js +1 -1
  54. package/dist/tsup/db/mod.cjs +3 -3
  55. package/dist/tsup/db/mod.d.cts +2 -2
  56. package/dist/tsup/db/mod.d.ts +2 -2
  57. package/dist/tsup/db/mod.js +2 -2
  58. package/dist/tsup/dynamic/mod.d.cts +2 -2
  59. package/dist/tsup/dynamic/mod.d.ts +2 -2
  60. package/dist/tsup/inspector/mod.cjs +5 -5
  61. package/dist/tsup/inspector/mod.js +4 -4
  62. package/dist/tsup/inspector/workflow.cjs +4 -4
  63. package/dist/tsup/inspector/workflow.js +3 -3
  64. package/dist/tsup/inspector-tab/mod.d.cts +3 -3
  65. package/dist/tsup/inspector-tab/mod.d.ts +3 -3
  66. package/dist/tsup/mod.cjs +254 -166
  67. package/dist/tsup/mod.cjs.map +1 -1
  68. package/dist/tsup/mod.d.cts +4 -4
  69. package/dist/tsup/mod.d.ts +4 -4
  70. package/dist/tsup/mod.js +168 -80
  71. package/dist/tsup/mod.js.map +1 -1
  72. package/dist/tsup/test/mod.cjs +7 -7
  73. package/dist/tsup/test/mod.d.cts +2 -2
  74. package/dist/tsup/test/mod.d.ts +2 -2
  75. package/dist/tsup/test/mod.js +5 -5
  76. package/dist/tsup/unstable/migrations.d.cts +1 -1
  77. package/dist/tsup/unstable/migrations.d.ts +1 -1
  78. package/dist/tsup/utils.cjs +2 -2
  79. package/dist/tsup/utils.js +1 -1
  80. package/dist/tsup/workflow/mod.cjs +17 -17
  81. package/dist/tsup/workflow/mod.d.cts +4 -4
  82. package/dist/tsup/workflow/mod.d.ts +4 -4
  83. package/dist/tsup/workflow/mod.js +5 -5
  84. package/package.json +9 -10
  85. package/src/actor/keys.ts +86 -0
  86. package/src/client/actor-conn.ts +92 -6
  87. package/src/client/actor-handle.ts +21 -3
  88. package/src/client/client.ts +6 -0
  89. package/src/client/config.ts +12 -1
  90. package/src/client/lifecycle-errors.ts +16 -1
  91. package/src/client/retry-budget.ts +78 -0
  92. package/src/client/utils.ts +13 -2
  93. package/src/common/database/config.ts +7 -1
  94. package/src/common/database/native-database.ts +17 -6
  95. package/src/common/router.ts +1 -1
  96. package/src/engine-api/actors.ts +3 -3
  97. package/src/engine-client/actor-websocket-client.ts +4 -2
  98. package/src/engine-client/api-endpoints.ts +44 -14
  99. package/src/engine-client/mod.ts +2 -2
  100. package/src/registry/config/envoy.ts +1 -1
  101. package/src/registry/config/index.ts +28 -31
  102. package/src/registry/index.ts +51 -19
  103. package/src/registry/napi-runtime.ts +3 -2
  104. package/src/registry/native.ts +126 -34
  105. package/src/registry/runtime.ts +27 -10
  106. package/src/registry/wasm-runtime.ts +6 -3
  107. package/src/utils/env-vars.ts +15 -10
  108. package/dist/tsup/chunk-2BIRGEIU.cjs.map +0 -1
  109. package/dist/tsup/chunk-CORYIKDC.js.map +0 -1
  110. package/dist/tsup/chunk-I472KTIC.js.map +0 -1
  111. package/dist/tsup/chunk-JI6GZ2C2.js.map +0 -1
  112. package/dist/tsup/chunk-MYWZCDML.cjs.map +0 -1
  113. package/dist/tsup/chunk-P2PHHJMH.cjs.map +0 -1
  114. package/src/actor/config.test.ts +0 -78
  115. package/src/actor/keys.test.ts +0 -275
  116. package/src/actor/utils.test.ts +0 -48
  117. package/src/client/test.ts +0 -44
  118. package/src/client/token-provider.test.ts +0 -86
  119. package/src/common/database/mod.test.ts +0 -364
  120. package/src/common/database/native-database.test.ts +0 -220
  121. package/src/common/inline-websocket-adapter.test.ts +0 -51
  122. package/src/db/drizzle.test.ts +0 -164
  123. package/src/registry/config/index.test.ts +0 -25
  124. package/src/registry/run-handler-coordinator.test.ts +0 -252
  125. package/src/registry/runtime.test.ts +0 -124
  126. package/src/unstable/migrations.test.ts +0 -170
  127. package/src/utils/endpoint-parser.test.ts +0 -206
  128. package/src/utils.test.ts +0 -34
  129. package/src/workflow/driver.test.ts +0 -62
  130. /package/dist/tsup/{chunk-4SNS3PYZ.js.map → chunk-3URL73AV.js.map} +0 -0
  131. /package/dist/tsup/{chunk-EKAQ7HG2.js.map → chunk-442XBCAF.js.map} +0 -0
  132. /package/dist/tsup/{chunk-OUB7SGSA.js.map → chunk-ANNVZW6B.js.map} +0 -0
  133. /package/dist/tsup/{chunk-MXV26ZBB.js.map → chunk-OLZ4KAFL.js.map} +0 -0
  134. /package/dist/tsup/{chunk-H3SGQEHZ.js.map → chunk-WGKL7HMH.js.map} +0 -0
  135. /package/dist/tsup/{chunk-6LOYTFPZ.js.map → chunk-WGQHG26S.js.map} +0 -0
@@ -158,7 +158,7 @@ export const RegistryConfigSchema = z
158
158
  // // created or must be imported async using `await import(...)`
159
159
  // getUpgradeWebSocket: z.custom<GetUpgradeWebSocket>().optional(),
160
160
 
161
- // MARK: Runner Configuration
161
+ // MARK: Node Configuration
162
162
  /**
163
163
  * Endpoint to connect to for Rivet Engine.
164
164
  *
@@ -251,14 +251,14 @@ export const RegistryConfigSchema = z
251
251
  .default(() => getRivetRunEngineVersion() ?? VERSION),
252
252
  /**
253
253
  * Starts the first-party Services actor host when RivetKit manages
254
- * a local engine. Defaults to the local-engine decision. Set
255
- * RIVET_RUN_SERVICES=0 or 1 to override it.
254
+ * a local engine. Disabled by default. Set RIVET_RUN_SERVICES=1
255
+ * or startServices: true to enable it.
256
256
  */
257
257
  startServices: z.boolean().optional(),
258
258
  /**
259
259
  * @experimental
260
260
  *
261
- * Automatically configure serverless envoys in the engine.
261
+ * Automatically configure serverless nodes in the engine.
262
262
  */
263
263
  configurePool: ConfigurePoolSchema.optional(),
264
264
 
@@ -266,7 +266,7 @@ export const RegistryConfigSchema = z
266
266
  serverless: ServerlessConfigSchema.optional().default(() =>
267
267
  ServerlessConfigSchema.parse({}),
268
268
  ),
269
- envoy: EnvoyConfigSchema.optional().default(() =>
269
+ node: EnvoyConfigSchema.optional().default(() =>
270
270
  EnvoyConfigSchema.parse({}),
271
271
  ),
272
272
 
@@ -274,9 +274,9 @@ export const RegistryConfigSchema = z
274
274
  /**
275
275
  * Graceful shutdown configuration for SIGINT/SIGTERM.
276
276
  *
277
- * When a persistent envoy is running (Mode A, started via `registry.start()`),
277
+ * When a persistent node is running (Mode A, started via `registry.start()`),
278
278
  * rivetkit installs Node SIGINT/SIGTERM handlers that call into core's
279
- * `shutdown()` and wait up to `gracePeriodMs` for the envoy to drain
279
+ * `shutdown()` and wait up to `gracePeriodMs` for the node to drain
280
280
  * before re-raising the signal to let Node exit via its default path.
281
281
  *
282
282
  * Handlers are NOT installed when `handler(request)` is used alone
@@ -289,9 +289,11 @@ export const RegistryConfigSchema = z
289
289
  /**
290
290
  * Wait this many milliseconds for the serve promise to resolve
291
291
  * after calling `CoreRegistry::shutdown()`. Defaults to the
292
- * engine-provided actor stop threshold once the envoy connects.
292
+ * engine-provided actor stop threshold once the node connects. A
293
+ * signal-driven shutdown of a managed local development engine waits
294
+ * one second by default so a file watcher can restart promptly.
293
295
  *
294
- * Must be long enough for rivetkit-core to drain the envoy.
296
+ * Set a longer period when a complete graceful node drain is required.
295
297
  */
296
298
  gracePeriodMs: z.number().int().min(1_000).optional(),
297
299
  /**
@@ -348,12 +350,12 @@ export const RegistryConfigSchema = z
348
350
  });
349
351
  }
350
352
 
351
- // configurePool requires an engine (via endpoint or startEngine).
352
- if (config.configurePool && !parsedEndpoint && !config.startEngine) {
353
+ // Serverless pool configuration is disabled for v3.
354
+ if (config.configurePool) {
353
355
  ctx.addIssue({
354
356
  code: "custom",
355
- message:
356
- "configurePool requires either endpoint or startEngine",
357
+ message: "serverless is disabled in this version",
358
+ path: ["configurePool"],
357
359
  });
358
360
  }
359
361
 
@@ -366,13 +368,7 @@ export const RegistryConfigSchema = z
366
368
  ? localEngineEndpoint
367
369
  : (parsedEndpoint?.endpoint ??
368
370
  (isDevEnv ? localEngineEndpoint : undefined));
369
- // Services is another local development actor host. Its default
370
- // follows the same local-endpoint decision that causes core to manage the
371
- // Engine, while explicit config and RIVET_RUN_SERVICES take precedence.
372
- const startServices =
373
- config.startServices ??
374
- getRivetRunServices() ??
375
- Boolean(endpoint && isLocalEngineEndpoint(endpoint));
371
+ const startServices = config.startServices ?? getRivetRunServices();
376
372
  const validateServerlessEndpoint = Boolean(
377
373
  config.startEngine || parsedEndpoint,
378
374
  );
@@ -422,6 +418,7 @@ export const RegistryConfigSchema = z
422
418
  ...config,
423
419
  sqlite,
424
420
  endpoint,
421
+ engineSpawn: !parsedEndpoint && Boolean(endpoint),
425
422
  startServices,
426
423
  namespace,
427
424
  token,
@@ -468,10 +465,10 @@ export function buildActorNames(
468
465
 
469
466
  export const DocConfigurePoolSchema = z
470
467
  .object({
471
- name: z.string().optional().describe("Name of the runner pool."),
468
+ name: z.string().optional().describe("Name of the node pool."),
472
469
  url: z
473
470
  .string()
474
- .describe("URL of the serverless platform to configure runners."),
471
+ .describe("URL of the serverless platform to configure nodes."),
475
472
  headers: z
476
473
  .record(z.string(), z.string())
477
474
  .optional()
@@ -504,7 +501,7 @@ export const DocConfigurePoolSchema = z
504
501
  .boolean()
505
502
  .optional()
506
503
  .describe(
507
- "Drain runners when a new version is deployed. Defaults to true.",
504
+ "Drain nodes when a new version is deployed. Defaults to true.",
508
505
  ),
509
506
  })
510
507
  .optional();
@@ -547,17 +544,17 @@ export const DocEnvoyConfigSchema = z
547
544
  poolName: z
548
545
  .string()
549
546
  .optional()
550
- .describe("Name of this envoy pool. Default: 'default'"),
551
- envoyKey: z
547
+ .describe("Name of this node pool. Default: 'default'"),
548
+ nodeKey: z
552
549
  .string()
553
550
  .optional()
554
- .describe("Deprecated. Authentication key for the envoy."),
551
+ .describe("Deprecated. Authentication key for the node."),
555
552
  version: z
556
553
  .number()
557
554
  .optional()
558
- .describe("Version number of this envoy. Default: 1"),
555
+ .describe("Version number of this node. Default: 1"),
559
556
  })
560
- .describe("Configuration for envoy mode.");
557
+ .describe("Configuration for node mode.");
561
558
 
562
559
  export const DocSqliteConfigSchema = z
563
560
  .object({
@@ -658,12 +655,12 @@ export const DocRegistryConfigSchema = z
658
655
  .boolean()
659
656
  .optional()
660
657
  .describe(
661
- "Starts Services when RivetKit manages a local Engine. Defaults to the local-Engine decision. Set RIVET_RUN_SERVICES=0 or 1 to override.",
658
+ "Starts Services when RivetKit manages a local Engine. Disabled by default. Set RIVET_RUN_SERVICES=1 or startServices: true to enable.",
662
659
  ),
663
660
  configurePool: DocConfigurePoolSchema.describe(
664
- "Automatically configure serverless runners in the engine.",
661
+ "Automatically configure serverless nodes in the engine.",
665
662
  ),
666
663
  serverless: DocServerlessConfigSchema.optional(),
667
- envoy: DocEnvoyConfigSchema.optional(),
664
+ node: DocEnvoyConfigSchema.optional(),
668
665
  })
669
666
  .describe("RivetKit registry configuration.");
@@ -30,6 +30,11 @@ type ShutdownSignal = "SIGINT" | "SIGTERM";
30
30
 
31
31
  const REGISTRY_READY_TIMEOUT_MS = 30_000;
32
32
 
33
+ // Serverless is disabled for v3; keep the implementation for a later release.
34
+ function rejectServerless(): void {
35
+ throw new Error("serverless is disabled in this version");
36
+ }
37
+
33
38
  function signalExitCode(signal: ShutdownSignal): number {
34
39
  switch (signal) {
35
40
  case "SIGINT":
@@ -190,6 +195,7 @@ export class Registry<A extends RegistryActors> {
190
195
  config: RegistryConfig,
191
196
  ): Promise<void> | undefined {
192
197
  if (!config.configurePool) return undefined;
198
+ rejectServerless();
193
199
 
194
200
  if (!this.#configureServerlessPoolPromise) {
195
201
  this.#configureServerlessPoolPromise = configureServerlessPool(
@@ -215,6 +221,7 @@ export class Registry<A extends RegistryActors> {
215
221
  * ```
216
222
  */
217
223
  public async handler(request: Request): Promise<Response> {
224
+ rejectServerless();
218
225
  const config = this.parseConfig();
219
226
  this.#printWelcome(config, "serverless");
220
227
 
@@ -413,6 +420,7 @@ export class Registry<A extends RegistryActors> {
413
420
  * ```
414
421
  */
415
422
  public serve(): ServerlessHandler {
423
+ rejectServerless();
416
424
  return {
417
425
  fetch: (request) => this.handler(request),
418
426
  };
@@ -438,6 +446,9 @@ export class Registry<A extends RegistryActors> {
438
446
  * ```
439
447
  */
440
448
  public async listen(opts: RegistryListenOptions = {}): Promise<void> {
449
+ if (!opts.application) {
450
+ rejectServerless();
451
+ }
441
452
  const port = opts.port ?? parsePortEnv(process.env.RIVET_PORT) ?? 3000;
442
453
  const publicDir = opts.publicDir ?? getRivetkitPublicDir();
443
454
  const config = this.parseConfig();
@@ -554,6 +565,7 @@ export class Registry<A extends RegistryActors> {
554
565
  * Returns serverless metadata suitable for mounting in a user-owned router.
555
566
  */
556
567
  async #metadataRoute(): Promise<Response> {
568
+ rejectServerless();
557
569
  const configured = await this.#activeConfiguredRegistry();
558
570
  if (!configured) {
559
571
  return new Response("registry not started\n", {
@@ -626,7 +638,7 @@ export class Registry<A extends RegistryActors> {
626
638
  */
627
639
  #startEnvoy(config: RegistryConfig, printWelcome: boolean) {
628
640
  if (!this.#runtimeServePromise) {
629
- const configuredEnvoyVersion = this.#config.envoy?.version;
641
+ const configuredEnvoyVersion = this.#config.node?.version;
630
642
  const envoyVersionSource =
631
643
  configuredEnvoyVersion !== undefined
632
644
  ? "config"
@@ -637,7 +649,7 @@ export class Registry<A extends RegistryActors> {
637
649
  logger().info({
638
650
  msg: "starting rivetkit envoy",
639
651
  rivetkitVersion: VERSION,
640
- envoyVersion: config.envoy.version,
652
+ envoyVersion: config.node.version,
641
653
  envoyVersionSource,
642
654
  });
643
655
 
@@ -646,8 +658,8 @@ export class Registry<A extends RegistryActors> {
646
658
  getNodeEnv() === "production"
647
659
  ) {
648
660
  logger().error({
649
- msg: "envoy version defaulted because neither config.envoy.version nor RIVET_ENVOY_VERSION is set; actors will not drain automatically across deploys. See https://rivet.dev/docs/actors/versions",
650
- envoyVersion: config.envoy.version,
661
+ msg: "node version defaulted because neither config.node.version nor RIVET_NODE_VERSION is set; actors will not drain automatically across deploys. See https://rivet.dev/docs/actors/versions",
662
+ envoyVersion: config.node.version,
651
663
  envoyVersionSource,
652
664
  });
653
665
  }
@@ -666,7 +678,13 @@ export class Registry<A extends RegistryActors> {
666
678
  // rejection unhandled. Downstream awaits (e.g. #runShutdown's
667
679
  // Promise.race) attach their own catches and still observe
668
680
  // resolution via the race.
669
- logger().warn({ error }, "runtime registry serve errored");
681
+ logger().error({ error }, "runtime registry serve errored");
682
+ if (
683
+ typeof process !== "undefined" &&
684
+ typeof process.exit === "function"
685
+ ) {
686
+ process.exit(1);
687
+ }
670
688
  });
671
689
  // Install signal handlers once an envoy lifecycle has begun. Only
672
690
  // Mode A ever reaches here. Mode B (handler(request)) intentionally
@@ -714,7 +732,7 @@ export class Registry<A extends RegistryActors> {
714
732
  finishShutdownSignal(signal);
715
733
  return;
716
734
  }
717
- this.#shutdownInFlight = this.#drain(config)
735
+ this.#shutdownInFlight = this.#drain(config, true)
718
736
  .catch((err) => {
719
737
  logger().warn({ err }, "shutdown error");
720
738
  })
@@ -757,18 +775,28 @@ export class Registry<A extends RegistryActors> {
757
775
  return this.#shutdownInFlight;
758
776
  }
759
777
 
760
- async #drain(config: RegistryConfig): Promise<void> {
778
+ async #drain(config: RegistryConfig, signalDriven = false): Promise<void> {
761
779
  const modeAPromise = this.#runtimeServeConfiguredPromise;
762
780
  const modeBPromise = this.#runtimeServerlessPromise;
763
781
  const runtimes = new Set<CoreRuntime>();
764
782
 
765
- const gracePeriodMs =
766
- config.shutdown?.gracePeriodMs ??
767
- (await this.#actorStopThresholdMs(modeAPromise ?? modeBPromise)) ??
768
- 30 * 60 * 1000;
783
+ const localDevSignal =
784
+ signalDriven &&
785
+ getNodeEnv() !== "production" &&
786
+ config.engineSpawn &&
787
+ config.endpoint !== undefined &&
788
+ isLocalEngineEndpoint(config.endpoint);
789
+ let gracePeriodMs = config.shutdown?.gracePeriodMs;
790
+ if (gracePeriodMs === undefined) {
791
+ gracePeriodMs = localDevSignal
792
+ ? 1_000
793
+ : ((await this.#actorStopThresholdMs(
794
+ modeAPromise ?? modeBPromise,
795
+ )) ?? 30 * 60 * 1000);
796
+ }
769
797
  // Race the entire drain sequence (both modes + serve promise) against
770
- // a single grace ceiling. By default, this uses the engine-provided
771
- // actor stop threshold, matching Pegboard's hard cutoff for actors.
798
+ // a single grace ceiling. Local dev signal shutdowns allow the watcher
799
+ // to restart promptly; other shutdowns use the engine's actor stop threshold.
772
800
  const drain = async () => {
773
801
  // Shut down every live `CoreRegistry` we know about. Mode A
774
802
  // (`start()`) and Mode B (`handler()`) each build a separate
@@ -887,12 +915,16 @@ export class Registry<A extends RegistryActors> {
887
915
  );
888
916
  }
889
917
  if (this.#runtimeReadyPromise) return this.#runtimeReadyPromise;
890
- if (getRivetkitRuntimeMode() === "serverless") {
891
- return Promise.reject(
892
- new Error(
893
- "registry.startAndWait() requires envoy runtime mode; serverless registries become ready per request",
894
- ),
895
- );
918
+ try {
919
+ if (getRivetkitRuntimeMode() === "serverless") {
920
+ return Promise.reject(
921
+ new Error(
922
+ "registry.startAndWait() requires envoy runtime mode; serverless registries become ready per request",
923
+ ),
924
+ );
925
+ }
926
+ } catch (error) {
927
+ return Promise.reject(error);
896
928
  }
897
929
 
898
930
  const config = this.parseConfig();
@@ -481,8 +481,9 @@ export class NapiCoreRuntime implements CoreRuntime {
481
481
  onCancellationTokenCancelled(
482
482
  token: CancellationTokenHandle,
483
483
  callback: (...args: unknown[]) => unknown,
484
- ): void {
485
- asNativeCancellationToken(token).onCancelled(callback);
484
+ ): () => void {
485
+ const listener = asNativeCancellationToken(token).onCancelled(callback);
486
+ return () => listener.release();
486
487
  }
487
488
 
488
489
  actorState(ctx: ActorContextHandle): Buffer {
@@ -26,6 +26,7 @@ import {
26
26
  } from "@/actor/config";
27
27
  import type { AnyActorDefinition } from "@/actor/definition";
28
28
  import {
29
+ actorStopping,
29
30
  decodeBridgeRivetError,
30
31
  encodeBridgeRivetError,
31
32
  forbiddenError,
@@ -126,6 +127,7 @@ import type {
126
127
  RuntimeStateDeltaPayload,
127
128
  WebSocketHandle,
128
129
  } from "./runtime";
130
+ import { servicesBinaryUnavailable } from "./runtime";
129
131
  import { loadWasmRuntime } from "./wasm-runtime";
130
132
  import {
131
133
  createWriteThroughProxy,
@@ -347,6 +349,11 @@ type NativeDestroyGate = {
347
349
  type NativeDatabaseClientState = {
348
350
  client: unknown;
349
351
  };
352
+ type NativeActorAbort = {
353
+ signal: AbortSignal;
354
+ listeners: Set<() => void>;
355
+ };
356
+
350
357
  type NativeActorRuntimeState = {
351
358
  sql?: ReturnType<typeof wrapJsNativeDatabase>;
352
359
  databaseClient?: NativeDatabaseClientState;
@@ -354,6 +361,17 @@ type NativeActorRuntimeState = {
354
361
  vars?: unknown;
355
362
  destroyGate?: NativeDestroyGate;
356
363
  persistState?: NativePersistActorState;
364
+ // One native actor abort signal shared by every context of the actor. Each
365
+ // native signal parks a task and a callback handle until the actor stops,
366
+ // so creating one per request or action leaks both. Per-context listeners
367
+ // live in `listeners` so the shared signal never collects one listener per
368
+ // in-flight request.
369
+ actorAbort?: NativeActorAbort;
370
+ // Set once shutdown cleanup has cleared this generation's caches. Work that
371
+ // outlives the generation, such as an untracked timer or a late WebSocket
372
+ // message handler, then fails with `actor.stopping` instead of reading an
373
+ // empty bag as if `vars` or the database were never initialized.
374
+ cleared?: true;
357
375
  };
358
376
 
359
377
  // Keep JS-only actor caches on the NAPI ActorContext runtime-state bag instead
@@ -363,9 +381,10 @@ function getNativeRuntimeState(
363
381
  runtime: CoreRuntime,
364
382
  ctx: ActorContextHandle,
365
383
  ): NativeActorRuntimeState {
366
- const runtimeState = callNativeSync(() =>
367
- runtime.actorRuntimeState(ctx),
368
- ) as NativeActorRuntimeState;
384
+ const runtimeState = readNativeRuntimeStateBag(runtime, ctx);
385
+ if (runtimeState.cleared) {
386
+ throw actorStopping(callNativeSync(() => runtime.actorId(ctx)));
387
+ }
369
388
  if (!runtimeState.destroyGate) {
370
389
  runtimeState.destroyGate = {};
371
390
  }
@@ -379,6 +398,22 @@ function getNativeRuntimeState(
379
398
  return runtimeState;
380
399
  }
381
400
 
401
+ function readNativeRuntimeStateBag(
402
+ runtime: CoreRuntime,
403
+ ctx: ActorContextHandle,
404
+ ): NativeActorRuntimeState {
405
+ return callNativeSync(() =>
406
+ runtime.actorRuntimeState(ctx),
407
+ ) as NativeActorRuntimeState;
408
+ }
409
+
410
+ function isNativeRuntimeStateCleared(
411
+ runtime: CoreRuntime,
412
+ ctx: ActorContextHandle,
413
+ ): boolean {
414
+ return readNativeRuntimeStateBag(runtime, ctx).cleared === true;
415
+ }
416
+
382
417
  function getNativePersistState(
383
418
  runtime: CoreRuntime,
384
419
  ctx: ActorContextHandle,
@@ -521,7 +556,18 @@ function clearNativeRuntimeState(
521
556
  runtime: CoreRuntime,
522
557
  ctx: ActorContextHandle,
523
558
  ) {
559
+ // A coalesced save armed before the clear would otherwise fire against the
560
+ // cleared bag and throw outside any caller.
561
+ const pendingSaveHandle = readNativeRuntimeStateBag(runtime, ctx)
562
+ .persistState?.pendingSaveHandle;
563
+ if (pendingSaveHandle !== undefined) {
564
+ clearImmediate(pendingSaveHandle);
565
+ }
524
566
  callNativeSync(() => runtime.actorClearRuntimeState(ctx));
567
+ // The runtime hands out a fresh bag after a clear. Each generation has its
568
+ // own bag, so marking it makes later access from this generation fail typed
569
+ // without touching any newer generation of the same actor.
570
+ readNativeRuntimeStateBag(runtime, ctx).cleared = true;
525
571
  }
526
572
 
527
573
  async function cleanupNativeSleepRuntimeState(
@@ -536,8 +582,7 @@ async function cleanupNativeSleepRuntimeState(
536
582
  // the tracked work finishes without a deadline.
537
583
  const drained = await runtime.actorWaitForTrackedShutdownWork(ctx);
538
584
  if (!drained) {
539
- await closeNativeDatabaseClient(runtime, ctx);
540
- await closeNativeSqlDatabase(runtime, ctx);
585
+ await detachNativeDatabaseForShutdown(runtime, ctx);
541
586
  void runtime
542
587
  .actorWaitForTrackedShutdownWorkUnbounded(ctx)
543
588
  .then(async () => {
@@ -554,11 +599,26 @@ async function cleanupNativeSleepRuntimeState(
554
599
  }
555
600
 
556
601
  await afterTrackedWorkDrained?.();
557
- await closeNativeDatabaseClient(runtime, ctx);
558
- await closeNativeSqlDatabase(runtime, ctx);
602
+ await detachNativeDatabaseForShutdown(runtime, ctx);
559
603
  clearNativeRuntimeState(runtime, ctx);
560
604
  }
561
605
 
606
+ async function detachNativeDatabaseForShutdown(
607
+ runtime: CoreRuntime,
608
+ ctx: ActorContextHandle,
609
+ ): Promise<void> {
610
+ const runtimeState = getNativeRuntimeState(runtime, ctx);
611
+ // Stop JS access without closing the runtime-owned SQLite backend. Rust still
612
+ // needs it to reconcile scheduled alarms and performs the physical close after
613
+ // this lifecycle callback returns.
614
+ await runtimeState.sql?.detachForShutdown();
615
+ try {
616
+ await closeNativeDatabaseClient(runtime, ctx);
617
+ } finally {
618
+ runtimeState.sql = undefined;
619
+ }
620
+ }
621
+
562
622
  function closeNativeSqlDatabase(
563
623
  runtime: CoreRuntime,
564
624
  ctx: ActorContextHandle,
@@ -784,7 +844,7 @@ async function loadEngineCli(): Promise<typeof import("@rivetkit/engine-cli")> {
784
844
  return import(["@rivetkit", "engine-cli"].join("/"));
785
845
  }
786
846
 
787
- async function loadServices(): Promise<typeof import("@rivet-dev/services")> {
847
+ async function loadServices(): Promise<{ getServicesPath: () => string }> {
788
848
  // Keep the native binary opaque to edge bundlers for the same reason as
789
849
  // engine-cli above.
790
850
  return import(["@rivet-dev", "services"].join("/"));
@@ -3022,22 +3082,28 @@ export class ActorContextHandleAdapter {
3022
3082
 
3023
3083
  get abortSignal(): AbortSignal {
3024
3084
  if (!this.#abortSignal) {
3025
- const actorSignal = this.#createActorAbortSignal();
3085
+ const actorAbort = this.#actorAbort();
3086
+ const actorSignal = actorAbort.signal;
3026
3087
  if (this.#dispatchCancelToken === undefined) {
3027
3088
  this.#abortSignal = actorSignal;
3028
3089
  } else {
3029
3090
  const controller = new AbortController();
3030
3091
  let cleanedUp = false;
3092
+ let releaseDispatchListener: (() => void) | undefined;
3031
3093
  const onActorAbort = () => {
3032
3094
  cleanup();
3033
3095
  controller.abort();
3034
3096
  };
3097
+ // Releasing the native listener on dispose frees it even when the
3098
+ // dispatch token is never cancelled.
3035
3099
  const cleanup = () => {
3036
3100
  if (cleanedUp) {
3037
3101
  return;
3038
3102
  }
3039
3103
  cleanedUp = true;
3040
- actorSignal.removeEventListener("abort", onActorAbort);
3104
+ actorAbort.listeners.delete(onActorAbort);
3105
+ releaseDispatchListener?.();
3106
+ releaseDispatchListener = undefined;
3041
3107
  this.#abortSignalCleanup = undefined;
3042
3108
  };
3043
3109
 
@@ -3051,10 +3117,8 @@ export class ActorContextHandleAdapter {
3051
3117
  } else {
3052
3118
  const dispatchCancelToken = this.#dispatchCancelToken;
3053
3119
  this.#abortSignalCleanup = cleanup;
3054
- actorSignal.addEventListener("abort", onActorAbort, {
3055
- once: true,
3056
- });
3057
- callNativeSync(() =>
3120
+ actorAbort.listeners.add(onActorAbort);
3121
+ releaseDispatchListener = callNativeSync(() =>
3058
3122
  this.#runtime.onCancellationTokenCancelled(
3059
3123
  dispatchCancelToken,
3060
3124
  () => {
@@ -3506,24 +3570,49 @@ export class ActorContextHandleAdapter {
3506
3570
 
3507
3571
  async dispose(): Promise<void> {
3508
3572
  // Flush any save coalesced for this tick before the context is torn
3509
- // down so the request-save and onStateChange always run.
3510
- this.#flushStateChange();
3573
+ // down so the request-save and onStateChange always run. Shutdown
3574
+ // cleanup may already have cleared the generation, which leaves no
3575
+ // pending save to flush.
3576
+ if (!isNativeRuntimeStateCleared(this.#runtime, this.#ctx)) {
3577
+ this.#flushStateChange();
3578
+ }
3511
3579
  this.#abortSignalCleanup?.();
3512
3580
  }
3513
3581
 
3514
- #createActorAbortSignal(): AbortSignal {
3582
+ #actorAbort(): NativeActorAbort {
3583
+ // An aborted cached signal may belong to a previous generation, so
3584
+ // only a live one is reused.
3585
+ const runtimeState = readNativeRuntimeStateBag(
3586
+ this.#runtime,
3587
+ this.#ctx,
3588
+ );
3589
+ const cached = runtimeState.actorAbort;
3590
+ if (cached && !cached.signal.aborted) {
3591
+ return cached;
3592
+ }
3593
+
3515
3594
  const nativeSignal = callNativeSync(() =>
3516
3595
  this.#runtime.actorAbortSignal(this.#ctx),
3517
3596
  );
3518
3597
  const controller = new AbortController();
3519
- if (nativeSignal.aborted) {
3598
+ const actorAbort: NativeActorAbort = {
3599
+ signal: controller.signal,
3600
+ listeners: new Set(),
3601
+ };
3602
+ const abort = () => {
3520
3603
  controller.abort();
3604
+ for (const listener of actorAbort.listeners) {
3605
+ listener();
3606
+ }
3607
+ actorAbort.listeners.clear();
3608
+ };
3609
+ if (nativeSignal.aborted) {
3610
+ abort();
3521
3611
  } else {
3522
- nativeSignal.addEventListener("abort", () => controller.abort(), {
3523
- once: true,
3524
- });
3612
+ nativeSignal.addEventListener("abort", abort, { once: true });
3525
3613
  }
3526
- return controller.signal;
3614
+ runtimeState.actorAbort = actorAbort;
3615
+ return actorAbort;
3527
3616
  }
3528
3617
 
3529
3618
  #readState(): unknown {
@@ -4846,7 +4935,7 @@ export function buildNativeFactory(
4846
4935
  }
4847
4936
  } finally {
4848
4937
  resolveNativeDestroy(runtime, ctx);
4849
- await actorCtx.closeDatabase();
4938
+ await detachNativeDatabaseForShutdown(runtime, ctx);
4850
4939
  clearNativeRuntimeState(runtime, ctx);
4851
4940
  await actorCtx.dispose();
4852
4941
  }
@@ -5598,11 +5687,12 @@ export async function buildServeConfig(
5598
5687
  }
5599
5688
 
5600
5689
  const serveConfig: RuntimeServeConfig = {
5601
- version: config.envoy.version,
5690
+ version: config.node.version,
5602
5691
  endpoint: config.endpoint,
5692
+ engineSpawn: config.engineSpawn,
5603
5693
  token: config.token,
5604
5694
  namespace: config.namespace,
5605
- poolName: config.envoy.poolName,
5695
+ poolName: config.node.poolName,
5606
5696
  startServices: resolveServicesBinary && config.startServices,
5607
5697
  handleInspectorHttpInRuntime: true,
5608
5698
  serverlessBasePath: config.serverless.basePath,
@@ -5616,7 +5706,7 @@ export async function buildServeConfig(
5616
5706
 
5617
5707
  // Always best-effort resolve the npm-installed engine binary and hand its
5618
5708
  // path to the core. The core alone decides whether to actually spawn a local
5619
- // engine (its `should_manage_engine`, based on the endpoint + spawn mode), so
5709
+ // engine using the resolved spawn decision, so
5620
5710
  // JS must not duplicate that decision here. Only JS knows the npm
5621
5711
  // `node_modules` layout, so it resolves the path; if no binary is available
5622
5712
  // (remote-only install, unsupported platform, optional deps skipped), leave
@@ -5636,14 +5726,16 @@ export async function buildServeConfig(
5636
5726
  });
5637
5727
  }
5638
5728
  if (resolveServicesBinary && config.startServices) {
5639
- try {
5640
- const { getServicesPath } = await loadServices();
5641
- serveConfig.servicesBinaryPath = getServicesPath();
5642
- } catch (error) {
5643
- logger().warn({
5644
- msg: "could not resolve the Services binary; a local engine will fail with services.binary_unavailable — set RIVET_RUN_SERVICES=0 to disable Services or RIVET_SERVICES_BINARY to a local rivet-services binary",
5645
- error: stringifyError(error),
5646
- });
5729
+ const servicesBinary = getEnvUniversal("RIVET_SERVICES_BINARY");
5730
+ if (servicesBinary) {
5731
+ serveConfig.servicesBinaryPath = servicesBinary;
5732
+ } else {
5733
+ try {
5734
+ const { getServicesPath } = await loadServices();
5735
+ serveConfig.servicesBinaryPath = getServicesPath();
5736
+ } catch (error) {
5737
+ throw servicesBinaryUnavailable(error);
5738
+ }
5647
5739
  }
5648
5740
  }
5649
5741
  serveConfig.engineHost = config.engineHost;