pg-workflows 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/dist/index.cjs +285 -135
- package/dist/index.d.cts +8 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +285 -135
- package/dist/index.js.map +5 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -441,6 +441,41 @@ await engine.resumeWorkflow({
|
|
|
441
441
|
});
|
|
442
442
|
```
|
|
443
443
|
|
|
444
|
+
### Fast-Forward
|
|
445
|
+
|
|
446
|
+
Skip the current waiting step and immediately resume execution. `fastForwardWorkflow` inspects the paused step and dispatches the right internal action — `triggerEvent` for `waitFor`, timeout triggers for `delay`/`waitUntil`, resume for `pause`, and direct output writes for `poll`. If the workflow is not paused, it's a no-op.
|
|
447
|
+
|
|
448
|
+
This is useful for testing, debugging, or manually advancing workflows past long waits.
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
// Fast-forward a waitFor step, providing mock event data
|
|
452
|
+
await engine.fastForwardWorkflow({
|
|
453
|
+
runId: run.id,
|
|
454
|
+
resourceId: 'user-123',
|
|
455
|
+
data: { approved: true, reviewer: 'admin' },
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// Fast-forward a delay/waitUntil step (no data needed)
|
|
459
|
+
await engine.fastForwardWorkflow({
|
|
460
|
+
runId: run.id,
|
|
461
|
+
resourceId: 'user-123',
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// Fast-forward a poll step with mock result data
|
|
465
|
+
await engine.fastForwardWorkflow({
|
|
466
|
+
runId: run.id,
|
|
467
|
+
resourceId: 'user-123',
|
|
468
|
+
data: { paymentId: 'pay_123', status: 'completed' },
|
|
469
|
+
});
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
| Paused step type | Behavior |
|
|
473
|
+
|------------------|----------|
|
|
474
|
+
| `step.waitFor()` | Triggers the event with `data` (defaults to `{}`) |
|
|
475
|
+
| `step.delay()` / `step.waitUntil()` | Triggers the timeout event to skip the wait |
|
|
476
|
+
| `step.poll()` | Writes `data` as the poll result and triggers resolution |
|
|
477
|
+
| `step.pause()` | Delegates to `resumeWorkflow()` |
|
|
478
|
+
|
|
444
479
|
---
|
|
445
480
|
|
|
446
481
|
## Examples
|
|
@@ -588,6 +623,7 @@ When `boss` is omitted, pg-boss is created automatically with an isolated schema
|
|
|
588
623
|
| `resumeWorkflow({ runId, resourceId?, options? })` | Resume a paused workflow |
|
|
589
624
|
| `cancelWorkflow({ runId, resourceId? })` | Cancel a workflow |
|
|
590
625
|
| `triggerEvent({ runId, resourceId?, eventName, data?, options? })` | Send an event to a workflow |
|
|
626
|
+
| `fastForwardWorkflow({ runId, resourceId?, data? })` | Skip the current waiting step and resume execution |
|
|
591
627
|
| `getRun({ runId, resourceId? })` | Get workflow run details |
|
|
592
628
|
| `checkProgress({ runId, resourceId? })` | Get workflow progress |
|
|
593
629
|
| `getRuns(filters)` | List workflow runs with pagination |
|
package/dist/index.cjs
CHANGED
|
@@ -271,15 +271,23 @@ async function runMigrations(db) {
|
|
|
271
271
|
job_id varchar(256)
|
|
272
272
|
);
|
|
273
273
|
`, []);
|
|
274
|
-
await db.executeSql(`
|
|
275
|
-
CREATE INDEX workflow_runs_workflow_id_idx ON workflow_runs USING btree (workflow_id);
|
|
276
|
-
`, []);
|
|
277
274
|
await db.executeSql(`
|
|
278
275
|
CREATE INDEX workflow_runs_created_at_idx ON workflow_runs USING btree (created_at);
|
|
276
|
+
CREATE INDEX workflow_runs_resource_id_created_at_idx ON workflow_runs USING btree (resource_id, created_at DESC);
|
|
277
|
+
CREATE INDEX workflow_runs_status_created_at_idx ON workflow_runs USING btree (status, created_at DESC);
|
|
278
|
+
CREATE INDEX workflow_runs_workflow_id_created_at_idx ON workflow_runs USING btree (workflow_id, created_at DESC);
|
|
279
|
+
CREATE INDEX workflow_runs_resource_id_workflow_id_created_at_idx ON workflow_runs USING btree (resource_id, workflow_id, created_at DESC);
|
|
279
280
|
`, []);
|
|
281
|
+
} else {
|
|
280
282
|
await db.executeSql(`
|
|
281
|
-
|
|
282
|
-
|
|
283
|
+
DROP INDEX IF EXISTS workflow_runs_workflow_id_idx;
|
|
284
|
+
DROP INDEX IF EXISTS workflow_runs_resource_id_idx;
|
|
285
|
+
CREATE INDEX IF NOT EXISTS workflow_runs_created_at_idx ON workflow_runs USING btree (created_at);
|
|
286
|
+
CREATE INDEX IF NOT EXISTS workflow_runs_resource_id_created_at_idx ON workflow_runs USING btree (resource_id, created_at DESC);
|
|
287
|
+
CREATE INDEX IF NOT EXISTS workflow_runs_status_created_at_idx ON workflow_runs USING btree (status, created_at DESC);
|
|
288
|
+
CREATE INDEX IF NOT EXISTS workflow_runs_workflow_id_created_at_idx ON workflow_runs USING btree (workflow_id, created_at DESC);
|
|
289
|
+
CREATE INDEX IF NOT EXISTS workflow_runs_resource_id_workflow_id_created_at_idx ON workflow_runs USING btree (resource_id, workflow_id, created_at DESC);
|
|
290
|
+
`, []);
|
|
283
291
|
}
|
|
284
292
|
}
|
|
285
293
|
|
|
@@ -322,13 +330,13 @@ async function insertWorkflowRun({
|
|
|
322
330
|
const runId = generateKSUID("run");
|
|
323
331
|
const now = new Date;
|
|
324
332
|
const result = await db.executeSql(`INSERT INTO workflow_runs (
|
|
325
|
-
id,
|
|
326
|
-
resource_id,
|
|
327
|
-
workflow_id,
|
|
328
|
-
current_step_id,
|
|
329
|
-
status,
|
|
330
|
-
input,
|
|
331
|
-
max_retries,
|
|
333
|
+
id,
|
|
334
|
+
resource_id,
|
|
335
|
+
workflow_id,
|
|
336
|
+
current_step_id,
|
|
337
|
+
status,
|
|
338
|
+
input,
|
|
339
|
+
max_retries,
|
|
332
340
|
timeout_at,
|
|
333
341
|
created_at,
|
|
334
342
|
updated_at,
|
|
@@ -375,7 +383,8 @@ async function getWorkflowRun({
|
|
|
375
383
|
async function updateWorkflowRun({
|
|
376
384
|
runId,
|
|
377
385
|
resourceId,
|
|
378
|
-
data
|
|
386
|
+
data,
|
|
387
|
+
expectedStatuses
|
|
379
388
|
}, db) {
|
|
380
389
|
const now = new Date;
|
|
381
390
|
const updates = ["updated_at = $1"];
|
|
@@ -431,10 +440,20 @@ async function updateWorkflowRun({
|
|
|
431
440
|
values.push(data.jobId);
|
|
432
441
|
paramIndex++;
|
|
433
442
|
}
|
|
434
|
-
const whereClause = resourceId ? `WHERE id = $${paramIndex} AND resource_id = $${paramIndex + 1}` : `WHERE id = $${paramIndex}`;
|
|
435
443
|
values.push(runId);
|
|
444
|
+
const idParam = paramIndex;
|
|
445
|
+
paramIndex++;
|
|
436
446
|
if (resourceId) {
|
|
437
447
|
values.push(resourceId);
|
|
448
|
+
paramIndex++;
|
|
449
|
+
}
|
|
450
|
+
if (expectedStatuses && expectedStatuses.length > 0) {
|
|
451
|
+
values.push(expectedStatuses);
|
|
452
|
+
paramIndex++;
|
|
453
|
+
}
|
|
454
|
+
let whereClause = resourceId ? `WHERE id = $${idParam} AND resource_id = $${idParam + 1}` : `WHERE id = $${idParam}`;
|
|
455
|
+
if (expectedStatuses && expectedStatuses.length > 0) {
|
|
456
|
+
whereClause += ` AND status = ANY($${paramIndex - 1})`;
|
|
438
457
|
}
|
|
439
458
|
const query = `
|
|
440
459
|
UPDATE workflow_runs
|
|
@@ -475,37 +494,50 @@ async function getWorkflowRuns({
|
|
|
475
494
|
values.push(workflowId);
|
|
476
495
|
paramIndex++;
|
|
477
496
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
497
|
+
const cursorIds = [startingAfter, endingBefore].filter(Boolean);
|
|
498
|
+
if (cursorIds.length > 0) {
|
|
499
|
+
const cursorResult = await db.executeSql("SELECT id, created_at FROM workflow_runs WHERE id = ANY($1)", [cursorIds]);
|
|
500
|
+
const cursorMap = new Map;
|
|
501
|
+
for (const row of cursorResult.rows) {
|
|
502
|
+
cursorMap.set(row.id, typeof row.created_at === "string" ? new Date(row.created_at) : row.created_at);
|
|
484
503
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
504
|
+
if (startingAfter) {
|
|
505
|
+
const cursor = cursorMap.get(startingAfter);
|
|
506
|
+
if (cursor) {
|
|
507
|
+
conditions.push(`created_at < $${paramIndex}`);
|
|
508
|
+
values.push(cursor);
|
|
509
|
+
paramIndex++;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (endingBefore) {
|
|
513
|
+
const cursor = cursorMap.get(endingBefore);
|
|
514
|
+
if (cursor) {
|
|
515
|
+
conditions.push(`created_at > $${paramIndex}`);
|
|
516
|
+
values.push(cursor);
|
|
517
|
+
paramIndex++;
|
|
518
|
+
}
|
|
492
519
|
}
|
|
493
520
|
}
|
|
494
521
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
495
522
|
const actualLimit = Math.min(Math.max(limit, 1), 100) + 1;
|
|
523
|
+
const isBackward = !!endingBefore && !startingAfter;
|
|
496
524
|
const query = `
|
|
497
525
|
SELECT * FROM workflow_runs
|
|
498
526
|
${whereClause}
|
|
499
|
-
ORDER BY created_at DESC
|
|
527
|
+
ORDER BY created_at ${isBackward ? "ASC" : "DESC"}
|
|
500
528
|
LIMIT $${paramIndex}
|
|
501
529
|
`;
|
|
502
530
|
values.push(actualLimit);
|
|
503
531
|
const result = await db.executeSql(query, values);
|
|
504
532
|
const rows = result.rows;
|
|
505
|
-
const
|
|
506
|
-
const rawItems =
|
|
533
|
+
const hasExtraRow = rows.length > (limit ?? 20);
|
|
534
|
+
const rawItems = hasExtraRow ? rows.slice(0, limit) : rows;
|
|
535
|
+
if (isBackward) {
|
|
536
|
+
rawItems.reverse();
|
|
537
|
+
}
|
|
507
538
|
const items = rawItems.map((row) => mapRowToWorkflowRun(row));
|
|
508
|
-
const
|
|
539
|
+
const hasMore = isBackward ? items.length > 0 : hasExtraRow;
|
|
540
|
+
const hasPrev = isBackward ? hasExtraRow : !!startingAfter && items.length > 0;
|
|
509
541
|
const nextCursor = hasMore && items.length > 0 ? items[items.length - 1]?.id ?? null : null;
|
|
510
542
|
const prevCursor = hasPrev && items.length > 0 ? items[0]?.id ?? null : null;
|
|
511
543
|
return { items, nextCursor, prevCursor, hasMore, hasPrev };
|
|
@@ -711,7 +743,8 @@ class WorkflowEngine {
|
|
|
711
743
|
data: {
|
|
712
744
|
status: "paused" /* PAUSED */,
|
|
713
745
|
pausedAt: new Date
|
|
714
|
-
}
|
|
746
|
+
},
|
|
747
|
+
expectedStatuses: ["running" /* RUNNING */, "pending" /* PENDING */]
|
|
715
748
|
});
|
|
716
749
|
this.logger.log("Paused workflow run", {
|
|
717
750
|
runId,
|
|
@@ -725,6 +758,10 @@ class WorkflowEngine {
|
|
|
725
758
|
options
|
|
726
759
|
}) {
|
|
727
760
|
await this.checkIfHasStarted();
|
|
761
|
+
const current = await this.getRun({ runId, resourceId });
|
|
762
|
+
if (current.status !== "paused" /* PAUSED */) {
|
|
763
|
+
throw new WorkflowEngineError(`Cannot resume workflow run in '${current.status}' status, must be 'paused'`, current.workflowId, runId);
|
|
764
|
+
}
|
|
728
765
|
return this.triggerEvent({
|
|
729
766
|
runId,
|
|
730
767
|
resourceId,
|
|
@@ -733,6 +770,51 @@ class WorkflowEngine {
|
|
|
733
770
|
options
|
|
734
771
|
});
|
|
735
772
|
}
|
|
773
|
+
async fastForwardWorkflow({
|
|
774
|
+
runId,
|
|
775
|
+
resourceId,
|
|
776
|
+
data
|
|
777
|
+
}) {
|
|
778
|
+
await this.checkIfHasStarted();
|
|
779
|
+
const run = await this.getRun({ runId, resourceId });
|
|
780
|
+
if (run.status !== "paused" /* PAUSED */) {
|
|
781
|
+
return run;
|
|
782
|
+
}
|
|
783
|
+
const stepId = run.currentStepId;
|
|
784
|
+
const waitForStep = this.getWaitForStepEntry(run.timeline, stepId);
|
|
785
|
+
if (!waitForStep) {
|
|
786
|
+
return run;
|
|
787
|
+
}
|
|
788
|
+
const { eventName, timeoutEvent, skipOutput } = waitForStep.waitFor;
|
|
789
|
+
if (eventName === PAUSE_EVENT_NAME) {
|
|
790
|
+
return this.resumeWorkflow({ runId, resourceId });
|
|
791
|
+
}
|
|
792
|
+
if (skipOutput && timeoutEvent) {
|
|
793
|
+
await withPostgresTransaction(this.db, async (db) => {
|
|
794
|
+
const freshRun = await this.getRun({ runId, resourceId }, { exclusiveLock: true, db });
|
|
795
|
+
return this.updateRun({
|
|
796
|
+
runId,
|
|
797
|
+
resourceId,
|
|
798
|
+
data: {
|
|
799
|
+
timeline: import_es_toolkit.merge(freshRun.timeline, {
|
|
800
|
+
[stepId]: {
|
|
801
|
+
output: data ?? {},
|
|
802
|
+
timestamp: new Date
|
|
803
|
+
}
|
|
804
|
+
})
|
|
805
|
+
}
|
|
806
|
+
}, { db });
|
|
807
|
+
}, this.pool);
|
|
808
|
+
return this.triggerEvent({ runId, resourceId, eventName: timeoutEvent });
|
|
809
|
+
}
|
|
810
|
+
if (eventName) {
|
|
811
|
+
return this.triggerEvent({ runId, resourceId, eventName, data: data ?? {} });
|
|
812
|
+
}
|
|
813
|
+
if (timeoutEvent) {
|
|
814
|
+
return this.triggerEvent({ runId, resourceId, eventName: timeoutEvent, data: data ?? {} });
|
|
815
|
+
}
|
|
816
|
+
return run;
|
|
817
|
+
}
|
|
736
818
|
async cancelWorkflow({
|
|
737
819
|
runId,
|
|
738
820
|
resourceId
|
|
@@ -743,7 +825,8 @@ class WorkflowEngine {
|
|
|
743
825
|
resourceId,
|
|
744
826
|
data: {
|
|
745
827
|
status: "cancelled" /* CANCELLED */
|
|
746
|
-
}
|
|
828
|
+
},
|
|
829
|
+
expectedStatuses: ["pending" /* PENDING */, "running" /* RUNNING */, "paused" /* PAUSED */]
|
|
747
830
|
});
|
|
748
831
|
this.logger.log(`cancelled workflow run with id ${runId}`);
|
|
749
832
|
return run;
|
|
@@ -768,7 +851,7 @@ class WorkflowEngine {
|
|
|
768
851
|
data
|
|
769
852
|
}
|
|
770
853
|
};
|
|
771
|
-
this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
|
|
854
|
+
await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
|
|
772
855
|
expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds
|
|
773
856
|
});
|
|
774
857
|
this.logger.log(`event ${eventName} sent for workflow run with id ${runId}`);
|
|
@@ -784,10 +867,17 @@ class WorkflowEngine {
|
|
|
784
867
|
async updateRun({
|
|
785
868
|
runId,
|
|
786
869
|
resourceId,
|
|
787
|
-
data
|
|
870
|
+
data,
|
|
871
|
+
expectedStatuses
|
|
788
872
|
}, { db } = {}) {
|
|
789
|
-
const run = await updateWorkflowRun({ runId, resourceId, data }, db ?? this.db);
|
|
873
|
+
const run = await updateWorkflowRun({ runId, resourceId, data, expectedStatuses }, db ?? this.db);
|
|
790
874
|
if (!run) {
|
|
875
|
+
if (expectedStatuses) {
|
|
876
|
+
const current = await getWorkflowRun({ runId, resourceId }, { db: db ?? this.db });
|
|
877
|
+
if (current) {
|
|
878
|
+
throw new WorkflowEngineError(`Cannot update workflow run in '${current.status}' status, expected: ${expectedStatuses.join(", ")}`, current.workflowId, runId);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
791
881
|
throw new WorkflowRunNotFoundError(runId);
|
|
792
882
|
}
|
|
793
883
|
return run;
|
|
@@ -870,36 +960,40 @@ class WorkflowEngine {
|
|
|
870
960
|
throw new WorkflowEngineError("Missing current step id", workflowId, runId);
|
|
871
961
|
}
|
|
872
962
|
if (run.status === "paused" /* PAUSED */) {
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
const
|
|
881
|
-
const
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
963
|
+
run = await withPostgresTransaction(this.db, async (db) => {
|
|
964
|
+
const lockedRun = await this.getRun({ runId, resourceId: scopedResourceId }, { exclusiveLock: true, db });
|
|
965
|
+
if (lockedRun.status !== "paused" /* PAUSED */) {
|
|
966
|
+
return lockedRun;
|
|
967
|
+
}
|
|
968
|
+
const waitForStep = this.getWaitForStepEntry(lockedRun.timeline, lockedRun.currentStepId);
|
|
969
|
+
const currentStep = this.getCachedStepEntry(lockedRun.timeline, lockedRun.currentStepId);
|
|
970
|
+
const waitFor = waitForStep?.waitFor;
|
|
971
|
+
const hasCurrentStepOutput = currentStep?.output !== undefined;
|
|
972
|
+
const eventMatches = waitFor && event?.name && (event.name === waitFor.eventName || event.name === waitFor.timeoutEvent) && !hasCurrentStepOutput;
|
|
973
|
+
if (eventMatches) {
|
|
974
|
+
const isTimeout = event?.name === waitFor?.timeoutEvent;
|
|
975
|
+
const skipOutput = waitFor?.skipOutput;
|
|
976
|
+
return this.updateRun({
|
|
977
|
+
runId,
|
|
978
|
+
resourceId: scopedResourceId,
|
|
979
|
+
data: {
|
|
980
|
+
status: "running" /* RUNNING */,
|
|
981
|
+
pausedAt: null,
|
|
982
|
+
resumedAt: new Date,
|
|
983
|
+
jobId: job?.id,
|
|
984
|
+
...skipOutput ? {} : {
|
|
985
|
+
timeline: import_es_toolkit.merge(lockedRun.timeline, {
|
|
986
|
+
[lockedRun.currentStepId]: {
|
|
987
|
+
output: event?.data ?? {},
|
|
988
|
+
...isTimeout ? { timedOut: true } : {},
|
|
989
|
+
timestamp: new Date
|
|
990
|
+
}
|
|
991
|
+
})
|
|
992
|
+
}
|
|
898
993
|
}
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
run = await this.updateRun({
|
|
994
|
+
}, { db });
|
|
995
|
+
}
|
|
996
|
+
return this.updateRun({
|
|
903
997
|
runId,
|
|
904
998
|
resourceId: scopedResourceId,
|
|
905
999
|
data: {
|
|
@@ -908,8 +1002,8 @@ class WorkflowEngine {
|
|
|
908
1002
|
resumedAt: new Date,
|
|
909
1003
|
jobId: job?.id
|
|
910
1004
|
}
|
|
911
|
-
});
|
|
912
|
-
}
|
|
1005
|
+
}, { db });
|
|
1006
|
+
}, this.pool);
|
|
913
1007
|
}
|
|
914
1008
|
const baseStep = {
|
|
915
1009
|
run: async (stepId, handler) => {
|
|
@@ -1039,6 +1133,10 @@ class WorkflowEngine {
|
|
|
1039
1133
|
const stepEntry = timeline[stepId];
|
|
1040
1134
|
return stepEntry && typeof stepEntry === "object" && "output" in stepEntry ? stepEntry : null;
|
|
1041
1135
|
}
|
|
1136
|
+
getWaitForStepEntry(timeline, stepId) {
|
|
1137
|
+
const entry = timeline[`${stepId}-wait-for`];
|
|
1138
|
+
return entry && typeof entry === "object" && "waitFor" in entry ? entry : null;
|
|
1139
|
+
}
|
|
1042
1140
|
async runStep({
|
|
1043
1141
|
stepId,
|
|
1044
1142
|
run,
|
|
@@ -1080,7 +1178,7 @@ class WorkflowEngine {
|
|
|
1080
1178
|
runId: run.id,
|
|
1081
1179
|
resourceId: run.resourceId ?? undefined,
|
|
1082
1180
|
data: {
|
|
1083
|
-
timeline: import_es_toolkit.merge(
|
|
1181
|
+
timeline: import_es_toolkit.merge(persistedRun.timeline, {
|
|
1084
1182
|
[stepId]: {
|
|
1085
1183
|
output,
|
|
1086
1184
|
timestamp: new Date
|
|
@@ -1125,33 +1223,45 @@ ${error.stack}` : String(error)
|
|
|
1125
1223
|
return cached.timedOut ? undefined : cached.output;
|
|
1126
1224
|
}
|
|
1127
1225
|
const timeoutEvent = timeoutDate ? `__timeout_${stepId}` : undefined;
|
|
1128
|
-
await this.
|
|
1129
|
-
runId: run.id,
|
|
1130
|
-
|
|
1131
|
-
data: {
|
|
1132
|
-
status: "paused" /* PAUSED */,
|
|
1133
|
-
currentStepId: stepId,
|
|
1134
|
-
pausedAt: new Date,
|
|
1135
|
-
timeline: import_es_toolkit.merge(run.timeline, {
|
|
1136
|
-
[`${stepId}-wait-for`]: {
|
|
1137
|
-
waitFor: { eventName, timeoutEvent },
|
|
1138
|
-
timestamp: new Date
|
|
1139
|
-
}
|
|
1140
|
-
})
|
|
1141
|
-
}
|
|
1142
|
-
});
|
|
1143
|
-
if (timeoutDate && timeoutEvent) {
|
|
1144
|
-
const job = {
|
|
1226
|
+
await withPostgresTransaction(this.db, async (db) => {
|
|
1227
|
+
const freshRun = await this.getRun({ runId: run.id, resourceId: run.resourceId ?? undefined }, { exclusiveLock: true, db });
|
|
1228
|
+
return this.updateRun({
|
|
1145
1229
|
runId: run.id,
|
|
1146
1230
|
resourceId: run.resourceId ?? undefined,
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1231
|
+
data: {
|
|
1232
|
+
status: "paused" /* PAUSED */,
|
|
1233
|
+
currentStepId: stepId,
|
|
1234
|
+
pausedAt: new Date,
|
|
1235
|
+
timeline: import_es_toolkit.merge(freshRun.timeline, {
|
|
1236
|
+
[`${stepId}-wait-for`]: {
|
|
1237
|
+
waitFor: { eventName, timeoutEvent },
|
|
1238
|
+
timestamp: new Date
|
|
1239
|
+
}
|
|
1240
|
+
})
|
|
1241
|
+
}
|
|
1242
|
+
}, { db });
|
|
1243
|
+
}, this.pool);
|
|
1244
|
+
if (timeoutDate && timeoutEvent) {
|
|
1245
|
+
try {
|
|
1246
|
+
const job = {
|
|
1247
|
+
runId: run.id,
|
|
1248
|
+
resourceId: run.resourceId ?? undefined,
|
|
1249
|
+
workflowId: run.workflowId,
|
|
1250
|
+
input: run.input,
|
|
1251
|
+
event: { name: timeoutEvent, data: { date: timeoutDate.toISOString() } }
|
|
1252
|
+
};
|
|
1253
|
+
await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
|
|
1254
|
+
startAfter: timeoutDate.getTime() <= Date.now() ? new Date : timeoutDate,
|
|
1255
|
+
expireInSeconds: defaultExpireInSeconds
|
|
1256
|
+
});
|
|
1257
|
+
} catch (error) {
|
|
1258
|
+
await this.updateRun({
|
|
1259
|
+
runId: run.id,
|
|
1260
|
+
resourceId: run.resourceId ?? undefined,
|
|
1261
|
+
data: { status: "running" /* RUNNING */, pausedAt: null }
|
|
1262
|
+
});
|
|
1263
|
+
throw error;
|
|
1264
|
+
}
|
|
1155
1265
|
}
|
|
1156
1266
|
this.logger.log(`Step ${stepId} waiting${eventName ? ` for event "${eventName}"` : ""}${timeoutDate ? ` until ${timeoutDate.toISOString()}` : ""}`, { runId: run.id, workflowId: run.workflowId });
|
|
1157
1267
|
}
|
|
@@ -1176,59 +1286,99 @@ ${error.stack}` : String(error)
|
|
|
1176
1286
|
const pollStateEntry = persistedRun.timeline[`${stepId}-poll`];
|
|
1177
1287
|
const startedAt = pollStateEntry && typeof pollStateEntry === "object" && "startedAt" in pollStateEntry ? new Date(pollStateEntry.startedAt) : new Date;
|
|
1178
1288
|
if (timeoutMs !== undefined && Date.now() >= startedAt.getTime() + timeoutMs) {
|
|
1179
|
-
await this.
|
|
1289
|
+
await withPostgresTransaction(this.db, async (db) => {
|
|
1290
|
+
const freshRun = await this.getRun({ runId: run.id, resourceId: run.resourceId ?? undefined }, { exclusiveLock: true, db });
|
|
1291
|
+
return this.updateRun({
|
|
1292
|
+
runId: run.id,
|
|
1293
|
+
resourceId: run.resourceId ?? undefined,
|
|
1294
|
+
data: {
|
|
1295
|
+
currentStepId: stepId,
|
|
1296
|
+
timeline: import_es_toolkit.merge(freshRun.timeline, {
|
|
1297
|
+
[stepId]: { output: {}, timedOut: true, timestamp: new Date }
|
|
1298
|
+
})
|
|
1299
|
+
}
|
|
1300
|
+
}, { db });
|
|
1301
|
+
}, this.pool);
|
|
1302
|
+
return { timedOut: true };
|
|
1303
|
+
}
|
|
1304
|
+
let result;
|
|
1305
|
+
try {
|
|
1306
|
+
result = await conditionFn();
|
|
1307
|
+
} catch (error) {
|
|
1308
|
+
this.logger.error(`Poll conditionFn for step ${stepId} threw an error, treating as non-match and continuing to poll`, error, { runId: run.id, workflowId: run.workflowId });
|
|
1309
|
+
if (timeoutMs !== undefined && Date.now() >= startedAt.getTime() + timeoutMs) {
|
|
1310
|
+
await withPostgresTransaction(this.db, async (db) => {
|
|
1311
|
+
const freshRun = await this.getRun({ runId: run.id, resourceId: run.resourceId ?? undefined }, { exclusiveLock: true, db });
|
|
1312
|
+
return this.updateRun({
|
|
1313
|
+
runId: run.id,
|
|
1314
|
+
resourceId: run.resourceId ?? undefined,
|
|
1315
|
+
data: {
|
|
1316
|
+
currentStepId: stepId,
|
|
1317
|
+
timeline: import_es_toolkit.merge(freshRun.timeline, {
|
|
1318
|
+
[stepId]: { output: {}, timedOut: true, timestamp: new Date }
|
|
1319
|
+
})
|
|
1320
|
+
}
|
|
1321
|
+
}, { db });
|
|
1322
|
+
}, this.pool);
|
|
1323
|
+
return { timedOut: true };
|
|
1324
|
+
}
|
|
1325
|
+
result = false;
|
|
1326
|
+
}
|
|
1327
|
+
if (result !== false) {
|
|
1328
|
+
await withPostgresTransaction(this.db, async (db) => {
|
|
1329
|
+
const freshRun = await this.getRun({ runId: run.id, resourceId: run.resourceId ?? undefined }, { exclusiveLock: true, db });
|
|
1330
|
+
return this.updateRun({
|
|
1331
|
+
runId: run.id,
|
|
1332
|
+
resourceId: run.resourceId ?? undefined,
|
|
1333
|
+
data: {
|
|
1334
|
+
currentStepId: stepId,
|
|
1335
|
+
timeline: import_es_toolkit.merge(freshRun.timeline, {
|
|
1336
|
+
[stepId]: { output: result, timestamp: new Date }
|
|
1337
|
+
})
|
|
1338
|
+
}
|
|
1339
|
+
}, { db });
|
|
1340
|
+
}, this.pool);
|
|
1341
|
+
return { timedOut: false, data: result };
|
|
1342
|
+
}
|
|
1343
|
+
const pollEvent = `__poll_${stepId}`;
|
|
1344
|
+
await withPostgresTransaction(this.db, async (db) => {
|
|
1345
|
+
const freshRun = await this.getRun({ runId: run.id, resourceId: run.resourceId ?? undefined }, { exclusiveLock: true, db });
|
|
1346
|
+
return this.updateRun({
|
|
1180
1347
|
runId: run.id,
|
|
1181
1348
|
resourceId: run.resourceId ?? undefined,
|
|
1182
1349
|
data: {
|
|
1350
|
+
status: "paused" /* PAUSED */,
|
|
1183
1351
|
currentStepId: stepId,
|
|
1184
|
-
|
|
1185
|
-
|
|
1352
|
+
pausedAt: new Date,
|
|
1353
|
+
timeline: import_es_toolkit.merge(freshRun.timeline, {
|
|
1354
|
+
[`${stepId}-poll`]: { startedAt: startedAt.toISOString() },
|
|
1355
|
+
[`${stepId}-wait-for`]: {
|
|
1356
|
+
waitFor: { timeoutEvent: pollEvent, skipOutput: true },
|
|
1357
|
+
timestamp: new Date
|
|
1358
|
+
}
|
|
1186
1359
|
})
|
|
1187
1360
|
}
|
|
1361
|
+
}, { db });
|
|
1362
|
+
}, this.pool);
|
|
1363
|
+
try {
|
|
1364
|
+
await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, {
|
|
1365
|
+
runId: run.id,
|
|
1366
|
+
resourceId: run.resourceId ?? undefined,
|
|
1367
|
+
workflowId: run.workflowId,
|
|
1368
|
+
input: run.input,
|
|
1369
|
+
event: { name: pollEvent, data: {} }
|
|
1370
|
+
}, {
|
|
1371
|
+
startAfter: new Date(Date.now() + intervalMs),
|
|
1372
|
+
expireInSeconds: defaultExpireInSeconds
|
|
1188
1373
|
});
|
|
1189
|
-
|
|
1190
|
-
}
|
|
1191
|
-
const result = await conditionFn();
|
|
1192
|
-
if (result !== false) {
|
|
1374
|
+
} catch (error) {
|
|
1193
1375
|
await this.updateRun({
|
|
1194
1376
|
runId: run.id,
|
|
1195
1377
|
resourceId: run.resourceId ?? undefined,
|
|
1196
|
-
data: {
|
|
1197
|
-
currentStepId: stepId,
|
|
1198
|
-
timeline: import_es_toolkit.merge(persistedRun.timeline, {
|
|
1199
|
-
[stepId]: { output: result, timestamp: new Date }
|
|
1200
|
-
})
|
|
1201
|
-
}
|
|
1378
|
+
data: { status: "running" /* RUNNING */, pausedAt: null }
|
|
1202
1379
|
});
|
|
1203
|
-
|
|
1380
|
+
throw error;
|
|
1204
1381
|
}
|
|
1205
|
-
const pollEvent = `__poll_${stepId}`;
|
|
1206
|
-
await this.updateRun({
|
|
1207
|
-
runId: run.id,
|
|
1208
|
-
resourceId: run.resourceId ?? undefined,
|
|
1209
|
-
data: {
|
|
1210
|
-
status: "paused" /* PAUSED */,
|
|
1211
|
-
currentStepId: stepId,
|
|
1212
|
-
pausedAt: new Date,
|
|
1213
|
-
timeline: import_es_toolkit.merge(persistedRun.timeline, {
|
|
1214
|
-
[`${stepId}-poll`]: { startedAt: startedAt.toISOString() },
|
|
1215
|
-
[`${stepId}-wait-for`]: {
|
|
1216
|
-
waitFor: { timeoutEvent: pollEvent, skipOutput: true },
|
|
1217
|
-
timestamp: new Date
|
|
1218
|
-
}
|
|
1219
|
-
})
|
|
1220
|
-
}
|
|
1221
|
-
});
|
|
1222
|
-
await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, {
|
|
1223
|
-
runId: run.id,
|
|
1224
|
-
resourceId: run.resourceId ?? undefined,
|
|
1225
|
-
workflowId: run.workflowId,
|
|
1226
|
-
input: run.input,
|
|
1227
|
-
event: { name: pollEvent, data: {} }
|
|
1228
|
-
}, {
|
|
1229
|
-
startAfter: new Date(Date.now() + intervalMs),
|
|
1230
|
-
expireInSeconds: defaultExpireInSeconds
|
|
1231
|
-
});
|
|
1232
1382
|
this.logger.log(`Step ${stepId} polling every ${intervalMs}ms...`, {
|
|
1233
1383
|
runId: run.id,
|
|
1234
1384
|
workflowId: run.workflowId
|
|
@@ -1273,5 +1423,5 @@ ${error.stack}` : String(error)
|
|
|
1273
1423
|
}
|
|
1274
1424
|
}
|
|
1275
1425
|
|
|
1276
|
-
//# debugId=
|
|
1426
|
+
//# debugId=12BE08AB4C2E4D0564756E2164756E21
|
|
1277
1427
|
//# sourceMappingURL=index.js.map
|
package/dist/index.d.cts
CHANGED
|
@@ -211,6 +211,11 @@ declare class WorkflowEngine {
|
|
|
211
211
|
expireInSeconds?: number;
|
|
212
212
|
};
|
|
213
213
|
}): Promise<WorkflowRun>;
|
|
214
|
+
fastForwardWorkflow({ runId, resourceId, data }: {
|
|
215
|
+
runId: string;
|
|
216
|
+
resourceId?: string;
|
|
217
|
+
data?: Record<string, unknown>;
|
|
218
|
+
}): Promise<WorkflowRun>;
|
|
214
219
|
cancelWorkflow({ runId, resourceId }: {
|
|
215
220
|
runId: string;
|
|
216
221
|
resourceId?: string;
|
|
@@ -231,10 +236,11 @@ declare class WorkflowEngine {
|
|
|
231
236
|
exclusiveLock?: boolean;
|
|
232
237
|
db?: Db;
|
|
233
238
|
}): Promise<WorkflowRun>;
|
|
234
|
-
updateRun({ runId, resourceId, data }: {
|
|
239
|
+
updateRun({ runId, resourceId, data, expectedStatuses }: {
|
|
235
240
|
runId: string;
|
|
236
241
|
resourceId?: string;
|
|
237
242
|
data: Partial<WorkflowRun>;
|
|
243
|
+
expectedStatuses?: string[];
|
|
238
244
|
}, { db }?: {
|
|
239
245
|
db?: Db;
|
|
240
246
|
}): Promise<WorkflowRun>;
|
|
@@ -251,6 +257,7 @@ declare class WorkflowEngine {
|
|
|
251
257
|
private resolveScopedResourceId;
|
|
252
258
|
private handleWorkflowRun;
|
|
253
259
|
private getCachedStepEntry;
|
|
260
|
+
private getWaitForStepEntry;
|
|
254
261
|
private runStep;
|
|
255
262
|
private waitStep;
|
|
256
263
|
private pollStep;
|