bunqueue 2.7.0 → 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 +6 -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 +16 -1
- package/dist/client/workflow/engine.d.ts.map +1 -1
- package/dist/client/workflow/engine.js +42 -1
- package/dist/client/workflow/engine.js.map +1 -1
- package/dist/client/workflow/executor.d.ts +13 -11
- package/dist/client/workflow/executor.d.ts.map +1 -1
- package/dist/client/workflow/executor.js +117 -123
- package/dist/client/workflow/executor.js.map +1 -1
- package/dist/client/workflow/index.d.ts +2 -1
- package/dist/client/workflow/index.d.ts.map +1 -1
- package/dist/client/workflow/index.js +1 -0
- package/dist/client/workflow/index.js.map +1 -1
- 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 +6 -0
- package/dist/client/workflow/store.d.ts.map +1 -1
- package/dist/client/workflow/store.js +55 -0
- package/dist/client/workflow/store.js.map +1 -1
- package/dist/client/workflow/types.d.ts +50 -0
- package/dist/client/workflow/types.d.ts.map +1 -1
- package/dist/client/workflow/workflow.d.ts +5 -1
- package/dist/client/workflow/workflow.d.ts.map +1 -1
- package/dist/client/workflow/workflow.js +29 -0
- package/dist/client/workflow/workflow.js.map +1 -1
- package/package.json +13 -2
package/README.md
CHANGED
|
@@ -519,7 +519,13 @@ await engine.start('order-pipeline', { orderId: 'ORD-1', amount: 99.99 });
|
|
|
519
519
|
|
|
520
520
|
- **Saga pattern** — Compensation handlers run in reverse when a step fails
|
|
521
521
|
- **Branching** — Route to different paths based on runtime conditions
|
|
522
|
+
- **Parallel steps** — Run independent steps concurrently with `.parallel()`
|
|
522
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
|
|
523
529
|
- **Step timeouts** — Prevent steps from running indefinitely
|
|
524
530
|
- **Context passing** — Each step accesses input and all previous step results
|
|
525
531
|
- **SQLite persistence** — Execution state survives restarts
|
|
@@ -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"}
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
* Manages lifecycle of internal Queue, Worker, and Store.
|
|
4
4
|
*/
|
|
5
5
|
import type { Workflow } from './workflow';
|
|
6
|
-
import type { EngineOptions, RunHandle, Execution, ExecutionState } from './types';
|
|
6
|
+
import type { EngineOptions, RunHandle, Execution, ExecutionState, WorkflowEventType, WorkflowEventListener } from './types';
|
|
7
7
|
export declare class Engine {
|
|
8
8
|
private readonly queue;
|
|
9
9
|
private readonly worker;
|
|
10
10
|
private readonly store;
|
|
11
11
|
private readonly executor;
|
|
12
|
+
private readonly emitter;
|
|
12
13
|
constructor(opts?: EngineOptions);
|
|
13
14
|
/** Register a workflow definition */
|
|
14
15
|
register(workflow: Workflow): this;
|
|
@@ -20,6 +21,20 @@ export declare class Engine {
|
|
|
20
21
|
listExecutions(workflowName?: string, state?: ExecutionState): Execution[];
|
|
21
22
|
/** Send a signal to a waiting execution */
|
|
22
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;
|
|
23
38
|
/** Shut down the engine */
|
|
24
39
|
close(force?: boolean): Promise<void>;
|
|
25
40
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../src/client/workflow/engine.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
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"}
|
|
@@ -6,12 +6,14 @@ import { Queue } from '../queue/queue';
|
|
|
6
6
|
import { Worker } from '../worker/worker';
|
|
7
7
|
import { WorkflowStore } from './store';
|
|
8
8
|
import { WorkflowExecutor } from './executor';
|
|
9
|
+
import { WorkflowEmitter } from './emitter';
|
|
9
10
|
const DEFAULT_QUEUE_NAME = '__wf:steps';
|
|
10
11
|
export class Engine {
|
|
11
12
|
queue;
|
|
12
13
|
worker;
|
|
13
14
|
store;
|
|
14
15
|
executor;
|
|
16
|
+
emitter;
|
|
15
17
|
constructor(opts = {}) {
|
|
16
18
|
const queueName = opts.queueName ?? DEFAULT_QUEUE_NAME;
|
|
17
19
|
this.queue = new Queue(queueName, {
|
|
@@ -20,7 +22,11 @@ export class Engine {
|
|
|
20
22
|
dataPath: opts.dataPath,
|
|
21
23
|
});
|
|
22
24
|
this.store = new WorkflowStore(opts.dataPath);
|
|
23
|
-
this.
|
|
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);
|
|
24
30
|
this.worker = new Worker(queueName, async (job) => {
|
|
25
31
|
const data = job.data;
|
|
26
32
|
return this.executor.processStep(data);
|
|
@@ -52,11 +58,46 @@ export class Engine {
|
|
|
52
58
|
async signal(executionId, event, payload) {
|
|
53
59
|
return this.executor.signal(executionId, event, payload);
|
|
54
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
|
+
}
|
|
55
95
|
/** Shut down the engine */
|
|
56
96
|
async close(force = false) {
|
|
57
97
|
await this.worker.close(force);
|
|
58
98
|
this.queue.close();
|
|
59
99
|
this.store.close();
|
|
100
|
+
this.emitter.removeAllListeners();
|
|
60
101
|
}
|
|
61
102
|
}
|
|
62
103
|
//# sourceMappingURL=engine.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../../src/client/workflow/engine.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AACvC,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../../src/client/workflow/engine.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,28 +5,30 @@
|
|
|
5
5
|
import type { Queue } from '../queue/queue';
|
|
6
6
|
import type { Workflow } from './workflow';
|
|
7
7
|
import type { WorkflowStore } from './store';
|
|
8
|
+
import type { WorkflowEmitter } from './emitter';
|
|
8
9
|
import type { Execution, StepJobData, RunHandle } from './types';
|
|
9
10
|
export declare class WorkflowExecutor {
|
|
10
11
|
private readonly store;
|
|
11
12
|
private readonly queue;
|
|
13
|
+
private readonly emitter;
|
|
12
14
|
private readonly workflows;
|
|
13
|
-
|
|
15
|
+
private readonly timeoutTimers;
|
|
16
|
+
constructor(store: WorkflowStore, queue: Queue, emitter?: WorkflowEmitter | null);
|
|
14
17
|
register(workflow: Workflow): void;
|
|
15
18
|
start(workflowName: string, input: unknown): Promise<RunHandle>;
|
|
16
|
-
/** Process a step job — this is the Worker processor function */
|
|
17
19
|
processStep(data: StepJobData): Promise<unknown>;
|
|
18
20
|
signal(executionId: string, event: string, payload: unknown): Promise<void>;
|
|
19
21
|
getExecution(id: string): Execution | null;
|
|
20
22
|
listExecutions(workflowName?: string, state?: Execution['state']): Execution[];
|
|
21
23
|
private executeNode;
|
|
22
|
-
private
|
|
23
|
-
private
|
|
24
|
-
private
|
|
25
|
-
private
|
|
26
|
-
private
|
|
27
|
-
private
|
|
28
|
-
private
|
|
29
|
-
private
|
|
30
|
-
private
|
|
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;
|
|
31
33
|
}
|
|
32
34
|
//# sourceMappingURL=executor.d.ts.map
|
|
@@ -1 +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,
|
|
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"}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* WorkflowExecutor - Core execution logic
|
|
3
3
|
* Processes steps as bunqueue jobs, handles transitions, branching, compensation.
|
|
4
4
|
*/
|
|
5
|
-
|
|
5
|
+
import { executeStepWithRetry, executeParallelSteps, executeSubWorkflow, findStepDef, buildContext, } from './runner';
|
|
6
6
|
class WaitForSignalError extends Error {
|
|
7
7
|
event;
|
|
8
8
|
constructor(event) {
|
|
@@ -13,10 +13,13 @@ class WaitForSignalError extends Error {
|
|
|
13
13
|
export class WorkflowExecutor {
|
|
14
14
|
store;
|
|
15
15
|
queue;
|
|
16
|
+
emitter;
|
|
16
17
|
workflows = new Map();
|
|
17
|
-
|
|
18
|
+
timeoutTimers = new Map();
|
|
19
|
+
constructor(store, queue, emitter = null) {
|
|
18
20
|
this.store = store;
|
|
19
21
|
this.queue = queue;
|
|
22
|
+
this.emitter = emitter;
|
|
20
23
|
}
|
|
21
24
|
register(workflow) {
|
|
22
25
|
const names = workflow.getStepNames();
|
|
@@ -45,14 +48,17 @@ export class WorkflowExecutor {
|
|
|
45
48
|
updatedAt: now,
|
|
46
49
|
};
|
|
47
50
|
this.store.save(exec);
|
|
48
|
-
|
|
51
|
+
this.emitter?.emitWorkflow('workflow:started', exec.id, workflowName, 'running', { input });
|
|
52
|
+
await this.enqueue(exec);
|
|
49
53
|
return { id: exec.id, workflowName };
|
|
50
54
|
}
|
|
51
|
-
/** Process a step job — this is the Worker processor function */
|
|
52
55
|
async processStep(data) {
|
|
53
56
|
const exec = this.store.get(data.executionId);
|
|
54
|
-
if (exec
|
|
57
|
+
if (!exec || (exec.state !== 'running' && exec.state !== 'waiting'))
|
|
55
58
|
return null;
|
|
59
|
+
// If waiting, set back to running for timeout re-check
|
|
60
|
+
if (exec.state === 'waiting')
|
|
61
|
+
exec.state = 'running';
|
|
56
62
|
const wf = this.workflows.get(exec.workflowName);
|
|
57
63
|
if (!wf)
|
|
58
64
|
throw new Error(`Workflow "${exec.workflowName}" not registered`);
|
|
@@ -60,6 +66,7 @@ export class WorkflowExecutor {
|
|
|
60
66
|
if (!node) {
|
|
61
67
|
exec.state = 'completed';
|
|
62
68
|
this.store.update(exec);
|
|
69
|
+
this.emitter?.emitWorkflow('workflow:completed', exec.id, exec.workflowName, 'completed');
|
|
63
70
|
return null;
|
|
64
71
|
}
|
|
65
72
|
try {
|
|
@@ -70,7 +77,8 @@ export class WorkflowExecutor {
|
|
|
70
77
|
return null;
|
|
71
78
|
exec.state = 'failed';
|
|
72
79
|
this.store.update(exec);
|
|
73
|
-
|
|
80
|
+
this.emitter?.emitWorkflow('workflow:failed', exec.id, exec.workflowName, 'failed');
|
|
81
|
+
await this.compensate(exec, wf);
|
|
74
82
|
throw err;
|
|
75
83
|
}
|
|
76
84
|
return null;
|
|
@@ -79,10 +87,17 @@ export class WorkflowExecutor {
|
|
|
79
87
|
const exec = this.store.get(executionId);
|
|
80
88
|
if (!exec)
|
|
81
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
|
+
}
|
|
82
96
|
exec.signals[event] = payload;
|
|
83
97
|
exec.state = 'running';
|
|
84
98
|
this.store.update(exec);
|
|
85
|
-
|
|
99
|
+
this.emitter?.emitSignal('signal:received', exec.id, exec.workflowName, event, payload);
|
|
100
|
+
await this.enqueue(exec);
|
|
86
101
|
}
|
|
87
102
|
getExecution(id) {
|
|
88
103
|
return this.store.get(id);
|
|
@@ -90,84 +105,99 @@ export class WorkflowExecutor {
|
|
|
90
105
|
listExecutions(workflowName, state) {
|
|
91
106
|
return this.store.list(workflowName, state);
|
|
92
107
|
}
|
|
93
|
-
// ============
|
|
94
|
-
async executeNode(exec, node,
|
|
95
|
-
if (node.type === 'step')
|
|
96
|
-
await this.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
else
|
|
102
|
-
await this.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
this.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
status: 'completed',
|
|
113
|
-
result,
|
|
114
|
-
startedAt: exec.steps[def.name].startedAt,
|
|
115
|
-
completedAt: Date.now(),
|
|
116
|
-
};
|
|
117
|
-
exec.currentNodeIndex = nodeIndex + 1;
|
|
118
|
-
this.store.update(exec);
|
|
119
|
-
await this.advanceToNext(exec, wf);
|
|
120
|
-
}
|
|
121
|
-
catch (err) {
|
|
122
|
-
exec.steps[def.name] = {
|
|
123
|
-
status: 'failed',
|
|
124
|
-
error: String(err),
|
|
125
|
-
startedAt: exec.steps[def.name].startedAt,
|
|
126
|
-
completedAt: Date.now(),
|
|
127
|
-
};
|
|
128
|
-
this.store.update(exec);
|
|
129
|
-
throw err;
|
|
130
|
-
}
|
|
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);
|
|
131
127
|
}
|
|
132
|
-
async
|
|
133
|
-
const
|
|
134
|
-
const pathName = node.def.condition(ctx);
|
|
128
|
+
async runBranch(exec, node, idx, wf) {
|
|
129
|
+
const pathName = node.def.condition(buildContext(exec));
|
|
135
130
|
const pathSteps = node.def.paths.get(pathName);
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
// Execute branch steps inline
|
|
143
|
-
for (const step of pathSteps) {
|
|
144
|
-
await this.executeStep(exec, step, nodeIndex, wf);
|
|
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
|
+
}
|
|
145
137
|
}
|
|
146
|
-
exec
|
|
147
|
-
|
|
148
|
-
|
|
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);
|
|
149
145
|
}
|
|
150
|
-
async
|
|
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) {
|
|
151
153
|
if (exec.signals[node.event] !== undefined) {
|
|
152
|
-
exec
|
|
153
|
-
this.store.update(exec);
|
|
154
|
-
await this.enqueueNode(exec);
|
|
154
|
+
await this.advance(exec, idx + 1, wf);
|
|
155
155
|
return;
|
|
156
156
|
}
|
|
157
|
-
|
|
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
|
+
}
|
|
158
182
|
exec.state = 'waiting';
|
|
159
183
|
this.store.update(exec);
|
|
184
|
+
this.emitter?.emitWorkflow('workflow:waiting', exec.id, exec.workflowName, 'waiting');
|
|
160
185
|
throw new WaitForSignalError(node.event);
|
|
161
186
|
}
|
|
162
|
-
|
|
163
|
-
|
|
187
|
+
// ============ Helpers ============
|
|
188
|
+
async advance(exec, nextIdx, wf) {
|
|
189
|
+
exec.currentNodeIndex = nextIdx;
|
|
190
|
+
this.store.update(exec);
|
|
191
|
+
if (nextIdx >= wf.nodes.length) {
|
|
164
192
|
exec.state = 'completed';
|
|
165
193
|
this.store.update(exec);
|
|
166
|
-
|
|
194
|
+
this.emitter?.emitWorkflow('workflow:completed', exec.id, exec.workflowName, 'completed');
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
await this.enqueue(exec);
|
|
167
198
|
}
|
|
168
|
-
await this.enqueueNode(exec);
|
|
169
199
|
}
|
|
170
|
-
async
|
|
200
|
+
async enqueue(exec) {
|
|
171
201
|
const jobData = {
|
|
172
202
|
executionId: exec.id,
|
|
173
203
|
workflowName: exec.workflowName,
|
|
@@ -175,73 +205,37 @@ export class WorkflowExecutor {
|
|
|
175
205
|
};
|
|
176
206
|
await this.queue.add('wf:step', jobData);
|
|
177
207
|
}
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
.
|
|
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('__'))
|
|
181
219
|
.reverse();
|
|
182
|
-
if (
|
|
220
|
+
if (completed.length === 0)
|
|
183
221
|
return;
|
|
184
222
|
exec.state = 'compensating';
|
|
185
223
|
this.store.update(exec);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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);
|
|
189
228
|
if (def?.compensate) {
|
|
190
229
|
try {
|
|
191
230
|
await def.compensate(ctx);
|
|
192
231
|
}
|
|
193
232
|
catch {
|
|
194
|
-
// Compensation errors
|
|
233
|
+
// Compensation errors don't stop the chain
|
|
195
234
|
}
|
|
196
235
|
}
|
|
197
236
|
}
|
|
198
237
|
exec.state = 'failed';
|
|
199
238
|
this.store.update(exec);
|
|
200
239
|
}
|
|
201
|
-
findStepDef(wf, name) {
|
|
202
|
-
for (const node of wf.nodes) {
|
|
203
|
-
if (node.type === 'step' && node.def.name === name)
|
|
204
|
-
return node.def;
|
|
205
|
-
if (node.type === 'branch') {
|
|
206
|
-
for (const steps of node.def.paths.values()) {
|
|
207
|
-
const found = steps.find((s) => s.name === name);
|
|
208
|
-
if (found)
|
|
209
|
-
return found;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
return null;
|
|
214
|
-
}
|
|
215
|
-
buildContext(exec) {
|
|
216
|
-
const stepResults = {};
|
|
217
|
-
for (const [name, record] of Object.entries(exec.steps)) {
|
|
218
|
-
if (record.status === 'completed')
|
|
219
|
-
stepResults[name] = record.result;
|
|
220
|
-
}
|
|
221
|
-
return {
|
|
222
|
-
input: exec.input,
|
|
223
|
-
steps: stepResults,
|
|
224
|
-
signals: exec.signals,
|
|
225
|
-
executionId: exec.id,
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
runWithTimeout(promise, timeoutMs) {
|
|
229
|
-
if (!(promise instanceof Promise))
|
|
230
|
-
return Promise.resolve(promise);
|
|
231
|
-
if (timeoutMs <= 0)
|
|
232
|
-
return promise;
|
|
233
|
-
return new Promise((resolve, reject) => {
|
|
234
|
-
const timer = setTimeout(() => {
|
|
235
|
-
reject(new Error(`Step timed out after ${timeoutMs}ms`));
|
|
236
|
-
}, timeoutMs);
|
|
237
|
-
promise.then((v) => {
|
|
238
|
-
clearTimeout(timer);
|
|
239
|
-
resolve(v);
|
|
240
|
-
}, (e) => {
|
|
241
|
-
clearTimeout(timer);
|
|
242
|
-
reject(e instanceof Error ? e : new Error(String(e)));
|
|
243
|
-
});
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
240
|
}
|
|
247
241
|
//# sourceMappingURL=executor.js.map
|