@bratsos/workflow-engine-host-node 0.2.2
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 +120 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +162 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# @bratsos/workflow-engine-host-node
|
|
2
|
+
|
|
3
|
+
Node.js host for the [`@bratsos/workflow-engine`](../workflow-engine) command kernel. Provides process loops, signal handling, and continuous job processing.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @bratsos/workflow-engine-host-node
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { createKernel } from "@bratsos/workflow-engine/kernel";
|
|
15
|
+
import { createNodeHost } from "@bratsos/workflow-engine-host-node";
|
|
16
|
+
import { createPrismaJobQueue } from "@bratsos/workflow-engine";
|
|
17
|
+
|
|
18
|
+
const kernel = createKernel({ /* ... */ });
|
|
19
|
+
const jobTransport = createPrismaJobQueue(prisma);
|
|
20
|
+
|
|
21
|
+
const host = createNodeHost({
|
|
22
|
+
kernel,
|
|
23
|
+
jobTransport,
|
|
24
|
+
workerId: "worker-1",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
await host.start();
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## API
|
|
31
|
+
|
|
32
|
+
### `createNodeHost(config): NodeHost`
|
|
33
|
+
|
|
34
|
+
Creates a new Node host instance.
|
|
35
|
+
|
|
36
|
+
### `NodeHostConfig`
|
|
37
|
+
|
|
38
|
+
| Option | Type | Default | Description |
|
|
39
|
+
|--------|------|---------|-------------|
|
|
40
|
+
| `kernel` | `Kernel` | required | Kernel instance to dispatch commands to |
|
|
41
|
+
| `jobTransport` | `JobTransport` | required | Job transport for dequeue/complete/suspend/fail |
|
|
42
|
+
| `workerId` | `string` | required | Unique worker identifier |
|
|
43
|
+
| `orchestrationIntervalMs` | `number` | `10_000` | Interval for claim/poll/reap/flush orchestration tick |
|
|
44
|
+
| `jobPollIntervalMs` | `number` | `1_000` | Interval for polling job queue when empty |
|
|
45
|
+
| `staleLeaseThresholdMs` | `number` | `60_000` | Time before a job lease is considered stale |
|
|
46
|
+
| `maxClaimsPerTick` | `number` | `10` | Max pending runs to claim per orchestration tick |
|
|
47
|
+
| `maxSuspendedChecksPerTick` | `number` | `10` | Max suspended stages to poll per tick |
|
|
48
|
+
| `maxOutboxFlushPerTick` | `number` | `100` | Max outbox events to flush per tick |
|
|
49
|
+
|
|
50
|
+
### `NodeHost`
|
|
51
|
+
|
|
52
|
+
| Method | Returns | Description |
|
|
53
|
+
|--------|---------|-------------|
|
|
54
|
+
| `start()` | `Promise<void>` | Start polling loops and register SIGTERM/SIGINT handlers |
|
|
55
|
+
| `stop()` | `Promise<void>` | Graceful shutdown -- clears timers and signal handlers |
|
|
56
|
+
| `getStats()` | `HostStats` | Runtime statistics |
|
|
57
|
+
|
|
58
|
+
### `HostStats`
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
interface HostStats {
|
|
62
|
+
workerId: string;
|
|
63
|
+
jobsProcessed: number;
|
|
64
|
+
orchestrationTicks: number;
|
|
65
|
+
isRunning: boolean;
|
|
66
|
+
uptimeMs: number;
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## How It Works
|
|
71
|
+
|
|
72
|
+
The host runs two concurrent loops:
|
|
73
|
+
|
|
74
|
+
1. **Orchestration timer** (every `orchestrationIntervalMs`):
|
|
75
|
+
- `run.claimPending` -- claim pending runs, enqueue first-stage jobs
|
|
76
|
+
- `stage.pollSuspended` -- check if suspended stages are ready to resume
|
|
77
|
+
- `lease.reapStale` -- release stale job leases from crashed workers
|
|
78
|
+
- `outbox.flush` -- publish pending events through EventSink
|
|
79
|
+
|
|
80
|
+
2. **Job processing loop** (continuous):
|
|
81
|
+
- Dequeue next job from `jobTransport`
|
|
82
|
+
- Dispatch `job.execute` to the kernel
|
|
83
|
+
- On completion: mark complete, dispatch `run.transition`
|
|
84
|
+
- On suspension: mark suspended with next poll time
|
|
85
|
+
- On failure: mark failed with retry flag
|
|
86
|
+
- Sleep `jobPollIntervalMs` when queue is empty
|
|
87
|
+
|
|
88
|
+
Signal handlers (`SIGTERM`, `SIGINT`) automatically call `stop()` for graceful shutdown.
|
|
89
|
+
|
|
90
|
+
## Worker Process Pattern
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// worker.ts
|
|
94
|
+
import { host } from "./setup";
|
|
95
|
+
|
|
96
|
+
await host.start();
|
|
97
|
+
// Host runs until SIGTERM/SIGINT or host.stop() is called
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npx tsx worker.ts
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Multi-Worker
|
|
105
|
+
|
|
106
|
+
Multiple workers can share the same database. Each needs a unique `workerId`:
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
// worker-1
|
|
110
|
+
createNodeHost({ kernel, jobTransport, workerId: "worker-1" });
|
|
111
|
+
|
|
112
|
+
// worker-2
|
|
113
|
+
createNodeHost({ kernel, jobTransport, workerId: "worker-2" });
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Run claiming uses `FOR UPDATE SKIP LOCKED` in PostgreSQL to prevent race conditions.
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Kernel, JobTransport } from '@bratsos/workflow-engine/kernel';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Node Host for Workflow Engine Command Kernel
|
|
5
|
+
*
|
|
6
|
+
* Wraps the environment-agnostic kernel with Node.js process loops,
|
|
7
|
+
* signal handling, and job processing. The host dispatches kernel
|
|
8
|
+
* commands on intervals and manages the job dequeue/execute cycle.
|
|
9
|
+
*
|
|
10
|
+
* The kernel remains unaware of process state — all timers, signals,
|
|
11
|
+
* and loop pacing live here.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
interface NodeHostConfig {
|
|
15
|
+
/** Kernel instance to dispatch commands to. */
|
|
16
|
+
kernel: Kernel;
|
|
17
|
+
/** Job transport for dequeue/complete/suspend/fail. */
|
|
18
|
+
jobTransport: JobTransport;
|
|
19
|
+
/** Unique worker identifier. */
|
|
20
|
+
workerId: string;
|
|
21
|
+
/** Orchestration poll interval in milliseconds (default: 10_000). */
|
|
22
|
+
orchestrationIntervalMs?: number;
|
|
23
|
+
/** Job dequeue poll interval when queue is empty (default: 1_000). */
|
|
24
|
+
jobPollIntervalMs?: number;
|
|
25
|
+
/** Stale lease threshold in milliseconds (default: 60_000). */
|
|
26
|
+
staleLeaseThresholdMs?: number;
|
|
27
|
+
/** Max pending runs to claim per orchestration tick (default: 10). */
|
|
28
|
+
maxClaimsPerTick?: number;
|
|
29
|
+
/** Max suspended stages to check per tick (default: 10). */
|
|
30
|
+
maxSuspendedChecksPerTick?: number;
|
|
31
|
+
/** Max outbox events to flush per tick (default: 100). */
|
|
32
|
+
maxOutboxFlushPerTick?: number;
|
|
33
|
+
}
|
|
34
|
+
interface HostStats {
|
|
35
|
+
workerId: string;
|
|
36
|
+
jobsProcessed: number;
|
|
37
|
+
orchestrationTicks: number;
|
|
38
|
+
isRunning: boolean;
|
|
39
|
+
uptimeMs: number;
|
|
40
|
+
}
|
|
41
|
+
interface NodeHost {
|
|
42
|
+
start(): Promise<void>;
|
|
43
|
+
stop(): Promise<void>;
|
|
44
|
+
getStats(): HostStats;
|
|
45
|
+
}
|
|
46
|
+
declare function createNodeHost(config: NodeHostConfig): NodeHost;
|
|
47
|
+
|
|
48
|
+
export { type HostStats, type NodeHost, type NodeHostConfig, createNodeHost };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// src/host.ts
|
|
2
|
+
var NodeHostImpl = class {
|
|
3
|
+
running = false;
|
|
4
|
+
jobsProcessed = 0;
|
|
5
|
+
orchestrationTicks = 0;
|
|
6
|
+
startTime = 0;
|
|
7
|
+
orchestrationTimer = null;
|
|
8
|
+
signalHandlers = [];
|
|
9
|
+
kernel;
|
|
10
|
+
jobTransport;
|
|
11
|
+
workerId;
|
|
12
|
+
orchestrationIntervalMs;
|
|
13
|
+
jobPollIntervalMs;
|
|
14
|
+
staleLeaseThresholdMs;
|
|
15
|
+
maxClaimsPerTick;
|
|
16
|
+
maxSuspendedChecksPerTick;
|
|
17
|
+
maxOutboxFlushPerTick;
|
|
18
|
+
constructor(config) {
|
|
19
|
+
this.kernel = config.kernel;
|
|
20
|
+
this.jobTransport = config.jobTransport;
|
|
21
|
+
this.workerId = config.workerId;
|
|
22
|
+
this.orchestrationIntervalMs = config.orchestrationIntervalMs ?? 1e4;
|
|
23
|
+
this.jobPollIntervalMs = config.jobPollIntervalMs ?? 1e3;
|
|
24
|
+
this.staleLeaseThresholdMs = config.staleLeaseThresholdMs ?? 6e4;
|
|
25
|
+
this.maxClaimsPerTick = config.maxClaimsPerTick ?? 10;
|
|
26
|
+
this.maxSuspendedChecksPerTick = config.maxSuspendedChecksPerTick ?? 10;
|
|
27
|
+
this.maxOutboxFlushPerTick = config.maxOutboxFlushPerTick ?? 100;
|
|
28
|
+
}
|
|
29
|
+
// --------------------------------------------------------------------------
|
|
30
|
+
// Lifecycle
|
|
31
|
+
// --------------------------------------------------------------------------
|
|
32
|
+
async start() {
|
|
33
|
+
if (this.running) return;
|
|
34
|
+
this.running = true;
|
|
35
|
+
this.startTime = Date.now();
|
|
36
|
+
this.orchestrationTimer = setInterval(
|
|
37
|
+
() => void this.orchestrationTick(),
|
|
38
|
+
this.orchestrationIntervalMs
|
|
39
|
+
);
|
|
40
|
+
void this.orchestrationTick();
|
|
41
|
+
void this.processJobs();
|
|
42
|
+
const onSignal = () => void this.stop();
|
|
43
|
+
this.signalHandlers = [
|
|
44
|
+
{ signal: "SIGTERM", handler: onSignal },
|
|
45
|
+
{ signal: "SIGINT", handler: onSignal }
|
|
46
|
+
];
|
|
47
|
+
for (const { signal, handler } of this.signalHandlers) {
|
|
48
|
+
process.once(signal, handler);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async stop() {
|
|
52
|
+
if (!this.running) return;
|
|
53
|
+
this.running = false;
|
|
54
|
+
if (this.orchestrationTimer) {
|
|
55
|
+
clearInterval(this.orchestrationTimer);
|
|
56
|
+
this.orchestrationTimer = null;
|
|
57
|
+
}
|
|
58
|
+
for (const { signal, handler } of this.signalHandlers) {
|
|
59
|
+
process.removeListener(signal, handler);
|
|
60
|
+
}
|
|
61
|
+
this.signalHandlers = [];
|
|
62
|
+
}
|
|
63
|
+
getStats() {
|
|
64
|
+
return {
|
|
65
|
+
workerId: this.workerId,
|
|
66
|
+
jobsProcessed: this.jobsProcessed,
|
|
67
|
+
orchestrationTicks: this.orchestrationTicks,
|
|
68
|
+
isRunning: this.running,
|
|
69
|
+
uptimeMs: this.running ? Date.now() - this.startTime : 0
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// --------------------------------------------------------------------------
|
|
73
|
+
// Orchestration timer
|
|
74
|
+
// --------------------------------------------------------------------------
|
|
75
|
+
async orchestrationTick() {
|
|
76
|
+
try {
|
|
77
|
+
this.orchestrationTicks++;
|
|
78
|
+
await this.kernel.dispatch({
|
|
79
|
+
type: "run.claimPending",
|
|
80
|
+
workerId: this.workerId,
|
|
81
|
+
maxClaims: this.maxClaimsPerTick
|
|
82
|
+
});
|
|
83
|
+
const pollResult = await this.kernel.dispatch({
|
|
84
|
+
type: "stage.pollSuspended",
|
|
85
|
+
maxChecks: this.maxSuspendedChecksPerTick
|
|
86
|
+
});
|
|
87
|
+
for (const workflowRunId of pollResult.resumedWorkflowRunIds) {
|
|
88
|
+
await this.kernel.dispatch({
|
|
89
|
+
type: "run.transition",
|
|
90
|
+
workflowRunId
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
await this.kernel.dispatch({
|
|
94
|
+
type: "lease.reapStale",
|
|
95
|
+
staleThresholdMs: this.staleLeaseThresholdMs
|
|
96
|
+
});
|
|
97
|
+
await this.kernel.dispatch({
|
|
98
|
+
type: "outbox.flush",
|
|
99
|
+
maxEvents: this.maxOutboxFlushPerTick
|
|
100
|
+
});
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error("[NodeHost] Orchestration tick error:", error);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// --------------------------------------------------------------------------
|
|
106
|
+
// Job processing loop
|
|
107
|
+
// --------------------------------------------------------------------------
|
|
108
|
+
async processJobs() {
|
|
109
|
+
while (this.running) {
|
|
110
|
+
try {
|
|
111
|
+
const job = await this.jobTransport.dequeue();
|
|
112
|
+
if (!job) {
|
|
113
|
+
await this.sleep(this.jobPollIntervalMs);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const config = job.payload.config || {};
|
|
117
|
+
const result = await this.kernel.dispatch({
|
|
118
|
+
type: "job.execute",
|
|
119
|
+
idempotencyKey: `job:${job.jobId}:attempt:${job.attempt}`,
|
|
120
|
+
workflowRunId: job.workflowRunId,
|
|
121
|
+
workflowId: job.workflowId,
|
|
122
|
+
stageId: job.stageId,
|
|
123
|
+
config
|
|
124
|
+
});
|
|
125
|
+
this.jobsProcessed++;
|
|
126
|
+
if (result.outcome === "completed") {
|
|
127
|
+
await this.jobTransport.complete(job.jobId);
|
|
128
|
+
await this.kernel.dispatch({
|
|
129
|
+
type: "run.transition",
|
|
130
|
+
workflowRunId: job.workflowRunId
|
|
131
|
+
});
|
|
132
|
+
} else if (result.outcome === "suspended") {
|
|
133
|
+
const nextPollAt = result.nextPollAt ?? new Date(Date.now() + 6e4);
|
|
134
|
+
await this.jobTransport.suspend(job.jobId, nextPollAt);
|
|
135
|
+
} else if (result.outcome === "failed") {
|
|
136
|
+
const canRetry = job.attempt < (job.maxAttempts ?? 3);
|
|
137
|
+
await this.jobTransport.fail(
|
|
138
|
+
job.jobId,
|
|
139
|
+
result.error ?? "Unknown error",
|
|
140
|
+
canRetry
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error("[NodeHost] Job processing error:", error);
|
|
145
|
+
await this.sleep(5e3);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// --------------------------------------------------------------------------
|
|
150
|
+
// Helpers
|
|
151
|
+
// --------------------------------------------------------------------------
|
|
152
|
+
sleep(ms) {
|
|
153
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
function createNodeHost(config) {
|
|
157
|
+
return new NodeHostImpl(config);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export { createNodeHost };
|
|
161
|
+
//# sourceMappingURL=index.js.map
|
|
162
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/host.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Node Host for Workflow Engine Command Kernel\n *\n * Wraps the environment-agnostic kernel with Node.js process loops,\n * signal handling, and job processing. The host dispatches kernel\n * commands on intervals and manages the job dequeue/execute cycle.\n *\n * The kernel remains unaware of process state — all timers, signals,\n * and loop pacing live here.\n */\n\nimport type { JobTransport, Kernel } from \"@bratsos/workflow-engine/kernel\";\n\n// ============================================================================\n// Public interfaces\n// ============================================================================\n\nexport interface NodeHostConfig {\n /** Kernel instance to dispatch commands to. */\n kernel: Kernel;\n\n /** Job transport for dequeue/complete/suspend/fail. */\n jobTransport: JobTransport;\n\n /** Unique worker identifier. */\n workerId: string;\n\n /** Orchestration poll interval in milliseconds (default: 10_000). */\n orchestrationIntervalMs?: number;\n\n /** Job dequeue poll interval when queue is empty (default: 1_000). */\n jobPollIntervalMs?: number;\n\n /** Stale lease threshold in milliseconds (default: 60_000). */\n staleLeaseThresholdMs?: number;\n\n /** Max pending runs to claim per orchestration tick (default: 10). */\n maxClaimsPerTick?: number;\n\n /** Max suspended stages to check per tick (default: 10). */\n maxSuspendedChecksPerTick?: number;\n\n /** Max outbox events to flush per tick (default: 100). */\n maxOutboxFlushPerTick?: number;\n}\n\nexport interface HostStats {\n workerId: string;\n jobsProcessed: number;\n orchestrationTicks: number;\n isRunning: boolean;\n uptimeMs: number;\n}\n\nexport interface NodeHost {\n start(): Promise<void>;\n stop(): Promise<void>;\n getStats(): HostStats;\n}\n\n// ============================================================================\n// Implementation\n// ============================================================================\n\nclass NodeHostImpl implements NodeHost {\n private running = false;\n private jobsProcessed = 0;\n private orchestrationTicks = 0;\n private startTime = 0;\n private orchestrationTimer: ReturnType<typeof setInterval> | null = null;\n private signalHandlers: { signal: string; handler: () => void }[] = [];\n\n private readonly kernel: Kernel;\n private readonly jobTransport: JobTransport;\n private readonly workerId: string;\n private readonly orchestrationIntervalMs: number;\n private readonly jobPollIntervalMs: number;\n private readonly staleLeaseThresholdMs: number;\n private readonly maxClaimsPerTick: number;\n private readonly maxSuspendedChecksPerTick: number;\n private readonly maxOutboxFlushPerTick: number;\n\n constructor(config: NodeHostConfig) {\n this.kernel = config.kernel;\n this.jobTransport = config.jobTransport;\n this.workerId = config.workerId;\n this.orchestrationIntervalMs = config.orchestrationIntervalMs ?? 10_000;\n this.jobPollIntervalMs = config.jobPollIntervalMs ?? 1_000;\n this.staleLeaseThresholdMs = config.staleLeaseThresholdMs ?? 60_000;\n this.maxClaimsPerTick = config.maxClaimsPerTick ?? 10;\n this.maxSuspendedChecksPerTick = config.maxSuspendedChecksPerTick ?? 10;\n this.maxOutboxFlushPerTick = config.maxOutboxFlushPerTick ?? 100;\n }\n\n // --------------------------------------------------------------------------\n // Lifecycle\n // --------------------------------------------------------------------------\n\n async start(): Promise<void> {\n if (this.running) return;\n\n this.running = true;\n this.startTime = Date.now();\n\n // Start orchestration timer\n this.orchestrationTimer = setInterval(\n () => void this.orchestrationTick(),\n this.orchestrationIntervalMs,\n );\n\n // Immediate first tick\n void this.orchestrationTick();\n\n // Start job processing loop (runs until stop())\n void this.processJobs();\n\n // Signal handlers — use wrapper functions so we can remove them on stop\n const onSignal = () => void this.stop();\n this.signalHandlers = [\n { signal: \"SIGTERM\", handler: onSignal },\n { signal: \"SIGINT\", handler: onSignal },\n ];\n for (const { signal, handler } of this.signalHandlers) {\n process.once(signal as NodeJS.Signals, handler);\n }\n }\n\n async stop(): Promise<void> {\n if (!this.running) return;\n\n this.running = false;\n\n if (this.orchestrationTimer) {\n clearInterval(this.orchestrationTimer);\n this.orchestrationTimer = null;\n }\n\n // Remove signal handlers to avoid leaks\n for (const { signal, handler } of this.signalHandlers) {\n process.removeListener(signal, handler);\n }\n this.signalHandlers = [];\n }\n\n getStats(): HostStats {\n return {\n workerId: this.workerId,\n jobsProcessed: this.jobsProcessed,\n orchestrationTicks: this.orchestrationTicks,\n isRunning: this.running,\n uptimeMs: this.running ? Date.now() - this.startTime : 0,\n };\n }\n\n // --------------------------------------------------------------------------\n // Orchestration timer\n // --------------------------------------------------------------------------\n\n private async orchestrationTick(): Promise<void> {\n try {\n this.orchestrationTicks++;\n\n // 1. Claim pending runs → enqueue first-stage jobs\n await this.kernel.dispatch({\n type: \"run.claimPending\",\n workerId: this.workerId,\n maxClaims: this.maxClaimsPerTick,\n });\n\n // 2. Poll suspended stages → resume if ready\n const pollResult = await this.kernel.dispatch({\n type: \"stage.pollSuspended\",\n maxChecks: this.maxSuspendedChecksPerTick,\n });\n for (const workflowRunId of pollResult.resumedWorkflowRunIds) {\n await this.kernel.dispatch({\n type: \"run.transition\",\n workflowRunId,\n });\n }\n\n // 3. Reap stale leases → release crashed worker locks\n await this.kernel.dispatch({\n type: \"lease.reapStale\",\n staleThresholdMs: this.staleLeaseThresholdMs,\n });\n\n // 4. Flush outbox → publish pending events through EventSink\n await this.kernel.dispatch({\n type: \"outbox.flush\",\n maxEvents: this.maxOutboxFlushPerTick,\n });\n } catch (error) {\n // Orchestration errors are non-fatal — the next tick will retry\n console.error(\"[NodeHost] Orchestration tick error:\", error);\n }\n }\n\n // --------------------------------------------------------------------------\n // Job processing loop\n // --------------------------------------------------------------------------\n\n private async processJobs(): Promise<void> {\n while (this.running) {\n try {\n const job = await this.jobTransport.dequeue();\n\n if (!job) {\n await this.sleep(this.jobPollIntervalMs);\n continue;\n }\n\n const config =\n (job.payload as { config?: Record<string, unknown> }).config || {};\n\n const result = await this.kernel.dispatch({\n type: \"job.execute\",\n idempotencyKey: `job:${job.jobId}:attempt:${job.attempt}`,\n workflowRunId: job.workflowRunId,\n workflowId: job.workflowId,\n stageId: job.stageId,\n config,\n });\n\n this.jobsProcessed++;\n\n if (result.outcome === \"completed\") {\n await this.jobTransport.complete(job.jobId);\n await this.kernel.dispatch({\n type: \"run.transition\",\n workflowRunId: job.workflowRunId,\n });\n } else if (result.outcome === \"suspended\") {\n const nextPollAt = result.nextPollAt ?? new Date(Date.now() + 60_000);\n await this.jobTransport.suspend(job.jobId, nextPollAt);\n } else if (result.outcome === \"failed\") {\n const canRetry = job.attempt < (job.maxAttempts ?? 3);\n await this.jobTransport.fail(\n job.jobId,\n result.error ?? \"Unknown error\",\n canRetry,\n );\n }\n } catch (error) {\n // Job processing errors are non-fatal — back off and retry\n console.error(\"[NodeHost] Job processing error:\", error);\n await this.sleep(5_000);\n }\n }\n }\n\n // --------------------------------------------------------------------------\n // Helpers\n // --------------------------------------------------------------------------\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n\n// ============================================================================\n// Factory\n// ============================================================================\n\nexport function createNodeHost(config: NodeHostConfig): NodeHost {\n return new NodeHostImpl(config);\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bratsos/workflow-engine-host-node",
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "Node.js host for @bratsos/workflow-engine command kernel",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Alex Bratsos",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsup",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"typecheck": "tsc --noEmit"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@bratsos/workflow-engine": "workspace:*"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^22",
|
|
33
|
+
"tsup": "^8.5.1",
|
|
34
|
+
"typescript": "^5.8.3",
|
|
35
|
+
"vitest": "^3.2.4",
|
|
36
|
+
"zod": "^4.1.12"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=22.11.0"
|
|
40
|
+
}
|
|
41
|
+
}
|