Two small WS polish items from the post-v2.4 audit:
Item 1 — cancelled mid-subscribe leak. ``SubscriptionManager.subscribe``
sends a ``subscribe`` cmd then awaits ``_wait_for_response``; if the
awaiting task is cancelled between send and ack, the lock releases but
the server still completes the subscribe and emits a ``subscribed``
envelope. That ack lands in ``MessageDispatcher.dispatch`` with no
``sid -> client_id`` mapping installed, was silently dropped, and the
server held an active sid until the WS closed (data frames thereafter
logged "Message for unknown sid"; the leaked sub counted against any
server-side quota).
Detect the orphan in the dispatcher: when a ``subscribed`` control
envelope arrives with a sid not in ``_sub_mgr._sid_to_client``, log a
WARNING and send an ``unsubscribe`` back through the connection to
release the server-side state. Best-effort: a send failure is logged
and swallowed (socket is likely already gone, in which case the server
reaps on close anyway). Sids that already have a mapping (normal-path
race where the mapping landed first) are a no-op.
Item 2 — ``OrderbookSnapshotPayload`` ``yes``/``no`` no longer have a
``default_factory=dict``. A wire frame missing both ``yes_dollars_fp``
and ``no_dollars_fp`` was previously parsing as a fully empty snapshot
— silently resetting the local book to empty on schema drift. Now both
sides are required so a malformed snapshot raises ``ValidationError``,
which pairs with the #241 seq-rollback path so the watermark isn't
silently advanced past the never-delivered frame. (The mutable-default
anti-pattern was already fixed by #263 switching to ``default_factory``;
this drops the default entirely.)
Tests:
- 4 orphan-subscribed dispatcher tests (sends unsubscribe with
nested/top-level sid, no-op on known sid, send-failure swallowed).
- Snapshot regression: missing ``yes`` / missing ``no`` / missing both
each raise ``ValidationError``.
Closes #268
Closes #268
Part of #273 (post-v2.4 audit), Wave 3.