# `interruption-classifier` _(no submodules)_ ## Purpose `InterruptionClassifier` maps a structural disconnect cause to exactly one of `network | media-permission | signalling | peer | unknown`. Classification is deterministic — every rule is a typed equality or membership check, no probabilistic heuristics — so the same cause record always produces the same verdict regardless of timing or ordering. This is essential for the auto-reconnect policy that lands in chunk D: the policy decides whether to retry by inspecting the verdict label, and a flaky classifier would translate into flaky reconnect behaviour for users. The classifier is intentionally state-free. Every consumer (the `StreamSession`, future React hooks, integration tests) constructs one instance and calls `classify` on every disconnect — there is no internal counter, ring buffer, or memo, so a single instance can be shared across sessions without contention. ## Features - Five disjoint labels covering every observed interruption shape. - Priority-ordered rules with explicit precedence: `peer` beats `media-permission` beats `network` beats `signalling`. - WebRTC-friendly: keys directly off `iceConnectionState` and `connectionState`. - WebSocket-friendly: keys off RFC 6455 close codes, skipping the clean-close codes (1000, 1001) so a graceful server-side close does not trigger reconnect. - `shouldAutoReconnect(class)` helper exposes the chunk-D reconnect policy in a single place. - Zero allocations on the hot path beyond the verdict object. ## Usage: Basic ```ts import { InterruptionClassifier } from "@suaveplan/react-audio-stream/session"; const classifier = new InterruptionClassifier(); const verdict = classifier.classify({ iceConnectionState: "failed" }); // verdict.class === "network" ``` ## Usage: Advanced ```ts import { InterruptionClassifier, type InterruptionCause, } from "@suaveplan/react-audio-stream/session"; const classifier = new InterruptionClassifier(); function onDisconnect(rawCause: InterruptionCause) { const verdict = classifier.classify(rawCause); if (classifier.shouldAutoReconnect(verdict.class)) { scheduleReconnect(verdict); } else { surfaceToUi(verdict); } } // Hard browser-permission revoke onDisconnect({ mediaErrorName: "NotAllowedError", reason: "user revoked microphone permission", }); // → verdict.class === "media-permission"; no reconnect. // Server-initiated abnormal close onDisconnect({ signallingCloseCode: 1006, reason: "abrupt server close" }); // → verdict.class === "signalling"; reconnect scheduled. ``` ## API Reference - `class InterruptionClassifier` — stateless classifier. - `classify(cause: InterruptionCause): InterruptionVerdict` — apply the rule mapping and return `{ class, cause }`. - `shouldAutoReconnect(class: InterruptionClass): boolean` — true iff the class is `network` or `signalling`. - `InterruptionClass`, `InterruptionCause`, `InterruptionVerdict` — re-exported types. ## Implementation Notes The rule ordering is the only piece of business logic with subtle consequences. `peer` is evaluated first because an intentional remote leave should never be treated as a network glitch — even if the ICE state also happens to read `disconnected` during the closing handshake. Permission errors take precedence over ICE because a user-revoked microphone often surfaces as both a `getUserMedia` rejection and an ICE failure milliseconds later; the permission failure is the actionable cause. Close codes 1000 and 1001 are explicitly skipped so a clean server shutdown (`going away`) does not show up as a signalling interruption — those map to `unknown` instead, which the reconnect policy treats as non-retryable. The classifier deliberately holds no state so it composes cleanly into the FSM-driven session flow without spreading hidden coupling.