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/dist/index.d.ts 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;
package/dist/index.js CHANGED
@@ -195,15 +195,23 @@ async function runMigrations(db) {
195
195
  job_id varchar(256)
196
196
  );
197
197
  `, []);
198
- await db.executeSql(`
199
- CREATE INDEX workflow_runs_workflow_id_idx ON workflow_runs USING btree (workflow_id);
200
- `, []);
201
198
  await db.executeSql(`
202
199
  CREATE INDEX workflow_runs_created_at_idx ON workflow_runs USING btree (created_at);
200
+ CREATE INDEX workflow_runs_resource_id_created_at_idx ON workflow_runs USING btree (resource_id, created_at DESC);
201
+ CREATE INDEX workflow_runs_status_created_at_idx ON workflow_runs USING btree (status, created_at DESC);
202
+ CREATE INDEX workflow_runs_workflow_id_created_at_idx ON workflow_runs USING btree (workflow_id, created_at DESC);
203
+ CREATE INDEX workflow_runs_resource_id_workflow_id_created_at_idx ON workflow_runs USING btree (resource_id, workflow_id, created_at DESC);
203
204
  `, []);
205
+ } else {
204
206
  await db.executeSql(`
205
- CREATE INDEX workflow_runs_resource_id_idx ON workflow_runs USING btree (resource_id);
206
- `, []);
207
+ DROP INDEX IF EXISTS workflow_runs_workflow_id_idx;
208
+ DROP INDEX IF EXISTS workflow_runs_resource_id_idx;
209
+ CREATE INDEX IF NOT EXISTS workflow_runs_created_at_idx ON workflow_runs USING btree (created_at);
210
+ CREATE INDEX IF NOT EXISTS workflow_runs_resource_id_created_at_idx ON workflow_runs USING btree (resource_id, created_at DESC);
211
+ CREATE INDEX IF NOT EXISTS workflow_runs_status_created_at_idx ON workflow_runs USING btree (status, created_at DESC);
212
+ CREATE INDEX IF NOT EXISTS workflow_runs_workflow_id_created_at_idx ON workflow_runs USING btree (workflow_id, created_at DESC);
213
+ 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);
214
+ `, []);
207
215
  }
208
216
  }
209
217
 
