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.
- package/README.md +86 -0
- package/dist/client/workflow/emitter.d.ts +18 -0
- package/dist/client/workflow/emitter.d.ts.map +1 -0
- package/dist/client/workflow/emitter.js +75 -0
- package/dist/client/workflow/emitter.js.map +1 -0
- package/dist/client/workflow/engine.d.ts +41 -0
- package/dist/client/workflow/engine.d.ts.map +1 -0
- package/dist/client/workflow/engine.js +103 -0
- package/dist/client/workflow/engine.js.map +1 -0
- package/dist/client/workflow/executor.d.ts +34 -0
- package/dist/client/workflow/executor.d.ts.map +1 -0
- package/dist/client/workflow/executor.js +241 -0
- package/dist/client/workflow/executor.js.map +1 -0
- package/dist/client/workflow/index.d.ts +23 -0
- package/dist/client/workflow/index.d.ts.map +1 -0
- package/dist/client/workflow/index.js +22 -0
- package/dist/client/workflow/index.js.map +1 -0
- package/dist/client/workflow/runner.d.ts +21 -0
- package/dist/client/workflow/runner.d.ts.map +1 -0
- package/dist/client/workflow/runner.js +157 -0
- package/dist/client/workflow/runner.js.map +1 -0
- package/dist/client/workflow/store.d.ts +22 -0
- package/dist/client/workflow/store.d.ts.map +1 -0
- package/dist/client/workflow/store.js +160 -0
- package/dist/client/workflow/store.js.map +1 -0
- package/dist/client/workflow/types.d.ts +150 -0
- package/dist/client/workflow/types.d.ts.map +1 -0
- package/dist/client/workflow/types.js +5 -0
- package/dist/client/workflow/types.js.map +1 -0
- package/dist/client/workflow/workflow.d.ts +27 -0
- package/dist/client/workflow/workflow.d.ts.map +1 -0
- package/dist/client/workflow/workflow.js +102 -0
- package/dist/client/workflow/workflow.js.map +1 -0
- 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
|