Files
node_server/bot/workflow/runtime/compile.ts
T
2026-10-22 17:07:08 +08:00

74 lines
4.0 KiB
TypeScript

import type {CompiledWorkflow} from '../../../types/bot/workflow/runtime.ts';
import {validate_workflow} from './validate.ts';
import {assert_json} from './value.ts';
import {resolve_fields} from './template.ts';
import {node_registry, start_registry} from '../registry.ts';
import {node_fields, policy_fields} from '../common_template.ts';
import type {Json} from '../../../types/bot/workflow/workflow.ts';
export function compile_workflow(value: unknown): CompiledWorkflow {
assert_json(value);
const normalized = structuredClone(value) as Record<string, any>;
if (normalized && !Array.isArray(normalized) && typeof normalized === 'object') {
const start = normalized.start;
if (start && typeof start.type === 'string' && Object.hasOwn(start_registry, start.type)) {
const module = start_registry[start.type as keyof typeof start_registry];
normalized.start = resolve_fields(module.config_template.config, start);
}
if (normalized.policy !== null && (normalized.policy === undefined || typeof normalized.policy === 'object' && !Array.isArray(normalized.policy))) {
normalized.policy = resolve_fields(policy_fields, normalized.policy ?? {});
}
if (Array.isArray(normalized.nodes)) normalized.nodes = normalized.nodes.map((node: Record<string, any>) => {
if (!node || typeof node !== 'object' || Array.isArray(node) || typeof node.type !== 'string' || !Object.hasOwn(node_registry, node.type)) return node;
const fields = node_registry[node.type as keyof typeof node_registry].config_template.config;
const result = resolve_fields(node_fields, node as Record<string, Json>);
if (node.config && typeof node.config === 'object' && !Array.isArray(node.config)) result.config = resolve_fields(fields, node.config);
return result;
});
}
validate_workflow(normalized);
const definition = normalized;
const graph: CompiledWorkflow = {
definition,
nodes: new Map(definition.nodes.map(node => [node.id, node])),
outgoing: new Map(), incoming: new Map(), join_owners: new Map(),
};
for (const edge of definition.edges) {
graph.outgoing.set(edge.source, [...graph.outgoing.get(edge.source) ?? [], edge]);
graph.incoming.set(edge.target, [...graph.incoming.get(edge.target) ?? [], edge]);
}
graph.join_owners = find_join_owners(graph);
return graph;
}
/** The nearest dominating fork owns each join, including joins downstream of other joins. */
export function find_join_owners(graph: CompiledWorkflow): Map<string, string> {
const ids = [...graph.nodes.keys()];
const dominators = new Map(ids.map(id => [id, new Set(id === graph.definition.entry ? [id] : ids)]));
let changed = true;
while (changed) {
changed = false;
for (const id of ids) {
if (id === graph.definition.entry) continue;
const predecessors = (graph.incoming.get(id) ?? []).map(edge => edge.source);
const common = new Set(dominators.get(predecessors[0]) ?? []);
for (const predecessor of predecessors.slice(1)) {
for (const candidate of common) if (!dominators.get(predecessor)!.has(candidate)) common.delete(candidate);
}
common.add(id);
const previous = dominators.get(id)!;
if (common.size !== previous.size || [...common].some(item => !previous.has(item))) {
dominators.set(id, common); changed = true;
}
}
}
const owners = new Map<string, string>();
for (const node of graph.nodes.values()) {
if (node.type !== 'merge' || node.config.mode !== 'all') continue;
const candidates = [...dominators.get(node.id)!].filter(id => id !== node.id && (graph.outgoing.get(id)?.length ?? 0) > 1);
candidates.sort((a, b) => dominators.get(b)!.size - dominators.get(a)!.size);
if (candidates[0]) owners.set(node.id, candidates[0]);
}
return owners;
}