Package Exports
- @vizij/node-graph-wasm
- @vizij/node-graph-wasm/metadata
- @vizij/node-graph-wasm/metadata/registry.json
Readme
@vizij/node-graph-wasm
Vizij’s node graph engine for JavaScript.
This package ships the WebAssembly build of vizij-graph-core together with a TypeScript wrapper, schema helpers, and sample graphs. Load Vizij GraphSpecs, stage host inputs, evaluate outputs, and update node parameters from any modern JS runtime without compiling Rust.
Table of Contents
- Overview
- Key Concepts
- Installation
- API
- Usage
- Samples & Fixtures
- Development & Testing
- Related Packages
Overview
- Built from
vizij-graph-corewithwasm-bindgen; the npm package is the canonical JavaScript distribution maintained by the Vizij team. - Provides a high-level
Graphclass, low-level bindings, TypeScript definitions, and ready-to-use fixtures. - Supports both browser and Node environments—
init()chooses the right loader and validates the ABI (abi_version() === 2). - Ships GraphSpec normalisers and schema inspection helpers so editors and tooling can speak the same language as Vizij runtimes.
- Bakes the node registry (
metadata/registry.json) straight from the Rust core so build-time tooling and authoring UIs stay in sync with the runtime.
Key Concepts
- GraphSpec – Declarative JSON describing nodes, parameters, and the explicit
edgesarray that connects node outputs to inputs (selectors, output keys). The package normalises shorthand specs automatically. - Graph Runtime – The
Graphclass owns aGraphRuntime, handlingloadGraph,stageInput,setParam,step, andevalAll. Structural parameter changes (e.g.,Split.sizes) invalidate the wasm plan cache and the JS wrapper resets its delta baseline automatically so the next eval re-establishes a full snapshot before returning to deltas. - Plan caching & invalidation – The wasm engine caches a compiled execution plan (topological order + port layouts + input bindings) for performance.
- The cache is reused across frames when the graph layout is unchanged.
- Only structural edits (changes that can affect port layouts or bindings) invalidate the plan. In practice today this includes
Split.sizes; most other param changes are non-structural and do not force a plan rebuild. GraphSpec.specVersion(andfingerprint) are treated as plan-validity keys (not a generic "state version"). The wrapper auto-fills them and bumps them only for structural changes, so ordinary value tweaks do not degrade steady-state performance.
- Staged Inputs – Host-provided values keyed by
TypedPath. They are latched until you replace or remove them. - Evaluation Result –
evalAll()returns per-node port snapshots plus aWriteBatchof sink writes (each with Value + Shape metadata). - Node Schema Registry –
getNodeSchemas()exposes the runtime-supported nodes, ideal for palettes/editors. - ABI Guard –
abi_version()ensures the JS glue and.wasmbinary are compatible. Rebuild when versions change.
Installation
npm install @vizij/node-graph-wasm
# or pnpm add @vizij/node-graph-wasmFor local development inside Vizij:
pnpm run build:wasm:graph
cd npm/@vizij/node-graph-wasm
pnpm install
pnpm run buildLink into vizij-web while iterating:
(cd npm/@vizij/node-graph-wasm && pnpm link --global)
(cd ../vizij-web && pnpm link @vizij/node-graph-wasm)Bundler Configuration
Like the other Vizij wasm packages, this module now exports an ESM wrapper that first attempts a static import of the wasm-bindgen JS glue. Bundlers that support async WebAssembly (Webpack 5, Vite, etc.) should treat pkg/vizij_graph_wasm_bg.wasm as an emitted asset. For Next.js configure:
// next.config.js
module.exports = {
webpack: (config) => {
config.experiments = { ...(config.experiments ?? {}), asyncWebAssembly: true };
config.module.rules.push({
test: /\.wasm$/,
type: "asset/resource",
});
return config;
},
};If you host the wasm binary elsewhere, pass a string URL to init():
await init("https://cdn.example.com/vizij/node_graph_wasm_bg.wasm");Passing a string avoids Webpack’s RelativeURL helper, which previously attempted to call .replace() on a URL object.
API
async function init(input?: InitInput): Promise<void>;
function abi_version(): number;
async function normalizeGraphSpec(spec: GraphSpec | string): Promise<GraphSpec>;
async function getNodeSchemas(): Promise<Registry>;
function getNodeRegistry(): Registry;
function findNodeSignature(typeId: NodeType | string): NodeSignature | undefined;
function requireNodeSignature(typeId: NodeType | string): NodeSignature;
function listNodeTypeIds(): NodeType[];
function groupNodeSignaturesByCategory(): Map<string, NodeSignature[]>;
const nodeRegistryVersion: string;
async function logNodeSchemaDocs(nodeType?: NodeType | string): Promise<void>;
const graphSamples: Record<string, GraphSpec>;
class Graph {
constructor();
loadGraph(
specOrJson: GraphSpec | string,
opts?: { hotPaths?: string[]; epsilon?: number; autoClearDroppedHotPaths?: boolean }
): void;
unloadGraph(): void;
stageInput(path: string, value: ValueInput, shape?: ShapeJSON, immediateEval?: boolean): void;
setHotPaths(paths: string[], opts?: { epsilon?: number; autoClearDroppedHotPaths?: boolean }): void;
stageInputs(paths: string[], values: Float32Array, shapes?: (ShapeJSON | null)[]): void; // routes through smart staging
stageInputsBySlotDiff(indices: Uint32Array, values: Float32Array, epsilon?: number): void;
clearSlot(idx: number): Promise<void>;
clearInput(path: string): Promise<void>;
clearStagedInputs(): void;
applyStagedInputs(): void;
evalAll(): EvalResult;
evalAllFull(): EvalResult; // resets the delta baseline
getOutputsDelta(sinceVersion?: number): EvalResult & { version: number };
setParam(nodeId: string, key: string, value: ValueInput): void;
setTime(t: number): void;
step(dt: number): void;
getWrites(): WriteOpJSON[];
clearWrites(): void;
waitForGraphReady?(): Promise<void>; // only populated when used through React provider
}Normalization, Schema & Docs Helpers
normalizeGraphSpec(spec)– round-trips any GraphSpec (object or JSON string) through the Rust normaliser so shorthand inputs/legacyinputsmaps come back with explicitedges, typed paths, and canonical casing.getNodeSchemas()/getNodeRegistry()– runtime and baked access to the node registry (including ports and params) for palette/editor usage.findNodeSignature(typeId)/requireNodeSignature(typeId)– quick lookups into the baked registry.listNodeTypeIds()/groupNodeSignaturesByCategory()– helpers for palettes or UI grouping.logNodeSchemaDocs(nodeType?)– pretty-prints the schema docs for every node or a specificNodeTyperight to the console (handy while prototyping editors).graphSamples– curated ready-to-load specs that already reflect the canonicaledgesform and typedpathparameters.
Each registry entry exposes:
| Field | Description |
|---|---|
doc / short_doc |
Human-readable description for node palettes and tooltips. |
inputs / outputs |
Port metadata (label, doc, shape hints) useful for editors. |
params |
Parameter schema with expected value types and default values. |
categories |
Optional grouping tags for UI organisation. |
Types (GraphSpec, EvalResult, ValueJSON, ShapeJSON, etc.) are exported from src/types.
Performance guardrails:
- Structural param edits that change port layouts (e.g.,
Split.sizes) rebuild the plan; the wrapper drops its cached baseline and will emit a full snapshot on the next eval before returning to deltas.- The
Graphwrapper automatically picks the optimized slots + delta path; callinginner.eval_all_js/ other legacy exports bypasses these optimizations and is slower.
Delta semantics (baseline resync)
getOutputsDelta(sinceVersion?) is designed for long-running loops where you want to transfer only the ports that changed since a version token.
- Pass
0(or omit the argument) for the first call to establish a baseline snapshot. - If the caller's
sinceVersiondoes not match the runtime's cached baseline (including when the wasm runtime resets versions afterloadGraph,clear, or a structural edit), the runtime returns a full snapshot flagged withfull: true. - When
full: true, treat the payload as a replacement baseline (do not merge it with an older snapshot).
The Graph wrapper handles baseline management automatically for the common case; this section is mainly relevant if you call low-level bindings directly.
Usage
import {
init,
Graph,
normalizeGraphSpec,
graphSamples,
valueAsNumber,
} from "@vizij/node-graph-wasm";
await init();
const graph = new Graph();
const spec = await normalizeGraphSpec(graphSamples.vectorPlayground);
graph.loadGraph(spec);
graph.stageInput("demo/path", { float: 1 }, undefined, true);
const result = graph.evalAll();
const nodeValue =
result.nodes["const"]?.out?.value ?? { type: "float", data: NaN };
console.log("Node value", valueAsNumber(nodeValue));
for (const write of result.writes) {
console.log("Write", write.path, write.value);
}Hot-path fast staging (unified)
const graph = new Graph();
graph.loadGraph(spec, { hotPaths: ["demo/a", "demo/b"], epsilon: 0, autoClearDroppedHotPaths: true });
// setHotPaths is still available; loadGraph can now register the hot list for you.
const paths = ["demo/a", "demo/b", "demo/rare"];
const values = new Float32Array([1, 2, 3]);
// Routes hot paths through slots (diffed after first call), others through path batch.
graph.stageInputs(paths, values);
const frame = graph.evalAll(); // first call returns full snapshot; subsequent calls use delta internally
// Debug visibility
graph.setDebugLogging(true);
console.log(graph.inspectStaging());Note: The hot-path fast lane currently targets numeric scalars. Non-scalar or explicitly shaped inputs in the hot list will fall back to path staging for that call (log in debug mode). Use
clearSlot/clearInputto stop replaying cached values.
Fast-path loop with deltas (slots-only eval)
let version = 0n;
const inputs = new Float32Array(paths.length);
const indices = graph.registerInputPaths(paths);
graph.prepareInputSlots(indices);
function tick(frame) {
inputs.fill(frame);
graph.stageInputsBySlotDiff(indices, inputs); // only sends changed slots
graph.setTime(frame / 60);
graph.step(1 / 60);
const delta = graph.getOutputsDelta(Number(version));
version = BigInt(delta.version);
// delta.nodes contains only changed ports when the baseline matches.
// When the baseline does NOT match (first call, after loadGraph, after structural edits),
// the runtime will return a full snapshot and the wrapper will replace its cached baseline.
}Manual time control (force a full baseline refresh when needed):
graph.setTime(0);
graph.step(1 / 60);
graph.evalAllFull(); // resets delta baselineStaging inputs lazily:
graph.stageInput("demo/path", { vec3: [0, 1, 0] });
graph.applyStagedInputs();
graph.evalAll();Performance tips
- Batch steady frames: If inputs remain constant for N ticks, prefer
graph.evalSteps(N, dt)to advance time and return only the final outputs/writes. This collapses N JS↔WASM calls into one. Don’t use it when you need to inject new inputs every frame. - Avoid JSON for numeric streams: For hot numeric inputs, call the underlying wasm export
graph.inner.stage_input_f32(path, float32Array)to skip JSON encode/decode. To read numeric outputs without JSON, usegraph.inner.get_output_f32(nodeId, outputKey)to receive aFloat32Array. Keep the JSON/ValueJSON path for mixed or non-numeric data. - One call per frame: Even with per-frame inputs, batch all staging calls, then a single
evalAll(orevalStepswhen valid) per frame to minimize boundary crossings.
Custom loader options
init(input?: InitInput) accepts any input supported by @vizij/wasm-loader:
import { init } from "@vizij/node-graph-wasm";
import { readFile } from "node:fs/promises";
// Host wasm from your CDN
await init(new URL("https://cdn.example.com/vizij/node_graph_wasm_bg.wasm"));
// Node / Electron / tests
const bytes = await readFile("dist/node_graph_wasm_bg.wasm");
await init(bytes);This is useful for service workers, Electron, or any environment that needs explicit control over fetch behaviour.
Samples & Fixtures
The package exports several ready-to-run specs:
graphSamplesmap (e.g.,vectorPlayground,oscillatorBasics,logicGate).- Named exports (
oscillatorBasics,nestedTelemetry, etc.) for convenience. - Helpers:
import { loadNodeGraphBundle } from "@vizij/node-graph-wasm"; const { spec, stage } = await loadNodeGraphBundle("urdf-ik-position"); graph.loadGraph(spec); if (stage) { for (const [path, payload] of Object.entries(stage)) { graph.stageInput(path, payload.value, payload.shape); } }
Fixtures originate from @vizij/test-fixtures so tests and demos share the same assets.
Troubleshooting
- Selector mismatch – Errors such as
selector index 5 out of boundsmean the GraphSpec referenced an array element that does not exist. Normalise the spec and confirm upstream nodes emit the expected shape. set_paramvalidation – Parameters enforce specific value types (float,text, tuple pairs). Coerce values withnormalizeValueJSONbefore callingsetParamto avoid runtime throws.- ABI mismatch – Re-run
pnpm run build:wasm:graphifabi_version()differs from the expected version logged by the package. - Missing fixtures –
loadNodeGraphBundleresolves names from@vizij/test-fixtures. Ensurepnpm run build:sharedhas been executed in local development.
Development & Testing
pnpm run build:wasm:graph # regenerate pkg/
cd npm/@vizij/node-graph-wasm
pnpm testThe Vitest suite runs sample graphs through the wasm bridge, checking evaluation results and write batches. For Rust-side coverage, run cargo test -p vizij-graph-wasm.
Related Packages
vizij-graph-wasm– Rust crate producing the wasm build.vizij-graph-core– underlying evaluator.@vizij/node-graph-react– React integration built on this npm package.@vizij/value-json– shared value helpers used during staging.
Need assistance or spot a bug? Open an issue—robust bindings keep Vizij graphs portable. 🧠
Inspecting Schema Documentation
Each NodeSignature in the schema registry now ships with human-friendly descriptions for the node itself, its ports, and parameters.
import { getNodeSchemas, logNodeSchemaDocs } from "@vizij/node-graph-wasm";
await init();
// Fetch the registry and inspect docs programmatically.
const registry = await getNodeSchemas();
for (const node of registry.nodes) {
console.log(node.name, node.doc); // node.doc is a plain string
for (const port of node.inputs) {
console.log(" input:", port.label, port.doc);
}
}
// Or print a nicely formatted summary for all nodes…
await logNodeSchemaDocs();
// …or just a single node type.
await logNodeSchemaDocs("remap");The same documentation is embedded in the wasm JSON (get_node_schemas_json) so downstream tools can consume it without relying on these helpers.