workerflow 0.1.0 → 0.3.0
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/README.md +35 -29
- package/package.json +1 -1
- package/src/definition.ts +126 -174
- package/src/json.ts +5 -7
- package/src/migrations/0000_initial.ts +98 -294
- package/src/runtime.ts +634 -998
- package/test/runtime.spec.ts +709 -1113
- package/test/tsconfig.json +1 -4
- package/test/worker.ts +1 -3
- package/demo/README.md +0 -73
- package/demo/index.html +0 -13
- package/demo/package.json +0 -33
- package/demo/public/vite.svg +0 -1
- package/demo/src/App.css +0 -0
- package/demo/src/App.tsx +0 -9
- package/demo/src/assets/Cloudflare_Logo.svg +0 -51
- package/demo/src/assets/react.svg +0 -1
- package/demo/src/index.css +0 -1
- package/demo/src/main.tsx +0 -10
- package/demo/tsconfig.app.json +0 -28
- package/demo/tsconfig.json +0 -14
- package/demo/tsconfig.node.json +0 -25
- package/demo/tsconfig.worker.json +0 -13
- package/demo/vite.config.ts +0 -9
- package/demo/worker/index.ts +0 -16
- package/demo/worker-configuration.d.ts +0 -12851
- package/demo/wrangler.jsonc +0 -32
package/src/runtime.ts
CHANGED
|
@@ -4,15 +4,11 @@ import type { Json } from "./json";
|
|
|
4
4
|
import mig000 from "./migrations/0000_initial";
|
|
5
5
|
import type { Brand } from "./brand";
|
|
6
6
|
|
|
7
|
-
export abstract class WorkflowRuntime<
|
|
8
|
-
TInput extends Json | undefined = Json | undefined,
|
|
9
|
-
TVersion extends string = string
|
|
10
|
-
> extends DurableObject {
|
|
7
|
+
export abstract class WorkflowRuntime<TInput extends Json | undefined = Json | undefined> extends DurableObject {
|
|
11
8
|
private static readonly MIGRATIONS = [mig000];
|
|
12
9
|
private readonly sql: SqlStorage;
|
|
13
10
|
#status: WorkflowStatus;
|
|
14
11
|
#isRunLoopActive: boolean = false;
|
|
15
|
-
#definitionVersion: TVersion | undefined;
|
|
16
12
|
#definitionInput: TInput | undefined;
|
|
17
13
|
|
|
18
14
|
/**
|
|
@@ -46,24 +42,19 @@ export abstract class WorkflowRuntime<
|
|
|
46
42
|
console.error("Database migration version is ahead of the codebase. Please check your migrations.");
|
|
47
43
|
}
|
|
48
44
|
|
|
49
|
-
const [metadata] = this.sql
|
|
50
|
-
.exec<WorkflowMetadata_Row<TVersion>>("SELECT * FROM workflow_metadata WHERE id = 1")
|
|
51
|
-
.toArray();
|
|
45
|
+
const [metadata] = this.sql.exec<WorkflowMetadata_Row>("SELECT * FROM workflow_metadata WHERE id = 1").toArray();
|
|
52
46
|
if (metadata === undefined) {
|
|
53
47
|
this.sql.exec("INSERT INTO workflow_metadata (id, status) VALUES (1, ?)", "pending");
|
|
54
48
|
this.sql.exec("INSERT INTO workflow_events (type) VALUES (?)", "created");
|
|
55
49
|
this.#status = "pending";
|
|
56
50
|
} else {
|
|
57
51
|
this.#status = metadata.status;
|
|
58
|
-
this.#definitionVersion = metadata.definition_version === null ? undefined : metadata.definition_version;
|
|
59
52
|
this.#definitionInput =
|
|
60
53
|
metadata.definition_input === null ? undefined : (JSON.parse(metadata.definition_input) as TInput);
|
|
61
54
|
}
|
|
62
55
|
}
|
|
63
56
|
|
|
64
|
-
protected abstract
|
|
65
|
-
version: TVersion
|
|
66
|
-
): (options: {
|
|
57
|
+
protected abstract readonly definition: (options: {
|
|
67
58
|
props: { requestId: string; runtimeInstanceId: string; input: TInput };
|
|
68
59
|
}) => Fetcher<WorkflowDefinition<TInput>>;
|
|
69
60
|
|
|
@@ -117,19 +108,61 @@ export abstract class WorkflowRuntime<
|
|
|
117
108
|
* @returns An array containing the formatted steps for all steps in the
|
|
118
109
|
* workflow.
|
|
119
110
|
*/
|
|
120
|
-
getSteps_experimental():
|
|
111
|
+
getSteps_experimental(): Array<(RunStep & { attempts: RunStepAttempt[] }) | SleepStep | WaitStep> {
|
|
121
112
|
const steps = this.sql.exec<Step_Row>("SELECT * FROM steps ORDER BY created_at ASC").toArray();
|
|
122
|
-
return steps.map((step) =>
|
|
113
|
+
return steps.map((step) => {
|
|
114
|
+
if (step.type === "run") {
|
|
115
|
+
const attempts = this.sql
|
|
116
|
+
.exec<RunStepAttempt_Row>(
|
|
117
|
+
`SELECT * FROM run_step_attempts WHERE step_id = ? ORDER BY started_at ASC, id ASC`,
|
|
118
|
+
step.id
|
|
119
|
+
)
|
|
120
|
+
.toArray();
|
|
121
|
+
return {
|
|
122
|
+
...formatRunStep(step),
|
|
123
|
+
attempts: attempts.map((attempt) => formatRunStepAttempt(attempt))
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (step.type === "sleep") {
|
|
127
|
+
return formatSleepStep(step);
|
|
128
|
+
} else if (step.type === "wait") {
|
|
129
|
+
if (step.state === "satisfied") {
|
|
130
|
+
return formatSatisfiedWaitStep(step, this.getInboundEventForWaitStep(step.id).payload);
|
|
131
|
+
} else if (step.state === "timed_out") {
|
|
132
|
+
return formatTimedOutWaitStep(step);
|
|
133
|
+
} else {
|
|
134
|
+
return formatWaitingWaitStep(step);
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
throw new Error("Unexpected step type. Expected 'run', 'sleep', or 'wait'.");
|
|
138
|
+
}
|
|
139
|
+
});
|
|
123
140
|
}
|
|
124
141
|
|
|
125
142
|
/**
|
|
126
|
-
*
|
|
143
|
+
* Loads the `inbound_events` row whose `claimed_by` is this wait step (`waitStepId`).
|
|
127
144
|
*
|
|
128
|
-
* @
|
|
145
|
+
* @throws If no such row exists (storage invariant broken or the step is not in a satisfied state with a claim).
|
|
129
146
|
*/
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
147
|
+
private getInboundEventForWaitStep<T extends Json | undefined = Json | undefined>(
|
|
148
|
+
stepId: WaitStepId
|
|
149
|
+
): InboundEvent<T> {
|
|
150
|
+
const [row] = this.sql
|
|
151
|
+
.exec<ClaimedInboundEvent_Row>(`SELECT * FROM inbound_events WHERE claimed_by = ? LIMIT 1`, stepId)
|
|
152
|
+
.toArray();
|
|
153
|
+
if (row === undefined) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`Wait step '${stepId}' is satisfied in durable state but no inbound_events row is claimed by this step.`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
id: row.id,
|
|
160
|
+
eventName: row.event_name,
|
|
161
|
+
payload: (row.payload === null ? undefined : JSON.parse(row.payload)) as T,
|
|
162
|
+
createdAt: new Date(row.created_at),
|
|
163
|
+
claimedBy: row.claimed_by,
|
|
164
|
+
claimedAt: new Date(row.claimed_at)
|
|
165
|
+
};
|
|
133
166
|
}
|
|
134
167
|
|
|
135
168
|
/**
|
|
@@ -157,7 +190,9 @@ export abstract class WorkflowRuntime<
|
|
|
157
190
|
return;
|
|
158
191
|
}
|
|
159
192
|
|
|
160
|
-
|
|
193
|
+
// SQL NULL encodes `undefined` (no payload); raw JSON.stringify for everything else
|
|
194
|
+
// (including JSON null, which becomes the TEXT literal 'null').
|
|
195
|
+
const serializedPayload = payload === undefined ? null : JSON.stringify(payload);
|
|
161
196
|
|
|
162
197
|
// If the workflow is paused, queue the event but do not satisfy any wait step or call run().
|
|
163
198
|
// The event will be picked up when the workflow is resumed and execution hits getOrCreateWaitStep.
|
|
@@ -184,34 +219,24 @@ export abstract class WorkflowRuntime<
|
|
|
184
219
|
.toArray();
|
|
185
220
|
|
|
186
221
|
if (step !== undefined) {
|
|
187
|
-
this.
|
|
188
|
-
|
|
222
|
+
this.ctx.storage.transactionSync(() => {
|
|
223
|
+
this.sql.exec(
|
|
224
|
+
`INSERT INTO inbound_events (event_name, payload, claimed_by, claimed_at)
|
|
225
|
+
VALUES (?, ?, ?, CAST(unixepoch('subsecond') * 1000 AS INTEGER))`,
|
|
226
|
+
event,
|
|
227
|
+
serializedPayload,
|
|
228
|
+
step.id
|
|
229
|
+
);
|
|
230
|
+
this.sql.exec(
|
|
231
|
+
`UPDATE steps
|
|
189
232
|
SET state = 'satisfied',
|
|
190
|
-
|
|
191
|
-
resolved_at = CAST(unixepoch('subsecond') * 1000 AS INTEGER),
|
|
192
|
-
timeout_at = NULL
|
|
233
|
+
resolved_at = CAST(unixepoch('subsecond') * 1000 AS INTEGER)
|
|
193
234
|
WHERE id = ?
|
|
194
235
|
AND type = 'wait'
|
|
195
236
|
AND state = 'waiting'`,
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
this.sql.exec(
|
|
201
|
-
`INSERT INTO step_events (step_id, type, payload)
|
|
202
|
-
VALUES (?, ?, ?)`,
|
|
203
|
-
step.id,
|
|
204
|
-
"wait_satisfied",
|
|
205
|
-
serializedPayload
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
this.sql.exec(
|
|
209
|
-
`INSERT INTO inbound_events (event_name, payload, claimed_by, claimed_at)
|
|
210
|
-
VALUES (?, ?, ?, CAST(unixepoch('subsecond') * 1000 AS INTEGER))`,
|
|
211
|
-
event,
|
|
212
|
-
serializedPayload,
|
|
213
|
-
step.id
|
|
214
|
-
);
|
|
237
|
+
step.id
|
|
238
|
+
);
|
|
239
|
+
});
|
|
215
240
|
|
|
216
241
|
await this.run();
|
|
217
242
|
} else {
|
|
@@ -280,56 +305,49 @@ export abstract class WorkflowRuntime<
|
|
|
280
305
|
|
|
281
306
|
// Schedule another safety alarm if the run loop is still active.
|
|
282
307
|
if (this.#isRunLoopActive) {
|
|
283
|
-
await this.ctx.storage.setAlarm(Date.now() +
|
|
308
|
+
await this.ctx.storage.setAlarm(Date.now() + 30 * 60 * 1000); // 30 minutes
|
|
284
309
|
} else {
|
|
285
310
|
await this.run();
|
|
286
311
|
}
|
|
287
312
|
}
|
|
288
313
|
|
|
289
314
|
/**
|
|
290
|
-
* Creates a new workflow instance and pins the
|
|
291
|
-
* return early. Otherwise, it will pin the
|
|
292
|
-
* pinned to a different version, it will throw an error.
|
|
315
|
+
* Creates a new workflow instance and pins the input. If the workflow is in a terminal state or paused, it will
|
|
316
|
+
* return early. Otherwise, it will pin the input the first time the instance is initialized and start execution.
|
|
293
317
|
*
|
|
294
|
-
* @param
|
|
295
|
-
*
|
|
296
|
-
* @param options.input - The input to the workflow instance. This will be passed to the workflow definition as the
|
|
297
|
-
* `input` property.
|
|
318
|
+
* @param input - The input to the workflow instance. This will be passed to the workflow definition as the `input`
|
|
319
|
+
* property.
|
|
298
320
|
*/
|
|
299
|
-
public async create(
|
|
321
|
+
public async create(...args: undefined extends TInput ? [input?: TInput] : [input: TInput]): Promise<void> {
|
|
322
|
+
const input = args[0];
|
|
300
323
|
if (this.isTerminalStatus(this.#status)) return;
|
|
301
324
|
if (this.#status === "paused") return;
|
|
302
325
|
|
|
303
|
-
const version = options.definitionVersion;
|
|
304
326
|
let metadata = this.sql
|
|
305
|
-
.exec<Pick<WorkflowMetadata_Row
|
|
306
|
-
"SELECT
|
|
327
|
+
.exec<Pick<WorkflowMetadata_Row, "status" | "definition_input">>(
|
|
328
|
+
"SELECT status, definition_input FROM workflow_metadata WHERE id = 1"
|
|
307
329
|
)
|
|
308
330
|
.one();
|
|
309
331
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
`Workflow definition version is already pinned to '${metadata.definition_version}' and cannot be changed to '${version}'.`
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// If the workflow is not yet pinned to a definition version, we pin it to the new version and set the input.
|
|
317
|
-
if (metadata.definition_version === null) {
|
|
332
|
+
// If the workflow is not yet initialized, pin the input. `undefined` is encoded as SQL NULL.
|
|
333
|
+
if (metadata.status === "pending") {
|
|
318
334
|
metadata = this.sql
|
|
319
|
-
.exec<Pick<WorkflowMetadata_Row
|
|
335
|
+
.exec<Pick<WorkflowMetadata_Row, "status" | "definition_input">>(
|
|
320
336
|
`UPDATE workflow_metadata
|
|
321
|
-
SET
|
|
337
|
+
SET status = 'initialized',
|
|
322
338
|
definition_input = ?,
|
|
323
339
|
updated_at = CAST(unixepoch('subsecond') * 1000 AS INTEGER)
|
|
324
|
-
WHERE id = 1
|
|
325
|
-
|
|
326
|
-
|
|
340
|
+
WHERE id = 1
|
|
341
|
+
AND status = 'pending'
|
|
342
|
+
RETURNING status, definition_input`,
|
|
343
|
+
input === undefined ? null : JSON.stringify(input)
|
|
327
344
|
)
|
|
328
345
|
.one();
|
|
329
346
|
}
|
|
330
347
|
|
|
331
|
-
this.#
|
|
332
|
-
this.#definitionInput =
|
|
348
|
+
this.#status = metadata.status;
|
|
349
|
+
this.#definitionInput =
|
|
350
|
+
metadata.definition_input === null ? undefined : (JSON.parse(metadata.definition_input) as TInput);
|
|
333
351
|
|
|
334
352
|
await this.run();
|
|
335
353
|
}
|
|
@@ -338,11 +356,10 @@ export abstract class WorkflowRuntime<
|
|
|
338
356
|
if (this.isTerminalStatus(this.#status)) return;
|
|
339
357
|
if (this.#status === "paused") return;
|
|
340
358
|
|
|
341
|
-
if (this.#
|
|
359
|
+
if (this.#status === "pending") return;
|
|
342
360
|
|
|
343
361
|
if (this.#status !== "running") {
|
|
344
362
|
this.#setStatus({ type: "running" });
|
|
345
|
-
this.#status = "running";
|
|
346
363
|
|
|
347
364
|
if (this.onStatusChange_experimental !== undefined) {
|
|
348
365
|
await this.onStatusChange_experimental("running");
|
|
@@ -352,7 +369,7 @@ export abstract class WorkflowRuntime<
|
|
|
352
369
|
if (this.#isRunLoopActive) return;
|
|
353
370
|
|
|
354
371
|
const requestId = crypto.randomUUID();
|
|
355
|
-
const context = new WorkflowRuntimeContext(this.ctx.storage
|
|
372
|
+
const context = new WorkflowRuntimeContext(this.ctx.storage);
|
|
356
373
|
|
|
357
374
|
this.#isRunLoopActive = true;
|
|
358
375
|
|
|
@@ -378,15 +395,11 @@ export abstract class WorkflowRuntime<
|
|
|
378
395
|
}
|
|
379
396
|
|
|
380
397
|
try {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
throw new Error(
|
|
384
|
-
"Workflow definition version has not been initialized. Call 'start()' before running the workflow."
|
|
385
|
-
);
|
|
398
|
+
if (this.#status === "pending") {
|
|
399
|
+
throw new Error("Workflow input has not been initialized. Call 'create()' before running the workflow.");
|
|
386
400
|
}
|
|
387
401
|
|
|
388
|
-
const
|
|
389
|
-
const executor = definition({
|
|
402
|
+
const executor = this.definition({
|
|
390
403
|
props: {
|
|
391
404
|
runtimeInstanceId: this.ctx.id.toString(),
|
|
392
405
|
requestId,
|
|
@@ -428,7 +441,12 @@ export abstract class WorkflowRuntime<
|
|
|
428
441
|
if (result.resume.type === "immediate") continue;
|
|
429
442
|
|
|
430
443
|
// A 'suspended' resume hint indicates that the workflow should suspend itself and wait for the next alarm or inbound event to resume.
|
|
431
|
-
if (result.resume.type === "suspended")
|
|
444
|
+
if (result.resume.type === "suspended") {
|
|
445
|
+
if (result.resume.wakeAt !== undefined) {
|
|
446
|
+
await this.ctx.storage.setAlarm(result.resume.wakeAt);
|
|
447
|
+
}
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
432
450
|
|
|
433
451
|
break;
|
|
434
452
|
} catch (error) {
|
|
@@ -476,201 +494,130 @@ export abstract class WorkflowRuntime<
|
|
|
476
494
|
export class WorkflowRuntimeContext extends RpcTarget {
|
|
477
495
|
private readonly storage: DurableObjectStorage;
|
|
478
496
|
private readonly sql: SqlStorage;
|
|
479
|
-
private readonly requestId?: string;
|
|
480
497
|
private static readonly BACKOFF_DELAYS = [250, 500, 1_000, 2_000, 4_000, 8_000, 10_000] as const;
|
|
481
498
|
|
|
482
499
|
private static readonly DEFAULT_MAX_ATTEMPTS = 3;
|
|
483
500
|
|
|
484
|
-
constructor(storage: DurableObjectStorage
|
|
501
|
+
constructor(storage: DurableObjectStorage) {
|
|
485
502
|
super();
|
|
486
503
|
this.storage = storage;
|
|
487
504
|
this.sql = storage.sql;
|
|
488
|
-
this.requestId = options?.requestId;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
public async getOrCreateStep(
|
|
492
|
-
id: RunStepId,
|
|
493
|
-
options: { type: "run"; maxAttempts?: number | null; parentStepId: RunStepId | null }
|
|
494
|
-
): Promise<RunStep>;
|
|
495
|
-
public async getOrCreateStep(
|
|
496
|
-
id: SleepStepId,
|
|
497
|
-
options: { type: "sleep"; wakeAt: Date; parentStepId: RunStepId | null }
|
|
498
|
-
): Promise<SleepStep>;
|
|
499
|
-
public async getOrCreateStep(
|
|
500
|
-
id: WaitStepId,
|
|
501
|
-
options: { type: "wait"; eventName: string; timeoutAt?: Date; parentStepId: RunStepId | null }
|
|
502
|
-
): Promise<WaitStep>;
|
|
503
|
-
public async getOrCreateStep(
|
|
504
|
-
id: RunStepId | SleepStepId | WaitStepId,
|
|
505
|
-
options:
|
|
506
|
-
| { type: "run"; maxAttempts?: number | null; parentStepId: RunStepId | null }
|
|
507
|
-
| { type: "sleep"; wakeAt: Date; parentStepId: RunStepId | null }
|
|
508
|
-
| { type: "wait"; eventName: string; timeoutAt?: Date; parentStepId: RunStepId | null }
|
|
509
|
-
): Promise<Step> {
|
|
510
|
-
try {
|
|
511
|
-
if (options.type === "run") {
|
|
512
|
-
return await this.getOrCreateRunStep(id as RunStepId, {
|
|
513
|
-
maxAttempts: options?.maxAttempts,
|
|
514
|
-
parentStepId: options.parentStepId
|
|
515
|
-
});
|
|
516
|
-
} else if (options.type === "sleep") {
|
|
517
|
-
return await this.getOrCreateSleepStep(id as SleepStepId, {
|
|
518
|
-
wakeAt: options.wakeAt,
|
|
519
|
-
parentStepId: options.parentStepId
|
|
520
|
-
});
|
|
521
|
-
} else {
|
|
522
|
-
return await this.getOrCreateWaitStep(id as WaitStepId, {
|
|
523
|
-
eventName: options.eventName,
|
|
524
|
-
timeoutAt: options.timeoutAt,
|
|
525
|
-
parentStepId: options.parentStepId
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
} catch (error) {
|
|
529
|
-
console.error(error instanceof Error ? error : new Error(String(error)), { requestId: this.requestId });
|
|
530
|
-
if (error instanceof Error && isSqliteInvariantViolation(error.message)) {
|
|
531
|
-
throw new WorkflowInvariantError(error.message);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// All other errors are considered to be infrastructure/critical errors and may cause the DO to be reset
|
|
535
|
-
// The following are examples of such errors:
|
|
536
|
-
// 'SQLITE_FULL', // Database or disk is full
|
|
537
|
-
// 'SQLITE_IOERR', // I/O error
|
|
538
|
-
// 'SQLITE_BUSY', // Database is locked
|
|
539
|
-
// 'SQLITE_NOMEM', // Out of memory
|
|
540
|
-
// 'SQLITE_INTERRUPT', // Operation interrupted
|
|
541
|
-
// 'SQLITE_CORRUPT', // Database file is corrupted
|
|
542
|
-
// 'SQLITE_CANTOPEN', // Cannot open database file
|
|
543
|
-
throw error;
|
|
544
|
-
}
|
|
545
505
|
}
|
|
546
506
|
|
|
547
507
|
/**
|
|
548
|
-
*
|
|
549
|
-
* typically `run` in `running` or `pending`, `sleep`/`wait` in `waiting`, or a successful-but-not-failed child row
|
|
550
|
-
* (`succeeded` run, `satisfied` wait, `elapsed` sleep) while the parent has not yet recorded its own outcome.
|
|
508
|
+
* Loads the `inbound_events` row whose `claimed_by` is this wait step (`waitStepId`).
|
|
551
509
|
*
|
|
552
|
-
*
|
|
510
|
+
* @throws If no such row exists (storage invariant broken or the step is not in a satisfied state with a claim).
|
|
553
511
|
*/
|
|
554
|
-
|
|
555
|
-
const
|
|
556
|
-
.exec<
|
|
557
|
-
`SELECT 1 AS x FROM steps
|
|
558
|
-
WHERE parent_step_id = ?
|
|
559
|
-
AND state NOT IN ('failed', 'timed_out')
|
|
560
|
-
LIMIT 1`,
|
|
561
|
-
stepId
|
|
562
|
-
)
|
|
512
|
+
private getInboundEventForWaitStep<T extends Json | undefined>(stepId: WaitStepId): InboundEvent<T> {
|
|
513
|
+
const [row] = this.sql
|
|
514
|
+
.exec<ClaimedInboundEvent_Row>(`SELECT * FROM inbound_events WHERE claimed_by = ? LIMIT 1`, stepId)
|
|
563
515
|
.toArray();
|
|
564
|
-
|
|
516
|
+
if (row === undefined) {
|
|
517
|
+
throw new Error(
|
|
518
|
+
`Wait step '${stepId}' is satisfied in durable state but no inbound_events row is claimed by this step.`
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
return {
|
|
522
|
+
id: row.id,
|
|
523
|
+
eventName: row.event_name,
|
|
524
|
+
payload: (row.payload === null ? undefined : JSON.parse(row.payload)) as T,
|
|
525
|
+
createdAt: new Date(row.created_at),
|
|
526
|
+
claimedBy: row.claimed_by,
|
|
527
|
+
claimedAt: new Date(row.claimed_at)
|
|
528
|
+
};
|
|
565
529
|
}
|
|
566
530
|
|
|
567
|
-
|
|
531
|
+
getOrCreateRunStep(
|
|
568
532
|
id: RunStepId,
|
|
569
|
-
options: {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
if (existing === undefined) {
|
|
577
|
-
const inserted = this.sql
|
|
578
|
-
.exec<RunStep_Row>(
|
|
579
|
-
`INSERT INTO steps (id, type, state, attempt_count, max_attempts, next_attempt_at, parent_step_id) VALUES (?, 'run', 'pending', 0, ?, CAST(unixepoch('subsecond') * 1000 AS INTEGER), ?) RETURNING *`,
|
|
580
|
-
id,
|
|
581
|
-
maxAttempts,
|
|
582
|
-
options.parentStepId
|
|
583
|
-
)
|
|
584
|
-
.one();
|
|
585
|
-
|
|
586
|
-
return formatStep(inserted);
|
|
587
|
-
} else {
|
|
588
|
-
// If the step exists and is in 'pending' state, we update the alarm to wake at the correct time.
|
|
589
|
-
if (existing.state === "pending" && Date.now() < existing.next_attempt_at) {
|
|
590
|
-
await transaction.setAlarm(existing.next_attempt_at);
|
|
591
|
-
}
|
|
592
|
-
return formatStep(existing);
|
|
593
|
-
}
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
private async getOrCreateSleepStep(
|
|
598
|
-
id: SleepStepId,
|
|
599
|
-
options: { wakeAt: Date; parentStepId: RunStepId | null }
|
|
600
|
-
): Promise<SleepStep> {
|
|
601
|
-
return await this.storage.transaction(async (transaction) => {
|
|
602
|
-
const [existing] = this.sql
|
|
603
|
-
.exec<SleepStep_Row>("SELECT * FROM steps WHERE id = ? AND type = 'sleep'", id)
|
|
604
|
-
.toArray();
|
|
605
|
-
if (existing !== undefined) {
|
|
606
|
-
// If the step exists and is in 'waiting' state, we update the alarm to wake at the correct time.
|
|
607
|
-
if (existing.state === "waiting" && Date.now() < existing.wake_at) {
|
|
608
|
-
await transaction.setAlarm(existing.wake_at);
|
|
609
|
-
}
|
|
610
|
-
return formatStep(existing);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// If the step does not exist, we create it, set it to 'waiting' state and set the alarm to wake at the correct time.
|
|
614
|
-
const wakeAt = options.wakeAt.getTime();
|
|
533
|
+
options: {
|
|
534
|
+
maxAttempts?: number | null;
|
|
535
|
+
parentStepId: RunStepId | null;
|
|
536
|
+
}
|
|
537
|
+
): RunStep & { attempts: RunStepAttempt[] } {
|
|
538
|
+
const [existing] = this.sql.exec<RunStep_Row>("SELECT * FROM steps WHERE id = ? AND type = 'run'", id).toArray();
|
|
539
|
+
if (existing === undefined) {
|
|
615
540
|
const inserted = this.sql
|
|
616
|
-
.exec<
|
|
617
|
-
`INSERT INTO steps (id, type,
|
|
541
|
+
.exec<RunStep_Row>(
|
|
542
|
+
`INSERT INTO steps (id, type, parent_step_id, max_attempts) VALUES (?, 'run', ?, ?) RETURNING *`,
|
|
618
543
|
id,
|
|
619
|
-
|
|
620
|
-
options.
|
|
544
|
+
options.parentStepId,
|
|
545
|
+
options.maxAttempts ?? WorkflowRuntimeContext.DEFAULT_MAX_ATTEMPTS
|
|
621
546
|
)
|
|
622
547
|
.one();
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
548
|
+
return { ...formatRunStep(inserted), attempts: [] };
|
|
549
|
+
} else {
|
|
550
|
+
const attempts = this.sql
|
|
551
|
+
.exec<RunStepAttempt_Row>(
|
|
552
|
+
`SELECT * FROM run_step_attempts WHERE step_id = ? ORDER BY started_at ASC, id ASC`,
|
|
553
|
+
id
|
|
554
|
+
)
|
|
555
|
+
.toArray();
|
|
556
|
+
|
|
557
|
+
return {
|
|
558
|
+
...formatRunStep(existing),
|
|
559
|
+
attempts: attempts.map((attempt) => formatRunStepAttempt(attempt))
|
|
560
|
+
};
|
|
561
|
+
}
|
|
627
562
|
}
|
|
628
563
|
|
|
629
|
-
|
|
564
|
+
getOrCreateSleepStep(id: SleepStepId, options: { wakeAt: Date; parentStepId: RunStepId | null }): SleepStep {
|
|
565
|
+
const [existing] = this.sql
|
|
566
|
+
.exec<SleepStep_Row>("SELECT * FROM steps WHERE id = ? AND type = 'sleep'", id)
|
|
567
|
+
.toArray();
|
|
568
|
+
if (existing !== undefined) {
|
|
569
|
+
return formatSleepStep(existing);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const wakeAt = options.wakeAt.getTime();
|
|
573
|
+
const inserted = this.sql
|
|
574
|
+
.exec<SleepStep_Row>(
|
|
575
|
+
`INSERT INTO steps (id, type, state, target_wake_at, parent_step_id) VALUES (?, 'sleep', 'waiting', ?, ?) RETURNING *`,
|
|
576
|
+
id,
|
|
577
|
+
wakeAt,
|
|
578
|
+
options.parentStepId
|
|
579
|
+
)
|
|
580
|
+
.one();
|
|
581
|
+
return formatSleepStep(inserted);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
getOrCreateWaitStep<T extends Json | undefined>(
|
|
630
585
|
id: WaitStepId,
|
|
631
586
|
options: { eventName: string; timeoutAt?: Date; parentStepId: RunStepId | null }
|
|
632
|
-
):
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
if (existing
|
|
639
|
-
return
|
|
587
|
+
): WaitStep<T> {
|
|
588
|
+
const [existing] = this.sql.exec<WaitStep_Row>("SELECT * FROM steps WHERE id = ? AND type = 'wait'", id).toArray();
|
|
589
|
+
// If the step exists and isn't in 'waiting' state (i.e. in terminal state of 'satisfied' or 'timed_out'), we return the step as is as no further action is needed.
|
|
590
|
+
if (existing !== undefined && existing.state !== "waiting") {
|
|
591
|
+
if (existing.state === "satisfied") {
|
|
592
|
+
return formatSatisfiedWaitStep<T>(existing, this.getInboundEventForWaitStep<T>(existing.id).payload);
|
|
593
|
+
} else if (existing.state === "timed_out") {
|
|
594
|
+
return formatTimedOutWaitStep(existing);
|
|
640
595
|
}
|
|
596
|
+
}
|
|
641
597
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
598
|
+
let waiting: Extract<WaitStep_Row, { state: "waiting" }>;
|
|
599
|
+
if (existing !== undefined) {
|
|
600
|
+
waiting = existing;
|
|
601
|
+
} else {
|
|
602
|
+
waiting = this.sql
|
|
603
|
+
.exec<Extract<WaitStep_Row, { state: "waiting" }>>(
|
|
604
|
+
`
|
|
649
605
|
INSERT INTO steps (id, type, state, event_name, timeout_at, parent_step_id)
|
|
650
606
|
VALUES (?, 'wait', 'waiting', ?, ?, ?)
|
|
651
607
|
RETURNING *
|
|
652
608
|
`,
|
|
653
|
-
id,
|
|
654
|
-
options.eventName,
|
|
655
|
-
options.timeoutAt !== undefined ? options.timeoutAt.getTime() : null,
|
|
656
|
-
options.parentStepId
|
|
657
|
-
)
|
|
658
|
-
.one();
|
|
659
|
-
this.sql.exec(
|
|
660
|
-
`INSERT INTO step_events (step_id, type, event_name, timeout_at) VALUES (?, ?, ?, ?)`,
|
|
661
609
|
id,
|
|
662
|
-
"wait_waiting",
|
|
663
610
|
options.eventName,
|
|
664
|
-
options.timeoutAt !== undefined ? options.timeoutAt.getTime() : null
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
611
|
+
options.timeoutAt !== undefined ? options.timeoutAt.getTime() : null,
|
|
612
|
+
options.parentStepId
|
|
613
|
+
)
|
|
614
|
+
.one();
|
|
615
|
+
}
|
|
669
616
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
617
|
+
// Attempt to claim any inbound event that is not claimed yet for the given event name.
|
|
618
|
+
const [claimed] = this.sql
|
|
619
|
+
.exec<{ id: string; payload: string | null }>(
|
|
620
|
+
`
|
|
674
621
|
UPDATE inbound_events
|
|
675
622
|
SET claimed_by = ?,
|
|
676
623
|
claimed_at = CAST(unixepoch('subsecond') * 1000 AS INTEGER)
|
|
@@ -685,280 +632,228 @@ export class WorkflowRuntimeContext extends RpcTarget {
|
|
|
685
632
|
AND claimed_by IS NULL
|
|
686
633
|
RETURNING id, payload
|
|
687
634
|
`,
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
635
|
+
id,
|
|
636
|
+
options.eventName
|
|
637
|
+
)
|
|
638
|
+
.toArray();
|
|
692
639
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
`
|
|
640
|
+
if (claimed !== undefined) {
|
|
641
|
+
const satisfied = this.sql
|
|
642
|
+
.exec<SatisfiedWaitStep_Row>(
|
|
643
|
+
`
|
|
698
644
|
UPDATE steps
|
|
699
645
|
SET state = 'satisfied',
|
|
700
|
-
|
|
701
|
-
resolved_at = CAST(unixepoch('subsecond') * 1000 AS INTEGER),
|
|
702
|
-
timeout_at = NULL
|
|
646
|
+
resolved_at = CAST(unixepoch('subsecond') * 1000 AS INTEGER)
|
|
703
647
|
WHERE id = ?
|
|
704
648
|
AND type = 'wait'
|
|
705
649
|
AND state = 'waiting'
|
|
706
650
|
RETURNING *
|
|
707
651
|
`,
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
this.sql.exec(
|
|
714
|
-
`
|
|
715
|
-
INSERT INTO step_events (step_id, type, payload)
|
|
716
|
-
VALUES (?, ?, ?)
|
|
717
|
-
`,
|
|
718
|
-
id,
|
|
719
|
-
"wait_satisfied",
|
|
720
|
-
event.payload
|
|
721
|
-
);
|
|
652
|
+
id
|
|
653
|
+
)
|
|
654
|
+
.one();
|
|
655
|
+
return formatSatisfiedWaitStep<T>(satisfied, claimed.payload === null ? undefined : JSON.parse(claimed.payload));
|
|
656
|
+
}
|
|
722
657
|
|
|
723
|
-
|
|
724
|
-
}
|
|
725
|
-
// If no queued inbound event was found, we return the step as is.
|
|
726
|
-
else {
|
|
727
|
-
if (timeoutAt !== null && Date.now() < timeoutAt) {
|
|
728
|
-
await transaction.setAlarm(timeoutAt);
|
|
729
|
-
} else {
|
|
730
|
-
await transaction.deleteAlarm();
|
|
731
|
-
}
|
|
732
|
-
return formatStep(waiting);
|
|
733
|
-
}
|
|
734
|
-
});
|
|
658
|
+
return formatWaitingWaitStep(waiting);
|
|
735
659
|
}
|
|
736
|
-
async handleRunAttemptEvent(
|
|
737
|
-
id: RunStepId,
|
|
738
|
-
event:
|
|
739
|
-
| { type: "running"; attemptCount: number }
|
|
740
|
-
| { type: "succeeded"; attemptCount: number; result: string }
|
|
741
|
-
| {
|
|
742
|
-
type: "failed";
|
|
743
|
-
attemptCount: number;
|
|
744
|
-
errorMessage: string;
|
|
745
|
-
errorName?: string;
|
|
746
|
-
isNonRetryableStepError?: boolean;
|
|
747
|
-
}
|
|
748
|
-
): Promise<void> {
|
|
749
|
-
try {
|
|
750
|
-
await this.storage.transaction(async (transaction) => {
|
|
751
|
-
const [existing] = this.sql
|
|
752
|
-
.exec<RunStep_Row>("SELECT * FROM steps WHERE id = ? AND type = 'run'", id)
|
|
753
|
-
.toArray();
|
|
754
|
-
if (existing === undefined) {
|
|
755
|
-
throw new WorkflowInvariantError(`Step '${id}' of type 'run' not found.`);
|
|
756
|
-
}
|
|
757
660
|
|
|
758
|
-
|
|
661
|
+
/**
|
|
662
|
+
* True if this run step has at least one **direct** child that is still in progress.
|
|
663
|
+
*/
|
|
664
|
+
hasInProgressChildSteps(stepId: RunStepId): boolean {
|
|
665
|
+
const rows = this.sql
|
|
666
|
+
.exec<{ x: number }>(
|
|
667
|
+
`SELECT 1 AS x FROM steps c
|
|
668
|
+
WHERE c.parent_step_id = ?
|
|
669
|
+
AND (
|
|
670
|
+
(c.type = 'sleep' AND c.state IN ('waiting', 'elapsed'))
|
|
671
|
+
OR (c.type = 'wait' AND c.state IN ('waiting', 'satisfied'))
|
|
672
|
+
OR (
|
|
673
|
+
c.type = 'run'
|
|
674
|
+
AND NOT EXISTS (
|
|
675
|
+
SELECT 1
|
|
676
|
+
FROM run_step_attempts a
|
|
677
|
+
WHERE a.step_id = c.id
|
|
678
|
+
AND a.state = 'failed'
|
|
679
|
+
AND a.next_attempt_at IS NULL
|
|
680
|
+
AND a.id = (
|
|
681
|
+
SELECT a2.id
|
|
682
|
+
FROM run_step_attempts a2
|
|
683
|
+
WHERE a2.step_id = c.id
|
|
684
|
+
ORDER BY a2.started_at DESC, a2.id DESC
|
|
685
|
+
LIMIT 1
|
|
686
|
+
)
|
|
687
|
+
)
|
|
688
|
+
)
|
|
689
|
+
)
|
|
690
|
+
LIMIT 1`,
|
|
691
|
+
stepId
|
|
692
|
+
)
|
|
693
|
+
.toArray();
|
|
694
|
+
return rows.length > 0;
|
|
695
|
+
}
|
|
759
696
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
`Unexpected next attempt at for run step '${id}'. Expected a NULL value or a value that is in the past but got ${new Date(existing.next_attempt_at).toISOString()}.`
|
|
769
|
-
);
|
|
770
|
-
}
|
|
771
|
-
if (existing.attempt_count !== attemptCount - 1) {
|
|
772
|
-
throw new WorkflowInvariantError(
|
|
773
|
-
`Unexpected attempt count for run step '${id}'. Expected ${attemptCount - 1} but got ${existing.attempt_count}.`
|
|
774
|
-
);
|
|
775
|
-
}
|
|
776
|
-
// Update the step to the 'running' state and insert an `attempt_started` step_events row.
|
|
777
|
-
this.sql.exec(
|
|
778
|
-
`UPDATE steps SET state = 'running', attempt_count = ?, next_attempt_at = NULL WHERE id = ?`,
|
|
779
|
-
attemptCount,
|
|
780
|
-
id
|
|
781
|
-
);
|
|
782
|
-
this.sql.exec(
|
|
783
|
-
"INSERT INTO step_events (step_id, type, attempt_number) VALUES (?, ?, ?)",
|
|
784
|
-
id,
|
|
785
|
-
"attempt_started",
|
|
786
|
-
attemptCount
|
|
787
|
-
);
|
|
788
|
-
} else if (event.type === "succeeded") {
|
|
789
|
-
if (existing.state !== "running") {
|
|
790
|
-
throw new WorkflowInvariantError(
|
|
791
|
-
`Unexpected state for run step '${id}'. Expected 'running' but got ${existing.state}.`
|
|
792
|
-
);
|
|
793
|
-
}
|
|
794
|
-
if (existing.attempt_count !== attemptCount) {
|
|
795
|
-
throw new WorkflowInvariantError(
|
|
796
|
-
`Unexpected attempt count for run step '${id}'. Expected ${attemptCount} but got ${existing.attempt_count}.`
|
|
797
|
-
);
|
|
798
|
-
}
|
|
799
|
-
// Update the step to the 'succeeded' state and insert an `attempt_succeeded` step_events row.
|
|
800
|
-
this.sql.exec(
|
|
801
|
-
`UPDATE steps SET state = 'succeeded', result = ?, resolved_at = CAST(unixepoch('subsecond') * 1000 AS INTEGER) WHERE id = ?`,
|
|
802
|
-
event.result,
|
|
803
|
-
id
|
|
804
|
-
);
|
|
805
|
-
this.sql.exec(
|
|
806
|
-
"INSERT INTO step_events (step_id, type, attempt_number, result) VALUES (?, ?, ?, ?)",
|
|
807
|
-
id,
|
|
808
|
-
"attempt_succeeded",
|
|
809
|
-
attemptCount,
|
|
810
|
-
event.result
|
|
811
|
-
);
|
|
812
|
-
} else if (event.type === "failed") {
|
|
813
|
-
if (existing.state !== "running") {
|
|
814
|
-
throw new WorkflowInvariantError(
|
|
815
|
-
`Unexpected state for run step '${id}'. Expected 'running' but got ${existing.state}.`
|
|
816
|
-
);
|
|
817
|
-
}
|
|
697
|
+
handleRunAttemptStarted(stepId: RunStepId): StartedRunStepAttempt {
|
|
698
|
+
// If a run step with the given id does not exist, we throw a 'WorkflowInvariantError' indicating that the step was not found.
|
|
699
|
+
const [existing] = this.sql
|
|
700
|
+
.exec<RunStep_Row>(`SELECT * FROM steps WHERE id = ? AND type = 'run'`, stepId)
|
|
701
|
+
.toArray();
|
|
702
|
+
if (existing === undefined) {
|
|
703
|
+
throw new Error(`Run step '${stepId}' not found.`);
|
|
704
|
+
}
|
|
818
705
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
706
|
+
// Get the last attempt for the step.
|
|
707
|
+
const [lastAttempt] = this.sql
|
|
708
|
+
.exec<RunStepAttempt_Row>(
|
|
709
|
+
"SELECT * FROM run_step_attempts WHERE step_id = ? ORDER BY started_at DESC, id DESC",
|
|
710
|
+
stepId
|
|
711
|
+
)
|
|
712
|
+
.toArray();
|
|
824
713
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
event.isNonRetryableStepError
|
|
829
|
-
) {
|
|
830
|
-
this.sql.exec(
|
|
831
|
-
`UPDATE steps SET state = 'failed', error_message = ?, error_name = ?, resolved_at = CAST(unixepoch('subsecond') * 1000 AS INTEGER) WHERE id = ?`,
|
|
832
|
-
event.errorMessage,
|
|
833
|
-
event.errorName ?? null,
|
|
834
|
-
id
|
|
835
|
-
);
|
|
836
|
-
|
|
837
|
-
// Insert an `attempt_failed` step_events row
|
|
838
|
-
this.sql.exec(
|
|
839
|
-
"INSERT INTO step_events (step_id, type, attempt_number, error_message, error_name) VALUES (?, ?, ?, ?, ?)",
|
|
840
|
-
id,
|
|
841
|
-
"attempt_failed",
|
|
842
|
-
attemptCount,
|
|
843
|
-
event.errorMessage,
|
|
844
|
-
event.errorName ?? null
|
|
845
|
-
);
|
|
846
|
-
}
|
|
847
|
-
// Otherwise (if the step hasn't reached the maximum number of attempts), we mark the step as 'pending'
|
|
848
|
-
// and update 'next_attempt_at' to the next backoff time and set the alarm to wake up at the same time.
|
|
849
|
-
else {
|
|
850
|
-
const backoff =
|
|
851
|
-
WorkflowRuntimeContext.BACKOFF_DELAYS[attemptCount - 1] ??
|
|
852
|
-
(WorkflowRuntimeContext.BACKOFF_DELAYS[WorkflowRuntimeContext.BACKOFF_DELAYS.length - 1] as number);
|
|
853
|
-
const nextAttemptAt = Date.now() + backoff;
|
|
854
|
-
this.sql.exec(`UPDATE steps SET state = 'pending', next_attempt_at = ? WHERE id = ?`, nextAttemptAt, id);
|
|
855
|
-
this.sql.exec(
|
|
856
|
-
"INSERT INTO step_events (step_id, type, attempt_number, error_message, error_name, next_attempt_at) VALUES (?, ?, ?, ?, ?, ?)",
|
|
857
|
-
id,
|
|
858
|
-
"attempt_failed",
|
|
859
|
-
attemptCount,
|
|
860
|
-
event.errorMessage,
|
|
861
|
-
event.errorName ?? null,
|
|
862
|
-
nextAttemptAt
|
|
863
|
-
);
|
|
864
|
-
await transaction.setAlarm(nextAttemptAt);
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
});
|
|
868
|
-
} catch (error) {
|
|
869
|
-
console.error(error instanceof Error ? error : new Error(String(error)), { requestId: this.requestId });
|
|
870
|
-
if (error instanceof Error && isSqliteInvariantViolation(error.message)) {
|
|
871
|
-
throw new WorkflowInvariantError(error.message);
|
|
872
|
-
}
|
|
873
|
-
throw error;
|
|
714
|
+
// If the last attempt has been started, we throw a 'WorkflowInvariantError' indicating that the attempt is already in progress.
|
|
715
|
+
if (lastAttempt !== undefined && lastAttempt.state === "started") {
|
|
716
|
+
throw new Error(`Attempt '${lastAttempt.id}' for run step '${stepId}' is already in progress.`);
|
|
874
717
|
}
|
|
718
|
+
|
|
719
|
+
// Insert a new attempt for the step and return the new attempt
|
|
720
|
+
const attempt = this.sql
|
|
721
|
+
.exec<StartedRunStepAttempt_Row>(
|
|
722
|
+
`INSERT INTO run_step_attempts (step_id, state) VALUES (?, 'started') RETURNING *`,
|
|
723
|
+
stepId
|
|
724
|
+
)
|
|
725
|
+
.one();
|
|
726
|
+
return formatRunStepAttempt(attempt);
|
|
875
727
|
}
|
|
876
728
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
729
|
+
handleRunAttemptFailed(
|
|
730
|
+
stepId: RunStepId,
|
|
731
|
+
result: {
|
|
732
|
+
errorMessage: string;
|
|
733
|
+
errorName?: string;
|
|
734
|
+
isNonRetryableStepError?: boolean;
|
|
735
|
+
}
|
|
736
|
+
): FailedRunStepAttempt {
|
|
737
|
+
// If a run step with the given id does not exist, we throw a 'WorkflowInvariantError' indicating that the step was not found.
|
|
738
|
+
const [existing] = this.sql
|
|
739
|
+
.exec<RunStep_Row>(`SELECT * FROM steps WHERE id = ? AND type = 'run'`, stepId)
|
|
740
|
+
.toArray();
|
|
741
|
+
if (existing === undefined) {
|
|
742
|
+
throw new Error(`Run step '${stepId}' not found.`);
|
|
743
|
+
}
|
|
885
744
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
745
|
+
// Get the last attempt for the step.
|
|
746
|
+
const attempts = this.sql
|
|
747
|
+
.exec<RunStepAttempt_Row>("SELECT * FROM run_step_attempts WHERE step_id = ? ORDER BY started_at ASC", stepId)
|
|
748
|
+
.toArray();
|
|
749
|
+
|
|
750
|
+
const lastAttempt = attempts[attempts.length - 1];
|
|
751
|
+
if (lastAttempt === undefined) {
|
|
752
|
+
throw new Error(`No attempt in progress for run step '${stepId}'.`);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (result.isNonRetryableStepError || attempts.length === existing.max_attempts) {
|
|
756
|
+
const updated = this.sql
|
|
757
|
+
.exec<Extract<RunStepAttempt_Row, { state: "failed" }>>(
|
|
758
|
+
`UPDATE run_step_attempts SET state = 'failed', ended_at = CAST(unixepoch('subsecond') * 1000 AS INTEGER), error_message = ?, error_name = ?, next_attempt_at = NULL WHERE id = ? AND state = 'started' RETURNING *`,
|
|
759
|
+
result.errorMessage,
|
|
760
|
+
result.errorName ?? null,
|
|
761
|
+
lastAttempt.id
|
|
762
|
+
)
|
|
763
|
+
.one();
|
|
764
|
+
return formatRunStepAttempt(updated);
|
|
765
|
+
} else {
|
|
766
|
+
const backoff =
|
|
767
|
+
WorkflowRuntimeContext.BACKOFF_DELAYS[attempts.length - 1] ??
|
|
768
|
+
(WorkflowRuntimeContext.BACKOFF_DELAYS[WorkflowRuntimeContext.BACKOFF_DELAYS.length - 1] as number);
|
|
769
|
+
const nextAttemptAt = Date.now() + backoff;
|
|
770
|
+
|
|
771
|
+
const updated = this.sql
|
|
772
|
+
.exec<Extract<RunStepAttempt_Row, { state: "failed" }>>(
|
|
773
|
+
`UPDATE run_step_attempts SET state = 'failed', ended_at = CAST(unixepoch('subsecond') * 1000 AS INTEGER), error_message = ?, error_name = ?, next_attempt_at = ? WHERE id = ? AND state = 'started' RETURNING *`,
|
|
774
|
+
result.errorMessage,
|
|
775
|
+
result.errorName ?? null,
|
|
776
|
+
nextAttemptAt,
|
|
777
|
+
lastAttempt.id
|
|
778
|
+
)
|
|
779
|
+
.one();
|
|
780
|
+
return formatRunStepAttempt(updated);
|
|
905
781
|
}
|
|
906
782
|
}
|
|
907
783
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
if (event.type === "timed_out") {
|
|
930
|
-
this.sql.exec(
|
|
931
|
-
"UPDATE steps SET state = 'timed_out', resolved_at = CAST(unixepoch('subsecond') * 1000 AS INTEGER), timeout_at = NULL WHERE id = ?",
|
|
932
|
-
id
|
|
933
|
-
);
|
|
934
|
-
this.sql.exec("INSERT INTO step_events (step_id, type) VALUES (?, ?)", id, "wait_timed_out");
|
|
935
|
-
}
|
|
936
|
-
});
|
|
937
|
-
} catch (error) {
|
|
938
|
-
console.error(error instanceof Error ? error : new Error(String(error)), { requestId: this.requestId });
|
|
939
|
-
if (error instanceof Error && isSqliteInvariantViolation(error.message)) {
|
|
940
|
-
throw new WorkflowInvariantError(error.message);
|
|
941
|
-
}
|
|
942
|
-
throw error;
|
|
784
|
+
/**
|
|
785
|
+
* Marks the in-flight attempt as succeeded.
|
|
786
|
+
*
|
|
787
|
+
* @param resultJson - Raw JSON string for the result value (`null` when the callback returned `undefined`). The
|
|
788
|
+
* `result_type` discriminator is derived: `null` → `'none'`, non-null → `'json'`.
|
|
789
|
+
*/
|
|
790
|
+
handleRunAttemptSucceeded(stepId: RunStepId, resultJson: string | null): SucceededRunStepAttempt {
|
|
791
|
+
const [existing] = this.sql
|
|
792
|
+
.exec<RunStep_Row>(`SELECT * FROM steps WHERE id = ? AND type = 'run'`, stepId)
|
|
793
|
+
.toArray();
|
|
794
|
+
if (existing === undefined) {
|
|
795
|
+
throw new Error(`Run step '${stepId}' not found.`);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const attempts = this.sql
|
|
799
|
+
.exec<RunStepAttempt_Row>("SELECT * FROM run_step_attempts WHERE step_id = ? ORDER BY started_at ASC", stepId)
|
|
800
|
+
.toArray();
|
|
801
|
+
|
|
802
|
+
const lastAttempt = attempts[attempts.length - 1];
|
|
803
|
+
if (lastAttempt === undefined || lastAttempt?.state !== "started") {
|
|
804
|
+
throw new Error(`No attempt in progress for run step '${stepId}'.`);
|
|
943
805
|
}
|
|
806
|
+
|
|
807
|
+
const resultType = resultJson === null ? "none" : "json";
|
|
808
|
+
const updated = this.sql
|
|
809
|
+
.exec<SucceededRunStepAttempt_Row>(
|
|
810
|
+
`UPDATE run_step_attempts SET state = 'succeeded', ended_at = CAST(unixepoch('subsecond') * 1000 AS INTEGER), result_type = ?, result_json = ? WHERE id = ? AND state = 'started' RETURNING *`,
|
|
811
|
+
resultType,
|
|
812
|
+
resultJson,
|
|
813
|
+
lastAttempt.id
|
|
814
|
+
)
|
|
815
|
+
.one();
|
|
816
|
+
return formatRunStepAttempt(updated);
|
|
944
817
|
}
|
|
945
|
-
}
|
|
946
818
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
message.includes("SQLITE_TOOBIG") // String or BLOB too large
|
|
955
|
-
);
|
|
956
|
-
}
|
|
819
|
+
handleSleepStepElapsed(id: SleepStepId): void {
|
|
820
|
+
const [existing] = this.sql
|
|
821
|
+
.exec<SleepStep_Row>("SELECT * FROM steps WHERE id = ? AND type = 'sleep'", id)
|
|
822
|
+
.toArray();
|
|
823
|
+
if (existing === undefined) {
|
|
824
|
+
throw new Error(`Step '${id}' of type 'sleep' not found.`);
|
|
825
|
+
}
|
|
957
826
|
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
this.
|
|
827
|
+
if (existing.state !== "waiting") {
|
|
828
|
+
throw new Error(`Unexpected state for sleep step '${id}'. Expected 'waiting' but got ${existing.state}.`);
|
|
829
|
+
}
|
|
830
|
+
this.sql.exec(
|
|
831
|
+
`UPDATE steps SET state = 'elapsed', resolved_at = CAST(unixepoch('subsecond') * 1000 AS INTEGER) WHERE id = ?`,
|
|
832
|
+
id
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
handleWaitStepTimedOut(id: WaitStepId): void {
|
|
837
|
+
this.storage.transactionSync(() => {
|
|
838
|
+
const [existing] = this.sql
|
|
839
|
+
.exec<WaitStep_Row>("SELECT * FROM steps WHERE id = ? AND type = 'wait'", id)
|
|
840
|
+
.toArray();
|
|
841
|
+
if (existing === undefined) {
|
|
842
|
+
throw new Error(`Step '${id}' of type 'wait' not found.`);
|
|
843
|
+
}
|
|
844
|
+
if (existing.state !== "waiting") {
|
|
845
|
+
throw new Error(`Unexpected state for wait step '${id}'. Expected 'waiting' but got ${existing.state}.`);
|
|
846
|
+
}
|
|
847
|
+
if (existing.timeout_at !== null && existing.timeout_at > Date.now()) {
|
|
848
|
+
throw new Error(
|
|
849
|
+
`Unexpected timeout at for wait step '${id}'. Expected a NULL value or a value that is in the past but got ${new Date(existing.timeout_at).toISOString()}.`
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
this.sql.exec(
|
|
853
|
+
"UPDATE steps SET state = 'timed_out', resolved_at = CAST(unixepoch('subsecond') * 1000 AS INTEGER) WHERE id = ?",
|
|
854
|
+
id
|
|
855
|
+
);
|
|
856
|
+
});
|
|
962
857
|
}
|
|
963
858
|
}
|
|
964
859
|
|
|
@@ -966,33 +861,27 @@ export type RunStepId = Brand<string, "RunStepId">;
|
|
|
966
861
|
export type SleepStepId = Brand<string, "SleepStepId">;
|
|
967
862
|
export type WaitStepId = Brand<string, "WaitStepId">;
|
|
968
863
|
|
|
969
|
-
type
|
|
864
|
+
export type RunStepAttempt = {
|
|
865
|
+
id: string;
|
|
866
|
+
stepId: RunStepId;
|
|
867
|
+
startedAt: Date;
|
|
868
|
+
} & (
|
|
869
|
+
| { state: "started" }
|
|
870
|
+
| ({ state: "succeeded"; endedAt: Date } & ({ resultType: "json"; resultJson: string } | { resultType: "none" }))
|
|
871
|
+
| { state: "failed"; errorMessage: string; errorName?: string; endedAt: Date; nextAttemptAt?: Date }
|
|
872
|
+
);
|
|
873
|
+
|
|
874
|
+
export type StartedRunStepAttempt = Extract<RunStepAttempt, { state: "started" }>;
|
|
875
|
+
export type SucceededRunStepAttempt = Extract<RunStepAttempt, { state: "succeeded" }>;
|
|
876
|
+
export type FailedRunStepAttempt = Extract<RunStepAttempt, { state: "failed" }>;
|
|
877
|
+
|
|
878
|
+
export type RunStep = {
|
|
970
879
|
type: "run";
|
|
971
880
|
id: RunStepId;
|
|
972
881
|
createdAt: Date;
|
|
973
|
-
attemptCount: number;
|
|
974
882
|
maxAttempts: number | null;
|
|
975
883
|
parentStepId: RunStepId | null;
|
|
976
|
-
}
|
|
977
|
-
| {
|
|
978
|
-
state: "pending";
|
|
979
|
-
nextAttemptAt: Date;
|
|
980
|
-
}
|
|
981
|
-
| {
|
|
982
|
-
state: "running";
|
|
983
|
-
}
|
|
984
|
-
| {
|
|
985
|
-
state: "succeeded";
|
|
986
|
-
result: string;
|
|
987
|
-
resolvedAt: Date;
|
|
988
|
-
}
|
|
989
|
-
| {
|
|
990
|
-
state: "failed";
|
|
991
|
-
errorMessage: string;
|
|
992
|
-
errorName?: string;
|
|
993
|
-
resolvedAt: Date;
|
|
994
|
-
}
|
|
995
|
-
);
|
|
884
|
+
};
|
|
996
885
|
|
|
997
886
|
type SleepStep = {
|
|
998
887
|
type: "sleep";
|
|
@@ -1010,7 +899,7 @@ type SleepStep = {
|
|
|
1010
899
|
}
|
|
1011
900
|
);
|
|
1012
901
|
|
|
1013
|
-
type WaitStep = {
|
|
902
|
+
type WaitStep<T extends Json | undefined = Json | undefined> = {
|
|
1014
903
|
type: "wait";
|
|
1015
904
|
id: WaitStepId;
|
|
1016
905
|
createdAt: Date;
|
|
@@ -1023,256 +912,171 @@ type WaitStep = {
|
|
|
1023
912
|
}
|
|
1024
913
|
| {
|
|
1025
914
|
state: "satisfied";
|
|
1026
|
-
payload:
|
|
915
|
+
payload: T;
|
|
1027
916
|
resolvedAt: Date;
|
|
917
|
+
timeoutAt?: Date;
|
|
1028
918
|
}
|
|
1029
919
|
| {
|
|
1030
920
|
state: "timed_out";
|
|
1031
921
|
resolvedAt: Date;
|
|
922
|
+
timeoutAt: Date;
|
|
1032
923
|
}
|
|
1033
924
|
);
|
|
1034
925
|
|
|
1035
|
-
type
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
function formatStep(step: SleepStep_Row): SleepStep;
|
|
1039
|
-
function formatStep(step: WaitStep_Row): WaitStep;
|
|
1040
|
-
function formatStep(step: Step_Row): Step;
|
|
1041
|
-
function formatStep(step: Step_Row): Step {
|
|
1042
|
-
switch (step.type) {
|
|
1043
|
-
case "run": {
|
|
1044
|
-
switch (step.state) {
|
|
1045
|
-
case "pending":
|
|
1046
|
-
return {
|
|
1047
|
-
type: "run",
|
|
1048
|
-
id: step.id,
|
|
1049
|
-
state: "pending",
|
|
1050
|
-
nextAttemptAt: new Date(step.next_attempt_at),
|
|
1051
|
-
createdAt: new Date(step.created_at),
|
|
1052
|
-
attemptCount: step.attempt_count,
|
|
1053
|
-
maxAttempts: step.max_attempts,
|
|
1054
|
-
parentStepId: step.parent_step_id
|
|
1055
|
-
} satisfies RunStep;
|
|
1056
|
-
case "running":
|
|
1057
|
-
return {
|
|
1058
|
-
type: "run",
|
|
1059
|
-
id: step.id,
|
|
1060
|
-
state: "running",
|
|
1061
|
-
createdAt: new Date(step.created_at),
|
|
1062
|
-
attemptCount: step.attempt_count,
|
|
1063
|
-
maxAttempts: step.max_attempts,
|
|
1064
|
-
parentStepId: step.parent_step_id
|
|
1065
|
-
} satisfies RunStep;
|
|
1066
|
-
case "succeeded":
|
|
1067
|
-
return {
|
|
1068
|
-
type: "run",
|
|
1069
|
-
id: step.id,
|
|
1070
|
-
state: "succeeded",
|
|
1071
|
-
result: step.result,
|
|
1072
|
-
resolvedAt: new Date(step.resolved_at),
|
|
1073
|
-
createdAt: new Date(step.created_at),
|
|
1074
|
-
attemptCount: step.attempt_count,
|
|
1075
|
-
maxAttempts: step.max_attempts,
|
|
1076
|
-
parentStepId: step.parent_step_id
|
|
1077
|
-
} satisfies RunStep;
|
|
1078
|
-
case "failed":
|
|
1079
|
-
return {
|
|
1080
|
-
type: "run",
|
|
1081
|
-
id: step.id,
|
|
1082
|
-
state: "failed",
|
|
1083
|
-
errorMessage: step.error_message,
|
|
1084
|
-
errorName: step.error_name ?? undefined,
|
|
1085
|
-
resolvedAt: new Date(step.resolved_at),
|
|
1086
|
-
createdAt: new Date(step.created_at),
|
|
1087
|
-
attemptCount: step.attempt_count,
|
|
1088
|
-
maxAttempts: step.max_attempts,
|
|
1089
|
-
parentStepId: step.parent_step_id
|
|
1090
|
-
} satisfies RunStep;
|
|
1091
|
-
default:
|
|
1092
|
-
throw new Error("Unexpected step state");
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
case "sleep":
|
|
1096
|
-
switch (step.state) {
|
|
1097
|
-
case "waiting":
|
|
1098
|
-
return {
|
|
1099
|
-
type: "sleep",
|
|
1100
|
-
id: step.id,
|
|
1101
|
-
state: "waiting",
|
|
1102
|
-
wakeAt: new Date(step.wake_at),
|
|
1103
|
-
createdAt: new Date(step.created_at),
|
|
1104
|
-
parentStepId: step.parent_step_id
|
|
1105
|
-
} satisfies SleepStep;
|
|
1106
|
-
case "elapsed":
|
|
1107
|
-
return {
|
|
1108
|
-
type: "sleep",
|
|
1109
|
-
id: step.id,
|
|
1110
|
-
state: "elapsed",
|
|
1111
|
-
resolvedAt: new Date(step.resolved_at),
|
|
1112
|
-
createdAt: new Date(step.created_at),
|
|
1113
|
-
parentStepId: step.parent_step_id
|
|
1114
|
-
} satisfies SleepStep;
|
|
1115
|
-
default:
|
|
1116
|
-
throw new Error("Unexpected step state");
|
|
1117
|
-
}
|
|
1118
|
-
case "wait":
|
|
1119
|
-
switch (step.state) {
|
|
1120
|
-
case "waiting":
|
|
1121
|
-
return {
|
|
1122
|
-
type: "wait",
|
|
1123
|
-
id: step.id,
|
|
1124
|
-
state: "waiting",
|
|
1125
|
-
eventName: step.event_name,
|
|
1126
|
-
timeoutAt: step.timeout_at ? new Date(step.timeout_at) : undefined,
|
|
1127
|
-
createdAt: new Date(step.created_at),
|
|
1128
|
-
parentStepId: step.parent_step_id
|
|
1129
|
-
} satisfies WaitStep;
|
|
1130
|
-
case "satisfied":
|
|
1131
|
-
return {
|
|
1132
|
-
type: "wait",
|
|
1133
|
-
id: step.id,
|
|
1134
|
-
state: "satisfied",
|
|
1135
|
-
payload: step.payload,
|
|
1136
|
-
createdAt: new Date(step.created_at),
|
|
1137
|
-
eventName: step.event_name,
|
|
1138
|
-
resolvedAt: new Date(step.resolved_at),
|
|
1139
|
-
parentStepId: step.parent_step_id
|
|
1140
|
-
} satisfies WaitStep;
|
|
1141
|
-
case "timed_out":
|
|
1142
|
-
return {
|
|
1143
|
-
type: "wait",
|
|
1144
|
-
id: step.id,
|
|
1145
|
-
state: "timed_out",
|
|
1146
|
-
eventName: step.event_name,
|
|
1147
|
-
resolvedAt: new Date(step.resolved_at),
|
|
1148
|
-
createdAt: new Date(step.created_at),
|
|
1149
|
-
parentStepId: step.parent_step_id
|
|
1150
|
-
} satisfies WaitStep;
|
|
1151
|
-
default:
|
|
1152
|
-
throw new Error("Unexpected step state");
|
|
1153
|
-
}
|
|
1154
|
-
default:
|
|
1155
|
-
throw new Error("Unexpected step type");
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
926
|
+
type WaitingWaitStep = Extract<WaitStep, { state: "waiting" }>;
|
|
927
|
+
type SatisfiedWaitStep<T extends Json | undefined> = Extract<WaitStep<T>, { state: "satisfied" }>;
|
|
928
|
+
type TimedOutWaitStep = Extract<WaitStep, { state: "timed_out" }>;
|
|
1158
929
|
|
|
1159
930
|
/**
|
|
1160
|
-
*
|
|
931
|
+
* SQLite row shape for `run_step_attempts`.
|
|
932
|
+
*
|
|
933
|
+
* Succeeded attempts use a `result_type` discriminator:
|
|
934
|
+
*
|
|
935
|
+
* - `'json'` → `result_json` holds the raw JSON value (never NULL)
|
|
936
|
+
* - `'none'` → callback returned `undefined`/`void`; no result data
|
|
1161
937
|
*/
|
|
1162
|
-
type
|
|
938
|
+
type RunStepAttempt_Row = {
|
|
1163
939
|
id: string;
|
|
1164
|
-
|
|
1165
|
-
|
|
940
|
+
step_id: RunStepId;
|
|
941
|
+
started_at: number;
|
|
1166
942
|
} & (
|
|
1167
943
|
| {
|
|
1168
|
-
|
|
1169
|
-
attemptNumber: number;
|
|
1170
|
-
}
|
|
1171
|
-
| {
|
|
1172
|
-
type: "attempt_succeeded";
|
|
1173
|
-
attemptNumber: number;
|
|
1174
|
-
result?: string;
|
|
1175
|
-
}
|
|
1176
|
-
| {
|
|
1177
|
-
type: "attempt_failed";
|
|
1178
|
-
attemptNumber: number;
|
|
1179
|
-
errorMessage: string;
|
|
1180
|
-
errorName?: string;
|
|
1181
|
-
nextAttemptAt?: Date;
|
|
1182
|
-
}
|
|
1183
|
-
| {
|
|
1184
|
-
type: "sleep_waiting";
|
|
1185
|
-
wakeAt: Date;
|
|
1186
|
-
}
|
|
1187
|
-
| {
|
|
1188
|
-
type: "sleep_elapsed";
|
|
1189
|
-
}
|
|
1190
|
-
| {
|
|
1191
|
-
type: "wait_waiting";
|
|
1192
|
-
eventName: string;
|
|
1193
|
-
timeoutAt?: Date;
|
|
1194
|
-
}
|
|
1195
|
-
| {
|
|
1196
|
-
type: "wait_satisfied";
|
|
1197
|
-
payload?: string;
|
|
944
|
+
state: "started";
|
|
1198
945
|
}
|
|
946
|
+
| ({
|
|
947
|
+
state: "succeeded";
|
|
948
|
+
ended_at: number;
|
|
949
|
+
} & ({ result_type: "json"; result_json: string } | { result_type: "none" }))
|
|
1199
950
|
| {
|
|
1200
|
-
|
|
951
|
+
state: "failed";
|
|
952
|
+
error_message: string;
|
|
953
|
+
error_name: string | null;
|
|
954
|
+
ended_at: number;
|
|
955
|
+
next_attempt_at: number | null;
|
|
1201
956
|
}
|
|
1202
957
|
);
|
|
1203
958
|
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
case "
|
|
1215
|
-
return {
|
|
1216
|
-
id: row.id,
|
|
1217
|
-
type: "attempt_succeeded",
|
|
1218
|
-
attemptNumber: row.attempt_number,
|
|
1219
|
-
result: row.result,
|
|
1220
|
-
stepId: row.step_id,
|
|
1221
|
-
recordedAt: new Date(row.recorded_at)
|
|
1222
|
-
};
|
|
1223
|
-
case "attempt_failed":
|
|
1224
|
-
return {
|
|
1225
|
-
id: row.id,
|
|
1226
|
-
type: "attempt_failed",
|
|
1227
|
-
attemptNumber: row.attempt_number,
|
|
1228
|
-
errorMessage: row.error_message,
|
|
1229
|
-
errorName: row.error_name ?? undefined,
|
|
1230
|
-
nextAttemptAt: row.next_attempt_at ? new Date(row.next_attempt_at) : undefined,
|
|
1231
|
-
stepId: row.step_id,
|
|
1232
|
-
recordedAt: new Date(row.recorded_at)
|
|
1233
|
-
};
|
|
1234
|
-
case "sleep_waiting":
|
|
959
|
+
type StartedRunStepAttempt_Row = Extract<RunStepAttempt_Row, { state: "started" }>;
|
|
960
|
+
type SucceededRunStepAttempt_Row = Extract<RunStepAttempt_Row, { state: "succeeded" }>;
|
|
961
|
+
type FailedRunStepAttempt_Row = Extract<RunStepAttempt_Row, { state: "failed" }>;
|
|
962
|
+
|
|
963
|
+
function formatRunStepAttempt(attempt: StartedRunStepAttempt_Row): StartedRunStepAttempt;
|
|
964
|
+
function formatRunStepAttempt(attempt: SucceededRunStepAttempt_Row): SucceededRunStepAttempt;
|
|
965
|
+
function formatRunStepAttempt(attempt: FailedRunStepAttempt_Row): FailedRunStepAttempt;
|
|
966
|
+
function formatRunStepAttempt(attempt: RunStepAttempt_Row): RunStepAttempt;
|
|
967
|
+
function formatRunStepAttempt(attempt: RunStepAttempt_Row): RunStepAttempt {
|
|
968
|
+
switch (attempt.state) {
|
|
969
|
+
case "started":
|
|
1235
970
|
return {
|
|
1236
|
-
id:
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
recordedAt: new Date(row.recorded_at)
|
|
971
|
+
id: attempt.id,
|
|
972
|
+
stepId: attempt.step_id,
|
|
973
|
+
startedAt: new Date(attempt.started_at),
|
|
974
|
+
state: "started"
|
|
1241
975
|
};
|
|
1242
|
-
case "
|
|
1243
|
-
|
|
1244
|
-
id:
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
976
|
+
case "succeeded": {
|
|
977
|
+
const base = {
|
|
978
|
+
id: attempt.id,
|
|
979
|
+
stepId: attempt.step_id,
|
|
980
|
+
startedAt: new Date(attempt.started_at),
|
|
981
|
+
state: "succeeded" as const,
|
|
982
|
+
endedAt: new Date(attempt.ended_at)
|
|
1248
983
|
};
|
|
1249
|
-
|
|
984
|
+
if (attempt.result_type === "json") {
|
|
985
|
+
return { ...base, resultType: "json" as const, resultJson: attempt.result_json };
|
|
986
|
+
}
|
|
987
|
+
return { ...base, resultType: attempt.result_type };
|
|
988
|
+
}
|
|
989
|
+
case "failed":
|
|
1250
990
|
return {
|
|
1251
|
-
id:
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
991
|
+
id: attempt.id,
|
|
992
|
+
stepId: attempt.step_id,
|
|
993
|
+
startedAt: new Date(attempt.started_at),
|
|
994
|
+
state: "failed",
|
|
995
|
+
errorMessage: attempt.error_message,
|
|
996
|
+
errorName: attempt.error_name ?? undefined,
|
|
997
|
+
endedAt: new Date(attempt.ended_at),
|
|
998
|
+
nextAttemptAt: attempt.next_attempt_at != null ? new Date(attempt.next_attempt_at) : undefined
|
|
1257
999
|
};
|
|
1258
|
-
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function formatRunStep(step: RunStep_Row): RunStep {
|
|
1004
|
+
return {
|
|
1005
|
+
type: "run",
|
|
1006
|
+
id: step.id,
|
|
1007
|
+
createdAt: new Date(step.created_at),
|
|
1008
|
+
maxAttempts: step.max_attempts,
|
|
1009
|
+
parentStepId: step.parent_step_id
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function formatSleepStep(step: SleepStep_Row): SleepStep {
|
|
1014
|
+
switch (step.state) {
|
|
1015
|
+
case "waiting":
|
|
1259
1016
|
return {
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1017
|
+
type: "sleep",
|
|
1018
|
+
id: step.id,
|
|
1019
|
+
state: "waiting",
|
|
1020
|
+
wakeAt: new Date(step.target_wake_at),
|
|
1021
|
+
createdAt: new Date(step.created_at),
|
|
1022
|
+
parentStepId: step.parent_step_id
|
|
1023
|
+
} satisfies SleepStep;
|
|
1024
|
+
case "elapsed":
|
|
1267
1025
|
return {
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1026
|
+
type: "sleep",
|
|
1027
|
+
id: step.id,
|
|
1028
|
+
state: "elapsed",
|
|
1029
|
+
resolvedAt: new Date(step.resolved_at),
|
|
1030
|
+
createdAt: new Date(step.created_at),
|
|
1031
|
+
parentStepId: step.parent_step_id
|
|
1032
|
+
} satisfies SleepStep;
|
|
1033
|
+
default:
|
|
1034
|
+
throw new Error("Unexpected sleep step state");
|
|
1273
1035
|
}
|
|
1274
1036
|
}
|
|
1275
1037
|
|
|
1038
|
+
function formatWaitingWaitStep(step: WaitingWaitStep_Row): WaitingWaitStep {
|
|
1039
|
+
return {
|
|
1040
|
+
type: "wait",
|
|
1041
|
+
id: step.id,
|
|
1042
|
+
state: "waiting",
|
|
1043
|
+
eventName: step.event_name,
|
|
1044
|
+
timeoutAt: step.timeout_at != null ? new Date(step.timeout_at) : undefined,
|
|
1045
|
+
createdAt: new Date(step.created_at),
|
|
1046
|
+
parentStepId: step.parent_step_id
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function formatTimedOutWaitStep(step: TimedOutWaitStep_Row): TimedOutWaitStep {
|
|
1051
|
+
return {
|
|
1052
|
+
type: "wait",
|
|
1053
|
+
id: step.id,
|
|
1054
|
+
state: "timed_out",
|
|
1055
|
+
eventName: step.event_name,
|
|
1056
|
+
resolvedAt: new Date(step.resolved_at),
|
|
1057
|
+
createdAt: new Date(step.created_at),
|
|
1058
|
+
parentStepId: step.parent_step_id,
|
|
1059
|
+
timeoutAt: new Date(step.timeout_at)
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function formatSatisfiedWaitStep<T extends Json | undefined>(
|
|
1064
|
+
step: SatisfiedWaitStep_Row,
|
|
1065
|
+
payload: T
|
|
1066
|
+
): SatisfiedWaitStep<T> {
|
|
1067
|
+
return {
|
|
1068
|
+
type: "wait",
|
|
1069
|
+
id: step.id,
|
|
1070
|
+
state: "satisfied",
|
|
1071
|
+
payload: payload,
|
|
1072
|
+
createdAt: new Date(step.created_at),
|
|
1073
|
+
eventName: step.event_name,
|
|
1074
|
+
resolvedAt: new Date(step.resolved_at),
|
|
1075
|
+
parentStepId: step.parent_step_id,
|
|
1076
|
+
timeoutAt: step.timeout_at != null ? new Date(step.timeout_at) : undefined
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1276
1080
|
/**
|
|
1277
1081
|
* SQLite row shape for `workflow_events` (append-only workflow status transitions).
|
|
1278
1082
|
*/
|
|
@@ -1332,128 +1136,56 @@ type RunStep_Row = {
|
|
|
1332
1136
|
* Enclosing run step id when this run step was created inside that run's callback; otherwise null.
|
|
1333
1137
|
*/
|
|
1334
1138
|
parent_step_id: RunStepId | null;
|
|
1335
|
-
|
|
1336
|
-
/**
|
|
1337
|
-
* Number of attempts that have been durably started for this step.
|
|
1338
|
-
*
|
|
1339
|
-
* Invariants:
|
|
1340
|
-
*
|
|
1341
|
-
* - `0` before the first attempt starts
|
|
1342
|
-
* - Incremented exactly when an attempt transitions into `running`
|
|
1343
|
-
* - Never decremented
|
|
1344
|
-
* - Unchanged while the step is pending between retries/backoff
|
|
1345
|
-
*
|
|
1346
|
-
* When greater than `0`, this is also the 1-based number of the most recently started attempt. The next attempt, if
|
|
1347
|
-
* one is started, will have number `attempts_started + 1`.
|
|
1348
|
-
*/
|
|
1349
|
-
attempt_count: number;
|
|
1350
1139
|
/**
|
|
1351
1140
|
* Maximum number of attempts that can be made for this step. If not present, the step can be retried indefinitely. If
|
|
1352
1141
|
* present, the step can be retried up to this number of times. If the step has reached the maximum number of
|
|
1353
1142
|
* attempts, it will transition to the `failed` state.
|
|
1354
1143
|
*/
|
|
1355
1144
|
max_attempts: number | null;
|
|
1356
|
-
}
|
|
1357
|
-
| {
|
|
1358
|
-
/**
|
|
1359
|
-
* The step has been reached but is not yet resolved, and no attempt is currently in progress.
|
|
1360
|
-
*
|
|
1361
|
-
* This includes steps that: 1. have been reached but have not yet started their first attempt 2. are waiting
|
|
1362
|
-
* until a retry/backoff time 3. are eligible to run immediately.
|
|
1363
|
-
*/
|
|
1364
|
-
state: "pending";
|
|
1365
|
-
|
|
1366
|
-
/**
|
|
1367
|
-
* Earliest time at which the next attempt may start.
|
|
1368
|
-
*
|
|
1369
|
-
* Semantics: - On first creation, this is typically set to "now", meaning the step is immediately runnable. -
|
|
1370
|
-
* After a failed attempt with backoff, this is set to the retry time. - While the current time is before this
|
|
1371
|
-
* value, the step remains `pending` and no new attempt may start. - Once the current time reaches or passes this
|
|
1372
|
-
* value, the step becomes eligible to transition from `pending` to `running`.
|
|
1373
|
-
*/
|
|
1374
|
-
next_attempt_at: number;
|
|
1375
|
-
}
|
|
1376
|
-
| {
|
|
1377
|
-
/**
|
|
1378
|
-
* An attempt was durably started, but its outcome has not yet been durably recorded.
|
|
1379
|
-
*
|
|
1380
|
-
* This does not guarantee that code is actively executing at this instant. After a restart or interruption,
|
|
1381
|
-
* `running` means only that a start was recorded and no durable success/failure was recorded afterward.
|
|
1382
|
-
*/
|
|
1383
|
-
state: "running";
|
|
1384
|
-
}
|
|
1385
|
-
| {
|
|
1386
|
-
/**
|
|
1387
|
-
* The step completed successfully and its result was durably recorded.
|
|
1388
|
-
*/
|
|
1389
|
-
state: "succeeded";
|
|
1390
|
-
|
|
1391
|
-
/**
|
|
1392
|
-
* Serialized successful result for the step.
|
|
1393
|
-
*/
|
|
1394
|
-
result: string;
|
|
1395
|
-
|
|
1396
|
-
/**
|
|
1397
|
-
* Time at which the step became terminal by succeeding.
|
|
1398
|
-
*/
|
|
1399
|
-
resolved_at: number;
|
|
1400
|
-
}
|
|
1401
|
-
| {
|
|
1402
|
-
/**
|
|
1403
|
-
* The step failed terminally and no further attempts will be made.
|
|
1404
|
-
*/
|
|
1405
|
-
state: "failed";
|
|
1406
|
-
|
|
1407
|
-
/**
|
|
1408
|
-
* Serialized failure message for the terminal failure.
|
|
1409
|
-
*/
|
|
1410
|
-
error_message: string;
|
|
1411
|
-
|
|
1412
|
-
/**
|
|
1413
|
-
* Optional serialized error class/name for the terminal failure.
|
|
1414
|
-
*/
|
|
1415
|
-
error_name: string | null;
|
|
1416
|
-
|
|
1417
|
-
/**
|
|
1418
|
-
* Time at which the step became terminal by failing.
|
|
1419
|
-
*/
|
|
1420
|
-
resolved_at: number;
|
|
1421
|
-
}
|
|
1422
|
-
);
|
|
1145
|
+
};
|
|
1423
1146
|
|
|
1424
1147
|
type SleepStep_Row = {
|
|
1425
1148
|
id: SleepStepId;
|
|
1426
1149
|
type: "sleep";
|
|
1427
1150
|
created_at: number;
|
|
1428
1151
|
/**
|
|
1429
|
-
*
|
|
1152
|
+
* Id of the enclosing run step if this sleep step was created from within that run step's callback; otherwise null.
|
|
1430
1153
|
*/
|
|
1431
1154
|
parent_step_id: RunStepId | null;
|
|
1432
1155
|
} & (
|
|
1433
1156
|
| {
|
|
1434
1157
|
/**
|
|
1435
|
-
* The sleep step has
|
|
1158
|
+
* The sleep step has started and is not yet terminal.
|
|
1436
1159
|
*
|
|
1437
|
-
*
|
|
1438
|
-
*
|
|
1160
|
+
* In this state, the step is considered active until its target wake time is reached and that transition is
|
|
1161
|
+
* durably recorded.
|
|
1439
1162
|
*/
|
|
1440
1163
|
state: "waiting";
|
|
1441
1164
|
|
|
1442
1165
|
/**
|
|
1443
|
-
*
|
|
1166
|
+
* Target time at or after which this sleep step may transition to `elapsed`.
|
|
1444
1167
|
*
|
|
1445
|
-
*
|
|
1168
|
+
* This is part of the step's configured behavior, not an indicator that the step is still pending.
|
|
1446
1169
|
*/
|
|
1447
|
-
|
|
1170
|
+
target_wake_at: number;
|
|
1448
1171
|
}
|
|
1449
1172
|
| {
|
|
1450
1173
|
/**
|
|
1451
|
-
* The sleep
|
|
1174
|
+
* The sleep step has completed because its target wake time was reached and that outcome was durably recorded.
|
|
1452
1175
|
*/
|
|
1453
1176
|
state: "elapsed";
|
|
1454
1177
|
|
|
1455
1178
|
/**
|
|
1456
|
-
*
|
|
1179
|
+
* Target time at or after which this sleep step became eligible to transition to `elapsed`.
|
|
1180
|
+
*
|
|
1181
|
+
* Retained on terminal rows so the completed step still carries its original timing configuration.
|
|
1182
|
+
*/
|
|
1183
|
+
target_wake_at: number;
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* Time at which the transition to `elapsed` was durably recorded.
|
|
1187
|
+
*
|
|
1188
|
+
* This may be equal to or later than `target_wake_at`.
|
|
1457
1189
|
*/
|
|
1458
1190
|
resolved_at: number;
|
|
1459
1191
|
}
|
|
@@ -1464,242 +1196,146 @@ type WaitStep_Row = {
|
|
|
1464
1196
|
type: "wait";
|
|
1465
1197
|
created_at: number;
|
|
1466
1198
|
/**
|
|
1467
|
-
*
|
|
1199
|
+
* Id of the enclosing run step if this wait step was created from within that run step's callback; otherwise null.
|
|
1468
1200
|
*/
|
|
1469
1201
|
parent_step_id: RunStepId | null;
|
|
1470
1202
|
|
|
1471
1203
|
/**
|
|
1472
|
-
* Name of the inbound event that can satisfy this step.
|
|
1204
|
+
* Name of the inbound event that can satisfy this wait step.
|
|
1473
1205
|
*/
|
|
1474
1206
|
event_name: string;
|
|
1475
1207
|
} & (
|
|
1476
1208
|
| {
|
|
1477
1209
|
/**
|
|
1478
|
-
* The step has
|
|
1479
|
-
*
|
|
1210
|
+
* The wait step has started and is not yet terminal.
|
|
1211
|
+
*
|
|
1212
|
+
* In this state, the expected event has not yet been durably matched to the step, and no timeout outcome has been
|
|
1213
|
+
* durably recorded.
|
|
1480
1214
|
*/
|
|
1481
1215
|
state: "waiting";
|
|
1482
1216
|
|
|
1483
1217
|
/**
|
|
1484
|
-
* Optional
|
|
1218
|
+
* Optional time at or after which this wait step may transition to `timed_out`.
|
|
1485
1219
|
*
|
|
1486
|
-
* - If
|
|
1487
|
-
* - If
|
|
1220
|
+
* - If null, the step may wait indefinitely.
|
|
1221
|
+
* - If set, reaching or passing this time makes the step eligible to time out.
|
|
1488
1222
|
*/
|
|
1489
1223
|
timeout_at: number | null;
|
|
1490
1224
|
}
|
|
1491
1225
|
| {
|
|
1492
1226
|
/**
|
|
1493
|
-
* The
|
|
1227
|
+
* The wait step has completed because a matching event was durably received and recorded.
|
|
1494
1228
|
*/
|
|
1495
1229
|
state: "satisfied";
|
|
1496
1230
|
|
|
1497
1231
|
/**
|
|
1498
|
-
*
|
|
1232
|
+
* Optional timeout that applied while this step was active.
|
|
1233
|
+
*
|
|
1234
|
+
* Retained on terminal rows so the completed step still carries its original timing configuration.
|
|
1499
1235
|
*/
|
|
1500
|
-
|
|
1236
|
+
timeout_at: number | null;
|
|
1501
1237
|
|
|
1502
1238
|
/**
|
|
1503
|
-
* Time at which the
|
|
1239
|
+
* Time at which the transition to `satisfied` was durably recorded.
|
|
1504
1240
|
*/
|
|
1505
1241
|
resolved_at: number;
|
|
1506
1242
|
}
|
|
1507
1243
|
| {
|
|
1508
1244
|
/**
|
|
1509
|
-
* The step
|
|
1245
|
+
* The wait step has completed by timing out before a matching event was durably received.
|
|
1510
1246
|
*/
|
|
1511
1247
|
state: "timed_out";
|
|
1512
1248
|
|
|
1513
1249
|
/**
|
|
1514
|
-
* Time at which
|
|
1250
|
+
* Time at or after which this step became eligible to transition to `timed_out`.
|
|
1251
|
+
*/
|
|
1252
|
+
timeout_at: number;
|
|
1253
|
+
|
|
1254
|
+
/**
|
|
1255
|
+
* Time at which the transition to `timed_out` was durably recorded.
|
|
1256
|
+
*
|
|
1257
|
+
* This may be equal to or later than `timeout_at`.
|
|
1515
1258
|
*/
|
|
1516
1259
|
resolved_at: number;
|
|
1517
1260
|
}
|
|
1518
1261
|
);
|
|
1519
1262
|
|
|
1263
|
+
type WaitingWaitStep_Row = Extract<WaitStep_Row, { state: "waiting" }>;
|
|
1264
|
+
type SatisfiedWaitStep_Row = Extract<WaitStep_Row, { state: "satisfied" }>;
|
|
1265
|
+
type TimedOutWaitStep_Row = Extract<WaitStep_Row, { state: "timed_out" }>;
|
|
1266
|
+
|
|
1520
1267
|
/**
|
|
1521
1268
|
* A step row is created once the workflow reaches that step. From that point on, `status` describes the step's durable
|
|
1522
1269
|
* lifecycle state.
|
|
1523
1270
|
*/
|
|
1524
1271
|
type Step_Row = RunStep_Row | SleepStep_Row | WaitStep_Row;
|
|
1525
1272
|
|
|
1526
|
-
/**
|
|
1527
|
-
* SQLite row shape for `step_events`: append-only durable transitions for steps.
|
|
1528
|
-
*
|
|
1529
|
-
* The `steps` table holds current state; `step_events` records how that state evolved.
|
|
1530
|
-
*/
|
|
1531
|
-
type StepEventRow = {
|
|
1532
|
-
id: string;
|
|
1533
|
-
|
|
1534
|
-
/**
|
|
1535
|
-
* `steps.id` this event applies to.
|
|
1536
|
-
*/
|
|
1537
|
-
step_id: RunStepId | SleepStepId | WaitStepId;
|
|
1538
|
-
|
|
1539
|
-
/**
|
|
1540
|
-
* When this event row was persisted (`unixepoch` ms).
|
|
1541
|
-
*/
|
|
1542
|
-
recorded_at: number;
|
|
1543
|
-
} & (
|
|
1544
|
-
| {
|
|
1545
|
-
/**
|
|
1546
|
-
* A run step attempt was durably started.
|
|
1547
|
-
*
|
|
1548
|
-
* This corresponds to the step transitioning from `pending` → `running`.
|
|
1549
|
-
*/
|
|
1550
|
-
type: "attempt_started";
|
|
1551
|
-
|
|
1552
|
-
/**
|
|
1553
|
-
* 1-based attempt number.
|
|
1554
|
-
*
|
|
1555
|
-
* Equals the value of `attempts_started` after the transition.
|
|
1556
|
-
*/
|
|
1557
|
-
attempt_number: number;
|
|
1558
|
-
}
|
|
1559
|
-
| {
|
|
1560
|
-
/**
|
|
1561
|
-
* A run step attempt completed successfully.
|
|
1562
|
-
*
|
|
1563
|
-
* The step transitioned from `running` to `succeeded`.
|
|
1564
|
-
*/
|
|
1565
|
-
type: "attempt_succeeded";
|
|
1566
|
-
|
|
1567
|
-
attempt_number: number;
|
|
1568
|
-
|
|
1569
|
-
/**
|
|
1570
|
-
* Serialized result produced by the step.
|
|
1571
|
-
*/
|
|
1572
|
-
result: string;
|
|
1573
|
-
}
|
|
1574
|
-
| {
|
|
1575
|
-
/**
|
|
1576
|
-
* A run step attempt failed.
|
|
1577
|
-
*
|
|
1578
|
-
* The step either: - scheduled a next attempt, or - transitioned to terminal failure.
|
|
1579
|
-
*/
|
|
1580
|
-
type: "attempt_failed";
|
|
1581
|
-
|
|
1582
|
-
attempt_number: number;
|
|
1583
|
-
|
|
1584
|
-
error_message: string;
|
|
1585
|
-
error_name: string | null;
|
|
1586
|
-
|
|
1587
|
-
/**
|
|
1588
|
-
* If present, the next attempt time.
|
|
1589
|
-
*
|
|
1590
|
-
* Absence indicates this failure was terminal.
|
|
1591
|
-
*/
|
|
1592
|
-
next_attempt_at?: number;
|
|
1593
|
-
}
|
|
1594
|
-
| {
|
|
1595
|
-
/**
|
|
1596
|
-
* A sleep step began waiting.
|
|
1597
|
-
*
|
|
1598
|
-
* Corresponds to the step entering `waiting`.
|
|
1599
|
-
*/
|
|
1600
|
-
type: "sleep_waiting";
|
|
1601
|
-
|
|
1602
|
-
/**
|
|
1603
|
-
* Wake time for the sleep step.
|
|
1604
|
-
*/
|
|
1605
|
-
wake_at: number;
|
|
1606
|
-
}
|
|
1607
|
-
| {
|
|
1608
|
-
/**
|
|
1609
|
-
* A sleep step completed because the wake condition became satisfied.
|
|
1610
|
-
*/
|
|
1611
|
-
type: "sleep_elapsed";
|
|
1612
|
-
}
|
|
1613
|
-
| {
|
|
1614
|
-
/**
|
|
1615
|
-
* A wait step began waiting for the expected event.
|
|
1616
|
-
*/
|
|
1617
|
-
type: "wait_waiting";
|
|
1618
|
-
|
|
1619
|
-
event_name: string;
|
|
1620
|
-
timeout_at: number | null;
|
|
1621
|
-
}
|
|
1622
|
-
| {
|
|
1623
|
-
/**
|
|
1624
|
-
* A wait step was satisfied by receiving the expected event.
|
|
1625
|
-
*/
|
|
1626
|
-
type: "wait_satisfied";
|
|
1627
|
-
|
|
1628
|
-
payload: string;
|
|
1629
|
-
}
|
|
1630
|
-
| {
|
|
1631
|
-
/**
|
|
1632
|
-
* A wait step failed because its timeout deadline was reached.
|
|
1633
|
-
*/
|
|
1634
|
-
type: "wait_timed_out";
|
|
1635
|
-
}
|
|
1636
|
-
);
|
|
1637
|
-
|
|
1638
1273
|
export type WorkflowStatus =
|
|
1639
|
-
| "pending" //
|
|
1274
|
+
| "pending" // Durable metadata exists, but create() has not initialized the workflow input yet
|
|
1275
|
+
| "initialized" // create() has pinned the workflow input, but execution has not started yet
|
|
1640
1276
|
| "running" // The workflow is currently executing; steps are being created/processed
|
|
1641
1277
|
| "paused" // The workflow is paused and will not make progress until resumed
|
|
1642
1278
|
| "completed" // The workflow completed successfully; ('Workflow.next' returned { done: true, status: "succeeded" })
|
|
1643
1279
|
| "failed" // A step exhausted retries and the workflow aborted; ('Workflow.next' returned { done: true, status: "failed" })
|
|
1644
1280
|
| "cancelled"; // The workflow was terminated explicitly by the user.
|
|
1645
1281
|
|
|
1646
|
-
type WorkflowMetadata_Row
|
|
1282
|
+
type WorkflowMetadata_Row = {
|
|
1647
1283
|
created_at: number;
|
|
1648
1284
|
updated_at: number;
|
|
1649
1285
|
status: WorkflowStatus;
|
|
1650
|
-
definition_version: TVersion | null;
|
|
1651
1286
|
definition_input: string | null;
|
|
1652
1287
|
};
|
|
1653
1288
|
|
|
1654
|
-
type WorkflowMetadata<TVersion extends string> = {
|
|
1655
|
-
createdAt: Date;
|
|
1656
|
-
updatedAt: Date;
|
|
1657
|
-
status: WorkflowStatus;
|
|
1658
|
-
definitionVersion?: TVersion;
|
|
1659
|
-
definitionInput?: Json;
|
|
1660
|
-
};
|
|
1661
|
-
|
|
1662
|
-
export function formatWorkflowMetadata<TVersion extends string>(
|
|
1663
|
-
metadata: WorkflowMetadata_Row<TVersion>
|
|
1664
|
-
): WorkflowMetadata<TVersion> {
|
|
1665
|
-
return {
|
|
1666
|
-
createdAt: new Date(metadata.created_at),
|
|
1667
|
-
updatedAt: new Date(metadata.updated_at),
|
|
1668
|
-
status: metadata.status,
|
|
1669
|
-
definitionVersion: metadata.definition_version ?? undefined,
|
|
1670
|
-
definitionInput: metadata.definition_input ? JSON.parse(metadata.definition_input) : undefined
|
|
1671
|
-
};
|
|
1672
|
-
}
|
|
1673
1289
|
/**
|
|
1674
1290
|
* Durable record of inbound events (`inbound_events`) that may satisfy wait steps.
|
|
1675
1291
|
*
|
|
1676
1292
|
* Persists delivered signals across restarts so step resolution is based on durable state rather than in-memory state.
|
|
1677
1293
|
*/
|
|
1678
|
-
|
|
1679
|
-
/**
|
|
1680
|
-
* Unique identifier for the event.
|
|
1681
|
-
*/
|
|
1294
|
+
type InboundEvent_Row = {
|
|
1682
1295
|
id: string;
|
|
1683
|
-
/**
|
|
1684
|
-
* Name of the event.
|
|
1685
|
-
*
|
|
1686
|
-
* Used by wait steps to determine whether this event can satisfy them.
|
|
1687
|
-
*/
|
|
1688
1296
|
event_name: string;
|
|
1689
1297
|
/**
|
|
1690
|
-
*
|
|
1691
|
-
*/
|
|
1692
|
-
payload: string;
|
|
1693
|
-
/**
|
|
1694
|
-
* Time the event was durably recorded.
|
|
1298
|
+
* Raw JSON value, or SQL NULL for undefined (no payload).
|
|
1695
1299
|
*/
|
|
1300
|
+
payload: string | null;
|
|
1696
1301
|
created_at: number;
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1302
|
+
} & (
|
|
1303
|
+
| {
|
|
1304
|
+
/**
|
|
1305
|
+
* Step that claimed the event.
|
|
1306
|
+
*/
|
|
1307
|
+
claimed_by: WaitStepId;
|
|
1308
|
+
/**
|
|
1309
|
+
* Time the event was claimed.
|
|
1310
|
+
*/
|
|
1311
|
+
claimed_at: number;
|
|
1312
|
+
}
|
|
1313
|
+
| {
|
|
1314
|
+
claimed_by: null;
|
|
1315
|
+
claimed_at: null;
|
|
1316
|
+
}
|
|
1317
|
+
);
|
|
1318
|
+
|
|
1319
|
+
type ClaimedInboundEvent_Row = Extract<
|
|
1320
|
+
InboundEvent_Row,
|
|
1321
|
+
{
|
|
1322
|
+
claimed_by: WaitStepId;
|
|
1323
|
+
claimed_at: number;
|
|
1324
|
+
}
|
|
1325
|
+
>;
|
|
1326
|
+
|
|
1327
|
+
type InboundEvent<T extends Json | undefined = Json | undefined> = {
|
|
1328
|
+
id: string;
|
|
1329
|
+
eventName: string;
|
|
1330
|
+
payload: T;
|
|
1331
|
+
createdAt: Date;
|
|
1332
|
+
} & (
|
|
1333
|
+
| {
|
|
1334
|
+
claimedBy: WaitStepId;
|
|
1335
|
+
claimedAt: Date;
|
|
1336
|
+
}
|
|
1337
|
+
| {
|
|
1338
|
+
claimedBy?: never;
|
|
1339
|
+
claimedAt?: never;
|
|
1340
|
+
}
|
|
1341
|
+
);
|