Files
skills/docs/ui/react/react-audio-stream/src/transport-share/control-channel.md
2026-05-29 08:20:56 +01:00

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, and bpm events 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, and receiving always detaches the previous direction's listeners with no leaks.
  • Imperative notifySeek(seconds) for callers performing programmatic seeks (the underlying transport does not emit a seek event).
  • onModeChange and onEnvelope subscriptions 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.
  • TransportShareCoordinator
    • getMode(): TransportShareMode
    • drive(): void
    • receive(): void
    • release(): void
    • notifySeek(seconds: number): void
    • onModeChange(listener): () => void
    • onEnvelope(listener): () => void
    • dispose(): 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 real Transport satisfies via the useTransport adapter 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.