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/client.entry.cjs +48 -12
- package/dist/client.entry.d.cts +37 -10
- package/dist/client.entry.d.ts +37 -10
- package/dist/client.entry.js +1 -1
- package/dist/client.entry.js.map +8 -8
- package/dist/index.cjs +369 -83
- package/dist/index.d.cts +68 -20
- package/dist/index.d.ts +68 -20
- package/dist/index.js +326 -73
- package/dist/index.js.map +10 -10
- package/dist/shared/{chunk-fr76gdwj.js → chunk-nygamc7b.js} +50 -14
- package/dist/shared/chunk-nygamc7b.js.map +16 -0
- package/package.json +1 -1
- package/dist/shared/chunk-fr76gdwj.js.map +0 -16
package/dist/index.cjs
CHANGED
|
@@ -90,10 +90,15 @@ var WORKFLOW_RUN_DLQ_QUEUE_NAME = "workflow_run_dlq";
|
|
|
90
90
|
var DEFAULT_PGBOSS_SCHEMA = "pgboss_v12_pgworkflow";
|
|
91
91
|
var MAX_WORKFLOW_ID_LENGTH = 256;
|
|
92
92
|
var MAX_RESOURCE_ID_LENGTH = 256;
|
|
93
|
+
var INVOKE_CHILD_WORKFLOW_TIMELINE_SUFFIX = "invoke-child-workflow";
|
|
94
|
+
var WAIT_FOR_TIMELINE_SUFFIX = "wait-for";
|
|
95
|
+
var invokeChildWorkflowTimelineKey = (stepId) => `${stepId}-${INVOKE_CHILD_WORKFLOW_TIMELINE_SUFFIX}`;
|
|
96
|
+
var waitForTimelineKey = (stepId) => `${stepId}-${WAIT_FOR_TIMELINE_SUFFIX}`;
|
|
97
|
+
var isInvokeChildWorkflowTimelineEntry = (entry) => !!entry && typeof entry === "object" && ("invokeChildWorkflow" in entry);
|
|
93
98
|
|
|
94
99
|
// src/db/migration.ts
|
|
95
100
|
var MIGRATION_LOCK_ID = 738291645;
|
|
96
|
-
var CURRENT_SCHEMA_VERSION =
|
|
101
|
+
var CURRENT_SCHEMA_VERSION = 4;
|
|
97
102
|
async function runMigrations(db) {
|
|
98
103
|
if (await isSchemaUpToDate(db)) {
|
|
99
104
|
return;
|
|
@@ -151,6 +156,11 @@ async function runMigrations(db) {
|
|
|
151
156
|
commands.push("ALTER TABLE workflow_runs ALTER COLUMN resource_id TYPE varchar(256)");
|
|
152
157
|
commands.push("ALTER TABLE workflow_runs ALTER COLUMN workflow_id TYPE varchar(256)");
|
|
153
158
|
}
|
|
159
|
+
if (currentVersion < 4) {
|
|
160
|
+
commands.push("ALTER TABLE workflow_runs ADD COLUMN IF NOT EXISTS parent_run_id varchar(32)");
|
|
161
|
+
commands.push("ALTER TABLE workflow_runs ADD COLUMN IF NOT EXISTS parent_step_id varchar(256)");
|
|
162
|
+
commands.push("ALTER TABLE workflow_runs ADD COLUMN IF NOT EXISTS parent_resource_id varchar(256)");
|
|
163
|
+
}
|
|
154
164
|
if (currentVersion === 0) {
|
|
155
165
|
commands.push(`INSERT INTO workflow_schema_version (version) VALUES (${CURRENT_SCHEMA_VERSION})`);
|
|
156
166
|
} else {
|
|
@@ -213,7 +223,10 @@ function mapRowToWorkflowRun(row) {
|
|
|
213
223
|
retryCount: row.retry_count,
|
|
214
224
|
maxRetries: row.max_retries,
|
|
215
225
|
jobId: row.job_id,
|
|
216
|
-
idempotencyKey: row.idempotency_key
|
|
226
|
+
idempotencyKey: row.idempotency_key,
|
|
227
|
+
parentRunId: row.parent_run_id,
|
|
228
|
+
parentStepId: row.parent_step_id,
|
|
229
|
+
parentResourceId: row.parent_resource_id
|
|
217
230
|
};
|
|
218
231
|
}
|
|
219
232
|
async function insertWorkflowRun({
|
|
@@ -224,7 +237,10 @@ async function insertWorkflowRun({
|
|
|
224
237
|
input,
|
|
225
238
|
maxRetries,
|
|
226
239
|
timeoutAt,
|
|
227
|
-
idempotencyKey
|
|
240
|
+
idempotencyKey,
|
|
241
|
+
parentRunId,
|
|
242
|
+
parentStepId,
|
|
243
|
+
parentResourceId
|
|
228
244
|
}, db) {
|
|
229
245
|
const runId = generateKSUID("run");
|
|
230
246
|
const now = new Date;
|
|
@@ -241,9 +257,12 @@ async function insertWorkflowRun({
|
|
|
241
257
|
updated_at,
|
|
242
258
|
timeline,
|
|
243
259
|
retry_count,
|
|
244
|
-
idempotency_key
|
|
260
|
+
idempotency_key,
|
|
261
|
+
parent_run_id,
|
|
262
|
+
parent_step_id,
|
|
263
|
+
parent_resource_id
|
|
245
264
|
)
|
|
246
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
|
265
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
|
247
266
|
ON CONFLICT (idempotency_key) WHERE idempotency_key IS NOT NULL DO NOTHING
|
|
248
267
|
RETURNING *`, [
|
|
249
268
|
runId,
|
|
@@ -258,7 +277,10 @@ async function insertWorkflowRun({
|
|
|
258
277
|
now,
|
|
259
278
|
"{}",
|
|
260
279
|
0,
|
|
261
|
-
idempotencyKey ?? null
|
|
280
|
+
idempotencyKey ?? null,
|
|
281
|
+
parentRunId ?? null,
|
|
282
|
+
parentStepId ?? null,
|
|
283
|
+
parentResourceId ?? null
|
|
262
284
|
]);
|
|
263
285
|
if (result.rows[0]) {
|
|
264
286
|
return { run: mapRowToWorkflowRun(result.rows[0]), created: true };
|
|
@@ -529,6 +551,7 @@ var StepType;
|
|
|
529
551
|
StepType2["WAIT_UNTIL"] = "waitUntil";
|
|
530
552
|
StepType2["DELAY"] = "delay";
|
|
531
553
|
StepType2["POLL"] = "poll";
|
|
554
|
+
StepType2["INVOKE_CHILD_WORKFLOW"] = "invokeChildWorkflow";
|
|
532
555
|
})(StepType ||= {});
|
|
533
556
|
|
|
534
557
|
// src/client.ts
|
|
@@ -636,7 +659,8 @@ class WorkflowClient {
|
|
|
636
659
|
};
|
|
637
660
|
await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
|
|
638
661
|
startAfter: new Date,
|
|
639
|
-
expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds
|
|
662
|
+
expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds,
|
|
663
|
+
db: _db
|
|
640
664
|
});
|
|
641
665
|
}
|
|
642
666
|
return insertedRun;
|
|
@@ -699,6 +723,11 @@ class WorkflowClient {
|
|
|
699
723
|
if (current.status !== "paused" /* PAUSED */) {
|
|
700
724
|
throw new WorkflowEngineError(`Cannot resume workflow run in '${current.status}' status, must be 'paused'`, current.workflowId, runId);
|
|
701
725
|
}
|
|
726
|
+
const currentStepId = current.currentStepId;
|
|
727
|
+
const currentStepTimelineEntry = current.timeline[invokeChildWorkflowTimelineKey(currentStepId)];
|
|
728
|
+
if (isInvokeChildWorkflowTimelineEntry(currentStepTimelineEntry)) {
|
|
729
|
+
return current;
|
|
730
|
+
}
|
|
702
731
|
return this.triggerEvent({
|
|
703
732
|
runId,
|
|
704
733
|
resourceId,
|
|
@@ -717,8 +746,12 @@ class WorkflowClient {
|
|
|
717
746
|
if (run.status !== "paused" /* PAUSED */) {
|
|
718
747
|
return run;
|
|
719
748
|
}
|
|
720
|
-
const
|
|
721
|
-
const
|
|
749
|
+
const currentStepId = run.currentStepId;
|
|
750
|
+
const currentStepTimelineEntry = run.timeline[invokeChildWorkflowTimelineKey(currentStepId)];
|
|
751
|
+
if (isInvokeChildWorkflowTimelineEntry(currentStepTimelineEntry)) {
|
|
752
|
+
return run;
|
|
753
|
+
}
|
|
754
|
+
const waitForEntry = run.timeline[waitForTimelineKey(currentStepId)];
|
|
722
755
|
if (!waitForEntry || typeof waitForEntry !== "object" || !("waitFor" in waitForEntry)) {
|
|
723
756
|
return run;
|
|
724
757
|
}
|
|
@@ -736,7 +769,7 @@ class WorkflowClient {
|
|
|
736
769
|
resourceId,
|
|
737
770
|
data: {
|
|
738
771
|
timeline: import_es_toolkit.merge(freshRun.timeline, {
|
|
739
|
-
[
|
|
772
|
+
[currentStepId]: {
|
|
740
773
|
output: data ?? {},
|
|
741
774
|
timestamp: new Date
|
|
742
775
|
}
|
|
@@ -836,7 +869,10 @@ function createWorkflowRef(id, options) {
|
|
|
836
869
|
retries: defineOptions?.retries
|
|
837
870
|
});
|
|
838
871
|
Object.defineProperty(ref, "id", { value: id, enumerable: true });
|
|
839
|
-
Object.defineProperty(ref, "inputSchema", {
|
|
872
|
+
Object.defineProperty(ref, "inputSchema", {
|
|
873
|
+
value: options?.inputSchema,
|
|
874
|
+
enumerable: true
|
|
875
|
+
});
|
|
840
876
|
return ref;
|
|
841
877
|
}
|
|
842
878
|
function createWorkflowFactory(plugins = []) {
|
|
@@ -931,7 +967,7 @@ function parseWorkflowHandler(handler) {
|
|
|
931
967
|
const propertyAccess = node.expression;
|
|
932
968
|
const objectName = propertyAccess.expression.getText(sourceFile);
|
|
933
969
|
const methodName = propertyAccess.name.text;
|
|
934
|
-
if (objectName === "step" && (methodName === "run" || methodName === "waitFor" || methodName === "pause" || methodName === "waitUntil" || methodName === "delay" || methodName === "sleep" || methodName === "poll")) {
|
|
970
|
+
if (objectName === "step" && (methodName === "run" || methodName === "waitFor" || methodName === "pause" || methodName === "waitUntil" || methodName === "delay" || methodName === "sleep" || methodName === "poll" || methodName === "invokeChildWorkflow")) {
|
|
935
971
|
const firstArg = node.arguments[0];
|
|
936
972
|
if (firstArg) {
|
|
937
973
|
const { id, isDynamic } = extractStepId(firstArg);
|
|
@@ -964,7 +1000,8 @@ var StepTypeToIcon = {
|
|
|
964
1000
|
["pause" /* PAUSE */]: "⏸",
|
|
965
1001
|
["waitUntil" /* WAIT_UNTIL */]: "⏲",
|
|
966
1002
|
["delay" /* DELAY */]: "⏱",
|
|
967
|
-
["poll" /* POLL */]: "↻"
|
|
1003
|
+
["poll" /* POLL */]: "↻",
|
|
1004
|
+
["invokeChildWorkflow" /* INVOKE_CHILD_WORKFLOW */]: "↪"
|
|
968
1005
|
};
|
|
969
1006
|
var defaultLogger2 = {
|
|
970
1007
|
log: (_message) => console.warn(_message),
|
|
@@ -976,6 +1013,7 @@ var retrySendOptions = (maxRetries) => ({
|
|
|
976
1013
|
retryBackoff: true,
|
|
977
1014
|
retryDelay: 1
|
|
978
1015
|
});
|
|
1016
|
+
var getInvokeChildWorkflowEventName = (childRunId) => `__invoke_child_workflow_completed:${childRunId}`;
|
|
979
1017
|
var defaultHeartbeatSeconds = process.env.WORKFLOW_RUN_HEARTBEAT_SECONDS ? Number.parseInt(process.env.WORKFLOW_RUN_HEARTBEAT_SECONDS, 10) : 30;
|
|
980
1018
|
|
|
981
1019
|
class WorkflowEngine {
|
|
@@ -1081,31 +1119,57 @@ class WorkflowEngine {
|
|
|
1081
1119
|
this.workflows.clear();
|
|
1082
1120
|
return this;
|
|
1083
1121
|
}
|
|
1084
|
-
|
|
1085
|
-
let workflowId;
|
|
1086
|
-
let input;
|
|
1087
|
-
let resourceId;
|
|
1088
|
-
let idempotencyKey;
|
|
1089
|
-
let options;
|
|
1122
|
+
resolveWorkflowRunParameters(refOrParams, inputArg, optionsArg) {
|
|
1090
1123
|
if (typeof refOrParams === "function" && "id" in refOrParams) {
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
workflowId = params.workflowId;
|
|
1099
|
-
input = params.input;
|
|
1100
|
-
resourceId = params.resourceId;
|
|
1101
|
-
idempotencyKey = params.idempotencyKey;
|
|
1102
|
-
options = params.options;
|
|
1124
|
+
return {
|
|
1125
|
+
workflowId: refOrParams.id,
|
|
1126
|
+
input: inputArg,
|
|
1127
|
+
options: optionsArg,
|
|
1128
|
+
resourceId: optionsArg?.resourceId,
|
|
1129
|
+
idempotencyKey: optionsArg?.idempotencyKey
|
|
1130
|
+
};
|
|
1103
1131
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1132
|
+
const params = refOrParams;
|
|
1133
|
+
return {
|
|
1134
|
+
workflowId: params.workflowId,
|
|
1135
|
+
input: params.input,
|
|
1136
|
+
resourceId: params.resourceId ?? params.options?.resourceId,
|
|
1137
|
+
idempotencyKey: params.idempotencyKey ?? params.options?.idempotencyKey,
|
|
1138
|
+
options: params.options
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
async startWorkflow(refOrParams, inputArg, optionsArg) {
|
|
1142
|
+
const { workflowId, input, resourceId, idempotencyKey, options } = this.resolveWorkflowRunParameters(refOrParams, inputArg, optionsArg);
|
|
1106
1143
|
if (!this._started) {
|
|
1107
1144
|
await this.start(false, { batchSize: options?.batchSize ?? 1 });
|
|
1108
1145
|
}
|
|
1146
|
+
const { run } = await this.createWorkflowRun({
|
|
1147
|
+
workflowId,
|
|
1148
|
+
input,
|
|
1149
|
+
resourceId,
|
|
1150
|
+
idempotencyKey,
|
|
1151
|
+
options
|
|
1152
|
+
});
|
|
1153
|
+
this.logger.log("Started workflow run", {
|
|
1154
|
+
runId: run.id,
|
|
1155
|
+
workflowId
|
|
1156
|
+
});
|
|
1157
|
+
return run;
|
|
1158
|
+
}
|
|
1159
|
+
async createWorkflowRun({
|
|
1160
|
+
workflowId,
|
|
1161
|
+
input,
|
|
1162
|
+
resourceId,
|
|
1163
|
+
idempotencyKey,
|
|
1164
|
+
options,
|
|
1165
|
+
parentRunId,
|
|
1166
|
+
parentStepId,
|
|
1167
|
+
parentResourceId,
|
|
1168
|
+
enqueue = true,
|
|
1169
|
+
db
|
|
1170
|
+
}) {
|
|
1171
|
+
validateWorkflowId(workflowId);
|
|
1172
|
+
validateResourceId(resourceId);
|
|
1109
1173
|
const workflow2 = this.workflows.get(workflowId);
|
|
1110
1174
|
if (!workflow2) {
|
|
1111
1175
|
throw new WorkflowEngineError(`Unknown workflow ${workflowId}`);
|
|
@@ -1122,38 +1186,60 @@ class WorkflowEngine {
|
|
|
1122
1186
|
}
|
|
1123
1187
|
}
|
|
1124
1188
|
const initialStepId = workflow2.steps[0]?.id ?? "__start__";
|
|
1125
|
-
const
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
};
|
|
1144
|
-
await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
|
|
1145
|
-
startAfter: new Date,
|
|
1146
|
-
expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds2,
|
|
1147
|
-
...retrySendOptions(insertedRun.maxRetries)
|
|
1148
|
-
});
|
|
1189
|
+
const timeoutAt = options?.timeout ? new Date(Date.now() + options.timeout) : workflow2.timeout ? new Date(Date.now() + workflow2.timeout) : null;
|
|
1190
|
+
const insertRun = async (targetDb) => await insertWorkflowRun({
|
|
1191
|
+
resourceId,
|
|
1192
|
+
workflowId,
|
|
1193
|
+
currentStepId: initialStepId,
|
|
1194
|
+
status: "running" /* RUNNING */,
|
|
1195
|
+
input,
|
|
1196
|
+
maxRetries: options?.retries ?? workflow2.retries ?? 0,
|
|
1197
|
+
timeoutAt,
|
|
1198
|
+
idempotencyKey,
|
|
1199
|
+
parentRunId,
|
|
1200
|
+
parentStepId,
|
|
1201
|
+
parentResourceId
|
|
1202
|
+
}, targetDb);
|
|
1203
|
+
const insertAndEnqueue = async (targetDb) => {
|
|
1204
|
+
const result = await insertRun(targetDb);
|
|
1205
|
+
if (enqueue && result.created) {
|
|
1206
|
+
await this.enqueueWorkflowRun(result.run, options, targetDb);
|
|
1149
1207
|
}
|
|
1150
|
-
return
|
|
1151
|
-
}
|
|
1152
|
-
this.
|
|
1208
|
+
return result;
|
|
1209
|
+
};
|
|
1210
|
+
const { run, created } = db ? await insertAndEnqueue(db) : await withPostgresTransaction(this.boss.getDb(), insertAndEnqueue, this.pool);
|
|
1211
|
+
return { run, created };
|
|
1212
|
+
}
|
|
1213
|
+
async enqueueWorkflowRun(run, options, db) {
|
|
1214
|
+
const job = {
|
|
1153
1215
|
runId: run.id,
|
|
1154
|
-
|
|
1216
|
+
resourceId: run.resourceId ?? undefined,
|
|
1217
|
+
workflowId: run.workflowId,
|
|
1218
|
+
input: run.input
|
|
1219
|
+
};
|
|
1220
|
+
await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
|
|
1221
|
+
startAfter: new Date,
|
|
1222
|
+
expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds2,
|
|
1223
|
+
...retrySendOptions(run.maxRetries),
|
|
1224
|
+
...db ? { db } : {}
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
async notifyParentOfChildTerminalRun(childRun) {
|
|
1228
|
+
if (!childRun.parentRunId || !childRun.parentStepId) {
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
const parentRun = await getWorkflowRun({
|
|
1232
|
+
runId: childRun.parentRunId,
|
|
1233
|
+
resourceId: childRun.parentResourceId ?? undefined
|
|
1234
|
+
}, { db: this.db });
|
|
1235
|
+
if (!parentRun || parentRun.status === "completed" /* COMPLETED */ || parentRun.status === "failed" /* FAILED */ || parentRun.status === "cancelled" /* CANCELLED */) {
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
await this.triggerEvent({
|
|
1239
|
+
runId: parentRun.id,
|
|
1240
|
+
resourceId: parentRun.resourceId ?? undefined,
|
|
1241
|
+
eventName: getInvokeChildWorkflowEventName(childRun.id)
|
|
1155
1242
|
});
|
|
1156
|
-
return run;
|
|
1157
1243
|
}
|
|
1158
1244
|
async pauseWorkflow({
|
|
1159
1245
|
runId,
|
|
@@ -1185,6 +1271,9 @@ class WorkflowEngine {
|
|
|
1185
1271
|
if (current.status !== "paused" /* PAUSED */) {
|
|
1186
1272
|
throw new WorkflowEngineError(`Cannot resume workflow run in '${current.status}' status, must be 'paused'`, current.workflowId, runId);
|
|
1187
1273
|
}
|
|
1274
|
+
if (this.getInvokeChildWorkflowStepEntry(current.timeline, current.currentStepId)) {
|
|
1275
|
+
return current;
|
|
1276
|
+
}
|
|
1188
1277
|
return this.triggerEvent({
|
|
1189
1278
|
runId,
|
|
1190
1279
|
resourceId,
|
|
@@ -1204,6 +1293,9 @@ class WorkflowEngine {
|
|
|
1204
1293
|
return run;
|
|
1205
1294
|
}
|
|
1206
1295
|
const stepId = run.currentStepId;
|
|
1296
|
+
if (this.getInvokeChildWorkflowStepEntry(run.timeline, stepId)) {
|
|
1297
|
+
return run;
|
|
1298
|
+
}
|
|
1207
1299
|
const waitForStep = this.getWaitForStepEntry(run.timeline, stepId);
|
|
1208
1300
|
if (!waitForStep) {
|
|
1209
1301
|
return run;
|
|
@@ -1252,6 +1344,7 @@ class WorkflowEngine {
|
|
|
1252
1344
|
expectedStatuses: ["pending" /* PENDING */, "running" /* RUNNING */, "paused" /* PAUSED */]
|
|
1253
1345
|
});
|
|
1254
1346
|
this.logger.log(`cancelled workflow run with id ${runId}`);
|
|
1347
|
+
await this.notifyParentOfChildTerminalRun(run);
|
|
1255
1348
|
return run;
|
|
1256
1349
|
}
|
|
1257
1350
|
async triggerEvent({
|
|
@@ -1489,6 +1582,22 @@ class WorkflowEngine {
|
|
|
1489
1582
|
}
|
|
1490
1583
|
const timeoutMs = options?.timeout ? parseDuration(options.timeout) : undefined;
|
|
1491
1584
|
return this.pollStep({ run, stepId, conditionFn, intervalMs, timeoutMs });
|
|
1585
|
+
},
|
|
1586
|
+
invokeChildWorkflow: async (stepId, refOrParams, inputArg, optionsArg) => {
|
|
1587
|
+
if (!run) {
|
|
1588
|
+
throw new WorkflowEngineError("Missing workflow run", workflowId, runId);
|
|
1589
|
+
}
|
|
1590
|
+
const resolvedChildCall = this.resolveWorkflowRunParameters(refOrParams, inputArg, optionsArg);
|
|
1591
|
+
const childWorkflowInvocation = {
|
|
1592
|
+
run,
|
|
1593
|
+
stepId,
|
|
1594
|
+
workflowId: resolvedChildCall.workflowId,
|
|
1595
|
+
input: resolvedChildCall.input,
|
|
1596
|
+
options: resolvedChildCall.options,
|
|
1597
|
+
resourceId: resolvedChildCall.resourceId,
|
|
1598
|
+
idempotencyKey: resolvedChildCall.idempotencyKey
|
|
1599
|
+
};
|
|
1600
|
+
return this.invokeChildWorkflowStep(childWorkflowInvocation);
|
|
1492
1601
|
}
|
|
1493
1602
|
};
|
|
1494
1603
|
let step = { ...baseStep };
|
|
@@ -1513,7 +1622,7 @@ class WorkflowEngine {
|
|
|
1513
1622
|
const shouldComplete = run.status === "running" /* RUNNING */ && (noParsedSteps || isLastParsedStep || hasPluginSteps && result !== undefined);
|
|
1514
1623
|
if (shouldComplete) {
|
|
1515
1624
|
const normalizedResult = result === undefined ? {} : result;
|
|
1516
|
-
await this.updateRun({
|
|
1625
|
+
const completedRun = await this.updateRun({
|
|
1517
1626
|
runId,
|
|
1518
1627
|
resourceId: scopedResourceId,
|
|
1519
1628
|
data: {
|
|
@@ -1523,6 +1632,7 @@ class WorkflowEngine {
|
|
|
1523
1632
|
jobId: job?.id
|
|
1524
1633
|
}
|
|
1525
1634
|
});
|
|
1635
|
+
await this.notifyParentOfChildTerminalRun(completedRun);
|
|
1526
1636
|
this.logger.log("Workflow run completed.", {
|
|
1527
1637
|
runId,
|
|
1528
1638
|
workflowId
|
|
@@ -1530,7 +1640,7 @@ class WorkflowEngine {
|
|
|
1530
1640
|
}
|
|
1531
1641
|
} catch (error) {
|
|
1532
1642
|
if (runId) {
|
|
1533
|
-
await this.updateRun({
|
|
1643
|
+
const updatedRun = await this.updateRun({
|
|
1534
1644
|
runId,
|
|
1535
1645
|
resourceId: scopedResourceId,
|
|
1536
1646
|
data: {
|
|
@@ -1538,6 +1648,9 @@ class WorkflowEngine {
|
|
|
1538
1648
|
jobId: job?.id
|
|
1539
1649
|
}
|
|
1540
1650
|
});
|
|
1651
|
+
if (updatedRun.status === "completed" /* COMPLETED */ || updatedRun.status === "failed" /* FAILED */ || updatedRun.status === "cancelled" /* CANCELLED */) {
|
|
1652
|
+
await this.notifyParentOfChildTerminalRun(updatedRun);
|
|
1653
|
+
}
|
|
1541
1654
|
}
|
|
1542
1655
|
throw error;
|
|
1543
1656
|
}
|
|
@@ -1549,7 +1662,7 @@ class WorkflowEngine {
|
|
|
1549
1662
|
const run = await getWorkflowRun({ runId }, { db: this.db });
|
|
1550
1663
|
if (!run || run.status !== "running" /* RUNNING */)
|
|
1551
1664
|
return;
|
|
1552
|
-
await this.updateRun({
|
|
1665
|
+
const failedRun = await this.updateRun({
|
|
1553
1666
|
runId,
|
|
1554
1667
|
resourceId: run.resourceId ?? undefined,
|
|
1555
1668
|
data: {
|
|
@@ -1557,6 +1670,7 @@ class WorkflowEngine {
|
|
|
1557
1670
|
error: run.error ?? "Workflow run worker died or job expired before completion"
|
|
1558
1671
|
}
|
|
1559
1672
|
});
|
|
1673
|
+
await this.notifyParentOfChildTerminalRun(failedRun);
|
|
1560
1674
|
this.logger.log("Marked stuck workflow run as failed", {
|
|
1561
1675
|
runId,
|
|
1562
1676
|
workflowId: run.workflowId
|
|
@@ -1567,9 +1681,195 @@ class WorkflowEngine {
|
|
|
1567
1681
|
return stepEntry && typeof stepEntry === "object" && "output" in stepEntry ? stepEntry : null;
|
|
1568
1682
|
}
|
|
1569
1683
|
getWaitForStepEntry(timeline, stepId) {
|
|
1570
|
-
const entry = timeline[
|
|
1684
|
+
const entry = timeline[waitForTimelineKey(stepId)];
|
|
1571
1685
|
return entry && typeof entry === "object" && "waitFor" in entry ? entry : null;
|
|
1572
1686
|
}
|
|
1687
|
+
getInvokeChildWorkflowStepEntry(timeline, stepId) {
|
|
1688
|
+
const entry = timeline[invokeChildWorkflowTimelineKey(stepId)];
|
|
1689
|
+
return isInvokeChildWorkflowTimelineEntry(entry) ? entry : null;
|
|
1690
|
+
}
|
|
1691
|
+
getCompletedChildOutput(childRun) {
|
|
1692
|
+
return childRun.output === undefined ? {} : childRun.output;
|
|
1693
|
+
}
|
|
1694
|
+
throwForNonCompletedChild(childRun) {
|
|
1695
|
+
throw new WorkflowEngineError(`Child workflow ${childRun.workflowId} ${childRun.status}${childRun.error ? `: ${childRun.error}` : ""}`, childRun.workflowId, childRun.id);
|
|
1696
|
+
}
|
|
1697
|
+
assertInvokeChildWorkflowStepOwnership({
|
|
1698
|
+
childRun,
|
|
1699
|
+
parentRun,
|
|
1700
|
+
stepId,
|
|
1701
|
+
workflowId
|
|
1702
|
+
}) {
|
|
1703
|
+
const expectedParentResourceId = parentRun.resourceId ?? null;
|
|
1704
|
+
const matches = childRun.workflowId === workflowId && childRun.parentRunId === parentRun.id && childRun.parentStepId === stepId && childRun.parentResourceId === expectedParentResourceId;
|
|
1705
|
+
if (!matches) {
|
|
1706
|
+
throw new WorkflowEngineError(`Idempotency key resolved to workflow run ${childRun.id}, which does not belong to invokeChildWorkflow step '${stepId}'`, workflowId, parentRun.id);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
async invokeChildWorkflowStep({
|
|
1710
|
+
run,
|
|
1711
|
+
stepId,
|
|
1712
|
+
workflowId,
|
|
1713
|
+
input,
|
|
1714
|
+
resourceId,
|
|
1715
|
+
idempotencyKey,
|
|
1716
|
+
options
|
|
1717
|
+
}) {
|
|
1718
|
+
let invokeOutput;
|
|
1719
|
+
let hasInvokeOutput = false;
|
|
1720
|
+
const childResourceId = resourceId ?? run.resourceId ?? undefined;
|
|
1721
|
+
const childIdempotencyKey = idempotencyKey;
|
|
1722
|
+
await withPostgresTransaction(this.db, async (db) => {
|
|
1723
|
+
const lockedRun = await this.getRun({ runId: run.id, resourceId: run.resourceId ?? undefined }, { exclusiveLock: true, db });
|
|
1724
|
+
if (lockedRun.status === "cancelled" /* CANCELLED */ || lockedRun.status === "paused" /* PAUSED */ || lockedRun.status === "failed" /* FAILED */) {
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
const lockedCached = this.getCachedStepEntry(lockedRun.timeline, stepId);
|
|
1728
|
+
if (lockedCached?.output !== undefined) {
|
|
1729
|
+
invokeOutput = lockedCached.output;
|
|
1730
|
+
hasInvokeOutput = true;
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
const lockedInvoke = this.getInvokeChildWorkflowStepEntry(lockedRun.timeline, stepId);
|
|
1734
|
+
if (lockedInvoke) {
|
|
1735
|
+
const existingChildResourceId = "childResourceId" in lockedInvoke.invokeChildWorkflow ? lockedInvoke.invokeChildWorkflow.childResourceId ?? undefined : childResourceId;
|
|
1736
|
+
const existingChildRun = await this.getRun({
|
|
1737
|
+
runId: lockedInvoke.invokeChildWorkflow.childRunId,
|
|
1738
|
+
resourceId: existingChildResourceId
|
|
1739
|
+
});
|
|
1740
|
+
if (existingChildRun.status === "completed" /* COMPLETED */) {
|
|
1741
|
+
invokeOutput = this.getCompletedChildOutput(existingChildRun);
|
|
1742
|
+
hasInvokeOutput = true;
|
|
1743
|
+
await this.updateRun({
|
|
1744
|
+
runId: run.id,
|
|
1745
|
+
resourceId: run.resourceId ?? undefined,
|
|
1746
|
+
data: {
|
|
1747
|
+
timeline: import_es_toolkit2.merge(lockedRun.timeline, {
|
|
1748
|
+
[stepId]: {
|
|
1749
|
+
output: invokeOutput,
|
|
1750
|
+
timestamp: new Date
|
|
1751
|
+
}
|
|
1752
|
+
})
|
|
1753
|
+
}
|
|
1754
|
+
}, { db });
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
if (existingChildRun.status === "failed" /* FAILED */ || existingChildRun.status === "cancelled" /* CANCELLED */) {
|
|
1758
|
+
this.throwForNonCompletedChild(existingChildRun);
|
|
1759
|
+
}
|
|
1760
|
+
await this.pauseRunForWait({
|
|
1761
|
+
run: lockedRun,
|
|
1762
|
+
stepId,
|
|
1763
|
+
eventName: getInvokeChildWorkflowEventName(existingChildRun.id),
|
|
1764
|
+
skipOutput: true,
|
|
1765
|
+
db
|
|
1766
|
+
});
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
const result = await this.createWorkflowRun({
|
|
1770
|
+
workflowId,
|
|
1771
|
+
input,
|
|
1772
|
+
resourceId: childResourceId,
|
|
1773
|
+
idempotencyKey: childIdempotencyKey,
|
|
1774
|
+
options,
|
|
1775
|
+
parentRunId: run.id,
|
|
1776
|
+
parentStepId: stepId,
|
|
1777
|
+
parentResourceId: run.resourceId ?? undefined,
|
|
1778
|
+
enqueue: true,
|
|
1779
|
+
db
|
|
1780
|
+
});
|
|
1781
|
+
const childRun = result.run;
|
|
1782
|
+
if (!result.created) {
|
|
1783
|
+
this.assertInvokeChildWorkflowStepOwnership({
|
|
1784
|
+
childRun,
|
|
1785
|
+
parentRun: lockedRun,
|
|
1786
|
+
stepId,
|
|
1787
|
+
workflowId
|
|
1788
|
+
});
|
|
1789
|
+
if (childRun.status === "completed" /* COMPLETED */) {
|
|
1790
|
+
invokeOutput = this.getCompletedChildOutput(childRun);
|
|
1791
|
+
hasInvokeOutput = true;
|
|
1792
|
+
await this.updateRun({
|
|
1793
|
+
runId: run.id,
|
|
1794
|
+
resourceId: run.resourceId ?? undefined,
|
|
1795
|
+
data: {
|
|
1796
|
+
timeline: import_es_toolkit2.merge(lockedRun.timeline, {
|
|
1797
|
+
[invokeChildWorkflowTimelineKey(stepId)]: {
|
|
1798
|
+
invokeChildWorkflow: {
|
|
1799
|
+
childRunId: childRun.id,
|
|
1800
|
+
childWorkflowId: childRun.workflowId,
|
|
1801
|
+
childResourceId: childRun.resourceId
|
|
1802
|
+
},
|
|
1803
|
+
timestamp: new Date
|
|
1804
|
+
},
|
|
1805
|
+
[stepId]: {
|
|
1806
|
+
output: invokeOutput,
|
|
1807
|
+
timestamp: new Date
|
|
1808
|
+
}
|
|
1809
|
+
})
|
|
1810
|
+
}
|
|
1811
|
+
}, { db });
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
if (childRun.status === "failed" /* FAILED */ || childRun.status === "cancelled" /* CANCELLED */) {
|
|
1815
|
+
this.throwForNonCompletedChild(childRun);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
await this.pauseRunForWait({
|
|
1819
|
+
run: lockedRun,
|
|
1820
|
+
stepId,
|
|
1821
|
+
eventName: getInvokeChildWorkflowEventName(childRun.id),
|
|
1822
|
+
skipOutput: true,
|
|
1823
|
+
db,
|
|
1824
|
+
timeline: import_es_toolkit2.merge(lockedRun.timeline, {
|
|
1825
|
+
[invokeChildWorkflowTimelineKey(stepId)]: {
|
|
1826
|
+
invokeChildWorkflow: {
|
|
1827
|
+
childRunId: childRun.id,
|
|
1828
|
+
childWorkflowId: childRun.workflowId,
|
|
1829
|
+
childResourceId: childRun.resourceId
|
|
1830
|
+
},
|
|
1831
|
+
timestamp: new Date
|
|
1832
|
+
}
|
|
1833
|
+
})
|
|
1834
|
+
});
|
|
1835
|
+
}, this.pool);
|
|
1836
|
+
if (hasInvokeOutput) {
|
|
1837
|
+
return invokeOutput;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
async pauseRunForWait({
|
|
1841
|
+
run,
|
|
1842
|
+
stepId,
|
|
1843
|
+
eventName,
|
|
1844
|
+
timeoutEvent,
|
|
1845
|
+
skipOutput,
|
|
1846
|
+
db,
|
|
1847
|
+
timeline
|
|
1848
|
+
}) {
|
|
1849
|
+
const baseTimeline = timeline ?? run.timeline;
|
|
1850
|
+
const waitFor = {};
|
|
1851
|
+
if (eventName)
|
|
1852
|
+
waitFor.eventName = eventName;
|
|
1853
|
+
if (timeoutEvent)
|
|
1854
|
+
waitFor.timeoutEvent = timeoutEvent;
|
|
1855
|
+
if (skipOutput)
|
|
1856
|
+
waitFor.skipOutput = true;
|
|
1857
|
+
await this.updateRun({
|
|
1858
|
+
runId: run.id,
|
|
1859
|
+
resourceId: run.resourceId ?? undefined,
|
|
1860
|
+
data: {
|
|
1861
|
+
status: "paused" /* PAUSED */,
|
|
1862
|
+
currentStepId: stepId,
|
|
1863
|
+
pausedAt: new Date,
|
|
1864
|
+
timeline: import_es_toolkit2.merge(baseTimeline, {
|
|
1865
|
+
[waitForTimelineKey(stepId)]: {
|
|
1866
|
+
waitFor,
|
|
1867
|
+
timestamp: new Date
|
|
1868
|
+
}
|
|
1869
|
+
})
|
|
1870
|
+
}
|
|
1871
|
+
}, { db });
|
|
1872
|
+
}
|
|
1573
1873
|
async runStep({
|
|
1574
1874
|
stepId,
|
|
1575
1875
|
run,
|
|
@@ -1658,21 +1958,7 @@ ${error.stack}` : String(error)
|
|
|
1658
1958
|
const timeoutEvent = timeoutDate ? `__timeout_${stepId}` : undefined;
|
|
1659
1959
|
await withPostgresTransaction(this.db, async (db) => {
|
|
1660
1960
|
const freshRun = await this.getRun({ runId: run.id, resourceId: run.resourceId ?? undefined }, { exclusiveLock: true, db });
|
|
1661
|
-
return this.
|
|
1662
|
-
runId: run.id,
|
|
1663
|
-
resourceId: run.resourceId ?? undefined,
|
|
1664
|
-
data: {
|
|
1665
|
-
status: "paused" /* PAUSED */,
|
|
1666
|
-
currentStepId: stepId,
|
|
1667
|
-
pausedAt: new Date,
|
|
1668
|
-
timeline: import_es_toolkit2.merge(freshRun.timeline, {
|
|
1669
|
-
[`${stepId}-wait-for`]: {
|
|
1670
|
-
waitFor: { eventName, timeoutEvent },
|
|
1671
|
-
timestamp: new Date
|
|
1672
|
-
}
|
|
1673
|
-
})
|
|
1674
|
-
}
|
|
1675
|
-
}, { db });
|
|
1961
|
+
return this.pauseRunForWait({ run: freshRun, stepId, eventName, timeoutEvent, db });
|
|
1676
1962
|
}, this.pool);
|
|
1677
1963
|
if (timeoutDate && timeoutEvent) {
|
|
1678
1964
|
try {
|
|
@@ -1786,7 +2072,7 @@ ${error.stack}` : String(error)
|
|
|
1786
2072
|
pausedAt: new Date,
|
|
1787
2073
|
timeline: import_es_toolkit2.merge(freshRun.timeline, {
|
|
1788
2074
|
[`${stepId}-poll`]: { startedAt: startedAt.toISOString() },
|
|
1789
|
-
[
|
|
2075
|
+
[waitForTimelineKey(stepId)]: {
|
|
1790
2076
|
waitFor: { timeoutEvent: pollEvent, skipOutput: true },
|
|
1791
2077
|
timestamp: new Date
|
|
1792
2078
|
}
|
|
@@ -1861,5 +2147,5 @@ ${error.stack}` : String(error)
|
|
|
1861
2147
|
}
|
|
1862
2148
|
}
|
|
1863
2149
|
|
|
1864
|
-
//# debugId=
|
|
2150
|
+
//# debugId=8E8C82F1948B934364756E2164756E21
|
|
1865
2151
|
//# sourceMappingURL=index.js.map
|