feat(host): implement GraphologyHostConfig — render ujsx templates to graphology DAG
This commit is contained in:
@@ -1 +1,324 @@
|
||||
export {};
|
||||
import { DirectedGraph } from "graphology";
|
||||
import { hasCycle } from "graphology-dag";
|
||||
import type { HostConfig } from "@alkdev/ujsx";
|
||||
import type { OperationNodeAttrs } from "../schema/node.js";
|
||||
import type { TemplateNodeAttrs } from "../schema/edge.js";
|
||||
import { CycleError } from "../error/index.js";
|
||||
|
||||
type WorkflowTag = "operation" | "sequential" | "parallel" | "conditional" | "map";
|
||||
|
||||
interface GraphNode {
|
||||
key: string;
|
||||
attributes: OperationNodeAttrs | TemplateNodeAttrs;
|
||||
}
|
||||
|
||||
interface OperationRegistry {
|
||||
resolve(name: string): unknown;
|
||||
}
|
||||
|
||||
interface GraphContext {
|
||||
graph: DirectedGraph;
|
||||
parentStack: string[];
|
||||
operationRegistry?: OperationRegistry;
|
||||
_containerCounter: number;
|
||||
}
|
||||
|
||||
interface ContainerState {
|
||||
tag: WorkflowTag;
|
||||
entryNodes: string[];
|
||||
exitNodes: string[];
|
||||
condition?: unknown;
|
||||
negated?: boolean;
|
||||
childEdgeInfo: ChildEdgeInfo[];
|
||||
}
|
||||
|
||||
interface ChildEdgeInfo {
|
||||
entryNodes: string[];
|
||||
exitNodes: string[];
|
||||
edgeType: "sequential" | "conditional";
|
||||
containerState?: ContainerState;
|
||||
}
|
||||
|
||||
const structuralContainers = new WeakMap<GraphNode, ContainerState>();
|
||||
|
||||
function isStructuralContainer(node: GraphNode): boolean {
|
||||
return node.key.startsWith("__");
|
||||
}
|
||||
|
||||
function inferDataFlow(edgeType: "sequential" | "conditional"): boolean {
|
||||
if (edgeType === "conditional") return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function addEdgeSafe(
|
||||
graph: DirectedGraph,
|
||||
source: string,
|
||||
target: string,
|
||||
attrs: Record<string, unknown>,
|
||||
): void {
|
||||
if (source === target) return;
|
||||
if (!graph.hasNode(source) || !graph.hasNode(target)) return;
|
||||
if (graph.hasDirectedEdge(source, target)) return;
|
||||
const key = `${source}->${target}`;
|
||||
graph.addEdgeWithKey(key, source, target, attrs);
|
||||
}
|
||||
|
||||
function buildEdgeAttrs(
|
||||
edgeType: "sequential" | "conditional",
|
||||
dataFlow: boolean,
|
||||
containerState?: ContainerState,
|
||||
): Record<string, unknown> {
|
||||
const attrs: Record<string, unknown> = { edgeType, dataFlow };
|
||||
if (containerState?.tag === "conditional") {
|
||||
if (containerState.condition !== undefined) {
|
||||
attrs.condition = containerState.condition;
|
||||
}
|
||||
if (containerState.negated === true) {
|
||||
attrs.negated = true;
|
||||
}
|
||||
}
|
||||
return attrs;
|
||||
}
|
||||
|
||||
function collectChildEdgeInfos(state: ContainerState): ChildEdgeInfo[] {
|
||||
const result: ChildEdgeInfo[] = [];
|
||||
switch (state.tag) {
|
||||
case "sequential": {
|
||||
const firstInfo = state.childEdgeInfo[0];
|
||||
if (firstInfo) {
|
||||
if (firstInfo.containerState && firstInfo.containerState.tag !== "conditional") {
|
||||
result.push(...collectChildEdgeInfos(firstInfo.containerState));
|
||||
} else {
|
||||
result.push(firstInfo);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "parallel":
|
||||
case "map": {
|
||||
for (const info of state.childEdgeInfo) {
|
||||
if (info.containerState && info.containerState.tag !== "conditional") {
|
||||
result.push(...collectChildEdgeInfos(info.containerState));
|
||||
} else {
|
||||
result.push(info);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "conditional": {
|
||||
for (const info of state.childEdgeInfo) {
|
||||
result.push({
|
||||
...info,
|
||||
edgeType: "conditional" as const,
|
||||
containerState: state,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function connectFromExitNodes(
|
||||
graph: DirectedGraph,
|
||||
exitNodes: string[],
|
||||
childInfo: ChildEdgeInfo,
|
||||
): void {
|
||||
const edgeType = childInfo.containerState?.tag === "conditional" ? "conditional" : childInfo.edgeType;
|
||||
const dataFlow = inferDataFlow(edgeType);
|
||||
const attrs = buildEdgeAttrs(edgeType, dataFlow, childInfo.containerState);
|
||||
for (const from of exitNodes) {
|
||||
for (const to of childInfo.entryNodes) {
|
||||
addEdgeSafe(graph, from, to, attrs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function flattenChildEdgeInfos(
|
||||
childState: ContainerState | undefined,
|
||||
childNode: GraphNode,
|
||||
): ChildEdgeInfo[] {
|
||||
if (!childState) {
|
||||
return [{ entryNodes: [childNode.key], exitNodes: [childNode.key], edgeType: "sequential" }];
|
||||
}
|
||||
if (childState.tag === "conditional") {
|
||||
return [{ entryNodes: [...childState.entryNodes], exitNodes: [...childState.exitNodes], edgeType: "conditional", containerState: childState }];
|
||||
}
|
||||
return collectChildEdgeInfos(childState);
|
||||
}
|
||||
|
||||
export const GraphologyHostConfig: HostConfig<WorkflowTag, GraphNode, GraphContext> = {
|
||||
name: "graphology",
|
||||
|
||||
createRootContext(_container, options) {
|
||||
const graph = new DirectedGraph({ type: "directed", multi: false, allowSelfLoops: false });
|
||||
return {
|
||||
graph,
|
||||
parentStack: [],
|
||||
operationRegistry: options?.registry as OperationRegistry | undefined,
|
||||
_containerCounter: 0,
|
||||
};
|
||||
},
|
||||
|
||||
finalizeRoot(ctx) {
|
||||
if (hasCycle(ctx.graph)) {
|
||||
throw new CycleError([]);
|
||||
}
|
||||
},
|
||||
|
||||
createInstance(tag, props, ctx, _parent) {
|
||||
if (tag === "operation") {
|
||||
const key = props.name as string;
|
||||
const attrs: OperationNodeAttrs = {
|
||||
name: key,
|
||||
namespace: (props.namespace as string) ?? "",
|
||||
version: (props.version as string) ?? "0.0.1",
|
||||
type: (props.type as OperationNodeAttrs["type"]) ?? "mutation",
|
||||
inputSchema: props.inputSchema ?? {},
|
||||
outputSchema: props.outputSchema ?? {},
|
||||
};
|
||||
if (props.description !== undefined) {
|
||||
attrs.description = props.description as string;
|
||||
}
|
||||
if (props.tags !== undefined) {
|
||||
attrs.tags = props.tags as string[];
|
||||
}
|
||||
ctx.graph.addNode(key, attrs as Record<string, unknown>);
|
||||
return { key, attributes: attrs };
|
||||
}
|
||||
|
||||
const counter = ctx._containerCounter++;
|
||||
const syntheticKey = `__${tag}_${counter}`;
|
||||
const node: GraphNode = { key: syntheticKey, attributes: {} as TemplateNodeAttrs };
|
||||
|
||||
const state: ContainerState = {
|
||||
tag,
|
||||
entryNodes: [],
|
||||
exitNodes: [],
|
||||
childEdgeInfo: [],
|
||||
};
|
||||
|
||||
if (tag === "conditional") {
|
||||
state.condition = props.test;
|
||||
state.negated = (props.negated as boolean) ?? false;
|
||||
}
|
||||
|
||||
structuralContainers.set(node, state);
|
||||
return node;
|
||||
},
|
||||
|
||||
createTextInstance(_text, _ctx, _parent) {
|
||||
const counter = _ctx._containerCounter++;
|
||||
return { key: `__text_${counter}`, attributes: {} as TemplateNodeAttrs };
|
||||
},
|
||||
|
||||
appendChild(parent, child, ctx) {
|
||||
const parentState = structuralContainers.get(parent);
|
||||
const childState = structuralContainers.get(child);
|
||||
|
||||
if (!isStructuralContainer(parent)) {
|
||||
if (isStructuralContainer(child)) {
|
||||
const flattenedInfos = flattenChildEdgeInfos(childState, child);
|
||||
for (const info of flattenedInfos) {
|
||||
const edgeType = info.edgeType;
|
||||
const dataFlow = inferDataFlow(edgeType);
|
||||
const attrs = buildEdgeAttrs(edgeType, dataFlow, info.containerState);
|
||||
for (const to of info.entryNodes) {
|
||||
addEdgeSafe(ctx.graph, parent.key, to, attrs);
|
||||
}
|
||||
}
|
||||
const exitNodes = childState ? childState.exitNodes : [child.key];
|
||||
ctx.parentStack = exitNodes;
|
||||
} else {
|
||||
const edgeType: "sequential" = "sequential";
|
||||
const dataFlow = inferDataFlow(edgeType);
|
||||
const attrs = buildEdgeAttrs(edgeType, dataFlow);
|
||||
addEdgeSafe(ctx.graph, parent.key, child.key, attrs);
|
||||
ctx.parentStack = [child.key];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!parentState) return;
|
||||
|
||||
const childEntryKeys = childState ? childState.entryNodes : [child.key];
|
||||
const childExitKeys = childState ? childState.exitNodes : [child.key];
|
||||
|
||||
switch (parentState.tag) {
|
||||
case "sequential": {
|
||||
if (parentState.exitNodes.length > 0) {
|
||||
const flattenedInfos = flattenChildEdgeInfos(childState, child);
|
||||
for (const info of flattenedInfos) {
|
||||
connectFromExitNodes(ctx.graph, parentState.exitNodes, info);
|
||||
}
|
||||
}
|
||||
if (parentState.entryNodes.length === 0) {
|
||||
parentState.entryNodes = [...childEntryKeys];
|
||||
}
|
||||
parentState.exitNodes = [...childExitKeys];
|
||||
|
||||
const childInfo: ChildEdgeInfo = childState
|
||||
? { entryNodes: [...childEntryKeys], exitNodes: [...childExitKeys], edgeType: childState.tag === "conditional" ? "conditional" : "sequential", containerState: childState }
|
||||
: { entryNodes: [child.key], exitNodes: [child.key], edgeType: "sequential" };
|
||||
parentState.childEdgeInfo.push(childInfo);
|
||||
ctx.parentStack = [...childExitKeys];
|
||||
break;
|
||||
}
|
||||
case "parallel": {
|
||||
parentState.entryNodes.push(...childEntryKeys);
|
||||
parentState.exitNodes.push(...childExitKeys);
|
||||
|
||||
const childInfo: ChildEdgeInfo = childState
|
||||
? { entryNodes: [...childEntryKeys], exitNodes: [...childExitKeys], edgeType: childState.tag === "conditional" ? "conditional" : "sequential", containerState: childState }
|
||||
: { entryNodes: [child.key], exitNodes: [child.key], edgeType: "sequential" };
|
||||
parentState.childEdgeInfo.push(childInfo);
|
||||
ctx.parentStack = [...parentState.exitNodes];
|
||||
break;
|
||||
}
|
||||
case "conditional": {
|
||||
parentState.entryNodes.push(...childEntryKeys);
|
||||
parentState.exitNodes.push(...childExitKeys);
|
||||
|
||||
const childInfo: ChildEdgeInfo = childState
|
||||
? { entryNodes: [...childEntryKeys], exitNodes: [...childExitKeys], edgeType: "conditional", containerState: parentState }
|
||||
: { entryNodes: [child.key], exitNodes: [child.key], edgeType: "conditional", containerState: parentState };
|
||||
parentState.childEdgeInfo.push(childInfo);
|
||||
break;
|
||||
}
|
||||
case "map": {
|
||||
parentState.entryNodes.push(...childEntryKeys);
|
||||
parentState.exitNodes.push(...childExitKeys);
|
||||
|
||||
const childInfo: ChildEdgeInfo = childState
|
||||
? { entryNodes: [...childEntryKeys], exitNodes: [...childExitKeys], edgeType: childState.tag === "conditional" ? "conditional" : "sequential", containerState: childState }
|
||||
: { entryNodes: [child.key], exitNodes: [child.key], edgeType: "sequential" };
|
||||
parentState.childEdgeInfo.push(childInfo);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
finalizeInstance(_instance, _ctx) {
|
||||
},
|
||||
|
||||
removeChild(parent, child, ctx) {
|
||||
if (!isStructuralContainer(parent) && !isStructuralContainer(child)) {
|
||||
const key = `${parent.key}->${child.key}`;
|
||||
if (ctx.graph.hasEdge(key)) {
|
||||
ctx.graph.dropEdge(key);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
prepareUpdate() {
|
||||
return null;
|
||||
},
|
||||
|
||||
commitUpdate() {
|
||||
},
|
||||
|
||||
emit() {
|
||||
},
|
||||
};
|
||||
|
||||
export type { WorkflowTag, GraphNode, GraphContext, OperationRegistry };
|
||||
@@ -1 +1,2 @@
|
||||
export {};
|
||||
export { GraphologyHostConfig } from "./graphology.js";
|
||||
export type { WorkflowTag, GraphNode, GraphContext, OperationRegistry } from "./graphology.js";
|
||||
Reference in New Issue
Block a user