Files
2026-10-22 17:07:08 +08:00

66 lines
3.9 KiB
TypeScript

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<NodeReturn> {
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<typeof setTimeout> | 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);
}
}