# `bus-orchestrator` ## Purpose `BusOrchestrator` is the per-adapter owner of the project-level sidechain bus graph. It wraps a single `DefaultBusRegistry` instance (or any caller-supplied `BusRegistry`), tracks every `(publisher track, bus)` and `(subscriber track, bus)` declaration, and refuses any new declaration that would close a directed cycle in the publish-then-subscribe DAG. It also exposes a topological ordering of tracks so the adapter can process publishers before subscribers in the same render block — the property the spec calls out under "publishers process before subscribers". Cycle detection happens **at declaration time**, not at first render. That keeps the failure visible at the call site that introduced the cycle and avoids a half-built render pipeline aborting mid-block. Every faulted declaration raises a `BrowserAudioError(BUS_CYCLE_DETECTED)`. ## Features - Wraps an injectable `BusRegistry` — defaults to `DefaultBusRegistry` from `@suaveplan/audio-fx-plugin-base`. - `declarePublish(trackId, bus, channels)` registers a publisher and caches the `SidechainPublisher` so `dispose()` can close it. - `declareSubscribe(trackId, bus)` records a subscriber edge. - Edges build a directed graph `publisher → subscriber` keyed on shared bus name; a cycle in this graph is rejected immediately. - `processOrder(allTracks)` returns a topologically-sorted track id list — tracks with no bus edges keep their insertion order (Kahn's algorithm). - `dispose()` closes every active publisher. ## Usage: Basic Example ```ts import { BusOrchestrator } from "@suaveplan/audio-engine-browser/adapter/bus-orchestrator"; const orch = new BusOrchestrator(); orch.declarePublish("kick", "kick-bus", 2); orch.declareSubscribe("bass", "kick-bus"); const order = orch.processOrder(["kick", "bass"]); // → ["kick", "bass"] ``` ## Usage: Advanced Example ```ts import { BusOrchestrator } from "@suaveplan/audio-engine-browser/adapter/bus-orchestrator"; const orch = new BusOrchestrator(); orch.declarePublish("a", "x", 2); orch.declareSubscribe("b", "x"); try { orch.declarePublish("b", "y", 2); orch.declareSubscribe("a", "y"); // closes a cycle } catch (e) { if (e instanceof BrowserAudioError) { // e.code === "AUDIO_ENGINE_BROWSER.BUS_CYCLE_DETECTED" } } orch.dispose(); // closes every publisher ``` ## API Reference ### `new BusOrchestrator(registry?)` Constructs the orchestrator. The default registry is a fresh `DefaultBusRegistry`. Tests can inject any conforming `BusRegistry`. ### `declarePublish(trackId, bus, channels) → void` Opens a publisher in the registry, caches the handle, and stores the edge. Throws `BUS_CYCLE_DETECTED` when the new edge would close a cycle. ### `declareSubscribe(trackId, bus) → void` Records a subscriber edge and runs the same cycle check. ### `processOrder(allTracks) → ReadonlyArray` Returns the topological order of `allTracks` honouring every recorded publisher→subscriber edge. ### `bus → BusRegistry` Read accessor for the underlying registry — handed to every `PluginChain` as `ChainContext.buses`. ### `dispose() → void` Closes every active publisher and clears internal state. ## Implementation Notes - The DFS cycle check uses three colours (white / grey / black) so the declaration-time check is `O(V + E)`. The `processOrder` step uses Kahn's algorithm and detects any residual cycle by checking that every node was emitted. - Edges are `(publisher_track, subscriber_track)` filtered on the shared bus name. Self-loops (a track publishing and subscribing to the same bus on itself) are intentionally excluded from the cycle graph because a single chain processes left-to-right within one track — self-feedback is the plugin's own responsibility to handle. - The cached `processOrder` is invalidated on every declaration so the adapter sees fresh orderings as tracks are added. - `dispose()` is best-effort about publisher closes — a misbehaving registry implementation does not prevent the rest from being released.