first commit
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
# `interruption-classifier`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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.
|
||||
Reference in New Issue
Block a user