@rivetkit/workflow-engine 2.1.0-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.
- package/LICENSE +203 -0
- package/dist/schemas/v1.ts +781 -0
- package/dist/tsup/chunk-GJ66YE5W.cjs +3441 -0
- package/dist/tsup/chunk-GJ66YE5W.cjs.map +1 -0
- package/dist/tsup/chunk-JWHWQBZP.js +3441 -0
- package/dist/tsup/chunk-JWHWQBZP.js.map +1 -0
- package/dist/tsup/index.cjs +93 -0
- package/dist/tsup/index.cjs.map +1 -0
- package/dist/tsup/index.d.cts +884 -0
- package/dist/tsup/index.d.ts +884 -0
- package/dist/tsup/index.js +93 -0
- package/dist/tsup/index.js.map +1 -0
- package/dist/tsup/testing.cjs +316 -0
- package/dist/tsup/testing.cjs.map +1 -0
- package/dist/tsup/testing.d.cts +52 -0
- package/dist/tsup/testing.d.ts +52 -0
- package/dist/tsup/testing.js +316 -0
- package/dist/tsup/testing.js.map +1 -0
- package/package.json +70 -0
- package/schemas/serde.ts +609 -0
- package/schemas/v1.bare +203 -0
- package/schemas/versioned.ts +107 -0
- package/src/context.ts +1845 -0
- package/src/driver.ts +103 -0
- package/src/errors.ts +170 -0
- package/src/index.ts +907 -0
- package/src/keys.ts +277 -0
- package/src/location.ts +168 -0
- package/src/storage.ts +364 -0
- package/src/testing.ts +292 -0
- package/src/types.ts +508 -0
- package/src/utils.ts +48 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,907 @@
|
|
|
1
|
+
import type { Logger } from "pino";
|
|
2
|
+
|
|
3
|
+
// Types
|
|
4
|
+
|
|
5
|
+
// Context
|
|
6
|
+
export {
|
|
7
|
+
DEFAULT_LOOP_COMMIT_INTERVAL,
|
|
8
|
+
DEFAULT_LOOP_HISTORY_EVERY,
|
|
9
|
+
DEFAULT_LOOP_HISTORY_KEEP,
|
|
10
|
+
DEFAULT_MAX_RETRIES,
|
|
11
|
+
DEFAULT_RETRY_BACKOFF_BASE,
|
|
12
|
+
DEFAULT_RETRY_BACKOFF_MAX,
|
|
13
|
+
DEFAULT_STEP_TIMEOUT,
|
|
14
|
+
WorkflowContextImpl,
|
|
15
|
+
} from "./context.js";
|
|
16
|
+
// Driver
|
|
17
|
+
export type { EngineDriver, KVEntry, KVWrite } from "./driver.js";
|
|
18
|
+
// Errors
|
|
19
|
+
export {
|
|
20
|
+
CancelledError,
|
|
21
|
+
CriticalError,
|
|
22
|
+
EntryInProgressError,
|
|
23
|
+
EvictedError,
|
|
24
|
+
HistoryDivergedError,
|
|
25
|
+
JoinError,
|
|
26
|
+
MessageWaitError,
|
|
27
|
+
RaceError,
|
|
28
|
+
RollbackCheckpointError,
|
|
29
|
+
RollbackError,
|
|
30
|
+
SleepError,
|
|
31
|
+
StepExhaustedError,
|
|
32
|
+
StepFailedError,
|
|
33
|
+
} from "./errors.js";
|
|
34
|
+
|
|
35
|
+
// Location utilities
|
|
36
|
+
export {
|
|
37
|
+
appendLoopIteration,
|
|
38
|
+
appendName,
|
|
39
|
+
emptyLocation,
|
|
40
|
+
isLocationPrefix,
|
|
41
|
+
isLoopIterationMarker,
|
|
42
|
+
locationsEqual,
|
|
43
|
+
locationToKey,
|
|
44
|
+
parentLocation,
|
|
45
|
+
registerName,
|
|
46
|
+
resolveName,
|
|
47
|
+
} from "./location.js";
|
|
48
|
+
|
|
49
|
+
// Storage utilities
|
|
50
|
+
export {
|
|
51
|
+
createEntry,
|
|
52
|
+
createHistorySnapshot,
|
|
53
|
+
createStorage,
|
|
54
|
+
deleteEntriesWithPrefix,
|
|
55
|
+
flush,
|
|
56
|
+
generateId,
|
|
57
|
+
getEntry,
|
|
58
|
+
getOrCreateMetadata,
|
|
59
|
+
loadMetadata,
|
|
60
|
+
loadStorage,
|
|
61
|
+
setEntry,
|
|
62
|
+
} from "./storage.js";
|
|
63
|
+
export type {
|
|
64
|
+
BranchConfig,
|
|
65
|
+
BranchOutput,
|
|
66
|
+
BranchStatus,
|
|
67
|
+
BranchStatusType,
|
|
68
|
+
Entry,
|
|
69
|
+
EntryKind,
|
|
70
|
+
EntryKindType,
|
|
71
|
+
EntryMetadata,
|
|
72
|
+
EntryStatus,
|
|
73
|
+
History,
|
|
74
|
+
JoinEntry,
|
|
75
|
+
Location,
|
|
76
|
+
LoopConfig,
|
|
77
|
+
LoopEntry,
|
|
78
|
+
LoopIterationMarker,
|
|
79
|
+
LoopResult,
|
|
80
|
+
Message,
|
|
81
|
+
MessageEntry,
|
|
82
|
+
NameIndex,
|
|
83
|
+
PathSegment,
|
|
84
|
+
RaceEntry,
|
|
85
|
+
RemovedEntry,
|
|
86
|
+
WorkflowEntryMetadataSnapshot,
|
|
87
|
+
WorkflowHistoryEntry,
|
|
88
|
+
WorkflowHistorySnapshot,
|
|
89
|
+
RollbackCheckpointEntry,
|
|
90
|
+
RollbackContextInterface,
|
|
91
|
+
RunWorkflowOptions,
|
|
92
|
+
SleepEntry,
|
|
93
|
+
SleepState,
|
|
94
|
+
StepConfig,
|
|
95
|
+
StepEntry,
|
|
96
|
+
Storage,
|
|
97
|
+
WorkflowContextInterface,
|
|
98
|
+
WorkflowFunction,
|
|
99
|
+
WorkflowHandle,
|
|
100
|
+
WorkflowQueue,
|
|
101
|
+
WorkflowQueueMessage,
|
|
102
|
+
WorkflowQueueNextOptions,
|
|
103
|
+
WorkflowMessageDriver,
|
|
104
|
+
WorkflowResult,
|
|
105
|
+
WorkflowRunMode,
|
|
106
|
+
WorkflowState,
|
|
107
|
+
} from "./types.js";
|
|
108
|
+
|
|
109
|
+
// Loop result helpers
|
|
110
|
+
export const Loop = {
|
|
111
|
+
continue: <S>(state: S): { continue: true; state: S } => ({
|
|
112
|
+
continue: true,
|
|
113
|
+
state,
|
|
114
|
+
}),
|
|
115
|
+
break: <T>(value: T): { break: true; value: T } => ({
|
|
116
|
+
break: true,
|
|
117
|
+
value,
|
|
118
|
+
}),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
import {
|
|
122
|
+
deserializeEntryMetadata,
|
|
123
|
+
deserializeWorkflowInput,
|
|
124
|
+
deserializeWorkflowOutput,
|
|
125
|
+
deserializeWorkflowState,
|
|
126
|
+
serializeEntryMetadata,
|
|
127
|
+
serializeWorkflowInput,
|
|
128
|
+
serializeWorkflowState,
|
|
129
|
+
} from "../schemas/serde.js";
|
|
130
|
+
import { type RollbackAction, WorkflowContextImpl } from "./context.js";
|
|
131
|
+
// Main workflow runner
|
|
132
|
+
import type { EngineDriver } from "./driver.js";
|
|
133
|
+
import {
|
|
134
|
+
EvictedError,
|
|
135
|
+
MessageWaitError,
|
|
136
|
+
RollbackCheckpointError,
|
|
137
|
+
RollbackStopError,
|
|
138
|
+
SleepError,
|
|
139
|
+
StepFailedError,
|
|
140
|
+
} from "./errors.js";
|
|
141
|
+
import {
|
|
142
|
+
buildEntryMetadataPrefix,
|
|
143
|
+
buildWorkflowErrorKey,
|
|
144
|
+
buildWorkflowInputKey,
|
|
145
|
+
buildWorkflowOutputKey,
|
|
146
|
+
buildWorkflowStateKey,
|
|
147
|
+
} from "./keys.js";
|
|
148
|
+
import {
|
|
149
|
+
createHistorySnapshot,
|
|
150
|
+
flush,
|
|
151
|
+
generateId,
|
|
152
|
+
loadMetadata,
|
|
153
|
+
loadStorage,
|
|
154
|
+
} from "./storage.js";
|
|
155
|
+
import type {
|
|
156
|
+
RollbackContextInterface,
|
|
157
|
+
RunWorkflowOptions,
|
|
158
|
+
Storage,
|
|
159
|
+
WorkflowHistorySnapshot,
|
|
160
|
+
WorkflowFunction,
|
|
161
|
+
WorkflowHandle,
|
|
162
|
+
WorkflowMessageDriver,
|
|
163
|
+
WorkflowResult,
|
|
164
|
+
WorkflowRunMode,
|
|
165
|
+
WorkflowState,
|
|
166
|
+
} from "./types.js";
|
|
167
|
+
import { setLongTimeout } from "./utils.js";
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Run a workflow and return a handle for managing it.
|
|
171
|
+
*
|
|
172
|
+
* The workflow starts executing immediately. Use the returned handle to:
|
|
173
|
+
* - `handle.result` - Await workflow completion (or yield in `yield` mode)
|
|
174
|
+
* - `handle.message()` - Send messages to the workflow
|
|
175
|
+
* - `handle.wake()` - Wake the workflow early
|
|
176
|
+
* - `handle.evict()` - Request graceful shutdown
|
|
177
|
+
* - `handle.getOutput()` / `handle.getState()` - Query status
|
|
178
|
+
*/
|
|
179
|
+
interface LiveRuntime {
|
|
180
|
+
sleepWaiter?: () => void;
|
|
181
|
+
isSleeping: boolean;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
type HistoryNotifier = (() => void) | undefined;
|
|
185
|
+
|
|
186
|
+
function createLiveRuntime(): LiveRuntime {
|
|
187
|
+
return {
|
|
188
|
+
isSleeping: false,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function createEvictionWait(signal: AbortSignal): {
|
|
193
|
+
promise: Promise<never>;
|
|
194
|
+
cleanup: () => void;
|
|
195
|
+
} {
|
|
196
|
+
if (signal.aborted) {
|
|
197
|
+
return {
|
|
198
|
+
promise: Promise.reject(new EvictedError()),
|
|
199
|
+
cleanup: () => {},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let onAbort: (() => void) | undefined;
|
|
204
|
+
const promise = new Promise<never>((_, reject) => {
|
|
205
|
+
onAbort = () => {
|
|
206
|
+
reject(new EvictedError());
|
|
207
|
+
};
|
|
208
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
promise,
|
|
213
|
+
cleanup: () => {
|
|
214
|
+
if (onAbort) {
|
|
215
|
+
signal.removeEventListener("abort", onAbort);
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function createRollbackContext(
|
|
222
|
+
workflowId: string,
|
|
223
|
+
abortController: AbortController,
|
|
224
|
+
): RollbackContextInterface {
|
|
225
|
+
return {
|
|
226
|
+
workflowId,
|
|
227
|
+
abortSignal: abortController.signal,
|
|
228
|
+
isEvicted: () => abortController.signal.aborted,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function awaitWithEviction<T>(
|
|
233
|
+
promise: Promise<T>,
|
|
234
|
+
abortSignal: AbortSignal,
|
|
235
|
+
): Promise<T> {
|
|
236
|
+
const { promise: evictionPromise, cleanup } =
|
|
237
|
+
createEvictionWait(abortSignal);
|
|
238
|
+
try {
|
|
239
|
+
return await Promise.race([promise, evictionPromise]);
|
|
240
|
+
} finally {
|
|
241
|
+
cleanup();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function executeRollback<TInput, TOutput>(
|
|
246
|
+
workflowId: string,
|
|
247
|
+
workflowFn: WorkflowFunction<TInput, TOutput>,
|
|
248
|
+
input: TInput,
|
|
249
|
+
driver: EngineDriver,
|
|
250
|
+
messageDriver: WorkflowMessageDriver,
|
|
251
|
+
abortController: AbortController,
|
|
252
|
+
storage: Storage,
|
|
253
|
+
historyNotifier?: HistoryNotifier,
|
|
254
|
+
logger?: Logger,
|
|
255
|
+
): Promise<void> {
|
|
256
|
+
const rollbackActions: RollbackAction[] = [];
|
|
257
|
+
const ctx = new WorkflowContextImpl(
|
|
258
|
+
workflowId,
|
|
259
|
+
storage,
|
|
260
|
+
driver,
|
|
261
|
+
messageDriver,
|
|
262
|
+
undefined,
|
|
263
|
+
abortController,
|
|
264
|
+
"rollback",
|
|
265
|
+
rollbackActions,
|
|
266
|
+
false,
|
|
267
|
+
historyNotifier,
|
|
268
|
+
logger,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
await workflowFn(ctx, input);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (error instanceof EvictedError) {
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
if (error instanceof RollbackStopError) {
|
|
278
|
+
// Stop replay once we hit incomplete history during rollback.
|
|
279
|
+
} else {
|
|
280
|
+
// Ignore workflow errors during rollback replay.
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (rollbackActions.length === 0) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const rollbackContext = createRollbackContext(workflowId, abortController);
|
|
289
|
+
|
|
290
|
+
for (let i = rollbackActions.length - 1; i >= 0; i--) {
|
|
291
|
+
if (abortController.signal.aborted) {
|
|
292
|
+
throw new EvictedError();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const action = rollbackActions[i];
|
|
296
|
+
const metadata = await loadMetadata(storage, driver, action.entryId);
|
|
297
|
+
if (metadata.rollbackCompletedAt !== undefined) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
await awaitWithEviction(
|
|
303
|
+
action.rollback(rollbackContext, action.output),
|
|
304
|
+
abortController.signal,
|
|
305
|
+
);
|
|
306
|
+
metadata.rollbackCompletedAt = Date.now();
|
|
307
|
+
metadata.rollbackError = undefined;
|
|
308
|
+
} catch (error) {
|
|
309
|
+
if (error instanceof EvictedError) {
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
metadata.rollbackError =
|
|
313
|
+
error instanceof Error ? error.message : String(error);
|
|
314
|
+
throw error;
|
|
315
|
+
} finally {
|
|
316
|
+
metadata.dirty = true;
|
|
317
|
+
await flush(storage, driver, historyNotifier);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function setSleepState<TOutput>(
|
|
323
|
+
storage: Storage,
|
|
324
|
+
driver: EngineDriver,
|
|
325
|
+
workflowId: string,
|
|
326
|
+
deadline: number,
|
|
327
|
+
messageNames?: string[],
|
|
328
|
+
historyNotifier?: HistoryNotifier,
|
|
329
|
+
): Promise<WorkflowResult<TOutput>> {
|
|
330
|
+
storage.state = "sleeping";
|
|
331
|
+
await flush(storage, driver, historyNotifier);
|
|
332
|
+
await driver.setAlarm(workflowId, deadline);
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
state: "sleeping",
|
|
336
|
+
sleepUntil: deadline,
|
|
337
|
+
waitingForMessages: messageNames,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function setMessageWaitState<TOutput>(
|
|
342
|
+
storage: Storage,
|
|
343
|
+
driver: EngineDriver,
|
|
344
|
+
messageNames: string[],
|
|
345
|
+
historyNotifier?: HistoryNotifier,
|
|
346
|
+
): Promise<WorkflowResult<TOutput>> {
|
|
347
|
+
storage.state = "sleeping";
|
|
348
|
+
await flush(storage, driver, historyNotifier);
|
|
349
|
+
|
|
350
|
+
return { state: "sleeping", waitingForMessages: messageNames };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function setEvictedState<TOutput>(
|
|
354
|
+
storage: Storage,
|
|
355
|
+
driver: EngineDriver,
|
|
356
|
+
historyNotifier?: HistoryNotifier,
|
|
357
|
+
): Promise<WorkflowResult<TOutput>> {
|
|
358
|
+
await flush(storage, driver, historyNotifier);
|
|
359
|
+
return { state: storage.state };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function setRetryState<TOutput>(
|
|
363
|
+
storage: Storage,
|
|
364
|
+
driver: EngineDriver,
|
|
365
|
+
workflowId: string,
|
|
366
|
+
historyNotifier?: HistoryNotifier,
|
|
367
|
+
): Promise<WorkflowResult<TOutput>> {
|
|
368
|
+
storage.state = "sleeping";
|
|
369
|
+
await flush(storage, driver, historyNotifier);
|
|
370
|
+
|
|
371
|
+
const retryAt = Date.now() + 100;
|
|
372
|
+
await driver.setAlarm(workflowId, retryAt);
|
|
373
|
+
|
|
374
|
+
return { state: "sleeping", sleepUntil: retryAt };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function setFailedState(
|
|
378
|
+
storage: Storage,
|
|
379
|
+
driver: EngineDriver,
|
|
380
|
+
error: unknown,
|
|
381
|
+
historyNotifier?: HistoryNotifier,
|
|
382
|
+
): Promise<void> {
|
|
383
|
+
storage.state = "failed";
|
|
384
|
+
storage.error = extractErrorInfo(error);
|
|
385
|
+
await flush(storage, driver, historyNotifier);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function waitForSleep(
|
|
389
|
+
runtime: LiveRuntime,
|
|
390
|
+
deadline: number,
|
|
391
|
+
abortSignal: AbortSignal,
|
|
392
|
+
): Promise<void> {
|
|
393
|
+
while (true) {
|
|
394
|
+
const remaining = deadline - Date.now();
|
|
395
|
+
if (remaining <= 0) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
let timeoutHandle: ReturnType<typeof setLongTimeout> | undefined;
|
|
400
|
+
const timeoutPromise = new Promise<void>((resolve) => {
|
|
401
|
+
timeoutHandle = setLongTimeout(resolve, remaining);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const wakePromise = new Promise<void>((resolve) => {
|
|
405
|
+
runtime.sleepWaiter = resolve;
|
|
406
|
+
});
|
|
407
|
+
runtime.isSleeping = true;
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
await awaitWithEviction(
|
|
411
|
+
Promise.race([timeoutPromise, wakePromise]),
|
|
412
|
+
abortSignal,
|
|
413
|
+
);
|
|
414
|
+
} finally {
|
|
415
|
+
runtime.isSleeping = false;
|
|
416
|
+
runtime.sleepWaiter = undefined;
|
|
417
|
+
timeoutHandle?.abort();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (abortSignal.aborted) {
|
|
421
|
+
throw new EvictedError();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (Date.now() >= deadline) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function executeLiveWorkflow<TInput, TOutput>(
|
|
431
|
+
workflowId: string,
|
|
432
|
+
workflowFn: WorkflowFunction<TInput, TOutput>,
|
|
433
|
+
input: TInput,
|
|
434
|
+
driver: EngineDriver,
|
|
435
|
+
messageDriver: WorkflowMessageDriver,
|
|
436
|
+
abortController: AbortController,
|
|
437
|
+
runtime: LiveRuntime,
|
|
438
|
+
onHistoryUpdated?: (history: WorkflowHistorySnapshot) => void,
|
|
439
|
+
logger?: Logger,
|
|
440
|
+
): Promise<WorkflowResult<TOutput>> {
|
|
441
|
+
let lastResult: WorkflowResult<TOutput> | undefined;
|
|
442
|
+
|
|
443
|
+
while (true) {
|
|
444
|
+
const result = await executeWorkflow(
|
|
445
|
+
workflowId,
|
|
446
|
+
workflowFn,
|
|
447
|
+
input,
|
|
448
|
+
driver,
|
|
449
|
+
messageDriver,
|
|
450
|
+
abortController,
|
|
451
|
+
onHistoryUpdated,
|
|
452
|
+
logger,
|
|
453
|
+
);
|
|
454
|
+
lastResult = result;
|
|
455
|
+
|
|
456
|
+
if (result.state !== "sleeping") {
|
|
457
|
+
return result;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const hasMessages = result.waitingForMessages !== undefined;
|
|
461
|
+
const hasDeadline = result.sleepUntil !== undefined;
|
|
462
|
+
|
|
463
|
+
if (hasMessages && hasDeadline) {
|
|
464
|
+
// Wait for EITHER a message OR the deadline (for queue.next timeout)
|
|
465
|
+
try {
|
|
466
|
+
const messagePromise = awaitWithEviction(
|
|
467
|
+
driver.waitForMessages(
|
|
468
|
+
result.waitingForMessages!,
|
|
469
|
+
abortController.signal,
|
|
470
|
+
),
|
|
471
|
+
abortController.signal,
|
|
472
|
+
);
|
|
473
|
+
const sleepPromise = waitForSleep(
|
|
474
|
+
runtime,
|
|
475
|
+
result.sleepUntil!,
|
|
476
|
+
abortController.signal,
|
|
477
|
+
);
|
|
478
|
+
await Promise.race([messagePromise, sleepPromise]);
|
|
479
|
+
} catch (error) {
|
|
480
|
+
if (error instanceof EvictedError) {
|
|
481
|
+
return lastResult;
|
|
482
|
+
}
|
|
483
|
+
throw error;
|
|
484
|
+
}
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (hasMessages) {
|
|
489
|
+
try {
|
|
490
|
+
await awaitWithEviction(
|
|
491
|
+
driver.waitForMessages(
|
|
492
|
+
result.waitingForMessages!,
|
|
493
|
+
abortController.signal,
|
|
494
|
+
),
|
|
495
|
+
abortController.signal,
|
|
496
|
+
);
|
|
497
|
+
} catch (error) {
|
|
498
|
+
if (error instanceof EvictedError) {
|
|
499
|
+
return lastResult;
|
|
500
|
+
}
|
|
501
|
+
throw error;
|
|
502
|
+
}
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (hasDeadline) {
|
|
507
|
+
try {
|
|
508
|
+
await waitForSleep(
|
|
509
|
+
runtime,
|
|
510
|
+
result.sleepUntil!,
|
|
511
|
+
abortController.signal,
|
|
512
|
+
);
|
|
513
|
+
} catch (error) {
|
|
514
|
+
if (error instanceof EvictedError) {
|
|
515
|
+
return lastResult;
|
|
516
|
+
}
|
|
517
|
+
throw error;
|
|
518
|
+
}
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return result;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export function runWorkflow<TInput, TOutput>(
|
|
527
|
+
workflowId: string,
|
|
528
|
+
workflowFn: WorkflowFunction<TInput, TOutput>,
|
|
529
|
+
input: TInput,
|
|
530
|
+
driver: EngineDriver,
|
|
531
|
+
options: RunWorkflowOptions = {},
|
|
532
|
+
): WorkflowHandle<TOutput> {
|
|
533
|
+
const messageDriver = driver.messageDriver;
|
|
534
|
+
const abortController = new AbortController();
|
|
535
|
+
const mode: WorkflowRunMode = options.mode ?? "yield";
|
|
536
|
+
const liveRuntime = mode === "live" ? createLiveRuntime() : undefined;
|
|
537
|
+
|
|
538
|
+
const logger = options.logger;
|
|
539
|
+
|
|
540
|
+
const resultPromise =
|
|
541
|
+
mode === "live" && liveRuntime
|
|
542
|
+
? executeLiveWorkflow(
|
|
543
|
+
workflowId,
|
|
544
|
+
workflowFn,
|
|
545
|
+
input,
|
|
546
|
+
driver,
|
|
547
|
+
messageDriver,
|
|
548
|
+
abortController,
|
|
549
|
+
liveRuntime,
|
|
550
|
+
options.onHistoryUpdated,
|
|
551
|
+
logger,
|
|
552
|
+
)
|
|
553
|
+
: executeWorkflow(
|
|
554
|
+
workflowId,
|
|
555
|
+
workflowFn,
|
|
556
|
+
input,
|
|
557
|
+
driver,
|
|
558
|
+
messageDriver,
|
|
559
|
+
abortController,
|
|
560
|
+
options.onHistoryUpdated,
|
|
561
|
+
logger,
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
workflowId,
|
|
566
|
+
result: resultPromise,
|
|
567
|
+
|
|
568
|
+
async message(name: string, data: unknown): Promise<void> {
|
|
569
|
+
const messageId = generateId();
|
|
570
|
+
await messageDriver.addMessage({
|
|
571
|
+
id: messageId,
|
|
572
|
+
name,
|
|
573
|
+
data,
|
|
574
|
+
sentAt: Date.now(),
|
|
575
|
+
});
|
|
576
|
+
},
|
|
577
|
+
|
|
578
|
+
async wake(): Promise<void> {
|
|
579
|
+
if (liveRuntime) {
|
|
580
|
+
if (liveRuntime.isSleeping && liveRuntime.sleepWaiter) {
|
|
581
|
+
liveRuntime.sleepWaiter();
|
|
582
|
+
}
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
await driver.setAlarm(workflowId, Date.now());
|
|
586
|
+
},
|
|
587
|
+
|
|
588
|
+
async recover(): Promise<void> {
|
|
589
|
+
const stateValue = await driver.get(buildWorkflowStateKey());
|
|
590
|
+
const state = stateValue
|
|
591
|
+
? deserializeWorkflowState(stateValue)
|
|
592
|
+
: "pending";
|
|
593
|
+
|
|
594
|
+
if (state !== "failed") {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const metadataEntries = await driver.list(
|
|
599
|
+
buildEntryMetadataPrefix(),
|
|
600
|
+
);
|
|
601
|
+
const writes: { key: Uint8Array; value: Uint8Array }[] = [];
|
|
602
|
+
|
|
603
|
+
for (const entry of metadataEntries) {
|
|
604
|
+
const metadata = deserializeEntryMetadata(entry.value);
|
|
605
|
+
if (
|
|
606
|
+
metadata.status !== "failed" &&
|
|
607
|
+
metadata.status !== "exhausted"
|
|
608
|
+
) {
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
metadata.status = "pending";
|
|
613
|
+
metadata.attempts = 0;
|
|
614
|
+
metadata.lastAttemptAt = 0;
|
|
615
|
+
metadata.error = undefined;
|
|
616
|
+
metadata.dirty = false;
|
|
617
|
+
|
|
618
|
+
writes.push({
|
|
619
|
+
key: entry.key,
|
|
620
|
+
value: serializeEntryMetadata(metadata),
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (writes.length > 0) {
|
|
625
|
+
await driver.batch(writes);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
await driver.delete(buildWorkflowErrorKey());
|
|
629
|
+
await driver.set(
|
|
630
|
+
buildWorkflowStateKey(),
|
|
631
|
+
serializeWorkflowState("sleeping"),
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
if (liveRuntime) {
|
|
635
|
+
if (liveRuntime.isSleeping && liveRuntime.sleepWaiter) {
|
|
636
|
+
liveRuntime.sleepWaiter();
|
|
637
|
+
}
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
await driver.setAlarm(workflowId, Date.now());
|
|
642
|
+
},
|
|
643
|
+
|
|
644
|
+
evict(): void {
|
|
645
|
+
abortController.abort(new EvictedError());
|
|
646
|
+
},
|
|
647
|
+
|
|
648
|
+
async cancel(): Promise<void> {
|
|
649
|
+
abortController.abort(new EvictedError());
|
|
650
|
+
|
|
651
|
+
await driver.set(
|
|
652
|
+
buildWorkflowStateKey(),
|
|
653
|
+
serializeWorkflowState("cancelled"),
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
await driver.clearAlarm(workflowId);
|
|
657
|
+
},
|
|
658
|
+
|
|
659
|
+
async getOutput(): Promise<TOutput | undefined> {
|
|
660
|
+
const value = await driver.get(buildWorkflowOutputKey());
|
|
661
|
+
if (!value) {
|
|
662
|
+
return undefined;
|
|
663
|
+
}
|
|
664
|
+
return deserializeWorkflowOutput<TOutput>(value);
|
|
665
|
+
},
|
|
666
|
+
|
|
667
|
+
async getState(): Promise<WorkflowState> {
|
|
668
|
+
const value = await driver.get(buildWorkflowStateKey());
|
|
669
|
+
if (!value) {
|
|
670
|
+
return "pending";
|
|
671
|
+
}
|
|
672
|
+
return deserializeWorkflowState(value);
|
|
673
|
+
},
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Internal: Execute the workflow and return the result.
|
|
679
|
+
*/
|
|
680
|
+
async function executeWorkflow<TInput, TOutput>(
|
|
681
|
+
workflowId: string,
|
|
682
|
+
workflowFn: WorkflowFunction<TInput, TOutput>,
|
|
683
|
+
input: TInput,
|
|
684
|
+
driver: EngineDriver,
|
|
685
|
+
messageDriver: WorkflowMessageDriver,
|
|
686
|
+
abortController: AbortController,
|
|
687
|
+
onHistoryUpdated?: (history: WorkflowHistorySnapshot) => void,
|
|
688
|
+
logger?: Logger,
|
|
689
|
+
): Promise<WorkflowResult<TOutput>> {
|
|
690
|
+
const storage = await loadStorage(driver);
|
|
691
|
+
const historyNotifier: HistoryNotifier = onHistoryUpdated
|
|
692
|
+
? () => onHistoryUpdated(createHistorySnapshot(storage))
|
|
693
|
+
: undefined;
|
|
694
|
+
if (historyNotifier) {
|
|
695
|
+
historyNotifier();
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (logger) {
|
|
699
|
+
const entryKeys = Array.from(storage.history.entries.keys());
|
|
700
|
+
logger.debug({
|
|
701
|
+
msg: "loaded workflow storage",
|
|
702
|
+
state: storage.state,
|
|
703
|
+
entryCount: entryKeys.length,
|
|
704
|
+
entries: entryKeys.slice(0, 10),
|
|
705
|
+
nameRegistry: storage.nameRegistry,
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Check if workflow was cancelled
|
|
710
|
+
if (storage.state === "cancelled") {
|
|
711
|
+
throw new EvictedError();
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Input persistence: store on first run, use stored input on resume
|
|
715
|
+
const storedInputBytes = await driver.get(buildWorkflowInputKey());
|
|
716
|
+
let effectiveInput: TInput;
|
|
717
|
+
|
|
718
|
+
if (storedInputBytes) {
|
|
719
|
+
// Resume: use stored input for deterministic replay
|
|
720
|
+
effectiveInput = deserializeWorkflowInput<TInput>(storedInputBytes);
|
|
721
|
+
} else {
|
|
722
|
+
// First run: store the input
|
|
723
|
+
effectiveInput = input;
|
|
724
|
+
await driver.set(
|
|
725
|
+
buildWorkflowInputKey(),
|
|
726
|
+
serializeWorkflowInput(input),
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (storage.state === "rolling_back") {
|
|
731
|
+
try {
|
|
732
|
+
await executeRollback(
|
|
733
|
+
workflowId,
|
|
734
|
+
workflowFn,
|
|
735
|
+
effectiveInput,
|
|
736
|
+
driver,
|
|
737
|
+
messageDriver,
|
|
738
|
+
abortController,
|
|
739
|
+
storage,
|
|
740
|
+
historyNotifier,
|
|
741
|
+
logger,
|
|
742
|
+
);
|
|
743
|
+
} catch (error) {
|
|
744
|
+
if (error instanceof EvictedError) {
|
|
745
|
+
return { state: storage.state };
|
|
746
|
+
}
|
|
747
|
+
throw error;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
storage.state = "failed";
|
|
751
|
+
await flush(storage, driver, historyNotifier);
|
|
752
|
+
|
|
753
|
+
const storedError = storage.error
|
|
754
|
+
? new Error(storage.error.message)
|
|
755
|
+
: new Error("Workflow failed");
|
|
756
|
+
if (storage.error?.name) {
|
|
757
|
+
storedError.name = storage.error.name;
|
|
758
|
+
}
|
|
759
|
+
throw storedError;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const ctx = new WorkflowContextImpl(
|
|
763
|
+
workflowId,
|
|
764
|
+
storage,
|
|
765
|
+
driver,
|
|
766
|
+
messageDriver,
|
|
767
|
+
undefined,
|
|
768
|
+
abortController,
|
|
769
|
+
"forward",
|
|
770
|
+
undefined,
|
|
771
|
+
false,
|
|
772
|
+
historyNotifier,
|
|
773
|
+
logger,
|
|
774
|
+
);
|
|
775
|
+
|
|
776
|
+
storage.state = "running";
|
|
777
|
+
|
|
778
|
+
try {
|
|
779
|
+
const output = await workflowFn(ctx, effectiveInput);
|
|
780
|
+
|
|
781
|
+
storage.state = "completed";
|
|
782
|
+
storage.output = output;
|
|
783
|
+
await flush(storage, driver, historyNotifier);
|
|
784
|
+
await driver.clearAlarm(workflowId);
|
|
785
|
+
|
|
786
|
+
return { state: "completed", output };
|
|
787
|
+
} catch (error) {
|
|
788
|
+
if (error instanceof SleepError) {
|
|
789
|
+
return await setSleepState(
|
|
790
|
+
storage,
|
|
791
|
+
driver,
|
|
792
|
+
workflowId,
|
|
793
|
+
error.deadline,
|
|
794
|
+
error.messageNames,
|
|
795
|
+
historyNotifier,
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (error instanceof MessageWaitError) {
|
|
800
|
+
return await setMessageWaitState(
|
|
801
|
+
storage,
|
|
802
|
+
driver,
|
|
803
|
+
error.messageNames,
|
|
804
|
+
historyNotifier,
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if (error instanceof EvictedError) {
|
|
809
|
+
return await setEvictedState(storage, driver, historyNotifier);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (error instanceof StepFailedError) {
|
|
813
|
+
return await setRetryState(
|
|
814
|
+
storage,
|
|
815
|
+
driver,
|
|
816
|
+
workflowId,
|
|
817
|
+
historyNotifier,
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (error instanceof RollbackCheckpointError) {
|
|
822
|
+
await setFailedState(storage, driver, error, historyNotifier);
|
|
823
|
+
throw error;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Unrecoverable error
|
|
827
|
+
storage.error = extractErrorInfo(error);
|
|
828
|
+
storage.state = "rolling_back";
|
|
829
|
+
await flush(storage, driver, historyNotifier);
|
|
830
|
+
|
|
831
|
+
try {
|
|
832
|
+
await executeRollback(
|
|
833
|
+
workflowId,
|
|
834
|
+
workflowFn,
|
|
835
|
+
effectiveInput,
|
|
836
|
+
driver,
|
|
837
|
+
messageDriver,
|
|
838
|
+
abortController,
|
|
839
|
+
storage,
|
|
840
|
+
historyNotifier,
|
|
841
|
+
logger,
|
|
842
|
+
);
|
|
843
|
+
} catch (rollbackError) {
|
|
844
|
+
if (rollbackError instanceof EvictedError) {
|
|
845
|
+
return { state: storage.state };
|
|
846
|
+
}
|
|
847
|
+
throw rollbackError;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
storage.state = "failed";
|
|
851
|
+
await flush(storage, driver, historyNotifier);
|
|
852
|
+
|
|
853
|
+
throw error;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Extract structured error information from an error.
|
|
859
|
+
*/
|
|
860
|
+
function extractErrorInfo(error: unknown): {
|
|
861
|
+
name: string;
|
|
862
|
+
message: string;
|
|
863
|
+
stack?: string;
|
|
864
|
+
metadata?: Record<string, unknown>;
|
|
865
|
+
} {
|
|
866
|
+
if (error instanceof Error) {
|
|
867
|
+
const result: {
|
|
868
|
+
name: string;
|
|
869
|
+
message: string;
|
|
870
|
+
stack?: string;
|
|
871
|
+
metadata?: Record<string, unknown>;
|
|
872
|
+
} = {
|
|
873
|
+
name: error.name,
|
|
874
|
+
message: error.message,
|
|
875
|
+
stack: error.stack,
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
// Extract custom properties from error
|
|
879
|
+
const metadata: Record<string, unknown> = {};
|
|
880
|
+
for (const key of Object.keys(error)) {
|
|
881
|
+
if (key !== "name" && key !== "message" && key !== "stack") {
|
|
882
|
+
const value = (error as unknown as Record<string, unknown>)[
|
|
883
|
+
key
|
|
884
|
+
];
|
|
885
|
+
// Only include serializable values
|
|
886
|
+
if (
|
|
887
|
+
typeof value === "string" ||
|
|
888
|
+
typeof value === "number" ||
|
|
889
|
+
typeof value === "boolean" ||
|
|
890
|
+
value === null
|
|
891
|
+
) {
|
|
892
|
+
metadata[key] = value;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
if (Object.keys(metadata).length > 0) {
|
|
897
|
+
result.metadata = metadata;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
return result;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
return {
|
|
904
|
+
name: "Error",
|
|
905
|
+
message: String(error),
|
|
906
|
+
};
|
|
907
|
+
}
|