feat: initial group nodes /w some bugs
Some checks failed
🚀 Lint & Test & Deploy / release (pull_request) Failing after 1m12s
📊 Benchmark the Runtime / release (pull_request) Successful in 50s

This commit is contained in:
2026-04-24 13:38:32 +02:00
parent 12572742eb
commit b1418f6778
21 changed files with 1563 additions and 73 deletions

View File

@@ -6,6 +6,142 @@ import type {
RuntimeExecutor,
SyncCache
} from '@nodarium/types';
function isGroupInstanceType(type: string): boolean {
return type === '__virtual/group/instance';
}
export function expandGroups(graph: Graph): Graph {
if (!graph.groups || Object.keys(graph.groups).length === 0) {
return graph;
}
let nodes = [...graph.nodes];
let edges = [...graph.edges];
const groups = graph.groups;
let changed = true;
while (changed) {
changed = false;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (!isGroupInstanceType(node.type)) continue;
const groupId = (node.props as Record<string, unknown> | undefined)?.groupId as string | undefined;
if (!groupId) continue;
const group = groups[groupId];
if (!group) continue;
changed = true;
// Recursively expand nested groups inside this group's internal graph
const expandedInternal = expandGroups({
id: 0,
nodes: group.graph.nodes,
edges: group.graph.edges,
groups
});
const ID_PREFIX = node.id * 1000000;
const idMap = new Map<number, number>();
const inputVirtualNode = expandedInternal.nodes.find(
n => n.type === '__virtual/group/input'
);
const outputVirtualNode = expandedInternal.nodes.find(
n => n.type === '__virtual/group/output'
);
const realInternalNodes = expandedInternal.nodes.filter(
n => n.type !== '__virtual/group/input' && n.type !== '__virtual/group/output'
);
for (const n of realInternalNodes) {
idMap.set(n.id, ID_PREFIX + n.id);
}
const parentIncomingEdges = edges.filter(e => e[2] === node.id);
const parentOutgoingEdges = edges.filter(e => e[0] === node.id);
// Edges from/to virtual nodes in the expanded internal graph
const edgesFromInput = expandedInternal.edges.filter(
e => e[0] === inputVirtualNode?.id
);
const edgesToOutput = expandedInternal.edges.filter(
e => e[2] === outputVirtualNode?.id
);
const newEdges: Graph['edges'] = [];
// Short-circuit: parent source → internal target (via group input)
for (const parentEdge of parentIncomingEdges) {
const socketName = parentEdge[3];
const socketIdx = group.inputs.findIndex(s => s.name === socketName);
if (socketIdx === -1) continue;
for (const internalEdge of edgesFromInput.filter(e => e[1] === socketIdx)) {
const remappedId = idMap.get(internalEdge[2]);
if (remappedId !== undefined) {
newEdges.push([parentEdge[0], parentEdge[1], remappedId, internalEdge[3]]);
}
}
}
// Short-circuit: internal source → parent target (via group output)
for (const parentEdge of parentOutgoingEdges) {
const outputIdx = parentEdge[1];
const outputSocketName = group.outputs[outputIdx]?.name;
if (!outputSocketName) continue;
for (const internalEdge of edgesToOutput.filter(e => e[3] === outputSocketName)) {
const remappedId = idMap.get(internalEdge[0]);
if (remappedId !== undefined) {
newEdges.push([remappedId, internalEdge[1], parentEdge[2], parentEdge[3]]);
}
}
}
// Remap internal-to-internal edges
const internalEdges = expandedInternal.edges.filter(
e => e[0] !== inputVirtualNode?.id
&& e[0] !== outputVirtualNode?.id
&& e[2] !== inputVirtualNode?.id
&& e[2] !== outputVirtualNode?.id
);
for (const e of internalEdges) {
const fromId = idMap.get(e[0]);
const toId = idMap.get(e[2]);
if (fromId !== undefined && toId !== undefined) {
newEdges.push([fromId, e[1], toId, e[3]]);
}
}
// Remove the group node
nodes.splice(i, 1);
// Add remapped internal nodes
for (const n of realInternalNodes) {
nodes.push({ ...n, id: idMap.get(n.id)! });
}
// Remove group node's edges and add short-circuit edges
const groupEdgeKeys = new Set([
...parentIncomingEdges.map(e => `${e[0]}-${e[1]}-${e[2]}-${e[3]}`),
...parentOutgoingEdges.map(e => `${e[0]}-${e[1]}-${e[2]}-${e[3]}`)
]);
edges = edges.filter(
e => !groupEdgeKeys.has(`${e[0]}-${e[1]}-${e[2]}-${e[3]}`)
);
edges.push(...newEdges);
break; // Restart loop with updated nodes array
}
}
return { ...graph, nodes, edges };
}
import {
concatEncodedArrays,
createLogger,
@@ -75,7 +211,11 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
throw new Error('Node registry is not ready');
}
await this.registry.load(graph.nodes.map((node) => node.type));
// Only load non-virtual types (virtual nodes are resolved locally)
const nonVirtualTypes = graph.nodes
.map(node => node.type)
.filter(t => !t.startsWith('__virtual/'));
await this.registry.load(nonVirtualTypes as any);
const typeMap = new Map<string, NodeDefinition>();
for (const node of graph.nodes) {
@@ -163,6 +303,9 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
let a = performance.now();
this.debugData = {};
// Expand group nodes into a flat graph before execution
graph = expandGroups(graph);
// Then we add some metadata to the graph
const [outputNode, nodes] = await this.addMetaData(graph);
let b = performance.now();

View File

@@ -1,12 +1,18 @@
import { debugNode } from '$lib/node-registry/debugNode';
import { groupInputNode, groupNode, groupOutputNode } from '$lib/node-registry/groupNodes';
import { IndexDBCache, RemoteNodeRegistry } from '$lib/node-registry/index';
import type { Graph } from '@nodarium/types';
import { createPerformanceStore } from '@nodarium/utils';
import { MemoryRuntimeExecutor } from './runtime-executor';
import { expandGroups, MemoryRuntimeExecutor } from './runtime-executor';
import { MemoryRuntimeCache } from './runtime-executor-cache';
const indexDbCache = new IndexDBCache('node-registry');
const nodeRegistry = new RemoteNodeRegistry('', indexDbCache, [debugNode]);
const nodeRegistry = new RemoteNodeRegistry('', indexDbCache, [
debugNode,
groupInputNode,
groupOutputNode,
groupNode
]);
const cache = new MemoryRuntimeCache();
const executor = new MemoryRuntimeExecutor(nodeRegistry, cache);
@@ -34,7 +40,13 @@ export async function executeGraph(
graph: Graph,
settings: Record<string, unknown>
): Promise<Int32Array> {
await nodeRegistry.load(graph.nodes.map((n) => n.type));
// Expand groups before loading types so we only load real (non-virtual) node types
const expandedGraph = expandGroups(graph);
await nodeRegistry.load(
expandedGraph.nodes
.map(n => n.type)
.filter(t => !t.startsWith('__virtual/')) as any
);
performanceStore.startRun();
const res = await executor.execute(graph, settings);
performanceStore.stopRun();