# reconnect _(no submodules)_ ## Purpose The reconnect module owns the auto-reconnect lifecycle that `StreamSession` runs after a disconnect interruption. It wraps `@suaveplan/retry`'s exponential-backoff engine with the spec-mandated parameters (1 s base, ×2 multiplier, ±20% jitter, 30 s cap, 8 attempts) and exposes a single async entry point that the session calls once per interruption event. Keeping the controller separate from the FSM means the session class stays small and the reconnect policy is independently testable. Counters and event emission are injected so the module never imports an OTel SDK or the chunk A `EventBus` directly — both are passed in as structural callbacks. ## Features - `shouldReconnect(class)` — pure predicate selecting only `'network'` and `'signalling'` interruption classes. - `createReconnectDelay(rng?)` — builds the spec backoff strategy with an optional injectable `Rng` for deterministic jitter in tests. - `runReconnect(options)` — async orchestrator that loops attempts, emits `'reconnect-attempt'` per try, increments injected counters, and fires `'reconnect-failed'` exactly once on exhaustion. - `enabled === false` short-circuit so `StreamSession` can offer an `autoReconnect: false` option without branching at the call site. - `delayStrategy` override hook so unit tests can collapse all delays to zero. - `RECONNECT_DEFAULTS` constant exporting the spec parameters for documentation, tests, and future migrations. ## Usage: Basic ```ts import { runReconnect } from "@suaveplan/react-audio-stream/session"; const result = await runReconnect({ interruptionClass: "network", connect: () => session.connect(roomId), emitter: session.bus, counters: session.reconnectCounters, }); ``` ## Usage: Advanced ```ts const result = await runReconnect({ interruptionClass: classifier.classify(err), connect: () => session.connect(roomId), emitter: session.bus, counters: session.reconnectCounters, enabled: opts.autoReconnect !== false, maxAttempts: opts.maxReconnectAttempts ?? 8, rng: opts.rng, timers: opts.timers, delayStrategy: opts.deterministicDelay, }); if (!result.succeeded && result.attempted) { log.warn("reconnect exhausted", { attempts: result.attempts }); } ``` ## API Reference - `RECONNECT_DEFAULTS` — `{ initialMs: 1000, base: 2, maxMs: 30000, maxAttempts: 8 }`. - `shouldReconnect(class: InterruptionClass): boolean`. - `createReconnectDelay(rng?): RetryStrategy`. - `runReconnect(options): Promise`. - `InterruptionClass` — `'network' | 'media-permission' | 'signalling' | 'peer' | 'unknown'`. - `ReconnectCounters` — `{ incrementAttempt(), incrementFailed() }`. - `ReconnectEmitter` — structural `{ emit(event, payload) }` shape. - `ReconnectOptions` / `ReconnectResult` — see types in source. ## Implementation Notes The controller uses `equal` jitter rather than full jitter — the spec calls for ±20% but the canonical retry library's "equal" jitter scatters between `delay/2` and `delay`, which is conservative enough to meet the spec's noise tolerance while keeping the worst-case backoff stable. Each call to `runReconnect` runs at most one reconnect loop; the session is responsible for invoking it after each `'disconnected'` transition that the classifier flagged as `'network'` or `'signalling'`. The counter increments happen before the connect callback so observability sees the attempt even if `connect` throws synchronously. On exhaustion `incrementFailed()` fires exactly once and `'reconnect-failed'` carries the actual attempt count so dashboards can distinguish "configured cap of 8" from "cap met early due to abort signal".