import {setImmediate} from 'node:timers/promises'; import {performance} from 'node:perf_hooks'; import type {BranchState, NodeReturn, WorkflowDependencies} from '../../../types/bot/workflow/runtime.ts'; import type {WorkflowNode} from '../../../types/bot/workflow/workflow.ts'; import {node_registry} from '../registry.ts'; import {assert_json} from './value.ts'; export async function execute_node(node: WorkflowNode, state: BranchState, signal: AbortSignal, deps: WorkflowDependencies): Promise { const controller = new AbortController(); const combined = AbortSignal.any([signal, controller.signal]); const timeout_ms = node.timeout_ms ?? 30_000; const timeout_error = Object.assign(new Error('Node execution timed out'), {code: 'NODE_TIMEOUT'}); const started_at = performance.now(); const valid_timeout = Number.isFinite(timeout_ms) && timeout_ms > 0 && timeout_ms <= 2_147_483_647; let timer: ReturnType | undefined; try { if (valid_timeout) timer = setTimeout(() => controller.abort(timeout_error), timeout_ms); await setImmediate(); signal.throwIfAborted(); if (!valid_timeout) { throw Object.assign(new Error('Invalid node timeout'), {code: 'INVALID_NODE_TIMEOUT'}); } combined.throwIfAborted(); if (!Object.hasOwn(node_registry, node.type)) { throw Object.assign(new Error('Unknown workflow node type'), {code: 'UNKNOWN_NODE_TYPE'}); } // Await the handler itself so cancellation never leaves an operation running in the background. const returned = await node_registry[node.type].execute(node, state, combined, deps); signal.throwIfAborted(); if (performance.now() - started_at >= timeout_ms) controller.abort(timeout_error); combined.throwIfAborted(); assert_json(returned); if (typeof returned.port !== 'string' || returned.port.length === 0 || !returned.result || !['success', 'failure'].includes(returned.result.status) || !Object.hasOwn(returned.result, 'output') || (returned.result.error !== undefined && (typeof returned.result.error.code !== 'string' || typeof returned.result.error.message !== 'string' || typeof returned.result.error.node_id !== 'string')) || (returned.vars !== undefined && (returned.vars === null || Array.isArray(returned.vars) || typeof returned.vars !== 'object')) || (returned.retry_counts !== undefined && (returned.retry_counts === null || typeof returned.retry_counts !== 'object' || Array.isArray(returned.retry_counts) || Object.values(returned.retry_counts).some(value => !Number.isSafeInteger(value) || value < 0)))) { throw Object.assign(new Error('Invalid node return value'), {code: 'INVALID_NODE_RETURN'}); } if (!node_registry[node.type].config_template.ports.some(port => port.name === returned.port)) { throw Object.assign(new Error('Node returned an undeclared port'), {code: 'INVALID_NODE_PORT'}); } signal.throwIfAborted(); if (performance.now() - started_at >= timeout_ms) throw timeout_error; return returned; } catch (error) { signal.throwIfAborted(); const cause = controller.signal.aborted || (valid_timeout && performance.now() - started_at >= timeout_ms) ? timeout_error : error; const coded = cause as {code?: unknown; message?: unknown} | null; const code = typeof coded?.code === 'string' ? coded.code : 'NODE_EXECUTION_FAILED'; const message = typeof coded?.message === 'string' ? coded.message : String(cause); return {port: 'failure', result: {status: 'failure', output: null, error: {code: code.slice(0, 256), message: message.slice(0, 4096), node_id: node.id}}}; } finally { if (timer !== undefined) clearTimeout(timer); } }