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
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, test, vi } from "vitest";
2
2
  import { defineRunHandler, type RunControl } from "@/actor/config";
3
+ import { workflow } from "@/workflow/mod";
3
4
  import { RunHandlerCoordinator } from "./run-handler-coordinator";
4
5
 
5
6
  class Deferred<T = void> {
@@ -51,6 +52,107 @@ function createCoordinator() {
51
52
  }
52
53
 
53
54
  describe("RunHandlerCoordinator", () => {
55
+ test("workflow inspector registrations stay per generation", () => {
56
+ const coordinator = new RunHandlerCoordinator(workflow(async () => {}));
57
+ const restart = vi.fn();
58
+
59
+ const oldInspector = coordinator.getInspector(
60
+ "actor-1",
61
+ restart,
62
+ 1,
63
+ )?.workflow;
64
+ const newInspector = coordinator.getInspector(
65
+ "actor-1",
66
+ restart,
67
+ 2,
68
+ )?.workflow;
69
+ expect(oldInspector).toBeDefined();
70
+ expect(newInspector).toBeDefined();
71
+ expect(newInspector).not.toBe(oldInspector);
72
+
73
+ // A superseded generation cannot re-initialize or rebind the current generation's
74
+ // workflow registration.
75
+ expect(coordinator.getInspector("actor-1", restart, 1)).toBeUndefined();
76
+ expect(coordinator.getInspector("actor-1", restart, 2)?.workflow).toBe(
77
+ newInspector,
78
+ );
79
+ });
80
+
81
+ test("a superseded generation's run is rejected before it touches the inspector", async () => {
82
+ const coordinator = new RunHandlerCoordinator(workflow(async () => {}));
83
+ const restart = vi.fn();
84
+ const newInspector = coordinator.getInspector(
85
+ "actor-1",
86
+ restart,
87
+ 2,
88
+ )?.workflow;
89
+
90
+ await expect(
91
+ coordinator.run("actor-1", restart, async () => {}, 1),
92
+ ).resolves.toBe("closed");
93
+ expect(coordinator.getInspector("actor-1", restart, 2)?.workflow).toBe(
94
+ newInspector,
95
+ );
96
+ });
97
+
98
+ test("a newer generation does not wait on an older generation's stuck run", async () => {
99
+ const coordinator = new RunHandlerCoordinator(
100
+ defineRunHandler(async () => {}, {
101
+ inspectorKind: "workflow",
102
+ createInspector: () => ({
103
+ inspector: {
104
+ workflow: {
105
+ getHistory: () => null,
106
+ getState: async () => null,
107
+ onHistoryUpdated: () => () => {},
108
+ replayFromStep: async () => null,
109
+ },
110
+ },
111
+ }),
112
+ }),
113
+ );
114
+ const oldRun = new Deferred();
115
+ const oldOutcome = coordinator.run(
116
+ "actor-1",
117
+ () => {},
118
+ () => oldRun.promise,
119
+ 1,
120
+ );
121
+
122
+ // The lost generation's JS callback cannot be cancelled, so it stays active.
123
+ await expect(
124
+ coordinator.run(
125
+ "actor-1",
126
+ () => {},
127
+ async () => {},
128
+ 2,
129
+ ),
130
+ ).resolves.toBe("ran");
131
+
132
+ // The superseded generation cannot start new runs, and its late destroy leaves the
133
+ // newer generation's state in place.
134
+ await expect(
135
+ coordinator.run(
136
+ "actor-1",
137
+ () => {},
138
+ async () => {},
139
+ 1,
140
+ ),
141
+ ).resolves.toBe("closed");
142
+ coordinator.destroy("actor-1", 1);
143
+ await expect(
144
+ coordinator.run(
145
+ "actor-1",
146
+ () => {},
147
+ async () => {},
148
+ 2,
149
+ ),
150
+ ).resolves.toBe("ran");
151
+
152
+ oldRun.resolve();
153
+ await expect(oldOutcome).resolves.toBe("ran");
154
+ });
155
+
54
156
  test("fails explicitly when a JavaScript factory omits required workflow controls", async () => {
55
157
  const coordinator = new RunHandlerCoordinator(
56
158
  defineRunHandler(async () => {}, {
@@ -11,6 +11,8 @@ import { RivetError } from "@/actor/errors";
11
11
  type RunHandler = ((...args: any[]) => any) | { run: (...args: any[]) => any };
12
12
 
13
13
  interface ActorRunState {
14
+ /** Actor generation that owns this state, when the runtime reports one. */
15
+ actorGeneration: number | undefined;
14
16
  active: boolean;
15
17
  closed: boolean;
16
18
  exclusive: boolean;
@@ -46,6 +48,32 @@ function notifyStateChanged(state: ActorRunState): void {
46
48
  }
47
49
  }
48
50
 
51
+ function createRunState(actorGeneration: number | undefined): ActorRunState {
52
+ return {
53
+ actorGeneration,
54
+ active: false,
55
+ closed: false,
56
+ exclusive: false,
57
+ exclusiveGeneration: 0,
58
+ inspectorInitialized: false,
59
+ queued: 0,
60
+ waiters: new Set(),
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Closes run state. The inspector disposer is user code, so it is skipped for a generation
66
+ * that was declared lost.
67
+ */
68
+ function closeRunState(state: ActorRunState, disposeInspector = true): void {
69
+ state.closed = true;
70
+ if (disposeInspector) {
71
+ state.inspector?.dispose?.();
72
+ }
73
+ state.inspector = undefined;
74
+ notifyStateChanged(state);
75
+ }
76
+
49
77
  function waitForStateChange(state: ActorRunState): Promise<void> {
50
78
  return new Promise((resolve) => {
51
79
  state.waiters.add(resolve);
@@ -69,8 +97,14 @@ export class RunHandlerCoordinator {
69
97
  actorId: string,
70
98
  restart: () => void | Promise<void>,
71
99
  callback: () => void | Promise<void>,
100
+ actorGeneration?: number,
72
101
  ): Promise<RunHandlerOutcome> {
73
- const state = this.#getOrCreate(actorId);
102
+ const state = this.#getOrCreate(actorId, actorGeneration);
103
+ // Closed state belongs to a superseded generation. It must not initialize an inspector,
104
+ // which would rebind the workflow registration of the current generation.
105
+ if (state.closed) {
106
+ return "closed";
107
+ }
74
108
  state.restart = restart;
75
109
  this.#initializeInspector(actorId, state);
76
110
  state.queued += 1;
@@ -110,21 +144,37 @@ export class RunHandlerCoordinator {
110
144
  getInspector(
111
145
  actorId: string,
112
146
  restart: () => void | Promise<void>,
147
+ actorGeneration?: number,
113
148
  ): RunInspectorConfig | undefined {
114
- const state = this.#getOrCreate(actorId);
149
+ const state = this.#getOrCreate(actorId, actorGeneration);
150
+ if (state.closed) {
151
+ return undefined;
152
+ }
115
153
  state.restart = restart;
116
154
  this.#initializeInspector(actorId, state);
117
155
  return state.inspector?.inspector;
118
156
  }
119
157
 
120
- destroy(actorId: string): void {
158
+ /**
159
+ * Closes the run state for `actorId`. With `actorGeneration`, only state owned by that
160
+ * generation is closed, so an older generation's late cleanup cannot close a newer one.
161
+ */
162
+ destroy(
163
+ actorId: string,
164
+ actorGeneration?: number,
165
+ options: { disposeInspector?: boolean } = {},
166
+ ): void {
121
167
  const state = this.#states.get(actorId);
122
168
  if (!state) return;
169
+ if (
170
+ actorGeneration !== undefined &&
171
+ state.actorGeneration !== undefined &&
172
+ state.actorGeneration !== actorGeneration
173
+ ) {
174
+ return;
175
+ }
123
176
 
124
- state.closed = true;
125
- state.inspector?.dispose?.();
126
- state.inspector = undefined;
127
- notifyStateChanged(state);
177
+ closeRunState(state, options.disposeInspector ?? true);
128
178
  this.#states.delete(actorId);
129
179
  }
130
180
 
@@ -173,18 +223,34 @@ export class RunHandlerCoordinator {
173
223
  };
174
224
  }
175
225
 
176
- #getOrCreate(actorId: string): ActorRunState {
226
+ #getOrCreate(
227
+ actorId: string,
228
+ actorGeneration: number | undefined,
229
+ ): ActorRunState {
177
230
  let state = this.#states.get(actorId);
231
+ if (
232
+ state &&
233
+ actorGeneration !== undefined &&
234
+ state.actorGeneration !== undefined &&
235
+ state.actorGeneration !== actorGeneration
236
+ ) {
237
+ if (actorGeneration < state.actorGeneration) {
238
+ // A superseded generation gets closed, detached state so it cannot run or
239
+ // block the current generation.
240
+ const detached = createRunState(actorGeneration);
241
+ detached.closed = true;
242
+ return detached;
243
+ }
244
+ // A newer generation replaces state left by an older one, which may still be
245
+ // marked active because its JS callback cannot be cancelled. That generation did not
246
+ // clean up after itself, which only happens when it was lost, so its user inspector
247
+ // disposer is not called.
248
+ closeRunState(state, false);
249
+ this.#states.delete(actorId);
250
+ state = undefined;
251
+ }
178
252
  if (!state) {
179
- state = {
180
- active: false,
181
- closed: false,
182
- exclusive: false,
183
- exclusiveGeneration: 0,
184
- inspectorInitialized: false,
185
- queued: 0,
186
- waiters: new Set(),
187
- };
253
+ state = createRunState(actorGeneration);
188
254
  this.#states.set(actorId, state);
189
255
  }
190
256
  return state;
@@ -194,6 +260,7 @@ export class RunHandlerCoordinator {
194
260
  if (state.inspectorInitialized) return;
195
261
  const inspector = createRunInspector(this.#run, {
196
262
  actorId,
263
+ actorGeneration: state.actorGeneration,
197
264
  control: this.#control(actorId, state),
198
265
  });
199
266
  if (this.inspectorKind === "workflow") {
@@ -583,6 +583,8 @@ export interface CoreRuntime {
583
583
  writes: RuntimeWorkflowKvWrite[],
584
584
  ): Promise<void>;
585
585
  actorId(ctx: ActorContextHandle): string;
586
+ actorGeneration(ctx: ActorContextHandle): number | undefined;
587
+ actorIsLost(ctx: ActorContextHandle): boolean;
586
588
  /**
587
589
  * Runs one actor callback with `ctx` as the current invocation: operations
588
590
  * on retained handles for the same actor resolve to it, and its Core span
@@ -538,6 +538,14 @@ export class WasmCoreRuntime implements CoreRuntime {
538
538
  return callHandle(asWasmActorContext(ctx), "actorId");
539
539
  }
540
540
 
541
+ actorGeneration(ctx: ActorContextHandle): number | undefined {
542
+ return callHandle(asWasmActorContext(ctx), "generation") ?? undefined;
543
+ }
544
+
545
+ actorIsLost(ctx: ActorContextHandle): boolean {
546
+ return callHandle(asWasmActorContext(ctx), "isLost");
547
+ }
548
+
541
549
  runWithActorInvocationContext<T>(
542
550
  _ctx: ActorContextHandle,
543
551
  run: () => T,
@@ -12,6 +12,7 @@ import type {
12
12
  WorkflowContextInterface,
13
13
  WorkflowQueueMessage,
14
14
  } from "@rivetkit/workflow-engine";
15
+ import { EvictedError } from "@rivetkit/workflow-engine";
15
16
  import type {
16
17
  QueueFilterName,
17
18
  QueueNextBatchOptions,
@@ -19,6 +20,7 @@ import type {
19
20
  QueueResultMessageForName,
20
21
  RunContext,
21
22
  } from "@/actor/config";
23
+ import { ACTOR_CONTEXT_INTERNAL_SYMBOL } from "@/actor/config";
22
24
  import type {
23
25
  AnyActorDefinition,
24
26
  BaseActorDefinition,
@@ -282,6 +284,20 @@ export type WorkflowBranchConfig<
282
284
  // never appears on the public surface.
283
285
  const DEACTIVATE_STEP = Symbol("workflow.step.deactivate");
284
286
 
287
+ /**
288
+ * Stops the workflow before it starts more user code once its actor generation was declared
289
+ * lost. The engine treats the eviction like a sleep and replays from history in the next
290
+ * generation.
291
+ */
292
+ export function throwIfGenerationLost(runCtx: unknown): void {
293
+ const actor = (
294
+ runCtx as { [ACTOR_CONTEXT_INTERNAL_SYMBOL]?: { isLost?: boolean } }
295
+ )[ACTOR_CONTEXT_INTERNAL_SYMBOL];
296
+ if (actor?.isLost === true) {
297
+ throw new EvictedError();
298
+ }
299
+ }
300
+
285
301
  /**
286
302
  * The context handed to a workflow step (`step` / `tryStep` callbacks). This is
287
303
  * the only scope where actor data (state, vars, db, client) and side effects
@@ -994,6 +1010,7 @@ export class WorkflowContext<
994
1010
  TQueues
995
1011
  >(this.#runCtx, () => this.#markGuardTriggered());
996
1012
 
1013
+ throwIfGenerationLost(this.#runCtx);
997
1014
  try {
998
1015
  return await run(stepCtx);
999
1016
  } finally {
@@ -1044,6 +1061,7 @@ export class WorkflowContext<
1044
1061
  TEvents,
1045
1062
  TQueues
1046
1063
  >(this.#runCtx, () => this.#markGuardTriggered());
1064
+ throwIfGenerationLost(this.#runCtx);
1047
1065
  try {
1048
1066
  await rollback(stepCtx, output);
1049
1067
  } finally {
@@ -1097,6 +1115,8 @@ export class WorkflowContext<
1097
1115
  TEvents,
1098
1116
  TQueues
1099
1117
  > {
1118
+ // Every try, loop, join, and race body runs through a child context.
1119
+ throwIfGenerationLost(this.#runCtx);
1100
1120
  return new WorkflowContext(ctx, this.#runCtx);
1101
1121
  }
1102
1122
  }
@@ -10,6 +10,7 @@ import type { RunContext } from "@/actor/config";
10
10
  import type { AnyStaticActorInstance } from "@/actor/definition";
11
11
  import { makeWorkflowKey, workflowStoragePrefix } from "@/actor/keys";
12
12
  import type { RawAccess } from "@/common/database/config";
13
+ import { throwIfGenerationLost } from "./context";
13
14
 
14
15
  const WORKFLOW_STORAGE_PREFIX = workflowStoragePrefix();
15
16
  // Keep workflow flushes below depot's 320-dirty-page commit ceiling. The
@@ -69,6 +70,26 @@ function normalizeSqlBlob(value: unknown): Uint8Array {
69
70
  throw new Error("workflow sqlite value was not a blob");
70
71
  }
71
72
 
73
+ function guardRawAccessAgainstLost(
74
+ access: RawAccess,
75
+ runCtx: RunContext<any, any, any, any, any, any, any, any> | undefined,
76
+ ): RawAccess {
77
+ return {
78
+ ...access,
79
+ execute: ((...args: Parameters<RawAccess["execute"]>) => {
80
+ throwIfGenerationLost(runCtx);
81
+ return access.execute(...args);
82
+ }) as RawAccess["execute"],
83
+ transaction: (callback, options) => {
84
+ throwIfGenerationLost(runCtx);
85
+ return access.transaction(
86
+ (tx) => callback(guardRawAccessAgainstLost(tx, runCtx)),
87
+ options,
88
+ );
89
+ },
90
+ };
91
+ }
92
+
72
93
  function runtimeDbFromContext(
73
94
  runCtx?: RunContext<any, any, any, any, any, any, any, any>,
74
95
  ): RawAccess | undefined {
@@ -79,7 +100,10 @@ function runtimeDbFromContext(
79
100
  "execute" in db &&
80
101
  "transaction" in db
81
102
  ) {
82
- return db as RawAccess;
103
+ // The client can come from a user database provider. Workflow storage calls it after
104
+ // awaits, including inside its own transactions, so every call checks that the
105
+ // generation was not lost first.
106
+ return guardRawAccessAgainstLost(db as RawAccess, runCtx);
83
107
  }
84
108
  return undefined;
85
109
  }
@@ -24,7 +24,7 @@ import { isActorAbortedError, RivetError } from "@/actor/errors";
24
24
  import type { EventSchemaConfig, QueueSchemaConfig } from "@/actor/schema";
25
25
  import type { AnyDatabaseProvider } from "@/common/database/config";
26
26
  import { stringifyError } from "@/utils";
27
- import { WorkflowContext } from "./context";
27
+ import { throwIfGenerationLost, WorkflowContext } from "./context";
28
28
  import { ActorWorkflowControlDriver, ActorWorkflowDriver } from "./driver";
29
29
  import { createWorkflowInspectorAdapter } from "./inspector";
30
30
 
@@ -178,13 +178,28 @@ export function workflow<
178
178
  type WorkflowInspectorRegistration = ReturnType<
179
179
  typeof createWorkflowInspectorAdapter
180
180
  > & { control?: RunControl };
181
+ // Keyed by actor id and generation so a lost generation that is still running cannot rebind
182
+ // the control or state of the generation that replaced it.
181
183
  const workflowInspectors = new Map<string, WorkflowInspectorRegistration>();
182
184
 
183
- function getWorkflowInspector(actorId: string) {
184
- let workflowInspector = workflowInspectors.get(actorId);
185
+ function workflowInspectorKey(
186
+ actorId: string,
187
+ actorGeneration: number | undefined,
188
+ ): string {
189
+ return actorGeneration === undefined
190
+ ? actorId
191
+ : `${actorId}#${actorGeneration}`;
192
+ }
193
+
194
+ function getWorkflowInspector(
195
+ actorId: string,
196
+ actorGeneration: number | undefined,
197
+ ) {
198
+ const key = workflowInspectorKey(actorId, actorGeneration);
199
+ let workflowInspector = workflowInspectors.get(key);
185
200
  if (!workflowInspector) {
186
201
  workflowInspector = createWorkflowInspectorAdapter();
187
- workflowInspectors.set(actorId, workflowInspector);
202
+ workflowInspectors.set(key, workflowInspector);
188
203
  }
189
204
  return workflowInspector;
190
205
  }
@@ -207,7 +222,11 @@ export function workflow<
207
222
  }
208
223
  )[ACTOR_CONTEXT_INTERNAL_SYMBOL];
209
224
  invariant(actor, "workflow() requires an actor instance");
210
- const workflowInspector = getWorkflowInspector(actor.id);
225
+ const actorGeneration = (actor as { generation?: number }).generation;
226
+ const workflowInspector = getWorkflowInspector(
227
+ actor.id,
228
+ actorGeneration,
229
+ );
211
230
 
212
231
  const driver = new ActorWorkflowDriver(actor, runCtx);
213
232
  const controlDriver = new ActorWorkflowControlDriver(actor, runCtx);
@@ -246,7 +265,10 @@ export function workflow<
246
265
 
247
266
  const handle = runWorkflow(
248
267
  actor.id,
249
- async (ctx) => await fn(new WorkflowContext(ctx, runCtx)),
268
+ async (ctx) => {
269
+ throwIfGenerationLost(runCtx);
270
+ return await fn(new WorkflowContext(ctx, runCtx));
271
+ },
250
272
  undefined,
251
273
  driver,
252
274
  {
@@ -256,7 +278,15 @@ export function workflow<
256
278
  logger: runCtx.log as RunWorkflowOptions["logger"],
257
279
  onHistoryUpdated: workflowInspector.update,
258
280
  onError: onError
259
- ? async (event) => await onError(runCtx, event)
281
+ ? async (event) => {
282
+ // A lost generation starts no new user hook.
283
+ if (
284
+ (actor as { isLost?: boolean }).isLost === true
285
+ ) {
286
+ return;
287
+ }
288
+ await onError(runCtx, event);
289
+ }
260
290
  : undefined,
261
291
  },
262
292
  );
@@ -302,16 +332,20 @@ export function workflow<
302
332
  return defineRunHandler(run, {
303
333
  icon: "diagram-project",
304
334
  inspectorKind: "workflow",
305
- createInspector: ({ actorId, control }) => {
306
- const workflowInspector = getWorkflowInspector(actorId);
335
+ createInspector: ({ actorId, actorGeneration, control }) => {
336
+ const key = workflowInspectorKey(actorId, actorGeneration);
337
+ const workflowInspector = getWorkflowInspector(
338
+ actorId,
339
+ actorGeneration,
340
+ );
307
341
  workflowInspector.control = control;
308
342
  return {
309
343
  inspector: { workflow: workflowInspector.adapter },
310
344
  dispose: () => {
311
345
  // Do not let a stale disposer remove a newly-created adapter for
312
- // the same actor id.
313
- if (workflowInspectors.get(actorId) === workflowInspector) {
314
- workflowInspectors.delete(actorId);
346
+ // the same actor generation.
347
+ if (workflowInspectors.get(key) === workflowInspector) {
348
+ workflowInspectors.delete(key);
315
349
  }
316
350
  },
317
351
  };