4.0 KiB
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 toDefaultBusRegistryfrom@suaveplan/audio-fx-plugin-base. declarePublish(trackId, bus, channels)registers a publisher and caches theSidechainPublishersodispose()can close it.declareSubscribe(trackId, bus)records a subscriber edge.- Edges build a directed graph
publisher → subscriberkeyed 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
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
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<string>
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). TheprocessOrderstep 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
processOrderis 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.