Skip to content

Ignore reserved binary IDs and end removed stream roles on server/activate - #436

Merged
chrisuthe merged 5 commits into
Sendspin:mainfrom
chrisuthe:chrisuthe/task/ignore-reserved-binary-ids-and-end-removed-stream
Sep 17, 2026
Merged

chrisuthe merged 5 commits into
Sendspin:mainfrom
chrisuthe:chrisuthe/task/ignore-reserved-binary-ids-and-end-removed-stream

Conversation

@chrisuthe

Copy link
Copy Markdown
Member

Part of #399. Implements the remaining receiver rules from Sendspin/spec#289 ("Protocol evolution").

Changes

  • Transport: without the legacy opt-in, EncryptedWebSocket now delivers binary IDs 2 and 3 as ordinary binary messages. Before, it returned an error, which made the client SDK close the connection. The client then ignores them like any other ID it does not implement. A 2/3 frame that arrives while a type 1 fragmented message is being reassembled is still an error. The server always sets the opt-in, so it keeps treating 2/3 as the DEPRECATED(spec-pr-172) legacy fragments.
  • Client SDK: when a server/activate removes player, artwork, visualizer or an application-specific role, the SDK now ends that role's stream. This covers explicit removal, implicit removal (the connection is no longer playback-capable) and replacement by another role version. Ending the stream marks it inactive, drops its config and blanks any artwork channels on display. The stream end listeners are called once with the removed families, even if an earlier stream/end already ended the stream.

Behaviour change for SDK embedders: the stream end listener now also fires when a role is removed. It fires even if the stream already ended, because the embedder may still be finishing output or holding an effect such as ducking. The listener docs now say the embedder must stop output, clear buffers and release temporary output effects. The SDK can't tell whether an application-specific role carries a stream, so removing one always fires the listener.

Rules

