4.3 KiB
stream-session
(no submodules)
Purpose
StreamSession is the public face of the stream-tier session core. It
wraps the FSM, the transport adapter, the interruption classifier, and
the per-peer StreamSourcePlugin registry into one orchestration
object. Consumers in chunks B/C (transports, hooks, components) talk to
this class — they never reach into the FSM or the classifier directly.
The class is intentionally a thin shell. Every lifecycle method
(connect, join, leave, disconnect) is a dispatchSessionEvent
followed by the matching transport call followed by another
dispatchSessionEvent. Transport faults flow through
reportInterruption, which classifies the cause, emits the
'interruption' event, drives the FSM to disconnected, and updates
telemetry. Auto-reconnect (chunk D) subscribes to 'interruption' and
decides whether to retry — the session itself never retries, keeping
the retry policy at the right layer for testability.
Features
- Eight
'state-changed'emissions covering the canonical happy path. 'peer-stream-added'/'peer-stream-removed'emissions for every peer the transport reports.- Deterministic
'interruption'emission via theInterruptionClassifier. - Injectable transport via constructor — no globals, no singletons.
- Telemetry: peer-connect / peer-disconnect counters, active-session gauge, RTT histogram (the latter wired by chunk B).
dispose()for clean teardown including peer chain removal.- Typed
EventBussurface so consumers in chunk C can writeuseEventListener(session.bus, "peer-stream-added", …)without anyascasts.
Usage: Basic
import { createStreamSession } from "@suaveplan/react-audio-stream/session";
const session = createStreamSession({ transport });
await session.connect("room-1");
await session.join();
// ...stream activity...
await session.leave();
await session.disconnect();
Usage: Advanced
import {
createStreamSession,
type StreamTransportAdapter,
} from "@suaveplan/react-audio-stream/session";
const session = createStreamSession({ transport });
session.bus.on("state-changed", ({ from, to }) => {
metrics.set("ras_state", to);
});
session.bus.on("peer-stream-added", ({ participantId, stream }) => {
console.log("attaching peer", participantId);
engine.attachSource(`peer:${participantId}`, stream);
});
session.bus.on("interruption", (verdict) => {
if (verdict.class === "network") {
scheduleReconnect();
}
});
try {
await session.connect("room-1");
await session.join();
} catch (err) {
// The session has already emitted 'interruption' and driven the FSM
// to 'disconnected'. The error is re-thrown so the caller can decide.
console.error(err);
}
API Reference
class StreamSessionconstructor(options: StreamSessionOptions)bus: EventBus<StreamSessionEventMap>— public event surface.state,roomId,peers()— read-only inspection.connect(roomId),join(),leave(),disconnect()— lifecycle methods that drive the FSM and the transport in lock-step.reportInterruption(cause)— classify + emit + transition todisconnected. Used by chunk-B transports.dispose()— unsubscribe transport listeners + tear down peers.createStreamSession(options)— factory wrapper.- Type re-exports:
StreamSessionEventMap,StreamSessionOptions,StreamTransportAdapter,PeerStreamEvent,InterruptionCause.
Implementation Notes
The session takes the classifier in the constructor rather than the
options object having to know about it, so tests can stub the verdict
without subclassing. Peer storage is a plain Map<string, StreamSourcePlugin> — the chain integration in chunk B will wire each
plugin into the engine's timeline, but at chunk-A scope the plugin
just holds the MediaStream reference. tearDownAllPeers runs
on every transition to disconnected (both explicit and via FAIL) so
the engine sees consistent peer-stream-removed events regardless of
how the session ended. extractReason keeps the failure path
allocation-free for the common Error case while still surfacing a
useful string when the transport throws a non-Error. The active flag
guards the OTel gauge against double-decrement when both an explicit
disconnect and an interruption land back-to-back.