From 64ea7ac349976d6803736a88c4c95150abdd3330 Mon Sep 17 00:00:00 2001 From: Max Richter Date: Sun, 23 Nov 2025 15:15:38 +0100 Subject: [PATCH] chore: make some things a bit more typesafe --- app/src/lib/graph-interface/graph-manager.ts | 248 ++++++++++++------ packages/registry/src/node-registry-client.ts | 55 ++-- packages/types/src/components.ts | 51 ++-- packages/types/src/types.ts | 60 +++-- 4 files changed, 252 insertions(+), 162 deletions(-) diff --git a/app/src/lib/graph-interface/graph-manager.ts b/app/src/lib/graph-interface/graph-manager.ts index c962126..87620c2 100644 --- a/app/src/lib/graph-interface/graph-manager.ts +++ b/app/src/lib/graph-interface/graph-manager.ts @@ -1,4 +1,11 @@ -import type { Edge, Graph, Node, NodeInput, NodeRegistry, Socket, } from "@nodes/types"; +import type { + Edge, + Graph, + Node, + NodeInput, + NodeRegistry, + Socket, +} from "@nodes/types"; import { fastHashString } from "@nodes/utils"; import { writable, type Writable } from "svelte/store"; import EventEmitter from "./helpers/EventEmitter.js"; @@ -10,17 +17,29 @@ const logger = createLogger("graph-manager"); logger.mute(); -const clone = "structuredClone" in self ? self.structuredClone : (args: any) => JSON.parse(JSON.stringify(args)); +const clone = + "structuredClone" in self + ? self.structuredClone + : (args: any) => JSON.parse(JSON.stringify(args)); -function areSocketsCompatible(output: string | undefined, inputs: string | string[] | undefined) { +function areSocketsCompatible( + output: string | undefined, + inputs: string | string[] | undefined, +) { if (Array.isArray(inputs) && output) { return inputs.includes(output); } return inputs === output; } -export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "settings": { types: Record, values: Record } }> { - +export class GraphManager extends EventEmitter<{ + save: Graph; + result: any; + settings: { + types: Record; + values: Record; + }; +}> { status: Writable<"loading" | "idle" | "error"> = writable("loading"); loaded = false; @@ -62,24 +81,32 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " } serialize(): Graph { - logger.group("serializing graph") - const nodes = Array.from(this._nodes.values()).map(node => ({ + logger.group("serializing graph"); + const nodes = Array.from(this._nodes.values()).map((node) => ({ id: node.id, position: [...node.position], type: node.type, props: node.props, })) as Node[]; - const edges = this._edges.map(edge => [edge[0].id, edge[1], edge[2].id, edge[3]]) as Graph["edges"]; - const serialized = { id: this.graph.id, settings: this.settings, nodes, edges }; + const edges = this._edges.map((edge) => [ + edge[0].id, + edge[1], + edge[2].id, + edge[3], + ]) as Graph["edges"]; + const serialized = { + id: this.graph.id, + settings: this.settings, + nodes, + edges, + }; logger.groupEnd(); return clone(serialized); } - private lastSettingsHash = 0; setSettings(settings: Record) { - let hash = fastHashString(JSON.stringify(settings)); if (hash === this.lastSettingsHash) return; this.lastSettingsHash = hash; @@ -89,8 +116,6 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " this.execute(); } - - getNodeDefinitions() { return this.registry.getAllNodes(); } @@ -104,7 +129,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " nodes.add(n); const children = this.getChildrenOfNode(n); const parents = this.getParentsOfNode(n); - const newNodes = [...children, ...parents].filter(n => !nodes.has(n)); + const newNodes = [...children, ...parents].filter((n) => !nodes.has(n)); stack.push(...newNodes); } return [...nodes.values()]; @@ -116,9 +141,16 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " const children = node.tmp?.children || []; for (const child of children) { if (nodes.includes(child)) { - const edge = this._edges.find(e => e[0].id === node.id && e[2].id === child.id); + const edge = this._edges.find( + (e) => e[0].id === node.id && e[2].id === child.id, + ); if (edge) { - edges.push([edge[0].id, edge[1], edge[2].id, edge[3]] as [number, number, number, string]); + edges.push([edge[0].id, edge[1], edge[2].id, edge[3]] as [ + number, + number, + number, + string, + ]); } } } @@ -127,25 +159,26 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " return edges; } - private _init(graph: Graph) { - const nodes = new Map(graph.nodes.map(node => { - const nodeType = this.registry.getNode(node.type); - if (nodeType) { - node.tmp = { - random: (Math.random() - 0.5) * 2, - type: nodeType - }; - } - return [node.id, node] - })); + const nodes = new Map( + graph.nodes.map((node) => { + const nodeType = this.registry.getNode(node.type); + if (nodeType) { + node.tmp = { + random: (Math.random() - 0.5) * 2, + type: nodeType, + }; + } + return [node.id, node]; + }), + ); const edges = graph.edges.map((edge) => { const from = nodes.get(edge[0]); const to = nodes.get(edge[2]); if (!from || !to) { throw new Error("Edge references non-existing node"); - }; + } from.tmp = from.tmp || {}; from.tmp.children = from.tmp.children || []; from.tmp.children.push(to); @@ -153,17 +186,15 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " to.tmp.parents = to.tmp.parents || []; to.tmp.parents.push(from); return [from, edge[1], to, edge[3]] as Edge; - }) + }); this.edges.set(edges); this.nodes.set(nodes); this.execute(); - } async load(graph: Graph) { - const a = performance.now(); this.loaded = false; @@ -171,7 +202,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " this.status.set("loading"); this.id.set(graph.id); - const nodeIds = Array.from(new Set([...graph.nodes.map(n => n.type)])); + const nodeIds = Array.from(new Set([...graph.nodes.map((n) => n.type)])); await this.registry.load(nodeIds); for (const node of this.graph.nodes) { @@ -186,9 +217,12 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " node.tmp.type = nodeType; } - // load settings - const settingTypes: Record = {}; + const settingTypes: Record< + string, + // Optional metadata to map settings to specific nodes + NodeInput & { __node_type: string; __node_input: string } + > = {}; const settingValues = graph.settings || {}; const types = this.getNodeDefinitions(); for (const type of types) { @@ -196,8 +230,15 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " for (const key in type.inputs) { let settingId = type.inputs[key].setting; if (settingId) { - settingTypes[settingId] = { __node_type: type.id, __node_input: key, ...type.inputs[key] }; - if (settingValues[settingId] === undefined && "value" in type.inputs[key]) { + settingTypes[settingId] = { + __node_type: type.id, + __node_input: key, + ...type.inputs[key], + }; + if ( + settingValues[settingId] === undefined && + "value" in type.inputs[key] + ) { settingValues[settingId] = type.inputs[key].value; } } @@ -220,7 +261,6 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " setTimeout(() => this.execute(), 100); } - getAllNodes() { return Array.from(this._nodes.values()); } @@ -234,7 +274,6 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " } async loadNode(id: string) { - await this.registry.load([id]); const nodeType = this.registry.getNode(id); @@ -247,7 +286,10 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " let settingId = nodeType.inputs[key].setting; if (settingId) { settingTypes[settingId] = nodeType.inputs[key]; - if (settingValues[settingId] === undefined && "value" in nodeType.inputs[key]) { + if ( + settingValues[settingId] === undefined && + "value" in nodeType.inputs[key] + ) { settingValues[settingId] = nodeType.inputs[key].value; } } @@ -266,7 +308,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " const child = stack.pop(); if (!child) continue; children.push(child); - stack.push(...child.tmp?.children || []); + stack.push(...(child.tmp?.children || [])); } return children; } @@ -278,10 +320,10 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " const fromParents = this.getParentsOfNode(from); if (toParents.includes(from)) { const fromChildren = this.getChildrenOfNode(from); - return toParents.filter(n => fromChildren.includes(n)); + return toParents.filter((n) => fromChildren.includes(n)); } else if (fromParents.includes(to)) { const toChildren = this.getChildrenOfNode(to); - return fromParents.filter(n => toChildren.includes(n)); + return fromParents.filter((n) => toChildren.includes(n)); } else { // these two nodes are not connected return; @@ -289,7 +331,6 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " } removeNode(node: Node, { restoreEdges = false } = {}) { - const edgesToNode = this._edges.filter((edge) => edge[2].id === node.id); const edgesFromNode = this._edges.filter((edge) => edge[0].id === node.id); for (const edge of [...edgesToNode, ...edgesFromNode]) { @@ -297,15 +338,17 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " } if (restoreEdges) { - const outputSockets = edgesToNode.map(e => [e[0], e[1]] as const); - const inputSockets = edgesFromNode.map(e => [e[2], e[3]] as const); + const outputSockets = edgesToNode.map((e) => [e[0], e[1]] as const); + const inputSockets = edgesFromNode.map((e) => [e[2], e[3]] as const); for (const [to, toSocket] of inputSockets) { for (const [from, fromSocket] of outputSockets) { const outputType = from.tmp?.type?.outputs?.[fromSocket]; const inputType = to?.tmp?.type?.inputs?.[toSocket]?.type; if (outputType === inputType) { - this.createEdge(from, fromSocket, to, toSocket, { applyUpdate: false }); + this.createEdge(from, fromSocket, to, toSocket, { + applyUpdate: false, + }); continue; } } @@ -318,7 +361,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " nodes.delete(node.id); return nodes; }); - this.execute() + this.execute(); this.save(); } @@ -328,7 +371,6 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " } createGraph(nodes: Node[], edges: [number, number, number, string][]) { - // map old ids to new ids const idMap = new Map(); @@ -344,9 +386,9 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " return { ...node, id, tmp: { type } }; }); - const _edges = edges.map(edge => { - const from = nodes.find(n => n.id === idMap.get(edge[0])); - const to = nodes.find(n => n.id === idMap.get(edge[2])); + const _edges = edges.map((edge) => { + const from = nodes.find((n) => n.id === idMap.get(edge[0])); + const to = nodes.find((n) => n.id === idMap.get(edge[2])); if (!from || !to) { throw new Error("Edge references non-existing node"); @@ -375,15 +417,28 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " return nodes; } - createNode({ type, position, props = {} }: { type: Node["type"], position: Node["position"], props: Node["props"] }) { - + createNode({ + type, + position, + props = {}, + }: { + type: Node["type"]; + position: Node["position"]; + props: Node["props"]; + }) { const nodeType = this.registry.getNode(type); if (!nodeType) { logger.error(`Node type not found: ${type}`); return; } - const node: Node = { id: this.createNodeId(), type, position, tmp: { type: nodeType }, props }; + const node: Node = { + id: this.createNodeId(), + type, + position, + tmp: { type: nodeType }, + props, + }; this.nodes.update((nodes) => { nodes.set(node.id, node); @@ -393,16 +448,23 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " this.save(); } - createEdge(from: Node, fromSocket: number, to: Node, toSocket: string, { applyUpdate = true } = {}) { - + createEdge( + from: Node, + fromSocket: number, + to: Node, + toSocket: string, + { applyUpdate = true } = {}, + ) { const existingEdges = this.getEdgesToNode(to); // check if this exact edge already exists - const existingEdge = existingEdges.find(e => e[0].id === from.id && e[1] === fromSocket && e[3] === toSocket); + const existingEdge = existingEdges.find( + (e) => e[0].id === from.id && e[1] === fromSocket && e[3] === toSocket, + ); if (existingEdge) { logger.error("Edge already exists", existingEdge); return; - }; + } // check if socket types match const fromSocketType = from.tmp?.type?.outputs?.[fromSocket]; @@ -412,11 +474,15 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " } if (!areSocketsCompatible(fromSocketType, toSocketType)) { - logger.error(`Socket types do not match: ${fromSocketType} !== ${toSocketType}`); + logger.error( + `Socket types do not match: ${fromSocketType} !== ${toSocketType}`, + ); return; } - const edgeToBeReplaced = this._edges.find(e => e[2].id === to.id && e[3] === toSocket); + const edgeToBeReplaced = this._edges.find( + (e) => e[2].id === to.id && e[3] === toSocket, + ); if (edgeToBeReplaced) { this.removeEdge(edgeToBeReplaced, { applyDeletion: false }); } @@ -450,14 +516,12 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " } } - redo() { const nextState = this.history.redo(); if (nextState) { this._init(nextState); this.emit("save", this.serialize()); } - } startUndoGroup() { @@ -482,30 +546,30 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " const stack = node.tmp?.parents?.slice(0); while (stack?.length) { if (parents.length > 1000000) { - logger.warn("Infinite loop detected") + logger.warn("Infinite loop detected"); break; } const parent = stack.pop(); if (!parent) continue; parents.push(parent); - stack.push(...parent.tmp?.parents || []); + stack.push(...(parent.tmp?.parents || [])); } return parents.reverse(); } getPossibleSockets({ node, index }: Socket): [Node, string | number][] { - const nodeType = node?.tmp?.type; if (!nodeType) return []; - const sockets: [Node, string | number][] = [] + const sockets: [Node, string | number][] = []; // if index is a string, we are an input looking for outputs if (typeof index === "string") { - // filter out self and child nodes - const children = new Set(this.getChildrenOfNode(node).map(n => n.id)); - const nodes = this.getAllNodes().filter(n => n.id !== node.id && !children.has(n.id)); + const children = new Set(this.getChildrenOfNode(node).map((n) => n.id)); + const nodes = this.getAllNodes().filter( + (n) => n.id !== node.id && !children.has(n.id), + ); const ownType = nodeType?.inputs?.[index].type; @@ -519,16 +583,21 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " } } } - } else if (typeof index === "number") { // if index is a number, we are an output looking for inputs // filter out self and parent nodes - const parents = new Set(this.getParentsOfNode(node).map(n => n.id)); - const nodes = this.getAllNodes().filter(n => n.id !== node.id && !parents.has(n.id)); + const parents = new Set(this.getParentsOfNode(node).map((n) => n.id)); + const nodes = this.getAllNodes().filter( + (n) => n.id !== node.id && !parents.has(n.id), + ); // get edges from this socket - const edges = new Map(this.getEdgesFromNode(node).filter(e => e[1] === index).map(e => [e[2].id, e[3]])); + const edges = new Map( + this.getEdgesFromNode(node) + .filter((e) => e[1] === index) + .map((e) => [e[2].id, e[3]]), + ); const ownType = nodeType.outputs?.[index]; @@ -536,11 +605,13 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " const inputs = node?.tmp?.type?.inputs; if (!inputs) continue; for (const key in inputs) { - const otherType = [inputs[key].type]; otherType.push(...(inputs[key].accepts || [])); - if (areSocketsCompatible(ownType, otherType) && edges.get(node.id) !== key) { + if ( + areSocketsCompatible(ownType, otherType) && + edges.get(node.id) !== key + ) { sockets.push([node, key]); } } @@ -548,38 +619,46 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " } return sockets; - } - removeEdge(edge: Edge, { applyDeletion = true }: { applyDeletion?: boolean } = {}) { + removeEdge( + edge: Edge, + { applyDeletion = true }: { applyDeletion?: boolean } = {}, + ) { const id0 = edge[0].id; const sid0 = edge[1]; const id2 = edge[2].id; const sid2 = edge[3]; - const _edge = this._edges.find((e) => e[0].id === id0 && e[1] === sid0 && e[2].id === id2 && e[3] === sid2); + const _edge = this._edges.find( + (e) => + e[0].id === id0 && e[1] === sid0 && e[2].id === id2 && e[3] === sid2, + ); if (!_edge) return; edge[0].tmp = edge[0].tmp || {}; if (edge[0].tmp.children) { - edge[0].tmp.children = edge[0].tmp.children.filter(n => n.id !== id2); + edge[0].tmp.children = edge[0].tmp.children.filter( + (n: Node) => n.id !== id2, + ); } edge[2].tmp = edge[2].tmp || {}; if (edge[2].tmp.parents) { - edge[2].tmp.parents = edge[2].tmp.parents.filter(n => n.id !== id0); + edge[2].tmp.parents = edge[2].tmp.parents.filter( + (n: Node) => n.id !== id0, + ); } if (applyDeletion) { this.edges.update((edges) => { - return edges.filter(e => e !== _edge); + return edges.filter((e) => e !== _edge); }); this.execute(); this.save(); } else { - this._edges = this._edges.filter(e => e !== _edge); + this._edges = this._edges.filter((e) => e !== _edge); } - } getEdgesToNode(node: Node) { @@ -605,7 +684,4 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " }) .filter(Boolean) as unknown as [Node, number, Node, string][]; } - - } - diff --git a/packages/registry/src/node-registry-client.ts b/packages/registry/src/node-registry-client.ts index 2d4e30e..b2d0006 100644 --- a/packages/registry/src/node-registry-client.ts +++ b/packages/registry/src/node-registry-client.ts @@ -1,19 +1,24 @@ -import { NodeDefinitionSchema, type AsyncCache, type NodeDefinition, type NodeRegistry } from "@nodes/types"; +import { + NodeDefinitionSchema, + type AsyncCache, + type NodeDefinition, + type NodeRegistry, +} from "@nodes/types"; import { createLogger, createWasmWrapper } from "@nodes/utils"; const log = createLogger("node-registry"); log.mute(); export class RemoteNodeRegistry implements NodeRegistry { - status: "loading" | "ready" | "error" = "loading"; private nodes: Map = new Map(); - cache?: AsyncCache; - fetch: typeof fetch = globalThis.fetch.bind(globalThis); - constructor(private url: string, private cache?: AsyncCache) { } + constructor( + private url: string, + private cache?: AsyncCache, + ) {} async fetchUsers() { const response = await this.fetch(`${this.url}/nodes/users.json`); @@ -32,7 +37,9 @@ export class RemoteNodeRegistry implements NodeRegistry { } async fetchCollection(userCollectionId: `${string}/${string}`) { - const response = await this.fetch(`${this.url}/nodes/${userCollectionId}.json`); + const response = await this.fetch( + `${this.url}/nodes/${userCollectionId}.json`, + ); if (!response.ok) { throw new Error(`Failed to load collection ${userCollectionId}`); } @@ -44,20 +51,16 @@ export class RemoteNodeRegistry implements NodeRegistry { if (!response.ok) { throw new Error(`Failed to load node definition ${nodeId}`); } - return response.json() + return response.json(); } private async fetchNodeWasm(nodeId: `${string}/${string}/${string}`) { - const fetchNode = async () => { const response = await this.fetch(`${this.url}/nodes/${nodeId}.wasm`); return response.arrayBuffer(); - } + }; - const res = await Promise.race([ - fetchNode(), - this.cache?.get(nodeId) - ]); + const res = await Promise.race([fetchNode(), this.cache?.get(nodeId)]); if (!res) { throw new Error(`Failed to load node wasm ${nodeId}`); @@ -69,18 +72,17 @@ export class RemoteNodeRegistry implements NodeRegistry { async load(nodeIds: `${string}/${string}/${string}`[]) { const a = performance.now(); - const nodes = await Promise.all([...new Set(nodeIds).values()].map(async id => { + const nodes = await Promise.all( + [...new Set(nodeIds).values()].map(async (id) => { + if (this.nodes.has(id)) { + return this.nodes.get(id)!; + } - if (this.nodes.has(id)) { - return this.nodes.get(id)!; - } - - const wasmBuffer = await this.fetchNodeWasm(id); - - return this.register(wasmBuffer); - - })); + const wasmBuffer = await this.fetchNodeWasm(id); + return this.register(wasmBuffer); + }), + ); const duration = performance.now() - a; @@ -90,11 +92,10 @@ export class RemoteNodeRegistry implements NodeRegistry { log.groupEnd(); this.status = "ready"; - return nodes + return nodes; } async register(wasmBuffer: ArrayBuffer) { - const wrapper = createWasmWrapper(wasmBuffer); const definition = NodeDefinitionSchema.safeParse(wrapper.get_definition()); @@ -110,8 +111,8 @@ export class RemoteNodeRegistry implements NodeRegistry { let node = { ...definition.data, - execute: wrapper.execute - } + execute: wrapper.execute, + }; this.nodes.set(definition.data.id, node); diff --git a/packages/types/src/components.ts b/packages/types/src/components.ts index cf2a71d..d15bdb5 100644 --- a/packages/types/src/components.ts +++ b/packages/types/src/components.ts @@ -2,17 +2,17 @@ import { Graph, NodeDefinition, NodeId } from "./types"; export interface NodeRegistry { /** - * The status of the node registry - * @remarks The status should be "loading" when the registry is loading, "ready" when the registry is ready, and "error" if an error occurred while loading the registry - */ + * The status of the node registry + * @remarks The status should be "loading" when the registry is loading, "ready" when the registry is ready, and "error" if an error occurred while loading the registry + */ status: "loading" | "ready" | "error"; /** * Load the nodes with the given ids - * @param nodeIds - The ids of the nodes to load - * @returns A promise that resolves when the nodes are loaded - * @throws An error if the nodes could not be loaded - * @remarks This method should be called before calling getNode or getAllNodes - */ + * @param nodeIds - The ids of the nodes to load + * @returns A promise that resolves when the nodes are loaded + * @throws An error if the nodes could not be loaded + * @remarks This method should be called before calling getNode or getAllNodes + */ load: (nodeIds: NodeId[]) => Promise; /** * Get a node by id @@ -27,30 +27,30 @@ export interface NodeRegistry { getAllNodes: () => NodeDefinition[]; /** - * Register a new node - * @param wasmBuffer - The WebAssembly buffer for the node - * @returns The node definition + * Register a new node + * @param wasmBuffer - The WebAssembly buffer for the node + * @returns The node definition */ register: (wasmBuffer: ArrayBuffer) => Promise; - - - cache?: AsyncCache; } export interface RuntimeExecutor { /** - * Execute the given graph - * @param graph - The graph to execute - * @returns The result of the execution - */ - execute: (graph: Graph, settings: Record) => Promise; + * Execute the given graph + * @param graph - The graph to execute + * @returns The result of the execution + */ + execute: ( + graph: Graph, + settings: Record, + ) => Promise; } export interface SyncCache { /** - * The maximum number of items that can be stored in the cache - * @remarks When the cache size exceeds this value, the oldest items should be removed - */ + * The maximum number of items that can be stored in the cache + * @remarks When the cache size exceeds this value, the oldest items should be removed + */ size: number; /** @@ -69,14 +69,13 @@ export interface SyncCache { * Clear the cache */ clear: () => void; - } export interface AsyncCache { /** - * The maximum number of items that can be stored in the cache - * @remarks When the cache size exceeds this value, the oldest items should be removed - */ + * The maximum number of items that can be stored in the cache + * @remarks When the cache size exceeds this value, the oldest items should be removed + */ size: number; /** diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index 1fa6023..e453d7f 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -1,17 +1,27 @@ import { z } from "zod"; import { NodeInputSchema } from "./inputs"; -export type NodeId = `${string}/${string}/${string}`; +export const NodeTypeSchema = z + .string() + .regex(/^[^/]+\/[^/]+\/[^/]+$/, "Invalid NodeId format") + .transform((value) => value as `${string}/${string}/${string}`); + +export type NodeType = z.infer; export const NodeSchema = z.object({ id: z.number(), - type: z.string(), - props: z.record(z.union([z.number(), z.array(z.number())])).optional(), - meta: z.object({ - title: z.string().optional(), - lastModified: z.string().optional(), - }).optional(), - position: z.tuple([z.number(), z.number()]) + type: NodeTypeSchema, + tmp: z.any().optional(), + props: z + .record(z.string(), z.union([z.number(), z.array(z.number())])) + .optional(), + meta: z + .object({ + title: z.string().optional(), + lastModified: z.string().optional(), + }) + .optional(), + position: z.tuple([z.number(), z.number()]), }); export type Node = { @@ -19,9 +29,9 @@ export type Node = { depth?: number; mesh?: any; random?: number; - parents?: Node[], - children?: Node[], - inputNodes?: Record + parents?: Node[]; + children?: Node[]; + inputNodes?: Record; type?: NodeDefinition; downX?: number; downY?: number; @@ -30,17 +40,19 @@ export type Node = { ref?: HTMLElement; visible?: boolean; isMoving?: boolean; - } + }; } & z.infer; export const NodeDefinitionSchema = z.object({ id: z.string(), - inputs: z.record(NodeInputSchema).optional(), + inputs: z.record(z.string(), NodeInputSchema).optional(), outputs: z.array(z.string()).optional(), - meta: z.object({ - description: z.string().optional(), - title: z.string().optional(), - }).optional(), + meta: z + .object({ + description: z.string().optional(), + title: z.string().optional(), + }) + .optional(), }); export type NodeDefinition = z.infer & { @@ -56,12 +68,14 @@ export type Socket = { export type Edge = [Node, number, Node, string]; export const GraphSchema = z.object({ - id: z.number().optional(), - meta: z.object({ - title: z.string().optional(), - lastModified: z.string().optional(), - }).optional(), - settings: z.record(z.any()).optional(), + id: z.number(), + meta: z + .object({ + title: z.string().optional(), + lastModified: z.string().optional(), + }) + .optional(), + settings: z.record(z.string(), z.any()).optional(), nodes: z.array(NodeSchema), edges: z.array(z.tuple([z.number(), z.number(), z.number(), z.string()])), });