pg-workflows 0.7.1 → 0.8.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 +83 -765
- package/dist/client.entry.cjs +826 -0
- package/dist/client.entry.d.cts +229 -0
- package/dist/client.entry.d.ts +229 -0
- package/dist/client.entry.js +13 -0
- package/dist/client.entry.js.map +16 -0
- package/dist/index.cjs +644 -323
- package/dist/index.d.cts +125 -11
- package/dist/index.d.ts +125 -11
- package/dist/index.js +75 -487
- package/dist/index.js.map +12 -10
- package/dist/shared/chunk-wsa44g1x.js +757 -0
- package/dist/shared/chunk-wsa44g1x.js.map +16 -0
- package/package.json +11 -1
package/dist/index.cjs
CHANGED
|
@@ -66,180 +66,25 @@ var exports_src = {};
|
|
|
66
66
|
__export(exports_src, {
|
|
67
67
|
workflow: () => workflow,
|
|
68
68
|
parseDuration: () => parseDuration,
|
|
69
|
+
createWorkflowRef: () => createWorkflowRef,
|
|
69
70
|
WorkflowStatus: () => WorkflowStatus,
|
|
70
71
|
WorkflowRunNotFoundError: () => WorkflowRunNotFoundError,
|
|
71
72
|
WorkflowEngineError: () => WorkflowEngineError,
|
|
72
73
|
WorkflowEngine: () => WorkflowEngine,
|
|
74
|
+
WorkflowClient: () => WorkflowClient,
|
|
73
75
|
StepType: () => StepType
|
|
74
76
|
});
|
|
75
77
|
module.exports = __toCommonJS(exports_src);
|
|
76
78
|
|
|
77
|
-
// src/
|
|
78
|
-
function createWorkflowFactory(plugins = []) {
|
|
79
|
-
const factory = (id, handler, { inputSchema, timeout, retries } = {}) => ({
|
|
80
|
-
id,
|
|
81
|
-
handler,
|
|
82
|
-
inputSchema,
|
|
83
|
-
timeout,
|
|
84
|
-
retries,
|
|
85
|
-
plugins: plugins.length > 0 ? plugins : undefined
|
|
86
|
-
});
|
|
87
|
-
factory.use = (plugin) => createWorkflowFactory([
|
|
88
|
-
...plugins,
|
|
89
|
-
plugin
|
|
90
|
-
]);
|
|
91
|
-
return factory;
|
|
92
|
-
}
|
|
93
|
-
var workflow = createWorkflowFactory();
|
|
94
|
-
// src/duration.ts
|
|
95
|
-
var import_parse_duration = __toESM(require("parse-duration"));
|
|
96
|
-
|
|
97
|
-
// src/error.ts
|
|
98
|
-
class WorkflowEngineError extends Error {
|
|
99
|
-
workflowId;
|
|
100
|
-
runId;
|
|
101
|
-
cause;
|
|
102
|
-
issues;
|
|
103
|
-
constructor(message, workflowId, runId, cause = undefined, issues) {
|
|
104
|
-
super(message);
|
|
105
|
-
this.workflowId = workflowId;
|
|
106
|
-
this.runId = runId;
|
|
107
|
-
this.cause = cause;
|
|
108
|
-
this.issues = issues;
|
|
109
|
-
this.name = "WorkflowEngineError";
|
|
110
|
-
if (Error.captureStackTrace) {
|
|
111
|
-
Error.captureStackTrace(this, WorkflowEngineError);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
class WorkflowRunNotFoundError extends WorkflowEngineError {
|
|
117
|
-
constructor(runId, workflowId) {
|
|
118
|
-
super("Workflow run not found", workflowId, runId);
|
|
119
|
-
this.name = "WorkflowRunNotFoundError";
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// src/duration.ts
|
|
124
|
-
var MS_PER_SECOND = 1000;
|
|
125
|
-
var MS_PER_MINUTE = 60 * MS_PER_SECOND;
|
|
126
|
-
var MS_PER_HOUR = 60 * MS_PER_MINUTE;
|
|
127
|
-
var MS_PER_DAY = 24 * MS_PER_HOUR;
|
|
128
|
-
var MS_PER_WEEK = 7 * MS_PER_DAY;
|
|
129
|
-
function parseDuration(duration) {
|
|
130
|
-
if (typeof duration === "string") {
|
|
131
|
-
if (duration.trim() === "") {
|
|
132
|
-
throw new WorkflowEngineError("Invalid duration: empty string");
|
|
133
|
-
}
|
|
134
|
-
const ms2 = import_parse_duration.default(duration);
|
|
135
|
-
if (ms2 == null || ms2 <= 0) {
|
|
136
|
-
throw new WorkflowEngineError(`Invalid duration: "${duration}"`);
|
|
137
|
-
}
|
|
138
|
-
return ms2;
|
|
139
|
-
}
|
|
140
|
-
const { weeks = 0, days = 0, hours = 0, minutes = 0, seconds = 0 } = duration;
|
|
141
|
-
const ms = weeks * MS_PER_WEEK + days * MS_PER_DAY + hours * MS_PER_HOUR + minutes * MS_PER_MINUTE + seconds * MS_PER_SECOND;
|
|
142
|
-
if (ms <= 0) {
|
|
143
|
-
throw new WorkflowEngineError("Invalid duration: must be a positive value");
|
|
144
|
-
}
|
|
145
|
-
return ms;
|
|
146
|
-
}
|
|
147
|
-
// src/engine.ts
|
|
79
|
+
// src/client.ts
|
|
148
80
|
var import_es_toolkit = require("es-toolkit");
|
|
149
81
|
var import_pg = __toESM(require("pg"));
|
|
150
82
|
var import_pg_boss = require("pg-boss");
|
|
151
83
|
|
|
152
|
-
// src/
|
|
153
|
-
var
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
var WorkflowStatus;
|
|
157
|
-
((WorkflowStatus2) => {
|
|
158
|
-
WorkflowStatus2["PENDING"] = "pending";
|
|
159
|
-
WorkflowStatus2["RUNNING"] = "running";
|
|
160
|
-
WorkflowStatus2["PAUSED"] = "paused";
|
|
161
|
-
WorkflowStatus2["COMPLETED"] = "completed";
|
|
162
|
-
WorkflowStatus2["FAILED"] = "failed";
|
|
163
|
-
WorkflowStatus2["CANCELLED"] = "cancelled";
|
|
164
|
-
})(WorkflowStatus ||= {});
|
|
165
|
-
var StepType;
|
|
166
|
-
((StepType2) => {
|
|
167
|
-
StepType2["PAUSE"] = "pause";
|
|
168
|
-
StepType2["RUN"] = "run";
|
|
169
|
-
StepType2["WAIT_FOR"] = "waitFor";
|
|
170
|
-
StepType2["WAIT_UNTIL"] = "waitUntil";
|
|
171
|
-
StepType2["DELAY"] = "delay";
|
|
172
|
-
StepType2["POLL"] = "poll";
|
|
173
|
-
})(StepType ||= {});
|
|
174
|
-
|
|
175
|
-
// src/ast-parser.ts
|
|
176
|
-
function parseWorkflowHandler(handler) {
|
|
177
|
-
const handlerSource = handler.toString();
|
|
178
|
-
const sourceFile = ts.createSourceFile("handler.ts", handlerSource, ts.ScriptTarget.Latest, true);
|
|
179
|
-
const steps = new Map;
|
|
180
|
-
function isInConditional(node) {
|
|
181
|
-
let current = node.parent;
|
|
182
|
-
while (current) {
|
|
183
|
-
if (ts.isIfStatement(current) || ts.isConditionalExpression(current) || ts.isSwitchStatement(current) || ts.isCaseClause(current)) {
|
|
184
|
-
return true;
|
|
185
|
-
}
|
|
186
|
-
current = current.parent;
|
|
187
|
-
}
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
190
|
-
function isInLoop(node) {
|
|
191
|
-
let current = node.parent;
|
|
192
|
-
while (current) {
|
|
193
|
-
if (ts.isForStatement(current) || ts.isForInStatement(current) || ts.isForOfStatement(current) || ts.isWhileStatement(current) || ts.isDoStatement(current)) {
|
|
194
|
-
return true;
|
|
195
|
-
}
|
|
196
|
-
current = current.parent;
|
|
197
|
-
}
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
function extractStepId(arg) {
|
|
201
|
-
if (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg)) {
|
|
202
|
-
return { id: arg.text, isDynamic: false };
|
|
203
|
-
}
|
|
204
|
-
if (ts.isTemplateExpression(arg)) {
|
|
205
|
-
let templateStr = arg.head.text;
|
|
206
|
-
for (const span of arg.templateSpans) {
|
|
207
|
-
templateStr += `\${...}`;
|
|
208
|
-
templateStr += span.literal.text;
|
|
209
|
-
}
|
|
210
|
-
return { id: templateStr, isDynamic: true };
|
|
211
|
-
}
|
|
212
|
-
return { id: arg.getText(sourceFile), isDynamic: true };
|
|
213
|
-
}
|
|
214
|
-
function visit(node) {
|
|
215
|
-
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
|
216
|
-
const propertyAccess = node.expression;
|
|
217
|
-
const objectName = propertyAccess.expression.getText(sourceFile);
|
|
218
|
-
const methodName = propertyAccess.name.text;
|
|
219
|
-
if (objectName === "step" && (methodName === "run" || methodName === "waitFor" || methodName === "pause" || methodName === "waitUntil" || methodName === "delay" || methodName === "sleep" || methodName === "poll")) {
|
|
220
|
-
const firstArg = node.arguments[0];
|
|
221
|
-
if (firstArg) {
|
|
222
|
-
const { id, isDynamic } = extractStepId(firstArg);
|
|
223
|
-
const stepType = methodName === "sleep" ? "delay" /* DELAY */ : methodName;
|
|
224
|
-
const stepDefinition = {
|
|
225
|
-
id,
|
|
226
|
-
type: stepType,
|
|
227
|
-
conditional: isInConditional(node),
|
|
228
|
-
loop: isInLoop(node),
|
|
229
|
-
isDynamic
|
|
230
|
-
};
|
|
231
|
-
if (steps.has(id)) {
|
|
232
|
-
throw new Error(`Duplicate step ID detected: '${id}'. Step IDs must be unique within a workflow.`);
|
|
233
|
-
}
|
|
234
|
-
steps.set(id, stepDefinition);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
ts.forEachChild(node, visit);
|
|
239
|
-
}
|
|
240
|
-
visit(sourceFile);
|
|
241
|
-
return { steps: Array.from(steps.values()) };
|
|
242
|
-
}
|
|
84
|
+
// src/constants.ts
|
|
85
|
+
var PAUSE_EVENT_NAME = "__internal_pause";
|
|
86
|
+
var WORKFLOW_RUN_QUEUE_NAME = "workflow-run";
|
|
87
|
+
var DEFAULT_PGBOSS_SCHEMA = "pgboss_v12_pgworkflow";
|
|
243
88
|
|
|
244
89
|
// src/db/migration.ts
|
|
245
90
|
var MIGRATION_LOCK_ID = 738291645;
|
|
@@ -508,123 +353,582 @@ async function updateWorkflowRun({
|
|
|
508
353
|
if (expectedStatuses && expectedStatuses.length > 0) {
|
|
509
354
|
whereClause += ` AND status = ANY($${paramIndex - 1})`;
|
|
510
355
|
}
|
|
511
|
-
const query = `
|
|
512
|
-
UPDATE workflow_runs
|
|
513
|
-
SET ${updates.join(", ")}
|
|
514
|
-
${whereClause}
|
|
515
|
-
RETURNING *
|
|
516
|
-
`;
|
|
517
|
-
const result = await db.executeSql(query, values);
|
|
518
|
-
const run = result.rows[0];
|
|
519
|
-
if (!run) {
|
|
520
|
-
return null;
|
|
356
|
+
const query = `
|
|
357
|
+
UPDATE workflow_runs
|
|
358
|
+
SET ${updates.join(", ")}
|
|
359
|
+
${whereClause}
|
|
360
|
+
RETURNING *
|
|
361
|
+
`;
|
|
362
|
+
const result = await db.executeSql(query, values);
|
|
363
|
+
const run = result.rows[0];
|
|
364
|
+
if (!run) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
return mapRowToWorkflowRun(run);
|
|
368
|
+
}
|
|
369
|
+
async function getWorkflowRuns({
|
|
370
|
+
resourceId,
|
|
371
|
+
startingAfter,
|
|
372
|
+
endingBefore,
|
|
373
|
+
limit = 20,
|
|
374
|
+
statuses,
|
|
375
|
+
workflowId
|
|
376
|
+
}, db) {
|
|
377
|
+
const conditions = [];
|
|
378
|
+
const values = [];
|
|
379
|
+
let paramIndex = 1;
|
|
380
|
+
if (resourceId) {
|
|
381
|
+
conditions.push(`resource_id = $${paramIndex}`);
|
|
382
|
+
values.push(resourceId);
|
|
383
|
+
paramIndex++;
|
|
384
|
+
}
|
|
385
|
+
if (statuses && statuses.length > 0) {
|
|
386
|
+
conditions.push(`status = ANY($${paramIndex})`);
|
|
387
|
+
values.push(statuses);
|
|
388
|
+
paramIndex++;
|
|
389
|
+
}
|
|
390
|
+
if (workflowId) {
|
|
391
|
+
conditions.push(`workflow_id = $${paramIndex}`);
|
|
392
|
+
values.push(workflowId);
|
|
393
|
+
paramIndex++;
|
|
394
|
+
}
|
|
395
|
+
const cursorIds = [startingAfter, endingBefore].filter(Boolean);
|
|
396
|
+
if (cursorIds.length > 0) {
|
|
397
|
+
const cursorResult = await db.executeSql("SELECT id, created_at FROM workflow_runs WHERE id = ANY($1)", [cursorIds]);
|
|
398
|
+
const cursorMap = new Map;
|
|
399
|
+
for (const row of cursorResult.rows) {
|
|
400
|
+
cursorMap.set(row.id, typeof row.created_at === "string" ? new Date(row.created_at) : row.created_at);
|
|
401
|
+
}
|
|
402
|
+
if (startingAfter) {
|
|
403
|
+
const cursor = cursorMap.get(startingAfter);
|
|
404
|
+
if (cursor) {
|
|
405
|
+
conditions.push(`created_at < $${paramIndex}`);
|
|
406
|
+
values.push(cursor);
|
|
407
|
+
paramIndex++;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (endingBefore) {
|
|
411
|
+
const cursor = cursorMap.get(endingBefore);
|
|
412
|
+
if (cursor) {
|
|
413
|
+
conditions.push(`created_at > $${paramIndex}`);
|
|
414
|
+
values.push(cursor);
|
|
415
|
+
paramIndex++;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
420
|
+
const actualLimit = Math.min(Math.max(limit, 1), 100) + 1;
|
|
421
|
+
const isBackward = !!endingBefore && !startingAfter;
|
|
422
|
+
const query = `
|
|
423
|
+
SELECT * FROM workflow_runs
|
|
424
|
+
${whereClause}
|
|
425
|
+
ORDER BY created_at ${isBackward ? "ASC" : "DESC"}
|
|
426
|
+
LIMIT $${paramIndex}
|
|
427
|
+
`;
|
|
428
|
+
values.push(actualLimit);
|
|
429
|
+
const result = await db.executeSql(query, values);
|
|
430
|
+
const rows = result.rows;
|
|
431
|
+
const hasExtraRow = rows.length > (limit ?? 20);
|
|
432
|
+
const rawItems = hasExtraRow ? rows.slice(0, limit) : rows;
|
|
433
|
+
if (isBackward) {
|
|
434
|
+
rawItems.reverse();
|
|
435
|
+
}
|
|
436
|
+
const items = rawItems.map((row) => mapRowToWorkflowRun(row));
|
|
437
|
+
const hasMore = isBackward ? items.length > 0 : hasExtraRow;
|
|
438
|
+
const hasPrev = isBackward ? hasExtraRow : !!startingAfter && items.length > 0;
|
|
439
|
+
const nextCursor = hasMore && items.length > 0 ? items[items.length - 1]?.id ?? null : null;
|
|
440
|
+
const prevCursor = hasPrev && items.length > 0 ? items[0]?.id ?? null : null;
|
|
441
|
+
return { items, nextCursor, prevCursor, hasMore, hasPrev };
|
|
442
|
+
}
|
|
443
|
+
async function withPostgresTransaction(db, callback, pool) {
|
|
444
|
+
let txDb;
|
|
445
|
+
let release;
|
|
446
|
+
if (pool) {
|
|
447
|
+
const client = await pool.connect();
|
|
448
|
+
txDb = {
|
|
449
|
+
executeSql: (text, values) => client.query(text, values)
|
|
450
|
+
};
|
|
451
|
+
release = () => client.release();
|
|
452
|
+
} else {
|
|
453
|
+
txDb = db;
|
|
454
|
+
}
|
|
455
|
+
try {
|
|
456
|
+
await txDb.executeSql("BEGIN", []);
|
|
457
|
+
const result = await callback(txDb);
|
|
458
|
+
await txDb.executeSql("COMMIT", []);
|
|
459
|
+
return result;
|
|
460
|
+
} catch (error) {
|
|
461
|
+
await txDb.executeSql("ROLLBACK", []);
|
|
462
|
+
throw error;
|
|
463
|
+
} finally {
|
|
464
|
+
release?.();
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/error.ts
|
|
469
|
+
class WorkflowEngineError extends Error {
|
|
470
|
+
workflowId;
|
|
471
|
+
runId;
|
|
472
|
+
cause;
|
|
473
|
+
issues;
|
|
474
|
+
constructor(message, workflowId, runId, cause = undefined, issues) {
|
|
475
|
+
super(message);
|
|
476
|
+
this.workflowId = workflowId;
|
|
477
|
+
this.runId = runId;
|
|
478
|
+
this.cause = cause;
|
|
479
|
+
this.issues = issues;
|
|
480
|
+
this.name = "WorkflowEngineError";
|
|
481
|
+
if (Error.captureStackTrace) {
|
|
482
|
+
Error.captureStackTrace(this, WorkflowEngineError);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
class WorkflowRunNotFoundError extends WorkflowEngineError {
|
|
488
|
+
constructor(runId, workflowId) {
|
|
489
|
+
super("Workflow run not found", workflowId, runId);
|
|
490
|
+
this.name = "WorkflowRunNotFoundError";
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// src/types.ts
|
|
495
|
+
var WorkflowStatus;
|
|
496
|
+
((WorkflowStatus2) => {
|
|
497
|
+
WorkflowStatus2["PENDING"] = "pending";
|
|
498
|
+
WorkflowStatus2["RUNNING"] = "running";
|
|
499
|
+
WorkflowStatus2["PAUSED"] = "paused";
|
|
500
|
+
WorkflowStatus2["COMPLETED"] = "completed";
|
|
501
|
+
WorkflowStatus2["FAILED"] = "failed";
|
|
502
|
+
WorkflowStatus2["CANCELLED"] = "cancelled";
|
|
503
|
+
})(WorkflowStatus ||= {});
|
|
504
|
+
var StepType;
|
|
505
|
+
((StepType2) => {
|
|
506
|
+
StepType2["PAUSE"] = "pause";
|
|
507
|
+
StepType2["RUN"] = "run";
|
|
508
|
+
StepType2["WAIT_FOR"] = "waitFor";
|
|
509
|
+
StepType2["WAIT_UNTIL"] = "waitUntil";
|
|
510
|
+
StepType2["DELAY"] = "delay";
|
|
511
|
+
StepType2["POLL"] = "poll";
|
|
512
|
+
})(StepType ||= {});
|
|
513
|
+
|
|
514
|
+
// src/client.ts
|
|
515
|
+
var LOG_PREFIX = "[WorkflowClient]";
|
|
516
|
+
var defaultLogger = {
|
|
517
|
+
log: (_message) => console.warn(_message),
|
|
518
|
+
error: (message, error) => console.error(message, error)
|
|
519
|
+
};
|
|
520
|
+
var defaultExpireInSeconds = process.env.WORKFLOW_RUN_EXPIRE_IN_SECONDS ? Number.parseInt(process.env.WORKFLOW_RUN_EXPIRE_IN_SECONDS, 10) : 5 * 60;
|
|
521
|
+
|
|
522
|
+
class WorkflowClient {
|
|
523
|
+
boss;
|
|
524
|
+
db;
|
|
525
|
+
pool;
|
|
526
|
+
_ownsPool = false;
|
|
527
|
+
_started = false;
|
|
528
|
+
logger;
|
|
529
|
+
constructor({ logger, ...connectionOptions }) {
|
|
530
|
+
this.logger = logger ?? defaultLogger;
|
|
531
|
+
if ("pool" in connectionOptions && connectionOptions.pool) {
|
|
532
|
+
this.pool = connectionOptions.pool;
|
|
533
|
+
} else if ("connectionString" in connectionOptions && connectionOptions.connectionString) {
|
|
534
|
+
this.pool = new import_pg.default.Pool({ connectionString: connectionOptions.connectionString });
|
|
535
|
+
this._ownsPool = true;
|
|
536
|
+
} else {
|
|
537
|
+
throw new WorkflowEngineError("Either pool or connectionString must be provided");
|
|
538
|
+
}
|
|
539
|
+
const db = {
|
|
540
|
+
executeSql: (text, values) => this.pool.query(text, values)
|
|
541
|
+
};
|
|
542
|
+
this.boss = new import_pg_boss.PgBoss({ db, schema: DEFAULT_PGBOSS_SCHEMA });
|
|
543
|
+
this.db = db;
|
|
544
|
+
}
|
|
545
|
+
async start() {
|
|
546
|
+
if (this._started) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
await this.boss.start();
|
|
550
|
+
this.db = this.boss.getDb();
|
|
551
|
+
await runMigrations(this.db);
|
|
552
|
+
await this.boss.createQueue(WORKFLOW_RUN_QUEUE_NAME);
|
|
553
|
+
this._started = true;
|
|
554
|
+
this.logger.log(`${LOG_PREFIX} Client started`);
|
|
555
|
+
}
|
|
556
|
+
async stop() {
|
|
557
|
+
await this.boss.stop();
|
|
558
|
+
if (this._ownsPool) {
|
|
559
|
+
await this.pool.end();
|
|
560
|
+
}
|
|
561
|
+
this._started = false;
|
|
562
|
+
this.logger.log(`${LOG_PREFIX} Client stopped`);
|
|
563
|
+
}
|
|
564
|
+
async startWorkflow(refOrParams, inputArg, optionsArg) {
|
|
565
|
+
await this.ensureStarted();
|
|
566
|
+
let workflowId;
|
|
567
|
+
let input;
|
|
568
|
+
let resourceId;
|
|
569
|
+
let idempotencyKey;
|
|
570
|
+
let options;
|
|
571
|
+
if (typeof refOrParams === "function" && "id" in refOrParams) {
|
|
572
|
+
const ref = refOrParams;
|
|
573
|
+
workflowId = ref.id;
|
|
574
|
+
input = inputArg;
|
|
575
|
+
options = optionsArg;
|
|
576
|
+
resourceId = optionsArg?.resourceId;
|
|
577
|
+
idempotencyKey = optionsArg?.idempotencyKey;
|
|
578
|
+
if (ref.inputSchema) {
|
|
579
|
+
const result = await ref.inputSchema["~standard"].validate(input);
|
|
580
|
+
if (result.issues) {
|
|
581
|
+
throw new WorkflowEngineError(JSON.stringify(result.issues), workflowId, undefined, undefined, result.issues);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
} else {
|
|
585
|
+
const params = refOrParams;
|
|
586
|
+
workflowId = params.workflowId;
|
|
587
|
+
input = params.input;
|
|
588
|
+
resourceId = params.resourceId;
|
|
589
|
+
idempotencyKey = params.idempotencyKey;
|
|
590
|
+
options = params.options;
|
|
591
|
+
}
|
|
592
|
+
const run = await withPostgresTransaction(this.db, async (_db) => {
|
|
593
|
+
const timeoutAt = options?.timeout ? new Date(Date.now() + options.timeout) : null;
|
|
594
|
+
const { run: insertedRun, created } = await insertWorkflowRun({
|
|
595
|
+
resourceId,
|
|
596
|
+
workflowId,
|
|
597
|
+
currentStepId: "__start__",
|
|
598
|
+
status: "running" /* RUNNING */,
|
|
599
|
+
input,
|
|
600
|
+
maxRetries: options?.retries ?? 0,
|
|
601
|
+
timeoutAt,
|
|
602
|
+
idempotencyKey
|
|
603
|
+
}, _db);
|
|
604
|
+
if (created) {
|
|
605
|
+
const job = {
|
|
606
|
+
runId: insertedRun.id,
|
|
607
|
+
resourceId,
|
|
608
|
+
workflowId,
|
|
609
|
+
input
|
|
610
|
+
};
|
|
611
|
+
await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
|
|
612
|
+
startAfter: new Date,
|
|
613
|
+
expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
return insertedRun;
|
|
617
|
+
}, this.pool);
|
|
618
|
+
this.logger.log(`${LOG_PREFIX} Started workflow run ${run.id} for ${workflowId}`);
|
|
619
|
+
return run;
|
|
620
|
+
}
|
|
621
|
+
async triggerEvent({
|
|
622
|
+
runId,
|
|
623
|
+
resourceId,
|
|
624
|
+
eventName,
|
|
625
|
+
data,
|
|
626
|
+
options
|
|
627
|
+
}) {
|
|
628
|
+
await this.ensureStarted();
|
|
629
|
+
const run = await this.getRun({ runId, resourceId });
|
|
630
|
+
const job = {
|
|
631
|
+
runId: run.id,
|
|
632
|
+
resourceId: resourceId ?? run.resourceId ?? undefined,
|
|
633
|
+
workflowId: run.workflowId,
|
|
634
|
+
input: run.input,
|
|
635
|
+
event: {
|
|
636
|
+
name: eventName,
|
|
637
|
+
data
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
|
|
641
|
+
expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds
|
|
642
|
+
});
|
|
643
|
+
this.logger.log(`${LOG_PREFIX} Event ${eventName} sent for workflow run ${runId}`);
|
|
644
|
+
return run;
|
|
645
|
+
}
|
|
646
|
+
async pauseWorkflow({
|
|
647
|
+
runId,
|
|
648
|
+
resourceId
|
|
649
|
+
}) {
|
|
650
|
+
await this.ensureStarted();
|
|
651
|
+
const run = await updateWorkflowRun({
|
|
652
|
+
runId,
|
|
653
|
+
resourceId,
|
|
654
|
+
data: {
|
|
655
|
+
status: "paused" /* PAUSED */,
|
|
656
|
+
pausedAt: new Date
|
|
657
|
+
},
|
|
658
|
+
expectedStatuses: ["running" /* RUNNING */, "pending" /* PENDING */]
|
|
659
|
+
}, this.db);
|
|
660
|
+
if (!run) {
|
|
661
|
+
throw new WorkflowRunNotFoundError(runId);
|
|
662
|
+
}
|
|
663
|
+
this.logger.log(`${LOG_PREFIX} Paused workflow run ${runId}`);
|
|
664
|
+
return run;
|
|
665
|
+
}
|
|
666
|
+
async resumeWorkflow({
|
|
667
|
+
runId,
|
|
668
|
+
resourceId,
|
|
669
|
+
options
|
|
670
|
+
}) {
|
|
671
|
+
await this.ensureStarted();
|
|
672
|
+
const current = await this.getRun({ runId, resourceId });
|
|
673
|
+
if (current.status !== "paused" /* PAUSED */) {
|
|
674
|
+
throw new WorkflowEngineError(`Cannot resume workflow run in '${current.status}' status, must be 'paused'`, current.workflowId, runId);
|
|
675
|
+
}
|
|
676
|
+
return this.triggerEvent({
|
|
677
|
+
runId,
|
|
678
|
+
resourceId,
|
|
679
|
+
eventName: PAUSE_EVENT_NAME,
|
|
680
|
+
data: {},
|
|
681
|
+
options
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
async fastForwardWorkflow({
|
|
685
|
+
runId,
|
|
686
|
+
resourceId,
|
|
687
|
+
data
|
|
688
|
+
}) {
|
|
689
|
+
await this.ensureStarted();
|
|
690
|
+
const run = await this.getRun({ runId, resourceId });
|
|
691
|
+
if (run.status !== "paused" /* PAUSED */) {
|
|
692
|
+
return run;
|
|
693
|
+
}
|
|
694
|
+
const stepId = run.currentStepId;
|
|
695
|
+
const waitForEntry = run.timeline[`${stepId}-wait-for`];
|
|
696
|
+
if (!waitForEntry || typeof waitForEntry !== "object" || !("waitFor" in waitForEntry)) {
|
|
697
|
+
return run;
|
|
698
|
+
}
|
|
699
|
+
const { eventName, timeoutEvent, skipOutput } = waitForEntry.waitFor;
|
|
700
|
+
if (eventName === PAUSE_EVENT_NAME) {
|
|
701
|
+
return this.resumeWorkflow({ runId, resourceId });
|
|
702
|
+
}
|
|
703
|
+
if (skipOutput && timeoutEvent) {
|
|
704
|
+
await withPostgresTransaction(this.db, async (db) => {
|
|
705
|
+
const freshRun = await getWorkflowRun({ runId, resourceId }, { exclusiveLock: true, db });
|
|
706
|
+
if (!freshRun)
|
|
707
|
+
throw new WorkflowRunNotFoundError(runId);
|
|
708
|
+
return updateWorkflowRun({
|
|
709
|
+
runId,
|
|
710
|
+
resourceId,
|
|
711
|
+
data: {
|
|
712
|
+
timeline: import_es_toolkit.merge(freshRun.timeline, {
|
|
713
|
+
[stepId]: {
|
|
714
|
+
output: data ?? {},
|
|
715
|
+
timestamp: new Date
|
|
716
|
+
}
|
|
717
|
+
})
|
|
718
|
+
}
|
|
719
|
+
}, db);
|
|
720
|
+
}, this.pool);
|
|
721
|
+
return this.triggerEvent({ runId, resourceId, eventName: timeoutEvent });
|
|
722
|
+
}
|
|
723
|
+
if (eventName) {
|
|
724
|
+
return this.triggerEvent({ runId, resourceId, eventName, data: data ?? {} });
|
|
725
|
+
}
|
|
726
|
+
if (timeoutEvent) {
|
|
727
|
+
return this.triggerEvent({ runId, resourceId, eventName: timeoutEvent, data: data ?? {} });
|
|
728
|
+
}
|
|
729
|
+
return run;
|
|
730
|
+
}
|
|
731
|
+
async cancelWorkflow({
|
|
732
|
+
runId,
|
|
733
|
+
resourceId
|
|
734
|
+
}) {
|
|
735
|
+
await this.ensureStarted();
|
|
736
|
+
const run = await updateWorkflowRun({
|
|
737
|
+
runId,
|
|
738
|
+
resourceId,
|
|
739
|
+
data: {
|
|
740
|
+
status: "cancelled" /* CANCELLED */
|
|
741
|
+
},
|
|
742
|
+
expectedStatuses: ["pending" /* PENDING */, "running" /* RUNNING */, "paused" /* PAUSED */]
|
|
743
|
+
}, this.db);
|
|
744
|
+
if (!run) {
|
|
745
|
+
throw new WorkflowRunNotFoundError(runId);
|
|
746
|
+
}
|
|
747
|
+
this.logger.log(`${LOG_PREFIX} Cancelled workflow run ${runId}`);
|
|
748
|
+
return run;
|
|
749
|
+
}
|
|
750
|
+
async getRun({
|
|
751
|
+
runId,
|
|
752
|
+
resourceId
|
|
753
|
+
}) {
|
|
754
|
+
await this.ensureStarted();
|
|
755
|
+
const run = await getWorkflowRun({ runId, resourceId }, { db: this.db });
|
|
756
|
+
if (!run) {
|
|
757
|
+
throw new WorkflowRunNotFoundError(runId);
|
|
758
|
+
}
|
|
759
|
+
return run;
|
|
760
|
+
}
|
|
761
|
+
async checkProgress({
|
|
762
|
+
runId,
|
|
763
|
+
resourceId
|
|
764
|
+
}) {
|
|
765
|
+
const run = await this.getRun({ runId, resourceId });
|
|
766
|
+
const completedSteps = Object.values(run.timeline).filter((entry) => typeof entry === "object" && entry !== null && ("output" in entry) && entry.output !== undefined).length;
|
|
767
|
+
const totalSteps = run.status === "completed" /* COMPLETED */ ? completedSteps : 0;
|
|
768
|
+
const completionPercentage = run.status === "completed" /* COMPLETED */ ? 100 : run.status === "failed" /* FAILED */ || run.status === "cancelled" /* CANCELLED */ ? 0 : 0;
|
|
769
|
+
return {
|
|
770
|
+
...run,
|
|
771
|
+
completedSteps,
|
|
772
|
+
completionPercentage,
|
|
773
|
+
totalSteps
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
async getRuns({
|
|
777
|
+
resourceId,
|
|
778
|
+
startingAfter,
|
|
779
|
+
endingBefore,
|
|
780
|
+
limit = 20,
|
|
781
|
+
statuses,
|
|
782
|
+
workflowId
|
|
783
|
+
}) {
|
|
784
|
+
await this.ensureStarted();
|
|
785
|
+
return getWorkflowRuns({
|
|
786
|
+
resourceId,
|
|
787
|
+
startingAfter,
|
|
788
|
+
endingBefore,
|
|
789
|
+
limit,
|
|
790
|
+
statuses,
|
|
791
|
+
workflowId
|
|
792
|
+
}, this.db);
|
|
793
|
+
}
|
|
794
|
+
async ensureStarted() {
|
|
795
|
+
if (!this._started) {
|
|
796
|
+
await this.start();
|
|
797
|
+
}
|
|
521
798
|
}
|
|
522
|
-
return mapRowToWorkflowRun(run);
|
|
523
799
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
800
|
+
// src/definition.ts
|
|
801
|
+
function createWorkflowRef(id, options) {
|
|
802
|
+
const ref = (handler, defineOptions) => ({
|
|
803
|
+
id,
|
|
804
|
+
handler,
|
|
805
|
+
inputSchema: options?.inputSchema,
|
|
806
|
+
timeout: defineOptions?.timeout,
|
|
807
|
+
retries: defineOptions?.retries
|
|
808
|
+
});
|
|
809
|
+
Object.defineProperty(ref, "id", { value: id, enumerable: true });
|
|
810
|
+
Object.defineProperty(ref, "inputSchema", { value: options?.inputSchema, enumerable: true });
|
|
811
|
+
return ref;
|
|
812
|
+
}
|
|
813
|
+
function createWorkflowFactory(plugins = []) {
|
|
814
|
+
const factory = (id, handler, { inputSchema, timeout, retries } = {}) => ({
|
|
815
|
+
id,
|
|
816
|
+
handler,
|
|
817
|
+
inputSchema,
|
|
818
|
+
timeout,
|
|
819
|
+
retries,
|
|
820
|
+
plugins: plugins.length > 0 ? plugins : undefined
|
|
821
|
+
});
|
|
822
|
+
factory.use = (plugin) => createWorkflowFactory([
|
|
823
|
+
...plugins,
|
|
824
|
+
plugin
|
|
825
|
+
]);
|
|
826
|
+
factory.ref = createWorkflowRef;
|
|
827
|
+
return factory;
|
|
828
|
+
}
|
|
829
|
+
var workflow = createWorkflowFactory();
|
|
830
|
+
// src/duration.ts
|
|
831
|
+
var import_parse_duration = __toESM(require("parse-duration"));
|
|
832
|
+
var MS_PER_SECOND = 1000;
|
|
833
|
+
var MS_PER_MINUTE = 60 * MS_PER_SECOND;
|
|
834
|
+
var MS_PER_HOUR = 60 * MS_PER_MINUTE;
|
|
835
|
+
var MS_PER_DAY = 24 * MS_PER_HOUR;
|
|
836
|
+
var MS_PER_WEEK = 7 * MS_PER_DAY;
|
|
837
|
+
function parseDuration(duration) {
|
|
838
|
+
if (typeof duration === "string") {
|
|
839
|
+
if (duration.trim() === "") {
|
|
840
|
+
throw new WorkflowEngineError("Invalid duration: empty string");
|
|
841
|
+
}
|
|
842
|
+
const ms2 = import_parse_duration.default(duration);
|
|
843
|
+
if (ms2 == null || ms2 <= 0) {
|
|
844
|
+
throw new WorkflowEngineError(`Invalid duration: "${duration}"`);
|
|
845
|
+
}
|
|
846
|
+
return ms2;
|
|
544
847
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
848
|
+
const { weeks = 0, days = 0, hours = 0, minutes = 0, seconds = 0 } = duration;
|
|
849
|
+
const ms = weeks * MS_PER_WEEK + days * MS_PER_DAY + hours * MS_PER_HOUR + minutes * MS_PER_MINUTE + seconds * MS_PER_SECOND;
|
|
850
|
+
if (ms <= 0) {
|
|
851
|
+
throw new WorkflowEngineError("Invalid duration: must be a positive value");
|
|
549
852
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
853
|
+
return ms;
|
|
854
|
+
}
|
|
855
|
+
// src/engine.ts
|
|
856
|
+
var import_es_toolkit2 = require("es-toolkit");
|
|
857
|
+
var import_pg2 = __toESM(require("pg"));
|
|
858
|
+
var import_pg_boss2 = require("pg-boss");
|
|
859
|
+
|
|
860
|
+
// src/ast-parser.ts
|
|
861
|
+
var ts = __toESM(require("typescript"));
|
|
862
|
+
function parseWorkflowHandler(handler) {
|
|
863
|
+
const handlerSource = handler.toString();
|
|
864
|
+
const sourceFile = ts.createSourceFile("handler.ts", handlerSource, ts.ScriptTarget.Latest, true);
|
|
865
|
+
const steps = new Map;
|
|
866
|
+
function isInConditional(node) {
|
|
867
|
+
let current = node.parent;
|
|
868
|
+
while (current) {
|
|
869
|
+
if (ts.isIfStatement(current) || ts.isConditionalExpression(current) || ts.isSwitchStatement(current) || ts.isCaseClause(current)) {
|
|
870
|
+
return true;
|
|
563
871
|
}
|
|
872
|
+
current = current.parent;
|
|
564
873
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
876
|
+
function isInLoop(node) {
|
|
877
|
+
let current = node.parent;
|
|
878
|
+
while (current) {
|
|
879
|
+
if (ts.isForStatement(current) || ts.isForInStatement(current) || ts.isForOfStatement(current) || ts.isWhileStatement(current) || ts.isDoStatement(current)) {
|
|
880
|
+
return true;
|
|
571
881
|
}
|
|
882
|
+
current = current.parent;
|
|
572
883
|
}
|
|
884
|
+
return false;
|
|
573
885
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const rawItems = hasExtraRow ? rows.slice(0, limit) : rows;
|
|
588
|
-
if (isBackward) {
|
|
589
|
-
rawItems.reverse();
|
|
590
|
-
}
|
|
591
|
-
const items = rawItems.map((row) => mapRowToWorkflowRun(row));
|
|
592
|
-
const hasMore = isBackward ? items.length > 0 : hasExtraRow;
|
|
593
|
-
const hasPrev = isBackward ? hasExtraRow : !!startingAfter && items.length > 0;
|
|
594
|
-
const nextCursor = hasMore && items.length > 0 ? items[items.length - 1]?.id ?? null : null;
|
|
595
|
-
const prevCursor = hasPrev && items.length > 0 ? items[0]?.id ?? null : null;
|
|
596
|
-
return { items, nextCursor, prevCursor, hasMore, hasPrev };
|
|
597
|
-
}
|
|
598
|
-
async function withPostgresTransaction(db, callback, pool) {
|
|
599
|
-
let txDb;
|
|
600
|
-
let release;
|
|
601
|
-
if (pool) {
|
|
602
|
-
const client = await pool.connect();
|
|
603
|
-
txDb = {
|
|
604
|
-
executeSql: (text, values) => client.query(text, values)
|
|
605
|
-
};
|
|
606
|
-
release = () => client.release();
|
|
607
|
-
} else {
|
|
608
|
-
txDb = db;
|
|
886
|
+
function extractStepId(arg) {
|
|
887
|
+
if (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg)) {
|
|
888
|
+
return { id: arg.text, isDynamic: false };
|
|
889
|
+
}
|
|
890
|
+
if (ts.isTemplateExpression(arg)) {
|
|
891
|
+
let templateStr = arg.head.text;
|
|
892
|
+
for (const span of arg.templateSpans) {
|
|
893
|
+
templateStr += `\${...}`;
|
|
894
|
+
templateStr += span.literal.text;
|
|
895
|
+
}
|
|
896
|
+
return { id: templateStr, isDynamic: true };
|
|
897
|
+
}
|
|
898
|
+
return { id: arg.getText(sourceFile), isDynamic: true };
|
|
609
899
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
900
|
+
function visit(node) {
|
|
901
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
|
902
|
+
const propertyAccess = node.expression;
|
|
903
|
+
const objectName = propertyAccess.expression.getText(sourceFile);
|
|
904
|
+
const methodName = propertyAccess.name.text;
|
|
905
|
+
if (objectName === "step" && (methodName === "run" || methodName === "waitFor" || methodName === "pause" || methodName === "waitUntil" || methodName === "delay" || methodName === "sleep" || methodName === "poll")) {
|
|
906
|
+
const firstArg = node.arguments[0];
|
|
907
|
+
if (firstArg) {
|
|
908
|
+
const { id, isDynamic } = extractStepId(firstArg);
|
|
909
|
+
const stepType = methodName === "sleep" ? "delay" /* DELAY */ : methodName;
|
|
910
|
+
const stepDefinition = {
|
|
911
|
+
id,
|
|
912
|
+
type: stepType,
|
|
913
|
+
conditional: isInConditional(node),
|
|
914
|
+
loop: isInLoop(node),
|
|
915
|
+
isDynamic
|
|
916
|
+
};
|
|
917
|
+
if (steps.has(id)) {
|
|
918
|
+
throw new Error(`Duplicate step ID detected: '${id}'. Step IDs must be unique within a workflow.`);
|
|
919
|
+
}
|
|
920
|
+
steps.set(id, stepDefinition);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
ts.forEachChild(node, visit);
|
|
620
925
|
}
|
|
926
|
+
visit(sourceFile);
|
|
927
|
+
return { steps: Array.from(steps.values()) };
|
|
621
928
|
}
|
|
622
929
|
|
|
623
930
|
// src/engine.ts
|
|
624
|
-
var
|
|
625
|
-
var WORKFLOW_RUN_QUEUE_NAME = "workflow-run";
|
|
626
|
-
var LOG_PREFIX = "[WorkflowEngine]";
|
|
627
|
-
var DEFAULT_PGBOSS_SCHEMA = "pgboss_v12_pgworkflow";
|
|
931
|
+
var LOG_PREFIX2 = "[WorkflowEngine]";
|
|
628
932
|
var StepTypeToIcon = {
|
|
629
933
|
["run" /* RUN */]: "λ",
|
|
630
934
|
["waitFor" /* WAIT_FOR */]: "○",
|
|
@@ -633,11 +937,11 @@ var StepTypeToIcon = {
|
|
|
633
937
|
["delay" /* DELAY */]: "⏱",
|
|
634
938
|
["poll" /* POLL */]: "↻"
|
|
635
939
|
};
|
|
636
|
-
var
|
|
940
|
+
var defaultLogger2 = {
|
|
637
941
|
log: (_message) => console.warn(_message),
|
|
638
942
|
error: (message, error) => console.error(message, error)
|
|
639
943
|
};
|
|
640
|
-
var
|
|
944
|
+
var defaultExpireInSeconds2 = process.env.WORKFLOW_RUN_EXPIRE_IN_SECONDS ? Number.parseInt(process.env.WORKFLOW_RUN_EXPIRE_IN_SECONDS, 10) : 5 * 60;
|
|
641
945
|
|
|
642
946
|
class WorkflowEngine {
|
|
643
947
|
boss;
|
|
@@ -649,11 +953,11 @@ class WorkflowEngine {
|
|
|
649
953
|
workflows = new Map;
|
|
650
954
|
logger;
|
|
651
955
|
constructor({ workflows, logger, boss, ...connectionOptions }) {
|
|
652
|
-
this.logger = this.buildLogger(logger ??
|
|
956
|
+
this.logger = this.buildLogger(logger ?? defaultLogger2);
|
|
653
957
|
if ("pool" in connectionOptions && connectionOptions.pool) {
|
|
654
958
|
this.pool = connectionOptions.pool;
|
|
655
959
|
} else if ("connectionString" in connectionOptions && connectionOptions.connectionString) {
|
|
656
|
-
this.pool = new
|
|
960
|
+
this.pool = new import_pg2.default.Pool({ connectionString: connectionOptions.connectionString });
|
|
657
961
|
this._ownsPool = true;
|
|
658
962
|
} else {
|
|
659
963
|
throw new WorkflowEngineError("Either pool or connectionString must be provided");
|
|
@@ -667,7 +971,7 @@ class WorkflowEngine {
|
|
|
667
971
|
if (boss) {
|
|
668
972
|
this.boss = boss;
|
|
669
973
|
} else {
|
|
670
|
-
this.boss = new
|
|
974
|
+
this.boss = new import_pg_boss2.PgBoss({ db, schema: DEFAULT_PGBOSS_SCHEMA });
|
|
671
975
|
}
|
|
672
976
|
this.db = this.boss.getDb();
|
|
673
977
|
}
|
|
@@ -731,13 +1035,26 @@ class WorkflowEngine {
|
|
|
731
1035
|
this.workflows.clear();
|
|
732
1036
|
return this;
|
|
733
1037
|
}
|
|
734
|
-
async startWorkflow({
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
idempotencyKey
|
|
739
|
-
options
|
|
740
|
-
|
|
1038
|
+
async startWorkflow(refOrParams, inputArg, optionsArg) {
|
|
1039
|
+
let workflowId;
|
|
1040
|
+
let input;
|
|
1041
|
+
let resourceId;
|
|
1042
|
+
let idempotencyKey;
|
|
1043
|
+
let options;
|
|
1044
|
+
if (typeof refOrParams === "function" && "id" in refOrParams) {
|
|
1045
|
+
workflowId = refOrParams.id;
|
|
1046
|
+
input = inputArg;
|
|
1047
|
+
options = optionsArg;
|
|
1048
|
+
resourceId = optionsArg?.resourceId;
|
|
1049
|
+
idempotencyKey = optionsArg?.idempotencyKey;
|
|
1050
|
+
} else {
|
|
1051
|
+
const params = refOrParams;
|
|
1052
|
+
workflowId = params.workflowId;
|
|
1053
|
+
input = params.input;
|
|
1054
|
+
resourceId = params.resourceId;
|
|
1055
|
+
idempotencyKey = params.idempotencyKey;
|
|
1056
|
+
options = params.options;
|
|
1057
|
+
}
|
|
741
1058
|
if (!this._started) {
|
|
742
1059
|
await this.start(false, { batchSize: options?.batchSize ?? 1 });
|
|
743
1060
|
}
|
|
@@ -778,7 +1095,7 @@ class WorkflowEngine {
|
|
|
778
1095
|
};
|
|
779
1096
|
await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
|
|
780
1097
|
startAfter: new Date,
|
|
781
|
-
expireInSeconds: options?.expireInSeconds ??
|
|
1098
|
+
expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds2
|
|
782
1099
|
});
|
|
783
1100
|
}
|
|
784
1101
|
return insertedRun;
|
|
@@ -853,7 +1170,7 @@ class WorkflowEngine {
|
|
|
853
1170
|
runId,
|
|
854
1171
|
resourceId,
|
|
855
1172
|
data: {
|
|
856
|
-
timeline:
|
|
1173
|
+
timeline: import_es_toolkit2.merge(freshRun.timeline, {
|
|
857
1174
|
[stepId]: {
|
|
858
1175
|
output: data ?? {},
|
|
859
1176
|
timestamp: new Date
|
|
@@ -909,7 +1226,7 @@ class WorkflowEngine {
|
|
|
909
1226
|
}
|
|
910
1227
|
};
|
|
911
1228
|
await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
|
|
912
|
-
expireInSeconds: options?.expireInSeconds ??
|
|
1229
|
+
expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds2
|
|
913
1230
|
});
|
|
914
1231
|
this.logger.log(`event ${eventName} sent for workflow run with id ${runId}`);
|
|
915
1232
|
return run;
|
|
@@ -988,27 +1305,29 @@ class WorkflowEngine {
|
|
|
988
1305
|
return run.resourceId ?? undefined;
|
|
989
1306
|
}
|
|
990
1307
|
async handleWorkflowRun([job]) {
|
|
991
|
-
const { runId, resourceId, workflowId, input, event } = job?.data ?? {};
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
}
|
|
995
|
-
if (!workflowId) {
|
|
996
|
-
throw new WorkflowEngineError("Invalid workflow run job, missing workflowId", undefined, runId);
|
|
997
|
-
}
|
|
998
|
-
const workflow2 = this.workflows.get(workflowId);
|
|
999
|
-
if (!workflow2) {
|
|
1000
|
-
throw new WorkflowEngineError(`Workflow ${workflowId} not found`, workflowId, runId);
|
|
1001
|
-
}
|
|
1002
|
-
this.logger.log("Processing workflow run...", {
|
|
1003
|
-
runId,
|
|
1004
|
-
workflowId
|
|
1005
|
-
});
|
|
1006
|
-
let run = await this.getRun({ runId });
|
|
1007
|
-
if (run.workflowId !== workflowId) {
|
|
1008
|
-
throw new WorkflowEngineError(`Workflow run ${runId} does not match job workflowId ${workflowId}`, workflowId, runId);
|
|
1009
|
-
}
|
|
1010
|
-
const scopedResourceId = this.resolveScopedResourceId(resourceId, run);
|
|
1308
|
+
const { runId = "", resourceId, workflowId = "", input, event } = job?.data ?? {};
|
|
1309
|
+
let run;
|
|
1310
|
+
let scopedResourceId;
|
|
1011
1311
|
try {
|
|
1312
|
+
if (!runId) {
|
|
1313
|
+
throw new WorkflowEngineError("Invalid workflow run job, missing runId", workflowId);
|
|
1314
|
+
}
|
|
1315
|
+
if (!workflowId) {
|
|
1316
|
+
throw new WorkflowEngineError("Invalid workflow run job, missing workflowId", undefined, runId);
|
|
1317
|
+
}
|
|
1318
|
+
const workflow2 = this.workflows.get(workflowId);
|
|
1319
|
+
if (!workflow2) {
|
|
1320
|
+
throw new WorkflowEngineError(`Workflow ${workflowId} not found`, workflowId, runId);
|
|
1321
|
+
}
|
|
1322
|
+
this.logger.log("Processing workflow run...", {
|
|
1323
|
+
runId,
|
|
1324
|
+
workflowId
|
|
1325
|
+
});
|
|
1326
|
+
run = await this.getRun({ runId });
|
|
1327
|
+
if (run.workflowId !== workflowId) {
|
|
1328
|
+
throw new WorkflowEngineError(`Workflow run ${runId} does not match job workflowId ${workflowId}`, workflowId, runId);
|
|
1329
|
+
}
|
|
1330
|
+
scopedResourceId = this.resolveScopedResourceId(resourceId, run);
|
|
1012
1331
|
if (run.status === "cancelled" /* CANCELLED */) {
|
|
1013
1332
|
this.logger.log(`Workflow run ${runId} is cancelled, skipping`);
|
|
1014
1333
|
return;
|
|
@@ -1039,7 +1358,7 @@ class WorkflowEngine {
|
|
|
1039
1358
|
resumedAt: new Date,
|
|
1040
1359
|
jobId: job?.id,
|
|
1041
1360
|
...skipOutput ? {} : {
|
|
1042
|
-
timeline:
|
|
1361
|
+
timeline: import_es_toolkit2.merge(lockedRun.timeline, {
|
|
1043
1362
|
[lockedRun.currentStepId]: {
|
|
1044
1363
|
output: event?.data ?? {},
|
|
1045
1364
|
...isTimeout ? { timedOut: true } : {},
|
|
@@ -1152,7 +1471,7 @@ class WorkflowEngine {
|
|
|
1152
1471
|
});
|
|
1153
1472
|
}
|
|
1154
1473
|
} catch (error) {
|
|
1155
|
-
if (run.retryCount < run.maxRetries) {
|
|
1474
|
+
if (run && run.retryCount < run.maxRetries) {
|
|
1156
1475
|
await this.updateRun({
|
|
1157
1476
|
runId,
|
|
1158
1477
|
resourceId: scopedResourceId,
|
|
@@ -1170,19 +1489,21 @@ class WorkflowEngine {
|
|
|
1170
1489
|
};
|
|
1171
1490
|
await this.boss?.send("workflow-run", pgBossJob, {
|
|
1172
1491
|
startAfter: new Date(Date.now() + retryDelay),
|
|
1173
|
-
expireInSeconds:
|
|
1492
|
+
expireInSeconds: defaultExpireInSeconds2
|
|
1174
1493
|
});
|
|
1175
1494
|
return;
|
|
1176
1495
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1496
|
+
if (runId) {
|
|
1497
|
+
await this.updateRun({
|
|
1498
|
+
runId,
|
|
1499
|
+
resourceId: scopedResourceId,
|
|
1500
|
+
data: {
|
|
1501
|
+
status: "failed" /* FAILED */,
|
|
1502
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1503
|
+
jobId: job?.id
|
|
1504
|
+
}
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1186
1507
|
throw error;
|
|
1187
1508
|
}
|
|
1188
1509
|
}
|
|
@@ -1235,7 +1556,7 @@ class WorkflowEngine {
|
|
|
1235
1556
|
runId: run.id,
|
|
1236
1557
|
resourceId: run.resourceId ?? undefined,
|
|
1237
1558
|
data: {
|
|
1238
|
-
timeline:
|
|
1559
|
+
timeline: import_es_toolkit2.merge(persistedRun.timeline, {
|
|
1239
1560
|
[stepId]: {
|
|
1240
1561
|
output,
|
|
1241
1562
|
timestamp: new Date
|
|
@@ -1289,7 +1610,7 @@ ${error.stack}` : String(error)
|
|
|
1289
1610
|
status: "paused" /* PAUSED */,
|
|
1290
1611
|
currentStepId: stepId,
|
|
1291
1612
|
pausedAt: new Date,
|
|
1292
|
-
timeline:
|
|
1613
|
+
timeline: import_es_toolkit2.merge(freshRun.timeline, {
|
|
1293
1614
|
[`${stepId}-wait-for`]: {
|
|
1294
1615
|
waitFor: { eventName, timeoutEvent },
|
|
1295
1616
|
timestamp: new Date
|
|
@@ -1309,7 +1630,7 @@ ${error.stack}` : String(error)
|
|
|
1309
1630
|
};
|
|
1310
1631
|
await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
|
|
1311
1632
|
startAfter: timeoutDate.getTime() <= Date.now() ? new Date : timeoutDate,
|
|
1312
|
-
expireInSeconds:
|
|
1633
|
+
expireInSeconds: defaultExpireInSeconds2
|
|
1313
1634
|
});
|
|
1314
1635
|
} catch (error) {
|
|
1315
1636
|
await this.updateRun({
|
|
@@ -1350,7 +1671,7 @@ ${error.stack}` : String(error)
|
|
|
1350
1671
|
resourceId: run.resourceId ?? undefined,
|
|
1351
1672
|
data: {
|
|
1352
1673
|
currentStepId: stepId,
|
|
1353
|
-
timeline:
|
|
1674
|
+
timeline: import_es_toolkit2.merge(freshRun.timeline, {
|
|
1354
1675
|
[stepId]: { output: {}, timedOut: true, timestamp: new Date }
|
|
1355
1676
|
})
|
|
1356
1677
|
}
|
|
@@ -1371,7 +1692,7 @@ ${error.stack}` : String(error)
|
|
|
1371
1692
|
resourceId: run.resourceId ?? undefined,
|
|
1372
1693
|
data: {
|
|
1373
1694
|
currentStepId: stepId,
|
|
1374
|
-
timeline:
|
|
1695
|
+
timeline: import_es_toolkit2.merge(freshRun.timeline, {
|
|
1375
1696
|
[stepId]: { output: {}, timedOut: true, timestamp: new Date }
|
|
1376
1697
|
})
|
|
1377
1698
|
}
|
|
@@ -1389,7 +1710,7 @@ ${error.stack}` : String(error)
|
|
|
1389
1710
|
resourceId: run.resourceId ?? undefined,
|
|
1390
1711
|
data: {
|
|
1391
1712
|
currentStepId: stepId,
|
|
1392
|
-
timeline:
|
|
1713
|
+
timeline: import_es_toolkit2.merge(freshRun.timeline, {
|
|
1393
1714
|
[stepId]: { output: result, timestamp: new Date }
|
|
1394
1715
|
})
|
|
1395
1716
|
}
|
|
@@ -1407,7 +1728,7 @@ ${error.stack}` : String(error)
|
|
|
1407
1728
|
status: "paused" /* PAUSED */,
|
|
1408
1729
|
currentStepId: stepId,
|
|
1409
1730
|
pausedAt: new Date,
|
|
1410
|
-
timeline:
|
|
1731
|
+
timeline: import_es_toolkit2.merge(freshRun.timeline, {
|
|
1411
1732
|
[`${stepId}-poll`]: { startedAt: startedAt.toISOString() },
|
|
1412
1733
|
[`${stepId}-wait-for`]: {
|
|
1413
1734
|
waitFor: { timeoutEvent: pollEvent, skipOutput: true },
|
|
@@ -1426,7 +1747,7 @@ ${error.stack}` : String(error)
|
|
|
1426
1747
|
event: { name: pollEvent, data: {} }
|
|
1427
1748
|
}, {
|
|
1428
1749
|
startAfter: new Date(Date.now() + intervalMs),
|
|
1429
|
-
expireInSeconds:
|
|
1750
|
+
expireInSeconds: defaultExpireInSeconds2
|
|
1430
1751
|
});
|
|
1431
1752
|
} catch (error) {
|
|
1432
1753
|
await this.updateRun({
|
|
@@ -1451,12 +1772,12 @@ ${error.stack}` : String(error)
|
|
|
1451
1772
|
return {
|
|
1452
1773
|
log: (message, context) => {
|
|
1453
1774
|
const { runId, workflowId } = context ?? {};
|
|
1454
|
-
const parts = [
|
|
1775
|
+
const parts = [LOG_PREFIX2, workflowId, runId].filter(Boolean).join(" ");
|
|
1455
1776
|
logger.log(`${parts}: ${message}`);
|
|
1456
1777
|
},
|
|
1457
1778
|
error: (message, error, context) => {
|
|
1458
1779
|
const { runId, workflowId } = context ?? {};
|
|
1459
|
-
const parts = [
|
|
1780
|
+
const parts = [LOG_PREFIX2, workflowId, runId].filter(Boolean).join(" ");
|
|
1460
1781
|
logger.error(`${parts}: ${message}`, error);
|
|
1461
1782
|
}
|
|
1462
1783
|
};
|
|
@@ -1480,5 +1801,5 @@ ${error.stack}` : String(error)
|
|
|
1480
1801
|
}
|
|
1481
1802
|
}
|
|
1482
1803
|
|
|
1483
|
-
//# debugId=
|
|
1804
|
+
//# debugId=8707EFFDC2423EEE64756E2164756E21
|
|
1484
1805
|
//# sourceMappingURL=index.js.map
|