@@ -246,13 +254,13 @@ async function insertWorkflowRun({
246
254
  const runId = generateKSUID("run");
247
255
  const now = new Date;
248
256
  const result = await db.executeSql(`INSERT INTO workflow_runs (
249
- id,
250
- resource_id,
251
- workflow_id,
252
- current_step_id,
253
- status,
254
- input,
255
- max_retries,
257
+ id,
258
+ resource_id,
259
+ workflow_id,
260
+ current_step_id,
261
+ status,
262
+ input,
263
+ max_retries,
256
264
  timeout_at,
257
265
  created_at,
258
266
  updated_at,
@@ -299,7 +307,8 @@ async function getWorkflowRun({
299
307
  async function updateWorkflowRun({
300
308
  runId,
301
309
  resourceId,
302
- data
310
+ data,
311
+ expectedStatuses
303
312
  }, db) {
304
313
  const now = new Date;
305
314
  const updates = ["updated_at = $1"];
@@ -355,10 +364,20 @@ async function updateWorkflowRun({
355
364
  values.push(data.jobId);
356
365
  paramIndex++;
357
366
  }
358
- const whereClause = resourceId ? `WHERE id = $${paramIndex} AND resource_id = $${paramIndex + 1}` : `WHERE id = $${paramIndex}`;
359
367
  values.push(runId);
368
+ const idParam = paramIndex;
369
+ paramIndex++;
360
370
  if (resourceId) {
361
371
  values.push(resourceId);
372
+ paramIndex++;
373
+ }
374
+ if (expectedStatuses && expectedStatuses.length > 0) {
375
+ values.push(expectedStatuses);
376
+ paramIndex++;
377
+ }
378
+ let whereClause = resourceId ? `WHERE id = $${idParam} AND resource_id = $${idParam + 1}` : `WHERE id = $${idParam}`;
379
+ if (expectedStatuses && expectedStatuses.length > 0) {
380
+ whereClause += ` AND status = ANY($${paramIndex - 1})`;
362
381
  }
363
382
  const query = `
364
383
  UPDATE workflow_runs
@@ -399,37 +418,50 @@ async function getWorkflowRuns({
399
418
  values.push(workflowId);
400
419
  paramIndex++;
401
420
  }
402
- if (startingAfter) {
403
- const cursorResult = await db.executeSql("SELECT created_at FROM workflow_runs WHERE id = $1 LIMIT 1", [startingAfter]);
404
- if (cursorResult.rows[0]?.created_at) {
405
- conditions.push(`created_at < $${paramIndex}`);
406
- values.push(typeof cursorResult.rows[0].created_at === "string" ? new Date(cursorResult.rows[0].created_at) : cursorResult.rows[0].created_at);
407
- paramIndex++;
421
+ const cursorIds = [startingAfter, endingBefore].filter(Boolean);
422
+ if (cursorIds.length > 0) {
423
+ const cursorResult = await db.executeSql("SELECT id, created_at FROM workflow_runs WHERE id = ANY($1)", [cursorIds]);
424
+ const cursorMap = new Map;
425
+ for (const row of cursorResult.rows) {
426
+ cursorMap.set(row.id, typeof row.created_at === "string" ? new Date(row.created_at) : row.created_at);
408
427
  }
409
- }
410
- if (endingBefore) {
411
- const cursorResult = await db.executeSql("SELECT created_at FROM workflow_runs WHERE id = $1 LIMIT 1", [endingBefore]);
412
- if (cursorResult.rows[0]?.created_at) {
413
- conditions.push(`created_at > $${paramIndex}`);
414
- values.push(typeof cursorResult.rows[0].created_at === "string" ? new Date(cursorResult.rows[0].created_at) : cursorResult.rows[0].created_at);
415
- paramIndex++;
428
+ if (startingAfter) {
429
+ const cursor = cursorMap.get(startingAfter);
430
+ if (cursor) {
431
+ conditions.push(`created_at < $${paramIndex}`);
432
+ values.push(cursor);
433
+ paramIndex++;
434
+ }
435
+ }
436
+ if (endingBefore) {
437
+ const cursor = cursorMap.get(endingBefore);
438
+ if (cursor) {
439
+ conditions.push(`created_at > $${paramIndex}`);
440
+ values.push(cursor);
441
+ paramIndex++;
442
+ }
416
443
  }
417
444
  }
418
445
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
419
446
  const actualLimit = Math.min(Math.max(limit, 1), 100) + 1;
447
+ const isBackward = !!endingBefore && !startingAfter;
420
448
  const query = `
421
449
  SELECT * FROM workflow_runs
422
450
  ${whereClause}
423
- ORDER BY created_at DESC
451
+ ORDER BY created_at ${isBackward ? "ASC" : "DESC"}
424
452
  LIMIT $${paramIndex}
425
453
  `;
426
454
  values.push(actualLimit);
427
455
  const result = await db.executeSql(query, values);
428
456
  const rows = result.rows;
429
- const hasMore = rows.length > (limit ?? 20);
430
- const rawItems = hasMore ? rows.slice(0, limit) : rows;
457
+ const hasExtraRow = rows.length > (limit ?? 20);
458
+ const rawItems = hasExtraRow ? rows.slice(0, limit) : rows;
459
+ if (isBackward) {
460
+ rawItems.reverse();
461
+ }
431
462
  const items = rawItems.map((row) => mapRowToWorkflowRun(row));
432
- const hasPrev = !!endingBefore;
463
+ const hasMore = isBackward ? items.length > 0 : hasExtraRow;
464
+ const hasPrev = isBackward ? hasExtraRow : !!startingAfter && items.length > 0;
433
465
  const nextCursor = hasMore && items.length > 0 ? items[items.length - 1]?.id ?? null : null;
434
466
  const prevCursor = hasPrev && items.length > 0 ? items[0]?.id ?? null : null;
435
467
  return { items, nextCursor, prevCursor, hasMore, hasPrev };
@@ -635,7 +667,8 @@ class WorkflowEngine {
635
667
  data: {
636
668
  status: "paused" /* PAUSED */,
637
669
  pausedAt: new Date
638
- }
670
+ },
671
+ expectedStatuses: ["running" /* RUNNING */, "pending" /* PENDING */]
639
672
  });
640
673
  this.logger.log("Paused workflow run", {
641
674
  runId,
@@ -649,6 +682,10 @@ class WorkflowEngine {
649
682
  options
650
683
  }) {
651
684
  await this.checkIfHasStarted();
685
+ const current = await this.getRun({ runId, resourceId });
686
+ if (current.status !== "paused" /* PAUSED */) {
687
+ throw new WorkflowEngineError(`Cannot resume workflow run in '${current.status}' status, must be 'paused'`, current.workflowId, runId);
688
+ }
652
689
  return this.triggerEvent({
653
690
  runId,
654
691
  resourceId,
@@ -657,6 +694,51 @@ class WorkflowEngine {
657
694
  options
658
695
  });
659
696
  }
697
+ async fastForwardWorkflow({
698
+ runId,
699
+ resourceId,
700
+ data
701
+ }) {
702
+ await this.checkIfHasStarted();
703
+ const run = await this.getRun({ runId, resourceId });
704
+ if (run.status !== "paused" /* PAUSED */) {
705
+ return run;
706
+ }
707
+ const stepId = run.currentStepId;
708
+ const waitForStep = this.getWaitForStepEntry(run.timeline, stepId);
709
+ if (!waitForStep) {
710
+ return run;
711
+ }
712
+ const { eventName, timeoutEvent, skipOutput } = waitForStep.waitFor;
713
+ if (eventName === PAUSE_EVENT_NAME) {
714
+ return this.resumeWorkflow({ runId, resourceId });
715
+ }
716
+ if (skipOutput && timeoutEvent) {
717
+ await withPostgresTransaction(this.db, async (db) => {
718
+ const freshRun = await this.getRun({ runId, resourceId }, { exclusiveLock: true, db });
719
+ return this.updateRun({
720
+ runId,
721
+ resourceId,
722
+ data: {
723
+ timeline: merge(freshRun.timeline, {
724
+ [stepId]: {
725
+ output: data ?? {},
726
+ timestamp: new Date
727
+ }
728
+ })
729
+ }
730
+ }, { db });
731
+ }, this.pool);
732
+ return this.triggerEvent({ runId, resourceId, eventName: timeoutEvent });
733
+ }
734
+ if (eventName) {
735
+ return this.triggerEvent({ runId, resourceId, eventName, data: data ?? {} });
736
+ }
737
+ if (timeoutEvent) {
738
+ return this.triggerEvent({ runId, resourceId, eventName: timeoutEvent, data: data ?? {} });
739
+ }
740
+ return run;
741
+ }
660
742
  async cancelWorkflow({
661
743
  runId,
662
744
  resourceId
@@ -667,7 +749,8 @@ class WorkflowEngine {
667
749
  resourceId,
668
750
  data: {
669
751
  status: "cancelled" /* CANCELLED */
670
- }
752
+ },
753
+ expectedStatuses: ["pending" /* PENDING */, "running" /* RUNNING */, "paused" /* PAUSED */]
671
754
  });
672
755
  this.logger.log(`cancelled workflow run with id ${runId}`);
673
756
  return run;
@@ -692,7 +775,7 @@ class WorkflowEngine {
692
775
  data
693
776
  }
694
777
  };
695
- this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
778
+ await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
696
779
  expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds
697
780
  });
698
781
  this.logger.log(`event ${eventName} sent for workflow run with id ${runId}`);
@@ -708,10 +791,17 @@ class WorkflowEngine {
708
791
  async updateRun({
709
792
  runId,
710
793
  resourceId,
711
- data
794
+ data,
795
+ expectedStatuses
712
796
  }, { db } = {}) {
713
- const run = await updateWorkflowRun({ runId, resourceId, data }, db ?? this.db);
797
+ const run = await updateWorkflowRun({ runId, resourceId, data, expectedStatuses }, db ?? this.db);
714
798
  if (!run) {
799
+ if (expectedStatuses) {
800
+ const current = await getWorkflowRun({ runId, resourceId }, { db: db ?? this.db });
801
+ if (current) {
802
+ throw new WorkflowEngineError(`Cannot update workflow run in '${current.status}' status, expected: ${expectedStatuses.join(", ")}`, current.workflowId, runId);
803
+ }
804
+ }
715
805
  throw new WorkflowRunNotFoundError(runId);
716
806
  }
717
807
  return run;
@@ -794,36 +884,40 @@ class WorkflowEngine {
794
884
  throw new WorkflowEngineError("Missing current step id", workflowId, runId);
795
885
  }
796
886
  if (run.status === "paused" /* PAUSED */) {
797
- const waitForStepEntry = run.timeline[`${run.currentStepId}-wait-for`];
798
- const waitForStep = waitForStepEntry && typeof waitForStepEntry === "object" && "waitFor" in waitForStepEntry ? waitForStepEntry : null;
799
- const currentStep = this.getCachedStepEntry(run.timeline, run.currentStepId);
800
- const waitFor = waitForStep?.waitFor;
801
- const hasCurrentStepOutput = currentStep?.output !== undefined;
802
- const eventMatches = waitFor && event?.name && (event.name === waitFor.eventName || event.name === waitFor.timeoutEvent) && !hasCurrentStepOutput;
803
- if (eventMatches) {
804
- const isTimeout = event?.name === waitFor?.timeoutEvent;
805
- const skipOutput = waitFor?.skipOutput;
806
- run = await this.updateRun({
807
- runId,
808
- resourceId: scopedResourceId,
809
- data: {
810
- status: "running" /* RUNNING */,
811
- pausedAt: null,
812
- resumedAt: new Date,
813
- jobId: job?.id,
814
- ...skipOutput ? {} : {
815
- timeline: merge(run.timeline, {
816
- [run.currentStepId]: {
817
- output: event?.data ?? {},
818
- ...isTimeout ? { timedOut: true } : {},
819
- timestamp: new Date
820
- }
821
- })
887
+ run = await withPostgresTransaction(this.db, async (db) => {
888
+ const lockedRun = await this.getRun({ runId, resourceId: scopedResourceId }, { exclusiveLock: true, db });
889
+ if (lockedRun.status !== "paused" /* PAUSED */) {
890
+ return lockedRun;
891
+ }
892
+ const waitForStep = this.getWaitForStepEntry(lockedRun.timeline, lockedRun.currentStepId);
893
+ const currentStep = this.getCachedStepEntry(lockedRun.timeline, lockedRun.currentStepId);
894
+ const waitFor = waitForStep?.waitFor;
895
+ const hasCurrentStepOutput = currentStep?.output !== undefined;
896
+ const eventMatches = waitFor && event?.name && (event.name === waitFor.eventName || event.name === waitFor.timeoutEvent) && !hasCurrentStepOutput;
897
+ if (eventMatches) {
898
+ const isTimeout = event?.name === waitFor?.timeoutEvent;
899
+ const skipOutput = waitFor?.skipOutput;
900
+ return this.updateRun({
901
+ runId,
902
+ resourceId: scopedResourceId,
903
+ data: {
904
+ status: "running" /* RUNNING */,
905
+ pausedAt: null,
906
+ resumedAt: new Date,
907
+ jobId: job?.id,
908
+ ...skipOutput ? {} : {
909
+ timeline: merge(lockedRun.timeline, {
910
+ [lockedRun.currentStepId]: {
911
+ output: event?.data ?? {},
912
+ ...isTimeout ? { timedOut: true } : {},
913
+ timestamp: new Date
914
+ }
915
+ })
916
+ }
822
917
  }
823
- }
824
- });
825
- } else {
826
- run = await this.updateRun({
918
+ }, { db });
919
+ }
920
+ return this.updateRun({
827
921
  runId,
828
922
  resourceId: scopedResourceId,
829
923
  data: {
@@ -832,8 +926,8 @@ class WorkflowEngine {
832
926
  resumedAt: new Date,
833
927
  jobId: job?.id
834
928
  }
835
- });
836
- }
929
+ }, { db });
930
+ }, this.pool);
837
931
  }
838
932
  const baseStep = {
839
933
  run: async (stepId, handler) => {
@@ -963,6 +1057,10 @@ class WorkflowEngine {
963
1057
  const stepEntry = timeline[stepId];
964
1058
  return stepEntry && typeof stepEntry === "object" && "output" in stepEntry ? stepEntry : null;
965
1059
  }
1060
+ getWaitForStepEntry(timeline, stepId) {
1061
+ const entry = timeline[`${stepId}-wait-for`];
1062
+ return entry && typeof entry === "object" && "waitFor" in entry ? entry : null;
1063
+ }
966
1064
  async runStep({
967
1065
  stepId,
968
1066
  run,
@@ -1004,7 +1102,7 @@ class WorkflowEngine {
1004
1102
  runId: run.id,
1005
1103
  resourceId: run.resourceId ?? undefined,
1006
1104
  data: {
1007
- timeline: merge(run.timeline, {
1105
+ timeline: merge(persistedRun.timeline, {
1008
1106
  [stepId]: {
1009
1107
  output,
1010
1108
  timestamp: new Date
@@ -1049,33 +1147,45 @@ ${error.stack}` : String(error)
1049
1147
  return cached.timedOut ? undefined : cached.output;
1050
1148
  }
1051
1149
  const timeoutEvent = timeoutDate ? `__timeout_${stepId}` : undefined;
1052
- await this.updateRun({
1053
- runId: run.id,
1054
- resourceId: run.resourceId ?? undefined,
1055
- data: {
1056
- status: "paused" /* PAUSED */,
1057
- currentStepId: stepId,
1058
- pausedAt: new Date,
1059
- timeline: merge(run.timeline, {
1060
- [`${stepId}-wait-for`]: {
1061
- waitFor: { eventName, timeoutEvent },
1062
- timestamp: new Date
1063
- }
1064
- })
1065
- }
1066
- });
1067
- if (timeoutDate && timeoutEvent) {
1068
- const job = {
1150
+ await withPostgresTransaction(this.db, async (db) => {
1151
+ const freshRun = await this.getRun({ runId: run.id, resourceId: run.resourceId ?? undefined }, { exclusiveLock: true, db });
1152
+ return this.updateRun({
1069
1153
  runId: run.id,
1070
1154
  resourceId: run.resourceId ?? undefined,
1071
- workflowId: run.workflowId,
1072
- input: run.input,
1073
- event: { name: timeoutEvent, data: { date: timeoutDate.toISOString() } }
1074
- };
1075
- await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
1076
- startAfter: timeoutDate.getTime() <= Date.now() ? new Date : timeoutDate,
1077
- expireInSeconds: defaultExpireInSeconds
1078
- });
1155
+ data: {
1156
+ status: "paused" /* PAUSED */,
1157
+ currentStepId: stepId,
1158
+ pausedAt: new Date,
1159
+ timeline: merge(freshRun.timeline, {
1160
+ [`${stepId}-wait-for`]: {
1161
+ waitFor: { eventName, timeoutEvent },
1162
+ timestamp: new Date
1163
+ }
1164
+ })
1165
+ }
1166
+ }, { db });
1167
+ }, this.pool);
1168
+ if (timeoutDate && timeoutEvent) {
1169
+ try {
1170
+ const job = {
1171
+ runId: run.id,
1172
+ resourceId: run.resourceId ?? undefined,
1173
+ workflowId: run.workflowId,
1174
+ input: run.input,
1175
+ event: { name: timeoutEvent, data: { date: timeoutDate.toISOString() } }
1176
+ };
1177
+ await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
1178
+ startAfter: timeoutDate.getTime() <= Date.now() ? new Date : timeoutDate,
1179
+ expireInSeconds: defaultExpireInSeconds
1180
+ });
1181
+ } catch (error) {
1182
+ await this.updateRun({
1183
+ runId: run.id,
1184
+ resourceId: run.resourceId ?? undefined,
1185
+ data: { status: "running" /* RUNNING */, pausedAt: null }
1186
+ });
1187
+ throw error;
1188
+ }
1079
1189
  }
1080
1190
  this.logger.log(`Step ${stepId} waiting${eventName ? ` for event "${eventName}"` : ""}${timeoutDate ? ` until ${timeoutDate.toISOString()}` : ""}`, { runId: run.id, workflowId: run.workflowId });
1081
1191
  }
@@ -1100,59 +1210,99 @@ ${error.stack}` : String(error)
1100
1210
  const pollStateEntry = persistedRun.timeline[`${stepId}-poll`];
1101
1211
  const startedAt = pollStateEntry && typeof pollStateEntry === "object" && "startedAt" in pollStateEntry ? new Date(pollStateEntry.startedAt) : new Date;
1102
1212
  if (timeoutMs !== undefined && Date.now() >= startedAt.getTime() + timeoutMs) {
1103
- await this.updateRun({
1213
+ await withPostgresTransaction(this.db, async (db) => {
1214
+ const freshRun = await this.getRun({ runId: run.id, resourceId: run.resourceId ?? undefined }, { exclusiveLock: true, db });
1215
+ return this.updateRun({
1216
+ runId: run.id,
1217
+ resourceId: run.resourceId ?? undefined,
1218
+ data: {
1219
+ currentStepId: stepId,
1220
+ timeline: merge(freshRun.timeline, {
1221
+ [stepId]: { output: {}, timedOut: true, timestamp: new Date }
1222
+ })
1223
+ }
1224
+ }, { db });
1225
+ }, this.pool);
1226
+ return { timedOut: true };
1227
+ }
1228
+ let result;
1229
+ try {
1230
+ result = await conditionFn();
1231
+ } catch (error) {
1232
+ 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 });
1233
+ if (timeoutMs !== undefined && Date.now() >= startedAt.getTime() + timeoutMs) {
1234
+ await withPostgresTransaction(this.db, async (db) => {
1235
+ const freshRun = await this.getRun({ runId: run.id, resourceId: run.resourceId ?? undefined }, { exclusiveLock: true, db });
1236
+ return this.updateRun({
1237
+ runId: run.id,
1238
+ resourceId: run.resourceId ?? undefined,
1239
+ data: {
1240
+ currentStepId: stepId,
1241
+ timeline: merge(freshRun.timeline, {
1242
+ [stepId]: { output: {}, timedOut: true, timestamp: new Date }
1243
+ })
1244
+ }
1245
+ }, { db });
1246
+ }, this.pool);
1247
+ return { timedOut: true };
1248
+ }
1249
+ result = false;
1250
+ }
1251
+ if (result !== false) {
1252
+ await withPostgresTransaction(this.db, async (db) => {
1253
+ const freshRun = await this.getRun({ runId: run.id, resourceId: run.resourceId ?? undefined }, { exclusiveLock: true, db });
1254
+ return this.updateRun({
1255
+ runId: run.id,
1256
+ resourceId: run.resourceId ?? undefined,
1257
+ data: {
1258
+ currentStepId: stepId,
1259
+ timeline: merge(freshRun.timeline, {
1260
+ [stepId]: { output: result, timestamp: new Date }
1261
+ })
1262
+ }
1263
+ }, { db });
1264
+ }, this.pool);
1265
+ return { timedOut: false, data: result };
1266
+ }
1267
+ const pollEvent = `__poll_${stepId}`;
1268
+ await withPostgresTransaction(this.db, async (db) => {
1269
+ const freshRun = await this.getRun({ runId: run.id, resourceId: run.resourceId ?? undefined }, { exclusiveLock: true, db });
1270
+ return this.updateRun({
1104
1271
  runId: run.id,
1105
1272
  resourceId: run.resourceId ?? undefined,
1106
1273
  data: {
1274
+ status: "paused" /* PAUSED */,
1107
1275
  currentStepId: stepId,
1108
- timeline: merge(persistedRun.timeline, {
1109
- [stepId]: { output: {}, timedOut: true, timestamp: new Date }
1276
+ pausedAt: new Date,
1277
+ timeline: merge(freshRun.timeline, {
1278
+ [`${stepId}-poll`]: { startedAt: startedAt.toISOString() },
1279
+ [`${stepId}-wait-for`]: {
1280
+ waitFor: { timeoutEvent: pollEvent, skipOutput: true },
1281
+ timestamp: new Date
1282
+ }
1110
1283
  })
1111
1284
  }
1285
+ }, { db });
1286
+ }, this.pool);
1287
+ try {
1288
+ await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, {
1289
+ runId: run.id,
1290
+ resourceId: run.resourceId ?? undefined,
1291
+ workflowId: run.workflowId,
1292
+ input: run.input,
1293
+ event: { name: pollEvent, data: {} }
1294
+ }, {
1295
+ startAfter: new Date(Date.now() + intervalMs),
1296
+ expireInSeconds: defaultExpireInSeconds
1112
1297
  });
1113
- return { timedOut: true };
1114
- }
1115
- const result = await conditionFn();
1116
- if (result !== false) {
1298
+ } catch (error) {
1117
1299
  await this.updateRun({
1118
1300
  runId: run.id,
1119
1301
  resourceId: run.resourceId ?? undefined,
1120
- data: {
1121
- currentStepId: stepId,
1122
- timeline: merge(persistedRun.timeline, {
1123
- [stepId]: { output: result, timestamp: new Date }
1124
- })
1125
- }
1302
+ data: { status: "running" /* RUNNING */, pausedAt: null }
1126
1303
  });
1127
- return { timedOut: false, data: result };
1304
+ throw error;
1128
1305
  }
1129
- const pollEvent = `__poll_${stepId}`;
1130
- await this.updateRun({
1131
- runId: run.id,
1132
- resourceId: run.resourceId ?? undefined,
1133
- data: {
1134
- status: "paused" /* PAUSED */,
1135
- currentStepId: stepId,
1136
- pausedAt: new Date,
1137
- timeline: merge(persistedRun.timeline, {
1138
- [`${stepId}-poll`]: { startedAt: startedAt.toISOString() },
1139
- [`${stepId}-wait-for`]: {
1140
- waitFor: { timeoutEvent: pollEvent, skipOutput: true },
1141
- timestamp: new Date
1142
- }
1143
- })
1144
- }
1145
- });
1146
- await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, {
1147
- runId: run.id,
1148
- resourceId: run.resourceId ?? undefined,
1149
- workflowId: run.workflowId,
1150
- input: run.input,
1151
- event: { name: pollEvent, data: {} }
1152
- }, {
1153
- startAfter: new Date(Date.now() + intervalMs),
1154
- expireInSeconds: defaultExpireInSeconds
1155
- });
1156
1306
  this.logger.log(`Step ${stepId} polling every ${intervalMs}ms...`, {
1157
1307
  runId: run.id,
1158
1308
  workflowId: run.workflowId
@@ -1206,5 +1356,5 @@ export {
1206
1356
  StepType
1207
1357
  };
1208
1358
 
1209
- //# debugId=3D0DF5019111390C64756E2164756E21
1359
+ //# debugId=0EF998205E7ED0B464756E2164756E21
1210
1360
  //# sourceMappingURL=index.js.map