rivetkit 2.3.23 → 2.3.24-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.
Files changed (109) hide show
  1. package/dist/browser/client.d.ts +2 -0
  2. package/dist/browser/client.js +1 -1
  3. package/dist/browser/client.js.map +1 -1
  4. package/dist/browser/inspector/client.js +1 -1
  5. package/dist/browser/inspector/client.js.map +1 -1
  6. package/dist/tsup/agent-os/index.cjs +1 -1
  7. package/dist/tsup/agent-os/index.cjs.map +1 -1
  8. package/dist/tsup/agent-os/index.d.cts +2 -0
  9. package/dist/tsup/agent-os/index.d.ts +2 -0
  10. package/dist/tsup/agent-os/index.js +1 -1
  11. package/dist/tsup/agent-os/index.js.map +1 -1
  12. package/dist/tsup/{chunk-UV5AN6YV.js → chunk-64CRWLOX.js} +3 -3
  13. package/dist/tsup/{chunk-CIWSGFQG.js → chunk-6HW3TU7Q.js} +2 -2
  14. package/dist/tsup/{chunk-LKNGMRIT.js → chunk-B6WTPGOR.js} +3 -3
  15. package/dist/tsup/{chunk-JLM6TML2.cjs → chunk-CE6NBGOL.cjs} +3 -3
  16. package/dist/tsup/{chunk-JLM6TML2.cjs.map → chunk-CE6NBGOL.cjs.map} +1 -1
  17. package/dist/tsup/{chunk-FVVRY42Y.cjs → chunk-DAC73W53.cjs} +7 -7
  18. package/dist/tsup/{chunk-FVVRY42Y.cjs.map → chunk-DAC73W53.cjs.map} +1 -1
  19. package/dist/tsup/{chunk-VJNX5RCS.cjs → chunk-ERBNWARB.cjs} +4 -4
  20. package/dist/tsup/{chunk-VJNX5RCS.cjs.map → chunk-ERBNWARB.cjs.map} +1 -1
  21. package/dist/tsup/{chunk-674WU4L6.js → chunk-JJCUSUWO.js} +2 -2
  22. package/dist/tsup/chunk-JJCUSUWO.js.map +1 -0
  23. package/dist/tsup/{chunk-7ZDC4XEA.js → chunk-JKDAOE4T.js} +4 -4
  24. package/dist/tsup/{chunk-CRZJ74SA.cjs → chunk-JMO6AVUC.cjs} +3 -3
  25. package/dist/tsup/{chunk-CRZJ74SA.cjs.map → chunk-JMO6AVUC.cjs.map} +1 -1
  26. package/dist/tsup/{chunk-CSOHAAZK.js → chunk-LTGBHN5Z.js} +1 -1
  27. package/dist/tsup/chunk-LTGBHN5Z.js.map +1 -0
  28. package/dist/tsup/{chunk-Z3Z5EOSP.js → chunk-M5J3PXNU.js} +2 -2
  29. package/dist/tsup/{chunk-NWCAIWHL.cjs → chunk-NGJWQMUU.cjs} +1 -1
  30. package/dist/tsup/chunk-NGJWQMUU.cjs.map +1 -0
  31. package/dist/tsup/{chunk-KC3T3MMB.js → chunk-OB5JLDBM.js} +2 -2
  32. package/dist/tsup/{chunk-Z7QK4HTR.cjs → chunk-PFHGTLF2.cjs} +10 -10
  33. package/dist/tsup/{chunk-Z7QK4HTR.cjs.map → chunk-PFHGTLF2.cjs.map} +1 -1
  34. package/dist/tsup/{chunk-HO6UA2RJ.js → chunk-QQ454DVR.js} +2 -2
  35. package/dist/tsup/{chunk-GZKR7ORE.cjs → chunk-R3UI7MO3.cjs} +140 -140
  36. package/dist/tsup/{chunk-GZKR7ORE.cjs.map → chunk-R3UI7MO3.cjs.map} +1 -1
  37. package/dist/tsup/{chunk-XUCVB6BD.cjs → chunk-SWQCSORE.cjs} +3 -3
  38. package/dist/tsup/{chunk-XUCVB6BD.cjs.map → chunk-SWQCSORE.cjs.map} +1 -1
  39. package/dist/tsup/{chunk-3Y7MKDLZ.cjs → chunk-YVVFOKEP.cjs} +2 -2
  40. package/dist/tsup/chunk-YVVFOKEP.cjs.map +1 -0
  41. package/dist/tsup/client/mod.cjs +5 -5
  42. package/dist/tsup/client/mod.d.cts +2 -2
  43. package/dist/tsup/client/mod.d.ts +2 -2
  44. package/dist/tsup/client/mod.js +4 -4
  45. package/dist/tsup/common/log.cjs +2 -2
  46. package/dist/tsup/common/log.js +1 -1
  47. package/dist/tsup/common/websocket.cjs +3 -3
  48. package/dist/tsup/common/websocket.js +2 -2
  49. package/dist/tsup/{config-osdJqQwX.d.cts → config-CuKzfH58.d.cts} +4 -0
  50. package/dist/tsup/{config-Cv6pjCt-.d.ts → config-RV6gIa2L.d.ts} +4 -0
  51. package/dist/tsup/{context-DpjrM7n7.d.cts → context-DPCxyjax.d.cts} +1 -1
  52. package/dist/tsup/{context-CoVbKBuk.d.ts → context-DoMKZBQA.d.ts} +1 -1
  53. package/dist/tsup/db/drizzle.cjs +2 -2
  54. package/dist/tsup/db/drizzle.js +1 -1
  55. package/dist/tsup/db/mod.cjs +3 -3
  56. package/dist/tsup/db/mod.js +2 -2
  57. package/dist/tsup/dynamic/mod.cjs +2 -2
  58. package/dist/tsup/dynamic/mod.d.cts +1 -1
  59. package/dist/tsup/dynamic/mod.d.ts +1 -1
  60. package/dist/tsup/dynamic/mod.js +1 -1
  61. package/dist/tsup/inspector/mod.cjs +5 -5
  62. package/dist/tsup/inspector/mod.js +4 -4
  63. package/dist/tsup/inspector/workflow.cjs +4 -4
  64. package/dist/tsup/inspector/workflow.js +3 -3
  65. package/dist/tsup/inspector-tab/mod.cjs +2 -2
  66. package/dist/tsup/inspector-tab/mod.d.cts +2 -2
  67. package/dist/tsup/inspector-tab/mod.d.ts +2 -2
  68. package/dist/tsup/inspector-tab/mod.js +1 -1
  69. package/dist/tsup/mod.cjs +416 -188
  70. package/dist/tsup/mod.cjs.map +1 -1
  71. package/dist/tsup/mod.d.cts +3 -3
  72. package/dist/tsup/mod.d.ts +3 -3
  73. package/dist/tsup/mod.js +318 -90
  74. package/dist/tsup/mod.js.map +1 -1
  75. package/dist/tsup/test/mod.cjs +6 -6
  76. package/dist/tsup/test/mod.d.cts +1 -1
  77. package/dist/tsup/test/mod.d.ts +1 -1
  78. package/dist/tsup/test/mod.js +4 -4
  79. package/dist/tsup/utils.cjs +2 -2
  80. package/dist/tsup/utils.js +1 -1
  81. package/dist/tsup/workflow/mod.cjs +69 -21
  82. package/dist/tsup/workflow/mod.cjs.map +1 -1
  83. package/dist/tsup/workflow/mod.d.cts +3 -3
  84. package/dist/tsup/workflow/mod.d.ts +3 -3
  85. package/dist/tsup/workflow/mod.js +64 -16
  86. package/dist/tsup/workflow/mod.js.map +1 -1
  87. package/package.json +9 -9
  88. package/src/actor/config.ts +2 -0
  89. package/src/registry/lost-callbacks.test.ts +64 -0
  90. package/src/registry/napi-runtime.ts +8 -0
  91. package/src/registry/native.ts +332 -65
  92. package/src/registry/run-handler-coordinator.test.ts +102 -0
  93. package/src/registry/run-handler-coordinator.ts +84 -17
  94. package/src/registry/runtime.ts +2 -0
  95. package/src/registry/wasm-runtime.ts +8 -0
  96. package/src/workflow/context.ts +20 -0
  97. package/src/workflow/driver.ts +25 -1
  98. package/src/workflow/mod.ts +46 -12
  99. package/dist/tsup/chunk-3Y7MKDLZ.cjs.map +0 -1
  100. package/dist/tsup/chunk-674WU4L6.js.map +0 -1
  101. package/dist/tsup/chunk-CSOHAAZK.js.map +0 -1
  102. package/dist/tsup/chunk-NWCAIWHL.cjs.map +0 -1
  103. /package/dist/tsup/{chunk-UV5AN6YV.js.map → chunk-64CRWLOX.js.map} +0 -0
  104. /package/dist/tsup/{chunk-CIWSGFQG.js.map → chunk-6HW3TU7Q.js.map} +0 -0
  105. /package/dist/tsup/{chunk-LKNGMRIT.js.map → chunk-B6WTPGOR.js.map} +0 -0
  106. /package/dist/tsup/{chunk-7ZDC4XEA.js.map → chunk-JKDAOE4T.js.map} +0 -0
  107. /package/dist/tsup/{chunk-Z3Z5EOSP.js.map → chunk-M5J3PXNU.js.map} +0 -0
  108. /package/dist/tsup/{chunk-KC3T3MMB.js.map → chunk-OB5JLDBM.js.map} +0 -0
  109. /package/dist/tsup/{chunk-HO6UA2RJ.js.map → chunk-QQ454DVR.js.map} +0 -0
