ws-kit 1.0.1

Generic authenticated typed WebSocket: BroadcastHub, RoomManager, heartbeat
Documentation
# Changelog

All notable changes to this project are documented here. Format: [Keep a
Changelog](https://keepachangelog.com/): versions follow [semver](https://semver.org).

## [Unreleased]

## [1.0.0] - 2026-10-24

### Declared stable

First 1.0. `ws-kit` carries authenticated realtime traffic in production; this
release commits the `BroadcastHub`, `RoomManager`, heartbeat, and auth-extraction
surfaces as API-stable.

### Guarantees from here on

- Breaking changes to public items require a minor bump plus a `#[deprecated]`
  window of at least one minor release.
- MSRV bumps are minor releases, not patch releases.
- `#![deny(missing_docs)]` and `#![forbid(unsafe_code)]` are enforced.

### Explicitly not guaranteed

- The wire frames this crate sends are internal to estate pairs; clients written
  against other stacks should keep to the documented protocol, not the bytes.
- Room event ordering across nodes is best-effort by design.

## [Unreleased]

## [0.4.2] - 2026-09-12

### Added

- `tests/config_matrix.rs` — per-knob behavior matrix for all 7 knobs
  (`WsConfig` 4 + `CompressionConfig` 3): broadcast-capacity lag
  eviction, room plumbing, max-connections threading, origin
  allow-list, compression level/size/threshold contrasts. Dead-knob
  report: `WsConfig::heartbeat_interval` is stored but never read (no
  heartbeat task exists in the library) — pinned inert.

## [0.4.1] - 2026-09-12

### Added

- **Claims proof-back** ([CLAIMS.md]CLAIMS.md): every numeric performance
  claim in README/PERF-SLO mapped to its proof artifact.
- `benches/iai_hot_path.rs` — iai-callgrind instruction-count gate for the
  hot paths: `Frame` text/binary round-trip and 1000-receiver fan-out
  (CI-gated; needs valgrind to execute locally). 2026-09-12 run: binary
  (21199 instr) ≤ text (21399) — the "binary is not slower" claim holds at
  the instruction level.
- `tests/zero_alloc_frame_rx.rs` — counting-allocator proof of the
  allocation profile: steady-state binary rx is allocation-free, text rx
  allocates exactly once (the `String` clone), `broadcast()` allocates only
  the caller's message value, owned-payload → `Frame` conversions are moves.
- Compression ratios re-verified (11.9× JSON 2 KiB; 125.6× ≈ 126× binary
  16 KiB); PERF-SLO table raw/compressed sizes corrected to the bench
  output (16448 B / 131 B).

### Changed

- PERF-SLO.md allocation profile upgraded from "code reading" to "proven"
  with test references; retired the "not yet verified with a counting
  allocator" disclaimer.

## [0.4.0] - 2026-09-12

### Added

- **Binary message support** — the wire model is now `codec::Frame`:
  `Frame::Text(String)` | `Frame::Binary(Bytes)`.
  - Routing and room broadcast accept anything `Frame` converts from
    (`String`, `&str`, `Bytes`, `Vec<u8>`, `&[u8]`) with zero re-encoding
    of owned payloads; `BroadcastHub<Frame>` works out of the box.
  - **Security posture (documented in `codec`):** binary payloads are never
    parsed as text/JSON — the JSON decoders take `&str`, so the type system
    enforces the separation; text is guaranteed UTF-8 end to end. Auth
    extraction is frame-kind-agnostic (it reads the HTTP upgrade request).
- **Compression** (`compression` feature, off by default) — application-layer
  DEFLATE via `flate2` (pure-Rust backend; `forbid(unsafe_code)` preserved):
  - `CompressionConfig` (level / `min_size` / `max_size`) +
    `FrameCompressor::compress_frame` / `decompress_frame` over a 1-byte
    envelope inside `Frame::Binary`.
  - **This is not `permessage-deflate`:** tokio-tungstenite 0.29 / axum 0.8
    do not implement RFC 7692 and expose no extension hook — the module
    docs carry the honest layer assessment. Rust-to-Rust links only;
    negotiation helpers (`EXTENSION_NAME`, `extension_accepted`) for custom
    handshake headers.
  - **Decompression-bomb guard**: output capped during inflation
    (default 1 MiB); unknown flags / corrupt streams fail closed.
  - Bench: `compression_roundtrip` — 2 KiB chat JSON 11.9×, repetitive
    16 KiB binary 126× (incompressible input is sent as identity, never grown).
- **Redis room adapter** (`redis` feature, off by default) — multi-node
  room fan-out over Redis pub/sub:
  - `room::RoomRegistry` trait extracted; `RoomManager` implements it
    (single-node), `RedisRoomRegistry` implements it for the local half.
  - Topology: `broadcast()` → local sinks **and** one `PUBLISH` on
    `ws-kit:room:{id}`; every node `PSUBSCRIBE`s the prefix and applies
    received messages to locally-hosted rooms only (no remote
    materialization). Random per-registry node id drops self-echoes.
  - **Semantics documented honestly: at-most-once, best-effort** — Redis
    pub/sub has no persistence or replay; ws-kit makes no at-least-once
    claim (see README for the Streams-based escape hatch).
  - Publish path tested against a trait-mocked `ConnectionLike`; the full
    two-node pub/sub loop is fixture-gated
    (`tests/redis_rooms.rs`, `#[ignore]`, docker Redis).
- **Backpressure stats hook** — `stats::StatsRecorder` (cheap-to-clone,
  lock-free): connections, rooms, messages/bytes in/out, drops — global +
  per-room, exposed as an immutable `Stats` snapshot. Wired into
  `RoomManager::with_stats` / `Room::with_stats` (outbound traffic and
  drops); `record_message_in` is the integrator read-loop hook.
- **`metrics` facade feature** (off by default) — the same counters emitted
  as `ws_kit_*` counters/gauges through the `metrics` crate, same pattern
  as the `breaker` crate.

### Changed

- `Room` payloads moved from `String` to `Frame`: `room.broadcast("x".into())`
  call sites keep compiling (`impl Into<Frame>`); `Room::subscribe()` now
  returns `broadcast::Receiver<Frame>` — match on the variant or use
  `Frame::into_text()` to restore old text-only behavior. (0.x minor;
  one-line migration for text-only subscribers.)
- `WsError` gained `Compression`, `PayloadTooLarge`, and `Redis(String)`
  variants (all `Clone + Eq` preserved).

### Performance

- Re-measured the rx path after the message-model change
  (`frame_roundtrip` bench): `Frame::Text` 1-rx round-trip ~90 ns (clean
  window; SLO < 150 ns), binary ~81 ns — **binary does not slow the text
  hot path**; 1000-receiver fan-out ~40 ns/receiver (was 42–44).
  Details: PERF-SLO.md.

## [0.3.0] - 2026-09-05

### Added

- Opt-in Origin validation for the WebSocket upgrade handshake (CSWSH
  defense, REQ-WSKIT-200/201):
  - `WsConfig::allowed_origins` field + `WsConfigBuilder::allow_origin` /
    `allowed_origins`.
  - `origin_allowed_in_parts(&Parts, &allowed)` — call in the upgrade
    handler **before** `on_upgrade`; missing or mismatched `Origin` should
    map to `403 Forbidden`.
  - `normalize_origin` — lowercase scheme/host, default ports (`http`/`ws`
    80, `https`/`wss` 443) omitted; exact match only, **no wildcard or
    suffix matching in v1**.
  - **Default (empty list) remains allow-all** — documented residual risk
    (REQ-WSKIT-201); existing users are not broken. Set the list to defend
    against cross-site WebSocket hijacking when auth rides cookies.

### Fixed

- Pre-existing `clippy::indexing_slicing` violation in
  `TokenExtractor::parse_bearer` (`bytes.get(..7)` instead of unchecked
  slicing) — the CI `-D warnings` gate now passes.

## [0.2.1] - 2026-09-04

### Fixed

- `--no-default-features` build.

### Testing

- Loom model-checking of `ConnectionCounter` invariants
  (`RUSTFLAGS="--cfg loom" cargo test --release --lib -- loom`): balanced
  increment/decrement pairs never lose updates or go negative; bounded
  increments hold under all interleavings.

## [0.2.0] - 2026-09-03

### Added

- Participants roster and manager counts on `RoomManager`.

## [0.1.0] - 2026-09-03

### Added

- Generic authenticated typed WebSocket kit: `BroadcastHub`,
  `RoomManager`, `TokenExtractor`, `Codec`, `WsConfig`.
- Feature-gated `axum` integration (default) and `tracing`
  instrumentation.