first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,114 @@
# `stream-session`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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
```ts
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
```ts
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.