pg-workflows 0.13.2 → 0.15.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/README.md +2 -0
- package/dist/client.entry.cjs +189 -64
- package/dist/client.entry.d.cts +43 -1
- package/dist/client.entry.d.ts +43 -1
- package/dist/client.entry.js +4 -2
- package/dist/client.entry.js.map +10 -9
- package/dist/index.cjs +209 -70
- package/dist/index.d.cts +44 -1
- package/dist/index.d.ts +44 -1
- package/dist/index.js +26 -8
- package/dist/index.js.map +11 -10
- package/dist/shared/{chunk-hjycwrsq.js → chunk-n0f7hd40.js} +158 -36
- package/dist/shared/chunk-n0f7hd40.js.map +17 -0
- package/package.json +9 -3
- package/dist/shared/chunk-hjycwrsq.js.map +0 -16
package/dist/index.cjs
CHANGED
|
@@ -69,6 +69,7 @@ __export(exports_src, {
|
|
|
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
|
|
@@ -97,7 +98,7 @@ var isInvokeChildWorkflowTimelineEntry = (entry) => !!entry && typeof entry ===
|
|
|
97
98
|
|
|
98
99
|
// src/db/migration.ts
|
|
99
100
|
var MIGRATION_LOCK_ID = 738291645;
|
|
100
|
-
var CURRENT_SCHEMA_VERSION =
|
|
101
|
+
var CURRENT_SCHEMA_VERSION = 7;
|
|
101
102
|
async function runMigrations(db) {
|
|
102
103
|
if (await isSchemaUpToDate(db)) {
|
|
103
104
|
return;
|
|
@@ -173,6 +174,17 @@ async function runMigrations(db) {
|
|
|
173
174
|
if (currentVersion < 5) {
|
|
174
175
|
commands.push("ALTER TABLE workflow_runs ADD COLUMN IF NOT EXISTS scheduled_at timestamp with time zone");
|
|
175
176
|
}
|
|
177
|
+
if (currentVersion < 6) {
|
|
178
|
+
commands.push("ALTER TABLE workflow_runs ADD COLUMN IF NOT EXISTS priority integer DEFAULT 0 NOT NULL");
|
|
179
|
+
}
|
|
180
|
+
if (currentVersion < 7) {
|
|
181
|
+
commands.push("ALTER TABLE workflow_runs ADD COLUMN IF NOT EXISTS singleton boolean DEFAULT false NOT NULL");
|
|
182
|
+
commands.push(`
|
|
183
|
+
CREATE UNIQUE INDEX IF NOT EXISTS workflow_runs_singleton_active_idx
|
|
184
|
+
ON workflow_runs (workflow_id)
|
|
185
|
+
WHERE singleton AND status IN ('pending', 'running')
|
|
186
|
+
`);
|
|
187
|
+
}
|
|
176
188
|
if (currentVersion === 0) {
|
|
177
189
|
commands.push(`INSERT INTO workflow_schema_version (version) VALUES (${CURRENT_SCHEMA_VERSION})`);
|
|
178
190
|
} else {
|
|
@@ -212,9 +224,65 @@ async function getCurrentVersion(db) {
|
|
|
212
224
|
|
|
213
225
|
// src/db/queries.ts
|
|
214
226
|
var import_ksuid = __toESM(require("ksuid"));
|
|
227
|
+
|
|
228
|
+
// src/error.ts
|
|
229
|
+
function validateWorkflowId(workflowId) {
|
|
230
|
+
if (workflowId.length > MAX_WORKFLOW_ID_LENGTH) {
|
|
231
|
+
throw new WorkflowEngineError(`workflowId exceeds maximum length of ${MAX_WORKFLOW_ID_LENGTH} characters (got ${workflowId.length})`, workflowId);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function validateResourceId(resourceId) {
|
|
235
|
+
if (resourceId != null && resourceId.length > MAX_RESOURCE_ID_LENGTH) {
|
|
236
|
+
throw new WorkflowEngineError(`resourceId exceeds maximum length of ${MAX_RESOURCE_ID_LENGTH} characters (got ${resourceId.length})`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
class WorkflowEngineError extends Error {
|
|
241
|
+
workflowId;
|
|
242
|
+
runId;
|
|
243
|
+
cause;
|
|
244
|
+
issues;
|
|
245
|
+
constructor(message, workflowId, runId, cause = undefined, issues) {
|
|
246
|
+
super(message);
|
|
247
|
+
this.workflowId = workflowId;
|
|
248
|
+
this.runId = runId;
|
|
249
|
+
this.cause = cause;
|
|
250
|
+
this.issues = issues;
|
|
251
|
+
this.name = "WorkflowEngineError";
|
|
252
|
+
if (Error.captureStackTrace) {
|
|
253
|
+
Error.captureStackTrace(this, WorkflowEngineError);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
class WorkflowRunNotFoundError extends WorkflowEngineError {
|
|
259
|
+
constructor(runId, workflowId) {
|
|
260
|
+
super("Workflow run not found", workflowId, runId);
|
|
261
|
+
this.name = "WorkflowRunNotFoundError";
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
class WorkflowRunInProgressError extends WorkflowEngineError {
|
|
266
|
+
constructor(workflowId, runId) {
|
|
267
|
+
super(`Workflow "${workflowId}" already has a running run; wait for it to pause, complete, fail, or be cancelled`, workflowId, runId);
|
|
268
|
+
this.name = "WorkflowRunInProgressError";
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/db/queries.ts
|
|
215
273
|
function generateKSUID(prefix) {
|
|
216
274
|
return `${prefix ? `${prefix}_` : ""}${import_ksuid.default.randomSync().string}`;
|
|
217
275
|
}
|
|
276
|
+
function isUniqueViolation(error) {
|
|
277
|
+
if (!error || typeof error !== "object") {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
if ("code" in error && error.code === "23505") {
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
const cause = "cause" in error ? error.cause : undefined;
|
|
284
|
+
return Boolean(cause && typeof cause === "object" && "code" in cause && cause.code === "23505");
|
|
285
|
+
}
|
|
218
286
|
function mapRowToWorkflowRun(row) {
|
|
219
287
|
return {
|
|
220
288
|
id: row.id,
|
|
@@ -234,12 +302,14 @@ function mapRowToWorkflowRun(row) {
|
|
|
234
302
|
timeoutAt: row.timeout_at ? new Date(row.timeout_at) : null,
|
|
235
303
|
retryCount: row.retry_count,
|
|
236
304
|
maxRetries: row.max_retries,
|
|
305
|
+
priority: row.priority,
|
|
237
306
|
jobId: row.job_id,
|
|
238
307
|
idempotencyKey: row.idempotency_key,
|
|
239
308
|
parentRunId: row.parent_run_id,
|
|
240
309
|
parentStepId: row.parent_step_id,
|
|
241
310
|
parentResourceId: row.parent_resource_id,
|
|
242
|
-
scheduledAt: row.scheduled_at ? new Date(row.scheduled_at) : null
|
|
311
|
+
scheduledAt: row.scheduled_at ? new Date(row.scheduled_at) : null,
|
|
312
|
+
singleton: Boolean(row.singleton)
|
|
243
313
|
};
|
|
244
314
|
}
|
|
245
315
|
async function insertWorkflowRun({
|
|
@@ -249,16 +319,26 @@ async function insertWorkflowRun({
|
|
|
249
319
|
status,
|
|
250
320
|
input,
|
|
251
321
|
maxRetries,
|
|
322
|
+
priority,
|
|
252
323
|
timeoutAt,
|
|
253
324
|
idempotencyKey,
|
|
254
325
|
parentRunId,
|
|
255
326
|
parentStepId,
|
|
256
327
|
parentResourceId,
|
|
257
|
-
scheduledAt
|
|
328
|
+
scheduledAt,
|
|
329
|
+
singleton = false
|
|
258
330
|
}, db) {
|
|
259
331
|
const runId = generateKSUID("run");
|
|
260
332
|
const now = new Date;
|
|
261
|
-
|
|
333
|
+
if (singleton) {
|
|
334
|
+
const active = await getActiveSingletonRun(workflowId, db);
|
|
335
|
+
if (active) {
|
|
336
|
+
throw new WorkflowRunInProgressError(workflowId, active.id);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
let result;
|
|
340
|
+
try {
|
|
341
|
+
result = await db.executeSql(`INSERT INTO workflow_runs (
|
|
262
342
|
id,
|
|
263
343
|
resource_id,
|
|
264
344
|
workflow_id,
|
|
@@ -266,6 +346,7 @@ async function insertWorkflowRun({
|
|
|
266
346
|
status,
|
|
267
347
|
input,
|
|
268
348
|
max_retries,
|
|
349
|
+
priority,
|
|
269
350
|
timeout_at,
|
|
270
351
|
created_at,
|
|
271
352
|
updated_at,
|
|
@@ -275,29 +356,39 @@ async function insertWorkflowRun({
|
|
|
275
356
|
parent_run_id,
|
|
276
357
|
parent_step_id,
|
|
277
358
|
parent_resource_id,
|
|
278
|
-
scheduled_at
|
|
359
|
+
scheduled_at,
|
|
360
|
+
singleton
|
|
279
361
|
)
|
|
280
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
|
|
362
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
|
|
281
363
|
ON CONFLICT (idempotency_key) WHERE idempotency_key IS NOT NULL DO NOTHING
|
|
282
364
|
RETURNING *`, [
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
365
|
+
runId,
|
|
366
|
+
resourceId ?? null,
|
|
367
|
+
workflowId,
|
|
368
|
+
currentStepId,
|
|
369
|
+
status,
|
|
370
|
+
JSON.stringify(input),
|
|
371
|
+
maxRetries,
|
|
372
|
+
priority,
|
|
373
|
+
timeoutAt,
|
|
374
|
+
now,
|
|
375
|
+
now,
|
|
376
|
+
"{}",
|
|
377
|
+
0,
|
|
378
|
+
idempotencyKey ?? null,
|
|
379
|
+
parentRunId ?? null,
|
|
380
|
+
parentStepId ?? null,
|
|
381
|
+
parentResourceId ?? null,
|
|
382
|
+
scheduledAt ?? null,
|
|
383
|
+
singleton
|
|
384
|
+
]);
|
|
385
|
+
} catch (error) {
|
|
386
|
+
if (singleton && isUniqueViolation(error)) {
|
|
387
|
+
const active = await getActiveSingletonRun(workflowId, db);
|
|
388
|
+
throw new WorkflowRunInProgressError(workflowId, active?.id);
|
|
389
|
+
}
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
301
392
|
if (result.rows[0]) {
|
|
302
393
|
return { run: mapRowToWorkflowRun(result.rows[0]), created: true };
|
|
303
394
|
}
|
|
@@ -309,6 +400,20 @@ async function insertWorkflowRun({
|
|
|
309
400
|
}
|
|
310
401
|
return { run: mapRowToWorkflowRun(existing.rows[0]), created: false };
|
|
311
402
|
}
|
|
403
|
+
async function getActiveSingletonRun(workflowId, db) {
|
|
404
|
+
const result = await db.executeSql(`SELECT * FROM workflow_runs
|
|
405
|
+
WHERE workflow_id = $1
|
|
406
|
+
AND singleton
|
|
407
|
+
AND status IN ('pending', 'running')
|
|
408
|
+
LIMIT 1`, [workflowId]);
|
|
409
|
+
return result.rows[0] ? mapRowToWorkflowRun(result.rows[0]) : null;
|
|
410
|
+
}
|
|
411
|
+
async function assertSingletonSlotAvailable({ workflowId, exceptRunId }, db) {
|
|
412
|
+
const active = await getActiveSingletonRun(workflowId, db);
|
|
413
|
+
if (active && active.id !== exceptRunId) {
|
|
414
|
+
throw new WorkflowRunInProgressError(workflowId, active.id);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
312
417
|
async function getWorkflowRun({
|
|
313
418
|
runId,
|
|
314
419
|
resourceId
|
|
@@ -423,7 +528,15 @@ async function updateWorkflowRun({
|
|
|
423
528
|
${whereClause}
|
|
424
529
|
RETURNING *
|
|
425
530
|
`;
|
|
426
|
-
|
|
531
|
+
let result;
|
|
532
|
+
try {
|
|
533
|
+
result = await db.executeSql(query, values);
|
|
534
|
+
} catch (error) {
|
|
535
|
+
if (data.status === "running" && isUniqueViolation(error)) {
|
|
536
|
+
throw new WorkflowRunInProgressError("unknown", runId);
|
|
537
|
+
}
|
|
538
|
+
throw error;
|
|
539
|
+
}
|
|
427
540
|
const run = result.rows[0];
|
|
428
541
|
if (!run) {
|
|
429
542
|
return null;
|
|
@@ -529,41 +642,28 @@ async function withPostgresTransaction(db, callback, pool) {
|
|
|
529
642
|
}
|
|
530
643
|
}
|
|
531
644
|
|
|
532
|
-
// src/
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
645
|
+
// src/priority.ts
|
|
646
|
+
var PRIORITY_LEVELS = { high: 100, normal: 0, low: -100 };
|
|
647
|
+
function priorityToInt(value) {
|
|
648
|
+
if (typeof value === "number") {
|
|
649
|
+
if (!Number.isInteger(value)) {
|
|
650
|
+
throw new WorkflowEngineError(`Invalid priority ${value}: numeric priority must be an integer.`);
|
|
651
|
+
}
|
|
652
|
+
return value;
|
|
536
653
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
throw new WorkflowEngineError(`resourceId exceeds maximum length of ${MAX_RESOURCE_ID_LENGTH} characters (got ${resourceId.length})`);
|
|
654
|
+
const mapped = PRIORITY_LEVELS[value];
|
|
655
|
+
if (mapped === undefined) {
|
|
656
|
+
throw new WorkflowEngineError(`Invalid priority "${value}": expected one of ${Object.keys(PRIORITY_LEVELS).map((k) => `"${k}"`).join(", ")} or an integer.`);
|
|
541
657
|
}
|
|
658
|
+
return mapped;
|
|
542
659
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
cause;
|
|
548
|
-
issues;
|
|
549
|
-
constructor(message, workflowId, runId, cause = undefined, issues) {
|
|
550
|
-
super(message);
|
|
551
|
-
this.workflowId = workflowId;
|
|
552
|
-
this.runId = runId;
|
|
553
|
-
this.cause = cause;
|
|
554
|
-
this.issues = issues;
|
|
555
|
-
this.name = "WorkflowEngineError";
|
|
556
|
-
if (Error.captureStackTrace) {
|
|
557
|
-
Error.captureStackTrace(this, WorkflowEngineError);
|
|
660
|
+
function resolvePriority(...candidates) {
|
|
661
|
+
for (const candidate of candidates) {
|
|
662
|
+
if (candidate !== undefined) {
|
|
663
|
+
return priorityToInt(candidate);
|
|
558
664
|
}
|
|
559
665
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
class WorkflowRunNotFoundError extends WorkflowEngineError {
|
|
563
|
-
constructor(runId, workflowId) {
|
|
564
|
-
super("Workflow run not found", workflowId, runId);
|
|
565
|
-
this.name = "WorkflowRunNotFoundError";
|
|
566
|
-
}
|
|
666
|
+
return PRIORITY_LEVELS.normal;
|
|
567
667
|
}
|
|
568
668
|
|
|
569
669
|
// src/types.ts
|
|
@@ -616,11 +716,10 @@ class WorkflowClient {
|
|
|
616
716
|
const db = {
|
|
617
717
|
executeSql: (text, values) => this.pool.query(text, values)
|
|
618
718
|
};
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
}
|
|
719
|
+
this.boss = boss ?? new import_pg_boss.PgBoss({ db, schema: DEFAULT_PGBOSS_SCHEMA });
|
|
720
|
+
this.boss.on("error", (error) => {
|
|
721
|
+
this.logger.error("pg-boss error", error);
|
|
722
|
+
});
|
|
624
723
|
this.db = db;
|
|
625
724
|
}
|
|
626
725
|
async start() {
|
|
@@ -649,6 +748,7 @@ class WorkflowClient {
|
|
|
649
748
|
let resourceId;
|
|
650
749
|
let idempotencyKey;
|
|
651
750
|
let options;
|
|
751
|
+
let singleton = false;
|
|
652
752
|
if (typeof refOrParams === "function" && "id" in refOrParams) {
|
|
653
753
|
const ref = refOrParams;
|
|
654
754
|
workflowId = ref.id;
|
|
@@ -656,6 +756,7 @@ class WorkflowClient {
|
|
|
656
756
|
options = optionsArg;
|
|
657
757
|
resourceId = optionsArg?.resourceId;
|
|
658
758
|
idempotencyKey = optionsArg?.idempotencyKey;
|
|
759
|
+
singleton = optionsArg?.singleton ?? ref.singleton === true;
|
|
659
760
|
if (ref.inputSchema) {
|
|
660
761
|
const result = await ref.inputSchema["~standard"].validate(input);
|
|
661
762
|
if (result.issues) {
|
|
@@ -669,6 +770,7 @@ class WorkflowClient {
|
|
|
669
770
|
resourceId = params.resourceId;
|
|
670
771
|
idempotencyKey = params.idempotencyKey;
|
|
671
772
|
options = params.options;
|
|
773
|
+
singleton = params.options?.singleton === true;
|
|
672
774
|
}
|
|
673
775
|
validateWorkflowId(workflowId);
|
|
674
776
|
validateResourceId(resourceId);
|
|
@@ -681,8 +783,10 @@ class WorkflowClient {
|
|
|
681
783
|
status: "running" /* RUNNING */,
|
|
682
784
|
input,
|
|
683
785
|
maxRetries: options?.retries ?? 0,
|
|
786
|
+
priority: resolvePriority(options?.priority),
|
|
684
787
|
timeoutAt,
|
|
685
|
-
idempotencyKey
|
|
788
|
+
idempotencyKey,
|
|
789
|
+
singleton
|
|
686
790
|
}, _db);
|
|
687
791
|
if (created) {
|
|
688
792
|
const job = {
|
|
@@ -694,6 +798,7 @@ class WorkflowClient {
|
|
|
694
798
|
await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
|
|
695
799
|
startAfter: new Date,
|
|
696
800
|
expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds,
|
|
801
|
+
priority: insertedRun.priority,
|
|
697
802
|
db: _db
|
|
698
803
|
});
|
|
699
804
|
}
|
|
@@ -711,6 +816,9 @@ class WorkflowClient {
|
|
|
711
816
|
}) {
|
|
712
817
|
await this.ensureStarted();
|
|
713
818
|
const run = await this.getRun({ runId, resourceId });
|
|
819
|
+
if (run.singleton) {
|
|
820
|
+
await assertSingletonSlotAvailable({ workflowId: run.workflowId, exceptRunId: run.id }, this.db);
|
|
821
|
+
}
|
|
714
822
|
const job = {
|
|
715
823
|
runId: run.id,
|
|
716
824
|
resourceId: resourceId ?? run.resourceId ?? undefined,
|
|
@@ -722,7 +830,8 @@ class WorkflowClient {
|
|
|
722
830
|
}
|
|
723
831
|
};
|
|
724
832
|
await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
|
|
725
|
-
expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds
|
|
833
|
+
expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds,
|
|
834
|
+
priority: run.priority
|
|
726
835
|
});
|
|
727
836
|
this.logger.log(`${LOG_PREFIX} Event ${eventName} sent for workflow run ${runId}`);
|
|
728
837
|
return run;
|
|
@@ -901,6 +1010,8 @@ function createWorkflowRef(id, options) {
|
|
|
901
1010
|
inputSchema: options?.inputSchema,
|
|
902
1011
|
timeout: defineOptions?.timeout,
|
|
903
1012
|
retries: defineOptions?.retries,
|
|
1013
|
+
priority: defineOptions?.priority,
|
|
1014
|
+
singleton: defineOptions?.singleton ?? options?.singleton,
|
|
904
1015
|
schedule: defineOptions?.schedule,
|
|
905
1016
|
timezone: defineOptions?.timezone
|
|
906
1017
|
});
|
|
@@ -909,15 +1020,29 @@ function createWorkflowRef(id, options) {
|
|
|
909
1020
|
value: options?.inputSchema,
|
|
910
1021
|
enumerable: true
|
|
911
1022
|
});
|
|
1023
|
+
Object.defineProperty(ref, "singleton", {
|
|
1024
|
+
value: options?.singleton,
|
|
1025
|
+
enumerable: true
|
|
1026
|
+
});
|
|
912
1027
|
return ref;
|
|
913
1028
|
}
|
|
914
1029
|
function createWorkflowFactory(plugins = []) {
|
|
915
|
-
const factory = (id, handler, {
|
|
1030
|
+
const factory = (id, handler, {
|
|
1031
|
+
inputSchema,
|
|
1032
|
+
timeout,
|
|
1033
|
+
retries,
|
|
1034
|
+
priority,
|
|
1035
|
+
singleton,
|
|
1036
|
+
schedule,
|
|
1037
|
+
timezone
|
|
1038
|
+
} = {}) => ({
|
|
916
1039
|
id,
|
|
917
1040
|
handler,
|
|
918
1041
|
inputSchema,
|
|
919
1042
|
timeout,
|
|
920
1043
|
retries,
|
|
1044
|
+
priority,
|
|
1045
|
+
singleton,
|
|
921
1046
|
schedule,
|
|
922
1047
|
timezone,
|
|
923
1048
|
plugins: plugins.length > 0 ? plugins : undefined
|
|
@@ -1130,11 +1255,10 @@ class WorkflowEngine {
|
|
|
1130
1255
|
const db = {
|
|
1131
1256
|
executeSql: (text, values) => this.pool.query(text, values)
|
|
1132
1257
|
};
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
}
|
|
1258
|
+
this.boss = boss ?? new import_pg_boss2.PgBoss({ db, schema: DEFAULT_PGBOSS_SCHEMA });
|
|
1259
|
+
this.boss.on("error", (error) => {
|
|
1260
|
+
this.logger.error("pg-boss error", error);
|
|
1261
|
+
});
|
|
1138
1262
|
this.db = this.boss.getDb();
|
|
1139
1263
|
}
|
|
1140
1264
|
async start(asEngine = true, {
|
|
@@ -1188,6 +1312,10 @@ class WorkflowEngine {
|
|
|
1188
1312
|
try {
|
|
1189
1313
|
await this.createWorkflowRun({ workflowId, input: {}, scheduledAt });
|
|
1190
1314
|
} catch (error) {
|
|
1315
|
+
if (error instanceof WorkflowRunInProgressError) {
|
|
1316
|
+
this.logger.log(`Skipping schedule fire for "${workflowId}": a run is already in progress`, { workflowId, runId: error.runId });
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1191
1319
|
this.logger.error(`Schedule fire failed to start a run for workflow "${workflowId}"`, error instanceof Error ? error : new Error(String(error)), { workflowId });
|
|
1192
1320
|
throw error;
|
|
1193
1321
|
}
|
|
@@ -1300,6 +1428,7 @@ class WorkflowEngine {
|
|
|
1300
1428
|
parentRunId,
|
|
1301
1429
|
parentStepId,
|
|
1302
1430
|
parentResourceId,
|
|
1431
|
+
parentPriority,
|
|
1303
1432
|
scheduledAt,
|
|
1304
1433
|
enqueue = true,
|
|
1305
1434
|
db
|
|
@@ -1330,12 +1459,14 @@ class WorkflowEngine {
|
|
|
1330
1459
|
status: "running" /* RUNNING */,
|
|
1331
1460
|
input,
|
|
1332
1461
|
maxRetries: options?.retries ?? workflow2.retries ?? 0,
|
|
1462
|
+
priority: resolvePriority(options?.priority, workflow2.priority, parentPriority),
|
|
1333
1463
|
timeoutAt,
|
|
1334
1464
|
idempotencyKey,
|
|
1335
1465
|
parentRunId,
|
|
1336
1466
|
parentStepId,
|
|
1337
1467
|
parentResourceId,
|
|
1338
|
-
scheduledAt
|
|
1468
|
+
scheduledAt,
|
|
1469
|
+
singleton: workflow2.singleton === true || options?.singleton === true
|
|
1339
1470
|
}, targetDb);
|
|
1340
1471
|
const insertAndEnqueue = async (targetDb) => {
|
|
1341
1472
|
const result = await insertRun(targetDb);
|
|
@@ -1357,6 +1488,7 @@ class WorkflowEngine {
|
|
|
1357
1488
|
await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
|
|
1358
1489
|
startAfter: new Date,
|
|
1359
1490
|
expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds2,
|
|
1491
|
+
priority: run.priority,
|
|
1360
1492
|
...retrySendOptions(run.maxRetries),
|
|
1361
1493
|
...db ? { db } : {}
|
|
1362
1494
|
});
|
|
@@ -1493,6 +1625,9 @@ class WorkflowEngine {
|
|
|
1493
1625
|
}) {
|
|
1494
1626
|
await this.checkIfHasStarted();
|
|
1495
1627
|
const run = await this.getRun({ runId, resourceId });
|
|
1628
|
+
if (run.singleton) {
|
|
1629
|
+
await assertSingletonSlotAvailable({ workflowId: run.workflowId, exceptRunId: run.id }, this.db);
|
|
1630
|
+
}
|
|
1496
1631
|
const jobResourceId = resourceId ?? run.resourceId ?? undefined;
|
|
1497
1632
|
const job = {
|
|
1498
1633
|
runId: run.id,
|
|
@@ -1506,6 +1641,7 @@ class WorkflowEngine {
|
|
|
1506
1641
|
};
|
|
1507
1642
|
await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
|
|
1508
1643
|
expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds2,
|
|
1644
|
+
priority: run.priority,
|
|
1509
1645
|
...retrySendOptions(run.maxRetries)
|
|
1510
1646
|
});
|
|
1511
1647
|
this.logger.log(`event ${eventName} sent for workflow run with id ${runId}`);
|
|
@@ -1915,6 +2051,7 @@ class WorkflowEngine {
|
|
|
1915
2051
|
parentRunId: run.id,
|
|
1916
2052
|
parentStepId: stepId,
|
|
1917
2053
|
parentResourceId: run.resourceId ?? undefined,
|
|
2054
|
+
parentPriority: run.priority,
|
|
1918
2055
|
enqueue: true,
|
|
1919
2056
|
db
|
|
1920
2057
|
});
|
|
@@ -2113,6 +2250,7 @@ ${error.stack}` : String(error)
|
|
|
2113
2250
|
await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
|
|
2114
2251
|
startAfter: timeoutDate.getTime() <= Date.now() ? new Date : timeoutDate,
|
|
2115
2252
|
expireInSeconds: defaultExpireInSeconds2,
|
|
2253
|
+
priority: run.priority,
|
|
2116
2254
|
...retrySendOptions(run.maxRetries)
|
|
2117
2255
|
});
|
|
2118
2256
|
} catch (error) {
|
|
@@ -2231,6 +2369,7 @@ ${error.stack}` : String(error)
|
|
|
2231
2369
|
}, {
|
|
2232
2370
|
startAfter: new Date(Date.now() + intervalMs),
|
|
2233
2371
|
expireInSeconds: defaultExpireInSeconds2,
|
|
2372
|
+
priority: run.priority,
|
|
2234
2373
|
...retrySendOptions(run.maxRetries)
|
|
2235
2374
|
});
|
|
2236
2375
|
} catch (error) {
|
|
@@ -2458,5 +2597,5 @@ function otelPlugin(options = {}) {
|
|
|
2458
2597
|
};
|
|
2459
2598
|
}
|
|
2460
2599
|
|
|
2461
|
-
//# debugId=
|
|
2600
|
+
//# debugId=0BBC9686D69E329D64756E2164756E21
|
|
2462
2601
|
//# sourceMappingURL=index.js.map
|
package/dist/index.d.cts
CHANGED
|
@@ -18,6 +18,15 @@ type WorkflowRun = {
|
|
|
18
18
|
timeoutAt: Date | null;
|
|
19
19
|
retryCount: number;
|
|
20
20
|
maxRetries: number;
|
|
21
|
+
/** Resolved scheduling priority (pg-boss integer; higher runs first). */
|
|
22
|
+
priority: number;
|
|
23
|
+
/**
|
|
24
|
+
* When true, this run participates in the per-workflow-ID uniqueness
|
|
25
|
+
* constraint: no other pending or running singleton run of the same
|
|
26
|
+
* workflow ID may exist at the same time. Paused, completed, failed, and
|
|
27
|
+
* cancelled runs release the slot.
|
|
28
|
+
*/
|
|
29
|
+
singleton: boolean;
|
|
21
30
|
jobId: string | null;
|
|
22
31
|
idempotencyKey: string | null;
|
|
23
32
|
parentRunId: string | null;
|
|
@@ -35,6 +44,18 @@ type DurationObject = {
|
|
|
35
44
|
seconds?: number;
|
|
36
45
|
};
|
|
37
46
|
type Duration = string | DurationObject;
|
|
47
|
+
/**
|
|
48
|
+
* Named priority levels mapped to pg-boss integer priorities. Higher numbers
|
|
49
|
+
* are fetched first (pg-boss orders by `priority DESC`), and `normal = 0`
|
|
50
|
+
* matches pg-boss's own default. The ±100 spacing leaves room for numeric
|
|
51
|
+
* tuning between tiers via the escape hatch.
|
|
52
|
+
*/
|
|
53
|
+
declare const PRIORITY_LEVELS: {
|
|
54
|
+
readonly high: 100;
|
|
55
|
+
readonly normal: 0;
|
|
56
|
+
readonly low: -100;
|
|
57
|
+
};
|
|
58
|
+
type WorkflowPriority = keyof typeof PRIORITY_LEVELS | number;
|
|
38
59
|
type Schedule = string | Exclude<Duration, string>;
|
|
39
60
|
declare enum WorkflowStatus {
|
|
40
61
|
PENDING = "pending",
|
|
@@ -61,11 +82,25 @@ type StartWorkflowOptions = {
|
|
|
61
82
|
retries?: number;
|
|
62
83
|
expireInSeconds?: number;
|
|
63
84
|
idempotencyKey?: string;
|
|
85
|
+
priority?: WorkflowPriority;
|
|
86
|
+
/**
|
|
87
|
+
* Client-side counterpart of `workflow(..., { singleton: true })`.
|
|
88
|
+
* The engine reads this from the registered definition; pass it here when
|
|
89
|
+
* starting from `WorkflowClient` so the uniqueness constraint applies.
|
|
90
|
+
*/
|
|
91
|
+
singleton?: boolean;
|
|
64
92
|
};
|
|
65
93
|
type WorkflowOptions<I extends InputParameters> = {
|
|
66
94
|
timeout?: number;
|
|
67
95
|
retries?: number;
|
|
68
96
|
inputSchema?: I;
|
|
97
|
+
priority?: WorkflowPriority;
|
|
98
|
+
/**
|
|
99
|
+
* When true, at most one pending or running run of this workflow ID may exist.
|
|
100
|
+
* A second `startWorkflow` throws `WorkflowRunInProgressError` until the
|
|
101
|
+
* current run pauses, completes, fails, or is cancelled.
|
|
102
|
+
*/
|
|
103
|
+
singleton?: boolean;
|
|
69
104
|
/**
|
|
70
105
|
* Recurring schedule. Accepts a cron expression (`'0 9 * * 1-5'`),
|
|
71
106
|
* a duration string (`'5m'`, `'1 hour'`), or a `DurationObject`.
|
|
@@ -173,6 +208,8 @@ type WorkflowDefinition<TInput extends InputParameters = InputParameters> = {
|
|
|
173
208
|
inputSchema?: TInput;
|
|
174
209
|
timeout?: number;
|
|
175
210
|
retries?: number;
|
|
211
|
+
priority?: WorkflowPriority;
|
|
212
|
+
singleton?: boolean;
|
|
176
213
|
schedule?: Schedule;
|
|
177
214
|
timezone?: string;
|
|
178
215
|
plugins?: WorkflowPlugin[];
|
|
@@ -199,6 +236,7 @@ interface WorkflowFactory<TStepExt = object> {
|
|
|
199
236
|
TOutput = unknown
|
|
200
237
|
>(id: string, options?: {
|
|
201
238
|
inputSchema?: TInput;
|
|
239
|
+
singleton?: boolean;
|
|
202
240
|
}): WorkflowRef<TInput, TOutput>;
|
|
203
241
|
}
|
|
204
242
|
/**
|
|
@@ -215,6 +253,7 @@ interface WorkflowRef<
|
|
|
215
253
|
(handler: (context: WorkflowContext<TInput, StepBaseContext>) => Promise<unknown>, options?: Omit<WorkflowOptions<TInput>, "inputSchema">): WorkflowDefinition<TInput>;
|
|
216
254
|
readonly id: string;
|
|
217
255
|
readonly inputSchema?: TInput;
|
|
256
|
+
readonly singleton?: boolean;
|
|
218
257
|
}
|
|
219
258
|
type WorkflowRunProgress = WorkflowRun & {
|
|
220
259
|
completionPercentage: number;
|
|
@@ -321,6 +360,7 @@ declare function createWorkflowRef<
|
|
|
321
360
|
TInput extends InputParameters = InputParameters
|
|
322
361
|
>(id: string, options?: {
|
|
323
362
|
inputSchema?: TInput;
|
|
363
|
+
singleton?: boolean;
|
|
324
364
|
}): WorkflowRef<TInput, TOutput>;
|
|
325
365
|
declare const workflow: WorkflowFactory;
|
|
326
366
|
import pg2 from "pg";
|
|
@@ -494,6 +534,9 @@ declare class WorkflowEngineError extends Error {
|
|
|
494
534
|
declare class WorkflowRunNotFoundError extends WorkflowEngineError {
|
|
495
535
|
constructor(runId?: string, workflowId?: string);
|
|
496
536
|
}
|
|
537
|
+
declare class WorkflowRunInProgressError extends WorkflowEngineError {
|
|
538
|
+
constructor(workflowId: string, runId?: string);
|
|
539
|
+
}
|
|
497
540
|
import { AttributeValue, Tracer } from "@opentelemetry/api";
|
|
498
541
|
type OtelPluginOptions = {
|
|
499
542
|
/** Tracer to use. Defaults to `trace.getTracer('pg-workflows')`. */
|
|
@@ -504,4 +547,4 @@ type OtelPluginOptions = {
|
|
|
504
547
|
attributes?: (context: WorkflowContext) => Record<string, AttributeValue>;
|
|
505
548
|
};
|
|
506
549
|
declare function otelPlugin(options?: OtelPluginOptions): WorkflowPlugin<StepBaseContext, object>;
|
|
507
|
-
export { workflow, otelPlugin, createWorkflowRef, WorkflowStatus, WorkflowRunProgress, WorkflowRunNotFoundError, WorkflowRun, WorkflowRef, WorkflowPlugin, WorkflowOptions, WorkflowLogger, WorkflowEngineOptions, WorkflowEngineError, WorkflowEngine, WorkflowDefinition, WorkflowContext, WorkflowClientOptions, WorkflowClient, StepBaseContext, StartWorkflowOptions, ScheduleContext, Schedule, OtelPluginOptions, InputParameters, InferInputParameters, Duration };
|
|
550
|
+
export { workflow, otelPlugin, createWorkflowRef, WorkflowStatus, WorkflowRunProgress, WorkflowRunNotFoundError, WorkflowRunInProgressError, WorkflowRun, WorkflowRef, WorkflowPriority, WorkflowPlugin, WorkflowOptions, WorkflowLogger, WorkflowEngineOptions, WorkflowEngineError, WorkflowEngine, WorkflowDefinition, WorkflowContext, WorkflowClientOptions, WorkflowClient, StepBaseContext, StartWorkflowOptions, ScheduleContext, Schedule, OtelPluginOptions, InputParameters, InferInputParameters, Duration };
|