@bratsos/workflow-engine-host-node 0.2.2 → 0.2.7

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Alex Bratsos
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
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] Orchestration tick error:", error);
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 canRetry = job.attempt < (job.maxAttempts ?? 3);
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,10 +1,15 @@
1
1
  {
2
2
  "name": "@bratsos/workflow-engine-host-node",
3
- "version": "0.2.2",
3
+ "version": "0.2.7",
4
4
  "description": "Node.js host for @bratsos/workflow-engine command kernel",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Alex Bratsos",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/bratsos/workflow-engine",
11
+ "directory": "packages/workflow-engine-host-node"
12
+ },
8
13
  "main": "./dist/index.js",
9
14
  "types": "./dist/index.d.ts",
10
15
  "exports": {
@@ -20,13 +25,8 @@
20
25
  "files": [
21
26
  "dist"
22
27
  ],
23
- "scripts": {
24
- "build": "tsup",
25
- "test": "vitest run",
26
- "typecheck": "tsc --noEmit"
27
- },
28
28
  "dependencies": {
29
- "@bratsos/workflow-engine": "workspace:*"
29
+ "@bratsos/workflow-engine": "0.4.1"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^22",
@@ -37,5 +37,10 @@
37
37
  },
38
38
  "engines": {
39
39
  "node": ">=22.11.0"
40
+ },
41
+ "scripts": {
42
+ "build": "tsup",
43
+ "test": "vitest run",
44
+ "typecheck": "tsc --noEmit"
40
45
  }
41
- }
46
+ }