workerflow 0.1.0 → 0.2.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 +32 -10
- package/package.json +1 -1
- package/src/definition.ts +126 -174
- package/src/migrations/0000_initial.ts +84 -285
- package/src/runtime.ts +609 -950
- package/test/runtime.spec.ts +618 -1074
- 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
|
@@ -117,19 +117,61 @@ export abstract class WorkflowRuntime<
|
|
|
117
117
|
* @returns An array containing the formatted steps for all steps in the
|
|
118
118
|
* workflow.
|
|
119
119
|
*/
|
|
120
|
-
getSteps_experimental():
|
|
120
|
+
getSteps_experimental(): Array<(RunStep & { attempts: RunStepAttempt[] }) | SleepStep | WaitStep> {
|
|
121
121
|
const steps = this.sql.exec<Step_Row>("SELECT * FROM steps ORDER BY created_at ASC").toArray();
|
|
122
|
-
return steps.map((step) =>
|
|
122
|
+
return steps.map((step) => {
|
|
123
|
+
if (step.type === "run") {
|
|
124
|
+
const attempts = this.sql
|
|
125
|
+
.exec<RunStepAttempt_Row>(
|
|
126
|
+
`SELECT * FROM run_step_attempts WHERE step_id = ? ORDER BY started_at ASC, id ASC`,
|
|
127
|
+
step.id
|
|
128
|
+
)
|
|
129
|
+
.toArray();
|
|
130
|
+
return {
|
|
131
|
+
...formatRunStep(step),
|
|
132
|
+
attempts: attempts.map((attempt) => formatRunStepAttempt(attempt))
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (step.type === "sleep") {
|
|
136
|
+
return formatSleepStep(step);
|
|
137
|
+
} else if (step.type === "wait") {
|
|
138
|
+
if (step.state === "satisfied") {
|
|
139
|
+
return formatSatisfiedWaitStep(step, this.getInboundEventForWaitStep(step.id).payload);
|
|
140
|
+
} else if (step.state === "timed_out") {
|
|
141
|
+
return formatTimedOutWaitStep(step);
|
|
142
|
+
} else {
|
|
143
|
+
return formatWaitingWaitStep(step);
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
throw new Error("Unexpected step type. Expected 'run', 'sleep', or 'wait'.");
|
|
147
|
+
}
|
|
148
|
+
});
|
|
123
149
|
}
|
|
124
150
|
|
|
125
151
|
/**
|
|
126
|
-
*
|
|
152
|
+
* Loads the `inbound_events` row whose `claimed_by` is this wait step (`waitStepId`).
|
|
127
153
|
*
|
|
128
|
-
* @
|
|
154
|
+
* @throws If no such row exists (storage invariant broken or the step is not in a satisfied state with a claim).
|
|
129
155
|
*/
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
156
|
+
private getInboundEventForWaitStep<T extends Json | undefined = Json | undefined>(
|
|
157
|
+
stepId: WaitStepId
|
|
158
|
+
): InboundEvent<T> {
|
|
159
|
+
const [row] = this.sql
|
|
160
|
+
.exec<ClaimedInboundEvent_Row>(`SELECT * FROM inbound_events WHERE claimed_by = ? LIMIT 1`, stepId)
|
|
161
|
+
.toArray();
|
|
162
|
+
if (row === undefined) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Wait step '${stepId}' is satisfied in durable state but no inbound_events row is claimed by this step.`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
id: row.id,
|
|
169
|
+
eventName: row.event_name,
|
|
170
|
+
payload: (row.payload === null ? undefined : JSON.parse(row.payload)) as T,
|
|
171
|
+
createdAt: new Date(row.created_at),
|
|
172
|
+
claimedBy: row.claimed_by,
|
|
173
|
+
claimedAt: new Date(row.claimed_at)
|
|
174
|
+
};
|
|
133
175
|
}
|
|
134
176
|
|
|
135
177
|
/**
|
|
@@ -157,7 +199,9 @@ export abstract class WorkflowRuntime<
|
|
|
157
199
|
return;
|
|
158
200
|
}
|
|
159
201
|
|
|
160
|
-
|
|
202
|
+
// SQL NULL encodes `undefined` (no payload); raw JSON.stringify for everything else
|
|
203
|
+
// (including JSON null, which becomes the TEXT literal 'null').
|
|
204
|
+
const serializedPayload = payload === undefined ? null : JSON.stringify(payload);
|
|
161
205
|
|
|
162
206
|
// If the workflow is paused, queue the event but do not satisfy any wait step or call run().
|
|
163
207
|
// The event will be picked up when the workflow is resumed and execution hits getOrCreateWaitStep.
|
|
@@ -184,34 +228,24 @@ export abstract class WorkflowRuntime<
|
|
|
184
228
|
.toArray();
|
|
185
229
|
|
|
186
230
|
if (step !== undefined) {
|
|
187
|
-
this.
|
|
188
|
-
|
|
231
|
+
this.ctx.storage.transactionSync(() => {
|
|
232
|
+
this.sql.exec(
|
|
233
|
+
`INSERT INTO inbound_events (event_name, payload, claimed_by, claimed_at)
|
|
234
|
+
VALUES (?, ?, ?, CAST(unixepoch('subsecond') * 1000 AS INTEGER))`,
|
|
235
|
+
event,
|
|
236
|
+
serializedPayload,
|
|
237
|
+
step.id
|
|
238
|
+
);
|
|
239
|
+
this.sql.exec(
|
|
240
|
+
`UPDATE steps
|
|
189
241
|
SET state = 'satisfied',
|
|
190
|
-
|
|
191
|
-
resolved_at = CAST(unixepoch('subsecond') * 1000 AS INTEGER),
|
|
192
|
-
timeout_at = NULL
|
|
242
|
+
resolved_at = CAST(unixepoch('subsecond') * 1000 AS INTEGER)
|
|
193
243
|
WHERE id = ?
|
|
194
244
|
AND type = 'wait'
|
|
195
245
|
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
|
-
);
|
|
246
|
+
step.id
|
|
247
|
+
);
|
|
248
|
+
});
|
|
215
249
|
|
|
216
250
|
await this.run();
|
|
217
251
|
} else {
|
|
@@ -280,7 +314,7 @@ export abstract class WorkflowRuntime<
|
|
|
280
314
|
|
|
281
315
|
// Schedule another safety alarm if the run loop is still active.
|
|
282
316
|
if (this.#isRunLoopActive) {
|
|
283
|
-
await this.ctx.storage.setAlarm(Date.now() +
|
|
317
|
+
await this.ctx.storage.setAlarm(Date.now() + 30 * 60 * 1000); // 30 minutes
|
|
284
318
|
} else {
|
|
285
319
|
await this.run();
|
|
286
320
|
}
|
|
@@ -342,7 +376,6 @@ export abstract class WorkflowRuntime<
|
|
|
342
376
|
|
|
343
377
|
if (this.#status !== "running") {
|
|
344
378
|
this.#setStatus({ type: "running" });
|
|
345
|
-
this.#status = "running";
|
|
346
379
|
|
|
347
380
|
if (this.onStatusChange_experimental !== undefined) {
|
|
348
381
|
await this.onStatusChange_experimental("running");
|
|
@@ -352,7 +385,7 @@ export abstract class WorkflowRuntime<
|
|
|
352
385
|
if (this.#isRunLoopActive) return;
|
|
353
386
|
|
|
354
387
|
const requestId = crypto.randomUUID();
|
|
355
|
-
const context = new WorkflowRuntimeContext(this.ctx.storage
|
|
388
|
+
const context = new WorkflowRuntimeContext(this.ctx.storage);
|
|
356
389
|
|
|
357
390
|
this.#isRunLoopActive = true;
|
|
358
391
|
|
|
@@ -381,7 +414,7 @@ export abstract class WorkflowRuntime<
|
|
|
381
414
|
const version = this.#definitionVersion;
|
|
382
415
|
if (version === undefined) {
|
|
383
416
|
throw new Error(
|
|
384
|
-
"Workflow definition version has not been initialized. Call '
|
|
417
|
+
"Workflow definition version has not been initialized. Call 'create()' before running the workflow."
|
|
385
418
|
);
|
|
386
419
|
}
|
|
387
420
|
|
|
@@ -428,7 +461,12 @@ export abstract class WorkflowRuntime<
|
|
|
428
461
|
if (result.resume.type === "immediate") continue;
|
|
429
462
|
|
|
430
463
|
// 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")
|
|
464
|
+
if (result.resume.type === "suspended") {
|
|
465
|
+
if (result.resume.wakeAt !== undefined) {
|
|
466
|
+
await this.ctx.storage.setAlarm(result.resume.wakeAt);
|
|
467
|
+
}
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
432
470
|
|
|
433
471
|
break;
|
|
434
472
|
} catch (error) {
|
|
@@ -476,201 +514,130 @@ export abstract class WorkflowRuntime<
|
|
|
476
514
|
export class WorkflowRuntimeContext extends RpcTarget {
|
|
477
515
|
private readonly storage: DurableObjectStorage;
|
|
478
516
|
private readonly sql: SqlStorage;
|
|
479
|
-
private readonly requestId?: string;
|
|
480
517
|
private static readonly BACKOFF_DELAYS = [250, 500, 1_000, 2_000, 4_000, 8_000, 10_000] as const;
|
|
481
518
|
|
|
482
519
|
private static readonly DEFAULT_MAX_ATTEMPTS = 3;
|
|
483
520
|
|
|
484
|
-
constructor(storage: DurableObjectStorage
|
|
521
|
+
constructor(storage: DurableObjectStorage) {
|
|
485
522
|
super();
|
|
486
523
|
this.storage = storage;
|
|
487
524
|
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
525
|
}
|
|
546
526
|
|
|
547
527
|
/**
|
|
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.
|
|
528
|
+
* Loads the `inbound_events` row whose `claimed_by` is this wait step (`waitStepId`).
|
|
551
529
|
*
|
|
552
|
-
*
|
|
530
|
+
* @throws If no such row exists (storage invariant broken or the step is not in a satisfied state with a claim).
|
|
553
531
|
*/
|
|
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
|
-
)
|
|
532
|
+
private getInboundEventForWaitStep<T extends Json | undefined>(stepId: WaitStepId): InboundEvent<T> {
|
|
533
|
+
const [row] = this.sql
|
|
534
|
+
.exec<ClaimedInboundEvent_Row>(`SELECT * FROM inbound_events WHERE claimed_by = ? LIMIT 1`, stepId)
|
|
563
535
|
.toArray();
|
|
564
|
-
|
|
536
|
+
if (row === undefined) {
|
|
537
|
+
throw new Error(
|
|
538
|
+
`Wait step '${stepId}' is satisfied in durable state but no inbound_events row is claimed by this step.`
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
return {
|
|
542
|
+
id: row.id,
|
|
543
|
+
eventName: row.event_name,
|
|
544
|
+
payload: (row.payload === null ? undefined : JSON.parse(row.payload)) as T,
|
|
545
|
+
createdAt: new Date(row.created_at),
|
|
546
|
+
claimedBy: row.claimed_by,
|
|
547
|
+
claimedAt: new Date(row.claimed_at)
|
|
548
|
+
};
|
|
565
549
|
}
|
|
566
550
|
|
|
567
|
-
|
|
551
|
+
getOrCreateRunStep(
|
|
568
552
|
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();
|
|
553
|
+
options: {
|
|
554
|
+
maxAttempts?: number | null;
|
|
555
|
+
parentStepId: RunStepId | null;
|
|
556
|
+
}
|
|
557
|
+
): RunStep & { attempts: RunStepAttempt[] } {
|
|
558
|
+
const [existing] = this.sql.exec<RunStep_Row>("SELECT * FROM steps WHERE id = ? AND type = 'run'", id).toArray();
|
|
559
|
+
if (existing === undefined) {
|
|
615
560
|
const inserted = this.sql
|
|
616
|
-
.exec<
|
|
617
|
-
`INSERT INTO steps (id, type,
|
|
561
|
+
.exec<RunStep_Row>(
|
|
562
|
+
`INSERT INTO steps (id, type, parent_step_id, max_attempts) VALUES (?, 'run', ?, ?) RETURNING *`,
|
|
618
563
|
id,
|
|
619
|
-
|
|
620
|
-
options.
|
|
564
|
+
options.parentStepId,
|
|
565
|
+
options.maxAttempts ?? WorkflowRuntimeContext.DEFAULT_MAX_ATTEMPTS
|
|
621
566
|
)
|
|
622
567
|
.one();
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
568
|
+
return { ...formatRunStep(inserted), attempts: [] };
|
|
569
|
+
} else {
|
|
570
|
+
const attempts = this.sql
|
|
571
|
+
.exec<RunStepAttempt_Row>(
|
|
572
|
+
`SELECT * FROM run_step_attempts WHERE step_id = ? ORDER BY started_at ASC, id ASC`,
|
|
573
|
+
id
|
|
574
|
+
)
|
|
575
|
+
.toArray();
|
|
576
|
+
|
|
577
|
+
return {
|
|
578
|
+
...formatRunStep(existing),
|
|
579
|
+
attempts: attempts.map((attempt) => formatRunStepAttempt(attempt))
|
|
580
|
+
};
|
|
581
|
+
}
|
|
627
582
|
}
|
|
628
583
|
|
|
629
|
-
|
|
584
|
+
getOrCreateSleepStep(id: SleepStepId, options: { wakeAt: Date; parentStepId: RunStepId | null }): SleepStep {
|
|
585
|
+
const [existing] = this.sql
|
|
586
|
+
.exec<SleepStep_Row>("SELECT * FROM steps WHERE id = ? AND type = 'sleep'", id)
|
|
587
|
+
.toArray();
|
|
588
|
+
if (existing !== undefined) {
|
|
589
|
+
return formatSleepStep(existing);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const wakeAt = options.wakeAt.getTime();
|
|
593
|
+
const inserted = this.sql
|
|
594
|
+
.exec<SleepStep_Row>(
|
|
595
|
+
`INSERT INTO steps (id, type, state, target_wake_at, parent_step_id) VALUES (?, 'sleep', 'waiting', ?, ?) RETURNING *`,
|
|
596
|
+
id,
|
|
597
|
+
wakeAt,
|
|
598
|
+
options.parentStepId
|
|
599
|
+
)
|
|
600
|
+
.one();
|
|
601
|
+
return formatSleepStep(inserted);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
getOrCreateWaitStep<T extends Json | undefined>(
|
|
630
605
|
id: WaitStepId,
|
|
631
606
|
options: { eventName: string; timeoutAt?: Date; parentStepId: RunStepId | null }
|
|
632
|
-
):
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
if (existing
|
|
639
|
-
return
|
|
607
|
+
): WaitStep<T> {
|
|
608
|
+
const [existing] = this.sql.exec<WaitStep_Row>("SELECT * FROM steps WHERE id = ? AND type = 'wait'", id).toArray();
|
|
609
|
+
// 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.
|
|
610
|
+
if (existing !== undefined && existing.state !== "waiting") {
|
|
611
|
+
if (existing.state === "satisfied") {
|
|
612
|
+
return formatSatisfiedWaitStep<T>(existing, this.getInboundEventForWaitStep<T>(existing.id).payload);
|
|
613
|
+
} else if (existing.state === "timed_out") {
|
|
614
|
+
return formatTimedOutWaitStep(existing);
|
|
640
615
|
}
|
|
616
|
+
}
|
|
641
617
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
618
|
+
let waiting: Extract<WaitStep_Row, { state: "waiting" }>;
|
|
619
|
+
if (existing !== undefined) {
|
|
620
|
+
waiting = existing;
|
|
621
|
+
} else {
|
|
622
|
+
waiting = this.sql
|
|
623
|
+
.exec<Extract<WaitStep_Row, { state: "waiting" }>>(
|
|
624
|
+
`
|
|
649
625
|
INSERT INTO steps (id, type, state, event_name, timeout_at, parent_step_id)
|
|
650
626
|
VALUES (?, 'wait', 'waiting', ?, ?, ?)
|
|
651
627
|
RETURNING *
|
|
652
628
|
`,
|
|
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
629
|
id,
|
|
662
|
-
"wait_waiting",
|
|
663
630
|
options.eventName,
|
|
664
|
-
options.timeoutAt !== undefined ? options.timeoutAt.getTime() : null
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
631
|
+
options.timeoutAt !== undefined ? options.timeoutAt.getTime() : null,
|
|
632
|
+
options.parentStepId
|
|
633
|
+
)
|
|
634
|
+
.one();
|
|
635
|
+
}
|
|
669
636
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
637
|
+
// Attempt to claim any inbound event that is not claimed yet for the given event name.
|
|
638
|
+
const [claimed] = this.sql
|
|
639
|
+
.exec<{ id: string; payload: string | null }>(
|
|
640
|
+
`
|
|
674
641
|
UPDATE inbound_events
|
|
675
642
|
SET claimed_by = ?,
|
|
676
643
|
claimed_at = CAST(unixepoch('subsecond') * 1000 AS INTEGER)
|
|
@@ -685,280 +652,228 @@ export class WorkflowRuntimeContext extends RpcTarget {
|
|
|
685
652
|
AND claimed_by IS NULL
|
|
686
653
|
RETURNING id, payload
|
|
687
654
|
`,
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
655
|
+
id,
|
|
656
|
+
options.eventName
|
|
657
|
+
)
|
|
658
|
+
.toArray();
|
|
692
659
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
`
|
|
660
|
+
if (claimed !== undefined) {
|
|
661
|
+
const satisfied = this.sql
|
|
662
|
+
.exec<SatisfiedWaitStep_Row>(
|
|
663
|
+
`
|
|
698
664
|
UPDATE steps
|
|
699
665
|
SET state = 'satisfied',
|
|
700
|
-
|
|
701
|
-
resolved_at = CAST(unixepoch('subsecond') * 1000 AS INTEGER),
|
|
702
|
-
timeout_at = NULL
|
|
666
|
+
resolved_at = CAST(unixepoch('subsecond') * 1000 AS INTEGER)
|
|
703
667
|
WHERE id = ?
|
|
704
668
|
AND type = 'wait'
|
|
705
669
|
AND state = 'waiting'
|
|
706
670
|
RETURNING *
|
|
707
671
|
`,
|
|
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
|
-
);
|
|
672
|
+
id
|
|
673
|
+
)
|
|
674
|
+
.one();
|
|
675
|
+
return formatSatisfiedWaitStep<T>(satisfied, claimed.payload === null ? undefined : JSON.parse(claimed.payload));
|
|
676
|
+
}
|
|
722
677
|
|
|
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
|
-
});
|
|
678
|
+
return formatWaitingWaitStep(waiting);
|
|
735
679
|
}
|
|
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
680
|
|
|
758
|
-
|
|
681
|
+
/**
|
|
682
|
+
* True if this run step has at least one **direct** child that is still in progress.
|
|
683
|
+
*/
|
|
684
|
+
hasInProgressChildSteps(stepId: RunStepId): boolean {
|
|
685
|
+
const rows = this.sql
|
|
686
|
+
.exec<{ x: number }>(
|
|
687
|
+
`SELECT 1 AS x FROM steps c
|
|
688
|
+
WHERE c.parent_step_id = ?
|
|
689
|
+
AND (
|
|
690
|
+
(c.type = 'sleep' AND c.state IN ('waiting', 'elapsed'))
|
|
691
|
+
OR (c.type = 'wait' AND c.state IN ('waiting', 'satisfied'))
|
|
692
|
+
OR (
|
|
693
|
+
c.type = 'run'
|
|
694
|
+
AND NOT EXISTS (
|
|
695
|
+
SELECT 1
|
|
696
|
+
FROM run_step_attempts a
|
|
697
|
+
WHERE a.step_id = c.id
|
|
698
|
+
AND a.state = 'failed'
|
|
699
|
+
AND a.next_attempt_at IS NULL
|
|
700
|
+
AND a.id = (
|
|
701
|
+
SELECT a2.id
|
|
702
|
+
FROM run_step_attempts a2
|
|
703
|
+
WHERE a2.step_id = c.id
|
|
704
|
+
ORDER BY a2.started_at DESC, a2.id DESC
|
|
705
|
+
LIMIT 1
|
|
706
|
+
)
|
|
707
|
+
)
|
|
708
|
+
)
|
|
709
|
+
)
|
|
710
|
+
LIMIT 1`,
|
|
711
|
+
stepId
|
|
712
|
+
)
|
|
713
|
+
.toArray();
|
|
714
|
+
return rows.length > 0;
|
|
715
|
+
}
|
|
759
716
|
|
|
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
|
-
}
|
|
717
|
+
handleRunAttemptStarted(stepId: RunStepId): StartedRunStepAttempt {
|
|
718
|
+
// If a run step with the given id does not exist, we throw a 'WorkflowInvariantError' indicating that the step was not found.
|
|
719
|
+
const [existing] = this.sql
|
|
720
|
+
.exec<RunStep_Row>(`SELECT * FROM steps WHERE id = ? AND type = 'run'`, stepId)
|
|
721
|
+
.toArray();
|
|
722
|
+
if (existing === undefined) {
|
|
723
|
+
throw new Error(`Run step '${stepId}' not found.`);
|
|
724
|
+
}
|
|
818
725
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
726
|
+
// Get the last attempt for the step.
|
|
727
|
+
const [lastAttempt] = this.sql
|
|
728
|
+
.exec<RunStepAttempt_Row>(
|
|
729
|
+
"SELECT * FROM run_step_attempts WHERE step_id = ? ORDER BY started_at DESC, id DESC",
|
|
730
|
+
stepId
|
|
731
|
+
)
|
|
732
|
+
.toArray();
|
|
824
733
|
|
|
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;
|
|
734
|
+
// If the last attempt has been started, we throw a 'WorkflowInvariantError' indicating that the attempt is already in progress.
|
|
735
|
+
if (lastAttempt !== undefined && lastAttempt.state === "started") {
|
|
736
|
+
throw new Error(`Attempt '${lastAttempt.id}' for run step '${stepId}' is already in progress.`);
|
|
874
737
|
}
|
|
738
|
+
|
|
739
|
+
// Insert a new attempt for the step and return the new attempt
|
|
740
|
+
const attempt = this.sql
|
|
741
|
+
.exec<StartedRunStepAttempt_Row>(
|
|
742
|
+
`INSERT INTO run_step_attempts (step_id, state) VALUES (?, 'started') RETURNING *`,
|
|
743
|
+
stepId
|
|
744
|
+
)
|
|
745
|
+
.one();
|
|
746
|
+
return formatRunStepAttempt(attempt);
|
|
875
747
|
}
|
|
876
748
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
749
|
+
handleRunAttemptFailed(
|
|
750
|
+
stepId: RunStepId,
|
|
751
|
+
result: {
|
|
752
|
+
errorMessage: string;
|
|
753
|
+
errorName?: string;
|
|
754
|
+
isNonRetryableStepError?: boolean;
|
|
755
|
+
}
|
|
756
|
+
): FailedRunStepAttempt {
|
|
757
|
+
// If a run step with the given id does not exist, we throw a 'WorkflowInvariantError' indicating that the step was not found.
|
|
758
|
+
const [existing] = this.sql
|
|
759
|
+
.exec<RunStep_Row>(`SELECT * FROM steps WHERE id = ? AND type = 'run'`, stepId)
|
|
760
|
+
.toArray();
|
|
761
|
+
if (existing === undefined) {
|
|
762
|
+
throw new Error(`Run step '${stepId}' not found.`);
|
|
763
|
+
}
|
|
885
764
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
765
|
+
// Get the last attempt for the step.
|
|
766
|
+
const attempts = this.sql
|
|
767
|
+
.exec<RunStepAttempt_Row>("SELECT * FROM run_step_attempts WHERE step_id = ? ORDER BY started_at ASC", stepId)
|
|
768
|
+
.toArray();
|
|
769
|
+
|
|
770
|
+
const lastAttempt = attempts[attempts.length - 1];
|
|
771
|
+
if (lastAttempt === undefined) {
|
|
772
|
+
throw new Error(`No attempt in progress for run step '${stepId}'.`);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (result.isNonRetryableStepError || attempts.length === existing.max_attempts) {
|
|
776
|
+
const updated = this.sql
|
|
777
|
+
.exec<Extract<RunStepAttempt_Row, { state: "failed" }>>(
|
|
778
|
+
`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 *`,
|
|
779
|
+
result.errorMessage,
|
|
780
|
+
result.errorName ?? null,
|
|
781
|
+
lastAttempt.id
|
|
782
|
+
)
|
|
783
|
+
.one();
|
|
784
|
+
return formatRunStepAttempt(updated);
|
|
785
|
+
} else {
|
|
786
|
+
const backoff =
|
|
787
|
+
WorkflowRuntimeContext.BACKOFF_DELAYS[attempts.length - 1] ??
|
|
788
|
+
(WorkflowRuntimeContext.BACKOFF_DELAYS[WorkflowRuntimeContext.BACKOFF_DELAYS.length - 1] as number);
|
|
789
|
+
const nextAttemptAt = Date.now() + backoff;
|
|
790
|
+
|
|
791
|
+
const updated = this.sql
|
|
792
|
+
.exec<Extract<RunStepAttempt_Row, { state: "failed" }>>(
|
|
793
|
+
`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 *`,
|
|
794
|
+
result.errorMessage,
|
|
795
|
+
result.errorName ?? null,
|
|
796
|
+
nextAttemptAt,
|
|
797
|
+
lastAttempt.id
|
|
798
|
+
)
|
|
799
|
+
.one();
|
|
800
|
+
return formatRunStepAttempt(updated);
|
|
905
801
|
}
|
|
906
802
|
}
|
|
907
803
|
|
|
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;
|
|
804
|
+
/**
|
|
805
|
+
* Marks the in-flight attempt as succeeded.
|
|
806
|
+
*
|
|
807
|
+
* @param resultJson - Raw JSON string for the result value (`null` when the callback returned `undefined`). The
|
|
808
|
+
* `result_type` discriminator is derived: `null` → `'none'`, non-null → `'json'`.
|
|
809
|
+
*/
|
|
810
|
+
handleRunAttemptSucceeded(stepId: RunStepId, resultJson: string | null): SucceededRunStepAttempt {
|
|
811
|
+
const [existing] = this.sql
|
|
812
|
+
.exec<RunStep_Row>(`SELECT * FROM steps WHERE id = ? AND type = 'run'`, stepId)
|
|
813
|
+
.toArray();
|
|
814
|
+
if (existing === undefined) {
|
|
815
|
+
throw new Error(`Run step '${stepId}' not found.`);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const attempts = this.sql
|
|
819
|
+
.exec<RunStepAttempt_Row>("SELECT * FROM run_step_attempts WHERE step_id = ? ORDER BY started_at ASC", stepId)
|
|
820
|
+
.toArray();
|
|
821
|
+
|
|
822
|
+
const lastAttempt = attempts[attempts.length - 1];
|
|
823
|
+
if (lastAttempt === undefined || lastAttempt?.state !== "started") {
|
|
824
|
+
throw new Error(`No attempt in progress for run step '${stepId}'.`);
|
|
943
825
|
}
|
|
826
|
+
|
|
827
|
+
const resultType = resultJson === null ? "none" : "json";
|
|
828
|
+
const updated = this.sql
|
|
829
|
+
.exec<SucceededRunStepAttempt_Row>(
|
|
830
|
+
`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 *`,
|
|
831
|
+
resultType,
|
|
832
|
+
resultJson,
|
|
833
|
+
lastAttempt.id
|
|
834
|
+
)
|
|
835
|
+
.one();
|
|
836
|
+
return formatRunStepAttempt(updated);
|
|
944
837
|
}
|
|
945
|
-
}
|
|
946
838
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
message.includes("SQLITE_TOOBIG") // String or BLOB too large
|
|
955
|
-
);
|
|
956
|
-
}
|
|
839
|
+
handleSleepStepElapsed(id: SleepStepId): void {
|
|
840
|
+
const [existing] = this.sql
|
|
841
|
+
.exec<SleepStep_Row>("SELECT * FROM steps WHERE id = ? AND type = 'sleep'", id)
|
|
842
|
+
.toArray();
|
|
843
|
+
if (existing === undefined) {
|
|
844
|
+
throw new Error(`Step '${id}' of type 'sleep' not found.`);
|
|
845
|
+
}
|
|
957
846
|
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
this.
|
|
847
|
+
if (existing.state !== "waiting") {
|
|
848
|
+
throw new Error(`Unexpected state for sleep step '${id}'. Expected 'waiting' but got ${existing.state}.`);
|
|
849
|
+
}
|
|
850
|
+
this.sql.exec(
|
|
851
|
+
`UPDATE steps SET state = 'elapsed', resolved_at = CAST(unixepoch('subsecond') * 1000 AS INTEGER) WHERE id = ?`,
|
|
852
|
+
id
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
handleWaitStepTimedOut(id: WaitStepId): void {
|
|
857
|
+
this.storage.transactionSync(() => {
|
|
858
|
+
const [existing] = this.sql
|
|
859
|
+
.exec<WaitStep_Row>("SELECT * FROM steps WHERE id = ? AND type = 'wait'", id)
|
|
860
|
+
.toArray();
|
|
861
|
+
if (existing === undefined) {
|
|
862
|
+
throw new Error(`Step '${id}' of type 'wait' not found.`);
|
|
863
|
+
}
|
|
864
|
+
if (existing.state !== "waiting") {
|
|
865
|
+
throw new Error(`Unexpected state for wait step '${id}'. Expected 'waiting' but got ${existing.state}.`);
|
|
866
|
+
}
|
|
867
|
+
if (existing.timeout_at !== null && existing.timeout_at > Date.now()) {
|
|
868
|
+
throw new Error(
|
|
869
|
+
`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()}.`
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
this.sql.exec(
|
|
873
|
+
"UPDATE steps SET state = 'timed_out', resolved_at = CAST(unixepoch('subsecond') * 1000 AS INTEGER) WHERE id = ?",
|
|
874
|
+
id
|
|
875
|
+
);
|
|
876
|
+
});
|
|
962
877
|
}
|
|
963
878
|
}
|
|
964
879
|
|
|
@@ -966,33 +881,30 @@ export type RunStepId = Brand<string, "RunStepId">;
|
|
|
966
881
|
export type SleepStepId = Brand<string, "SleepStepId">;
|
|
967
882
|
export type WaitStepId = Brand<string, "WaitStepId">;
|
|
968
883
|
|
|
969
|
-
type
|
|
884
|
+
export type RunStepAttempt = {
|
|
885
|
+
id: string;
|
|
886
|
+
stepId: RunStepId;
|
|
887
|
+
startedAt: Date;
|
|
888
|
+
} & (
|
|
889
|
+
| { state: "started" }
|
|
890
|
+
| ({ state: "succeeded"; endedAt: Date } & (
|
|
891
|
+
| { resultType: "json"; resultJson: string }
|
|
892
|
+
| { resultType: "none" }
|
|
893
|
+
))
|
|
894
|
+
| { state: "failed"; errorMessage: string; errorName?: string; endedAt: Date; nextAttemptAt?: Date }
|
|
895
|
+
);
|
|
896
|
+
|
|
897
|
+
export type StartedRunStepAttempt = Extract<RunStepAttempt, { state: "started" }>;
|
|
898
|
+
export type SucceededRunStepAttempt = Extract<RunStepAttempt, { state: "succeeded" }>;
|
|
899
|
+
export type FailedRunStepAttempt = Extract<RunStepAttempt, { state: "failed" }>;
|
|
900
|
+
|
|
901
|
+
export type RunStep = {
|
|
970
902
|
type: "run";
|
|
971
903
|
id: RunStepId;
|
|
972
904
|
createdAt: Date;
|
|
973
|
-
attemptCount: number;
|
|
974
905
|
maxAttempts: number | null;
|
|
975
906
|
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
|
-
);
|
|
907
|
+
};
|
|
996
908
|
|
|
997
909
|
type SleepStep = {
|
|
998
910
|
type: "sleep";
|
|
@@ -1010,7 +922,7 @@ type SleepStep = {
|
|
|
1010
922
|
}
|
|
1011
923
|
);
|
|
1012
924
|
|
|
1013
|
-
type WaitStep = {
|
|
925
|
+
type WaitStep<T extends Json | undefined = Json | undefined> = {
|
|
1014
926
|
type: "wait";
|
|
1015
927
|
id: WaitStepId;
|
|
1016
928
|
createdAt: Date;
|
|
@@ -1023,256 +935,171 @@ type WaitStep = {
|
|
|
1023
935
|
}
|
|
1024
936
|
| {
|
|
1025
937
|
state: "satisfied";
|
|
1026
|
-
payload:
|
|
938
|
+
payload: T;
|
|
1027
939
|
resolvedAt: Date;
|
|
940
|
+
timeoutAt?: Date;
|
|
1028
941
|
}
|
|
1029
942
|
| {
|
|
1030
943
|
state: "timed_out";
|
|
1031
944
|
resolvedAt: Date;
|
|
945
|
+
timeoutAt: Date;
|
|
1032
946
|
}
|
|
1033
947
|
);
|
|
1034
948
|
|
|
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
|
-
}
|
|
949
|
+
type WaitingWaitStep = Extract<WaitStep, { state: "waiting" }>;
|
|
950
|
+
type SatisfiedWaitStep<T extends Json | undefined> = Extract<WaitStep<T>, { state: "satisfied" }>;
|
|
951
|
+
type TimedOutWaitStep = Extract<WaitStep, { state: "timed_out" }>;
|
|
1158
952
|
|
|
1159
953
|
/**
|
|
1160
|
-
*
|
|
954
|
+
* SQLite row shape for `run_step_attempts`.
|
|
955
|
+
*
|
|
956
|
+
* Succeeded attempts use a `result_type` discriminator:
|
|
957
|
+
*
|
|
958
|
+
* - `'json'` → `result_json` holds the raw JSON value (never NULL)
|
|
959
|
+
* - `'none'` → callback returned `undefined`/`void`; no result data
|
|
1161
960
|
*/
|
|
1162
|
-
type
|
|
961
|
+
type RunStepAttempt_Row = {
|
|
1163
962
|
id: string;
|
|
1164
|
-
|
|
1165
|
-
|
|
963
|
+
step_id: RunStepId;
|
|
964
|
+
started_at: number;
|
|
1166
965
|
} & (
|
|
1167
966
|
| {
|
|
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;
|
|
967
|
+
state: "started";
|
|
1198
968
|
}
|
|
969
|
+
| ({
|
|
970
|
+
state: "succeeded";
|
|
971
|
+
ended_at: number;
|
|
972
|
+
} & ({ result_type: "json"; result_json: string } | { result_type: "none" }))
|
|
1199
973
|
| {
|
|
1200
|
-
|
|
974
|
+
state: "failed";
|
|
975
|
+
error_message: string;
|
|
976
|
+
error_name: string | null;
|
|
977
|
+
ended_at: number;
|
|
978
|
+
next_attempt_at: number | null;
|
|
1201
979
|
}
|
|
1202
980
|
);
|
|
1203
981
|
|
|
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":
|
|
982
|
+
type StartedRunStepAttempt_Row = Extract<RunStepAttempt_Row, { state: "started" }>;
|
|
983
|
+
type SucceededRunStepAttempt_Row = Extract<RunStepAttempt_Row, { state: "succeeded" }>;
|
|
984
|
+
type FailedRunStepAttempt_Row = Extract<RunStepAttempt_Row, { state: "failed" }>;
|
|
985
|
+
|
|
986
|
+
function formatRunStepAttempt(attempt: StartedRunStepAttempt_Row): StartedRunStepAttempt;
|
|
987
|
+
function formatRunStepAttempt(attempt: SucceededRunStepAttempt_Row): SucceededRunStepAttempt;
|
|
988
|
+
function formatRunStepAttempt(attempt: FailedRunStepAttempt_Row): FailedRunStepAttempt;
|
|
989
|
+
function formatRunStepAttempt(attempt: RunStepAttempt_Row): RunStepAttempt;
|
|
990
|
+
function formatRunStepAttempt(attempt: RunStepAttempt_Row): RunStepAttempt {
|
|
991
|
+
switch (attempt.state) {
|
|
992
|
+
case "started":
|
|
1235
993
|
return {
|
|
1236
|
-
id:
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
recordedAt: new Date(row.recorded_at)
|
|
994
|
+
id: attempt.id,
|
|
995
|
+
stepId: attempt.step_id,
|
|
996
|
+
startedAt: new Date(attempt.started_at),
|
|
997
|
+
state: "started"
|
|
1241
998
|
};
|
|
1242
|
-
case "
|
|
1243
|
-
|
|
1244
|
-
id:
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
999
|
+
case "succeeded": {
|
|
1000
|
+
const base = {
|
|
1001
|
+
id: attempt.id,
|
|
1002
|
+
stepId: attempt.step_id,
|
|
1003
|
+
startedAt: new Date(attempt.started_at),
|
|
1004
|
+
state: "succeeded" as const,
|
|
1005
|
+
endedAt: new Date(attempt.ended_at)
|
|
1248
1006
|
};
|
|
1249
|
-
|
|
1007
|
+
if (attempt.result_type === "json") {
|
|
1008
|
+
return { ...base, resultType: "json" as const, resultJson: attempt.result_json };
|
|
1009
|
+
}
|
|
1010
|
+
return { ...base, resultType: attempt.result_type };
|
|
1011
|
+
}
|
|
1012
|
+
case "failed":
|
|
1250
1013
|
return {
|
|
1251
|
-
id:
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1014
|
+
id: attempt.id,
|
|
1015
|
+
stepId: attempt.step_id,
|
|
1016
|
+
startedAt: new Date(attempt.started_at),
|
|
1017
|
+
state: "failed",
|
|
1018
|
+
errorMessage: attempt.error_message,
|
|
1019
|
+
errorName: attempt.error_name ?? undefined,
|
|
1020
|
+
endedAt: new Date(attempt.ended_at),
|
|
1021
|
+
nextAttemptAt: attempt.next_attempt_at != null ? new Date(attempt.next_attempt_at) : undefined
|
|
1257
1022
|
};
|
|
1258
|
-
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function formatRunStep(step: RunStep_Row): RunStep {
|
|
1027
|
+
return {
|
|
1028
|
+
type: "run",
|
|
1029
|
+
id: step.id,
|
|
1030
|
+
createdAt: new Date(step.created_at),
|
|
1031
|
+
maxAttempts: step.max_attempts,
|
|
1032
|
+
parentStepId: step.parent_step_id
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function formatSleepStep(step: SleepStep_Row): SleepStep {
|
|
1037
|
+
switch (step.state) {
|
|
1038
|
+
case "waiting":
|
|
1259
1039
|
return {
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1040
|
+
type: "sleep",
|
|
1041
|
+
id: step.id,
|
|
1042
|
+
state: "waiting",
|
|
1043
|
+
wakeAt: new Date(step.target_wake_at),
|
|
1044
|
+
createdAt: new Date(step.created_at),
|
|
1045
|
+
parentStepId: step.parent_step_id
|
|
1046
|
+
} satisfies SleepStep;
|
|
1047
|
+
case "elapsed":
|
|
1267
1048
|
return {
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1049
|
+
type: "sleep",
|
|
1050
|
+
id: step.id,
|
|
1051
|
+
state: "elapsed",
|
|
1052
|
+
resolvedAt: new Date(step.resolved_at),
|
|
1053
|
+
createdAt: new Date(step.created_at),
|
|
1054
|
+
parentStepId: step.parent_step_id
|
|
1055
|
+
} satisfies SleepStep;
|
|
1056
|
+
default:
|
|
1057
|
+
throw new Error("Unexpected sleep step state");
|
|
1273
1058
|
}
|
|
1274
1059
|
}
|
|
1275
1060
|
|
|
1061
|
+
function formatWaitingWaitStep(step: WaitingWaitStep_Row): WaitingWaitStep {
|
|
1062
|
+
return {
|
|
1063
|
+
type: "wait",
|
|
1064
|
+
id: step.id,
|
|
1065
|
+
state: "waiting",
|
|
1066
|
+
eventName: step.event_name,
|
|
1067
|
+
timeoutAt: step.timeout_at != null ? new Date(step.timeout_at) : undefined,
|
|
1068
|
+
createdAt: new Date(step.created_at),
|
|
1069
|
+
parentStepId: step.parent_step_id
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function formatTimedOutWaitStep(step: TimedOutWaitStep_Row): TimedOutWaitStep {
|
|
1074
|
+
return {
|
|
1075
|
+
type: "wait",
|
|
1076
|
+
id: step.id,
|
|
1077
|
+
state: "timed_out",
|
|
1078
|
+
eventName: step.event_name,
|
|
1079
|
+
resolvedAt: new Date(step.resolved_at),
|
|
1080
|
+
createdAt: new Date(step.created_at),
|
|
1081
|
+
parentStepId: step.parent_step_id,
|
|
1082
|
+
timeoutAt: new Date(step.timeout_at)
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function formatSatisfiedWaitStep<T extends Json | undefined>(
|
|
1087
|
+
step: SatisfiedWaitStep_Row,
|
|
1088
|
+
payload: T
|
|
1089
|
+
): SatisfiedWaitStep<T> {
|
|
1090
|
+
return {
|
|
1091
|
+
type: "wait",
|
|
1092
|
+
id: step.id,
|
|
1093
|
+
state: "satisfied",
|
|
1094
|
+
payload: payload,
|
|
1095
|
+
createdAt: new Date(step.created_at),
|
|
1096
|
+
eventName: step.event_name,
|
|
1097
|
+
resolvedAt: new Date(step.resolved_at),
|
|
1098
|
+
parentStepId: step.parent_step_id,
|
|
1099
|
+
timeoutAt: step.timeout_at != null ? new Date(step.timeout_at) : undefined
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1276
1103
|
/**
|
|
1277
1104
|
* SQLite row shape for `workflow_events` (append-only workflow status transitions).
|
|
1278
1105
|
*/
|
|
@@ -1332,128 +1159,56 @@ type RunStep_Row = {
|
|
|
1332
1159
|
* Enclosing run step id when this run step was created inside that run's callback; otherwise null.
|
|
1333
1160
|
*/
|
|
1334
1161
|
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
1162
|
/**
|
|
1351
1163
|
* Maximum number of attempts that can be made for this step. If not present, the step can be retried indefinitely. If
|
|
1352
1164
|
* present, the step can be retried up to this number of times. If the step has reached the maximum number of
|
|
1353
1165
|
* attempts, it will transition to the `failed` state.
|
|
1354
1166
|
*/
|
|
1355
1167
|
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
|
-
);
|
|
1168
|
+
};
|
|
1423
1169
|
|
|
1424
1170
|
type SleepStep_Row = {
|
|
1425
1171
|
id: SleepStepId;
|
|
1426
1172
|
type: "sleep";
|
|
1427
1173
|
created_at: number;
|
|
1428
1174
|
/**
|
|
1429
|
-
*
|
|
1175
|
+
* Id of the enclosing run step if this sleep step was created from within that run step's callback; otherwise null.
|
|
1430
1176
|
*/
|
|
1431
1177
|
parent_step_id: RunStepId | null;
|
|
1432
1178
|
} & (
|
|
1433
1179
|
| {
|
|
1434
1180
|
/**
|
|
1435
|
-
* The sleep step has
|
|
1181
|
+
* The sleep step has started and is not yet terminal.
|
|
1436
1182
|
*
|
|
1437
|
-
*
|
|
1438
|
-
*
|
|
1183
|
+
* In this state, the step is considered active until its target wake time is reached and that transition is
|
|
1184
|
+
* durably recorded.
|
|
1439
1185
|
*/
|
|
1440
1186
|
state: "waiting";
|
|
1441
1187
|
|
|
1442
1188
|
/**
|
|
1443
|
-
*
|
|
1189
|
+
* Target time at or after which this sleep step may transition to `elapsed`.
|
|
1444
1190
|
*
|
|
1445
|
-
*
|
|
1191
|
+
* This is part of the step's configured behavior, not an indicator that the step is still pending.
|
|
1446
1192
|
*/
|
|
1447
|
-
|
|
1193
|
+
target_wake_at: number;
|
|
1448
1194
|
}
|
|
1449
1195
|
| {
|
|
1450
1196
|
/**
|
|
1451
|
-
* The sleep
|
|
1197
|
+
* The sleep step has completed because its target wake time was reached and that outcome was durably recorded.
|
|
1452
1198
|
*/
|
|
1453
1199
|
state: "elapsed";
|
|
1454
1200
|
|
|
1455
1201
|
/**
|
|
1456
|
-
*
|
|
1202
|
+
* Target time at or after which this sleep step became eligible to transition to `elapsed`.
|
|
1203
|
+
*
|
|
1204
|
+
* Retained on terminal rows so the completed step still carries its original timing configuration.
|
|
1205
|
+
*/
|
|
1206
|
+
target_wake_at: number;
|
|
1207
|
+
|
|
1208
|
+
/**
|
|
1209
|
+
* Time at which the transition to `elapsed` was durably recorded.
|
|
1210
|
+
*
|
|
1211
|
+
* This may be equal to or later than `target_wake_at`.
|
|
1457
1212
|
*/
|
|
1458
1213
|
resolved_at: number;
|
|
1459
1214
|
}
|
|
@@ -1464,177 +1219,80 @@ type WaitStep_Row = {
|
|
|
1464
1219
|
type: "wait";
|
|
1465
1220
|
created_at: number;
|
|
1466
1221
|
/**
|
|
1467
|
-
*
|
|
1222
|
+
* Id of the enclosing run step if this wait step was created from within that run step's callback; otherwise null.
|
|
1468
1223
|
*/
|
|
1469
1224
|
parent_step_id: RunStepId | null;
|
|
1470
1225
|
|
|
1471
1226
|
/**
|
|
1472
|
-
* Name of the inbound event that can satisfy this step.
|
|
1227
|
+
* Name of the inbound event that can satisfy this wait step.
|
|
1473
1228
|
*/
|
|
1474
1229
|
event_name: string;
|
|
1475
1230
|
} & (
|
|
1476
1231
|
| {
|
|
1477
1232
|
/**
|
|
1478
|
-
* The step has
|
|
1479
|
-
*
|
|
1233
|
+
* The wait step has started and is not yet terminal.
|
|
1234
|
+
*
|
|
1235
|
+
* In this state, the expected event has not yet been durably matched to the step, and no timeout outcome has been
|
|
1236
|
+
* durably recorded.
|
|
1480
1237
|
*/
|
|
1481
1238
|
state: "waiting";
|
|
1482
1239
|
|
|
1483
1240
|
/**
|
|
1484
|
-
* Optional
|
|
1241
|
+
* Optional time at or after which this wait step may transition to `timed_out`.
|
|
1485
1242
|
*
|
|
1486
|
-
* - If
|
|
1487
|
-
* - If
|
|
1243
|
+
* - If null, the step may wait indefinitely.
|
|
1244
|
+
* - If set, reaching or passing this time makes the step eligible to time out.
|
|
1488
1245
|
*/
|
|
1489
1246
|
timeout_at: number | null;
|
|
1490
1247
|
}
|
|
1491
1248
|
| {
|
|
1492
1249
|
/**
|
|
1493
|
-
* The
|
|
1250
|
+
* The wait step has completed because a matching event was durably received and recorded.
|
|
1494
1251
|
*/
|
|
1495
1252
|
state: "satisfied";
|
|
1496
1253
|
|
|
1497
1254
|
/**
|
|
1498
|
-
*
|
|
1255
|
+
* Optional timeout that applied while this step was active.
|
|
1256
|
+
*
|
|
1257
|
+
* Retained on terminal rows so the completed step still carries its original timing configuration.
|
|
1499
1258
|
*/
|
|
1500
|
-
|
|
1259
|
+
timeout_at: number | null;
|
|
1501
1260
|
|
|
1502
1261
|
/**
|
|
1503
|
-
* Time at which the
|
|
1262
|
+
* Time at which the transition to `satisfied` was durably recorded.
|
|
1504
1263
|
*/
|
|
1505
1264
|
resolved_at: number;
|
|
1506
1265
|
}
|
|
1507
1266
|
| {
|
|
1508
1267
|
/**
|
|
1509
|
-
* The step
|
|
1268
|
+
* The wait step has completed by timing out before a matching event was durably received.
|
|
1510
1269
|
*/
|
|
1511
1270
|
state: "timed_out";
|
|
1512
1271
|
|
|
1513
1272
|
/**
|
|
1514
|
-
* Time at which
|
|
1273
|
+
* Time at or after which this step became eligible to transition to `timed_out`.
|
|
1274
|
+
*/
|
|
1275
|
+
timeout_at: number;
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* Time at which the transition to `timed_out` was durably recorded.
|
|
1279
|
+
*
|
|
1280
|
+
* This may be equal to or later than `timeout_at`.
|
|
1515
1281
|
*/
|
|
1516
1282
|
resolved_at: number;
|
|
1517
1283
|
}
|
|
1518
1284
|
);
|
|
1519
1285
|
|
|
1286
|
+
type WaitingWaitStep_Row = Extract<WaitStep_Row, { state: "waiting" }>;
|
|
1287
|
+
type SatisfiedWaitStep_Row = Extract<WaitStep_Row, { state: "satisfied" }>;
|
|
1288
|
+
type TimedOutWaitStep_Row = Extract<WaitStep_Row, { state: "timed_out" }>;
|
|
1289
|
+
|
|
1520
1290
|
/**
|
|
1521
1291
|
* A step row is created once the workflow reaches that step. From that point on, `status` describes the step's durable
|
|
1522
1292
|
* lifecycle state.
|
|
1523
1293
|
*/
|
|
1524
1294
|
type Step_Row = RunStep_Row | SleepStep_Row | WaitStep_Row;
|
|
1525
1295
|
|
|
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
1296
|
export type WorkflowStatus =
|
|
1639
1297
|
| "pending" // The workflow has been created but 'run' hasn't been called yet
|
|
1640
1298
|
| "running" // The workflow is currently executing; steps are being created/processed
|
|
@@ -1651,55 +1309,56 @@ type WorkflowMetadata_Row<TVersion extends string> = {
|
|
|
1651
1309
|
definition_input: string | null;
|
|
1652
1310
|
};
|
|
1653
1311
|
|
|
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
1312
|
/**
|
|
1674
1313
|
* Durable record of inbound events (`inbound_events`) that may satisfy wait steps.
|
|
1675
1314
|
*
|
|
1676
1315
|
* Persists delivered signals across restarts so step resolution is based on durable state rather than in-memory state.
|
|
1677
1316
|
*/
|
|
1678
|
-
|
|
1679
|
-
/**
|
|
1680
|
-
* Unique identifier for the event.
|
|
1681
|
-
*/
|
|
1317
|
+
type InboundEvent_Row = {
|
|
1682
1318
|
id: string;
|
|
1683
|
-
/**
|
|
1684
|
-
* Name of the event.
|
|
1685
|
-
*
|
|
1686
|
-
* Used by wait steps to determine whether this event can satisfy them.
|
|
1687
|
-
*/
|
|
1688
1319
|
event_name: string;
|
|
1689
1320
|
/**
|
|
1690
|
-
*
|
|
1691
|
-
*/
|
|
1692
|
-
payload: string;
|
|
1693
|
-
/**
|
|
1694
|
-
* Time the event was durably recorded.
|
|
1321
|
+
* Raw JSON value, or SQL NULL for undefined (no payload).
|
|
1695
1322
|
*/
|
|
1323
|
+
payload: string | null;
|
|
1696
1324
|
created_at: number;
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1325
|
+
} & (
|
|
1326
|
+
| {
|
|
1327
|
+
/**
|
|
1328
|
+
* Step that claimed the event.
|
|
1329
|
+
*/
|
|
1330
|
+
claimed_by: WaitStepId;
|
|
1331
|
+
/**
|
|
1332
|
+
* Time the event was claimed.
|
|
1333
|
+
*/
|
|
1334
|
+
claimed_at: number;
|
|
1335
|
+
}
|
|
1336
|
+
| {
|
|
1337
|
+
claimed_by: null;
|
|
1338
|
+
claimed_at: null;
|
|
1339
|
+
}
|
|
1340
|
+
);
|
|
1341
|
+
|
|
1342
|
+
type ClaimedInboundEvent_Row = Extract<
|
|
1343
|
+
InboundEvent_Row,
|
|
1344
|
+
{
|
|
1345
|
+
claimed_by: WaitStepId;
|
|
1346
|
+
claimed_at: number;
|
|
1347
|
+
}
|
|
1348
|
+
>;
|
|
1349
|
+
|
|
1350
|
+
type InboundEvent<T extends Json | undefined = Json | undefined> = {
|
|
1351
|
+
id: string;
|
|
1352
|
+
eventName: string;
|
|
1353
|
+
payload: T;
|
|
1354
|
+
createdAt: Date;
|
|
1355
|
+
} & (
|
|
1356
|
+
| {
|
|
1357
|
+
claimedBy: WaitStepId;
|
|
1358
|
+
claimedAt: Date;
|
|
1359
|
+
}
|
|
1360
|
+
| {
|
|
1361
|
+
claimedBy?: never;
|
|
1362
|
+
claimedAt?: never;
|
|
1363
|
+
}
|
|
1364
|
+
);
|