first commit
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
# `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<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)`. 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.
|
||||
Reference in New Issue
Block a user