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

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 the InterruptionClassifier.
  • 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 EventBus surface so consumers in chunk C can write useEventListener(session.bus, "peer-stream-added", …) without any as casts.

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 StreamSession
  • constructor(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 to disconnected. 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.