3.7 KiB
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 injectableRngfor 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 === falseshort-circuit soStreamSessioncan offer anautoReconnect: falseoption without branching at the call site.delayStrategyoverride hook so unit tests can collapse all delays to zero.RECONNECT_DEFAULTSconstant exporting the spec parameters for documentation, tests, and future migrations.
Usage: Basic
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
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<ReconnectResult>.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".