@@ -600,6 +600,14 @@ export class NapiCoreRuntime implements CoreRuntime {
600
600
  return asNativeActorContext(ctx).actorId();
601
601
  }
602
602
 
603
+ actorGeneration(ctx: ActorContextHandle): number | undefined {
604
+ return asNativeActorContext(ctx).generation() ?? undefined;
605
+ }
606
+
607
+ actorIsLost(ctx: ActorContextHandle): boolean {
608
+ return asNativeActorContext(ctx).isLost();
609
+ }
610
+
603
611
  runWithActorInvocationContext<T>(ctx: ActorContextHandle, run: () => T): T {
604
612
  return this.#runAs(asNativeActorContext(ctx), run);
605
613
  }
@@ -541,8 +541,11 @@ async function cleanupNativeSleepRuntimeState(
541
541
  void runtime
542
542
  .actorWaitForTrackedShutdownWorkUnbounded(ctx)
543
543
  .then(async () => {
544
- await afterTrackedWorkDrained?.();
545
- clearNativeRuntimeState(runtime, ctx);
544
+ try {
545
+ await afterTrackedWorkDrained?.();
546
+ } finally {
547
+ clearNativeRuntimeState(runtime, ctx);
548
+ }
546
549
  })
547
550
  .catch((error) => {
548
551
  logger().warn({
@@ -553,10 +556,31 @@ async function cleanupNativeSleepRuntimeState(
553
556
  return;
554
557
  }
555
558
 
556
- await afterTrackedWorkDrained?.();
559
+ try {
560
+ await afterTrackedWorkDrained?.();
561
+ } finally {
562
+ try {
563
+ await closeNativeDatabaseClient(runtime, ctx);
564
+ await closeNativeSqlDatabase(runtime, ctx);
565
+ } finally {
566
+ clearNativeRuntimeState(runtime, ctx);
567
+ }
568
+ }
569
+ }
570
+
571
+ async function cleanupNativeLostRuntimeState(
572
+ runtime: CoreRuntime,
573
+ ctx: ActorContextHandle,
574
+ ): Promise<void> {
575
+ // Core has already abandoned a lost generation's user work, so this does not wait for it.
576
+ // A custom database client's `close` is user code, so a lost generation drops the client
577
+ // without calling it. The native database underneath is closed by the runtime.
557
578
  await closeNativeDatabaseClient(runtime, ctx);
558
- await closeNativeSqlDatabase(runtime, ctx);
559
- clearNativeRuntimeState(runtime, ctx);
579
+ try {
580
+ await closeNativeSqlDatabase(runtime, ctx);
581
+ } finally {
582
+ clearNativeRuntimeState(runtime, ctx);
583
+ }
560
584
  }
561
585
 
562
586
  function closeNativeSqlDatabase(
@@ -585,6 +609,11 @@ async function closeNativeDatabaseClient(
585
609
 
586
610
  runtimeState.databaseClient = undefined;
587
611
 
612
+ // `close` is user code from a database provider. Cleanup paths await before reaching this,
613
+ // so check here: a lost generation drops the client without calling it.
614
+ if (callNativeSync(() => runtime.actorIsLost(ctx))) {
615
+ return;
616
+ }
588
617
  if (
589
618
  entry.client &&
590
619
  typeof entry.client === "object" &&
@@ -1301,6 +1330,76 @@ function wrapNativeCallback<Args extends Array<unknown>, Result>(
1301
1330
  };
1302
1331
  }
1303
1332
 
1333
+ /**
1334
+ * Callbacks that still run for a lost generation. They release the generation's runtime
1335
+ * resources and skip the user hook themselves.
1336
+ */
1337
+ const LOST_GENERATION_CLEANUP_CALLBACKS = new Set(["onSleep", "onDestroy"]);
1338
+
1339
+ /**
1340
+ * Refuses to start a user callback for a generation that was declared lost. Call it right
1341
+ * before the callback, after any other user code such as schema validation.
1342
+ */
1343
+ function throwIfActorLost(runtime: CoreRuntime, ctx: ActorContextHandle): void {
1344
+ if (callNativeSync(() => runtime.actorIsLost(ctx))) {
1345
+ throw actorGenerationLostError();
1346
+ }
1347
+ }
1348
+
1349
+ function actorGenerationLostError(): RivetError {
1350
+ return new RivetError(
1351
+ "actor",
1352
+ "stopping",
1353
+ "Actor generation was declared lost; it starts no new user callbacks.",
1354
+ );
1355
+ }
1356
+
1357
+ /**
1358
+ * Refuses every runtime callback for a generation that has been declared lost. Runtime
1359
+ * callbacks can be queued or spawned before the loss and only start afterward, so the check
1360
+ * runs when JS actually enters the callback.
1361
+ */
1362
+ export function refuseCallbacksAfterLost<T extends Record<string, unknown>>(
1363
+ runtime: CoreRuntime,
1364
+ callbacks: T,
1365
+ ): T {
1366
+ const guard = (callback: (...args: unknown[]) => unknown) => {
1367
+ return async (...args: unknown[]) => {
1368
+ const ctx = (args[1] as { ctx?: ActorContextHandle } | undefined)
1369
+ ?.ctx;
1370
+ if (
1371
+ ctx !== undefined &&
1372
+ callNativeSync(() => runtime.actorIsLost(ctx))
1373
+ ) {
1374
+ throw encodeNativeCallbackError(actorGenerationLostError());
1375
+ }
1376
+ return await callback(...args);
1377
+ };
1378
+ };
1379
+ const guarded: Record<string, unknown> = {};
1380
+ for (const [name, value] of Object.entries(callbacks)) {
1381
+ if (LOST_GENERATION_CLEANUP_CALLBACKS.has(name)) {
1382
+ guarded[name] = value;
1383
+ } else if (typeof value === "function") {
1384
+ guarded[name] = guard(value as (...args: unknown[]) => unknown);
1385
+ } else if (
1386
+ name === "actions" &&
1387
+ value !== null &&
1388
+ typeof value === "object"
1389
+ ) {
1390
+ guarded[name] = Object.fromEntries(
1391
+ Object.entries(value).map(([actionName, handler]) => [
1392
+ actionName,
1393
+ guard(handler as (...args: unknown[]) => unknown),
1394
+ ]),
1395
+ );
1396
+ } else {
1397
+ guarded[name] = value;
1398
+ }
1399
+ }
1400
+ return guarded as T;
1401
+ }
1402
+
1304
1403
  function decodeArgs(value?: RuntimeBytes | null): unknown[] {
1305
1404
  const decoded = decodeValue<unknown>(value);
1306
1405
  return normalizeArgs(decoded);
@@ -1880,9 +1979,26 @@ class NativeQueueAdapter {
1880
1979
  this.#schemas = schemas;
1881
1980
  }
1882
1981
 
1982
+ // Queue results arrive after awaits, and their schema validation is user code, so each
1983
+ // validation checks that the generation was not lost first.
1984
+ #validateBody(name: string, body: unknown): unknown {
1985
+ throwIfActorLost(this.#runtime, this.#ctx);
1986
+ return validateQueueBody(this.#schemas, name, body);
1987
+ }
1988
+
1989
+ #validateComplete(name: string, response: unknown): unknown {
1990
+ throwIfActorLost(this.#runtime, this.#ctx);
1991
+ return validateQueueComplete(this.#schemas, name, response);
1992
+ }
1993
+
1994
+ #wrap(message: Parameters<typeof wrapQueueMessage>[0]) {
1995
+ throwIfActorLost(this.#runtime, this.#ctx);
1996
+ return wrapQueueMessage(message, this.#schemas);
1997
+ }
1998
+
1883
1999
  async send(name: string, body: unknown) {
1884
- const validatedBody = validateQueueBody(this.#schemas, name, body);
1885
- return wrapQueueMessage(
2000
+ const validatedBody = this.#validateBody(name, body);
2001
+ return this.#wrap(
1886
2002
  await callNative(() =>
1887
2003
  this.#runtime.actorQueueSend(
1888
2004
  this.#ctx,
@@ -1890,7 +2006,6 @@ class NativeQueueAdapter {
1890
2006
  encodeValue(validatedBody),
1891
2007
  ),
1892
2008
  ),
1893
- this.#schemas,
1894
2009
  );
1895
2010
  }
1896
2011
 
@@ -1948,9 +2063,7 @@ class NativeQueueAdapter {
1948
2063
  token,
1949
2064
  ),
1950
2065
  );
1951
- const wrapped = messages.map((message) =>
1952
- wrapQueueMessage(message, this.#schemas),
1953
- );
2066
+ const wrapped = messages.map((message) => this.#wrap(message));
1954
2067
  return completable
1955
2068
  ? wrapped.map((message) =>
1956
2069
  this.#makeCompletableMessage(message),
@@ -1975,7 +2088,7 @@ class NativeQueueAdapter {
1975
2088
  );
1976
2089
 
1977
2090
  try {
1978
- return wrapQueueMessage(
2091
+ return this.#wrap(
1979
2092
  await callNative(() =>
1980
2093
  this.#runtime.actorQueueWaitForNames(
1981
2094
  this.#ctx,
@@ -1987,7 +2100,6 @@ class NativeQueueAdapter {
1987
2100
  token,
1988
2101
  ),
1989
2102
  ),
1990
- this.#schemas,
1991
2103
  );
1992
2104
  } finally {
1993
2105
  cleanup?.();
@@ -2032,8 +2144,7 @@ class NativeQueueAdapter {
2032
2144
  message: { id: number | bigint; name: string },
2033
2145
  response?: unknown,
2034
2146
  ): Promise<void> {
2035
- const validatedResponse = validateQueueComplete(
2036
- this.#schemas,
2147
+ const validatedResponse = this.#validateComplete(
2037
2148
  message.name,
2038
2149
  response,
2039
2150
  );
@@ -2056,7 +2167,7 @@ class NativeQueueAdapter {
2056
2167
  signal?: AbortSignal;
2057
2168
  },
2058
2169
  ) {
2059
- const validatedBody = validateQueueBody(this.#schemas, name, body);
2170
+ const validatedBody = this.#validateBody(name, body);
2060
2171
  const { token, cleanup } = await createCancellationTokenHandle(
2061
2172
  this.#runtime,
2062
2173
  options?.signal,
@@ -2076,11 +2187,7 @@ class NativeQueueAdapter {
2076
2187
  );
2077
2188
  return response === undefined || response === null
2078
2189
  ? undefined
2079
- : validateQueueComplete(
2080
- this.#schemas,
2081
- name,
2082
- decodeValue(response),
2083
- );
2190
+ : this.#validateComplete(name, decodeValue(response));
2084
2191
  } finally {
2085
2192
  cleanup?.();
2086
2193
  }
@@ -2583,6 +2690,12 @@ class TrackedWebSocketHandleAdapter implements UniversalWebSocket {
2583
2690
  handler: TrackedWebSocketListener,
2584
2691
  event: any,
2585
2692
  ): void {
2693
+ // Messages and opens start new user work, so a lost generation drops them. Each listener
2694
+ // is checked because an earlier one can run while the generation is lost. Close and
2695
+ // error still reach listeners, like the abort signal, so user code can wind down.
2696
+ if ((type === "message" || type === "open") && this.#ctx.isLost) {
2697
+ return;
2698
+ }
2586
2699
  try {
2587
2700
  const result = handler(event);
2588
2701
  if (!this.#isPromiseLike(result)) {
@@ -2963,6 +3076,15 @@ export class ActorContextHandleAdapter {
2963
3076
  return callNativeSync(() => this.#runtime.actorId(this.#ctx));
2964
3077
  }
2965
3078
 
3079
+ get actorGeneration(): number | undefined {
3080
+ return callNativeSync(() => this.#runtime.actorGeneration(this.#ctx));
3081
+ }
3082
+
3083
+ /** Whether this generation was declared lost. A lost generation starts no new user code. */
3084
+ get isLost(): boolean {
3085
+ return callNativeSync(() => this.#runtime.actorIsLost(this.#ctx));
3086
+ }
3087
+
2966
3088
  get name(): string {
2967
3089
  return callNativeSync(() => this.#runtime.actorName(this.#ctx));
2968
3090
  }
@@ -3096,6 +3218,10 @@ export class ActorContextHandleAdapter {
3096
3218
  }
3097
3219
 
3098
3220
  const actorId = this.actorId;
3221
+ // `createClient` is user code from a database provider.
3222
+ if (this.isLost) {
3223
+ throw actorGenerationLostError();
3224
+ }
3099
3225
  const createdClient = await this.#databaseProvider.createClient({
3100
3226
  actorId,
3101
3227
  kv: {
@@ -3273,9 +3399,12 @@ export class ActorContextHandleAdapter {
3273
3399
  return;
3274
3400
  }
3275
3401
 
3276
- await this.#databaseProvider.onMigrate(
3277
- (await this.ensureDatabaseClient()) as never,
3278
- );
3402
+ const client = await this.ensureDatabaseClient();
3403
+ // Creating the client awaits, and the generation can be lost meanwhile.
3404
+ if (this.isLost) {
3405
+ throw actorGenerationLostError();
3406
+ }
3407
+ await this.#databaseProvider.onMigrate(client as never);
3279
3408
  }
3280
3409
 
3281
3410
  async closeDatabase(): Promise<void> {
@@ -3610,7 +3739,8 @@ export class ActorContextHandleAdapter {
3610
3739
  this.#runtime.actorRequestSave(this.#ctx, { immediate: false }),
3611
3740
  );
3612
3741
 
3613
- if (!this.#onStateChange) {
3742
+ // State changes are flushed asynchronously and can land after the generation was lost.
3743
+ if (!this.#onStateChange || this.isLost) {
3614
3744
  return;
3615
3745
  }
3616
3746
 
@@ -3659,6 +3789,13 @@ class NativeWorkflowRuntimeAdapter {
3659
3789
  #completions = new Map<string, (response?: unknown) => Promise<void>>();
3660
3790
 
3661
3791
  readonly id: string;
3792
+ /** Actor generation, used to keep workflow registrations of different generations apart. */
3793
+ readonly generation: number | undefined;
3794
+
3795
+ /** Whether this generation was declared lost. Workflow code checks it before each user callback. */
3796
+ get isLost(): boolean {
3797
+ return this.#ctx.isLost;
3798
+ }
3662
3799
  readonly driver: {
3663
3800
  kvBatchGet: (
3664
3801
  actorId: string,
@@ -3730,6 +3867,7 @@ class NativeWorkflowRuntimeAdapter {
3730
3867
  this.#ctx = ctx;
3731
3868
  this.#onMessagesReceived = onMessagesReceived;
3732
3869
  this.id = ctx.actorId;
3870
+ this.generation = ctx.actorGeneration;
3733
3871
  this.driver = {
3734
3872
  kvBatchGet: async (actorId, keys) => {
3735
3873
  this.#assertActorId(actorId);
@@ -4101,14 +4239,29 @@ export function buildNativeFactory(
4101
4239
  ? new RunHandlerCoordinator(config.run)
4102
4240
  : undefined;
4103
4241
  const getNativeWorkflowInspector = (ctx: ActorContextHandle) => {
4242
+ // Inspector workflow calls run user code (history, state, replay). A lost generation
4243
+ // exposes no workflow inspector.
4244
+ if (callNativeSync(() => runtime.actorIsLost(ctx))) {
4245
+ return undefined;
4246
+ }
4104
4247
  const actorId = callNativeSync(() => runtime.actorId(ctx));
4105
4248
  const restart = () =>
4106
4249
  callNativeSync(() => runtime.actorRestartRunHandler(ctx));
4107
- return (runHandlerCoordinator?.getInspector(actorId, restart)
4108
- ?.workflow ??
4109
- getRunInspectorConfig(config.run, actorId)?.workflow) as
4250
+ const actorGeneration = callNativeSync(() =>
4251
+ runtime.actorGeneration(ctx),
4252
+ );
4253
+ const inspector = (runHandlerCoordinator?.getInspector(
4254
+ actorId,
4255
+ restart,
4256
+ actorGeneration,
4257
+ )?.workflow ?? getRunInspectorConfig(config.run, actorId)?.workflow) as
4110
4258
  | NativeWorkflowInspectorConfig
4111
4259
  | undefined;
4260
+ // Building the inspector can run a user factory, and the generation can be lost meanwhile.
4261
+ if (callNativeSync(() => runtime.actorIsLost(ctx))) {
4262
+ return undefined;
4263
+ }
4264
+ return inspector;
4112
4265
  };
4113
4266
  const onStateChange =
4114
4267
  typeof config.onStateChange === "function"
@@ -4228,6 +4381,11 @@ export function buildNativeFactory(
4228
4381
  }
4229
4382
  }
4230
4383
 
4384
+ // Reading connection params runs the user schema, and authentication awaited before this.
4385
+ const inspectorConnParams = (conn: { params: unknown }) => {
4386
+ throwIfActorLost(runtime, ctx);
4387
+ return conn.params;
4388
+ };
4231
4389
  const workflowHistory = () =>
4232
4390
  serializeWorkflowHistoryForJson(
4233
4391
  getNativeWorkflowInspector(ctx)?.getHistory() ?? null,
@@ -4266,7 +4424,7 @@ export function buildNativeFactory(
4266
4424
  id: conn.id,
4267
4425
  details: {
4268
4426
  type: null,
4269
- params: conn.params,
4427
+ params: inspectorConnParams(conn),
4270
4428
  stateEnabled: true,
4271
4429
  state: conn.state,
4272
4430
  subscriptions: 0,
@@ -4526,7 +4684,7 @@ export function buildNativeFactory(
4526
4684
  id: conn.id,
4527
4685
  details: {
4528
4686
  type: null,
4529
- params: conn.params,
4687
+ params: inspectorConnParams(conn),
4530
4688
  stateEnabled: true,
4531
4689
  state: conn.state,
4532
4690
  subscriptions: 0,
@@ -4588,15 +4746,22 @@ export function buildNativeFactory(
4588
4746
  body.properties !== undefined
4589
4747
  ? [body.properties]
4590
4748
  : normalizeArgs(body.args);
4749
+ // Authentication and body parsing may finish after the generation was lost.
4750
+ if (callNativeSync(() => runtime.actorIsLost(ctx))) {
4751
+ return jsonResponse(
4752
+ { error: "actor generation was declared lost" },
4753
+ { status: 503 },
4754
+ );
4755
+ }
4591
4756
  try {
4592
- const output = await action(
4593
- actorCtx,
4594
- ...validateActionArgs(
4595
- schemaConfig.actionInputSchemas,
4596
- actionName,
4597
- args,
4598
- ),
4757
+ const validatedArgs = validateActionArgs(
4758
+ schemaConfig.actionInputSchemas,
4759
+ actionName,
4760
+ args,
4599
4761
  );
4762
+ // Schema validation is user code and can run while the generation is lost.
4763
+ throwIfActorLost(runtime, ctx);
4764
+ const output = await action(actorCtx, ...validatedArgs);
4600
4765
  return jsonResponse({ output });
4601
4766
  } catch (error) {
4602
4767
  logger().error({
@@ -4738,7 +4903,14 @@ export function buildNativeFactory(
4738
4903
  await actorCtx.closeDatabase();
4739
4904
  }
4740
4905
  await actorCtx.runDatabaseMigrations();
4741
- if (typeof config.onMigrate === "function") {
4906
+ // Migrations may finish after the generation was lost, and a lost
4907
+ // generation starts no new user hooks.
4908
+ if (
4909
+ typeof config.onMigrate === "function" &&
4910
+ !callNativeSync(() =>
4911
+ runtime.actorIsLost(ctx),
4912
+ )
4913
+ ) {
4742
4914
  await config.onMigrate(actorCtx, isNew);
4743
4915
  }
4744
4916
  } catch (error) {
@@ -4794,10 +4966,38 @@ export function buildNativeFactory(
4794
4966
  const { ctx } = unwrapTsfnPayload(error, payload);
4795
4967
  const actorCtx = makeActorCtx(ctx);
4796
4968
  const actorId = callNativeSync(() => runtime.actorId(ctx));
4969
+ const actorGeneration = callNativeSync(() =>
4970
+ runtime.actorGeneration(ctx),
4971
+ );
4972
+ // The engine can declare this generation lost before or during `onSleep`.
4973
+ // A lost generation releases its own runtime resources only: no user hook,
4974
+ // no state save, and nothing keyed by actor id alone.
4975
+ const isLost = () =>
4976
+ callNativeSync(() => runtime.actorIsLost(ctx));
4977
+ if (isLost()) {
4978
+ try {
4979
+ await cleanupNativeLostRuntimeState(runtime, ctx);
4980
+ } finally {
4981
+ runHandlerCoordinator?.destroy(
4982
+ actorId,
4983
+ actorGeneration,
4984
+ {
4985
+ disposeInspector: false,
4986
+ },
4987
+ );
4988
+ await actorCtx.dispose();
4989
+ }
4990
+ return;
4991
+ }
4797
4992
  // TODO: Move this save hook into cleanupNativeSleepRuntimeState
4798
4993
  // so immediate and deferred sleep cleanup share one save-state
4799
4994
  // path instead of passing a callback through cleanup.
4800
4995
  const saveActorState = async () => {
4996
+ // Cleanup waits on tracked work first, and the generation can be lost
4997
+ // during that wait.
4998
+ if (isLost()) {
4999
+ return;
5000
+ }
4801
5001
  if (runtime.kind === "wasm") {
4802
5002
  // Wasm cannot use the native context save helper here because
4803
5003
  // the runtime owns the serialized state handoff.
@@ -4818,14 +5018,26 @@ export function buildNativeFactory(
4818
5018
  await saveActorState();
4819
5019
  } finally {
4820
5020
  try {
4821
- await cleanupNativeSleepRuntimeState(
4822
- runtime,
4823
- ctx,
4824
- saveActorState,
4825
- );
5021
+ if (isLost()) {
5022
+ await cleanupNativeLostRuntimeState(runtime, ctx);
5023
+ } else {
5024
+ await cleanupNativeSleepRuntimeState(
5025
+ runtime,
5026
+ ctx,
5027
+ saveActorState,
5028
+ );
5029
+ }
4826
5030
  } finally {
4827
- runHandlerCoordinator?.destroy(actorId);
4828
- disposeRunInspector(config.run, actorId);
5031
+ runHandlerCoordinator?.destroy(
5032
+ actorId,
5033
+ actorGeneration,
5034
+ {
5035
+ disposeInspector: !isLost(),
5036
+ },
5037
+ );
5038
+ if (!isLost()) {
5039
+ disposeRunInspector(config.run, actorId);
5040
+ }
4829
5041
  await actorCtx.dispose();
4830
5042
  }
4831
5043
  }
@@ -4836,12 +5048,22 @@ export function buildNativeFactory(
4836
5048
  const { ctx } = unwrapTsfnPayload(error, payload);
4837
5049
  const actorCtx = makeActorCtx(ctx);
4838
5050
  const actorId = callNativeSync(() => runtime.actorId(ctx));
5051
+ // A lost generation still releases its runtime state but starts no user hook. Loss
5052
+ // is rechecked before each user callback because it can happen while one runs.
5053
+ const isLost = () =>
5054
+ callNativeSync(() => runtime.actorIsLost(ctx));
4839
5055
  // Close run control before user cleanup so replay cannot race actor
4840
5056
  // destruction. Recreating this actor id receives a fresh controller.
4841
- runHandlerCoordinator?.destroy(actorId);
4842
- disposeRunInspector(config.run, actorId);
5057
+ runHandlerCoordinator?.destroy(
5058
+ actorId,
5059
+ callNativeSync(() => runtime.actorGeneration(ctx)),
5060
+ { disposeInspector: !isLost() },
5061
+ );
5062
+ if (!isLost()) {
5063
+ disposeRunInspector(config.run, actorId);
5064
+ }
4843
5065
  try {
4844
- if (typeof config.onDestroy === "function") {
5066
+ if (typeof config.onDestroy === "function" && !isLost()) {
4845
5067
  await config.onDestroy(actorCtx);
4846
5068
  }
4847
5069
  } finally {
@@ -4879,12 +5101,16 @@ export function buildNativeFactory(
4879
5101
  : undefined,
4880
5102
  );
4881
5103
  try {
5104
+ const validatedParams = validateConnParams(
5105
+ schemaConfig.connParamsSchema,
5106
+ decodeValue(params),
5107
+ );
5108
+ // Schema validation is user code and can run while the
5109
+ // generation is lost.
5110
+ throwIfActorLost(runtime, ctx);
4882
5111
  await config.onBeforeConnect(
4883
5112
  actorCtx,
4884
- validateConnParams(
4885
- schemaConfig.connParamsSchema,
4886
- decodeValue(params),
4887
- ),
5113
+ validatedParams,
4888
5114
  );
4889
5115
  } finally {
4890
5116
  await actorCtx.dispose();
@@ -4932,15 +5158,25 @@ export function buildNativeFactory(
4932
5158
  () => actorCtx.assertCanMutateState(),
4933
5159
  );
4934
5160
  try {
4935
- const nextConnState = hasStaticConnState
4936
- ? structuredClone(config.connState)
4937
- : await config.createConnState(
5161
+ let nextConnState: unknown;
5162
+ if (hasStaticConnState) {
5163
+ nextConnState = structuredClone(
5164
+ config.connState,
5165
+ );
5166
+ } else {
5167
+ const validatedParams = validateConnParams(
5168
+ schemaConfig.connParamsSchema,
5169
+ decodeValue(params),
5170
+ );
5171
+ // Schema validation is user code and can run while the
5172
+ // generation is lost.
5173
+ throwIfActorLost(runtime, ctx);
5174
+ nextConnState =
5175
+ await config.createConnState(
4938
5176
  actorCtx,
4939
- validateConnParams(
4940
- schemaConfig.connParamsSchema,
4941
- decodeValue(params),
4942
- ),
5177
+ validatedParams,
4943
5178
  );
5179
+ }
4944
5180
  connAdapter.initializeState(nextConnState);
4945
5181
  return encodeValue(nextConnState);
4946
5182
  } finally {
@@ -5099,6 +5335,13 @@ export function buildNativeFactory(
5099
5335
  ) => {
5100
5336
  const { ctx, name, args, output } =
5101
5337
  unwrapTsfnPayload(error, payload);
5338
+ // The action may finish after its generation was lost, and a lost
5339
+ // generation starts no new user callbacks.
5340
+ if (
5341
+ callNativeSync(() => runtime.actorIsLost(ctx))
5342
+ ) {
5343
+ return output;
5344
+ }
5102
5345
  const actorCtx = makeActorCtx(ctx);
5103
5346
  try {
5104
5347
  return encodeValue(
@@ -5196,6 +5439,9 @@ export function buildNativeFactory(
5196
5439
  }
5197
5440
  };
5198
5441
  try {
5442
+ // Inspector routing awaits, and the generation can be lost
5443
+ // meanwhile. Schema validation is user code.
5444
+ throwIfActorLost(runtime, ctx);
5199
5445
  const connParams = validateConnParams(
5200
5446
  schemaConfig.connParamsSchema,
5201
5447
  rawConnParams
@@ -5234,6 +5480,14 @@ export function buildNativeFactory(
5234
5480
  abortRequest,
5235
5481
  );
5236
5482
  }
5483
+ // Connection setup awaits, and the generation can be lost meanwhile.
5484
+ if (
5485
+ callNativeSync(() =>
5486
+ runtime.actorIsLost(ctx),
5487
+ )
5488
+ ) {
5489
+ throw actorGenerationLostError();
5490
+ }
5237
5491
  const response = await config.onRequest(
5238
5492
  requestCtx,
5239
5493
  handlerRequest,
@@ -5334,6 +5588,11 @@ export function buildNativeFactory(
5334
5588
  const { ctx } = unwrapTsfnPayload(error, payload);
5335
5589
  const actorId = callNativeSync(() => runtime.actorId(ctx));
5336
5590
  const executeRun = async () => {
5591
+ // The coordinator can hold `run` behind other run work until after the
5592
+ // generation was lost.
5593
+ if (callNativeSync(() => runtime.actorIsLost(ctx))) {
5594
+ return;
5595
+ }
5337
5596
  const actorCtx = makeActorCtx(ctx);
5338
5597
  try {
5339
5598
  await run(actorCtx);
@@ -5349,6 +5608,7 @@ export function buildNativeFactory(
5349
5608
  runtime.actorRestartRunHandler(ctx),
5350
5609
  ),
5351
5610
  executeRun,
5611
+ callNativeSync(() => runtime.actorGeneration(ctx)),
5352
5612
  );
5353
5613
  // Only a start suppressed behind a failed replay must preserve a
5354
5614
  // consumed durable wake. Closed actors must remain closed.
@@ -5425,14 +5685,18 @@ export function buildNativeFactory(
5425
5685
  : makeActorCtx(ctx, undefined, cancelToken);
5426
5686
  const runAction = async () => {
5427
5687
  try {
5688
+ const validatedArgs = validateActionArgs(
5689
+ schemaConfig.actionInputSchemas,
5690
+ name,
5691
+ decodeArgs(args),
5692
+ );
5693
+ // Schema validation is user code and can run while the
5694
+ // generation is lost.
5695
+ throwIfActorLost(runtime, ctx);
5428
5696
  return encodeValue(
5429
5697
  await handler(
5430
5698
  actorCtx,
5431
- ...validateActionArgs(
5432
- schemaConfig.actionInputSchemas,
5433
- name,
5434
- decodeArgs(args),
5435
- ),
5699
+ ...validatedArgs,
5436
5700
  ...(scheduledFire
5437
5701
  ? [scheduledFire]
5438
5702
  : []),
@@ -5511,6 +5775,9 @@ export function buildNativeFactory(
5511
5775
  if (canPublish && !(await canPublish(actorCtx))) {
5512
5776
  throw forbiddenError();
5513
5777
  }
5778
+ // `canPublish` can finish after the generation was lost, and
5779
+ // sending validates the body with user schema code.
5780
+ throwIfActorLost(runtime, ctx);
5514
5781
 
5515
5782
  const decodedBody = decodeValue(body);
5516
5783
  if (wait) {
@@ -5584,7 +5851,7 @@ export function buildNativeFactory(
5584
5851
  };
5585
5852
 
5586
5853
  return runtime.createActorFactory(
5587
- callbacks,
5854
+ refuseCallbacksAfterLost(runtime, callbacks),
5588
5855
  buildActorConfig(definition, registryConfig, runtime.kind),
5589
5856
  );
5590
5857
  }