pg-workflows 0.6.1 → 0.7.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 +122 -6
- package/dist/index.cjs +111 -54
- package/dist/index.d.cts +15 -16
- package/dist/index.d.ts +15 -16
- package/dist/index.js +111 -54
- package/dist/index.js.map +8 -8
- package/package.json +8 -12
package/README.md
CHANGED
|
@@ -51,7 +51,8 @@ If you need enterprise-grade features like distributed tracing, complex DAG sche
|
|
|
51
51
|
- **Built-in Retries** - Automatic retries with exponential backoff at the workflow level.
|
|
52
52
|
- **Configurable Timeouts** - Set workflow-level and step-level timeouts to prevent runaway executions.
|
|
53
53
|
- **Progress Tracking** - Monitor workflow completion percentage, completed steps, and total steps in real-time.
|
|
54
|
-
- **Input Validation** - Define schemas with Zod for type-safe, validated workflow inputs.
|
|
54
|
+
- **Input Validation** - Define schemas with any [Standard Schema](https://github.com/standard-schema/standard-schema)-compliant library (Zod, Valibot, ArkType, etc.) for type-safe, validated workflow inputs.
|
|
55
|
+
- **Idempotent starts** - Optional `idempotencyKey` on `startWorkflow()` so duplicate API calls or retries return the same run instead of enqueueing another job.
|
|
55
56
|
- **Built on pg-boss** - Leverages the battle-tested [pg-boss](https://github.com/timgit/pg-boss) job queue for reliable task scheduling. pg-boss is bundled as a dependency - no separate install or configuration needed.
|
|
56
57
|
|
|
57
58
|
---
|
|
@@ -151,6 +152,8 @@ const run = await engine.startWorkflow({
|
|
|
151
152
|
workflowId: 'send-welcome-email',
|
|
152
153
|
resourceId: 'user-123',
|
|
153
154
|
input: { email: 'user@example.com' },
|
|
155
|
+
// Optional: same key returns the existing run (deduplicates double-submits / retries)
|
|
156
|
+
idempotencyKey: 'welcome:user-123',
|
|
154
157
|
});
|
|
155
158
|
|
|
156
159
|
// Send an event to resume the waiting workflow
|
|
@@ -357,8 +360,8 @@ const myWorkflow = workflow(
|
|
|
357
360
|
// Your workflow logic here
|
|
358
361
|
},
|
|
359
362
|
{
|
|
360
|
-
inputSchema:
|
|
361
|
-
timeout: 60000,
|
|
363
|
+
inputSchema: mySchema, // any Standard Schema-compliant schema
|
|
364
|
+
timeout: 60000, // milliseconds
|
|
362
365
|
retries: 3,
|
|
363
366
|
}
|
|
364
367
|
);
|
|
@@ -426,6 +429,29 @@ const { items } = await engine.getRuns({
|
|
|
426
429
|
});
|
|
427
430
|
```
|
|
428
431
|
|
|
432
|
+
### Idempotency key
|
|
433
|
+
|
|
434
|
+
Pass an optional `idempotencyKey` to `startWorkflow()` when the same logical start might be requested more than once (user double-clicks, API retries, or at-least-once webhooks). The engine stores the key on the run; a second `startWorkflow` with the **same** key returns the **existing** run and does **not** enqueue a second job.
|
|
435
|
+
|
|
436
|
+
Keys are **globally unique** in the database (up to 256 characters), not scoped per workflow or resource. Prefer stable, namespaced strings so different workflows never collide—for example `send-welcome-email:order-123` instead of a bare order id.
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
const run = await engine.startWorkflow({
|
|
440
|
+
workflowId: 'send-welcome-email',
|
|
441
|
+
input: { email: 'user@example.com' },
|
|
442
|
+
idempotencyKey: 'send-welcome-email:checkout-session_cs_abc123',
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// Idempotent: returns the same run and run.id as above
|
|
446
|
+
const again = await engine.startWorkflow({
|
|
447
|
+
workflowId: 'send-welcome-email',
|
|
448
|
+
input: { email: 'other@example.com' }, // ignored for deduplication — existing run wins
|
|
449
|
+
idempotencyKey: 'send-welcome-email:checkout-session_cs_abc123',
|
|
450
|
+
});
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
The returned `WorkflowRun` includes `idempotencyKey` (or `null` if omitted).
|
|
454
|
+
|
|
429
455
|
### Pause and Resume
|
|
430
456
|
|
|
431
457
|
Manually pause a workflow and resume it later:
|
|
@@ -476,6 +502,96 @@ await engine.fastForwardWorkflow({
|
|
|
476
502
|
| `step.poll()` | Writes `data` as the poll result and triggers resolution |
|
|
477
503
|
| `step.pause()` | Delegates to `resumeWorkflow()` |
|
|
478
504
|
|
|
505
|
+
### Input Validation
|
|
506
|
+
|
|
507
|
+
pg-workflows supports any [Standard Schema](https://github.com/standard-schema/standard-schema)-compliant validation library for `inputSchema`. This means you can use Zod, Valibot, ArkType, or any library that implements the Standard Schema spec. When a schema is provided, the workflow input is validated before execution and the handler's `input` parameter is fully typed.
|
|
508
|
+
|
|
509
|
+
#### With Zod
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
import { workflow } from 'pg-workflows';
|
|
513
|
+
import { z } from 'zod';
|
|
514
|
+
|
|
515
|
+
const myWorkflow = workflow(
|
|
516
|
+
'user-onboarding',
|
|
517
|
+
async ({ step, input }) => {
|
|
518
|
+
// input is typed as { email: string; name: string }
|
|
519
|
+
await step.run('send-welcome', async () => {
|
|
520
|
+
return await sendEmail(input.email, `Welcome, ${input.name}!`);
|
|
521
|
+
});
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
inputSchema: z.object({
|
|
525
|
+
email: z.string().email(),
|
|
526
|
+
name: z.string(),
|
|
527
|
+
}),
|
|
528
|
+
}
|
|
529
|
+
);
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
#### With Valibot
|
|
533
|
+
|
|
534
|
+
```typescript
|
|
535
|
+
import { workflow } from 'pg-workflows';
|
|
536
|
+
import * as v from 'valibot';
|
|
537
|
+
|
|
538
|
+
const myWorkflow = workflow(
|
|
539
|
+
'user-onboarding',
|
|
540
|
+
async ({ step, input }) => {
|
|
541
|
+
// input is typed as { email: string; name: string }
|
|
542
|
+
await step.run('send-welcome', async () => {
|
|
543
|
+
return await sendEmail(input.email, `Welcome, ${input.name}!`);
|
|
544
|
+
});
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
inputSchema: v.object({
|
|
548
|
+
email: v.pipe(v.string(), v.email()),
|
|
549
|
+
name: v.string(),
|
|
550
|
+
}),
|
|
551
|
+
}
|
|
552
|
+
);
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
#### Without a Schema
|
|
556
|
+
|
|
557
|
+
When no `inputSchema` is provided, input is not validated and `input` is typed as `unknown`. This is because the engine has no guarantee about the shape of the data — it passes through whatever was provided to `startWorkflow()`. You are responsible for narrowing the type yourself, either with a type assertion or runtime checks:
|
|
558
|
+
|
|
559
|
+
```typescript
|
|
560
|
+
import { workflow } from 'pg-workflows';
|
|
561
|
+
|
|
562
|
+
const myWorkflow = workflow(
|
|
563
|
+
'process-order',
|
|
564
|
+
async ({ step, input }) => {
|
|
565
|
+
// Option 1: Type assertion — you trust the caller
|
|
566
|
+
const { orderId, amount } = input as { orderId: string; amount: number };
|
|
567
|
+
|
|
568
|
+
await step.run('charge', async () => {
|
|
569
|
+
return await chargeOrder(orderId, amount);
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
const myDefensiveWorkflow = workflow(
|
|
575
|
+
'process-order-safe',
|
|
576
|
+
async ({ step, input }) => {
|
|
577
|
+
// Option 2: Runtime checks — you verify before using
|
|
578
|
+
if (typeof input !== 'object' || input === null) {
|
|
579
|
+
throw new Error('Expected input to be an object');
|
|
580
|
+
}
|
|
581
|
+
const { orderId, amount } = input as Record<string, unknown>;
|
|
582
|
+
if (typeof orderId !== 'string' || typeof amount !== 'number') {
|
|
583
|
+
throw new Error('Invalid input shape');
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
await step.run('charge', async () => {
|
|
587
|
+
return await chargeOrder(orderId, amount);
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
);
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
Using an `inputSchema` is recommended — it validates input at the engine boundary before your handler runs, and gives you full type inference with no manual work.
|
|
594
|
+
|
|
479
595
|
---
|
|
480
596
|
|
|
481
597
|
## Examples
|
|
@@ -618,7 +734,7 @@ When `boss` is omitted, pg-boss is created automatically with an isolated schema
|
|
|
618
734
|
| `start(asEngine?, options?)` | Start the engine and workers |
|
|
619
735
|
| `stop()` | Stop the engine gracefully |
|
|
620
736
|
| `registerWorkflow(definition)` | Register a workflow definition |
|
|
621
|
-
| `startWorkflow({ workflowId, resourceId?, input, options? })` | Start a new workflow run. `resourceId` optionally ties the run to an external entity (see [Resource ID](#resource-id)). |
|
|
737
|
+
| `startWorkflow({ workflowId, resourceId?, input, idempotencyKey?, options? })` | Start a new workflow run. `resourceId` optionally ties the run to an external entity (see [Resource ID](#resource-id)). `idempotencyKey` optionally deduplicates starts (see [Idempotency key](#idempotency-key)). |
|
|
622
738
|
| `pauseWorkflow({ runId, resourceId? })` | Pause a running workflow |
|
|
623
739
|
| `resumeWorkflow({ runId, resourceId?, options? })` | Resume a paused workflow |
|
|
624
740
|
| `cancelWorkflow({ runId, resourceId? })` | Cancel a workflow |
|
|
@@ -699,7 +815,7 @@ enum WorkflowStatus {
|
|
|
699
815
|
|
|
700
816
|
The engine automatically runs migrations on startup to create the required tables:
|
|
701
817
|
|
|
702
|
-
- `workflow_runs` - Stores workflow execution state, step results, and timeline in the `public` schema. The optional `resource_id` column (indexed) associates each run with an external entity in your application. See [Resource ID](#resource-id).
|
|
818
|
+
- `workflow_runs` - Stores workflow execution state, step results, and timeline in the `public` schema. The optional `resource_id` column (indexed) associates each run with an external entity in your application. See [Resource ID](#resource-id). The optional `idempotency_key` column has a unique partial index for [idempotent starts](#idempotency-key).
|
|
703
819
|
- `pgboss_v12_pgworkflow.*` - pg-boss job queue tables for reliable task scheduling (isolated schema to avoid conflicts)
|
|
704
820
|
|
|
705
821
|
---
|
|
@@ -726,7 +842,7 @@ npm install pg-workflows pg
|
|
|
726
842
|
- Node.js >= 18.0.0
|
|
727
843
|
- PostgreSQL >= 10
|
|
728
844
|
- `pg` >= 8.0.0 (peer dependency)
|
|
729
|
-
-
|
|
845
|
+
- A [Standard Schema](https://github.com/standard-schema/standard-schema)-compliant validation library (Zod, Valibot, ArkType, etc.) if using `inputSchema`
|
|
730
846
|
|
|
731
847
|
## Acknowledgments
|
|
732
848
|
|
package/dist/index.cjs
CHANGED
|
@@ -99,11 +99,13 @@ class WorkflowEngineError extends Error {
|
|
|
99
99
|
workflowId;
|
|
100
100
|
runId;
|
|
101
101
|
cause;
|
|
102
|
-
|
|
102
|
+
issues;
|
|
103
|
+
constructor(message, workflowId, runId, cause = undefined, issues) {
|
|
103
104
|
super(message);
|
|
104
105
|
this.workflowId = workflowId;
|
|
105
106
|
this.runId = runId;
|
|
106
107
|
this.cause = cause;
|
|
108
|
+
this.issues = issues;
|
|
107
109
|
this.name = "WorkflowEngineError";
|
|
108
110
|
if (Error.captureStackTrace) {
|
|
109
111
|
Error.captureStackTrace(this, WorkflowEngineError);
|
|
@@ -240,17 +242,17 @@ function parseWorkflowHandler(handler) {
|
|
|
240
242
|
}
|
|
241
243
|
|
|
242
244
|
// src/db/migration.ts
|
|
245
|
+
var MIGRATION_LOCK_ID = 738291645;
|
|
246
|
+
var CURRENT_SCHEMA_VERSION = 2;
|
|
243
247
|
async function runMigrations(db) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
await db.executeSql(`
|
|
253
|
-
CREATE TABLE workflow_runs (
|
|
248
|
+
if (await isSchemaUpToDate(db)) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const currentVersion = await getCurrentVersion(db);
|
|
252
|
+
const commands = [];
|
|
253
|
+
if (currentVersion < 1) {
|
|
254
|
+
commands.push(`
|
|
255
|
+
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
254
256
|
id varchar(32) PRIMARY KEY NOT NULL,
|
|
255
257
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
256
258
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
@@ -269,25 +271,66 @@ async function runMigrations(db) {
|
|
|
269
271
|
retry_count integer DEFAULT 0 NOT NULL,
|
|
270
272
|
max_retries integer DEFAULT 0 NOT NULL,
|
|
271
273
|
job_id varchar(256)
|
|
272
|
-
)
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
CREATE INDEX workflow_runs_created_at_idx ON workflow_runs USING btree (created_at)
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
CREATE INDEX
|
|
279
|
-
|
|
280
|
-
|
|
274
|
+
)
|
|
275
|
+
`);
|
|
276
|
+
commands.push(`
|
|
277
|
+
CREATE INDEX IF NOT EXISTS workflow_runs_created_at_idx ON workflow_runs USING btree (created_at)
|
|
278
|
+
`);
|
|
279
|
+
commands.push(`
|
|
280
|
+
CREATE INDEX IF NOT EXISTS workflow_runs_resource_id_created_at_idx ON workflow_runs USING btree (resource_id, created_at DESC)
|
|
281
|
+
`);
|
|
282
|
+
commands.push(`
|
|
283
|
+
CREATE INDEX IF NOT EXISTS workflow_runs_status_created_at_idx ON workflow_runs USING btree (status, created_at DESC)
|
|
284
|
+
`);
|
|
285
|
+
commands.push(`
|
|
286
|
+
CREATE INDEX IF NOT EXISTS workflow_runs_workflow_id_created_at_idx ON workflow_runs USING btree (workflow_id, created_at DESC)
|
|
287
|
+
`);
|
|
288
|
+
commands.push(`
|
|
289
|
+
CREATE INDEX IF NOT EXISTS workflow_runs_resource_id_workflow_id_created_at_idx ON workflow_runs USING btree (resource_id, workflow_id, created_at DESC)
|
|
290
|
+
`);
|
|
291
|
+
}
|
|
292
|
+
if (currentVersion < 2) {
|
|
293
|
+
commands.push("DROP INDEX IF EXISTS workflow_runs_workflow_id_idx");
|
|
294
|
+
commands.push("DROP INDEX IF EXISTS workflow_runs_resource_id_idx");
|
|
295
|
+
commands.push("ALTER TABLE workflow_runs ADD COLUMN IF NOT EXISTS idempotency_key varchar(256)");
|
|
296
|
+
commands.push(`
|
|
297
|
+
CREATE UNIQUE INDEX IF NOT EXISTS workflow_runs_idempotency_key_idx ON workflow_runs (idempotency_key) WHERE idempotency_key IS NOT NULL
|
|
298
|
+
`);
|
|
299
|
+
}
|
|
300
|
+
if (currentVersion === 0) {
|
|
301
|
+
commands.push(`INSERT INTO workflow_schema_version (version) VALUES (${CURRENT_SCHEMA_VERSION})`);
|
|
281
302
|
} else {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
303
|
+
commands.push(`UPDATE workflow_schema_version SET version = ${CURRENT_SCHEMA_VERSION}`);
|
|
304
|
+
}
|
|
305
|
+
if (commands.length === 0) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const sql = [
|
|
309
|
+
"BEGIN",
|
|
310
|
+
"SET LOCAL lock_timeout = '30s'",
|
|
311
|
+
"SET LOCAL idle_in_transaction_session_timeout = '30s'",
|
|
312
|
+
`SELECT pg_advisory_xact_lock(${MIGRATION_LOCK_ID})`,
|
|
313
|
+
"CREATE TABLE IF NOT EXISTS workflow_schema_version (version integer NOT NULL)",
|
|
314
|
+
...commands,
|
|
315
|
+
"COMMIT"
|
|
316
|
+
].join(`;
|
|
317
|
+
`);
|
|
318
|
+
await db.executeSql(sql, []);
|
|
319
|
+
}
|
|
320
|
+
async function isSchemaUpToDate(db) {
|
|
321
|
+
try {
|
|
322
|
+
const result = await db.executeSql("SELECT version FROM workflow_schema_version LIMIT 1", []);
|
|
323
|
+
return (result.rows[0]?.version ?? 0) >= CURRENT_SCHEMA_VERSION;
|
|
324
|
+
} catch {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
async function getCurrentVersion(db) {
|
|
329
|
+
try {
|
|
330
|
+
const result = await db.executeSql("SELECT version FROM workflow_schema_version LIMIT 1", []);
|
|
331
|
+
return result.rows[0]?.version ?? 0;
|
|
332
|
+
} catch {
|
|
333
|
+
return 0;
|
|
291
334
|
}
|
|
292
335
|
}
|
|
293
336
|
|
|
@@ -315,7 +358,8 @@ function mapRowToWorkflowRun(row) {
|
|
|
315
358
|
timeoutAt: row.timeout_at ? new Date(row.timeout_at) : null,
|
|
316
359
|
retryCount: row.retry_count,
|
|
317
360
|
maxRetries: row.max_retries,
|
|
318
|
-
jobId: row.job_id
|
|
361
|
+
jobId: row.job_id,
|
|
362
|
+
idempotencyKey: row.idempotency_key
|
|
319
363
|
};
|
|
320
364
|
}
|
|
321
365
|
async function insertWorkflowRun({
|
|
@@ -325,7 +369,8 @@ async function insertWorkflowRun({
|
|
|
325
369
|
status,
|
|
326
370
|
input,
|
|
327
371
|
maxRetries,
|
|
328
|
-
timeoutAt
|
|
372
|
+
timeoutAt,
|
|
373
|
+
idempotencyKey
|
|
329
374
|
}, db) {
|
|
330
375
|
const runId = generateKSUID("run");
|
|
331
376
|
const now = new Date;
|
|
@@ -341,9 +386,11 @@ async function insertWorkflowRun({
|
|
|
341
386
|
created_at,
|
|
342
387
|
updated_at,
|
|
343
388
|
timeline,
|
|
344
|
-
retry_count
|
|
389
|
+
retry_count,
|
|
390
|
+
idempotency_key
|
|
345
391
|
)
|
|
346
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
|
392
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
|
393
|
+
ON CONFLICT (idempotency_key) WHERE idempotency_key IS NOT NULL DO NOTHING
|
|
347
394
|
RETURNING *`, [
|
|
348
395
|
runId,
|
|
349
396
|
resourceId ?? null,
|
|
@@ -356,13 +403,19 @@ async function insertWorkflowRun({
|
|
|
356
403
|
now,
|
|
357
404
|
now,
|
|
358
405
|
"{}",
|
|
359
|
-
0
|
|
406
|
+
0,
|
|
407
|
+
idempotencyKey ?? null
|
|
408
|
+
]);
|
|
409
|
+
if (result.rows[0]) {
|
|
410
|
+
return { run: mapRowToWorkflowRun(result.rows[0]), created: true };
|
|
411
|
+
}
|
|
412
|
+
const existing = await db.executeSql("SELECT * FROM workflow_runs WHERE idempotency_key = $1", [
|
|
413
|
+
idempotencyKey
|
|
360
414
|
]);
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
throw new Error("Failed to insert workflow run");
|
|
415
|
+
if (!existing.rows[0]) {
|
|
416
|
+
throw new Error(`Idempotency conflict: existing run not found for key "${idempotencyKey}"`);
|
|
364
417
|
}
|
|
365
|
-
return mapRowToWorkflowRun(
|
|
418
|
+
return { run: mapRowToWorkflowRun(existing.rows[0]), created: false };
|
|
366
419
|
}
|
|
367
420
|
async function getWorkflowRun({
|
|
368
421
|
runId,
|
|
@@ -682,6 +735,7 @@ class WorkflowEngine {
|
|
|
682
735
|
resourceId,
|
|
683
736
|
workflowId,
|
|
684
737
|
input,
|
|
738
|
+
idempotencyKey,
|
|
685
739
|
options
|
|
686
740
|
}) {
|
|
687
741
|
if (!this._started) {
|
|
@@ -697,33 +751,36 @@ class WorkflowEngine {
|
|
|
697
751
|
throw new WorkflowEngineError(`Workflow ${workflowId} has no steps`, workflowId);
|
|
698
752
|
}
|
|
699
753
|
if (workflow2.inputSchema) {
|
|
700
|
-
const result = workflow2.inputSchema.
|
|
701
|
-
if (
|
|
702
|
-
throw new WorkflowEngineError(result.
|
|
754
|
+
const result = await workflow2.inputSchema["~standard"].validate(input);
|
|
755
|
+
if (result.issues) {
|
|
756
|
+
throw new WorkflowEngineError(JSON.stringify(result.issues), workflowId, undefined, undefined, result.issues);
|
|
703
757
|
}
|
|
704
758
|
}
|
|
705
759
|
const initialStepId = workflow2.steps[0]?.id ?? "__start__";
|
|
706
760
|
const run = await withPostgresTransaction(this.boss.getDb(), async (_db) => {
|
|
707
761
|
const timeoutAt = options?.timeout ? new Date(Date.now() + options.timeout) : workflow2.timeout ? new Date(Date.now() + workflow2.timeout) : null;
|
|
708
|
-
const insertedRun = await insertWorkflowRun({
|
|
762
|
+
const { run: insertedRun, created } = await insertWorkflowRun({
|
|
709
763
|
resourceId,
|
|
710
764
|
workflowId,
|
|
711
765
|
currentStepId: initialStepId,
|
|
712
766
|
status: "running" /* RUNNING */,
|
|
713
767
|
input,
|
|
714
768
|
maxRetries: options?.retries ?? workflow2.retries ?? 0,
|
|
715
|
-
timeoutAt
|
|
769
|
+
timeoutAt,
|
|
770
|
+
idempotencyKey
|
|
716
771
|
}, _db);
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
772
|
+
if (created) {
|
|
773
|
+
const job = {
|
|
774
|
+
runId: insertedRun.id,
|
|
775
|
+
resourceId,
|
|
776
|
+
workflowId,
|
|
777
|
+
input
|
|
778
|
+
};
|
|
779
|
+
await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
|
|
780
|
+
startAfter: new Date,
|
|
781
|
+
expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds
|
|
782
|
+
});
|
|
783
|
+
}
|
|
727
784
|
return insertedRun;
|
|
728
785
|
}, this.pool);
|
|
729
786
|
this.logger.log("Started workflow run", {
|
|
@@ -1423,5 +1480,5 @@ ${error.stack}` : String(error)
|
|
|
1423
1480
|
}
|
|
1424
1481
|
}
|
|
1425
1482
|
|
|
1426
|
-
//# debugId=
|
|
1483
|
+
//# debugId=644364C840A9480364756E2164756E21
|
|
1427
1484
|
//# sourceMappingURL=index.js.map
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
2
|
type WorkflowRun = {
|
|
3
3
|
id: string;
|
|
4
4
|
createdAt: Date;
|
|
@@ -18,6 +18,7 @@ type WorkflowRun = {
|
|
|
18
18
|
retryCount: number;
|
|
19
19
|
maxRetries: number;
|
|
20
20
|
jobId: string | null;
|
|
21
|
+
idempotencyKey: string | null;
|
|
21
22
|
};
|
|
22
23
|
type DurationObject = {
|
|
23
24
|
weeks?: number;
|
|
@@ -44,8 +45,8 @@ declare enum StepType {
|
|
|
44
45
|
DELAY = "delay",
|
|
45
46
|
POLL = "poll"
|
|
46
47
|
}
|
|
47
|
-
type InputParameters =
|
|
48
|
-
type InferInputParameters<P extends InputParameters> =
|
|
48
|
+
type InputParameters = StandardSchemaV1;
|
|
49
|
+
type InferInputParameters<P extends InputParameters> = StandardSchemaV1.InferOutput<P>;
|
|
49
50
|
type WorkflowOptions<I extends InputParameters> = {
|
|
50
51
|
timeout?: number;
|
|
51
52
|
retries?: number;
|
|
@@ -109,12 +110,10 @@ type WorkflowContext<
|
|
|
109
110
|
timeline: Record<string, unknown>;
|
|
110
111
|
logger: WorkflowLogger;
|
|
111
112
|
};
|
|
112
|
-
type WorkflowDefinition<
|
|
113
|
-
TInput extends InputParameters = InputParameters,
|
|
114
|
-
TStep extends StepBaseContext = StepBaseContext
|
|
115
|
-
> = {
|
|
113
|
+
type WorkflowDefinition<TInput extends InputParameters = InputParameters> = {
|
|
116
114
|
id: string;
|
|
117
|
-
|
|
115
|
+
/** Widest context avoids contravariance when collecting definitions; `workflow()` still types the handler narrowly. */
|
|
116
|
+
handler: (context: WorkflowContext<InputParameters, StepBaseContext>) => Promise<unknown>;
|
|
118
117
|
inputSchema?: TInput;
|
|
119
118
|
timeout?: number;
|
|
120
119
|
retries?: number;
|
|
@@ -127,10 +126,7 @@ type StepInternalDefinition = {
|
|
|
127
126
|
loop: boolean;
|
|
128
127
|
isDynamic: boolean;
|
|
129
128
|
};
|
|
130
|
-
type WorkflowInternalDefinition<
|
|
131
|
-
TInput extends InputParameters = InputParameters,
|
|
132
|
-
TStep extends StepBaseContext = StepBaseContext
|
|
133
|
-
> = WorkflowDefinition<TInput, TStep> & {
|
|
129
|
+
type WorkflowInternalDefinition<TInput extends InputParameters = InputParameters> = WorkflowDefinition<TInput> & {
|
|
134
130
|
steps: StepInternalDefinition[];
|
|
135
131
|
};
|
|
136
132
|
/**
|
|
@@ -138,7 +134,7 @@ type WorkflowInternalDefinition<
|
|
|
138
134
|
* TStepExt is the accumulated step extension from all plugins (step = StepContext & TStepExt).
|
|
139
135
|
*/
|
|
140
136
|
interface WorkflowFactory<TStepExt = object> {
|
|
141
|
-
(id: string, handler: (context: WorkflowContext<
|
|
137
|
+
<I extends InputParameters = InputParameters>(id: string, handler: (context: WorkflowContext<I, StepBaseContext & TStepExt>) => Promise<unknown>, options?: WorkflowOptions<I>): WorkflowDefinition<I>;
|
|
142
138
|
use<TNewExt>(plugin: WorkflowPlugin<StepBaseContext & TStepExt, TNewExt>): WorkflowFactory<TStepExt & TNewExt>;
|
|
143
139
|
}
|
|
144
140
|
type WorkflowRunProgress = WorkflowRun & {
|
|
@@ -186,13 +182,14 @@ declare class WorkflowEngine {
|
|
|
186
182
|
batchSize?: number;
|
|
187
183
|
}): Promise<void>;
|
|
188
184
|
stop(): Promise<void>;
|
|
189
|
-
registerWorkflow
|
|
185
|
+
registerWorkflow(definition: WorkflowDefinition<InputParameters>): Promise<WorkflowEngine>;
|
|
190
186
|
unregisterWorkflow(workflowId: string): Promise<WorkflowEngine>;
|
|
191
187
|
unregisterAllWorkflows(): Promise<WorkflowEngine>;
|
|
192
|
-
startWorkflow({ resourceId, workflowId, input, options }: {
|
|
188
|
+
startWorkflow({ resourceId, workflowId, input, idempotencyKey, options }: {
|
|
193
189
|
resourceId?: string;
|
|
194
190
|
workflowId: string;
|
|
195
191
|
input: unknown;
|
|
192
|
+
idempotencyKey?: string;
|
|
196
193
|
options?: {
|
|
197
194
|
timeout?: number;
|
|
198
195
|
retries?: number;
|
|
@@ -278,11 +275,13 @@ declare class WorkflowEngine {
|
|
|
278
275
|
hasPrev: boolean;
|
|
279
276
|
}>;
|
|
280
277
|
}
|
|
278
|
+
import { StandardSchemaV1 as StandardSchemaV12 } from "@standard-schema/spec";
|
|
281
279
|
declare class WorkflowEngineError extends Error {
|
|
282
280
|
readonly workflowId?: string | undefined;
|
|
283
281
|
readonly runId?: string | undefined;
|
|
284
282
|
readonly cause: Error | undefined;
|
|
285
|
-
|
|
283
|
+
readonly issues?: StandardSchemaV12.FailureResult["issues"] | undefined;
|
|
284
|
+
constructor(message: string, workflowId?: string | undefined, runId?: string | undefined, cause?: Error | undefined, issues?: StandardSchemaV12.FailureResult["issues"] | undefined);
|
|
286
285
|
}
|
|
287
286
|
declare class WorkflowRunNotFoundError extends WorkflowEngineError {
|
|
288
287
|
constructor(runId?: string, workflowId?: string);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
2
|
type WorkflowRun = {
|
|
3
3
|
id: string;
|
|
4
4
|
createdAt: Date;
|
|
@@ -18,6 +18,7 @@ type WorkflowRun = {
|
|
|
18
18
|
retryCount: number;
|
|
19
19
|
maxRetries: number;
|
|
20
20
|
jobId: string | null;
|
|
21
|
+
idempotencyKey: string | null;
|
|
21
22
|
};
|
|
22
23
|
type DurationObject = {
|
|
23
24
|
weeks?: number;
|
|
@@ -44,8 +45,8 @@ declare enum StepType {
|
|
|
44
45
|
DELAY = "delay",
|
|
45
46
|
POLL = "poll"
|
|
46
47
|
}
|
|
47
|
-
type InputParameters =
|
|
48
|
-
type InferInputParameters<P extends InputParameters> =
|
|
48
|
+
type InputParameters = StandardSchemaV1;
|
|
49
|
+
type InferInputParameters<P extends InputParameters> = StandardSchemaV1.InferOutput<P>;
|
|
49
50
|
type WorkflowOptions<I extends InputParameters> = {
|
|
50
51
|
timeout?: number;
|
|
51
52
|
retries?: number;
|
|
@@ -109,12 +110,10 @@ type WorkflowContext<
|
|
|
109
110
|
timeline: Record<string, unknown>;
|
|
110
111
|
logger: WorkflowLogger;
|
|
111
112
|
};
|
|
112
|
-
type WorkflowDefinition<
|
|
113
|
-
TInput extends InputParameters = InputParameters,
|
|
114
|
-
TStep extends StepBaseContext = StepBaseContext
|
|
115
|
-
> = {
|
|
113
|
+
type WorkflowDefinition<TInput extends InputParameters = InputParameters> = {
|
|
116
114
|
id: string;
|
|
117
|
-
|
|
115
|
+
/** Widest context avoids contravariance when collecting definitions; `workflow()` still types the handler narrowly. */
|
|
116
|
+
handler: (context: WorkflowContext<InputParameters, StepBaseContext>) => Promise<unknown>;
|
|
118
117
|
inputSchema?: TInput;
|
|
119
118
|
timeout?: number;
|
|
120
119
|
retries?: number;
|
|
@@ -127,10 +126,7 @@ type StepInternalDefinition = {
|
|
|
127
126
|
loop: boolean;
|
|
128
127
|
isDynamic: boolean;
|
|
129
128
|
};
|
|
130
|
-
type WorkflowInternalDefinition<
|
|
131
|
-
TInput extends InputParameters = InputParameters,
|
|
132
|
-
TStep extends StepBaseContext = StepBaseContext
|
|
133
|
-
> = WorkflowDefinition<TInput, TStep> & {
|
|
129
|
+
type WorkflowInternalDefinition<TInput extends InputParameters = InputParameters> = WorkflowDefinition<TInput> & {
|
|
134
130
|
steps: StepInternalDefinition[];
|
|
135
131
|
};
|
|
136
132
|
/**
|
|
@@ -138,7 +134,7 @@ type WorkflowInternalDefinition<
|
|
|
138
134
|
* TStepExt is the accumulated step extension from all plugins (step = StepContext & TStepExt).
|
|
139
135
|
*/
|
|
140
136
|
interface WorkflowFactory<TStepExt = object> {
|
|
141
|
-
(id: string, handler: (context: WorkflowContext<
|
|
137
|
+
<I extends InputParameters = InputParameters>(id: string, handler: (context: WorkflowContext<I, StepBaseContext & TStepExt>) => Promise<unknown>, options?: WorkflowOptions<I>): WorkflowDefinition<I>;
|
|
142
138
|
use<TNewExt>(plugin: WorkflowPlugin<StepBaseContext & TStepExt, TNewExt>): WorkflowFactory<TStepExt & TNewExt>;
|
|
143
139
|
}
|
|
144
140
|
type WorkflowRunProgress = WorkflowRun & {
|
|
@@ -186,13 +182,14 @@ declare class WorkflowEngine {
|
|
|
186
182
|
batchSize?: number;
|
|
187
183
|
}): Promise<void>;
|
|
188
184
|
stop(): Promise<void>;
|
|
189
|
-
registerWorkflow
|
|
185
|
+
registerWorkflow(definition: WorkflowDefinition<InputParameters>): Promise<WorkflowEngine>;
|
|
190
186
|
unregisterWorkflow(workflowId: string): Promise<WorkflowEngine>;
|
|
191
187
|
unregisterAllWorkflows(): Promise<WorkflowEngine>;
|
|
192
|
-
startWorkflow({ resourceId, workflowId, input, options }: {
|
|
188
|
+
startWorkflow({ resourceId, workflowId, input, idempotencyKey, options }: {
|
|
193
189
|
resourceId?: string;
|
|
194
190
|
workflowId: string;
|
|
195
191
|
input: unknown;
|
|
192
|
+
idempotencyKey?: string;
|
|
196
193
|
options?: {
|
|
197
194
|
timeout?: number;
|
|
198
195
|
retries?: number;
|
|
@@ -278,11 +275,13 @@ declare class WorkflowEngine {
|
|
|
278
275
|
hasPrev: boolean;
|
|
279
276
|
}>;
|
|
280
277
|
}
|
|
278
|
+
import { StandardSchemaV1 as StandardSchemaV12 } from "@standard-schema/spec";
|
|
281
279
|
declare class WorkflowEngineError extends Error {
|
|
282
280
|
readonly workflowId?: string | undefined;
|
|
283
281
|
readonly runId?: string | undefined;
|
|
284
282
|
readonly cause: Error | undefined;
|
|
285
|
-
|
|
283
|
+
readonly issues?: StandardSchemaV12.FailureResult["issues"] | undefined;
|
|
284
|
+
constructor(message: string, workflowId?: string | undefined, runId?: string | undefined, cause?: Error | undefined, issues?: StandardSchemaV12.FailureResult["issues"] | undefined);
|
|
286
285
|
}
|
|
287
286
|
declare class WorkflowRunNotFoundError extends WorkflowEngineError {
|
|
288
287
|
constructor(runId?: string, workflowId?: string);
|