@bratsos/workflow-engine-host-node 0.2.6 → 0.2.8
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/dist/index.js +24 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -73,13 +73,17 @@ var NodeHostImpl = class {
|
|
|
73
73
|
// Orchestration timer
|
|
74
74
|
// --------------------------------------------------------------------------
|
|
75
75
|
async orchestrationTick() {
|
|
76
|
+
this.orchestrationTicks++;
|
|
76
77
|
try {
|
|
77
|
-
this.orchestrationTicks++;
|
|
78
78
|
await this.kernel.dispatch({
|
|
79
79
|
type: "run.claimPending",
|
|
80
80
|
workerId: this.workerId,
|
|
81
81
|
maxClaims: this.maxClaimsPerTick
|
|
82
82
|
});
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error("[NodeHost] run.claimPending error:", error);
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
83
87
|
const pollResult = await this.kernel.dispatch({
|
|
84
88
|
type: "stage.pollSuspended",
|
|
85
89
|
maxChecks: this.maxSuspendedChecksPerTick
|
|
@@ -90,16 +94,32 @@ var NodeHostImpl = class {
|
|
|
90
94
|
workflowRunId
|
|
91
95
|
});
|
|
92
96
|
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error("[NodeHost] stage.pollSuspended error:", error);
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
93
101
|
await this.kernel.dispatch({
|
|
94
102
|
type: "lease.reapStale",
|
|
95
103
|
staleThresholdMs: this.staleLeaseThresholdMs
|
|
96
104
|
});
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error("[NodeHost] lease.reapStale error:", error);
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
97
109
|
await this.kernel.dispatch({
|
|
98
110
|
type: "outbox.flush",
|
|
99
111
|
maxEvents: this.maxOutboxFlushPerTick
|
|
100
112
|
});
|
|
101
113
|
} catch (error) {
|
|
102
|
-
console.error("[NodeHost]
|
|
114
|
+
console.error("[NodeHost] outbox.flush error:", error);
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
await this.kernel.dispatch({
|
|
118
|
+
type: "run.reapStuck",
|
|
119
|
+
stuckThresholdMs: Math.max(this.staleLeaseThresholdMs * 3, 5 * 6e4)
|
|
120
|
+
});
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error("[NodeHost] run.reapStuck error:", error);
|
|
103
123
|
}
|
|
104
124
|
}
|
|
105
125
|
// --------------------------------------------------------------------------
|
|
@@ -133,7 +153,8 @@ var NodeHostImpl = class {
|
|
|
133
153
|
const nextPollAt = result.nextPollAt ?? new Date(Date.now() + 6e4);
|
|
134
154
|
await this.jobTransport.suspend(job.jobId, nextPollAt);
|
|
135
155
|
} else if (result.outcome === "failed") {
|
|
136
|
-
const
|
|
156
|
+
const isGhostJob = result.error?.includes("ghost job discarded");
|
|
157
|
+
const canRetry = !isGhostJob && job.attempt < (job.maxAttempts ?? 3);
|
|
137
158
|
await this.jobTransport.fail(
|
|
138
159
|
job.jobId,
|
|
139
160
|
result.error ?? "Unknown error",
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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"]}
|
|
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 this.orchestrationTicks++;\n\n // 1. Claim pending runs → enqueue first-stage jobs\n try {\n await this.kernel.dispatch({\n type: \"run.claimPending\",\n workerId: this.workerId,\n maxClaims: this.maxClaimsPerTick,\n });\n } catch (error) {\n console.error(\"[NodeHost] run.claimPending error:\", error);\n }\n\n // 2. Poll suspended stages → resume if ready\n try {\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 } catch (error) {\n console.error(\"[NodeHost] stage.pollSuspended error:\", error);\n }\n\n // 3. Reap stale leases → release crashed worker locks\n try {\n await this.kernel.dispatch({\n type: \"lease.reapStale\",\n staleThresholdMs: this.staleLeaseThresholdMs,\n });\n } catch (error) {\n console.error(\"[NodeHost] lease.reapStale error:\", error);\n }\n\n // 4. Flush outbox → publish pending events through EventSink\n try {\n await this.kernel.dispatch({\n type: \"outbox.flush\",\n maxEvents: this.maxOutboxFlushPerTick,\n });\n } catch (error) {\n console.error(\"[NodeHost] outbox.flush error:\", error);\n }\n\n // 5. Reap stuck runs → fail runs with no activity past threshold\n try {\n await this.kernel.dispatch({\n type: \"run.reapStuck\",\n stuckThresholdMs: Math.max(this.staleLeaseThresholdMs * 3, 5 * 60_000),\n });\n } catch (error) {\n console.error(\"[NodeHost] run.reapStuck 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 // Ghost jobs (discarded by kernel because run is not RUNNING)\n // should never be retried — they'll just fail again.\n const isGhostJob = result.error?.includes(\"ghost job discarded\");\n const canRetry = !isGhostJob && 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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bratsos/workflow-engine-host-node",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "Node.js host for @bratsos/workflow-engine command kernel",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"dist"
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@bratsos/workflow-engine": "0.
|
|
29
|
+
"@bratsos/workflow-engine": "0.5.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^22",
|