@@ -260,12 +260,24 @@ pub trait Dialer: Send {
260260
261261// ===========================================================================
262262// Layer 3 — dispatch (the semantics). Doc 034 § Layer 3 + § EnvelopeCodec.
263- // RPC and streaming unify in ONE trait (Decision 2): three reply cardinalities
264- // — `call` (one) / `subscribe` (many) / `write` (none).
263+ // RPC and streaming unify in ONE per-connection role (Decision 2): three reply
264+ // cardinalities — `call` (one) / `subscribe` (many) / `write` (none).
265+ //
266+ // The role is split across two traits so the shared, immutable half (one
267+ // `Arc<dyn Dispatch>` per server) and the per-connection mutable half (one
268+ // `Box<dyn Session>` per accepted connection) each own what they need:
269+ //
270+ // - [`Dispatch`] — `Send + Sync`, shared: `authenticate` + an `open` factory.
271+ // - [`Session`] — `Send`, per-connection: `call` / `subscribe` / `write` on
272+ // `&mut self`, so a connection can hold mutable state (e.g. `record.drain`'s
273+ // lazy per-record cursors — the one seam the AimX wire reshape did not
274+ // dissolve) without a lock. See doc 037 (the additive server-port refinement,
275+ // mirroring the Phase-2 `encode_inbound`/`decode_outbound` precedent).
265276// ===========================================================================
266277
267- /// The application dispatch: authenticate a session, then serve calls,
268- /// subscriptions, and writes against a [`SessionCtx`].
278+ /// The shared application dispatch: authenticate a connection, then open a
279+ /// per-connection [`Session`]. One `Arc<dyn Dispatch>` is shared across every
280+ /// connection a server accepts, so it stays `Sync` and behind `&self`.
269281pub trait Dispatch : Send + Sync {
270282 /// Resolve a [`SessionCtx`] from peer metadata and/or the first frame
271283 /// (WS supplies pre-resolved identity via [`PeerInfo`]; UDS reads a Hello).
@@ -275,28 +287,42 @@ pub trait Dispatch: Send + Sync {
275287 first : Option < & ' a [ u8 ] > ,
276288 ) -> BoxFut < ' a , Result < SessionCtx , AuthError > > ;
277289
290+ /// Open the per-connection [`Session`] once, after [`authenticate`]. The
291+ /// returned session owns the connection's mutable dispatch state (drain
292+ /// cursors today, per-session auth identity in Phase 4) that the shared
293+ /// `Arc<Self>` cannot hold behind `&self`; the engine threads `&mut` into it.
294+ fn open ( & self , ctx : & SessionCtx ) -> Box < dyn Session > ;
295+ }
296+
297+ /// The per-connection session: serves calls, subscriptions, and writes for one
298+ /// accepted [`Connection`]. The engine ([`run_session`]) owns the
299+ /// `Box<dyn Session>` and threads `&mut self` into each method, so a session can
300+ /// hold per-connection mutable state without a lock — while the shared,
301+ /// immutable role stays on [`Dispatch`].
302+ pub trait Session : Send {
278303 /// One-shot RPC: one request → one reply.
279304 fn call < ' a > (
280- & ' a self ,
281- ctx : & ' a SessionCtx ,
305+ & ' a mut self ,
282306 method : & ' a str ,
283307 params : Payload ,
284308 ) -> BoxFut < ' a , Result < Payload , RpcError > > ;
285309
286310 /// Streaming: open a subscription that yields many payloads. The stream is
287- /// `'static` so it can hold its own buffer reader inside the engine's
288- /// `FuturesUnordered` (doc 034 risk list).
289- fn subscribe (
290- & self ,
291- ctx : & SessionCtx ,
292- topic : & str ,
293- ) -> Result < BoxStream < ' static , Payload > , RpcError > ;
311+ /// `'static` (it captures cloned handles), so it outlives the `&mut` borrow
312+ /// and lives in the engine's `FuturesUnordered` (doc 034 risk list).
313+ ///
314+ /// Defaulted to [`RpcError::NotFound`] so a dispatch with no streaming
315+ /// surface need not implement it (doc 037 § the server-port refinement —
316+ /// the stream is side-neutral, so it is defaulted here for symmetry).
317+ fn subscribe ( & mut self , topic : & str ) -> Result < BoxStream < ' static , Payload > , RpcError > {
318+ let _ = topic;
319+ Err ( RpcError :: NotFound )
320+ }
294321
295322 /// Fire-and-forget write: no reply. Routes through the existing
296323 /// producer/arbiter path (single-writer-per-key stays intact).
297324 fn write < ' a > (
298- & ' a self ,
299- ctx : & ' a SessionCtx ,
325+ & ' a mut self ,
300326 topic : & ' a str ,
301327 payload : Payload ,
302328 ) -> BoxFut < ' a , Result < ( ) , RpcError > > ;
@@ -368,12 +394,13 @@ pub trait Source: Send {
368394// `Box<dyn Trait>` from a mock per the acceptance criteria.
369395// ===========================================================================
370396
371- #[ allow( dead_code) ]
397+ #[ allow( dead_code, clippy :: too_many_arguments ) ]
372398fn _assert_object_safe (
373399 _connection : & dyn Connection ,
374400 _listener : & dyn Listener ,
375401 _dialer : & dyn Dialer ,
376402 _dispatch : & dyn Dispatch ,
403+ _session : & dyn Session ,
377404 _codec : & dyn EnvelopeCodec ,
378405 _sink : & dyn Sink ,
379406 _source : & dyn Source ,
@@ -420,24 +447,25 @@ mod tests {
420447 ) -> BoxFut < ' a , Result < SessionCtx , AuthError > > {
421448 unimplemented ! ( )
422449 }
450+ fn open ( & self , _ctx : & SessionCtx ) -> Box < dyn Session > {
451+ unimplemented ! ( )
452+ }
453+ }
454+
455+ struct MockSession ;
456+ impl Session for MockSession {
423457 fn call < ' a > (
424- & ' a self ,
425- _ctx : & ' a SessionCtx ,
458+ & ' a mut self ,
426459 _method : & ' a str ,
427460 _params : Payload ,
428461 ) -> BoxFut < ' a , Result < Payload , RpcError > > {
429462 unimplemented ! ( )
430463 }
431- fn subscribe (
432- & self ,
433- _ctx : & SessionCtx ,
434- _topic : & str ,
435- ) -> Result < BoxStream < ' static , Payload > , RpcError > {
464+ fn subscribe ( & mut self , _topic : & str ) -> Result < BoxStream < ' static , Payload > , RpcError > {
436465 unimplemented ! ( )
437466 }
438467 fn write < ' a > (
439- & ' a self ,
440- _ctx : & ' a SessionCtx ,
468+ & ' a mut self ,
441469 _topic : & ' a str ,
442470 _payload : Payload ,
443471 ) -> BoxFut < ' a , Result < ( ) , RpcError > > {
@@ -487,6 +515,7 @@ mod tests {
487515 let _listener: Box < dyn Listener > = Box :: new ( MockListener ) ;
488516 let _dialer: Box < dyn Dialer > = Box :: new ( MockDialer ) ;
489517 let _dispatch: Box < dyn Dispatch > = Box :: new ( MockDispatch ) ;
518+ let _session: Box < dyn Session > = Box :: new ( MockSession ) ;
490519 let _codec: Box < dyn EnvelopeCodec > = Box :: new ( MockCodec ) ;
491520 let _sink: Box < dyn Sink > = Box :: new ( MockSink ) ;
492521 let _source: Box < dyn Source > = Box :: new ( MockSource ) ;
0 commit comments