pg-workflows 0.14.0 → 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 +160 -71
- package/dist/client.entry.d.cts +26 -1
- package/dist/client.entry.d.ts +26 -1
- package/dist/client.entry.js +4 -2
- package/dist/client.entry.js.map +8 -8
- package/dist/index.cjs +173 -77
- package/dist/index.d.cts +27 -1
- package/dist/index.d.ts +27 -1
- package/dist/index.js +18 -8
- package/dist/index.js.map +9 -9
- package/dist/shared/{chunk-w3qcrrwx.js → chunk-n0f7hd40.js} +122 -36
- package/dist/shared/chunk-n0f7hd40.js.map +17 -0
- package/package.json +9 -3
- package/dist/shared/chunk-w3qcrrwx.js.map +0 -17
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;
|
|
@@ -176,6 +177,14 @@ async function runMigrations(db) {
|
|
|
176
177
|
if (currentVersion < 6) {
|
|
177
178
|
commands.push("ALTER TABLE workflow_runs ADD COLUMN IF NOT EXISTS priority integer DEFAULT 0 NOT NULL");
|
|
178
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
|
+
}
|
|
179
188
|
if (currentVersion === 0) {
|
|
180
189
|
commands.push(`INSERT INTO workflow_schema_version (version) VALUES (${CURRENT_SCHEMA_VERSION})`);
|
|
181
190
|
} else {
|
|
@@ -215,9 +224,65 @@ async function getCurrentVersion(db) {
|
|
|
215
224
|
|
|
216
225
|
// src/db/queries.ts
|
|
217
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
|
|
218
273
|
function generateKSUID(prefix) {
|
|
219
274
|
return `${prefix ? `${prefix}_` : ""}${import_ksuid.default.randomSync().string}`;
|
|
220
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
|
+
}
|
|
221
286
|
function mapRowToWorkflowRun(row) {
|
|
222
287
|
return {
|
|
223
288
|
id: row.id,
|
|
@@ -243,7 +308,8 @@ function mapRowToWorkflowRun(row) {
|
|
|
243
308
|
parentRunId: row.parent_run_id,
|
|
244
309
|
parentStepId: row.parent_step_id,
|
|
245
310
|
parentResourceId: row.parent_resource_id,
|
|
246
|
-
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)
|
|
247
313
|
};
|
|
248
314
|
}
|
|
249
315
|
async function insertWorkflowRun({
|
|
@@ -259,11 +325,20 @@ async function insertWorkflowRun({
|
|
|
259
325
|
parentRunId,
|
|
260
326
|
parentStepId,
|
|
261
327
|
parentResourceId,
|
|
262
|
-
scheduledAt
|
|
328
|
+
scheduledAt,
|
|
329
|
+
singleton = false
|
|
263
330
|
}, db) {
|
|
264
331
|
const runId = generateKSUID("run");
|
|
265
332
|
const now = new Date;
|
|
266
|
-
|
|
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 (
|
|
267
342
|
id,
|
|
268
343
|
resource_id,
|
|
269
344
|
workflow_id,
|
|
@@ -281,30 +356,39 @@ async function insertWorkflowRun({
|
|
|
281
356
|
parent_run_id,
|
|
282
357
|
parent_step_id,
|
|
283
358
|
parent_resource_id,
|
|
284
|
-
scheduled_at
|
|
359
|
+
scheduled_at,
|
|
360
|
+
singleton
|
|
285
361
|
)
|
|
286
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
|
|
362
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
|
|
287
363
|
ON CONFLICT (idempotency_key) WHERE idempotency_key IS NOT NULL DO NOTHING
|
|
288
364
|
RETURNING *`, [
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
+
}
|
|
308
392
|
if (result.rows[0]) {
|
|
309
393
|
return { run: mapRowToWorkflowRun(result.rows[0]), created: true };
|
|
310
394
|
}
|
|
@@ -316,6 +400,20 @@ async function insertWorkflowRun({
|
|
|
316
400
|
}
|
|
317
401
|
return { run: mapRowToWorkflowRun(existing.rows[0]), created: false };
|
|
318
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
|
+
}
|
|
319
417
|
async function getWorkflowRun({
|
|
320
418
|
runId,
|
|
321
419
|
resourceId
|
|
@@ -430,7 +528,15 @@ async function updateWorkflowRun({
|
|
|
430
528
|
${whereClause}
|
|
431
529
|
RETURNING *
|
|
432
530
|
`;
|
|
433
|
-
|
|
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
|
+
}
|
|
434
540
|
const run = result.rows[0];
|
|
435
541
|
if (!run) {
|
|
436
542
|
return null;
|
|
@@ -536,43 +642,6 @@ async function withPostgresTransaction(db, callback, pool) {
|
|
|
536
642
|
}
|
|
537
643
|
}
|
|
538
644
|
|
|
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
645
|
// src/priority.ts
|
|
577
646
|
var PRIORITY_LEVELS = { high: 100, normal: 0, low: -100 };
|
|
578
647
|
function priorityToInt(value) {
|
|
@@ -647,11 +716,10 @@ class WorkflowClient {
|
|
|
647
716
|
const db = {
|
|
648
717
|
executeSql: (text, values) => this.pool.query(text, values)
|
|
649
718
|
};
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
}
|
|
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
|
+
});
|
|
655
723
|
this.db = db;
|
|
656
724
|
}
|
|
657
725
|
async start() {
|
|
@@ -680,6 +748,7 @@ class WorkflowClient {
|
|
|
680
748
|
let resourceId;
|
|
681
749
|
let idempotencyKey;
|
|
682
750
|
let options;
|
|
751
|
+
let singleton = false;
|
|
683
752
|
if (typeof refOrParams === "function" && "id" in refOrParams) {
|
|
684
753
|
const ref = refOrParams;
|
|
685
754
|
workflowId = ref.id;
|
|
@@ -687,6 +756,7 @@ class WorkflowClient {
|
|
|
687
756
|
options = optionsArg;
|
|
688
757
|
resourceId = optionsArg?.resourceId;
|
|
689
758
|
idempotencyKey = optionsArg?.idempotencyKey;
|
|
759
|
+
singleton = optionsArg?.singleton ?? ref.singleton === true;
|
|
690
760
|
if (ref.inputSchema) {
|
|
691
761
|
const result = await ref.inputSchema["~standard"].validate(input);
|
|
692
762
|
if (result.issues) {
|
|
@@ -700,6 +770,7 @@ class WorkflowClient {
|
|
|
700
770
|
resourceId = params.resourceId;
|
|
701
771
|
idempotencyKey = params.idempotencyKey;
|
|
702
772
|
options = params.options;
|
|
773
|
+
singleton = params.options?.singleton === true;
|
|
703
774
|
}
|
|
704
775
|
validateWorkflowId(workflowId);
|
|
705
776
|
validateResourceId(resourceId);
|
|
@@ -714,7 +785,8 @@ class WorkflowClient {
|
|
|
714
785
|
maxRetries: options?.retries ?? 0,
|
|
715
786
|
priority: resolvePriority(options?.priority),
|
|
716
787
|
timeoutAt,
|
|
717
|
-
idempotencyKey
|
|
788
|
+
idempotencyKey,
|
|
789
|
+
singleton
|
|
718
790
|
}, _db);
|
|
719
791
|
if (created) {
|
|
720
792
|
const job = {
|
|
@@ -744,6 +816,9 @@ class WorkflowClient {
|
|
|
744
816
|
}) {
|
|
745
817
|
await this.ensureStarted();
|
|
746
818
|
const run = await this.getRun({ runId, resourceId });
|
|
819
|
+
if (run.singleton) {
|
|
820
|
+
await assertSingletonSlotAvailable({ workflowId: run.workflowId, exceptRunId: run.id }, this.db);
|
|
821
|
+
}
|
|
747
822
|
const job = {
|
|
748
823
|
runId: run.id,
|
|
749
824
|
resourceId: resourceId ?? run.resourceId ?? undefined,
|
|
@@ -936,6 +1011,7 @@ function createWorkflowRef(id, options) {
|
|
|
936
1011
|
timeout: defineOptions?.timeout,
|
|
937
1012
|
retries: defineOptions?.retries,
|
|
938
1013
|
priority: defineOptions?.priority,
|
|
1014
|
+
singleton: defineOptions?.singleton ?? options?.singleton,
|
|
939
1015
|
schedule: defineOptions?.schedule,
|
|
940
1016
|
timezone: defineOptions?.timezone
|
|
941
1017
|
});
|
|
@@ -944,16 +1020,29 @@ function createWorkflowRef(id, options) {
|
|
|
944
1020
|
value: options?.inputSchema,
|
|
945
1021
|
enumerable: true
|
|
946
1022
|
});
|
|
1023
|
+
Object.defineProperty(ref, "singleton", {
|
|
1024
|
+
value: options?.singleton,
|
|
1025
|
+
enumerable: true
|
|
1026
|
+
});
|
|
947
1027
|
return ref;
|
|
948
1028
|
}
|
|
949
1029
|
function createWorkflowFactory(plugins = []) {
|
|
950
|
-
const factory = (id, handler, {
|
|
1030
|
+
const factory = (id, handler, {
|
|
1031
|
+
inputSchema,
|
|
1032
|
+
timeout,
|
|
1033
|
+
retries,
|
|
1034
|
+
priority,
|
|
1035
|
+
singleton,
|
|
1036
|
+
schedule,
|
|
1037
|
+
timezone
|
|
1038
|
+
} = {}) => ({
|
|
951
1039
|
id,
|
|
952
1040
|
handler,
|
|
953
1041
|
inputSchema,
|
|
954
1042
|
timeout,
|
|
955
1043
|
retries,
|
|
956
1044
|
priority,
|
|
1045
|
+
singleton,
|
|
957
1046
|
schedule,
|
|
958
1047
|
timezone,
|
|
959
1048
|
plugins: plugins.length > 0 ? plugins : undefined
|
|
@@ -1166,11 +1255,10 @@ class WorkflowEngine {
|
|
|
1166
1255
|
const db = {
|
|
1167
1256
|
executeSql: (text, values) => this.pool.query(text, values)
|
|
1168
1257
|
};
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
}
|
|
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
|
+
});
|
|
1174
1262
|
this.db = this.boss.getDb();
|
|
1175
1263
|
}
|
|
1176
1264
|
async start(asEngine = true, {
|
|
@@ -1224,6 +1312,10 @@ class WorkflowEngine {
|
|
|
1224
1312
|
try {
|
|
1225
1313
|
await this.createWorkflowRun({ workflowId, input: {}, scheduledAt });
|
|
1226
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
|
+
}
|
|
1227
1319
|
this.logger.error(`Schedule fire failed to start a run for workflow "${workflowId}"`, error instanceof Error ? error : new Error(String(error)), { workflowId });
|
|
1228
1320
|
throw error;
|
|
1229
1321
|
}
|
|
@@ -1373,7 +1465,8 @@ class WorkflowEngine {
|
|
|
1373
1465
|
parentRunId,
|
|
1374
1466
|
parentStepId,
|
|
1375
1467
|
parentResourceId,
|
|
1376
|
-
scheduledAt
|
|
1468
|
+
scheduledAt,
|
|
1469
|
+
singleton: workflow2.singleton === true || options?.singleton === true
|
|
1377
1470
|
}, targetDb);
|
|
1378
1471
|
const insertAndEnqueue = async (targetDb) => {
|
|
1379
1472
|
const result = await insertRun(targetDb);
|
|
@@ -1532,6 +1625,9 @@ class WorkflowEngine {
|
|
|
1532
1625
|
}) {
|
|
1533
1626
|
await this.checkIfHasStarted();
|
|
1534
1627
|
const run = await this.getRun({ runId, resourceId });
|
|
1628
|
+
if (run.singleton) {
|
|
1629
|
+
await assertSingletonSlotAvailable({ workflowId: run.workflowId, exceptRunId: run.id }, this.db);
|
|
1630
|
+
}
|
|
1535
1631
|
const jobResourceId = resourceId ?? run.resourceId ?? undefined;
|
|
1536
1632
|
const job = {
|
|
1537
1633
|
runId: run.id,
|
|
@@ -2501,5 +2597,5 @@ function otelPlugin(options = {}) {
|
|
|
2501
2597
|
};
|
|
2502
2598
|
}
|
|
2503
2599
|
|
|
2504
|
-
//# debugId=
|
|
2600
|
+
//# debugId=0BBC9686D69E329D64756E2164756E21
|
|
2505
2601
|
//# sourceMappingURL=index.js.map
|
package/dist/index.d.cts
CHANGED
|
@@ -20,6 +20,13 @@ type WorkflowRun = {
|
|
|
20
20
|
maxRetries: number;
|
|
21
21
|
/** Resolved scheduling priority (pg-boss integer; higher runs first). */
|
|
22
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;
|
|
23
30
|
jobId: string | null;
|
|
24
31
|
idempotencyKey: string | null;
|
|
25
32
|
parentRunId: string | null;
|
|
@@ -76,6 +83,12 @@ type StartWorkflowOptions = {
|
|
|
76
83
|
expireInSeconds?: number;
|
|
77
84
|
idempotencyKey?: string;
|
|
78
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;
|
|
79
92
|
};
|
|
80
93
|
type WorkflowOptions<I extends InputParameters> = {
|
|
81
94
|
timeout?: number;
|
|
@@ -83,6 +96,12 @@ type WorkflowOptions<I extends InputParameters> = {
|
|
|
83
96
|
inputSchema?: I;
|
|
84
97
|
priority?: WorkflowPriority;
|
|
85
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;
|
|
104
|
+
/**
|
|
86
105
|
* Recurring schedule. Accepts a cron expression (`'0 9 * * 1-5'`),
|
|
87
106
|
* a duration string (`'5m'`, `'1 hour'`), or a `DurationObject`.
|
|
88
107
|
*/
|
|
@@ -190,6 +209,7 @@ type WorkflowDefinition<TInput extends InputParameters = InputParameters> = {
|
|
|
190
209
|
timeout?: number;
|
|
191
210
|
retries?: number;
|
|
192
211
|
priority?: WorkflowPriority;
|
|
212
|
+
singleton?: boolean;
|
|
193
213
|
schedule?: Schedule;
|
|
194
214
|
timezone?: string;
|
|
195
215
|
plugins?: WorkflowPlugin[];
|
|
@@ -216,6 +236,7 @@ interface WorkflowFactory<TStepExt = object> {
|
|
|
216
236
|
TOutput = unknown
|
|
217
237
|
>(id: string, options?: {
|
|
218
238
|
inputSchema?: TInput;
|
|
239
|
+
singleton?: boolean;
|
|
219
240
|
}): WorkflowRef<TInput, TOutput>;
|
|
220
241
|
}
|
|
221
242
|
/**
|
|
@@ -232,6 +253,7 @@ interface WorkflowRef<
|
|
|
232
253
|
(handler: (context: WorkflowContext<TInput, StepBaseContext>) => Promise<unknown>, options?: Omit<WorkflowOptions<TInput>, "inputSchema">): WorkflowDefinition<TInput>;
|
|
233
254
|
readonly id: string;
|
|
234
255
|
readonly inputSchema?: TInput;
|
|
256
|
+
readonly singleton?: boolean;
|
|
235
257
|
}
|
|
236
258
|
type WorkflowRunProgress = WorkflowRun & {
|
|
237
259
|
completionPercentage: number;
|
|
@@ -338,6 +360,7 @@ declare function createWorkflowRef<
|
|
|
338
360
|
TInput extends InputParameters = InputParameters
|
|
339
361
|
>(id: string, options?: {
|
|
340
362
|
inputSchema?: TInput;
|
|
363
|
+
singleton?: boolean;
|
|
341
364
|
}): WorkflowRef<TInput, TOutput>;
|
|
342
365
|
declare const workflow: WorkflowFactory;
|
|
343
366
|
import pg2 from "pg";
|
|
@@ -511,6 +534,9 @@ declare class WorkflowEngineError extends Error {
|
|
|
511
534
|
declare class WorkflowRunNotFoundError extends WorkflowEngineError {
|
|
512
535
|
constructor(runId?: string, workflowId?: string);
|
|
513
536
|
}
|
|
537
|
+
declare class WorkflowRunInProgressError extends WorkflowEngineError {
|
|
538
|
+
constructor(workflowId: string, runId?: string);
|
|
539
|
+
}
|
|
514
540
|
import { AttributeValue, Tracer } from "@opentelemetry/api";
|
|
515
541
|
type OtelPluginOptions = {
|
|
516
542
|
/** Tracer to use. Defaults to `trace.getTracer('pg-workflows')`. */
|
|
@@ -521,4 +547,4 @@ type OtelPluginOptions = {
|
|
|
521
547
|
attributes?: (context: WorkflowContext) => Record<string, AttributeValue>;
|
|
522
548
|
};
|
|
523
549
|
declare function otelPlugin(options?: OtelPluginOptions): WorkflowPlugin<StepBaseContext, object>;
|
|
524
|
-
export { workflow, otelPlugin, createWorkflowRef, WorkflowStatus, WorkflowRunProgress, WorkflowRunNotFoundError, WorkflowRun, WorkflowRef, WorkflowPriority, 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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,13 @@ type WorkflowRun = {
|
|
|
20
20
|
maxRetries: number;
|
|
21
21
|
/** Resolved scheduling priority (pg-boss integer; higher runs first). */
|
|
22
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;
|
|
23
30
|
jobId: string | null;
|
|
24
31
|
idempotencyKey: string | null;
|
|
25
32
|
parentRunId: string | null;
|
|
@@ -76,6 +83,12 @@ type StartWorkflowOptions = {
|
|
|
76
83
|
expireInSeconds?: number;
|
|
77
84
|
idempotencyKey?: string;
|
|
78
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;
|
|
79
92
|
};
|
|
80
93
|
type WorkflowOptions<I extends InputParameters> = {
|
|
81
94
|
timeout?: number;
|
|
@@ -83,6 +96,12 @@ type WorkflowOptions<I extends InputParameters> = {
|
|
|
83
96
|
inputSchema?: I;
|
|
84
97
|
priority?: WorkflowPriority;
|
|
85
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;
|
|
104
|
+
/**
|
|
86
105
|
* Recurring schedule. Accepts a cron expression (`'0 9 * * 1-5'`),
|
|
87
106
|
* a duration string (`'5m'`, `'1 hour'`), or a `DurationObject`.
|
|
88
107
|
*/
|
|
@@ -190,6 +209,7 @@ type WorkflowDefinition<TInput extends InputParameters = InputParameters> = {
|
|
|
190
209
|
timeout?: number;
|
|
191
210
|
retries?: number;
|
|
192
211
|
priority?: WorkflowPriority;
|
|
212
|
+
singleton?: boolean;
|
|
193
213
|
schedule?: Schedule;
|
|
194
214
|
timezone?: string;
|
|
195
215
|
plugins?: WorkflowPlugin[];
|
|
@@ -216,6 +236,7 @@ interface WorkflowFactory<TStepExt = object> {
|
|
|
216
236
|
TOutput = unknown
|
|
217
237
|
>(id: string, options?: {
|
|
218
238
|
inputSchema?: TInput;
|
|
239
|
+
singleton?: boolean;
|
|
219
240
|
}): WorkflowRef<TInput, TOutput>;
|
|
220
241
|
}
|
|
221
242
|
/**
|
|
@@ -232,6 +253,7 @@ interface WorkflowRef<
|
|
|
232
253
|
(handler: (context: WorkflowContext<TInput, StepBaseContext>) => Promise<unknown>, options?: Omit<WorkflowOptions<TInput>, "inputSchema">): WorkflowDefinition<TInput>;
|
|
233
254
|
readonly id: string;
|
|
234
255
|
readonly inputSchema?: TInput;
|
|
256
|
+
readonly singleton?: boolean;
|
|
235
257
|
}
|
|
236
258
|
type WorkflowRunProgress = WorkflowRun & {
|
|
237
259
|
completionPercentage: number;
|
|
@@ -338,6 +360,7 @@ declare function createWorkflowRef<
|
|
|
338
360
|
TInput extends InputParameters = InputParameters
|
|
339
361
|
>(id: string, options?: {
|
|
340
362
|
inputSchema?: TInput;
|
|
363
|
+
singleton?: boolean;
|
|
341
364
|
}): WorkflowRef<TInput, TOutput>;
|
|
342
365
|
declare const workflow: WorkflowFactory;
|
|
343
366
|
import pg2 from "pg";
|
|
@@ -511,6 +534,9 @@ declare class WorkflowEngineError extends Error {
|
|
|
511
534
|
declare class WorkflowRunNotFoundError extends WorkflowEngineError {
|
|
512
535
|
constructor(runId?: string, workflowId?: string);
|
|
513
536
|
}
|
|
537
|
+
declare class WorkflowRunInProgressError extends WorkflowEngineError {
|
|
538
|
+
constructor(workflowId: string, runId?: string);
|
|
539
|
+
}
|
|
514
540
|
import { AttributeValue, Tracer } from "@opentelemetry/api";
|
|
515
541
|
type OtelPluginOptions = {
|
|
516
542
|
/** Tracer to use. Defaults to `trace.getTracer('pg-workflows')`. */
|
|
@@ -521,4 +547,4 @@ type OtelPluginOptions = {
|
|
|
521
547
|
attributes?: (context: WorkflowContext) => Record<string, AttributeValue>;
|
|
522
548
|
};
|
|
523
549
|
declare function otelPlugin(options?: OtelPluginOptions): WorkflowPlugin<StepBaseContext, object>;
|
|
524
|
-
export { workflow, otelPlugin, createWorkflowRef, WorkflowStatus, WorkflowRunProgress, WorkflowRunNotFoundError, WorkflowRun, WorkflowRef, WorkflowPriority, 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 };
|
package/dist/index.js
CHANGED
|
@@ -6,8 +6,10 @@ import {
|
|
|
6
6
|
WORKFLOW_RUN_QUEUE_NAME,
|
|
7
7
|
WorkflowClient,
|
|
8
8
|
WorkflowEngineError,
|
|
9
|
+
WorkflowRunInProgressError,
|
|
9
10
|
WorkflowRunNotFoundError,
|
|
10
11
|
WorkflowStatus,
|
|
12
|
+
assertSingletonSlotAvailable,
|
|
11
13
|
createWorkflowRef,
|
|
12
14
|
getWorkflowLastRun,
|
|
13
15
|
getWorkflowRun,
|
|
@@ -24,7 +26,7 @@ import {
|
|
|
24
26
|
waitForTimelineKey,
|
|
25
27
|
withPostgresTransaction,
|
|
26
28
|
workflow
|
|
27
|
-
} from "./shared/chunk-
|
|
29
|
+
} from "./shared/chunk-n0f7hd40.js";
|
|
28
30
|
// src/engine.ts
|
|
29
31
|
import { merge } from "es-toolkit";
|
|
30
32
|
import pg from "pg";
|
|
@@ -225,11 +227,10 @@ class WorkflowEngine {
|
|
|
225
227
|
const db = {
|
|
226
228
|
executeSql: (text, values) => this.pool.query(text, values)
|
|
227
229
|
};
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
230
|
+
this.boss = boss ?? new PgBoss({ db, schema: DEFAULT_PGBOSS_SCHEMA });
|
|
231
|
+
this.boss.on("error", (error) => {
|
|
232
|
+
this.logger.error("pg-boss error", error);
|
|
233
|
+
});
|
|
233
234
|
this.db = this.boss.getDb();
|
|
234
235
|
}
|
|
235
236
|
async start(asEngine = true, {
|
|
@@ -283,6 +284,10 @@ class WorkflowEngine {
|
|
|
283
284
|
try {
|
|
284
285
|
await this.createWorkflowRun({ workflowId, input: {}, scheduledAt });
|
|
285
286
|
} catch (error) {
|
|
287
|
+
if (error instanceof WorkflowRunInProgressError) {
|
|
288
|
+
this.logger.log(`Skipping schedule fire for "${workflowId}": a run is already in progress`, { workflowId, runId: error.runId });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
286
291
|
this.logger.error(`Schedule fire failed to start a run for workflow "${workflowId}"`, error instanceof Error ? error : new Error(String(error)), { workflowId });
|
|
287
292
|
throw error;
|
|
288
293
|
}
|
|
@@ -432,7 +437,8 @@ class WorkflowEngine {
|
|
|
432
437
|
parentRunId,
|
|
433
438
|
parentStepId,
|
|
434
439
|
parentResourceId,
|
|
435
|
-
scheduledAt
|
|
440
|
+
scheduledAt,
|
|
441
|
+
singleton: workflow2.singleton === true || options?.singleton === true
|
|
436
442
|
}, targetDb);
|
|
437
443
|
const insertAndEnqueue = async (targetDb) => {
|
|
438
444
|
const result = await insertRun(targetDb);
|
|
@@ -591,6 +597,9 @@ class WorkflowEngine {
|
|
|
591
597
|
}) {
|
|
592
598
|
await this.checkIfHasStarted();
|
|
593
599
|
const run = await this.getRun({ runId, resourceId });
|
|
600
|
+
if (run.singleton) {
|
|
601
|
+
await assertSingletonSlotAvailable({ workflowId: run.workflowId, exceptRunId: run.id }, this.db);
|
|
602
|
+
}
|
|
594
603
|
const jobResourceId = resourceId ?? run.resourceId ?? undefined;
|
|
595
604
|
const job = {
|
|
596
605
|
runId: run.id,
|
|
@@ -1569,10 +1578,11 @@ export {
|
|
|
1569
1578
|
createWorkflowRef,
|
|
1570
1579
|
WorkflowStatus,
|
|
1571
1580
|
WorkflowRunNotFoundError,
|
|
1581
|
+
WorkflowRunInProgressError,
|
|
1572
1582
|
WorkflowEngineError,
|
|
1573
1583
|
WorkflowEngine,
|
|
1574
1584
|
WorkflowClient
|
|
1575
1585
|
};
|
|
1576
1586
|
|
|
1577
|
-
//# debugId=
|
|
1587
|
+
//# debugId=4EA8624FFA9819BE64756E2164756E21
|
|
1578
1588
|
//# sourceMappingURL=index.js.map
|