bunqueue 2.6.116 → 2.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.
Files changed (34) hide show
  1. package/README.md +86 -0
  2. package/dist/client/workflow/emitter.d.ts +18 -0
  3. package/dist/client/workflow/emitter.d.ts.map +1 -0
  4. package/dist/client/workflow/emitter.js +75 -0
  5. package/dist/client/workflow/emitter.js.map +1 -0
  6. package/dist/client/workflow/engine.d.ts +41 -0
  7. package/dist/client/workflow/engine.d.ts.map +1 -0
  8. package/dist/client/workflow/engine.js +103 -0
  9. package/dist/client/workflow/engine.js.map +1 -0
  10. package/dist/client/workflow/executor.d.ts +34 -0
  11. package/dist/client/workflow/executor.d.ts.map +1 -0
  12. package/dist/client/workflow/executor.js +241 -0
  13. package/dist/client/workflow/executor.js.map +1 -0
  14. package/dist/client/workflow/index.d.ts +23 -0
  15. package/dist/client/workflow/index.d.ts.map +1 -0
  16. package/dist/client/workflow/index.js +22 -0
  17. package/dist/client/workflow/index.js.map +1 -0
  18. package/dist/client/workflow/runner.d.ts +21 -0
  19. package/dist/client/workflow/runner.d.ts.map +1 -0
  20. package/dist/client/workflow/runner.js +157 -0
  21. package/dist/client/workflow/runner.js.map +1 -0
  22. package/dist/client/workflow/store.d.ts +22 -0
  23. package/dist/client/workflow/store.d.ts.map +1 -0
  24. package/dist/client/workflow/store.js +160 -0
  25. package/dist/client/workflow/store.js.map +1 -0
  26. package/dist/client/workflow/types.d.ts +150 -0
  27. package/dist/client/workflow/types.d.ts.map +1 -0
  28. package/dist/client/workflow/types.js +5 -0
  29. package/dist/client/workflow/types.js.map +1 -0
  30. package/dist/client/workflow/workflow.d.ts +27 -0
  31. package/dist/client/workflow/workflow.d.ts.map +1 -0
  32. package/dist/client/workflow/workflow.js +102 -0
  33. package/dist/client/workflow/workflow.js.map +1 -0
  34. package/package.json +17 -2
