pg-workflows 0.14.0 → 0.16.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 +227 -93
- package/dist/client.entry.d.cts +31 -1
- package/dist/client.entry.d.ts +31 -1
- package/dist/client.entry.js +4 -2
- package/dist/client.entry.js.map +10 -10
- package/dist/index.cjs +249 -269
- package/dist/index.d.cts +44 -11
- package/dist/index.d.ts +44 -11
- package/dist/index.js +29 -182
- package/dist/index.js.map +12 -13
- package/dist/shared/{chunk-w3qcrrwx.js → chunk-d204nxd5.js} +167 -36
- package/dist/shared/chunk-d204nxd5.js.map +17 -0
- package/package.json +14 -25
- package/LICENSE +0 -22
- package/README.md +0 -214
- package/dist/shared/chunk-w3qcrrwx.js.map +0 -17
package/dist/index.cjs
CHANGED
|
@@ -65,10 +65,11 @@ var __export = (target, all) => {
|
|
|
65
65
|
var exports_src = {};
|
|
66
66
|
__export(exports_src, {
|
|
67
67
|
workflow: () => workflow,
|
|
68
|
-
|
|
68
|
+
isStepCached: () => isStepCached,
|
|
69
69
|
createWorkflowRef: () => createWorkflowRef,
|
|
70
70
|
WorkflowStatus: () => WorkflowStatus,
|
|
71
71
|
WorkflowRunNotFoundError: () => WorkflowRunNotFoundError,
|
|
72
|
+
WorkflowRunInProgressError: () => WorkflowRunInProgressError,
|
|
72
73
|
WorkflowEngineError: () => WorkflowEngineError,
|
|
73
74
|
WorkflowEngine: () => WorkflowEngine,
|
|
74
75
|
WorkflowClient: () => WorkflowClient
|
|
@@ -94,10 +95,11 @@ var WAIT_FOR_TIMELINE_SUFFIX = "wait-for";
|
|
|
94
95
|
var invokeChildWorkflowTimelineKey = (stepId) => `${stepId}-${INVOKE_CHILD_WORKFLOW_TIMELINE_SUFFIX}`;
|
|
95
96
|
var waitForTimelineKey = (stepId) => `${stepId}-${WAIT_FOR_TIMELINE_SUFFIX}`;
|
|
96
97
|
var isInvokeChildWorkflowTimelineEntry = (entry) => !!entry && typeof entry === "object" && ("invokeChildWorkflow" in entry);
|
|
98
|
+
var isStepCached = (timeline, stepId) => timeline[stepId]?.output !== undefined || isInvokeChildWorkflowTimelineEntry(timeline[invokeChildWorkflowTimelineKey(stepId)]);
|
|
97
99
|
|
|
98
100
|
// src/db/migration.ts
|
|
99
101
|
var MIGRATION_LOCK_ID = 738291645;
|
|
100
|
-
var CURRENT_SCHEMA_VERSION =
|
|
102
|
+
var CURRENT_SCHEMA_VERSION = 7;
|
|
101
103
|
async function runMigrations(db) {
|
|
102
104
|
if (await isSchemaUpToDate(db)) {
|
|
103
105
|
return;
|
|
@@ -176,6 +178,14 @@ async function runMigrations(db) {
|
|
|
176
178
|
if (currentVersion < 6) {
|
|
177
179
|
commands.push("ALTER TABLE workflow_runs ADD COLUMN IF NOT EXISTS priority integer DEFAULT 0 NOT NULL");
|
|
178
180
|
}
|
|
181
|
+
if (currentVersion < 7) {
|
|
182
|
+
commands.push("ALTER TABLE workflow_runs ADD COLUMN IF NOT EXISTS singleton boolean DEFAULT false NOT NULL");
|
|
183
|
+
commands.push(`
|
|
184
|
+
CREATE UNIQUE INDEX IF NOT EXISTS workflow_runs_singleton_active_idx
|
|
185
|
+
ON workflow_runs (workflow_id)
|
|
186
|
+
WHERE singleton AND status IN ('pending', 'running')
|
|
187
|
+
`);
|
|
188
|
+
}
|
|
179
189
|
if (currentVersion === 0) {
|
|
180
190
|
commands.push(`INSERT INTO workflow_schema_version (version) VALUES (${CURRENT_SCHEMA_VERSION})`);
|
|
181
191
|
} else {
|
|
@@ -215,9 +225,95 @@ async function getCurrentVersion(db) {
|
|
|
215
225
|
|
|
216
226
|
// src/db/queries.ts
|
|
217
227
|
var import_ksuid = __toESM(require("ksuid"));
|
|
228
|
+
|
|
229
|
+
// src/error.ts
|
|
230
|
+
function validateWorkflowId(workflowId) {
|
|
231
|
+
if (workflowId.length > MAX_WORKFLOW_ID_LENGTH) {
|
|
232
|
+
throw new WorkflowEngineError(`workflowId exceeds maximum length of ${MAX_WORKFLOW_ID_LENGTH} characters (got ${workflowId.length})`, workflowId);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function validateResourceId(resourceId) {
|
|
236
|
+
if (resourceId != null && resourceId.length > MAX_RESOURCE_ID_LENGTH) {
|
|
237
|
+
throw new WorkflowEngineError(`resourceId exceeds maximum length of ${MAX_RESOURCE_ID_LENGTH} characters (got ${resourceId.length})`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
class WorkflowEngineError extends Error {
|
|
242
|
+
workflowId;
|
|
243
|
+
runId;
|
|
244
|
+
cause;
|
|
245
|
+
issues;
|
|
246
|
+
constructor(message, workflowId, runId, cause = undefined, issues) {
|
|
247
|
+
super(message);
|
|
248
|
+
this.workflowId = workflowId;
|
|
249
|
+
this.runId = runId;
|
|
250
|
+
this.cause = cause;
|
|
251
|
+
this.issues = issues;
|
|
252
|
+
this.name = "WorkflowEngineError";
|
|
253
|
+
if (Error.captureStackTrace) {
|
|
254
|
+
Error.captureStackTrace(this, WorkflowEngineError);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
class WorkflowRunNotFoundError extends WorkflowEngineError {
|
|
260
|
+
constructor(runId, workflowId) {
|
|
261
|
+
super("Workflow run not found", workflowId, runId);
|
|
262
|
+
this.name = "WorkflowRunNotFoundError";
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
class WorkflowRunInProgressError extends WorkflowEngineError {
|
|
267
|
+
constructor(workflowId, runId) {
|
|
268
|
+
super(`Workflow "${workflowId}" already has a running run; wait for it to pause, complete, fail, or be cancelled`, workflowId, runId);
|
|
269
|
+
this.name = "WorkflowRunInProgressError";
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/types.ts
|
|
274
|
+
var WorkflowStatus;
|
|
275
|
+
((WorkflowStatus2) => {
|
|
276
|
+
WorkflowStatus2["PENDING"] = "pending";
|
|
277
|
+
WorkflowStatus2["RUNNING"] = "running";
|
|
278
|
+
WorkflowStatus2["PAUSED"] = "paused";
|
|
279
|
+
WorkflowStatus2["COMPLETED"] = "completed";
|
|
280
|
+
WorkflowStatus2["FAILED"] = "failed";
|
|
281
|
+
WorkflowStatus2["CANCELLED"] = "cancelled";
|
|
282
|
+
})(WorkflowStatus ||= {});
|
|
283
|
+
var _STEP_BASE_METHOD_TO_TYPE = {
|
|
284
|
+
run: "run" /* RUN */,
|
|
285
|
+
waitFor: "waitFor" /* WAIT_FOR */,
|
|
286
|
+
waitUntil: "waitUntil" /* WAIT_UNTIL */,
|
|
287
|
+
delay: "delay" /* DELAY */,
|
|
288
|
+
sleep: "delay" /* DELAY */,
|
|
289
|
+
pause: "pause" /* PAUSE */,
|
|
290
|
+
poll: "poll" /* POLL */,
|
|
291
|
+
invokeChildWorkflow: "invokeChildWorkflow" /* INVOKE_CHILD_WORKFLOW */
|
|
292
|
+
};
|
|
293
|
+
var STEP_BASE_METHOD_TYPES = new Map(Object.entries(_STEP_BASE_METHOD_TO_TYPE));
|
|
294
|
+
|
|
295
|
+
// src/db/queries.ts
|
|
296
|
+
var EMPTY_RUN_STATS = {
|
|
297
|
+
["pending" /* PENDING */]: 0,
|
|
298
|
+
["running" /* RUNNING */]: 0,
|
|
299
|
+
["paused" /* PAUSED */]: 0,
|
|
300
|
+
["completed" /* COMPLETED */]: 0,
|
|
301
|
+
["failed" /* FAILED */]: 0,
|
|
302
|
+
["cancelled" /* CANCELLED */]: 0
|
|
303
|
+
};
|
|
218
304
|
function generateKSUID(prefix) {
|
|
219
305
|
return `${prefix ? `${prefix}_` : ""}${import_ksuid.default.randomSync().string}`;
|
|
220
306
|
}
|
|
307
|
+
function isUniqueViolation(error) {
|
|
308
|
+
if (!error || typeof error !== "object") {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
if ("code" in error && error.code === "23505") {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
const cause = "cause" in error ? error.cause : undefined;
|
|
315
|
+
return Boolean(cause && typeof cause === "object" && "code" in cause && cause.code === "23505");
|
|
316
|
+
}
|
|
221
317
|
function mapRowToWorkflowRun(row) {
|
|
222
318
|
return {
|
|
223
319
|
id: row.id,
|
|
@@ -243,7 +339,8 @@ function mapRowToWorkflowRun(row) {
|
|
|
243
339
|
parentRunId: row.parent_run_id,
|
|
244
340
|
parentStepId: row.parent_step_id,
|
|
245
341
|
parentResourceId: row.parent_resource_id,
|
|
246
|
-
scheduledAt: row.scheduled_at ? new Date(row.scheduled_at) : null
|
|
342
|
+
scheduledAt: row.scheduled_at ? new Date(row.scheduled_at) : null,
|
|
343
|
+
singleton: Boolean(row.singleton)
|
|
247
344
|
};
|
|
248
345
|
}
|
|
249
346
|
async function insertWorkflowRun({
|
|
@@ -259,11 +356,20 @@ async function insertWorkflowRun({
|
|
|
259
356
|
parentRunId,
|
|
260
357
|
parentStepId,
|
|
261
358
|
parentResourceId,
|
|
262
|
-
scheduledAt
|
|
359
|
+
scheduledAt,
|
|
360
|
+
singleton = false
|
|
263
361
|
}, db) {
|
|
264
362
|
const runId = generateKSUID("run");
|
|
265
363
|
const now = new Date;
|
|
266
|
-
|
|
364
|
+
if (singleton) {
|
|
365
|
+
const active = await getActiveSingletonRun(workflowId, db);
|
|
366
|
+
if (active) {
|
|
367
|
+
throw new WorkflowRunInProgressError(workflowId, active.id);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
let result;
|
|
371
|
+
try {
|
|
372
|
+
result = await db.executeSql(`INSERT INTO workflow_runs (
|
|
267
373
|
id,
|
|
268
374
|
resource_id,
|
|
269
375
|
workflow_id,
|
|
@@ -281,30 +387,39 @@ async function insertWorkflowRun({
|
|
|
281
387
|
parent_run_id,
|
|
282
388
|
parent_step_id,
|
|
283
389
|
parent_resource_id,
|
|
284
|
-
scheduled_at
|
|
390
|
+
scheduled_at,
|
|
391
|
+
singleton
|
|
285
392
|
)
|
|
286
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
|
|
393
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
|
|
287
394
|
ON CONFLICT (idempotency_key) WHERE idempotency_key IS NOT NULL DO NOTHING
|
|
288
395
|
RETURNING *`, [
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
396
|
+
runId,
|
|
397
|
+
resourceId ?? null,
|
|
398
|
+
workflowId,
|
|
399
|
+
currentStepId,
|
|
400
|
+
status,
|
|
401
|
+
JSON.stringify(input),
|
|
402
|
+
maxRetries,
|
|
403
|
+
priority,
|
|
404
|
+
timeoutAt,
|
|
405
|
+
now,
|
|
406
|
+
now,
|
|
407
|
+
"{}",
|
|
408
|
+
0,
|
|
409
|
+
idempotencyKey ?? null,
|
|
410
|
+
parentRunId ?? null,
|
|
411
|
+
parentStepId ?? null,
|
|
412
|
+
parentResourceId ?? null,
|
|
413
|
+
scheduledAt ?? null,
|
|
414
|
+
singleton
|
|
415
|
+
]);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
if (singleton && isUniqueViolation(error)) {
|
|
418
|
+
const active = await getActiveSingletonRun(workflowId, db);
|
|
419
|
+
throw new WorkflowRunInProgressError(workflowId, active?.id);
|
|
420
|
+
}
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
308
423
|
if (result.rows[0]) {
|
|
309
424
|
return { run: mapRowToWorkflowRun(result.rows[0]), created: true };
|
|
310
425
|
}
|
|
@@ -316,6 +431,20 @@ async function insertWorkflowRun({
|
|
|
316
431
|
}
|
|
317
432
|
return { run: mapRowToWorkflowRun(existing.rows[0]), created: false };
|
|
318
433
|
}
|
|
434
|
+
async function getActiveSingletonRun(workflowId, db) {
|
|
435
|
+
const result = await db.executeSql(`SELECT * FROM workflow_runs
|
|
436
|
+
WHERE workflow_id = $1
|
|
437
|
+
AND singleton
|
|
438
|
+
AND status IN ('pending', 'running')
|
|
439
|
+
LIMIT 1`, [workflowId]);
|
|
440
|
+
return result.rows[0] ? mapRowToWorkflowRun(result.rows[0]) : null;
|
|
441
|
+
}
|
|
442
|
+
async function assertSingletonSlotAvailable({ workflowId, exceptRunId }, db) {
|
|
443
|
+
const active = await getActiveSingletonRun(workflowId, db);
|
|
444
|
+
if (active && active.id !== exceptRunId) {
|
|
445
|
+
throw new WorkflowRunInProgressError(workflowId, active.id);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
319
448
|
async function getWorkflowRun({
|
|
320
449
|
runId,
|
|
321
450
|
resourceId
|
|
@@ -430,7 +559,15 @@ async function updateWorkflowRun({
|
|
|
430
559
|
${whereClause}
|
|
431
560
|
RETURNING *
|
|
432
561
|
`;
|
|
433
|
-
|
|
562
|
+
let result;
|
|
563
|
+
try {
|
|
564
|
+
result = await db.executeSql(query, values);
|
|
565
|
+
} catch (error) {
|
|
566
|
+
if (data.status === "running" && isUniqueViolation(error)) {
|
|
567
|
+
throw new WorkflowRunInProgressError("unknown", runId);
|
|
568
|
+
}
|
|
569
|
+
throw error;
|
|
570
|
+
}
|
|
434
571
|
const run = result.rows[0];
|
|
435
572
|
if (!run) {
|
|
436
573
|
return null;
|
|
@@ -511,6 +648,32 @@ async function getWorkflowRuns({
|
|
|
511
648
|
const prevCursor = hasPrev && items.length > 0 ? items[0]?.id ?? null : null;
|
|
512
649
|
return { items, nextCursor, prevCursor, hasMore, hasPrev };
|
|
513
650
|
}
|
|
651
|
+
async function getWorkflowRunStats({
|
|
652
|
+
resourceId,
|
|
653
|
+
workflowId
|
|
654
|
+
}, db) {
|
|
655
|
+
const conditions = [];
|
|
656
|
+
const values = [];
|
|
657
|
+
let paramIndex = 1;
|
|
658
|
+
if (resourceId) {
|
|
659
|
+
conditions.push(`resource_id = $${paramIndex}`);
|
|
660
|
+
values.push(resourceId);
|
|
661
|
+
paramIndex++;
|
|
662
|
+
}
|
|
663
|
+
if (workflowId) {
|
|
664
|
+
conditions.push(`workflow_id = $${paramIndex}`);
|
|
665
|
+
values.push(workflowId);
|
|
666
|
+
}
|
|
667
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
668
|
+
const result = await db.executeSql(`SELECT status, COUNT(*)::int AS count FROM workflow_runs ${whereClause} GROUP BY status`, values);
|
|
669
|
+
const stats = { ...EMPTY_RUN_STATS };
|
|
670
|
+
for (const row of result.rows) {
|
|
671
|
+
if (row.status in stats) {
|
|
672
|
+
stats[row.status] = Number(row.count);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return stats;
|
|
676
|
+
}
|
|
514
677
|
async function withPostgresTransaction(db, callback, pool) {
|
|
515
678
|
let txDb;
|
|
516
679
|
let release;
|
|
@@ -536,43 +699,6 @@ async function withPostgresTransaction(db, callback, pool) {
|
|
|
536
699
|
}
|
|
537
700
|
}
|
|
538
701
|
|
|
539
|
-
// src/error.ts
|
|
540
|
-
function validateWorkflowId(workflowId) {
|
|
541
|
-
if (workflowId.length > MAX_WORKFLOW_ID_LENGTH) {
|
|
542
|
-
throw new WorkflowEngineError(`workflowId exceeds maximum length of ${MAX_WORKFLOW_ID_LENGTH} characters (got ${workflowId.length})`, workflowId);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
function validateResourceId(resourceId) {
|
|
546
|
-
if (resourceId != null && resourceId.length > MAX_RESOURCE_ID_LENGTH) {
|
|
547
|
-
throw new WorkflowEngineError(`resourceId exceeds maximum length of ${MAX_RESOURCE_ID_LENGTH} characters (got ${resourceId.length})`);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
class WorkflowEngineError extends Error {
|
|
552
|
-
workflowId;
|
|
553
|
-
runId;
|
|
554
|
-
cause;
|
|
555
|
-
issues;
|
|
556
|
-
constructor(message, workflowId, runId, cause = undefined, issues) {
|
|
557
|
-
super(message);
|
|
558
|
-
this.workflowId = workflowId;
|
|
559
|
-
this.runId = runId;
|
|
560
|
-
this.cause = cause;
|
|
561
|
-
this.issues = issues;
|
|
562
|
-
this.name = "WorkflowEngineError";
|
|
563
|
-
if (Error.captureStackTrace) {
|
|
564
|
-
Error.captureStackTrace(this, WorkflowEngineError);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
class WorkflowRunNotFoundError extends WorkflowEngineError {
|
|
570
|
-
constructor(runId, workflowId) {
|
|
571
|
-
super("Workflow run not found", workflowId, runId);
|
|
572
|
-
this.name = "WorkflowRunNotFoundError";
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
702
|
// src/priority.ts
|
|
577
703
|
var PRIORITY_LEVELS = { high: 100, normal: 0, low: -100 };
|
|
578
704
|
function priorityToInt(value) {
|
|
@@ -597,28 +723,6 @@ function resolvePriority(...candidates) {
|
|
|
597
723
|
return PRIORITY_LEVELS.normal;
|
|
598
724
|
}
|
|
599
725
|
|
|
600
|
-
// src/types.ts
|
|
601
|
-
var WorkflowStatus;
|
|
602
|
-
((WorkflowStatus2) => {
|
|
603
|
-
WorkflowStatus2["PENDING"] = "pending";
|
|
604
|
-
WorkflowStatus2["RUNNING"] = "running";
|
|
605
|
-
WorkflowStatus2["PAUSED"] = "paused";
|
|
606
|
-
WorkflowStatus2["COMPLETED"] = "completed";
|
|
607
|
-
WorkflowStatus2["FAILED"] = "failed";
|
|
608
|
-
WorkflowStatus2["CANCELLED"] = "cancelled";
|
|
609
|
-
})(WorkflowStatus ||= {});
|
|
610
|
-
var _STEP_BASE_METHOD_TO_TYPE = {
|
|
611
|
-
run: "run" /* RUN */,
|
|
612
|
-
waitFor: "waitFor" /* WAIT_FOR */,
|
|
613
|
-
waitUntil: "waitUntil" /* WAIT_UNTIL */,
|
|
614
|
-
delay: "delay" /* DELAY */,
|
|
615
|
-
sleep: "delay" /* DELAY */,
|
|
616
|
-
pause: "pause" /* PAUSE */,
|
|
617
|
-
poll: "poll" /* POLL */,
|
|
618
|
-
invokeChildWorkflow: "invokeChildWorkflow" /* INVOKE_CHILD_WORKFLOW */
|
|
619
|
-
};
|
|
620
|
-
var STEP_BASE_METHOD_TYPES = new Map(Object.entries(_STEP_BASE_METHOD_TO_TYPE));
|
|
621
|
-
|
|
622
726
|
// src/client.ts
|
|
623
727
|
var LOG_PREFIX = "[WorkflowClient]";
|
|
624
728
|
var defaultLogger = {
|
|
@@ -647,11 +751,10 @@ class WorkflowClient {
|
|
|
647
751
|
const db = {
|
|
648
752
|
executeSql: (text, values) => this.pool.query(text, values)
|
|
649
753
|
};
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
}
|
|
754
|
+
this.boss = boss ?? new import_pg_boss.PgBoss({ db, schema: DEFAULT_PGBOSS_SCHEMA });
|
|
755
|
+
this.boss.on("error", (error) => {
|
|
756
|
+
this.logger.error("pg-boss error", error);
|
|
757
|
+
});
|
|
655
758
|
this.db = db;
|
|
656
759
|
}
|
|
657
760
|
async start() {
|
|
@@ -680,6 +783,7 @@ class WorkflowClient {
|
|
|
680
783
|
let resourceId;
|
|
681
784
|
let idempotencyKey;
|
|
682
785
|
let options;
|
|
786
|
+
let singleton = false;
|
|
683
787
|
if (typeof refOrParams === "function" && "id" in refOrParams) {
|
|
684
788
|
const ref = refOrParams;
|
|
685
789
|
workflowId = ref.id;
|
|
@@ -687,6 +791,7 @@ class WorkflowClient {
|
|
|
687
791
|
options = optionsArg;
|
|
688
792
|
resourceId = optionsArg?.resourceId;
|
|
689
793
|
idempotencyKey = optionsArg?.idempotencyKey;
|
|
794
|
+
singleton = optionsArg?.singleton ?? ref.singleton === true;
|
|
690
795
|
if (ref.inputSchema) {
|
|
691
796
|
const result = await ref.inputSchema["~standard"].validate(input);
|
|
692
797
|
if (result.issues) {
|
|
@@ -700,6 +805,7 @@ class WorkflowClient {
|
|
|
700
805
|
resourceId = params.resourceId;
|
|
701
806
|
idempotencyKey = params.idempotencyKey;
|
|
702
807
|
options = params.options;
|
|
808
|
+
singleton = params.options?.singleton === true;
|
|
703
809
|
}
|
|
704
810
|
validateWorkflowId(workflowId);
|
|
705
811
|
validateResourceId(resourceId);
|
|
@@ -714,7 +820,8 @@ class WorkflowClient {
|
|
|
714
820
|
maxRetries: options?.retries ?? 0,
|
|
715
821
|
priority: resolvePriority(options?.priority),
|
|
716
822
|
timeoutAt,
|
|
717
|
-
idempotencyKey
|
|
823
|
+
idempotencyKey,
|
|
824
|
+
singleton
|
|
718
825
|
}, _db);
|
|
719
826
|
if (created) {
|
|
720
827
|
const job = {
|
|
@@ -744,6 +851,9 @@ class WorkflowClient {
|
|
|
744
851
|
}) {
|
|
745
852
|
await this.ensureStarted();
|
|
746
853
|
const run = await this.getRun({ runId, resourceId });
|
|
854
|
+
if (run.singleton) {
|
|
855
|
+
await assertSingletonSlotAvailable({ workflowId: run.workflowId, exceptRunId: run.id }, this.db);
|
|
856
|
+
}
|
|
747
857
|
const job = {
|
|
748
858
|
runId: run.id,
|
|
749
859
|
resourceId: resourceId ?? run.resourceId ?? undefined,
|
|
@@ -921,6 +1031,16 @@ class WorkflowClient {
|
|
|
921
1031
|
workflowId
|
|
922
1032
|
}, this.db);
|
|
923
1033
|
}
|
|
1034
|
+
async getStats({
|
|
1035
|
+
resourceId,
|
|
1036
|
+
workflowId
|
|
1037
|
+
} = {}) {
|
|
1038
|
+
await this.ensureStarted();
|
|
1039
|
+
if (workflowId)
|
|
1040
|
+
validateWorkflowId(workflowId);
|
|
1041
|
+
validateResourceId(resourceId);
|
|
1042
|
+
return getWorkflowRunStats({ resourceId, workflowId }, this.db);
|
|
1043
|
+
}
|
|
924
1044
|
async ensureStarted() {
|
|
925
1045
|
if (!this._started) {
|
|
926
1046
|
await this.start();
|
|
@@ -936,6 +1056,7 @@ function createWorkflowRef(id, options) {
|
|
|
936
1056
|
timeout: defineOptions?.timeout,
|
|
937
1057
|
retries: defineOptions?.retries,
|
|
938
1058
|
priority: defineOptions?.priority,
|
|
1059
|
+
singleton: defineOptions?.singleton ?? options?.singleton,
|
|
939
1060
|
schedule: defineOptions?.schedule,
|
|
940
1061
|
timezone: defineOptions?.timezone
|
|
941
1062
|
});
|
|
@@ -944,16 +1065,29 @@ function createWorkflowRef(id, options) {
|
|
|
944
1065
|
value: options?.inputSchema,
|
|
945
1066
|
enumerable: true
|
|
946
1067
|
});
|
|
1068
|
+
Object.defineProperty(ref, "singleton", {
|
|
1069
|
+
value: options?.singleton,
|
|
1070
|
+
enumerable: true
|
|
1071
|
+
});
|
|
947
1072
|
return ref;
|
|
948
1073
|
}
|
|
949
1074
|
function createWorkflowFactory(plugins = []) {
|
|
950
|
-
const factory = (id, handler, {
|
|
1075
|
+
const factory = (id, handler, {
|
|
1076
|
+
inputSchema,
|
|
1077
|
+
timeout,
|
|
1078
|
+
retries,
|
|
1079
|
+
priority,
|
|
1080
|
+
singleton,
|
|
1081
|
+
schedule,
|
|
1082
|
+
timezone
|
|
1083
|
+
} = {}) => ({
|
|
951
1084
|
id,
|
|
952
1085
|
handler,
|
|
953
1086
|
inputSchema,
|
|
954
1087
|
timeout,
|
|
955
1088
|
retries,
|
|
956
1089
|
priority,
|
|
1090
|
+
singleton,
|
|
957
1091
|
schedule,
|
|
958
1092
|
timezone,
|
|
959
1093
|
plugins: plugins.length > 0 ? plugins : undefined
|
|
@@ -1166,11 +1300,10 @@ class WorkflowEngine {
|
|
|
1166
1300
|
const db = {
|
|
1167
1301
|
executeSql: (text, values) => this.pool.query(text, values)
|
|
1168
1302
|
};
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
}
|
|
1303
|
+
this.boss = boss ?? new import_pg_boss2.PgBoss({ db, schema: DEFAULT_PGBOSS_SCHEMA });
|
|
1304
|
+
this.boss.on("error", (error) => {
|
|
1305
|
+
this.logger.error("pg-boss error", error);
|
|
1306
|
+
});
|
|
1174
1307
|
this.db = this.boss.getDb();
|
|
1175
1308
|
}
|
|
1176
1309
|
async start(asEngine = true, {
|
|
@@ -1224,6 +1357,10 @@ class WorkflowEngine {
|
|
|
1224
1357
|
try {
|
|
1225
1358
|
await this.createWorkflowRun({ workflowId, input: {}, scheduledAt });
|
|
1226
1359
|
} catch (error) {
|
|
1360
|
+
if (error instanceof WorkflowRunInProgressError) {
|
|
1361
|
+
this.logger.log(`Skipping schedule fire for "${workflowId}": a run is already in progress`, { workflowId, runId: error.runId });
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1227
1364
|
this.logger.error(`Schedule fire failed to start a run for workflow "${workflowId}"`, error instanceof Error ? error : new Error(String(error)), { workflowId });
|
|
1228
1365
|
throw error;
|
|
1229
1366
|
}
|
|
@@ -1373,7 +1510,8 @@ class WorkflowEngine {
|
|
|
1373
1510
|
parentRunId,
|
|
1374
1511
|
parentStepId,
|
|
1375
1512
|
parentResourceId,
|
|
1376
|
-
scheduledAt
|
|
1513
|
+
scheduledAt,
|
|
1514
|
+
singleton: workflow2.singleton === true || options?.singleton === true
|
|
1377
1515
|
}, targetDb);
|
|
1378
1516
|
const insertAndEnqueue = async (targetDb) => {
|
|
1379
1517
|
const result = await insertRun(targetDb);
|
|
@@ -1532,6 +1670,9 @@ class WorkflowEngine {
|
|
|
1532
1670
|
}) {
|
|
1533
1671
|
await this.checkIfHasStarted();
|
|
1534
1672
|
const run = await this.getRun({ runId, resourceId });
|
|
1673
|
+
if (run.singleton) {
|
|
1674
|
+
await assertSingletonSlotAvailable({ workflowId: run.workflowId, exceptRunId: run.id }, this.db);
|
|
1675
|
+
}
|
|
1535
1676
|
const jobResourceId = resourceId ?? run.resourceId ?? undefined;
|
|
1536
1677
|
const job = {
|
|
1537
1678
|
runId: run.id,
|
|
@@ -2329,177 +2470,16 @@ ${error.stack}` : String(error)
|
|
|
2329
2470
|
workflowId
|
|
2330
2471
|
}, this.db);
|
|
2331
2472
|
}
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
return
|
|
2340
|
-
}
|
|
2341
|
-
if (kind === "invokeChildWorkflow" && timeline[invokeChildWorkflowTimelineKey(stepId)]) {
|
|
2342
|
-
return true;
|
|
2473
|
+
async getStats({
|
|
2474
|
+
resourceId,
|
|
2475
|
+
workflowId
|
|
2476
|
+
} = {}) {
|
|
2477
|
+
if (workflowId)
|
|
2478
|
+
validateWorkflowId(workflowId);
|
|
2479
|
+
validateResourceId(resourceId);
|
|
2480
|
+
return getWorkflowRunStats({ resourceId, workflowId }, this.db);
|
|
2343
2481
|
}
|
|
2344
|
-
return false;
|
|
2345
|
-
}
|
|
2346
|
-
function otelPlugin(options = {}) {
|
|
2347
|
-
const tracer = options.tracer ?? import_api.trace.getTracer("pg-workflows");
|
|
2348
|
-
const prefix = options.spanNamePrefix ?? DEFAULT_PREFIX;
|
|
2349
|
-
const extraAttrs = options.attributes;
|
|
2350
|
-
return {
|
|
2351
|
-
name: "opentelemetry",
|
|
2352
|
-
methods: (step, context) => {
|
|
2353
|
-
const wrapVoidish = (kind, base) => {
|
|
2354
|
-
return async (stepId, ...args) => {
|
|
2355
|
-
if (isCachedHit(context.timeline, stepId, kind)) {
|
|
2356
|
-
return base(stepId, ...args);
|
|
2357
|
-
}
|
|
2358
|
-
const capturedCtx = import_api.context.active();
|
|
2359
|
-
const startTime = new Date;
|
|
2360
|
-
let result;
|
|
2361
|
-
let originalErr;
|
|
2362
|
-
let thrownError;
|
|
2363
|
-
try {
|
|
2364
|
-
result = await base(stepId, ...args);
|
|
2365
|
-
} catch (err) {
|
|
2366
|
-
originalErr = err;
|
|
2367
|
-
thrownError = err instanceof Error ? err : new Error(String(err));
|
|
2368
|
-
}
|
|
2369
|
-
const span = tracer.startSpan(`${prefix}.step.${kind}`, {
|
|
2370
|
-
startTime,
|
|
2371
|
-
attributes: { "step.id": stepId, "step.type": kind }
|
|
2372
|
-
}, capturedCtx);
|
|
2373
|
-
if (thrownError) {
|
|
2374
|
-
span.recordException(thrownError);
|
|
2375
|
-
span.setStatus({ code: import_api.SpanStatusCode.ERROR, message: thrownError.message });
|
|
2376
|
-
span.end();
|
|
2377
|
-
throw originalErr;
|
|
2378
|
-
}
|
|
2379
|
-
span.setStatus({ code: import_api.SpanStatusCode.OK });
|
|
2380
|
-
span.end();
|
|
2381
|
-
return result;
|
|
2382
|
-
};
|
|
2383
|
-
};
|
|
2384
|
-
return {
|
|
2385
|
-
run: async (stepId, handler) => {
|
|
2386
|
-
if (isCachedHit(context.timeline, stepId, "run")) {
|
|
2387
|
-
return step.run(stepId, handler);
|
|
2388
|
-
}
|
|
2389
|
-
const capturedCtx = import_api.context.active();
|
|
2390
|
-
const startTime = new Date;
|
|
2391
|
-
let result;
|
|
2392
|
-
let originalErr;
|
|
2393
|
-
let thrownError;
|
|
2394
|
-
try {
|
|
2395
|
-
result = await step.run(stepId, handler);
|
|
2396
|
-
} catch (err) {
|
|
2397
|
-
originalErr = err;
|
|
2398
|
-
thrownError = err instanceof Error ? err : new Error(String(err));
|
|
2399
|
-
}
|
|
2400
|
-
if (result === undefined && !thrownError) {
|
|
2401
|
-
return;
|
|
2402
|
-
}
|
|
2403
|
-
const span = tracer.startSpan(`${prefix}.step.run`, {
|
|
2404
|
-
startTime,
|
|
2405
|
-
attributes: { "step.id": stepId, "step.type": "run" }
|
|
2406
|
-
}, capturedCtx);
|
|
2407
|
-
if (thrownError) {
|
|
2408
|
-
span.recordException(thrownError);
|
|
2409
|
-
span.setStatus({ code: import_api.SpanStatusCode.ERROR, message: thrownError.message });
|
|
2410
|
-
span.end();
|
|
2411
|
-
throw originalErr;
|
|
2412
|
-
}
|
|
2413
|
-
span.setStatus({ code: import_api.SpanStatusCode.OK });
|
|
2414
|
-
span.end();
|
|
2415
|
-
return result;
|
|
2416
|
-
},
|
|
2417
|
-
waitFor: wrapVoidish("waitFor", step.waitFor),
|
|
2418
|
-
delay: wrapVoidish("delay", step.delay),
|
|
2419
|
-
sleep: wrapVoidish("delay", step.delay),
|
|
2420
|
-
waitUntil: wrapVoidish("waitUntil", step.waitUntil),
|
|
2421
|
-
pause: wrapVoidish("pause", step.pause),
|
|
2422
|
-
poll: async (stepId, conditionFn, pollOptions) => {
|
|
2423
|
-
const capturedCtx = import_api.context.active();
|
|
2424
|
-
const startTime = new Date;
|
|
2425
|
-
let result;
|
|
2426
|
-
let originalErr;
|
|
2427
|
-
let thrownError;
|
|
2428
|
-
try {
|
|
2429
|
-
result = await step.poll(stepId, conditionFn, pollOptions);
|
|
2430
|
-
} catch (err) {
|
|
2431
|
-
originalErr = err;
|
|
2432
|
-
thrownError = err instanceof Error ? err : new Error(String(err));
|
|
2433
|
-
}
|
|
2434
|
-
const span = tracer.startSpan(`${prefix}.step.poll`, {
|
|
2435
|
-
startTime,
|
|
2436
|
-
attributes: { "step.id": stepId, "step.type": "poll" }
|
|
2437
|
-
}, capturedCtx);
|
|
2438
|
-
if (thrownError) {
|
|
2439
|
-
span.recordException(thrownError);
|
|
2440
|
-
span.setStatus({ code: import_api.SpanStatusCode.ERROR, message: thrownError.message });
|
|
2441
|
-
span.end();
|
|
2442
|
-
throw originalErr;
|
|
2443
|
-
}
|
|
2444
|
-
span.setStatus({ code: import_api.SpanStatusCode.OK });
|
|
2445
|
-
span.end();
|
|
2446
|
-
return result;
|
|
2447
|
-
},
|
|
2448
|
-
invokeChildWorkflow: async (stepId, refOrParams, inputArg, optionsArg) => {
|
|
2449
|
-
if (isCachedHit(context.timeline, stepId, "invokeChildWorkflow")) {
|
|
2450
|
-
return step.invokeChildWorkflow(stepId, refOrParams, inputArg, optionsArg);
|
|
2451
|
-
}
|
|
2452
|
-
const capturedCtx = import_api.context.active();
|
|
2453
|
-
const startTime = new Date;
|
|
2454
|
-
let result;
|
|
2455
|
-
let originalErr;
|
|
2456
|
-
let thrownError;
|
|
2457
|
-
try {
|
|
2458
|
-
result = await step.invokeChildWorkflow(stepId, refOrParams, inputArg, optionsArg);
|
|
2459
|
-
} catch (err) {
|
|
2460
|
-
originalErr = err;
|
|
2461
|
-
thrownError = err instanceof Error ? err : new Error(String(err));
|
|
2462
|
-
}
|
|
2463
|
-
const span = tracer.startSpan(`${prefix}.step.invokeChildWorkflow`, {
|
|
2464
|
-
startTime,
|
|
2465
|
-
attributes: { "step.id": stepId, "step.type": "invokeChildWorkflow" }
|
|
2466
|
-
}, capturedCtx);
|
|
2467
|
-
if (thrownError) {
|
|
2468
|
-
span.recordException(thrownError);
|
|
2469
|
-
span.setStatus({ code: import_api.SpanStatusCode.ERROR, message: thrownError.message });
|
|
2470
|
-
span.end();
|
|
2471
|
-
throw originalErr;
|
|
2472
|
-
}
|
|
2473
|
-
span.setStatus({ code: import_api.SpanStatusCode.OK });
|
|
2474
|
-
span.end();
|
|
2475
|
-
return result;
|
|
2476
|
-
}
|
|
2477
|
-
};
|
|
2478
|
-
},
|
|
2479
|
-
wrap: (context, next) => tracer.startActiveSpan(`${prefix}.workflow.run`, {
|
|
2480
|
-
attributes: {
|
|
2481
|
-
"workflow.id": context.workflowId,
|
|
2482
|
-
"workflow.run_id": context.runId,
|
|
2483
|
-
"workflow.attempt": context.attempt,
|
|
2484
|
-
...context.resourceId ? { "workflow.resource_id": context.resourceId } : {},
|
|
2485
|
-
...extraAttrs ? extraAttrs(context) : {}
|
|
2486
|
-
}
|
|
2487
|
-
}, async (span) => {
|
|
2488
|
-
try {
|
|
2489
|
-
const result = await next();
|
|
2490
|
-
span.setStatus({ code: import_api.SpanStatusCode.OK });
|
|
2491
|
-
return result;
|
|
2492
|
-
} catch (err) {
|
|
2493
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
2494
|
-
span.recordException(error);
|
|
2495
|
-
span.setStatus({ code: import_api.SpanStatusCode.ERROR, message: error.message });
|
|
2496
|
-
throw err;
|
|
2497
|
-
} finally {
|
|
2498
|
-
span.end();
|
|
2499
|
-
}
|
|
2500
|
-
})
|
|
2501
|
-
};
|
|
2502
2482
|
}
|
|
2503
2483
|
|
|
2504
|
-
//# debugId=
|
|
2484
|
+
//# debugId=B101AE54CEFCC2C064756E2164756E21
|
|
2505
2485
|
//# sourceMappingURL=index.js.map
|