# `WebAudioGraph` _(no submodules)_ ## Purpose `WebAudioGraph` is the thin id-keyed registry that sits between `BrowserTimelineAdapter` and a `BaseAudioContext`. It exists so the adapter can address `AudioNode`s by stable string id (e.g. `track:42:gain`) rather than juggling raw node references, and so a populated graph can be snapshotted to a JSON-safe object and restored into a fresh context. The registry is intentionally narrow: it owns the node table and the edge list, validates ids on every operation, and produces a snapshot whose shape (node-types + edges) is independent of any live `AudioNode` instance. Snapshots round-trip through `JSON.stringify` and survive context replacement — the same snapshot can be restored into a realtime `AudioContext`, an `OfflineAudioContext`, or a deterministic test shim. ## Features - `addNode` / `removeNode` keyed by string id. - `connect` / `disconnect` wrapping `AudioNode.connect` / `disconnect` with full optional `outputIndex`/`inputIndex` support. - `getNode` and `listIds` for read-side inspection. - `snapshot()` returning a `Readonly` containing node-type tags and the edge list. - `restore(snapshot)` Zod-validates the snapshot and rebuilds the graph against the bound context, raising `AUDIO_ENGINE_BROWSER.NODE_NOT_FOUND` for orphan edges. - `classifyNode(node)` exported helper that maps a live `AudioNode` to its `AudioNodeType` tag using its `constructor.name`. - Defensive: every id mismatch raises a typed `BrowserAudioError(NODE_NOT_FOUND)` carrying the offending id. ## Usage: Basic Example ```ts import { WebAudioGraph } from "@suaveplan/audio-engine-browser/graph/web-audio-graph"; const graph = new WebAudioGraph(audioContext); graph.addNode("master", audioContext.createGain()); graph.addNode("dest", audioContext.destination); graph.connect("master", "dest"); ``` ## Usage: Advanced Example ```ts import { WebAudioGraph, } from "@suaveplan/audio-engine-browser/graph/web-audio-graph"; const graph = new WebAudioGraph(audioContext); graph.addNode("src", audioContext.createBufferSource()); graph.addNode("gain", audioContext.createGain()); graph.addNode("dest", audioContext.destination); graph.connect("src", "gain"); graph.connect("gain", "dest"); // Persist for an undo step const snap = graph.snapshot(); localStorage.setItem("graph", JSON.stringify(snap)); // Later — possibly in a new context — restore const restored = new WebAudioGraph(newContext); restored.restore(JSON.parse(localStorage.getItem("graph")!)); ``` ## API Reference ### `new WebAudioGraph(context)` Constructs a graph bound to a `BaseAudioContext`-shaped value. The context is the factory used by `restore()`; calls to `addNode` accept externally-constructed nodes. ### `addNode(id, node) → void` Registers a node under `id`. Existing entries are overwritten so the adapter can swap nodes during reconfiguration; the edge list is left untouched. ### `removeNode(id) → void` Disconnects the node, removes it, and prunes every edge that referenced it. Throws `NODE_NOT_FOUND` for an unknown id. ### `connect(sourceId, destinationId, outputIndex?, inputIndex?) → void` Wraps `AudioNode.connect`. Throws `NODE_NOT_FOUND` if either id is absent. Records the edge so it appears in `snapshot()`. ### `disconnect(sourceId, destinationId?) → void` When `destinationId` is omitted, disconnects every outgoing edge from `source`. Otherwise disconnects just that edge. Throws `NODE_NOT_FOUND` for missing source. ### `snapshot() → AudioGraphSnapshot` Returns `{ nodes: Record, connections: Edge[] }`. ### `restore(snapshot) → void` Zod-validates the snapshot, clears the current registry, rebuilds nodes via the bound context's factories, and replays every edge. Throws `NODE_NOT_FOUND` for invalid or orphan-edge snapshots. ### `classifyNode(node) → AudioNodeType` Static helper inferring the type tag from `constructor.name`. ## Implementation Notes - `snapshot()` records **node types**, not node instances; this is what makes it JSON-safe. - `restore()` calls into the context's `create*` factories (`createGain`, `createStereoPanner`, …) so the restored graph uses nodes native to the new context. Worklet and `ScriptProcessor` nodes cannot be reconstructed by tag alone and are rejected at restore. - Per-disconnect graph mutations are best-effort: the underlying `AudioNode.disconnect` may throw when the destination is not actually connected, which is treated as a no-op so a careless caller cannot desynchronise the edge list from the live graph. - The Zod validator rejects unknown node types and any edge referencing an absent node before any factory call is issued — the graph reaches an inconsistent state only when a downstream context factory fails, in which case the partial state is left for the caller to observe.