package/README.md CHANGED
@@ -481,6 +481,91 @@ process.on('SIGINT', async () => {
481
481
 
482
482
  ---
483
483
 
484
+ ## Workflow Engine
485
+
486
+ Orchestrate multi-step business processes with branching, saga compensation, and human-in-the-loop signals. Built on top of bunqueue — no new infrastructure.
487
+
488
+ ```typescript
489
+ import { Workflow, Engine } from 'bunqueue/workflow';
490
+
491
+ const orderFlow = new Workflow('order-pipeline')
492
+ .step('validate', async (ctx) => {
493
+ const { orderId, amount } = ctx.input as { orderId: string; amount: number };
494
+ if (amount <= 0) throw new Error('Invalid amount');
495
+ return { orderId };
496
+ })
497
+ .step('reserve-stock', async () => {
498
+ await inventory.reserve();
499
+ return { reserved: true };
500
+ }, {
501
+ compensate: async () => await inventory.release(), // Auto-rollback on failure
502
+ })
503
+ .step('charge', async () => {
504
+ return { txId: await payments.charge() };
505
+ }, {
506
+ compensate: async () => await payments.refund(),
507
+ })
508
+ .step('confirm', async (ctx) => {
509
+ const { txId } = ctx.steps['charge'] as { txId: string };
510
+ return { emailSent: true, txId };
511
+ });
512
+
513
+ const engine = new Engine({ embedded: true });
514
+ engine.register(orderFlow);
515
+ await engine.start('order-pipeline', { orderId: 'ORD-1', amount: 99.99 });
516
+ ```
517
+
518
+ **Features:**
519
+
520
+ - **Saga pattern** — Compensation handlers run in reverse when a step fails
521
+ - **Branching** — Route to different paths based on runtime conditions
522
+ - **Parallel steps** — Run independent steps concurrently with `.parallel()`
523
+ - **Human-in-the-loop** — `waitFor('event')` pauses execution, `engine.signal()` resumes it
524
+ - **Signal timeout** — `waitFor('event', { timeout })` fails if signal doesn't arrive in time
525
+ - **Step retry** — Automatic retry with exponential backoff and jitter
526
+ - **Nested workflows** — Compose workflows with `.subWorkflow()`, child results passed back
527
+ - **Observability** — Typed event emitter with 11 event types (`engine.on/onAny`)
528
+ - **Cleanup & archival** — `engine.cleanup()` / `engine.archive()` for execution history management
529
+ - **Step timeouts** — Prevent steps from running indefinitely
530
+ - **Context passing** — Each step accesses input and all previous step results
531
+ - **SQLite persistence** — Execution state survives restarts
532
+
533
+ **vs Competitors:**
534
+
535
+ | | **bunqueue** | **Temporal** | **Inngest** | **AWS Step Functions** |
536
+ |---|---|---|---|---|
537
+ | **Infrastructure** | None (embedded) | PostgreSQL + 7 services | Cloud-only | AWS-native |
538
+ | **Saga compensation** | Built-in | Manual | Manual | Manual |
539
+ | **Human-in-the-loop** | `.waitFor()` | Signals API | `step.waitForEvent()` | Callback tasks |
540
+ | **Self-hosted** | Zero-config | Complex | No | No |
541
+ | **Pricing** | Free (MIT) | Free / Cloud $$ | Per-execution | Per-transition |
542
+
543
+ ```typescript
544
+ // Branching
545
+ const flow = new Workflow('tiered')
546
+ .step('classify', async (ctx) => ({ tier: ctx.input.amount > 1000 ? 'vip' : 'basic' }))
547
+ .branch((ctx) => ctx.steps['classify'].tier)
548
+ .path('vip', (w) => w.step('vip-handler', async () => ({ discount: 20 })))
549
+ .path('basic', (w) => w.step('basic-handler', async () => ({ discount: 0 })))
550
+ .step('done', async () => ({ processed: true }));
551
+
552
+ // Human-in-the-loop
553
+ const approvalFlow = new Workflow('expense')
554
+ .step('submit', async (ctx) => ({ amount: ctx.input.amount }))
555
+ .waitFor('manager-approval')
556
+ .step('reimburse', async (ctx) => {
557
+ const decision = ctx.signals['manager-approval'] as { approved: boolean };
558
+ return { status: decision.approved ? 'paid' : 'rejected' };
559
+ });
560
+
561
+ // Signal a waiting workflow
562
+ await engine.signal(run.id, 'manager-approval', { approved: true });
563
+ ```
564
+
565
+ [Workflow Engine docs →](https://bunqueue.dev/guide/workflow/)
566
+
567
+ ---
568
+
484
569
  <p align="center">
485
570
  <strong>bunqueue Dashboard</strong><br/>
486
571
  <sub>A visual interface for managing queues, jobs, workers and monitoring in real time. Currently in beta.</sub>
@@ -728,6 +813,7 @@ docker compose --profile monitoring up -d
728
813
  **[Read the full documentation →](https://bunqueue.dev/)**
729
814
 
730
815
  - [Quick Start](https://bunqueue.dev/guide/quickstart/)
816
+ - [Workflow Engine](https://bunqueue.dev/guide/workflow/)
731
817
  - [MCP Server (AI Agents)](https://bunqueue.dev/guide/mcp/)
732
818
  - [Simple Mode](https://bunqueue.dev/guide/simple-mode/)
733
819
  - [Queue API](https://bunqueue.dev/guide/queue/)
@@ -0,0 +1,18 @@
1
+ /**
2
+ * WorkflowEmitter - Typed event system for workflow observability
3
+ */
4
+ import type { WorkflowEventType, StepEvent, WorkflowLifecycleEvent, WorkflowEventListener, ExecutionState } from './types';
5
+ export declare class WorkflowEmitter {
6
+ private readonly listeners;
7
+ private readonly globalListeners;
8
+ on(type: WorkflowEventType, listener: WorkflowEventListener): this;
9
+ onAny(listener: WorkflowEventListener): this;
10
+ off(type: WorkflowEventType, listener: WorkflowEventListener): this;
11
+ offAny(listener: WorkflowEventListener): this;
12
+ emitStep(type: 'step:started' | 'step:completed' | 'step:failed' | 'step:retry', executionId: string, workflowName: string, stepName: string, extra?: Partial<Omit<StepEvent, 'type' | 'executionId' | 'workflowName' | 'timestamp' | 'stepName'>>): void;
13
+ emitWorkflow(type: 'workflow:started' | 'workflow:completed' | 'workflow:failed' | 'workflow:compensating' | 'workflow:waiting', executionId: string, workflowName: string, state: ExecutionState, extra?: Partial<Omit<WorkflowLifecycleEvent, 'type' | 'executionId' | 'workflowName' | 'timestamp' | 'state'>>): void;
14
+ emitSignal(type: 'signal:received' | 'signal:timeout', executionId: string, workflowName: string, event: string, payload?: unknown): void;
15
+ removeAllListeners(): void;
16
+ private dispatch;
17
+ }
18
+ //# sourceMappingURL=emitter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emitter.d.ts","sourceRoot":"","sources":["../../../src/client/workflow/emitter.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EACV,iBAAiB,EAEjB,SAAS,EACT,sBAAsB,EAEtB,qBAAqB,EACrB,cAAc,EACf,MAAM,SAAS,CAAC;AAEjB,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA4D;IACtF,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAoC;IAEpE,EAAE,CAAC,IAAI,EAAE,iBAAiB,EAAE,QAAQ,EAAE,qBAAqB,GAAG,IAAI;IAUlE,KAAK,CAAC,QAAQ,EAAE,qBAAqB,GAAG,IAAI;IAK5C,GAAG,CAAC,IAAI,EAAE,iBAAiB,EAAE,QAAQ,EAAE,qBAAqB,GAAG,IAAI;IAKnE,MAAM,CAAC,QAAQ,EAAE,qBAAqB,GAAG,IAAI;IAK7C,QAAQ,CACN,IAAI,EAAE,cAAc,GAAG,gBAAgB,GAAG,aAAa,GAAG,YAAY,EACtE,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,KAAK,CAAC,EAAE,OAAO,CACb,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,GAAG,cAAc,GAAG,WAAW,GAAG,UAAU,CAAC,CACpF,GACA,IAAI;IAYP,YAAY,CACV,IAAI,EACA,kBAAkB,GAClB,oBAAoB,GACpB,iBAAiB,GACjB,uBAAuB,GACvB,kBAAkB,EACtB,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,cAAc,EACrB,KAAK,CAAC,EAAE,OAAO,CACb,IAAI,CAAC,sBAAsB,EAAE,MAAM,GAAG,aAAa,GAAG,cAAc,GAAG,WAAW,GAAG,OAAO,CAAC,CAC9F,GACA,IAAI;IAYP,UAAU,CACR,IAAI,EAAE,iBAAiB,GAAG,gBAAgB,EAC1C,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,OAAO,GAChB,IAAI;IAYP,kBAAkB,IAAI,IAAI;IAK1B,OAAO,CAAC,QAAQ;CAOjB"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * WorkflowEmitter - Typed event system for workflow observability
3
+ */
4
+ export class WorkflowEmitter {
5
+ listeners = new Map();
6
+ globalListeners = new Set();
7
+ on(type, listener) {
8
+ let set = this.listeners.get(type);
9
+ if (!set) {
10
+ set = new Set();
11
+ this.listeners.set(type, set);
12
+ }
13
+ set.add(listener);
14
+ return this;
15
+ }
16
+ onAny(listener) {
17
+ this.globalListeners.add(listener);
18
+ return this;
19
+ }
20
+ off(type, listener) {
21
+ this.listeners.get(type)?.delete(listener);
22
+ return this;
23
+ }
24
+ offAny(listener) {
25
+ this.globalListeners.delete(listener);
26
+ return this;
27
+ }
28
+ emitStep(type, executionId, workflowName, stepName, extra) {
29
+ const event = {
30
+ type,
31
+ executionId,
32
+ workflowName,
33
+ timestamp: Date.now(),
34
+ stepName,
35
+ ...extra,
36
+ };
37
+ this.dispatch(type, event);
38
+ }
39
+ emitWorkflow(type, executionId, workflowName, state, extra) {
40
+ const event = {
41
+ type,
42
+ executionId,
43
+ workflowName,
44
+ timestamp: Date.now(),
45
+ state,
46
+ ...extra,
47
+ };
48
+ this.dispatch(type, event);
49
+ }
50
+ emitSignal(type, executionId, workflowName, event, payload) {
51
+ const evt = {
52
+ type,
53
+ executionId,
54
+ workflowName,
55
+ timestamp: Date.now(),
56
+ event,
57
+ payload,
58
+ };
59
+ this.dispatch(type, evt);
60
+ }
61
+ removeAllListeners() {
62
+ this.listeners.clear();
63
+ this.globalListeners.clear();
64
+ }
65
+ dispatch(type, event) {
66
+ const typed = this.listeners.get(type);
67
+ if (typed) {
68
+ for (const fn of typed)
69
+ fn(event);
70
+ }
71
+ for (const fn of this.globalListeners)
72
+ fn(event);
73
+ }
74
+ }
75
+ //# sourceMappingURL=emitter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emitter.js","sourceRoot":"","sources":["../../../src/client/workflow/emitter.ts"],"names":[],"mappings":"AAAA;;GAEG;AAYH,MAAM,OAAO,eAAe;IACT,SAAS,GAAG,IAAI,GAAG,EAAiD,CAAC;IACrE,eAAe,GAAG,IAAI,GAAG,EAAyB,CAAC;IAEpE,EAAE,CAAC,IAAuB,EAAE,QAA+B;QACzD,IAAI,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,GAAG,GAAG,IAAI,GAAG,EAAE,CAAC;YAChB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAChC,CAAC;QACD,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAClB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,CAAC,QAA+B;QACnC,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACnC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,GAAG,CAAC,IAAuB,EAAE,QAA+B;QAC1D,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC3C,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,CAAC,QAA+B;QACpC,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACtC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,QAAQ,CACN,IAAsE,EACtE,WAAmB,EACnB,YAAoB,EACpB,QAAgB,EAChB,KAEC;QAED,MAAM,KAAK,GAAc;YACvB,IAAI;YACJ,WAAW;YACX,YAAY;YACZ,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;YACrB,QAAQ;YACR,GAAG,KAAK;SACT,CAAC;QACF,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC7B,CAAC;IAED,YAAY,CACV,IAKsB,EACtB,WAAmB,EACnB,YAAoB,EACpB,KAAqB,EACrB,KAEC;QAED,MAAM,KAAK,GAA2B;YACpC,IAAI;YACJ,WAAW;YACX,YAAY;YACZ,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;YACrB,KAAK;YACL,GAAG,KAAK;SACT,CAAC;QACF,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC7B,CAAC;IAED,UAAU,CACR,IAA0C,EAC1C,WAAmB,EACnB,YAAoB,EACpB,KAAa,EACb,OAAiB;QAEjB,MAAM,GAAG,GAAgB;YACvB,IAAI;YACJ,WAAW;YACX,YAAY;YACZ,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;YACrB,KAAK;YACL,OAAO;SACR,CAAC;QACF,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC3B,CAAC;IAED,kBAAkB;QAChB,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;QACvB,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,CAAC;IAC/B,CAAC;IAEO,QAAQ,CAAC,IAAuB,EAAE,KAAoB;QAC5D,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACvC,IAAI,KAAK,EAAE,CAAC;YACV,KAAK,MAAM,EAAE,IAAI,KAAK;gBAAE,EAAE,CAAC,KAAK,CAAC,CAAC;QACpC,CAAC;QACD,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,eAAe;YAAE,EAAE,CAAC,KAAK,CAAC,CAAC;IACnD,CAAC;CACF"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Engine - Public facade for the workflow engine
3
+ * Manages lifecycle of internal Queue, Worker, and Store.
4
+ */
5
+ import type { Workflow } from './workflow';
6
+ import type { EngineOptions, RunHandle, Execution, ExecutionState, WorkflowEventType, WorkflowEventListener } from './types';
7
+ export declare class Engine {
8
+ private readonly queue;
9
+ private readonly worker;
10
+ private readonly store;
11
+ private readonly executor;
12
+ private readonly emitter;
13
+ constructor(opts?: EngineOptions);
14
+ /** Register a workflow definition */
15
+ register(workflow: Workflow): this;
16
+ /** Start a new workflow execution */
17
+ start(workflowName: string, input?: unknown): Promise<RunHandle>;
18
+ /** Get execution state by ID */
19
+ getExecution(id: string): Execution | null;
20
+ /** List executions with optional filters */
21
+ listExecutions(workflowName?: string, state?: ExecutionState): Execution[];
22
+ /** Send a signal to a waiting execution */
23
+ signal(executionId: string, event: string, payload?: unknown): Promise<void>;
24
+ /** Subscribe to a specific workflow event type */
25
+ on(type: WorkflowEventType, listener: WorkflowEventListener): this;
26
+ /** Subscribe to all workflow events */
27
+ onAny(listener: WorkflowEventListener): this;
28
+ /** Unsubscribe from a specific event type */
29
+ off(type: WorkflowEventType, listener: WorkflowEventListener): this;
30
+ /** Unsubscribe a catch-all listener */
31
+ offAny(listener: WorkflowEventListener): this;
32
+ /** Remove old completed/failed executions */
33
+ cleanup(maxAgeMs: number, states?: ExecutionState[]): number;
34
+ /** Archive old executions to a separate table */
35
+ archive(maxAgeMs: number, states?: ExecutionState[]): number;
36
+ /** Get archived execution count */
37
+ getArchivedCount(): number;
38
+ /** Shut down the engine */
39
+ close(force?: boolean): Promise<void>;
40
+ }
41
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../src/client/workflow/engine.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EACV,aAAa,EACb,SAAS,EACT,SAAS,EACT,cAAc,EAEd,iBAAiB,EACjB,qBAAqB,EACtB,MAAM,SAAS,CAAC;AAIjB,qBAAa,MAAM;IACjB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgB;IACtC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmB;IAC5C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkB;gBAE9B,IAAI,GAAE,aAAkB;IAiCpC,qCAAqC;IACrC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI;IAKlC,qCAAqC;IAC/B,KAAK,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,SAAS,CAAC;IAItE,gCAAgC;IAChC,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAI1C,4CAA4C;IAC5C,cAAc,CAAC,YAAY,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,cAAc,GAAG,SAAS,EAAE;IAI1E,2CAA2C;IACrC,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAMlF,kDAAkD;IAClD,EAAE,CAAC,IAAI,EAAE,iBAAiB,EAAE,QAAQ,EAAE,qBAAqB,GAAG,IAAI;IAKlE,uCAAuC;IACvC,KAAK,CAAC,QAAQ,EAAE,qBAAqB,GAAG,IAAI;IAK5C,6CAA6C;IAC7C,GAAG,CAAC,IAAI,EAAE,iBAAiB,EAAE,QAAQ,EAAE,qBAAqB,GAAG,IAAI;IAKnE,uCAAuC;IACvC,MAAM,CAAC,QAAQ,EAAE,qBAAqB,GAAG,IAAI;IAO7C,6CAA6C;IAC7C,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,cAAc,EAAE,GAAG,MAAM;IAI5D,iDAAiD;IACjD,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,cAAc,EAAE,GAAG,MAAM;IAI5D,mCAAmC;IACnC,gBAAgB,IAAI,MAAM;IAI1B,2BAA2B;IACrB,KAAK,CAAC,KAAK,UAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;CAM1C"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Engine - Public facade for the workflow engine
3
+ * Manages lifecycle of internal Queue, Worker, and Store.
4
+ */
5
+ import { Queue } from '../queue/queue';
6
+ import { Worker } from '../worker/worker';
7
+ import { WorkflowStore } from './store';
8
+ import { WorkflowExecutor } from './executor';
9
+ import { WorkflowEmitter } from './emitter';
10
+ const DEFAULT_QUEUE_NAME = '__wf:steps';
11
+ export class Engine {
12
+ queue;
13
+ worker;
14
+ store;
15
+ executor;
16
+ emitter;
17
+ constructor(opts = {}) {
18
+ const queueName = opts.queueName ?? DEFAULT_QUEUE_NAME;
19
+ this.queue = new Queue(queueName, {
20
+ connection: opts.connection,
21
+ embedded: opts.embedded,
22
+ dataPath: opts.dataPath,
23
+ });
24
+ this.store = new WorkflowStore(opts.dataPath);
25
+ this.emitter = new WorkflowEmitter();
26
+ if (opts.onEvent) {
27
+ this.emitter.onAny(opts.onEvent);
28
+ }
29
+ this.executor = new WorkflowExecutor(this.store, this.queue, this.emitter);
30
+ this.worker = new Worker(queueName, async (job) => {
31
+ const data = job.data;
32
+ return this.executor.processStep(data);
33
+ }, {
34
+ connection: opts.connection,
35
+ embedded: opts.embedded,
36
+ dataPath: opts.dataPath,
37
+ concurrency: opts.concurrency ?? 5,
38
+ });
39
+ }
40
+ /** Register a workflow definition */
41
+ register(workflow) {
42
+ this.executor.register(workflow);
43
+ return this;
44
+ }
45
+ /** Start a new workflow execution */
46
+ async start(workflowName, input) {
47
+ return this.executor.start(workflowName, input);
48
+ }
49
+ /** Get execution state by ID */
50
+ getExecution(id) {
51
+ return this.executor.getExecution(id);
52
+ }
53
+ /** List executions with optional filters */
54
+ listExecutions(workflowName, state) {
55
+ return this.executor.listExecutions(workflowName, state);
56
+ }
57
+ /** Send a signal to a waiting execution */
58
+ async signal(executionId, event, payload) {
59
+ return this.executor.signal(executionId, event, payload);
60
+ }
61
+ // ============ Observability ============
62
+ /** Subscribe to a specific workflow event type */
63
+ on(type, listener) {
64
+ this.emitter.on(type, listener);
65
+ return this;
66
+ }
67
+ /** Subscribe to all workflow events */
68
+ onAny(listener) {
69
+ this.emitter.onAny(listener);
70
+ return this;
71
+ }
72
+ /** Unsubscribe from a specific event type */
73
+ off(type, listener) {
74
+ this.emitter.off(type, listener);
75
+ return this;
76
+ }
77
+ /** Unsubscribe a catch-all listener */
78
+ offAny(listener) {
79
+ this.emitter.offAny(listener);
80
+ return this;
81
+ }
82
+ // ============ Cleanup ============
83
+ /** Remove old completed/failed executions */
84
+ cleanup(maxAgeMs, states) {
85
+ return this.store.cleanup(maxAgeMs, states);
86
+ }
87
+ /** Archive old executions to a separate table */
88
+ archive(maxAgeMs, states) {
89
+ return this.store.archive(maxAgeMs, states);
90
+ }
91
+ /** Get archived execution count */
92
+ getArchivedCount() {
93
+ return this.store.getArchivedCount();
94
+ }
95
+ /** Shut down the engine */
96
+ async close(force = false) {
97
+ await this.worker.close(force);
98
+ this.queue.close();
99
+ this.store.close();
100
+ this.emitter.removeAllListeners();
101
+ }
102
+ }
103
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../../src/client/workflow/engine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * WorkflowExecutor - Core execution logic
3
+ * Processes steps as bunqueue jobs, handles transitions, branching, compensation.
4
+ */
5
+ import type { Queue } from '../queue/queue';
6
+ import type { Workflow } from './workflow';
7
+ import type { WorkflowStore } from './store';
8
+ import type { WorkflowEmitter } from './emitter';
9
+ import type { Execution, StepJobData, RunHandle } from './types';
10
+ export declare class WorkflowExecutor {
11
+ private readonly store;
12
+ private readonly queue;
13
+ private readonly emitter;
14
+ private readonly workflows;
15
+ private readonly timeoutTimers;
16
+ constructor(store: WorkflowStore, queue: Queue, emitter?: WorkflowEmitter | null);
17
+ register(workflow: Workflow): void;
18
+ start(workflowName: string, input: unknown): Promise<RunHandle>;
19
+ processStep(data: StepJobData): Promise<unknown>;
20
+ signal(executionId: string, event: string, payload: unknown): Promise<void>;
21
+ getExecution(id: string): Execution | null;
22
+ listExecutions(workflowName?: string, state?: Execution['state']): Execution[];
23
+ private executeNode;
24
+ private runStep;
25
+ private runBranch;
26
+ private runParallel;
27
+ private runSubWorkflow;
28
+ private runWaitFor;
29
+ private advance;
30
+ private enqueue;
31
+ private scheduleTimeoutCheck;
32
+ private compensate;
33
+ }
34
+ //# sourceMappingURL=executor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../../src/client/workflow/executor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,EAAgC,MAAM,SAAS,CAAC;AAe/F,qBAAa,gBAAgB;IAKzB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAN1B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA+B;IACzD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAoD;gBAG/D,KAAK,EAAE,aAAa,EACpB,KAAK,EAAE,KAAK,EACZ,OAAO,GAAE,eAAe,GAAG,IAAW;IAGzD,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI;IAS5B,KAAK,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,SAAS,CAAC;IAwB/D,WAAW,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC;IA8BhD,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBjF,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAI1C,cAAc,CAAC,YAAY,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,SAAS,CAAC,OAAO,CAAC,GAAG,SAAS,EAAE;YAMhE,WAAW;YAaX,OAAO;YAQP,SAAS;YAkBT,WAAW;YAYX,cAAc;YAiBd,UAAU;YA6CV,OAAO;YAYP,OAAO;IASrB,OAAO,CAAC,oBAAoB;YASd,UAAU;CAwBzB"}
@@ -0,0 +1,241 @@
1
+ /**
2
+ * WorkflowExecutor - Core execution logic
3
+ * Processes steps as bunqueue jobs, handles transitions, branching, compensation.
4
+ */
5
+ import { executeStepWithRetry, executeParallelSteps, executeSubWorkflow, findStepDef, buildContext, } from './runner';
6
+ class WaitForSignalError extends Error {
7
+ event;
8
+ constructor(event) {
9
+ super(`Waiting for signal: ${event}`);
10
+ this.event = event;
11
+ }
12
+ }
13
+ export class WorkflowExecutor {
14
+ store;
15
+ queue;
16
+ emitter;
17
+ workflows = new Map();
18
+ timeoutTimers = new Map();
19
+ constructor(store, queue, emitter = null) {
20
+ this.store = store;
21
+ this.queue = queue;
22
+ this.emitter = emitter;
23
+ }
24
+ register(workflow) {
25
+ const names = workflow.getStepNames();
26
+ const dupes = names.filter((n, i) => names.indexOf(n) !== i);
27
+ if (dupes.length > 0) {
28
+ throw new Error(`Duplicate step names in "${workflow.name}": ${dupes.join(', ')}`);
29
+ }
30
+ this.workflows.set(workflow.name, workflow);
31
+ }
32
+ async start(workflowName, input) {
33
+ const wf = this.workflows.get(workflowName);
34
+ if (!wf)
35
+ throw new Error(`Workflow "${workflowName}" not registered`);
36
+ if (wf.nodes.length === 0)
37
+ throw new Error(`Workflow "${workflowName}" has no steps`);
38
+ const now = Date.now();
39
+ const exec = {
40
+ id: `wf_${now}_${Math.random().toString(36).slice(2, 10)}`,
41
+ workflowName,
42
+ state: 'running',
43
+ input,
44
+ steps: {},
45
+ currentNodeIndex: 0,
46
+ signals: {},
47
+ createdAt: now,
48
+ updatedAt: now,
49
+ };
50
+ this.store.save(exec);
51
+ this.emitter?.emitWorkflow('workflow:started', exec.id, workflowName, 'running', { input });
52
+ await this.enqueue(exec);
53
+ return { id: exec.id, workflowName };
54
+ }
55
+ async processStep(data) {
56
+ const exec = this.store.get(data.executionId);
57
+ if (!exec || (exec.state !== 'running' && exec.state !== 'waiting'))
58
+ return null;
59
+ // If waiting, set back to running for timeout re-check
60
+ if (exec.state === 'waiting')
61
+ exec.state = 'running';
62
+ const wf = this.workflows.get(exec.workflowName);
63
+ if (!wf)
64
+ throw new Error(`Workflow "${exec.workflowName}" not registered`);
65
+ const node = wf.nodes[data.nodeIndex];
66
+ if (!node) {
67
+ exec.state = 'completed';
68
+ this.store.update(exec);
69
+ this.emitter?.emitWorkflow('workflow:completed', exec.id, exec.workflowName, 'completed');
70
+ return null;
71
+ }
72
+ try {
73
+ await this.executeNode(exec, node, data.nodeIndex, wf);
74
+ }
75
+ catch (err) {
76
+ if (err instanceof WaitForSignalError)
77
+ return null;
78
+ exec.state = 'failed';
79
+ this.store.update(exec);
80
+ this.emitter?.emitWorkflow('workflow:failed', exec.id, exec.workflowName, 'failed');
81
+ await this.compensate(exec, wf);
82
+ throw err;
83
+ }
84
+ return null;
85
+ }
86
+ async signal(executionId, event, payload) {
87
+ const exec = this.store.get(executionId);
88
+ if (!exec)
89
+ throw new Error(`Execution "${executionId}" not found`);
90
+ // Cancel any pending timeout timer for this execution
91
+ const timer = this.timeoutTimers.get(executionId);
92
+ if (timer) {
93
+ clearTimeout(timer);
94
+ this.timeoutTimers.delete(executionId);
95
+ }
96
+ exec.signals[event] = payload;
97
+ exec.state = 'running';
98
+ this.store.update(exec);
99
+ this.emitter?.emitSignal('signal:received', exec.id, exec.workflowName, event, payload);
100
+ await this.enqueue(exec);
101
+ }
102
+ getExecution(id) {
103
+ return this.store.get(id);
104
+ }
105
+ listExecutions(workflowName, state) {
106
+ return this.store.list(workflowName, state);
107
+ }
108
+ // ============ Node dispatch ============
109
+ async executeNode(exec, node, idx, wf) {
110
+ if (node.type === 'step')
111
+ await this.runStep(exec, node.def, idx, wf);
112
+ else if (node.type === 'branch')
113
+ await this.runBranch(exec, node, idx, wf);
114
+ else if (node.type === 'parallel')
115
+ await this.runParallel(exec, node, idx, wf);
116
+ else if (node.type === 'subWorkflow')
117
+ await this.runSubWorkflow(exec, node, idx, wf);
118
+ else
119
+ await this.runWaitFor(exec, node, idx, wf);
120
+ }
121
+ async runStep(exec, def, idx, wf) {
122
+ const ctx = buildContext(exec);
123
+ await executeStepWithRetry(def, ctx, exec, this.emitter, (e) => {
124
+ this.store.update(e);
125
+ });
126
+ await this.advance(exec, idx + 1, wf);
127
+ }
128
+ async runBranch(exec, node, idx, wf) {
129
+ const pathName = node.def.condition(buildContext(exec));
130
+ const pathSteps = node.def.paths.get(pathName);
131
+ if (pathSteps && pathSteps.length > 0) {
132
+ for (const step of pathSteps) {
133
+ await executeStepWithRetry(step, buildContext(exec), exec, this.emitter, (e) => {
134
+ this.store.update(e);
135
+ });
136
+ }
137
+ }
138
+ await this.advance(exec, idx + 1, wf);
139
+ }
140
+ async runParallel(exec, node, idx, wf) {
141
+ await executeParallelSteps(node.def.steps, buildContext(exec), exec, this.emitter, (e) => {
142
+ this.store.update(e);
143
+ });
144
+ await this.advance(exec, idx + 1, wf);
145
+ }
146
+ async runSubWorkflow(exec, node, idx, wf) {
147
+ const subInput = node.inputMapper(buildContext(exec));
148
+ const result = await executeSubWorkflow(node.name, subInput, (name, input) => this.start(name, input), (id) => this.store.get(id));
149
+ exec.steps[`sub:${node.name}`] = { status: 'completed', result, completedAt: Date.now() };
150
+ await this.advance(exec, idx + 1, wf);
151
+ }
152
+ async runWaitFor(exec, node, idx, wf) {
153
+ if (exec.signals[node.event] !== undefined) {
154
+ await this.advance(exec, idx + 1, wf);
155
+ return;
156
+ }
157
+ const waitKey = `__waitFor:${node.event}`;
158
+ if (node.timeout !== undefined) {
159
+ const existing = exec.steps[waitKey];
160
+ const waitingSince = existing?.startedAt ?? Date.now();
161
+ if (!existing) {
162
+ exec.steps[waitKey] = { status: 'running', startedAt: waitingSince };
163
+ }
164
+ if (Date.now() - waitingSince >= node.timeout) {
165
+ this.emitter?.emitSignal('signal:timeout', exec.id, exec.workflowName, node.event);
166
+ exec.steps[waitKey] = {
167
+ status: 'failed',
168
+ error: `Signal "${node.event}" timed out after ${node.timeout}ms`,
169
+ startedAt: waitingSince,
170
+ completedAt: Date.now(),
171
+ };
172
+ exec.state = 'failed';
173
+ this.store.update(exec);
174
+ await this.compensate(exec, wf);
175
+ throw new Error(`Signal "${node.event}" timed out`);
176
+ }
177
+ this.store.update(exec);
178
+ // Schedule a timer to re-check after timeout
179
+ const remaining = node.timeout - (Date.now() - waitingSince);
180
+ this.scheduleTimeoutCheck(exec.id, exec.workflowName, exec.currentNodeIndex, remaining);
181
+ }
182
+ exec.state = 'waiting';
183
+ this.store.update(exec);
184
+ this.emitter?.emitWorkflow('workflow:waiting', exec.id, exec.workflowName, 'waiting');
185
+ throw new WaitForSignalError(node.event);
186
+ }
187
+ // ============ Helpers ============
188
+ async advance(exec, nextIdx, wf) {
189
+ exec.currentNodeIndex = nextIdx;
190
+ this.store.update(exec);
191
+ if (nextIdx >= wf.nodes.length) {
192
+ exec.state = 'completed';
193
+ this.store.update(exec);
194
+ this.emitter?.emitWorkflow('workflow:completed', exec.id, exec.workflowName, 'completed');
195
+ }
196
+ else {
197
+ await this.enqueue(exec);
198
+ }
199
+ }
200
+ async enqueue(exec) {
201
+ const jobData = {
202
+ executionId: exec.id,
203
+ workflowName: exec.workflowName,
204
+ nodeIndex: exec.currentNodeIndex,
205
+ };
206
+ await this.queue.add('wf:step', jobData);
207
+ }
208
+ scheduleTimeoutCheck(execId, workflowName, nodeIdx, ms) {
209
+ const timer = setTimeout(() => {
210
+ this.timeoutTimers.delete(execId);
211
+ const jobData = { executionId: execId, workflowName, nodeIndex: nodeIdx };
212
+ this.queue.add('wf:step', jobData).catch(() => { }); // Queue may be closed
213
+ }, ms);
214
+ this.timeoutTimers.set(execId, timer);
215
+ }
216
+ async compensate(exec, wf) {
217
+ const completed = Object.entries(exec.steps)
218
+ .filter(([name, s]) => s.status === 'completed' && !name.startsWith('__'))
219
+ .reverse();
220
+ if (completed.length === 0)
221
+ return;
222
+ exec.state = 'compensating';
223
+ this.store.update(exec);
224
+ this.emitter?.emitWorkflow('workflow:compensating', exec.id, exec.workflowName, 'compensating');
225
+ const ctx = buildContext(exec);
226
+ for (const [name] of completed) {
227
+ const def = findStepDef(wf, name);
228
+ if (def?.compensate) {
229
+ try {
230
+ await def.compensate(ctx);
231
+ }
232
+ catch {
233
+ // Compensation errors don't stop the chain
234
+ }
235
+ }
236
+ }
237
+ exec.state = 'failed';
238
+ this.store.update(exec);
239
+ }
240
+ }
241
+ //# sourceMappingURL=executor.js.map