Summary
Represent one installed protocol as one strong owner per connection runtime, rather than cloning the same protocol Arc into six forwarding closures.
At the same time, give every packet-oriented outbound path one consistent hook-dispatch boundary, completing or explicitly coordinating #547 without reopening ADR 010.
Problem
ProtocolHooks::from_protocol currently creates separate boxed closures for:
on_connection_setup;
before_send;
on_command_end;
handle_error;
stream_end_frame;
on_eof.
Each closure captures its own clone of the same protocol Arc. The resulting ProtocolHooks value belongs to one connection actor, so these six strong references do not represent six independent lifetimes.
Separately, #547 records a correctness gap: ordinary WireframeApp responses bypass the configured packet-oriented before_send hook, while actor-driven push and streaming paths invoke it.
Design goals
- One installed protocol definition may be shared by independent connections.
- One connection should hold at most one strong protocol reference.
- Per-connection mutable context remains in
ConnectionContext, not inside a shared protocol lock.
- Existing custom hook construction, if retained, must remain possible without forcing the protocol adapter through six Arcs.
- Hook ordering and packet-versus-transport semantics remain consistent with ADR 010.
Proposed representation
Use an enum or equivalent explicit representation, for example:
enum ProtocolHooks<F, E> {
None,
Protocol(Arc<dyn WireframeProtocol<Frame = F, ProtocolError = E>>),
Custom(CustomProtocolHooks<F, E>),
}
CustomProtocolHooks may retain boxed FnMut/FnOnce callbacks where callers genuinely install independent closures. The protocol-backed variant dispatches directly to one shared protocol object.
An alternative representation is acceptable if it proves:
- one strong protocol owner per connection;
- no six-closure forwarding layer for
WireframeProtocol;
- no loss of custom hook behaviour.
Hook dispatch boundary
Preserve ADR 010's decision:
- hooks remain packet-oriented;
- the codec driver owns transport-frame emission;
- no second transport-frame hook API is introduced.
Ensure the installed protocol observes the intended events for all outbound packet paths:
- ordinary route-handler responses;
- push queue frames;
- streamed responses;
- multi-packet channels;
- stream-end terminators.
before_send should execute exactly once for each emitted packet before serialization. Avoid applying it once in the app path and again in the actor path.
Context ownership
Each connection owns one ConnectionContext. Every protocol method for that connection receives the same context instance in the documented order.
Do not place ConnectionContext behind Arc<Mutex<_>>; the connection runtime serializes hook invocation.
Acceptance criteria
Tests
Create a recording protocol that captures ordered events and counts strong ownership/drop behaviour. Cover:
- one ordinary response;
- one high-priority push;
- one low-priority push;
- one streamed response;
- one multi-packet response and terminator;
- one protocol error;
- clean and partial EOF;
- connection setup and final command completion.
Assert exact event sequences and exactly-once invocation.
Non-goals
- Changing ADR 010's packet/codec boundary.
- Making protocol implementations mutable through
&mut self; per-connection mutation belongs in ConnectionContext.
- Removing
Arc from the application-wide protocol root when independent connection tasks genuinely share it.
- Redesigning the public protocol error type beyond what this representation requires.
Dependencies
References
Summary
Represent one installed protocol as one strong owner per connection runtime, rather than cloning the same protocol
Arcinto six forwarding closures.At the same time, give every packet-oriented outbound path one consistent hook-dispatch boundary, completing or explicitly coordinating #547 without reopening ADR 010.
Problem
ProtocolHooks::from_protocolcurrently creates separate boxed closures for:on_connection_setup;before_send;on_command_end;handle_error;stream_end_frame;on_eof.Each closure captures its own clone of the same protocol
Arc. The resultingProtocolHooksvalue belongs to one connection actor, so these six strong references do not represent six independent lifetimes.Separately, #547 records a correctness gap: ordinary
WireframeAppresponses bypass the configured packet-orientedbefore_sendhook, while actor-driven push and streaming paths invoke it.Design goals
ConnectionContext, not inside a shared protocol lock.Proposed representation
Use an enum or equivalent explicit representation, for example:
CustomProtocolHooksmay retain boxedFnMut/FnOncecallbacks where callers genuinely install independent closures. The protocol-backed variant dispatches directly to one shared protocol object.An alternative representation is acceptable if it proves:
WireframeProtocol;Hook dispatch boundary
Preserve ADR 010's decision:
Ensure the installed protocol observes the intended events for all outbound packet paths:
before_sendshould execute exactly once for each emitted packet before serialization. Avoid applying it once in the app path and again in the actor path.Context ownership
Each connection owns one
ConnectionContext. Every protocol method for that connection receives the same context instance in the documented order.Do not place
ConnectionContextbehindArc<Mutex<_>>; the connection runtime serializes hook invocation.Acceptance criteria
ProtocolHooks::from_protocolno longer creates six protocol-owning forwarding closures.ConnectionContextis reused for the complete connection lifetime.before_sendruns exactly once for ordinary route-handler responses.before_sendcontinues to run exactly once for push, stream, multi-packet, and terminator frames.on_connection_setup,on_command_end,handle_error,stream_end_frame, andon_eofretain their current ordering and cardinality.protocol()is retained and tested only if downstream access still has a justified ownership contract.Tests
Create a recording protocol that captures ordered events and counts strong ownership/drop behaviour. Cover:
Assert exact event sequences and exactly-once invocation.
Non-goals
&mut self; per-connection mutation belongs inConnectionContext.Arcfrom the application-wide protocol root when independent connection tasks genuinely share it.Dependencies
References
docs/adr-010-transport-frame-boundary-for-zero-copy.mdsrc/hooks.rssrc/app/builder/protocol.rssrc/app/frame_handling/src/connection/