pg-workflows 0.9.0 → 0.10.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/dist/index.js CHANGED
@@ -12,13 +12,16 @@ import {
12
12
  getWorkflowRun,
13
13
  getWorkflowRuns,
14
14
  insertWorkflowRun,
15
+ invokeChildWorkflowTimelineKey,
16
+ isInvokeChildWorkflowTimelineEntry,
15
17
  runMigrations,
16
18
  updateWorkflowRun,
17
19
  validateResourceId,
18
20
  validateWorkflowId,
21
+ waitForTimelineKey,
19
22
  withPostgresTransaction,
20
23
  workflow
21
- } from "./shared/chunk-fr76gdwj.js";
24
+ } from "./shared/chunk-nygamc7b.js";
22
25
  // src/duration.ts
23
26
  import parse from "parse-duration";
24
27
  var MS_PER_SECOND = 1000;
@@ -94,7 +97,7 @@ function parseWorkflowHandler(handler) {
94
97
  const propertyAccess = node.expression;
95
98
  const objectName = propertyAccess.expression.getText(sourceFile);
96
99
  const methodName = propertyAccess.name.text;
97
- if (objectName === "step" && (methodName === "run" || methodName === "waitFor" || methodName === "pause" || methodName === "waitUntil" || methodName === "delay" || methodName === "sleep" || methodName === "poll")) {
100
+ if (objectName === "step" && (methodName === "run" || methodName === "waitFor" || methodName === "pause" || methodName === "waitUntil" || methodName === "delay" || methodName === "sleep" || methodName === "poll" || methodName === "invokeChildWorkflow")) {
98
101
  const firstArg = node.arguments[0];
99
102
  if (firstArg) {
100
103
  const { id, isDynamic } = extractStepId(firstArg);
@@ -127,7 +130,8 @@ var StepTypeToIcon = {
127
130
  ["pause" /* PAUSE */]: "⏸",
128
131
  ["waitUntil" /* WAIT_UNTIL */]: "⏲",
129
132
  ["delay" /* DELAY */]: "⏱",
130
- ["poll" /* POLL */]: "↻"
133
+ ["poll" /* POLL */]: "↻",
134
+ ["invokeChildWorkflow" /* INVOKE_CHILD_WORKFLOW */]: "↪"
131
135
  };
132
136
  var defaultLogger = {
133
137
  log: (_message) => console.warn(_message),
@@ -139,6 +143,7 @@ var retrySendOptions = (maxRetries) => ({
139
143
  retryBackoff: true,
140
144
  retryDelay: 1
141
145
  });
146
+ var getInvokeChildWorkflowEventName = (childRunId) => `__invoke_child_workflow_completed:${childRunId}`;
142
147
  var defaultHeartbeatSeconds = process.env.WORKFLOW_RUN_HEARTBEAT_SECONDS ? Number.parseInt(process.env.WORKFLOW_RUN_HEARTBEAT_SECONDS, 10) : 30;
143
148
 
144
149
  class WorkflowEngine {
@@ -244,31 +249,57 @@ class WorkflowEngine {
244
249
  this.workflows.clear();
245
250
  return this;
246
251
  }
247
- async startWorkflow(refOrParams, inputArg, optionsArg) {
248
- let workflowId;
249
- let input;
250
- let resourceId;
251
- let idempotencyKey;
252
- let options;
252
+ resolveWorkflowRunParameters(refOrParams, inputArg, optionsArg) {
253
253
  if (typeof refOrParams === "function" && "id" in refOrParams) {
254
- workflowId = refOrParams.id;
255
- input = inputArg;
256
- options = optionsArg;
257
- resourceId = optionsArg?.resourceId;
258
- idempotencyKey = optionsArg?.idempotencyKey;
259
- } else {
260
- const params = refOrParams;
261
- workflowId = params.workflowId;
262
- input = params.input;
263
- resourceId = params.resourceId;
264
- idempotencyKey = params.idempotencyKey;
265
- options = params.options;
254
+ return {
255
+ workflowId: refOrParams.id,
256
+ input: inputArg,
257
+ options: optionsArg,
258
+ resourceId: optionsArg?.resourceId,
259
+ idempotencyKey: optionsArg?.idempotencyKey
260
+ };
266
261
  }
267
- validateWorkflowId(workflowId);
268
- validateResourceId(resourceId);
262
+ const params = refOrParams;
263
+ return {
264
+ workflowId: params.workflowId,
265
+ input: params.input,
266
+ resourceId: params.resourceId ?? params.options?.resourceId,
267
+ idempotencyKey: params.idempotencyKey ?? params.options?.idempotencyKey,
268
+ options: params.options
269
+ };
270
+ }
271
+ async startWorkflow(refOrParams, inputArg, optionsArg) {
272
+ const { workflowId, input, resourceId, idempotencyKey, options } = this.resolveWorkflowRunParameters(refOrParams, inputArg, optionsArg);
269
273
  if (!this._started) {
270
274
  await this.start(false, { batchSize: options?.batchSize ?? 1 });
271
275
  }
276
+ const { run } = await this.createWorkflowRun({
277
+ workflowId,
278
+ input,
279
+ resourceId,
280
+ idempotencyKey,
281
+ options
282
+ });
283
+ this.logger.log("Started workflow run", {
284
+ runId: run.id,
285
+ workflowId
286
+ });
287
+ return run;
288
+ }
289
+ async createWorkflowRun({
290
+ workflowId,
291
+ input,
292
+ resourceId,
293
+ idempotencyKey,
294
+ options,
295
+ parentRunId,
296
+ parentStepId,
297
+ parentResourceId,
298
+ enqueue = true,
299
+ db
300
+ }) {
301
+ validateWorkflowId(workflowId);
302
+ validateResourceId(resourceId);
272
303
  const workflow2 = this.workflows.get(workflowId);
273
304
  if (!workflow2) {
274
305
  throw new WorkflowEngineError(`Unknown workflow ${workflowId}`);
@@ -285,38 +316,60 @@ class WorkflowEngine {
285
316
  }
286
317
  }
287
318
  const initialStepId = workflow2.steps[0]?.id ?? "__start__";
288
- const run = await withPostgresTransaction(this.boss.getDb(), async (_db) => {
289
- const timeoutAt = options?.timeout ? new Date(Date.now() + options.timeout) : workflow2.timeout ? new Date(Date.now() + workflow2.timeout) : null;
290
- const { run: insertedRun, created } = await insertWorkflowRun({
291
- resourceId,
292
- workflowId,
293
- currentStepId: initialStepId,
294
- status: "running" /* RUNNING */,
295
- input,
296
- maxRetries: options?.retries ?? workflow2.retries ?? 0,
297
- timeoutAt,
298
- idempotencyKey
299
- }, _db);
300
- if (created) {
301
- const job = {
302
- runId: insertedRun.id,
303
- resourceId,
304
- workflowId,
305
- input
306
- };
307
- await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
308
- startAfter: new Date,
309
- expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds,
310
- ...retrySendOptions(insertedRun.maxRetries)
311
- });
319
+ const timeoutAt = options?.timeout ? new Date(Date.now() + options.timeout) : workflow2.timeout ? new Date(Date.now() + workflow2.timeout) : null;
320
+ const insertRun = async (targetDb) => await insertWorkflowRun({
321
+ resourceId,
322
+ workflowId,
323
+ currentStepId: initialStepId,
324
+ status: "running" /* RUNNING */,
325
+ input,
326
+ maxRetries: options?.retries ?? workflow2.retries ?? 0,
327
+ timeoutAt,
328
+ idempotencyKey,
329
+ parentRunId,
330
+ parentStepId,
331
+ parentResourceId
332
+ }, targetDb);
333
+ const insertAndEnqueue = async (targetDb) => {
334
+ const result = await insertRun(targetDb);
335
+ if (enqueue && result.created) {
336
+ await this.enqueueWorkflowRun(result.run, options, targetDb);
312
337
  }
313
- return insertedRun;
314
- }, this.pool);
315
- this.logger.log("Started workflow run", {
338
+ return result;
339
+ };
340
+ const { run, created } = db ? await insertAndEnqueue(db) : await withPostgresTransaction(this.boss.getDb(), insertAndEnqueue, this.pool);
341
+ return { run, created };
342
+ }
343
+ async enqueueWorkflowRun(run, options, db) {
344
+ const job = {
316
345
  runId: run.id,
317
- workflowId
346
+ resourceId: run.resourceId ?? undefined,
347
+ workflowId: run.workflowId,
348
+ input: run.input
349
+ };
350
+ await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
351
+ startAfter: new Date,
352
+ expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds,
353
+ ...retrySendOptions(run.maxRetries),
354
+ ...db ? { db } : {}
355
+ });
356
+ }
357
+ async notifyParentOfChildTerminalRun(childRun) {
358
+ if (!childRun.parentRunId || !childRun.parentStepId) {
359
+ return;
360
+ }
361
+ const parentRun = await getWorkflowRun({
362
+ runId: childRun.parentRunId,
363
+ resourceId: childRun.parentResourceId ?? undefined
364
+ }, { db: this.db });
365
+ if (!parentRun || parentRun.status === "completed" /* COMPLETED */ || parentRun.status === "failed" /* FAILED */ || parentRun.status === "cancelled" /* CANCELLED */) {
366
+ return;
367
+ }
368
+ await this.triggerEvent({
369
+ runId: parentRun.id,
370
+ resourceId: parentRun.resourceId ?? undefined,
371
+ eventName: getInvokeChildWorkflowEventName(childRun.id)
318
372
  });
319
- return run;
320
373
  }
321
374
  async pauseWorkflow({
322
375
  runId,
@@ -348,6 +401,9 @@ class WorkflowEngine {
348
401
  if (current.status !== "paused" /* PAUSED */) {
349
402
  throw new WorkflowEngineError(`Cannot resume workflow run in '${current.status}' status, must be 'paused'`, current.workflowId, runId);
350
403
  }
404
+ if (this.getInvokeChildWorkflowStepEntry(current.timeline, current.currentStepId)) {
405
+ return current;
406
+ }
351
407
  return this.triggerEvent({
352
408
  runId,
353
409
  resourceId,
@@ -367,6 +423,9 @@ class WorkflowEngine {
367
423
  return run;
368
424
  }
369
425
  const stepId = run.currentStepId;
426
+ if (this.getInvokeChildWorkflowStepEntry(run.timeline, stepId)) {
427
+ return run;
428
+ }
370
429
  const waitForStep = this.getWaitForStepEntry(run.timeline, stepId);
371
430
  if (!waitForStep) {
372
431
  return run;
@@ -415,6 +474,7 @@ class WorkflowEngine {
415
474
  expectedStatuses: ["pending" /* PENDING */, "running" /* RUNNING */, "paused" /* PAUSED */]
416
475
  });
417
476
  this.logger.log(`cancelled workflow run with id ${runId}`);
477
+ await this.notifyParentOfChildTerminalRun(run);
418
478
  return run;
419
479
  }
420
480
  async triggerEvent({
@@ -652,6 +712,22 @@ class WorkflowEngine {
652
712
  }
653
713
  const timeoutMs = options?.timeout ? parseDuration(options.timeout) : undefined;
654
714
  return this.pollStep({ run, stepId, conditionFn, intervalMs, timeoutMs });
715
+ },
716
+ invokeChildWorkflow: async (stepId, refOrParams, inputArg, optionsArg) => {
717
+ if (!run) {
718
+ throw new WorkflowEngineError("Missing workflow run", workflowId, runId);
719
+ }
720
+ const resolvedChildCall = this.resolveWorkflowRunParameters(refOrParams, inputArg, optionsArg);
721
+ const childWorkflowInvocation = {
722
+ run,
723
+ stepId,
724
+ workflowId: resolvedChildCall.workflowId,
725
+ input: resolvedChildCall.input,
726
+ options: resolvedChildCall.options,
727
+ resourceId: resolvedChildCall.resourceId,
728
+ idempotencyKey: resolvedChildCall.idempotencyKey
729
+ };
730
+ return this.invokeChildWorkflowStep(childWorkflowInvocation);
655
731
  }
656
732
  };
657
733
  let step = { ...baseStep };
@@ -676,7 +752,7 @@ class WorkflowEngine {
676
752
  const shouldComplete = run.status === "running" /* RUNNING */ && (noParsedSteps || isLastParsedStep || hasPluginSteps && result !== undefined);
677
753
  if (shouldComplete) {
678
754
  const normalizedResult = result === undefined ? {} : result;
679
- await this.updateRun({
755
+ const completedRun = await this.updateRun({
680
756
  runId,
681
757
  resourceId: scopedResourceId,
682
758
  data: {
@@ -686,6 +762,7 @@ class WorkflowEngine {
686
762
  jobId: job?.id
687
763
  }
688
764
  });
765
+ await this.notifyParentOfChildTerminalRun(completedRun);
689
766
  this.logger.log("Workflow run completed.", {
690
767
  runId,
691
768
  workflowId
@@ -693,7 +770,7 @@ class WorkflowEngine {
693
770
  }
694
771
  } catch (error) {
695
772
  if (runId) {
696
- await this.updateRun({
773
+ const updatedRun = await this.updateRun({
697
774
  runId,
698
775
  resourceId: scopedResourceId,
699
776
  data: {
@@ -701,6 +778,9 @@ class WorkflowEngine {
701
778
  jobId: job?.id
702
779
  }
703
780
  });
781
+ if (updatedRun.status === "completed" /* COMPLETED */ || updatedRun.status === "failed" /* FAILED */ || updatedRun.status === "cancelled" /* CANCELLED */) {
782
+ await this.notifyParentOfChildTerminalRun(updatedRun);
783
+ }
704
784
  }
705
785
  throw error;
706
786
  }
@@ -712,7 +792,7 @@ class WorkflowEngine {
712
792
  const run = await getWorkflowRun({ runId }, { db: this.db });
713
793
  if (!run || run.status !== "running" /* RUNNING */)
714
794
  return;
715
- await this.updateRun({
795
+ const failedRun = await this.updateRun({
716
796
  runId,
717
797
  resourceId: run.resourceId ?? undefined,
718
798
  data: {
@@ -720,6 +800,7 @@ class WorkflowEngine {
720
800
  error: run.error ?? "Workflow run worker died or job expired before completion"
721
801
  }
722
802
  });
803
+ await this.notifyParentOfChildTerminalRun(failedRun);
723
804
  this.logger.log("Marked stuck workflow run as failed", {
724
805
  runId,
725
806
  workflowId: run.workflowId
@@ -730,9 +811,195 @@ class WorkflowEngine {
730
811
  return stepEntry && typeof stepEntry === "object" && "output" in stepEntry ? stepEntry : null;
731
812
  }
732
813
  getWaitForStepEntry(timeline, stepId) {
733
- const entry = timeline[`${stepId}-wait-for`];
814
+ const entry = timeline[waitForTimelineKey(stepId)];
734
815
  return entry && typeof entry === "object" && "waitFor" in entry ? entry : null;
735
816
  }
817
+ getInvokeChildWorkflowStepEntry(timeline, stepId) {
818
+ const entry = timeline[invokeChildWorkflowTimelineKey(stepId)];
819
+ return isInvokeChildWorkflowTimelineEntry(entry) ? entry : null;
820
+ }
821
+ getCompletedChildOutput(childRun) {
822
+ return childRun.output === undefined ? {} : childRun.output;
823
+ }
824
+ throwForNonCompletedChild(childRun) {
825
+ throw new WorkflowEngineError(`Child workflow ${childRun.workflowId} ${childRun.status}${childRun.error ? `: ${childRun.error}` : ""}`, childRun.workflowId, childRun.id);
826
+ }
827
+ assertInvokeChildWorkflowStepOwnership({
828
+ childRun,
829
+ parentRun,
830
+ stepId,
831
+ workflowId
832
+ }) {
833
+ const expectedParentResourceId = parentRun.resourceId ?? null;
834
+ const matches = childRun.workflowId === workflowId && childRun.parentRunId === parentRun.id && childRun.parentStepId === stepId && childRun.parentResourceId === expectedParentResourceId;
835
+ if (!matches) {
836
+ throw new WorkflowEngineError(`Idempotency key resolved to workflow run ${childRun.id}, which does not belong to invokeChildWorkflow step '${stepId}'`, workflowId, parentRun.id);
837
+ }
838
+ }
839
+ async invokeChildWorkflowStep({
840
+ run,
841
+ stepId,
842
+ workflowId,
843
+ input,
844
+ resourceId,
845
+ idempotencyKey,
846
+ options
847
+ }) {
848
+ let invokeOutput;
849
+ let hasInvokeOutput = false;
850
+ const childResourceId = resourceId ?? run.resourceId ?? undefined;
851
+ const childIdempotencyKey = idempotencyKey;
852
+ await withPostgresTransaction(this.db, async (db) => {
853
+ const lockedRun = await this.getRun({ runId: run.id, resourceId: run.resourceId ?? undefined }, { exclusiveLock: true, db });
854
+ if (lockedRun.status === "cancelled" /* CANCELLED */ || lockedRun.status === "paused" /* PAUSED */ || lockedRun.status === "failed" /* FAILED */) {
855
+ return;
856
+ }
857
+ const lockedCached = this.getCachedStepEntry(lockedRun.timeline, stepId);
858
+ if (lockedCached?.output !== undefined) {
859
+ invokeOutput = lockedCached.output;
860
+ hasInvokeOutput = true;
861
+ return;
862
+ }
863
+ const lockedInvoke = this.getInvokeChildWorkflowStepEntry(lockedRun.timeline, stepId);
864
+ if (lockedInvoke) {
865
+ const existingChildResourceId = "childResourceId" in lockedInvoke.invokeChildWorkflow ? lockedInvoke.invokeChildWorkflow.childResourceId ?? undefined : childResourceId;
866
+ const existingChildRun = await this.getRun({
867
+ runId: lockedInvoke.invokeChildWorkflow.childRunId,
868
+ resourceId: existingChildResourceId
869
+ });
870
+ if (existingChildRun.status === "completed" /* COMPLETED */) {
871
+ invokeOutput = this.getCompletedChildOutput(existingChildRun);
872
+ hasInvokeOutput = true;
873
+ await this.updateRun({
874
+ runId: run.id,
875
+ resourceId: run.resourceId ?? undefined,
876
+ data: {
877
+ timeline: merge(lockedRun.timeline, {
878
+ [stepId]: {
879
+ output: invokeOutput,
880
+ timestamp: new Date
881
+ }
882
+ })
883
+ }
884
+ }, { db });
885
+ return;
886
+ }
887
+ if (existingChildRun.status === "failed" /* FAILED */ || existingChildRun.status === "cancelled" /* CANCELLED */) {
888
+ this.throwForNonCompletedChild(existingChildRun);
889
+ }
890
+ await this.pauseRunForWait({
891
+ run: lockedRun,
892
+ stepId,
893
+ eventName: getInvokeChildWorkflowEventName(existingChildRun.id),
894
+ skipOutput: true,
895
+ db
896
+ });
897
+ return;
898
+ }
899
+ const result = await this.createWorkflowRun({
900
+ workflowId,
901
+ input,
902
+ resourceId: childResourceId,
903
+ idempotencyKey: childIdempotencyKey,
904
+ options,
905
+ parentRunId: run.id,
906
+ parentStepId: stepId,
907
+ parentResourceId: run.resourceId ?? undefined,
908
+ enqueue: true,
909
+ db
910
+ });
911
+ const childRun = result.run;
912
+ if (!result.created) {
913
+ this.assertInvokeChildWorkflowStepOwnership({
914
+ childRun,
915
+ parentRun: lockedRun,
916
+ stepId,
917
+ workflowId
918
+ });
919
+ if (childRun.status === "completed" /* COMPLETED */) {
920
+ invokeOutput = this.getCompletedChildOutput(childRun);
921
+ hasInvokeOutput = true;
922
+ await this.updateRun({
923
+ runId: run.id,
924
+ resourceId: run.resourceId ?? undefined,
925
+ data: {
926
+ timeline: merge(lockedRun.timeline, {
927
+ [invokeChildWorkflowTimelineKey(stepId)]: {
928
+ invokeChildWorkflow: {
929
+ childRunId: childRun.id,
930
+ childWorkflowId: childRun.workflowId,
931
+ childResourceId: childRun.resourceId
932
+ },
933
+ timestamp: new Date
934
+ },
935
+ [stepId]: {
936
+ output: invokeOutput,
937
+ timestamp: new Date
938
+ }
939
+ })
940
+ }
941
+ }, { db });
942
+ return;
943
+ }
944
+ if (childRun.status === "failed" /* FAILED */ || childRun.status === "cancelled" /* CANCELLED */) {
945
+ this.throwForNonCompletedChild(childRun);
946
+ }
947
+ }
948
+ await this.pauseRunForWait({
949
+ run: lockedRun,
950
+ stepId,
951
+ eventName: getInvokeChildWorkflowEventName(childRun.id),
952
+ skipOutput: true,
953
+ db,
954
+ timeline: merge(lockedRun.timeline, {
955
+ [invokeChildWorkflowTimelineKey(stepId)]: {
956
+ invokeChildWorkflow: {
957
+ childRunId: childRun.id,
958
+ childWorkflowId: childRun.workflowId,
959
+ childResourceId: childRun.resourceId
960
+ },
961
+ timestamp: new Date
962
+ }
963
+ })
964
+ });
965
+ }, this.pool);
966
+ if (hasInvokeOutput) {
967
+ return invokeOutput;
968
+ }
969
+ }
970
+ async pauseRunForWait({
971
+ run,
972
+ stepId,
973
+ eventName,
974
+ timeoutEvent,
975
+ skipOutput,
976
+ db,
977
+ timeline
978
+ }) {
979
+ const baseTimeline = timeline ?? run.timeline;
980
+ const waitFor = {};
981
+ if (eventName)
982
+ waitFor.eventName = eventName;
983
+ if (timeoutEvent)
984
+ waitFor.timeoutEvent = timeoutEvent;
985
+ if (skipOutput)
986
+ waitFor.skipOutput = true;
987
+ await this.updateRun({
988
+ runId: run.id,
989
+ resourceId: run.resourceId ?? undefined,
990
+ data: {
991
+ status: "paused" /* PAUSED */,
992
+ currentStepId: stepId,
993
+ pausedAt: new Date,
994
+ timeline: merge(baseTimeline, {
995
+ [waitForTimelineKey(stepId)]: {
996
+ waitFor,
997
+ timestamp: new Date
998
+ }
999
+ })
1000
+ }
1001
+ }, { db });
1002
+ }
736
1003
  async runStep({
737
1004
  stepId,
738
1005
  run,
@@ -821,21 +1088,7 @@ ${error.stack}` : String(error)
821
1088
  const timeoutEvent = timeoutDate ? `__timeout_${stepId}` : undefined;
822
1089
  await withPostgresTransaction(this.db, async (db) => {
823
1090
  const freshRun = await this.getRun({ runId: run.id, resourceId: run.resourceId ?? undefined }, { exclusiveLock: true, db });
824
- return this.updateRun({
825
- runId: run.id,
826
- resourceId: run.resourceId ?? undefined,
827
- data: {
828
- status: "paused" /* PAUSED */,
829
- currentStepId: stepId,
830
- pausedAt: new Date,
831
- timeline: merge(freshRun.timeline, {
832
- [`${stepId}-wait-for`]: {
833
- waitFor: { eventName, timeoutEvent },
834
- timestamp: new Date
835
- }
836
- })
837
- }
838
- }, { db });
1091
+ return this.pauseRunForWait({ run: freshRun, stepId, eventName, timeoutEvent, db });
839
1092
  }, this.pool);
840
1093
  if (timeoutDate && timeoutEvent) {
841
1094
  try {
@@ -949,7 +1202,7 @@ ${error.stack}` : String(error)
949
1202
  pausedAt: new Date,
950
1203
  timeline: merge(freshRun.timeline, {
951
1204
  [`${stepId}-poll`]: { startedAt: startedAt.toISOString() },
952
- [`${stepId}-wait-for`]: {
1205
+ [waitForTimelineKey(stepId)]: {
953
1206
  waitFor: { timeoutEvent: pollEvent, skipOutput: true },
954
1207
  timestamp: new Date
955
1208
  }
@@ -1037,5 +1290,5 @@ export {
1037
1290
  StepType
1038
1291
  };
1039
1292
 
1040
- //# debugId=FC991C83D3B1165A64756E2164756E21
1293
+ //# debugId=F3273B34BE68C60E64756E2164756E21
1041
1294
  //# sourceMappingURL=index.js.map