@bratsos/workflow-engine-host-node 0.2.9 → 0.3.0

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.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Kernel, JobTransport } from '@bratsos/workflow-engine/kernel';
1
+ import { Kernel, JobTransport, RunCreateCommand } from '@bratsos/workflow-engine/kernel';
2
2
 
3
3
  /**
4
4
  * Node Host for Workflow Engine Command Kernel
@@ -45,4 +45,50 @@ interface NodeHost {
45
45
  }
46
46
  declare function createNodeHost(config: NodeHostConfig): NodeHost;
47
47
 
48
- export { type HostStats, type NodeHost, type NodeHostConfig, createNodeHost };
48
+ interface RunAndWaitPersistence {
49
+ getRun(id: string): Promise<{
50
+ id: string;
51
+ status: string;
52
+ totalCost: number;
53
+ totalTokens: number;
54
+ startedAt: Date | null;
55
+ completedAt: Date | null;
56
+ duration: number | null;
57
+ output: unknown | null;
58
+ } | null>;
59
+ getStagesByRun(runId: string, options?: {
60
+ orderBy?: "asc" | "desc";
61
+ }): Promise<Array<{
62
+ stageId: string;
63
+ stageName: string;
64
+ status: string;
65
+ duration: number | null;
66
+ }>>;
67
+ }
68
+ interface StageStatus {
69
+ stageId: string;
70
+ stageName: string;
71
+ status: string;
72
+ duration: number | null;
73
+ }
74
+ interface RunAndWaitResult {
75
+ runId: string;
76
+ status: "COMPLETED" | "FAILED" | "CANCELLED";
77
+ stages: StageStatus[];
78
+ totalCost: number;
79
+ totalTokens: number;
80
+ duration: number | null;
81
+ output: unknown | null;
82
+ }
83
+ interface RunAndWaitOptions {
84
+ kernel: Kernel;
85
+ persistence: RunAndWaitPersistence;
86
+ host: NodeHost;
87
+ command: RunCreateCommand;
88
+ pollIntervalMs?: number;
89
+ onStageChange?: (stages: StageStatus[]) => void;
90
+ signal?: AbortSignal;
91
+ }
92
+ declare function runAndWait(options: RunAndWaitOptions): Promise<RunAndWaitResult>;
93
+
94
+ export { type HostStats, type NodeHost, type NodeHostConfig, type RunAndWaitOptions, type RunAndWaitPersistence, type RunAndWaitResult, type StageStatus, createNodeHost, runAndWait };
package/dist/index.js CHANGED
@@ -153,8 +153,7 @@ var NodeHostImpl = class {
153
153
  const nextPollAt = result.nextPollAt ?? new Date(Date.now() + 6e4);
154
154
  await this.jobTransport.suspend(job.jobId, nextPollAt);
155
155
  } else if (result.outcome === "failed") {
156
- const isGhostJob = result.error?.includes("ghost job discarded");
157
- const canRetry = !isGhostJob && job.attempt < (job.maxAttempts ?? 3);
156
+ const canRetry = !result.ghost && job.attempt < (job.maxAttempts ?? 3);
158
157
  await this.jobTransport.fail(
159
158
  job.jobId,
160
159
  result.error ?? "Unknown error",
@@ -178,6 +177,100 @@ function createNodeHost(config) {
178
177
  return new NodeHostImpl(config);
179
178
  }
180
179
 
181
- export { createNodeHost };
180
+ // src/run-and-wait.ts
181
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
182
+ "COMPLETED",
183
+ "FAILED",
184
+ "CANCELLED"
185
+ ]);
186
+ function isTerminalStatus(status) {
187
+ return TERMINAL_STATUSES.has(status);
188
+ }
189
+ async function runAndWait(options) {
190
+ const {
191
+ kernel,
192
+ persistence,
193
+ host,
194
+ command,
195
+ pollIntervalMs = 3e3,
196
+ onStageChange,
197
+ signal
198
+ } = options;
199
+ const createResult = await kernel.dispatch(command);
200
+ const runId = createResult.workflowRunId;
201
+ const wasAlreadyRunning = host.getStats().isRunning;
202
+ if (!wasAlreadyRunning) {
203
+ await host.start();
204
+ }
205
+ try {
206
+ return await pollUntilDone({
207
+ runId,
208
+ persistence,
209
+ pollIntervalMs,
210
+ onStageChange,
211
+ signal
212
+ });
213
+ } finally {
214
+ if (!wasAlreadyRunning) {
215
+ await host.stop();
216
+ }
217
+ }
218
+ }
219
+ async function pollUntilDone(opts) {
220
+ const { runId, persistence, pollIntervalMs, onStageChange, signal } = opts;
221
+ let previousStageSnapshot = "";
222
+ while (true) {
223
+ if (signal?.aborted) {
224
+ throw new Error("runAndWait aborted");
225
+ }
226
+ const run = await persistence.getRun(runId);
227
+ if (!run) {
228
+ throw new Error(`Run ${runId} not found`);
229
+ }
230
+ const stages = await persistence.getStagesByRun(runId, { orderBy: "asc" });
231
+ const stageStatuses = stages.map((s) => ({
232
+ stageId: s.stageId,
233
+ stageName: s.stageName,
234
+ status: s.status,
235
+ duration: s.duration
236
+ }));
237
+ const stageSnapshot = JSON.stringify(stageStatuses);
238
+ if (onStageChange && stageSnapshot !== previousStageSnapshot) {
239
+ previousStageSnapshot = stageSnapshot;
240
+ onStageChange(stageStatuses);
241
+ }
242
+ if (isTerminalStatus(run.status)) {
243
+ return {
244
+ runId,
245
+ status: run.status,
246
+ stages: stageStatuses,
247
+ totalCost: run.totalCost,
248
+ totalTokens: run.totalTokens,
249
+ duration: run.duration,
250
+ output: run.output
251
+ };
252
+ }
253
+ await sleep(pollIntervalMs, signal);
254
+ }
255
+ }
256
+ function sleep(ms, signal) {
257
+ return new Promise((resolve, reject) => {
258
+ if (signal?.aborted) {
259
+ reject(new Error("runAndWait aborted"));
260
+ return;
261
+ }
262
+ const id = setTimeout(resolve, ms);
263
+ signal?.addEventListener(
264
+ "abort",
265
+ () => {
266
+ clearTimeout(id);
267
+ reject(new Error("runAndWait aborted"));
268
+ },
269
+ { once: true }
270
+ );
271
+ });
272
+ }
273
+
274
+ export { createNodeHost, runAndWait };
182
275
  //# sourceMappingURL=index.js.map
183
276
  //# sourceMappingURL=index.js.map
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 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"]}
1
+ {"version":3,"sources":["../src/host.ts","../src/run-and-wait.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 canRetry =\n !result.ghost && 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","// packages/workflow-engine-host-node/src/run-and-wait.ts\nimport type { Kernel, RunCreateCommand } from \"@bratsos/workflow-engine/kernel\";\nimport type { NodeHost } from \"./host.js\";\n\nexport interface RunAndWaitPersistence {\n getRun(id: string): Promise<{\n id: string;\n status: string;\n totalCost: number;\n totalTokens: number;\n startedAt: Date | null;\n completedAt: Date | null;\n duration: number | null;\n output: unknown | null;\n } | null>;\n getStagesByRun(\n runId: string,\n options?: { orderBy?: \"asc\" | \"desc\" },\n ): Promise<\n Array<{\n stageId: string;\n stageName: string;\n status: string;\n duration: number | null;\n }>\n >;\n}\n\nexport interface StageStatus {\n stageId: string;\n stageName: string;\n status: string;\n duration: number | null;\n}\n\nexport interface RunAndWaitResult {\n runId: string;\n status: \"COMPLETED\" | \"FAILED\" | \"CANCELLED\";\n stages: StageStatus[];\n totalCost: number;\n totalTokens: number;\n duration: number | null;\n output: unknown | null;\n}\n\nexport interface RunAndWaitOptions {\n kernel: Kernel;\n persistence: RunAndWaitPersistence;\n host: NodeHost;\n command: RunCreateCommand;\n pollIntervalMs?: number;\n onStageChange?: (stages: StageStatus[]) => void;\n signal?: AbortSignal;\n}\n\nconst TERMINAL_STATUSES = new Set<RunAndWaitResult[\"status\"]>([\n \"COMPLETED\",\n \"FAILED\",\n \"CANCELLED\",\n]);\n\nfunction isTerminalStatus(\n status: string,\n): status is RunAndWaitResult[\"status\"] {\n return TERMINAL_STATUSES.has(status as RunAndWaitResult[\"status\"]);\n}\n\nexport async function runAndWait(\n options: RunAndWaitOptions,\n): Promise<RunAndWaitResult> {\n const {\n kernel,\n persistence,\n host,\n command,\n pollIntervalMs = 3_000,\n onStageChange,\n signal,\n } = options;\n\n const createResult = await kernel.dispatch(command);\n const runId = createResult.workflowRunId;\n\n const wasAlreadyRunning = host.getStats().isRunning;\n if (!wasAlreadyRunning) {\n await host.start();\n }\n\n try {\n return await pollUntilDone({\n runId,\n persistence,\n pollIntervalMs,\n onStageChange,\n signal,\n });\n } finally {\n if (!wasAlreadyRunning) {\n await host.stop();\n }\n }\n}\n\nasync function pollUntilDone(opts: {\n runId: string;\n persistence: RunAndWaitPersistence;\n pollIntervalMs: number;\n onStageChange?: (stages: StageStatus[]) => void;\n signal?: AbortSignal;\n}): Promise<RunAndWaitResult> {\n const { runId, persistence, pollIntervalMs, onStageChange, signal } = opts;\n let previousStageSnapshot = \"\";\n\n while (true) {\n if (signal?.aborted) {\n throw new Error(\"runAndWait aborted\");\n }\n\n const run = await persistence.getRun(runId);\n if (!run) {\n throw new Error(`Run ${runId} not found`);\n }\n\n const stages = await persistence.getStagesByRun(runId, { orderBy: \"asc\" });\n const stageStatuses: StageStatus[] = stages.map((s) => ({\n stageId: s.stageId,\n stageName: s.stageName,\n status: s.status,\n duration: s.duration,\n }));\n\n const stageSnapshot = JSON.stringify(stageStatuses);\n if (onStageChange && stageSnapshot !== previousStageSnapshot) {\n previousStageSnapshot = stageSnapshot;\n onStageChange(stageStatuses);\n }\n\n if (isTerminalStatus(run.status)) {\n return {\n runId,\n status: run.status,\n stages: stageStatuses,\n totalCost: run.totalCost,\n totalTokens: run.totalTokens,\n duration: run.duration,\n output: run.output,\n };\n }\n\n await sleep(pollIntervalMs, signal);\n }\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(new Error(\"runAndWait aborted\"));\n return;\n }\n const id = setTimeout(resolve, ms);\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(id);\n reject(new Error(\"runAndWait aborted\"));\n },\n { once: true },\n );\n });\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bratsos/workflow-engine-host-node",
3
- "version": "0.2.9",
3
+ "version": "0.3.0",
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.5.1"
29
+ "@bratsos/workflow-engine": "0.7.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^22",