4.0 KiB
control-channel
Purpose
The control-channel module is the wire-protocol heart of transport sharing. It defines the envelope shapes that flow between the granting peer (the "driver") and every connected receiver, and it implements the TransportShareCoordinator — a small state-aware adapter that wires a local SharedTransportTarget to a peer-level ControlChannel.
The coordinator is the only place in the package where transport events become wire messages. Everything else — hooks, components, session glue — observes the coordinator's mode and applied envelopes rather than touching the transport or channel directly. Keeping the translation pinned to one module means the protocol is straightforward to audit and to evolve without touching React code paths.
Features
- Four-envelope wire protocol:
play,pause,seek,setTempo. - Driver-side listener attaches to the transport's
start,pause, andbpmevents and forwards each as the matching envelope. - Receiver-side listener applies each inbound envelope to the local transport target via
start,pause,seek,setTempo. - Mode-flip safety: switching between
local,driving, andreceivingalways detaches the previous direction's listeners with no leaks. - Imperative
notifySeek(seconds)for callers performing programmatic seeks (the underlying transport does not emit aseekevent). onModeChangeandonEnvelopesubscriptions for higher layers (the hook).dispose()releases all listeners and the channel subscription.
Usage: Basic
import {
createTransportShareCoordinator,
type ControlChannel,
type SharedTransportTarget,
} from "@suaveplan/react-audio-stream/transport-share";
declare const transport: SharedTransportTarget;
declare const channel: ControlChannel;
const coordinator = createTransportShareCoordinator({ transport, channel });
coordinator.drive(); // become the driver — local events now propagate
Usage: Advanced
const coordinator = createTransportShareCoordinator({ transport, channel });
const unsubscribeMode = coordinator.onModeChange((mode) => {
console.info("share role is now", mode);
});
const unsubscribeEnvelope = coordinator.onEnvelope((envelope) => {
if (envelope.type === "setTempo") {
console.info("tempo synced to", envelope.bpm);
}
});
coordinator.receive(); // apply inbound envelopes to the local transport
// ... share session runs ...
coordinator.release(); // return to local-only mode
unsubscribeMode();
unsubscribeEnvelope();
coordinator.dispose();
API Reference
createTransportShareCoordinator(opts): TransportShareCoordinator— factory.TransportShareCoordinatorgetMode(): TransportShareModedrive(): voidreceive(): voidrelease(): voidnotifySeek(seconds: number): voidonModeChange(listener): () => voidonEnvelope(listener): () => voiddispose(): void
TransportShareControlMessage— discriminated union of envelope types.ControlChannel—{ send, onMessage }adapter implemented by chunk A's session over the peer transport.SharedTransportTarget— structural{ start, pause, seek, setTempo, on }shape that the realTransportsatisfies via theuseTransportadapter from@suaveplan/react-audio.
Implementation Notes
The coordinator is intentionally stateless beyond its current mode and the cached detach callbacks. Mode changes are atomic: detach previous direction, attach new direction, then flip the role flag and notify subscribers. The driver-side listener defensively reads payload.time / payload.offset / payload.bpm as number | undefined because the upstream TransportEventMap declares those keys as plain unknown; the coordinator coerces missing values to safe defaults rather than relying on transport internals. Disposal is idempotent so the React hook's cleanup effect can call it on unmount without guards. The factory exists so tests can construct the coordinator with the same call shape consumers use through the package's barrel.