Rule Verdict Test
Client ignores unknown server JSON type after activation already compliant tests/client/test_unrecognized_messages.py::test_unknown_json_type_is_ignored (new)
Server ignores unknown client JSON type already compliant (#412) tests/server/test_client_message_dispatch.py::test_unknown_message_type_keeps_connection_open
Client ignores unimplemented binary IDs already compliant tests/client/test_unrecognized_messages.py::test_unimplemented_binary_id_is_ignored (new)
Client ignores reserved binary IDs 2-3 fixed tests/noise/test_wire.py::test_receive_delivers_reserved_ids_without_legacy_opt_in, tests/client/test_unrecognized_messages.py::test_reserved_binary_id_is_ignored
Implemented ID with an inactive role is still recognised already compliant tests/client/test_unrecognized_messages.py::test_implemented_binary_id_with_inactive_stream_is_dropped (new), tests/test_client_artwork_transfers.py::test_malformed_message_outside_stream_closes_connection
Server ignores unimplemented client binary IDs already compliant tests/server/test_source_connection_dispatch.py::test_unimplemented_binary_type_is_ignored (new)
Server IDs 2-3 unchanged: implemented as the legacy fragment tolerance tests/noise/test_wire.py::test_legacy_fragments_reassemble_with_opt_in
Re-handshake: client may reject anything but handshake/activate already compliant tests/client/test_unrecognized_messages.py::test_rehandshake_closes_on_anything_but_server_activate (new)
Re-handshake: server discards old-key messages, including unknown types, before Noise message 2 already compliant (#411) tests/noise/test_driver.py::test_rehandshake_discards_old_key_messages_before_message_2 (extended with an unknown type and ID)
Fragment sequence and malformed-sequence rules already compliant tests/noise/test_wire.py::test_receive_rejects_non_fragment_frame_mid_reassembly, test_receive_rejects_reserved_ids_mid_reassembly_without_legacy_opt_in (new)
Discard fragments of an unimplemented orig_type (MAY) not implemented (optional) n/a
stream/end before a re-handshake whose activation removes a stream role already compliant tests/server/test_role_activation.py::test_long_term_pairing_ends_streams_before_rehandshake (new)
Server treats streams as inactive once it sends stream/end already compliant tests/server/test_role_activation.py::test_removed_roles_are_torn_down_before_server_activate
Client discards removed state roles on server/activate already compliant (#434) tests/client/test_server_state.py::test_activation_discards_removed_role_state
Client stops output, clears buffers and releases effects for removed stream roles, including after stream/end and on version replacement fixed tests/client/test_stream_role_removal.py

Only the long-term → pairing re-handshake can remove stream roles, and _quiesce_for_pairing ends the streams and flushes the writer before Noise message 1 goes out.

Without on_legacy_fragment, a type 2 or 3 frame is an ordinary binary
message for the owner to ignore, instead of a protocol error. It is still
an error while a type 1 fragmented message is in flight. Adds tests for the
client ignoring unknown JSON types and unimplemented binary IDs.
A server/activate that removes the player, artwork, visualizer or an
application-specific role, explicitly, implicitly or by version
replacement, now ends those streams and dispatches the stream end
listeners, even when an earlier stream/end already ended the stream.
Server ignores unimplemented client binary IDs, discards old-key messages
of unknown type before Noise message 2, and sends stream/end before the
re-handshake when pairing on a long-term PSK.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The changes are narrowly scoped, consistent with the stated protocol rules, and backed by comprehensive new/updated tests covering the key edge cases (reserved IDs, reassembly, activation role removal, and re-handshake sequencing).

Pull request overview

This PR advances Sendspin protocol-evolution conformance by making reserved binary IDs (2/3) non-fatal for newer clients and by ensuring the Python client SDK ends streams when server/activate removes stream-capable roles, aligning behavior with the updated receiver rules in Sendspin/spec#289.

Changes:

  • Updated EncryptedWebSocket to deliver reserved binary IDs 2/3 as ordinary binary frames unless the legacy fragment opt-in is enabled (while still rejecting them mid type-1 reassembly).
  • Updated the client SDK to end streams (and notify stream-end listeners) when server/activate removes stream-role families or replaces role versions, including after an earlier stream/end.
  • Added/extended targeted tests across Noise transport, client SDK behavior, and server dispatch/activation sequencing to lock in the new rules.
File summaries
File Description
tests/server/test_source_connection_dispatch.py Adds coverage that unknown client binary IDs are ignored even on strict servers.
tests/server/test_role_activation.py Adds coverage that long-term→pairing re-handshake sequencing puts stream/end on the wire before re-handshake message 1.
tests/noise/test_wire.py Updates transport tests to assert IDs 2/3 are delivered as ordinary binary without legacy opt-in and rejected mid-reassembly.
tests/noise/test_driver.py Extends re-handshake discard test to include unknown JSON type and unknown binary ID in flight.
tests/client/test_unrecognized_messages.py New tests verifying the SDK ignores unknown server JSON types and unimplemented/reserved binary IDs after activation (and closes appropriately post re-handshake).
tests/client/test_stream_role_removal.py New tests verifying stream teardown + listener semantics when server/activate removes/replaces stream roles (including app-specific roles).
aiosendspin/noise/wire.py Implements the reserved-ID delivery change in EncryptedWebSocket while preserving legacy fragment opt-in behavior and mid-reassembly erroring.
aiosendspin/client/connection.py Ends removed-role streams during activation application via _end_removed_role_streams() and factors stream teardown into _end_streams().
aiosendspin/client/client.py Updates stream-end listener documentation to include role-removal-triggered stream end and required embedder cleanup semantics.
Review details
  • Files reviewed: 9/9 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@chrisuthe
chrisuthe marked this pull request as ready for review September 17, 2026 13:00
@chrisuthe
chrisuthe merged commit 7df72a1 into Sendspin:main Sep 17, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Fixes a bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants