Skip to main content

unb_runtime/
core_runtime.rs

1use std::collections::HashMap;
2use std::collections::VecDeque;
3use std::future::Future;
4use std::pin::Pin;
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::Arc;
7
8use bytes::Bytes;
9use tokio::sync::{mpsc, oneshot, OwnedSemaphorePermit, Semaphore};
10use unb_core::{
11    ClientDelivery as CoreClientDelivery, ClientOperationId, CoreEffect, CoreInput, EffectId,
12    Envelope, ProtocolCore, RetirementReason, SendResult, SessionId,
13};
14use web_time::Instant;
15
16use crate::cancellation::CancellationToken;
17use crate::client::{ClientDelivery, ClientSession};
18use crate::error::WsError;
19use crate::transport::{Pipe, PipeReader, PipeWriter};
20use crate::wire::{Directive, Wire};
21use crate::{BodyStream, WireBody};
22
23const CORE_QUEUE: usize = 256;
24const SESSION_QUEUE: usize = 64;
25const OUTBOUND_QUEUE: usize = 8_192;
26const OUTBOUND_BYTE_BUDGET: usize = 64 * 1024 * 1024;
27const BODY_WINDOW_BYTES: usize = 256 * 1024;
28const WRITE_BATCH: usize = 8;
29const RELAY_BACKPRESSURE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
30const TERMINAL_SEND_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
31const CLIENT_DELIVERY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
32const ESTABLISHMENT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
33const PENDING_MODE_WRITES_MAX: usize = 128;
34const OPERATION_WRITE_QUEUE: usize = OUTBOUND_QUEUE;
35const OPERATION_ACTIVE_MAX: usize = 128;
36const OPERATION_BODY_CHUNK_BYTES: usize = 64 * 1024;
37
38#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
39fn clock_now() -> Instant {
40    n0_future::time::Instant::now().into_std()
41}
42
43#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
44async fn sleep_to(deadline: Instant) {
45    n0_future::time::sleep_until(n0_future::time::Instant::from_std(deadline)).await
46}
47
48#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
49fn clock_now() -> Instant {
50    n0_future::time::Instant::now()
51}
52
53#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
54async fn sleep_to(deadline: Instant) {
55    n0_future::time::sleep_until(deadline).await
56}
57
58#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
59pub use tokio::runtime::Handle as RuntimeHandle;
60
61#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
62#[derive(Clone, Debug)]
63pub struct RuntimeHandle;
64
65#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
66impl RuntimeHandle {
67    pub fn current() -> Self {
68        RuntimeHandle
69    }
70
71    pub fn spawn<F>(&self, future: F)
72    where
73        F: Future<Output = ()> + 'static,
74    {
75        n0_future::task::spawn(future);
76    }
77}
78
79pub type EffectFuture = Pin<Box<dyn Future<Output = Option<CoreInput>> + Send>>;
80
81pub trait EffectExecutor: Send + Sync + 'static {
82    fn execute(&self, effect: CoreEffect, handle: ProtocolCoreHandle) -> EffectFuture;
83}
84
85#[derive(Clone, Debug)]
86pub enum SessionOutcome {
87    Established,
88    Retired(RetirementReason),
89}
90
91pub trait SessionHandler: Send + 'static {
92    fn deliver(&mut self, envelope: Envelope) -> impl Future<Output = ()> + Send;
93
94    fn stream_closed(&mut self, _operation: ClientOperationId) -> impl Future<Output = ()> + Send {
95        async {}
96    }
97}
98
99impl SessionHandler for () {
100    async fn deliver(&mut self, _envelope: Envelope) {}
101}
102
103impl SessionHandler for mpsc::Sender<Envelope> {
104    async fn deliver(&mut self, envelope: Envelope) {
105        let _ = self.send(envelope).await;
106    }
107}
108
109impl SessionHandler for tokio::sync::broadcast::Sender<Envelope> {
110    async fn deliver(&mut self, envelope: Envelope) {
111        let _ = self.send(envelope);
112    }
113}
114
115type TokenBodies = Arc<std::sync::Mutex<HashMap<(SessionId, String), WireBody>>>;
116
117type BodyPumps = Arc<std::sync::Mutex<HashMap<(SessionId, String), (String, CancellationToken)>>>;
118
119const SESSION_BODY_TOKENS_MAX: usize = OUTBOUND_QUEUE;
120const SESSION_BODY_BYTES_MAX: usize = OUTBOUND_BYTE_BUDGET;
121
122fn mint_body_token() -> String {
123    use std::hash::{BuildHasher, Hasher};
124    type TokenKeys = (
125        std::collections::hash_map::RandomState,
126        std::collections::hash_map::RandomState,
127    );
128    static KEYS: std::sync::OnceLock<TokenKeys> = std::sync::OnceLock::new();
129    static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
130    let (low, high) = KEYS.get_or_init(TokenKeys::default);
131    let serial = NEXT.fetch_add(1, Ordering::Relaxed);
132    let mut a = low.build_hasher();
133    a.write_u64(serial);
134    let mut b = high.build_hasher();
135    b.write_u64(serial);
136    format!("b{:016x}{:016x}", a.finish(), b.finish())
137}
138
139fn application_core_input(
140    handle: &ProtocolCoreHandle,
141    session: &SessionId,
142    mut envelope: Envelope,
143) -> Result<CoreInput, unb_core::CoreError> {
144    if envelope.kind == unb_core::Kind::Discover {
145        return Ok(CoreInput::FrameReceived {
146            session: session.clone(),
147            envelope,
148        });
149    }
150    if !(envelope.kind.is_application_request()
151        || (envelope.corr.is_some()
152            && (envelope.kind.is_application_response()
153                || envelope.kind == unb_core::Kind::Cancel)))
154    {
155        return Ok(CoreInput::FrameReceived {
156            session: session.clone(),
157            envelope,
158        });
159    }
160    if envelope.body_token.is_some() && !envelope.payload.is_empty() {
161        return Err(unb_core::CoreError::Malformed(
162            "application frame cannot carry both inline and referenced bodies".into(),
163        ));
164    }
165    if envelope.body_token.is_none()
166        && envelope.kind != unb_core::Kind::Error
167        && !envelope.payload.is_empty()
168    {
169        let payload = std::mem::take(&mut envelope.payload);
170        envelope.body_token = Some(
171            handle
172                .register_body(session, WireBody::Bytes(payload))?
173                .to_string(),
174        );
175    }
176    Ok(CoreInput::ApplicationFrameReceived {
177        session: session.clone(),
178        frame: unb_core::ApplicationFrame::from_envelope(&envelope)?,
179    })
180}
181
182fn register_outbound_body(
183    handle: &ProtocolCoreHandle,
184    session: &SessionId,
185    payload: Bytes,
186    body: Option<BodyStream>,
187) -> Result<Option<unb_core::BodyId>, unb_core::CoreError> {
188    if body.is_some() && !payload.is_empty() {
189        return Err(unb_core::CoreError::Malformed(
190            "an operation cannot provide both unary bytes and a streaming body".into(),
191        ));
192    }
193    let body = match body {
194        Some(body) => Some(WireBody::Stream(body)),
195        None if !payload.is_empty() => Some(WireBody::Bytes(payload)),
196        None => None,
197    };
198    body.map(|body| handle.register_body(session, body))
199        .transpose()
200}
201
202#[derive(Clone)]
203pub struct ProtocolCoreHandle {
204    inputs: mpsc::Sender<ActorInput>,
205    cancellation: CancellationToken,
206    token_bodies: TokenBodies,
207    body_pumps: BodyPumps,
208}
209
210impl ProtocolCoreHandle {
211    pub fn spawn(
212        core: ProtocolCore,
213        executor: Arc<dyn EffectExecutor>,
214        cancellation: CancellationToken,
215        runtime: &RuntimeHandle,
216    ) -> Self {
217        let (inputs, receiver) = mpsc::channel(CORE_QUEUE);
218        let handle = Self {
219            inputs,
220            cancellation,
221            token_bodies: Arc::new(std::sync::Mutex::new(HashMap::new())),
222            body_pumps: Arc::new(std::sync::Mutex::new(HashMap::new())),
223        };
224        runtime.spawn(CoreRuntime::new(core, receiver, handle.clone(), executor).run());
225        handle
226    }
227
228    pub fn claim_body(&self, session: &SessionId, token: &str) -> Option<WireBody> {
229        self.token_bodies
230            .lock()
231            .expect("body token registry")
232            .remove(&(session.clone(), token.to_string()))
233    }
234
235    pub fn claim_body_stream(&self, session: &SessionId, token: &str) -> Option<BodyStream> {
236        self.claim_body(session, token).map(wire_body_stream)
237    }
238
239    pub fn register_body(
240        &self,
241        session: &SessionId,
242        body: WireBody,
243    ) -> Result<unb_core::BodyId, unb_core::CoreError> {
244        let mut registry = self.token_bodies.lock().expect("body token registry");
245        let owned = registry
246            .keys()
247            .filter(|(owner, _)| owner == session)
248            .count();
249        if owned >= SESSION_BODY_TOKENS_MAX {
250            return Err(unb_core::CoreError::EffectQueueFull);
251        }
252        let incoming = match &body {
253            WireBody::Bytes(payload) => payload.len(),
254            WireBody::Stream(_) => 0,
255        };
256        let retained = registry
257            .iter()
258            .filter_map(|((owner, _), body)| {
259                (owner == session).then_some(match body {
260                    WireBody::Bytes(payload) => payload.len(),
261                    WireBody::Stream(_) => 0,
262                })
263            })
264            .sum::<usize>();
265        if retained.saturating_add(incoming) > SESSION_BODY_BYTES_MAX {
266            return Err(unb_core::CoreError::BodyRegistryFull(
267                SESSION_BODY_BYTES_MAX,
268            ));
269        }
270        let token = mint_body_token();
271        registry.insert((session.clone(), token.clone()), body);
272        Ok(unb_core::BodyId::from(token))
273    }
274
275    fn register_body_pump(
276        &self,
277        session: &SessionId,
278        corr: &str,
279        token: &str,
280        cancellation: CancellationToken,
281    ) {
282        self.body_pumps.lock().expect("body pump registry").insert(
283            (session.clone(), corr.to_string()),
284            (token.to_string(), cancellation),
285        );
286    }
287
288    fn cancel_body_pump(&self, session: &SessionId, corr: &str) {
289        let cancelled = self
290            .body_pumps
291            .lock()
292            .expect("body pump registry")
293            .remove(&(session.clone(), corr.to_string()));
294        if let Some((_, cancellation)) = cancelled {
295            cancellation.cancel();
296        }
297    }
298
299    fn release_body_pump(&self, session: &SessionId, corr: &str, token: &str) {
300        let mut pumps = self.body_pumps.lock().expect("body pump registry");
301        let key = (session.clone(), corr.to_string());
302        if pumps.get(&key).is_some_and(|(owner, _)| owner == token) {
303            pumps.remove(&key);
304        }
305    }
306
307    pub async fn submit(&self, input: CoreInput) -> Result<(), WsError> {
308        self.inputs
309            .send(ActorInput::Core(input))
310            .await
311            .map_err(|_| WsError::Gone)
312    }
313
314    pub async fn submit_batch(&self, inputs: Vec<CoreInput>) -> Result<(), WsError> {
315        self.inputs
316            .send(ActorInput::CoreBatch(inputs))
317            .await
318            .map_err(|_| WsError::Gone)
319    }
320
321    pub async fn install<F>(&self, input: CoreInput, publish: F) -> Result<(), WsError>
322    where
323        F: FnOnce() + Send + 'static,
324    {
325        let (reply, response) = oneshot::channel();
326        self.inputs
327            .send(ActorInput::Install {
328                input,
329                publish: Box::new(publish),
330                reply,
331            })
332            .await
333            .map_err(|_| WsError::Gone)?;
334        response.await.map_err(|_| WsError::Gone)?
335    }
336
337    pub async fn attach<H: SessionHandler>(
338        &self,
339        session: SessionId,
340        transport: Pipe,
341        expected_peer: Option<String>,
342        handler: H,
343    ) -> Result<Arc<Wire>, WsError> {
344        self.attach_with_ceiling(
345            session,
346            transport,
347            expected_peer,
348            handler,
349            unb_transport::DEFAULT_MAX_FRAME_SIZE,
350        )
351        .await
352    }
353
354    pub async fn attach_with_ceiling<H: SessionHandler>(
355        &self,
356        session: SessionId,
357        transport: Pipe,
358        expected_peer: Option<String>,
359        handler: H,
360        ws_collect_ceiling: usize,
361    ) -> Result<Arc<Wire>, WsError> {
362        let cancellation = self.cancellation.child_token();
363        let (wire, directives, outcome, routes) = Wire::channel(cancellation.clone());
364        let (reply, response) = oneshot::channel();
365        self.inputs
366            .send(ActorInput::Attach {
367                session,
368                transport,
369                expected_peer,
370                directives,
371                handler: handler_channel(handler),
372                client: wire.client_session(),
373                cancellation,
374                establish_peer: true,
375                outcome,
376                routes,
377                ws_collect_ceiling,
378                reply,
379            })
380            .await
381            .map_err(|_| WsError::Gone)?;
382        response.await.map_err(|_| WsError::Gone)??;
383        Ok(wire)
384    }
385
386    pub fn shutdown(&self) {
387        self.cancellation.cancel();
388    }
389
390    pub(crate) fn open(transport: Pipe, runtime: &RuntimeHandle) -> (Arc<Wire>, Self) {
391        struct StandaloneExecutor;
392
393        impl EffectExecutor for StandaloneExecutor {
394            fn execute(&self, _effect: CoreEffect, _handle: ProtocolCoreHandle) -> EffectFuture {
395                Box::pin(async { None })
396            }
397        }
398
399        let cancellation = CancellationToken::new();
400        let handle = Self::spawn(
401            ProtocolCore::new("client"),
402            Arc::new(StandaloneExecutor),
403            cancellation.clone(),
404            runtime,
405        );
406        let session_cancellation = cancellation.child_token();
407        let (wire, directives, outcome, routes) =
408            Wire::standalone_channel(session_cancellation.clone());
409        let (reply, _response) = oneshot::channel();
410        handle
411            .inputs
412            .try_send(ActorInput::Attach {
413                session: SessionId::from("client-session"),
414                transport,
415                expected_peer: None,
416                directives,
417                handler: handler_channel(wire.observation_sender()),
418                client: wire.client_session(),
419                cancellation: session_cancellation,
420                establish_peer: false,
421                outcome,
422                routes,
423                ws_collect_ceiling: unb_transport::DEFAULT_MAX_FRAME_SIZE,
424                reply,
425            })
426            .expect("new protocol core input queue");
427        (wire, handle)
428    }
429}
430
431fn wire_body_stream(body: WireBody) -> BodyStream {
432    match body {
433        WireBody::Bytes(bytes) => Box::pin(futures_util::stream::once(async move { Ok(bytes) })),
434        WireBody::Stream(stream) => stream,
435    }
436}
437
438enum ActorInput {
439    Core(CoreInput),
440    CoreBatch(Vec<CoreInput>),
441    Install {
442        input: CoreInput,
443        publish: Box<dyn FnOnce() + Send>,
444        reply: oneshot::Sender<Result<(), WsError>>,
445    },
446    Directive(SessionId, Directive),
447    Attach {
448        session: SessionId,
449        transport: Pipe,
450        expected_peer: Option<String>,
451        directives: mpsc::Receiver<Directive>,
452        handler: mpsc::Sender<HandlerInput>,
453        client: ClientSession,
454        cancellation: CancellationToken,
455        establish_peer: bool,
456        outcome: tokio::sync::watch::Sender<Option<SessionOutcome>>,
457        routes: tokio::sync::watch::Sender<bool>,
458        ws_collect_ceiling: usize,
459        reply: oneshot::Sender<Result<(), WsError>>,
460    },
461    Write {
462        session: SessionId,
463        envelope: Envelope,
464        completion: Option<EffectId>,
465        relay: bool,
466    },
467    TransportModeSelected {
468        session: SessionId,
469        mode: unb_transport::TransportMode,
470    },
471}
472
473struct SessionIo {
474    outbound: mpsc::Sender<WriteRequest>,
475    byte_budget: Arc<Semaphore>,
476    ws_collect_ceiling: usize,
477    bodies: crate::transport::SessionStreams,
478    handler: mpsc::Sender<HandlerInput>,
479    client: ClientSession,
480    cancellation: CancellationToken,
481    reservation_pending: Arc<AtomicBool>,
482    establish_peer: bool,
483    deadline: tokio::sync::watch::Sender<Option<Instant>>,
484    routes: tokio::sync::watch::Sender<bool>,
485    transport_mode: Option<unb_transport::TransportMode>,
486    pending_mode_writes: VecDeque<PendingModeWrite>,
487    operation_lanes: OperationLanes,
488    operation_slots: Arc<Semaphore>,
489}
490
491struct PendingModeWrite {
492    envelope: Envelope,
493    completion: Option<EffectId>,
494    relay: bool,
495}
496
497type OperationLanes = Arc<std::sync::Mutex<HashMap<String, OperationLane>>>;
498
499#[derive(Clone)]
500struct OperationLane {
501    outbound: Option<mpsc::Sender<OperationWriteRequest>>,
502    control: Option<unb_transport::operation::OperationControl>,
503}
504
505struct OperationWriteRequest {
506    envelope: Envelope,
507    completion: Option<EffectId>,
508    relay: bool,
509    body: Option<BodyStream>,
510    _budget: OwnedSemaphorePermit,
511}
512
513struct WriteRequest {
514    envelope: Envelope,
515    completion: Option<EffectId>,
516    relay: bool,
517    _budget: OwnedSemaphorePermit,
518}
519
520impl WriteRequest {
521    fn weight(envelope: &Envelope) -> u32 {
522        let head = envelope.target.len()
523            + envelope.subject.len()
524            + envelope.id.len()
525            + envelope.corr.as_ref().map_or(0, String::len)
526            + envelope.path.iter().map(String::len).sum::<usize>()
527            + envelope
528                .headers
529                .iter()
530                .map(|(name, value)| name.len() + value.as_str().map_or(16, str::len))
531                .sum::<usize>();
532        (envelope.payload.len() + head).clamp(1, OUTBOUND_BYTE_BUDGET) as u32
533    }
534}
535
536#[derive(Debug)]
537pub(crate) struct MeteredChunk {
538    pub(crate) bytes: Bytes,
539    _window: OwnedSemaphorePermit,
540    _budget: OwnedSemaphorePermit,
541}
542
543pub(crate) struct MeteredBody {
544    stream: BodyStream,
545    budget: Arc<Semaphore>,
546    window: Arc<Semaphore>,
547    pending: Option<Bytes>,
548    cancellation: CancellationToken,
549}
550
551impl MeteredBody {
552    pub(crate) fn new(
553        stream: BodyStream,
554        budget: Arc<Semaphore>,
555        cancellation: CancellationToken,
556    ) -> MeteredBody {
557        MeteredBody {
558            stream,
559            budget,
560            window: Arc::new(Semaphore::new(BODY_WINDOW_BYTES)),
561            pending: None,
562            cancellation,
563        }
564    }
565
566    pub(crate) async fn next_chunk(&mut self) -> Result<Option<MeteredChunk>, WsError> {
567        let chunk = match self.pending.take() {
568            Some(chunk) => chunk,
569            None => {
570                let next = tokio::select! {
571                    biased;
572                    () = self.cancellation.cancelled() => return Ok(None),
573                    next = std::future::poll_fn(|cx| self.stream.as_mut().poll_next(cx)) => next,
574                };
575                match next {
576                    Some(Ok(chunk)) => chunk,
577                    Some(Err(error)) => return Err(error.into()),
578                    None => return Ok(None),
579                }
580            }
581        };
582        let len = chunk.len();
583        if len > unb_transport::DEFAULT_MAX_FRAME_SIZE {
584            return Err(
585                unb_core::CoreError::BodyTooLarge(unb_transport::DEFAULT_MAX_FRAME_SIZE).into(),
586            );
587        }
588        self.pending = Some(chunk);
589        let window_weight = len.clamp(1, BODY_WINDOW_BYTES) as u32;
590        let budget_weight = len.max(1) as u32;
591        let acquire = async {
592            let window = self
593                .window
594                .clone()
595                .acquire_many_owned(window_weight)
596                .await
597                .map_err(|_| unb_core::CoreError::BodyStalled)?;
598            let budget = self
599                .budget
600                .clone()
601                .acquire_many_owned(budget_weight)
602                .await
603                .map_err(|_| unb_core::CoreError::BodyStalled)?;
604            Ok::<_, unb_core::CoreError>((window, budget))
605        };
606        let acquired = tokio::select! {
607            biased;
608            () = self.cancellation.cancelled() => return Ok(None),
609            acquired = n0_future::time::timeout(RELAY_BACKPRESSURE_TIMEOUT, acquire) => acquired,
610        };
611        match acquired {
612            Ok(Ok((window, budget))) => Ok(Some(MeteredChunk {
613                bytes: self.pending.take().expect("pending chunk present"),
614                _window: window,
615                _budget: budget,
616            })),
617            Ok(Err(error)) => Err(error.into()),
618            Err(_) => Err(unb_core::CoreError::BodyStalled.into()),
619        }
620    }
621}
622
623struct CoreRuntime {
624    core: ProtocolCore,
625    inputs: mpsc::Receiver<ActorInput>,
626    handle: ProtocolCoreHandle,
627    executor: Arc<dyn EffectExecutor>,
628    sessions: HashMap<SessionId, SessionIo>,
629    outcomes: HashMap<SessionId, tokio::sync::watch::Sender<Option<SessionOutcome>>>,
630    client_starts: HashMap<SessionId, PendingClientStart>,
631    pending_bodies: std::sync::Mutex<HashMap<(SessionId, String), BodyStream>>,
632}
633
634struct PendingClientStart {
635    sender: mpsc::Sender<ClientDelivery>,
636    reply: oneshot::Sender<Result<ClientOperationId, unb_core::CoreError>>,
637}
638
639impl CoreRuntime {
640    fn new(
641        core: ProtocolCore,
642        inputs: mpsc::Receiver<ActorInput>,
643        handle: ProtocolCoreHandle,
644        executor: Arc<dyn EffectExecutor>,
645    ) -> Self {
646        Self {
647            core,
648            inputs,
649            handle,
650            executor,
651            sessions: HashMap::new(),
652            outcomes: HashMap::new(),
653            client_starts: HashMap::new(),
654            pending_bodies: std::sync::Mutex::new(HashMap::new()),
655        }
656    }
657
658    async fn run(mut self) {
659        loop {
660            let input = tokio::select! {
661                biased;
662                () = self.handle.cancellation.cancelled() => return,
663                input = self.inputs.recv() => input,
664            };
665            let Some(input) = input else { return };
666            match input {
667                ActorInput::Core(input) => {
668                    let _ = self.core.handle(clock_now(), input);
669                }
670                ActorInput::CoreBatch(inputs) => {
671                    let now = clock_now();
672                    for input in inputs {
673                        let _ = self.core.handle(now, input);
674                    }
675                }
676                ActorInput::Install {
677                    input,
678                    publish,
679                    reply,
680                } => {
681                    let result = self.core.handle(clock_now(), input).map_err(WsError::from);
682                    if result.is_ok() {
683                        publish();
684                    }
685                    let _ = reply.send(result);
686                }
687                ActorInput::Directive(session, directive) => {
688                    self.directive(&session, directive).await;
689                }
690                ActorInput::Attach {
691                    session,
692                    transport,
693                    expected_peer,
694                    directives,
695                    handler,
696                    client,
697                    cancellation,
698                    establish_peer,
699                    outcome,
700                    routes,
701                    ws_collect_ceiling,
702                    reply,
703                } => {
704                    let result = self
705                        .attach(
706                            session,
707                            transport,
708                            expected_peer,
709                            directives,
710                            handler,
711                            client,
712                            cancellation,
713                            establish_peer,
714                            outcome,
715                            routes,
716                            ws_collect_ceiling,
717                        )
718                        .await;
719                    let _ = reply.send(result);
720                }
721                ActorInput::Write {
722                    session,
723                    envelope,
724                    completion,
725                    relay,
726                } => {
727                    self.write(&session, envelope, completion, relay).await;
728                }
729                ActorInput::TransportModeSelected { session, mode } => {
730                    let pending = if let Some(io) = self.sessions.get_mut(&session) {
731                        io.transport_mode = Some(mode);
732                        std::mem::take(&mut io.pending_mode_writes)
733                    } else {
734                        VecDeque::new()
735                    };
736                    if mode == unb_transport::TransportMode::OperationStreamsV1 {
737                        self.start_operation_acceptor(&session);
738                    }
739                    for pending in pending {
740                        self.write(
741                            &session,
742                            pending.envelope,
743                            pending.completion,
744                            pending.relay,
745                        )
746                        .await;
747                    }
748                }
749            }
750            self.drain().await;
751        }
752    }
753
754    #[allow(clippy::too_many_arguments)]
755    async fn attach(
756        &mut self,
757        session: SessionId,
758        transport: Pipe,
759        expected_peer: Option<String>,
760        directives: mpsc::Receiver<Directive>,
761        handler: mpsc::Sender<HandlerInput>,
762        client: ClientSession,
763        cancellation: CancellationToken,
764        establish_peer: bool,
765        outcome: tokio::sync::watch::Sender<Option<SessionOutcome>>,
766        routes: tokio::sync::watch::Sender<bool>,
767        ws_collect_ceiling: usize,
768    ) -> Result<(), WsError> {
769        let initiator = transport.initiator();
770        let bodies = transport.streams();
771        let (reader, writer) = transport.split();
772        let (outbound, outbound_rx) = mpsc::channel(OUTBOUND_QUEUE);
773        let (deadline, deadline_rx) = tokio::sync::watch::channel(None);
774        self.core.handle(
775            clock_now(),
776            CoreInput::SessionOpened {
777                session: session.clone(),
778                initiator,
779                establish_peer,
780                expected_peer,
781            },
782        )?;
783        self.sessions.insert(
784            session.clone(),
785            SessionIo {
786                outbound,
787                byte_budget: Arc::new(Semaphore::new(OUTBOUND_BYTE_BUDGET)),
788                ws_collect_ceiling,
789                bodies: bodies.clone(),
790                handler,
791                client,
792                cancellation: cancellation.clone(),
793                reservation_pending: Arc::new(AtomicBool::new(false)),
794                establish_peer,
795                deadline,
796                routes,
797                transport_mode: None,
798                pending_mode_writes: VecDeque::new(),
799                operation_lanes: Arc::new(std::sync::Mutex::new(HashMap::new())),
800                operation_slots: Arc::new(Semaphore::new(OPERATION_ACTIVE_MAX)),
801            },
802        );
803        self.outcomes.insert(session.clone(), outcome);
804        spawn_session_deadline(
805            session.clone(),
806            deadline_rx,
807            self.handle.clone(),
808            cancellation.clone(),
809        );
810        spawn_directives(
811            session.clone(),
812            directives,
813            self.handle.clone(),
814            cancellation.clone(),
815        );
816        spawn_reader(
817            session.clone(),
818            reader,
819            self.handle.clone(),
820            cancellation.clone(),
821            bodies,
822        );
823        spawn_writer(
824            session.clone(),
825            writer,
826            outbound_rx,
827            self.handle.clone(),
828            cancellation,
829        );
830        if establish_peer {
831            spawn_establishment_timeout(session, self.handle.clone());
832        }
833        Ok(())
834    }
835
836    async fn directive(&mut self, session: &SessionId, directive: Directive) {
837        match directive {
838            Directive::StartClientOperation {
839                target_path,
840                kind,
841                payload,
842                hops,
843                headers,
844                body,
845                timeout,
846                sender,
847                reply,
848            } => {
849                let input = if kind == unb_core::Kind::Discover {
850                    if body.is_some() {
851                        Err(unb_core::CoreError::Malformed(
852                            "discovery does not accept a streaming body".into(),
853                        ))
854                    } else {
855                        serde_json::from_slice(&payload)
856                            .map(unb_core::OperationInput::Discovery)
857                            .map_err(|error| unb_core::CoreError::Malformed(error.to_string()))
858                    }
859                } else {
860                    register_outbound_body(&self.handle, session, payload, body)
861                        .map(unb_core::OperationInput::Body)
862                };
863                let input = match input {
864                    Ok(input) => input,
865                    Err(error) => {
866                        let _ = reply.send(Err(error));
867                        return;
868                    }
869                };
870                let registered_body = match &input {
871                    unb_core::OperationInput::Body(body) => body.clone(),
872                    unb_core::OperationInput::Discovery(_) => None,
873                };
874                let result = self.core.handle(
875                    clock_now(),
876                    CoreInput::StartClientOperation {
877                        session: session.clone(),
878                        target_path,
879                        kind,
880                        input,
881                        hops,
882                        headers,
883                        timeout,
884                    },
885                );
886                if let Err(error) = result {
887                    if let Some(body) = &registered_body {
888                        let _ = self.handle.claim_body(session, body.as_str());
889                    }
890                    let _ = reply.send(Err(error));
891                } else {
892                    self.client_starts
893                        .insert(session.clone(), PendingClientStart { sender, reply });
894                }
895            }
896            Directive::OpenStream {
897                target_path,
898                kind,
899                payload,
900                hops,
901                headers,
902                body,
903                opened,
904                reply,
905            } => {
906                let result = match register_outbound_body(&self.handle, session, payload, body) {
907                    Ok(body) => {
908                        let result = self.core.open_stream_body(
909                            session,
910                            &target_path,
911                            kind,
912                            body.clone(),
913                            hops,
914                            headers,
915                        );
916                        if result.is_err() {
917                            if let Some(body) = &body {
918                                let _ = self.handle.claim_body(session, body.as_str());
919                            }
920                        }
921                        result
922                    }
923                    Err(error) => Err(error),
924                };
925                if let (Ok(corr), Some(opened)) = (&result, opened) {
926                    opened(corr.clone()).await;
927                }
928                let _ = reply.send(result);
929            }
930            Directive::Send { corr, payload } => {
931                match register_outbound_body(&self.handle, session, payload, None) {
932                    Ok(body) => {
933                        if self.core.send_body(session, &corr, body.clone()).is_err() {
934                            if let Some(body) = &body {
935                                let _ = self.handle.claim_body(session, body.as_str());
936                            }
937                        }
938                    }
939                    Err(error) => {
940                        let _ = self.core.fail(
941                            session,
942                            &corr,
943                            unb_core::ErrorCode::Busy,
944                            &error.to_string(),
945                        );
946                    }
947                }
948            }
949            Directive::Respond {
950                corr,
951                payload,
952                headers,
953                body,
954            } => match register_outbound_body(&self.handle, session, payload, body) {
955                Ok(body) => {
956                    if self
957                        .core
958                        .respond_body(session, &corr, body.clone(), headers)
959                        .is_err()
960                    {
961                        if let Some(body) = &body {
962                            let _ = self.handle.claim_body(session, body.as_str());
963                        }
964                    }
965                }
966                Err(error) => {
967                    let _ = self.core.fail(
968                        session,
969                        &corr,
970                        unb_core::ErrorCode::Busy,
971                        &error.to_string(),
972                    );
973                }
974            },
975            Directive::Fail {
976                corr,
977                code,
978                message,
979            } => {
980                let _ = self.core.fail(session, &corr, code, &message);
981            }
982            Directive::Cancel { corr } => {
983                let _ = self.core.cancel(session, &corr);
984            }
985            Directive::CancelClientOperation { operation } => {
986                let _ = self.core.handle(
987                    clock_now(),
988                    CoreInput::CancelClientOperation {
989                        session: session.clone(),
990                        operation,
991                    },
992                );
993            }
994            Directive::Control { kind, payload } => {
995                let _ = self.core.control(session, kind, payload);
996            }
997        }
998    }
999
1000    async fn drain(&mut self) {
1001        while let Some(effect) = self.core.poll_effect() {
1002            match effect {
1003                CoreEffect::SendFrame { session, envelope } => {
1004                    self.write(&session, envelope, None, false).await
1005                }
1006                CoreEffect::HandshakeEstablished { session, .. } => {
1007                    if let Some(bodies) =
1008                        self.sessions.get(&session).and_then(|io| io.bodies.clone())
1009                    {
1010                        let inputs = self.handle.inputs.clone();
1011                        let mode_session = session.clone();
1012                        n0_future::task::spawn(async move {
1013                            let mode = bodies.negotiate().await;
1014                            let _ = inputs
1015                                .send(ActorInput::TransportModeSelected {
1016                                    session: mode_session,
1017                                    mode,
1018                                })
1019                                .await;
1020                        });
1021                    }
1022                    if self
1023                        .sessions
1024                        .get(&session)
1025                        .is_some_and(|io| !io.establish_peer)
1026                    {
1027                        if let Some(outcome) = self.outcomes.get(&session) {
1028                            let _ = outcome.send(Some(SessionOutcome::Established));
1029                        }
1030                    }
1031                }
1032                CoreEffect::DeliverClient {
1033                    session,
1034                    operation,
1035                    delivery,
1036                } => {
1037                    let body = match &delivery {
1038                        CoreClientDelivery::Item(frame) | CoreClientDelivery::Terminal(frame) => {
1039                            frame
1040                                .body
1041                                .as_ref()
1042                                .and_then(|body| self.handle.claim_body(&session, body.as_str()))
1043                        }
1044                        _ => None,
1045                    };
1046                    let terminal = matches!(
1047                        delivery,
1048                        CoreClientDelivery::Terminal(_)
1049                            | CoreClientDelivery::Cancelled
1050                            | CoreClientDelivery::TimedOut
1051                            | CoreClientDelivery::SessionClosed
1052                    );
1053                    let client = self.sessions.get(&session).map(|io| io.client.clone());
1054                    if let Some(client) = client {
1055                        let delivered = n0_future::time::timeout(
1056                            CLIENT_DELIVERY_TIMEOUT,
1057                            client.deliver(&operation, delivery, body),
1058                        )
1059                        .await
1060                        .is_ok();
1061                        if !delivered {
1062                            client.abandon(&operation);
1063                            let _ = self.core.handle(
1064                                clock_now(),
1065                                CoreInput::CancelClientOperation {
1066                                    session: session.clone(),
1067                                    operation: operation.clone(),
1068                                },
1069                            );
1070                        }
1071                    }
1072                    if terminal {
1073                        self.handler(&session, HandlerInput::StreamClosed(operation))
1074                            .await;
1075                    }
1076                }
1077                CoreEffect::RegisterClient { session, operation } => {
1078                    let Some(start) = self.client_starts.remove(&session) else {
1079                        continue;
1080                    };
1081                    let client = self.sessions.get(&session).map(|io| io.client.clone());
1082                    let result = if let Some(client) = &client {
1083                        client
1084                            .register(operation.clone(), start.sender)
1085                            .map(|()| operation)
1086                            .map_err(|error| unb_core::CoreError::UnknownStream(error.to_string()))
1087                    } else {
1088                        Err(unb_core::CoreError::UnknownSession(session.to_string()))
1089                    };
1090                    if let Err(Ok(operation)) = start.reply.send(result) {
1091                        if let Some(client) = client {
1092                            client.abandon(&operation);
1093                        }
1094                        let _ = self.core.handle(
1095                            clock_now(),
1096                            CoreInput::CancelClientOperation { session, operation },
1097                        );
1098                    }
1099                }
1100                CoreEffect::Deliver {
1101                    session,
1102                    mut envelope,
1103                } => {
1104                    if let Some(token) = envelope.body_token.take() {
1105                        match self.handle.claim_body(&session, &token) {
1106                            Some(WireBody::Bytes(payload)) => {
1107                                envelope.payload = payload;
1108                            }
1109                            Some(body @ WireBody::Stream(_)) => {
1110                                let ceiling = self
1111                                    .sessions
1112                                    .get(&session)
1113                                    .map_or(unb_transport::DEFAULT_MAX_FRAME_SIZE, |io| {
1114                                        io.ws_collect_ceiling
1115                                    });
1116                                match body.collect_to(ceiling).await {
1117                                    Ok(payload) => envelope.payload = payload,
1118                                    Err(error) => {
1119                                        envelope.kind = unb_core::Kind::Error;
1120                                        envelope.payload =
1121                                            Envelope::encode_payload(&serde_json::json!({
1122                                                "code": unb_core::ErrorCode::PayloadTooLarge,
1123                                                "message": error.to_string(),
1124                                            }));
1125                                    }
1126                                }
1127                            }
1128                            None => {}
1129                        }
1130                    }
1131                    self.handler(&session, HandlerInput::Deliver(envelope))
1132                        .await
1133                }
1134                CoreEffect::ReleaseBody { session, body } => {
1135                    let _ = self.handle.claim_body(&session, body.as_str());
1136                }
1137                CoreEffect::StreamClosed { session, operation } => {
1138                    self.handler(&session, HandlerInput::StreamClosed(operation))
1139                        .await
1140                }
1141                CoreEffect::CloseTransport {
1142                    session,
1143                    code,
1144                    message,
1145                } => {
1146                    let _ = (code, message);
1147                    if let Some(io) = self.sessions.get(&session) {
1148                        io.cancellation.cancel();
1149                    }
1150                }
1151                CoreEffect::ScheduleSessionDeadline { session, deadline } => {
1152                    if let Some(io) = self.sessions.get(&session) {
1153                        io.deadline.send_replace(Some(deadline));
1154                    }
1155                }
1156                CoreEffect::RouteExportAcked { session } => {
1157                    if let Some(io) = self.sessions.get(&session) {
1158                        io.routes.send_replace(true);
1159                    }
1160                }
1161                CoreEffect::ScheduleClientDeadline {
1162                    session,
1163                    operation,
1164                    deadline,
1165                } => spawn_client_timeout(
1166                    session.clone(),
1167                    operation,
1168                    deadline,
1169                    self.handle.clone(),
1170                    self.sessions
1171                        .get(&session)
1172                        .map(|io| io.cancellation.clone()),
1173                ),
1174                CoreEffect::Send {
1175                    effect,
1176                    session,
1177                    frame,
1178                } => {
1179                    if frame.head.kind == unb_core::Kind::Error {
1180                        let _ = self.core.handle(
1181                            clock_now(),
1182                            CoreInput::SendCompleted {
1183                                effect,
1184                                result: SendResult::Written,
1185                            },
1186                        );
1187                        self.write(&session, frame.into_envelope(), None, false)
1188                            .await;
1189                    } else {
1190                        self.write(&session, frame.into_envelope(), Some(effect), false)
1191                            .await;
1192                    }
1193                }
1194                CoreEffect::SendProtocol {
1195                    effect,
1196                    session,
1197                    envelope,
1198                } => {
1199                    self.write(&session, envelope, Some(effect), false).await;
1200                }
1201                CoreEffect::ContinueDiscovery { stream } => {
1202                    let _ = self
1203                        .core
1204                        .handle(clock_now(), CoreInput::ContinueDiscovery { stream });
1205                }
1206                CoreEffect::SessionRetired { session, reason } => {
1207                    #[cfg(feature = "observability")]
1208                    {
1209                        metrics::counter!("unb_sessions_retired", "reason" => format!("{reason:?}"))
1210                            .increment(1);
1211                        tracing::debug!(session = %session, reason = ?reason, "session retired");
1212                    }
1213                    if let Some(outcome) = self.outcomes.remove(&session) {
1214                        let _ = outcome.send(Some(SessionOutcome::Retired(reason)));
1215                    }
1216                    if let Some(io) = self.sessions.remove(&session) {
1217                        io.cancellation.cancel();
1218                    }
1219                    self.pending_bodies
1220                        .lock()
1221                        .expect("pending body registry")
1222                        .retain(|(owner, _), _| owner != &session);
1223                    self.handle
1224                        .token_bodies
1225                        .lock()
1226                        .expect("body token registry")
1227                        .retain(|(owner, _), _| owner != &session);
1228                    self.execute(CoreEffect::SessionRetired { session, reason });
1229                }
1230                CoreEffect::SessionEstablished { session, peer } => {
1231                    let effect = CoreEffect::SessionEstablished {
1232                        session: session.clone(),
1233                        peer,
1234                    };
1235                    if let Some(input) = self.executor.execute(effect, self.handle.clone()).await {
1236                        let _ = self.core.handle(clock_now(), input);
1237                    }
1238                    if let Some(outcome) = self.outcomes.get(&session) {
1239                        let _ = outcome.send(Some(SessionOutcome::Established));
1240                    }
1241                }
1242                CoreEffect::ForwardRelay {
1243                    effect,
1244                    source,
1245                    target,
1246                    mut frame,
1247                    ..
1248                } => {
1249                    if let Some(body_id) = frame.body.take() {
1250                        let Some(body) = self.handle.claim_body(&source.session, body_id.as_str())
1251                        else {
1252                            let _ = self.core.handle(
1253                                clock_now(),
1254                                CoreInput::RelayForwardCompleted {
1255                                    effect,
1256                                    result: SendResult::Refused {
1257                                        code: unb_core::ErrorCode::Protocol,
1258                                        message: "relay body capability is unavailable".into(),
1259                                    },
1260                                },
1261                            );
1262                            continue;
1263                        };
1264                        match self.handle.register_body(&target.session, body) {
1265                            Ok(target_body) => frame.body = Some(target_body),
1266                            Err(error) => {
1267                                let _ = self.core.handle(
1268                                    clock_now(),
1269                                    CoreInput::RelayForwardCompleted {
1270                                        effect,
1271                                        result: SendResult::Refused {
1272                                            code: unb_core::ErrorCode::Busy,
1273                                            message: error.to_string(),
1274                                        },
1275                                    },
1276                                );
1277                                continue;
1278                            }
1279                        }
1280                    }
1281                    self.write(&target.session, frame.into_envelope(), Some(effect), true)
1282                        .await;
1283                }
1284                effect @ (CoreEffect::RouteSnapshotApplied { .. }
1285                | CoreEffect::RouteDeltaApplied { .. }
1286                | CoreEffect::RouteSessionWithdrawn { .. }) => {
1287                    if let Some(input) = self.executor.execute(effect, self.handle.clone()).await {
1288                        let _ = self.core.handle(clock_now(), input);
1289                    }
1290                }
1291                effect => self.execute(effect),
1292            }
1293        }
1294    }
1295
1296    fn execute(&self, effect: CoreEffect) {
1297        let executor = self.executor.clone();
1298        let handle = self.handle.clone();
1299        n0_future::task::spawn(async move {
1300            if let Some(input) = executor.execute(effect, handle.clone()).await {
1301                let _ = handle.submit(input).await;
1302            }
1303        });
1304    }
1305
1306    async fn write(
1307        &mut self,
1308        session: &SessionId,
1309        mut envelope: Envelope,
1310        completion: Option<EffectId>,
1311        relay: bool,
1312    ) {
1313        if is_operation_envelope(&envelope) {
1314            let mode = self.sessions.get(session).and_then(|io| io.transport_mode);
1315            match mode {
1316                None if self
1317                    .sessions
1318                    .get(session)
1319                    .is_some_and(|io| io.bodies.is_some()) =>
1320                {
1321                    let queued = self.sessions.get_mut(session).is_some_and(|io| {
1322                        if io.pending_mode_writes.len() >= PENDING_MODE_WRITES_MAX {
1323                            false
1324                        } else {
1325                            io.pending_mode_writes.push_back(PendingModeWrite {
1326                                envelope,
1327                                completion,
1328                                relay,
1329                            });
1330                            true
1331                        }
1332                    });
1333                    if !queued {
1334                        if let Some(effect) = completion {
1335                            self.complete_send(effect, relay, SendResult::ReservationTimedOut);
1336                        }
1337                    }
1338                    return;
1339                }
1340                Some(unb_transport::TransportMode::OperationStreamsV1) => {
1341                    self.write_operation(session, envelope, completion, relay)
1342                        .await;
1343                    return;
1344                }
1345                _ => {}
1346            }
1347        }
1348        if envelope.kind == unb_core::Kind::Cancel {
1349            if let Some(corr) = &envelope.corr {
1350                self.handle.cancel_body_pump(session, corr);
1351            }
1352        }
1353        let body = match (&envelope.body_token, &envelope.corr) {
1354            (Some(token), _) => self.handle.claim_body(session, token),
1355            (None, Some(corr))
1356                if envelope.kind.is_application_request()
1357                    || envelope.kind == unb_core::Kind::Response =>
1358            {
1359                let pending = self
1360                    .pending_bodies
1361                    .lock()
1362                    .expect("pending body registry")
1363                    .remove(&(session.clone(), corr.clone()));
1364                if let Some(stream) = pending {
1365                    envelope.body_token = Some(mint_body_token());
1366                    Some(WireBody::Stream(stream))
1367                } else {
1368                    None
1369                }
1370            }
1371            _ => None,
1372        };
1373        let Some(io) = self.sessions.get(session) else {
1374            if let Some(effect) = completion {
1375                self.complete_send(effect, relay, SendResult::Closed);
1376            }
1377            return;
1378        };
1379        if let Some(WireBody::Bytes(payload)) = body {
1380            envelope.body_token = None;
1381            envelope.payload = payload;
1382        } else if let Some(WireBody::Stream(stream)) = body {
1383            if let Some(peer) = io.bodies.clone().filter(|bodies| bodies.can_open()) {
1384                let token = envelope.body_token.clone().expect("tokenized body");
1385                let pump_cancellation = io.cancellation.child_token();
1386                if let Some(corr) = &envelope.corr {
1387                    self.handle.register_body_pump(
1388                        session,
1389                        corr,
1390                        &token,
1391                        pump_cancellation.clone(),
1392                    );
1393                }
1394                spawn_body_pump(
1395                    peer,
1396                    token,
1397                    stream,
1398                    io.byte_budget.clone(),
1399                    pump_cancellation,
1400                    self.handle.clone(),
1401                    envelope.corr.clone().map(|corr| (session.clone(), corr)),
1402                );
1403            } else {
1404                envelope.body_token = None;
1405                spawn_body_collect(
1406                    self.handle.clone(),
1407                    session.clone(),
1408                    envelope,
1409                    stream,
1410                    io.ws_collect_ceiling,
1411                    completion,
1412                    relay,
1413                    io.cancellation.clone(),
1414                );
1415                return;
1416            }
1417        }
1418        let envelope = match io
1419            .byte_budget
1420            .clone()
1421            .try_acquire_many_owned(WriteRequest::weight(&envelope))
1422        {
1423            Ok(budget) => {
1424                let request = WriteRequest {
1425                    envelope,
1426                    completion,
1427                    relay,
1428                    _budget: budget,
1429                };
1430                match io.outbound.try_send(request) {
1431                    Ok(()) => {
1432                        if let Some(effect) = completion {
1433                            self.complete_send(effect, relay, SendResult::Reserved);
1434                        }
1435                        return;
1436                    }
1437                    Err(mpsc::error::TrySendError::Closed(_)) => {
1438                        if let Some(effect) = completion {
1439                            self.complete_send(effect, relay, SendResult::Closed);
1440                        }
1441                        return;
1442                    }
1443                    Err(mpsc::error::TrySendError::Full(request)) => {
1444                        #[cfg(feature = "observability")]
1445                        {
1446                            metrics::counter!("unb_write_queue_full").increment(1);
1447                            tracing::warn!(session = %session, "outbound write queue is full");
1448                        }
1449                        request.envelope
1450                    }
1451                }
1452            }
1453            Err(_) => {
1454                #[cfg(feature = "observability")]
1455                {
1456                    metrics::counter!("unb_write_budget_exhausted").increment(1);
1457                    tracing::warn!(session = %session, "outbound byte budget is exhausted");
1458                }
1459                envelope
1460            }
1461        };
1462        if completion.is_none()
1463            || io
1464                .reservation_pending
1465                .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
1466                .is_ok()
1467        {
1468            let timeout = if matches!(
1469                envelope.kind,
1470                unb_core::Kind::Response | unb_core::Kind::Error | unb_core::Kind::Cancel
1471            ) {
1472                TERMINAL_SEND_TIMEOUT
1473            } else {
1474                RELAY_BACKPRESSURE_TIMEOUT
1475            };
1476            reserve_write(
1477                io.outbound.clone(),
1478                io.byte_budget.clone(),
1479                envelope,
1480                completion,
1481                relay,
1482                self.handle.clone(),
1483                io.cancellation.clone(),
1484                completion.map(|_| io.reservation_pending.clone()),
1485                timeout,
1486            );
1487        } else if let Some(effect) = completion {
1488            self.complete_send(effect, relay, SendResult::ReservationTimedOut);
1489        }
1490    }
1491
1492    fn start_operation_acceptor(&self, session: &SessionId) {
1493        let Some(io) = self.sessions.get(session) else {
1494            return;
1495        };
1496        let Some(bodies) = io.bodies.clone() else {
1497            return;
1498        };
1499        spawn_operation_acceptor(
1500            session.clone(),
1501            bodies,
1502            io.operation_lanes.clone(),
1503            io.operation_slots.clone(),
1504            io.byte_budget.clone(),
1505            self.handle.clone(),
1506            io.cancellation.clone(),
1507        );
1508    }
1509
1510    async fn write_operation(
1511        &mut self,
1512        session: &SessionId,
1513        mut envelope: Envelope,
1514        completion: Option<EffectId>,
1515        relay: bool,
1516    ) {
1517        let Some(corr) = envelope.corr.clone() else {
1518            if let Some(effect) = completion {
1519                self.complete_send(
1520                    effect,
1521                    relay,
1522                    SendResult::WriteFailed("missing unb-corr".into()),
1523                );
1524            }
1525            return;
1526        };
1527        let body = match &envelope.body_token {
1528            Some(token) => self.handle.claim_body(session, token),
1529            None if envelope.kind.is_application_request()
1530                || envelope.kind == unb_core::Kind::Response =>
1531            {
1532                self.pending_bodies
1533                    .lock()
1534                    .expect("pending body registry")
1535                    .remove(&(session.clone(), corr.clone()))
1536                    .map(WireBody::Stream)
1537            }
1538            None => None,
1539        };
1540        let body = match body {
1541            Some(WireBody::Bytes(payload)) => {
1542                envelope.payload = payload;
1543                None
1544            }
1545            Some(WireBody::Stream(stream)) => Some(stream),
1546            None => None,
1547        };
1548        envelope.body_token = None;
1549        let Some(io) = self.sessions.get(session) else {
1550            if let Some(effect) = completion {
1551                self.complete_send(effect, relay, SendResult::Closed);
1552            }
1553            return;
1554        };
1555        if envelope.kind == unb_core::Kind::Cancel {
1556            let lane = io
1557                .operation_lanes
1558                .lock()
1559                .expect("operation lane registry")
1560                .remove(&corr);
1561            if let Some(control) = lane.and_then(|lane| lane.control) {
1562                let _ = control.reset(unb_transport::operation::RESET_CANCELLED);
1563                let _ = control.stop(unb_transport::operation::RESET_CANCELLED);
1564            }
1565            if let Some(effect) = completion {
1566                self.complete_send(effect, relay, SendResult::Written);
1567            }
1568            return;
1569        }
1570        if envelope.kind.is_application_request() {
1571            spawn_outbound_operation(
1572                session.clone(),
1573                corr,
1574                envelope,
1575                body,
1576                completion,
1577                relay,
1578                io.bodies
1579                    .clone()
1580                    .expect("operation mode has stream facility"),
1581                io.operation_lanes.clone(),
1582                io.operation_slots.clone(),
1583                io.byte_budget.clone(),
1584                self.handle.clone(),
1585                io.cancellation.clone(),
1586            );
1587            return;
1588        }
1589        let lane = io
1590            .operation_lanes
1591            .lock()
1592            .expect("operation lane registry")
1593            .get(&corr)
1594            .cloned();
1595        let Some(sender) = lane.and_then(|lane| lane.outbound) else {
1596            if let Some(effect) = completion {
1597                self.complete_send(effect, relay, SendResult::Written);
1598            }
1599            return;
1600        };
1601        let budget = match io
1602            .byte_budget
1603            .clone()
1604            .try_acquire_many_owned(WriteRequest::weight(&envelope))
1605        {
1606            Ok(budget) => budget,
1607            Err(_) => {
1608                if let Some(effect) = completion {
1609                    self.complete_send(effect, relay, SendResult::ReservationTimedOut);
1610                }
1611                return;
1612            }
1613        };
1614        let request = OperationWriteRequest {
1615            envelope,
1616            completion,
1617            relay,
1618            body,
1619            _budget: budget,
1620        };
1621        match sender.try_send(request) {
1622            Ok(()) => {
1623                if let Some(effect) = completion {
1624                    self.complete_send(effect, relay, SendResult::Reserved);
1625                }
1626            }
1627            Err(_) => {
1628                if let Some(effect) = completion {
1629                    self.complete_send(effect, relay, SendResult::ReservationTimedOut);
1630                }
1631            }
1632        }
1633    }
1634
1635    fn complete_send(&mut self, effect: EffectId, relay: bool, result: SendResult) {
1636        let _ = self
1637            .core
1638            .handle(clock_now(), send_completion(effect, relay, result));
1639    }
1640
1641    async fn handler(&self, session: &SessionId, event: HandlerInput) {
1642        if let Some(io) = self.sessions.get(session) {
1643            let _ = io.handler.send(event).await;
1644        }
1645    }
1646}
1647
1648enum HandlerInput {
1649    Deliver(Envelope),
1650    StreamClosed(ClientOperationId),
1651}
1652
1653fn handler_channel<H: SessionHandler>(mut handler: H) -> mpsc::Sender<HandlerInput> {
1654    let (events, mut receiver) = mpsc::channel(SESSION_QUEUE);
1655    n0_future::task::spawn(async move {
1656        while let Some(event) = receiver.recv().await {
1657            match event {
1658                HandlerInput::Deliver(envelope) => handler.deliver(envelope).await,
1659                HandlerInput::StreamClosed(operation) => handler.stream_closed(operation).await,
1660            }
1661        }
1662    });
1663    events
1664}
1665
1666fn spawn_directives(
1667    session: SessionId,
1668    mut directives: mpsc::Receiver<Directive>,
1669    handle: ProtocolCoreHandle,
1670    cancellation: CancellationToken,
1671) {
1672    n0_future::task::spawn(async move {
1673        loop {
1674            let directive = tokio::select! {
1675                biased;
1676                () = cancellation.cancelled() => return,
1677                directive = directives.recv() => directive,
1678            };
1679            let Some(directive) = directive else { return };
1680            if handle
1681                .inputs
1682                .send(ActorInput::Directive(session.clone(), directive))
1683                .await
1684                .is_err()
1685            {
1686                return;
1687            }
1688        }
1689    });
1690}
1691
1692fn is_operation_envelope(envelope: &Envelope) -> bool {
1693    envelope.corr.is_some()
1694        && (envelope.kind.is_application_request()
1695            || envelope.kind.is_application_response()
1696            || envelope.kind == unb_core::Kind::Cancel)
1697}
1698
1699fn spawn_operation_acceptor(
1700    session: SessionId,
1701    bodies: Arc<unb_transport::ConnectionStreams>,
1702    lanes: OperationLanes,
1703    operation_slots: Arc<Semaphore>,
1704    byte_budget: Arc<Semaphore>,
1705    handle: ProtocolCoreHandle,
1706    cancellation: CancellationToken,
1707) {
1708    n0_future::task::spawn(async move {
1709        loop {
1710            let operation = tokio::select! {
1711                biased;
1712                () = cancellation.cancelled() => return,
1713                operation = bodies.accept_operation() => operation,
1714            };
1715            let Ok(operation) = operation else {
1716                return;
1717            };
1718            let Ok(operation_slot) = operation_slots.clone().try_acquire_owned() else {
1719                if let Some(control) = operation.control {
1720                    let _ = control.reset(unb_transport::operation::RESET_BUSY);
1721                    let _ = control.stop(unb_transport::operation::RESET_BUSY);
1722                }
1723                continue;
1724            };
1725            let corr = operation.corr.clone();
1726            let (outbound, receiver) = mpsc::channel(OPERATION_WRITE_QUEUE);
1727            let lane = OperationLane {
1728                outbound: Some(outbound),
1729                control: operation.control.clone(),
1730            };
1731            let admitted = {
1732                let mut lanes = lanes.lock().expect("operation lane registry");
1733                if lanes.contains_key(&corr) {
1734                    false
1735                } else {
1736                    lanes.insert(corr.clone(), lane);
1737                    true
1738                }
1739            };
1740            if !admitted {
1741                if let Some(control) = operation.control {
1742                    let _ = control.reset(unb_transport::operation::RESET_PROTOCOL);
1743                }
1744                continue;
1745            }
1746            spawn_accepted_operation(
1747                session.clone(),
1748                operation,
1749                receiver,
1750                lanes.clone(),
1751                byte_budget.clone(),
1752                handle.clone(),
1753                cancellation.child_token(),
1754                operation_slot,
1755            );
1756        }
1757    });
1758}
1759
1760#[allow(clippy::too_many_arguments)]
1761fn spawn_outbound_operation(
1762    session: SessionId,
1763    corr: String,
1764    envelope: Envelope,
1765    body: Option<BodyStream>,
1766    completion: Option<EffectId>,
1767    relay: bool,
1768    bodies: Arc<unb_transport::ConnectionStreams>,
1769    lanes: OperationLanes,
1770    operation_slots: Arc<Semaphore>,
1771    byte_budget: Arc<Semaphore>,
1772    handle: ProtocolCoreHandle,
1773    cancellation: CancellationToken,
1774) {
1775    n0_future::task::spawn(async move {
1776        let open_byte_budget = byte_budget.clone();
1777        let open = async {
1778            let operation_slot = operation_slots
1779                .acquire_owned()
1780                .await
1781                .map_err(|_| unb_transport::TransportError::Closed)?;
1782            let budget = open_byte_budget
1783                .acquire_many_owned(WriteRequest::weight(&envelope))
1784                .await
1785                .map_err(|_| unb_transport::TransportError::Closed)?;
1786            let operation = bodies
1787                .open_operation(&corr)
1788                .await?
1789                .ok_or(unb_transport::TransportError::Closed)?;
1790            Ok::<_, unb_transport::TransportError>((operation, budget, operation_slot))
1791        };
1792        let opened = tokio::select! {
1793            biased;
1794            () = cancellation.cancelled() => return,
1795            opened = n0_future::time::timeout(RELAY_BACKPRESSURE_TIMEOUT, open) => opened,
1796        };
1797        let Ok(Ok((operation, _budget, _operation_slot))) = opened else {
1798            if let Some(effect) = completion {
1799                let _ = handle
1800                    .submit(send_completion(
1801                        effect,
1802                        relay,
1803                        SendResult::ReservationTimedOut,
1804                    ))
1805                    .await;
1806            }
1807            return;
1808        };
1809        lanes.lock().expect("operation lane registry").insert(
1810            corr.clone(),
1811            OperationLane {
1812                outbound: None,
1813                control: operation.control.clone(),
1814            },
1815        );
1816        if let Some(effect) = completion {
1817            if handle
1818                .submit(send_completion(effect, relay, SendResult::Reserved))
1819                .await
1820                .is_err()
1821            {
1822                return;
1823            }
1824        }
1825        let mut writer = unb_transport::operation::OperationRecordWriter::new(operation.write);
1826        let sent = write_operation_envelope(
1827            &mut writer,
1828            envelope,
1829            body,
1830            byte_budget.clone(),
1831            cancellation.clone(),
1832        )
1833        .await;
1834        let sent = match sent {
1835            Ok(()) => writer.finish().await,
1836            Err(error) => Err(error),
1837        };
1838        if let Some(effect) = completion {
1839            let _ = handle
1840                .submit(send_completion(effect, relay, SendResult::Written))
1841                .await;
1842        }
1843        if let Err(error) = sent {
1844            if !operation_peer_stopped_cancelled(&error) {
1845                if let Some(control) = &operation.control {
1846                    let _ = control.reset(error.reset_code());
1847                }
1848                let _ = submit_operation_end(
1849                    &handle,
1850                    &session,
1851                    &corr,
1852                    OperationDirection::Opening,
1853                    operation_outcome(&error),
1854                )
1855                .await;
1856                lanes.lock().expect("operation lane registry").remove(&corr);
1857                return;
1858            }
1859        }
1860        let result = read_operation_direction(
1861            &session,
1862            &corr,
1863            operation.read,
1864            OperationDirection::Return,
1865            &handle,
1866            &cancellation,
1867        )
1868        .await;
1869        if let Err(error) = result {
1870            if let Some(control) = &operation.control {
1871                let _ = control.reset(error.reset_code());
1872                let _ = control.stop(error.reset_code());
1873            }
1874            let _ = submit_operation_end(
1875                &handle,
1876                &session,
1877                &corr,
1878                OperationDirection::Return,
1879                operation_outcome(&error),
1880            )
1881            .await;
1882        }
1883        lanes.lock().expect("operation lane registry").remove(&corr);
1884    });
1885}
1886
1887#[allow(clippy::too_many_arguments)]
1888fn spawn_accepted_operation(
1889    session: SessionId,
1890    operation: unb_transport::operation::OperationIo,
1891    receiver: mpsc::Receiver<OperationWriteRequest>,
1892    lanes: OperationLanes,
1893    byte_budget: Arc<Semaphore>,
1894    handle: ProtocolCoreHandle,
1895    cancellation: CancellationToken,
1896    operation_slot: OwnedSemaphorePermit,
1897) {
1898    let corr = operation.corr.clone();
1899    let control = operation.control.clone();
1900    let read_corr = corr.clone();
1901    let read_lanes = lanes.clone();
1902    let read_handle = handle.clone();
1903    let read_cancellation = cancellation.clone();
1904    let writer_session = session.clone();
1905    n0_future::task::spawn(async move {
1906        let result = read_operation_direction(
1907            &session,
1908            &read_corr,
1909            operation.read,
1910            OperationDirection::Opening,
1911            &read_handle,
1912            &read_cancellation,
1913        )
1914        .await;
1915        if let Err(error) = result {
1916            if operation_body_consumer_closed(&error) {
1917                if let Some(control) = control {
1918                    let _ = control.stop(unb_transport::operation::RESET_CANCELLED);
1919                }
1920                return;
1921            }
1922            if let Some(control) = control {
1923                let _ = control.reset(error.reset_code());
1924                let _ = control.stop(error.reset_code());
1925            }
1926            let _ = submit_operation_end(
1927                &read_handle,
1928                &session,
1929                &read_corr,
1930                OperationDirection::Opening,
1931                operation_outcome(&error),
1932            )
1933            .await;
1934            read_lanes
1935                .lock()
1936                .expect("operation lane registry")
1937                .remove(&read_corr);
1938        }
1939    });
1940    spawn_operation_writer(
1941        writer_session,
1942        corr,
1943        operation.write,
1944        operation.control,
1945        receiver,
1946        lanes,
1947        byte_budget,
1948        handle,
1949        cancellation,
1950        operation_slot,
1951    );
1952}
1953
1954#[allow(clippy::too_many_arguments)]
1955fn spawn_operation_writer(
1956    session: SessionId,
1957    corr: String,
1958    write: Box<dyn tokio::io::AsyncWrite + Send + Unpin>,
1959    control: Option<unb_transport::operation::OperationControl>,
1960    mut receiver: mpsc::Receiver<OperationWriteRequest>,
1961    lanes: OperationLanes,
1962    byte_budget: Arc<Semaphore>,
1963    handle: ProtocolCoreHandle,
1964    cancellation: CancellationToken,
1965    _operation_slot: OwnedSemaphorePermit,
1966) {
1967    n0_future::task::spawn(async move {
1968        let mut writer = unb_transport::operation::OperationRecordWriter::new(write);
1969        loop {
1970            let request = tokio::select! {
1971                biased;
1972                () = cancellation.cancelled() => break,
1973                request = receiver.recv() => request,
1974            };
1975            let Some(request) = request else {
1976                break;
1977            };
1978            let terminal = matches!(
1979                request.envelope.kind,
1980                unb_core::Kind::Response | unb_core::Kind::Error
1981            );
1982            let result = write_operation_envelope(
1983                &mut writer,
1984                request.envelope,
1985                request.body,
1986                byte_budget.clone(),
1987                cancellation.clone(),
1988            )
1989            .await;
1990            let result = if result.is_ok() && terminal {
1991                writer.finish().await
1992            } else {
1993                result
1994            };
1995            if let Some(effect) = request.completion {
1996                let _ = handle
1997                    .submit(send_completion(effect, request.relay, SendResult::Written))
1998                    .await;
1999            }
2000            if result.is_err() || terminal {
2001                if let Err(error) = result {
2002                    if let Some(control) = &control {
2003                        let _ = control.reset(error.reset_code());
2004                    }
2005                    let _ = submit_operation_end(
2006                        &handle,
2007                        &session,
2008                        &corr,
2009                        OperationDirection::Return,
2010                        operation_outcome(&error),
2011                    )
2012                    .await;
2013                }
2014                break;
2015            }
2016        }
2017        lanes.lock().expect("operation lane registry").remove(&corr);
2018    });
2019}
2020
2021async fn write_operation_envelope<W>(
2022    writer: &mut unb_transport::operation::OperationRecordWriter<W>,
2023    mut envelope: Envelope,
2024    body: Option<BodyStream>,
2025    byte_budget: Arc<Semaphore>,
2026    cancellation: CancellationToken,
2027) -> Result<(), unb_transport::operation::OperationCodecError>
2028where
2029    W: tokio::io::AsyncWrite + Unpin,
2030{
2031    let Some(body) = body else {
2032        return writer
2033            .write_record(
2034                unb_transport::operation::OperationRecordKind::CompleteMessage,
2035                &envelope.encode(),
2036            )
2037            .await;
2038    };
2039    envelope.body_token = None;
2040    envelope.payload = Bytes::new();
2041    writer
2042        .write_record(
2043            unb_transport::operation::OperationRecordKind::Head,
2044            &envelope.encode(),
2045        )
2046        .await?;
2047    let mut body = MeteredBody::new(body, byte_budget, cancellation.clone());
2048    loop {
2049        let chunk = body.next_chunk().await.map_err(|error| {
2050            unb_transport::operation::OperationCodecError::Io(error.to_string())
2051        })?;
2052        let Some(chunk) = chunk else {
2053            if cancellation.is_cancelled() {
2054                return Err(unb_transport::operation::OperationCodecError::Io(
2055                    "operation body cancelled".into(),
2056                ));
2057            }
2058            break;
2059        };
2060        for piece in chunk.bytes.chunks(OPERATION_BODY_CHUNK_BYTES) {
2061            writer
2062                .write_record(
2063                    unb_transport::operation::OperationRecordKind::BodyChunk,
2064                    piece,
2065                )
2066                .await?;
2067        }
2068    }
2069    writer
2070        .write_record(unb_transport::operation::OperationRecordKind::BodyEnd, &[])
2071        .await
2072}
2073
2074#[derive(Clone, Copy)]
2075enum OperationDirection {
2076    Opening,
2077    Return,
2078}
2079
2080async fn read_operation_direction(
2081    session: &SessionId,
2082    corr: &str,
2083    read: Box<dyn tokio::io::AsyncRead + Send + Unpin>,
2084    direction: OperationDirection,
2085    handle: &ProtocolCoreHandle,
2086    cancellation: &CancellationToken,
2087) -> Result<(), unb_transport::operation::OperationCodecError> {
2088    let mut reader = unb_transport::operation::OperationRecordReader::new(read);
2089    let mut records = 0usize;
2090    let mut terminal = false;
2091    let mut body_sender: Option<mpsc::Sender<Result<Bytes, unb_core::CoreError>>> = None;
2092    loop {
2093        let kind = tokio::select! {
2094            biased;
2095            () = cancellation.cancelled() => {
2096                if let Some(sender) = body_sender.take() {
2097                    let _ = sender.send(Err(unb_core::CoreError::Malformed(
2098                        "operation body cancelled".into(),
2099                    ))).await;
2100                }
2101                return Ok(())
2102            },
2103            record = reader.read_record() => record,
2104        };
2105        let (kind, payload) = match kind {
2106            Ok(record) => record,
2107            Err(error) => {
2108                if let Some(sender) = body_sender.take() {
2109                    let _ = sender
2110                        .send(Err(unb_core::CoreError::Malformed(error.to_string())))
2111                        .await;
2112                }
2113                return Err(error);
2114            }
2115        };
2116        match kind {
2117            unb_transport::operation::OperationRecordKind::CompleteMessage => {
2118                if body_sender.is_some() {
2119                    return Err(unb_transport::operation::OperationCodecError::Protocol(
2120                        "complete message arrived before body end".into(),
2121                    ));
2122                }
2123                let envelope = decode_operation_message(payload, corr, direction, records)?;
2124                terminal |= matches!(
2125                    envelope.kind,
2126                    unb_core::Kind::Response | unb_core::Kind::Error
2127                );
2128                records += 1;
2129                let input = application_core_input(handle, session, envelope).map_err(|error| {
2130                    unb_transport::operation::OperationCodecError::Protocol(error.to_string())
2131                })?;
2132                handle.submit(input).await.map_err(|error| {
2133                    unb_transport::operation::OperationCodecError::Io(error.to_string())
2134                })?;
2135            }
2136            unb_transport::operation::OperationRecordKind::Head => {
2137                if body_sender.is_some() {
2138                    return Err(unb_transport::operation::OperationCodecError::Protocol(
2139                        "operation head arrived before body end".into(),
2140                    ));
2141                }
2142                let mut envelope = decode_operation_message(payload, corr, direction, records)?;
2143                let (token, sender) = register_operation_body(handle, session)?;
2144                envelope.body_token = Some(token);
2145                terminal |= matches!(
2146                    envelope.kind,
2147                    unb_core::Kind::Response | unb_core::Kind::Error
2148                );
2149                records += 1;
2150                body_sender = Some(sender);
2151                let input = application_core_input(handle, session, envelope).map_err(|error| {
2152                    unb_transport::operation::OperationCodecError::Protocol(error.to_string())
2153                })?;
2154                handle.submit(input).await.map_err(|error| {
2155                    unb_transport::operation::OperationCodecError::Io(error.to_string())
2156                })?;
2157            }
2158            unb_transport::operation::OperationRecordKind::BodyChunk => {
2159                let Some(sender) = &body_sender else {
2160                    return Err(unb_transport::operation::OperationCodecError::Protocol(
2161                        "body chunk arrived without a head".into(),
2162                    ));
2163                };
2164                sender.send(Ok(payload)).await.map_err(|_| {
2165                    unb_transport::operation::OperationCodecError::Io(
2166                        "operation body consumer closed".into(),
2167                    )
2168                })?;
2169            }
2170            unb_transport::operation::OperationRecordKind::BodyEnd => {
2171                let Some(sender) = body_sender.take() else {
2172                    return Err(unb_transport::operation::OperationCodecError::Protocol(
2173                        "body end arrived without a head".into(),
2174                    ));
2175                };
2176                drop(sender);
2177            }
2178            unb_transport::operation::OperationRecordKind::DirectionEnd => {
2179                if body_sender.is_some() {
2180                    return Err(unb_transport::operation::OperationCodecError::Truncated(
2181                        "direction ended before body end".into(),
2182                    ));
2183                }
2184                if records == 0 || matches!(direction, OperationDirection::Return) && !terminal {
2185                    return Err(unb_transport::operation::OperationCodecError::Protocol(
2186                        "operation direction ended before its required message".into(),
2187                    ));
2188                }
2189                reader.expect_fin().await?;
2190                submit_operation_end(
2191                    handle,
2192                    session,
2193                    corr,
2194                    direction,
2195                    unb_core::OperationStreamOutcome::Clean,
2196                )
2197                .await
2198                .map_err(|error| {
2199                    unb_transport::operation::OperationCodecError::Io(error.to_string())
2200                })?;
2201                return Ok(());
2202            }
2203        }
2204    }
2205}
2206
2207fn register_operation_body(
2208    handle: &ProtocolCoreHandle,
2209    session: &SessionId,
2210) -> Result<
2211    (String, mpsc::Sender<Result<Bytes, unb_core::CoreError>>),
2212    unb_transport::operation::OperationCodecError,
2213> {
2214    let token = mint_body_token();
2215    let (sender, receiver) = mpsc::channel(8);
2216    let stream: BodyStream = Box::pin(futures_util::stream::unfold(
2217        receiver,
2218        |mut receiver| async move { receiver.recv().await.map(|chunk| (chunk, receiver)) },
2219    ));
2220    let mut registry = handle.token_bodies.lock().expect("body token registry");
2221    let owned = registry
2222        .keys()
2223        .filter(|(owner, _)| owner == session)
2224        .count();
2225    if owned >= SESSION_BODY_TOKENS_MAX {
2226        return Err(unb_transport::operation::OperationCodecError::Protocol(
2227            "session operation body limit reached".into(),
2228        ));
2229    }
2230    registry.insert((session.clone(), token.clone()), WireBody::Stream(stream));
2231    Ok((token, sender))
2232}
2233
2234fn operation_outcome(
2235    error: &unb_transport::operation::OperationCodecError,
2236) -> unb_core::OperationStreamOutcome {
2237    match error {
2238        unb_transport::operation::OperationCodecError::Protocol(message) => {
2239            unb_core::OperationStreamOutcome::Protocol(message.clone())
2240        }
2241        unb_transport::operation::OperationCodecError::Truncated(_) => {
2242            unb_core::OperationStreamOutcome::Truncated
2243        }
2244        unb_transport::operation::OperationCodecError::Oversized { size, max } => {
2245            unb_core::OperationStreamOutcome::Protocol(format!(
2246                "operation record of {size} bytes exceeds max {max}"
2247            ))
2248        }
2249        unb_transport::operation::OperationCodecError::Io(message) => {
2250            unb_core::OperationStreamOutcome::Transport(message.clone())
2251        }
2252    }
2253}
2254
2255fn operation_body_consumer_closed(error: &unb_transport::operation::OperationCodecError) -> bool {
2256    matches!(
2257        error,
2258        unb_transport::operation::OperationCodecError::Io(message)
2259            if message == "operation body consumer closed"
2260    )
2261}
2262
2263fn operation_peer_stopped_cancelled(error: &unb_transport::operation::OperationCodecError) -> bool {
2264    matches!(
2265        error,
2266        unb_transport::operation::OperationCodecError::Io(message)
2267            if message.contains(&unb_transport::operation::RESET_CANCELLED.to_string())
2268    )
2269}
2270
2271async fn submit_operation_end(
2272    handle: &ProtocolCoreHandle,
2273    session: &SessionId,
2274    corr: &str,
2275    direction: OperationDirection,
2276    outcome: unb_core::OperationStreamOutcome,
2277) -> Result<(), WsError> {
2278    handle
2279        .submit(CoreInput::OperationStreamEnded {
2280            session: session.clone(),
2281            corr: unb_core::CorrelationId::from(corr.to_owned()),
2282            direction: match direction {
2283                OperationDirection::Opening => unb_core::OperationStreamDirection::Opening,
2284                OperationDirection::Return => unb_core::OperationStreamDirection::Return,
2285            },
2286            outcome,
2287        })
2288        .await
2289}
2290
2291fn decode_operation_message(
2292    payload: Bytes,
2293    corr: &str,
2294    direction: OperationDirection,
2295    records: usize,
2296) -> Result<Envelope, unb_transport::operation::OperationCodecError> {
2297    let envelope = Envelope::decode(payload).map_err(|error| {
2298        unb_transport::operation::OperationCodecError::Protocol(error.to_string())
2299    })?;
2300    if envelope.corr.as_deref() != Some(corr) {
2301        return Err(unb_transport::operation::OperationCodecError::Protocol(
2302            "operation record changed unb-corr".into(),
2303        ));
2304    }
2305    match direction {
2306        OperationDirection::Opening if records != 0 || !envelope.kind.is_application_request() => {
2307            Err(unb_transport::operation::OperationCodecError::Protocol(
2308                "opening direction requires exactly one application opener".into(),
2309            ))
2310        }
2311        OperationDirection::Return if !envelope.kind.is_application_response() => {
2312            Err(unb_transport::operation::OperationCodecError::Protocol(
2313                "return direction requires application responses".into(),
2314            ))
2315        }
2316        _ => Ok(envelope),
2317    }
2318}
2319
2320fn spawn_body_pump(
2321    peer: std::sync::Arc<unb_transport::ConnectionStreams>,
2322    token: String,
2323    stream: BodyStream,
2324    budget: Arc<Semaphore>,
2325    cancellation: CancellationToken,
2326    handle: ProtocolCoreHandle,
2327    registration: Option<(SessionId, String)>,
2328) {
2329    n0_future::task::spawn(async move {
2330        let pump = async {
2331            let Ok(mut writer) = peer.open_body(&token).await else {
2332                return;
2333            };
2334            let mut metered = MeteredBody::new(stream, budget, cancellation.clone());
2335            loop {
2336                match metered.next_chunk().await {
2337                    Ok(Some(chunk)) => {
2338                        if writer.write_chunk(&chunk.bytes).await.is_err() {
2339                            return;
2340                        }
2341                    }
2342                    Ok(None) => {
2343                        if cancellation.is_cancelled() {
2344                            writer.abort().await;
2345                        } else {
2346                            let _ = writer.finish().await;
2347                        }
2348                        return;
2349                    }
2350                    Err(_) => {
2351                        writer.abort().await;
2352                        return;
2353                    }
2354                }
2355            }
2356        };
2357        pump.await;
2358        if let Some((session, corr)) = registration {
2359            handle.release_body_pump(&session, &corr, &token);
2360        }
2361    });
2362}
2363
2364#[allow(clippy::too_many_arguments)]
2365fn spawn_body_collect(
2366    handle: ProtocolCoreHandle,
2367    session: SessionId,
2368    mut envelope: Envelope,
2369    stream: BodyStream,
2370    ceiling: usize,
2371    completion: Option<EffectId>,
2372    relay: bool,
2373    cancellation: CancellationToken,
2374) {
2375    n0_future::task::spawn(async move {
2376        let collected = tokio::select! {
2377            biased;
2378            () = cancellation.cancelled() => {
2379                if let Some(effect) = completion {
2380                    let _ = handle
2381                        .submit(send_completion(effect, relay, SendResult::Cancelled))
2382                        .await;
2383                }
2384                return;
2385            }
2386            collected = WireBody::Stream(stream).collect_to(ceiling) => collected,
2387        };
2388        match collected {
2389            Ok(payload) => {
2390                envelope.payload = payload;
2391                let _ = handle
2392                    .inputs
2393                    .send(ActorInput::Write {
2394                        session,
2395                        envelope,
2396                        completion,
2397                        relay,
2398                    })
2399                    .await;
2400            }
2401            Err(error) => {
2402                let (code, message) = match error {
2403                    unb_core::CoreError::BodyTooLarge(_) => (
2404                        unb_core::ErrorCode::PayloadTooLarge,
2405                        "streaming body exceeds this route's WS collect ceiling; raise the \
2406                         ceiling or reach this node over WebTransport"
2407                            .to_string(),
2408                    ),
2409                    other => (unb_core::ErrorCode::Protocol, other.to_string()),
2410                };
2411                if let Some(effect) = completion {
2412                    let refused = SendResult::Refused { code, message };
2413                    let _ = handle.submit(send_completion(effect, relay, refused)).await;
2414                } else if envelope.kind.is_application_request() {
2415                    if let Some(corr) = envelope.corr.clone() {
2416                        let outcome = if code == unb_core::ErrorCode::PayloadTooLarge {
2417                            unb_core::OperationStreamOutcome::PayloadTooLarge(message)
2418                        } else {
2419                            unb_core::OperationStreamOutcome::Protocol(message)
2420                        };
2421                        let _ = handle
2422                            .submit(CoreInput::OperationStreamEnded {
2423                                session,
2424                                corr: unb_core::CorrelationId::from(corr),
2425                                direction: unb_core::OperationStreamDirection::Opening,
2426                                outcome,
2427                            })
2428                            .await;
2429                    }
2430                } else if envelope.kind == unb_core::Kind::Response {
2431                    envelope.kind = unb_core::Kind::Error;
2432                    envelope.payload = Envelope::encode_payload(&serde_json::json!({
2433                        "code": code,
2434                        "message": message,
2435                    }));
2436                    let _ = handle
2437                        .inputs
2438                        .send(ActorInput::Write {
2439                            session,
2440                            envelope,
2441                            completion: None,
2442                            relay: false,
2443                        })
2444                        .await;
2445                }
2446            }
2447        }
2448    });
2449}
2450
2451fn spawn_reader(
2452    session: SessionId,
2453    mut reader: PipeReader,
2454    handle: ProtocolCoreHandle,
2455    cancellation: CancellationToken,
2456    bodies: crate::transport::SessionStreams,
2457) {
2458    n0_future::task::spawn(async move {
2459        loop {
2460            let received = tokio::select! {
2461                biased;
2462                () = cancellation.cancelled() => {
2463                    let _ = handle.submit(CoreInput::SessionClosed { session }).await;
2464                    return;
2465                },
2466                received = reader.recv() => received,
2467            };
2468            let envelope = match received {
2469                Ok(Some((envelope, _body))) => {
2470                    if let Some(token) = &envelope.body_token {
2471                        if !(envelope.kind.is_application_request()
2472                            || envelope.kind == unb_core::Kind::Response)
2473                        {
2474                            let _ = handle.submit(CoreInput::SessionFailed { session }).await;
2475                            return;
2476                        }
2477                        {
2478                            let Some(peer) = bodies.clone() else {
2479                                let _ = handle.submit(CoreInput::SessionFailed { session }).await;
2480                                return;
2481                            };
2482                            let lazy_token = token.clone();
2483                            let accepted = futures_util::stream::once(async move {
2484                                peer.accept_body(&lazy_token, RELAY_BACKPRESSURE_TIMEOUT)
2485                                    .await
2486                            });
2487                            let stream: BodyStream =
2488                                Box::pin(futures_util::StreamExt::flat_map(accepted, |outcome| {
2489                                    match outcome {
2490                                        Ok(reader) => futures_util::StreamExt::boxed(
2491                                            futures_util::StreamExt::map(reader, |chunk| {
2492                                                chunk.map_err(|error| {
2493                                                    unb_core::CoreError::Malformed(
2494                                                        error.to_string(),
2495                                                    )
2496                                                })
2497                                            }),
2498                                        ),
2499                                        Err(error) => futures_util::StreamExt::boxed(
2500                                            futures_util::stream::once(async move {
2501                                                Err(unb_core::CoreError::Malformed(
2502                                                    error.to_string(),
2503                                                ))
2504                                            }),
2505                                        ),
2506                                    }
2507                                }));
2508                            let rejected = {
2509                                let mut registry =
2510                                    handle.token_bodies.lock().expect("body token registry");
2511                                let key = (session.clone(), token.clone());
2512                                let owned = registry
2513                                    .keys()
2514                                    .filter(|(owner, _)| owner == &session)
2515                                    .count();
2516                                if registry.contains_key(&key) || owned >= SESSION_BODY_TOKENS_MAX {
2517                                    true
2518                                } else {
2519                                    registry.insert(key, WireBody::Stream(stream));
2520                                    false
2521                                }
2522                            };
2523                            if rejected {
2524                                let _ = handle.submit(CoreInput::SessionFailed { session }).await;
2525                                return;
2526                            }
2527                        }
2528                    }
2529                    envelope
2530                }
2531                Ok(None) => {
2532                    let _ = handle.submit(CoreInput::SessionClosed { session }).await;
2533                    return;
2534                }
2535                Err(_) => {
2536                    #[cfg(feature = "observability")]
2537                    {
2538                        metrics::counter!("unb_transport_failures").increment(1);
2539                        tracing::warn!(session = %session, "transport read failed");
2540                    }
2541                    let _ = handle.submit(CoreInput::SessionFailed { session }).await;
2542                    return;
2543                }
2544            };
2545            if envelope.kind == unb_core::Kind::Cancel {
2546                if let Some(corr) = &envelope.corr {
2547                    handle.cancel_body_pump(&session, corr);
2548                }
2549            }
2550            let input = match application_core_input(&handle, &session, envelope) {
2551                Ok(input) => input,
2552                Err(_) => {
2553                    let _ = handle.submit(CoreInput::SessionFailed { session }).await;
2554                    return;
2555                }
2556            };
2557            if handle.submit(input).await.is_err() {
2558                return;
2559            }
2560        }
2561    });
2562}
2563
2564fn spawn_writer(
2565    session: SessionId,
2566    mut writer: PipeWriter,
2567    mut outbound: mpsc::Receiver<WriteRequest>,
2568    handle: ProtocolCoreHandle,
2569    cancellation: CancellationToken,
2570) {
2571    n0_future::task::spawn(async move {
2572        loop {
2573            let request = tokio::select! {
2574                biased;
2575                () = cancellation.cancelled() => {
2576                    let farewell = async {
2577                        while let Ok(request) = outbound.try_recv() {
2578                            if writer.feed(request.envelope).await.is_err() {
2579                                break;
2580                            }
2581                        }
2582                        let _ = writer.flush().await;
2583                        let _ = writer.close().await;
2584                    };
2585                    let _ = n0_future::time::timeout(TERMINAL_SEND_TIMEOUT, farewell).await;
2586                    return;
2587                },
2588                request = outbound.recv() => request,
2589            };
2590            let Some(request) = request else {
2591                let _ = handle
2592                    .submit(CoreInput::SessionClosed {
2593                        session: session.clone(),
2594                    })
2595                    .await;
2596                return;
2597            };
2598            let mut batch = Vec::with_capacity(WRITE_BATCH);
2599            batch.push(request);
2600            while batch.len() < WRITE_BATCH {
2601                match outbound.try_recv() {
2602                    Ok(request) => batch.push(request),
2603                    Err(_) => break,
2604                }
2605            }
2606            let completions = batch
2607                .iter()
2608                .filter_map(|request| request.completion.map(|effect| (effect, request.relay)))
2609                .collect::<Vec<_>>();
2610            let result = async {
2611                for request in batch {
2612                    writer.feed(request.envelope).await?;
2613                }
2614                writer.flush().await
2615            }
2616            .await;
2617            let send_result = result.as_ref().map_or_else(
2618                |error| SendResult::WriteFailed(error.to_string()),
2619                |_| SendResult::Written,
2620            );
2621            let completions = completions
2622                .into_iter()
2623                .map(|(effect, relay)| send_completion(effect, relay, send_result.clone()))
2624                .collect::<Vec<_>>();
2625            if !completions.is_empty()
2626                && handle
2627                    .inputs
2628                    .send(ActorInput::CoreBatch(completions))
2629                    .await
2630                    .is_err()
2631            {
2632                return;
2633            }
2634            if result.is_err() {
2635                let _ = handle
2636                    .submit(CoreInput::SessionClosed {
2637                        session: session.clone(),
2638                    })
2639                    .await;
2640                cancellation.cancel();
2641                let _ = writer.close().await;
2642                return;
2643            }
2644        }
2645    });
2646}
2647
2648#[allow(clippy::too_many_arguments)]
2649fn reserve_write(
2650    sender: mpsc::Sender<WriteRequest>,
2651    byte_budget: Arc<Semaphore>,
2652    envelope: Envelope,
2653    completion: Option<EffectId>,
2654    relay: bool,
2655    handle: ProtocolCoreHandle,
2656    cancellation: CancellationToken,
2657    reservation_pending: Option<Arc<AtomicBool>>,
2658    timeout: std::time::Duration,
2659) {
2660    n0_future::task::spawn(async move {
2661        let effect = completion;
2662        let acquire = async {
2663            let budget = byte_budget
2664                .acquire_many_owned(WriteRequest::weight(&envelope))
2665                .await
2666                .map_err(|_| ())?;
2667            let slot = sender.reserve_owned().await.map_err(|_| ())?;
2668            Ok::<_, ()>((budget, slot))
2669        };
2670        let reserved = tokio::select! {
2671            biased;
2672            () = cancellation.cancelled() => {
2673                if let Some(pending) = &reservation_pending {
2674                    pending.store(false, Ordering::Release);
2675                }
2676                if let Some(effect) = effect {
2677                    let _ = handle.submit(send_completion(effect, relay, SendResult::Cancelled)).await;
2678                }
2679                return;
2680            }
2681            reserved = n0_future::time::timeout(timeout, acquire) => reserved,
2682        };
2683        if let Some(pending) = &reservation_pending {
2684            pending.store(false, Ordering::Release);
2685        }
2686        match reserved {
2687            Ok(Ok((budget, slot))) => {
2688                if let Some(effect) = effect {
2689                    if handle
2690                        .submit(send_completion(effect, relay, SendResult::Reserved))
2691                        .await
2692                        .is_err()
2693                    {
2694                        return;
2695                    }
2696                }
2697                slot.send(WriteRequest {
2698                    envelope,
2699                    completion,
2700                    relay,
2701                    _budget: budget,
2702                });
2703            }
2704            Ok(Err(())) => {
2705                if let Some(effect) = effect {
2706                    let _ = handle
2707                        .submit(send_completion(effect, relay, SendResult::Closed))
2708                        .await;
2709                }
2710            }
2711            Err(_) => {
2712                if let Some(effect) = effect {
2713                    let _ = handle
2714                        .submit(send_completion(
2715                            effect,
2716                            relay,
2717                            SendResult::ReservationTimedOut,
2718                        ))
2719                        .await;
2720                }
2721            }
2722        }
2723    });
2724}
2725
2726fn send_completion(effect: EffectId, relay: bool, result: SendResult) -> CoreInput {
2727    if relay {
2728        CoreInput::RelayForwardCompleted { effect, result }
2729    } else {
2730        CoreInput::SendCompleted { effect, result }
2731    }
2732}
2733
2734fn spawn_session_deadline(
2735    session: SessionId,
2736    mut deadlines: tokio::sync::watch::Receiver<Option<Instant>>,
2737    handle: ProtocolCoreHandle,
2738    cancellation: CancellationToken,
2739) {
2740    n0_future::task::spawn(async move {
2741        loop {
2742            let deadline = *deadlines.borrow_and_update();
2743            match deadline {
2744                Some(deadline) if clock_now() < deadline => {
2745                    tokio::select! {
2746                        biased;
2747                        () = cancellation.cancelled() => return,
2748                        () = sleep_to(deadline) => {}
2749                    }
2750                }
2751                Some(_) => {
2752                    if handle
2753                        .submit(CoreInput::SessionTimeout {
2754                            session: session.clone(),
2755                        })
2756                        .await
2757                        .is_err()
2758                    {
2759                        return;
2760                    }
2761                    tokio::select! {
2762                        biased;
2763                        () = cancellation.cancelled() => return,
2764                        changed = deadlines.changed() => {
2765                            if changed.is_err() {
2766                                return;
2767                            }
2768                        }
2769                    }
2770                }
2771                None => {
2772                    tokio::select! {
2773                        biased;
2774                        () = cancellation.cancelled() => return,
2775                        changed = deadlines.changed() => {
2776                            if changed.is_err() {
2777                                return;
2778                            }
2779                        }
2780                    }
2781                }
2782            }
2783        }
2784    });
2785}
2786
2787fn spawn_client_timeout(
2788    session: SessionId,
2789    operation: ClientOperationId,
2790    deadline: Instant,
2791    handle: ProtocolCoreHandle,
2792    cancellation: Option<CancellationToken>,
2793) {
2794    let Some(cancellation) = cancellation else {
2795        return;
2796    };
2797    n0_future::task::spawn(async move {
2798        tokio::select! {
2799            biased;
2800            () = cancellation.cancelled() => {}
2801            () = sleep_to(deadline) => {
2802                let _ = handle.submit(CoreInput::ClientOperationTimeout { session, operation }).await;
2803            }
2804        }
2805    });
2806}
2807
2808fn spawn_establishment_timeout(session: SessionId, handle: ProtocolCoreHandle) {
2809    n0_future::task::spawn(async move {
2810        n0_future::time::sleep(ESTABLISHMENT_TIMEOUT).await;
2811        let _ = handle
2812            .submit(CoreInput::EstablishmentTimeout { session })
2813            .await;
2814    });
2815}
2816
2817#[cfg(test)]
2818mod tests {
2819    use super::*;
2820    use bytes::Bytes;
2821    use std::sync::atomic::{AtomicBool, Ordering};
2822    use unb_core::{DiscoverPlan, Kind, RetirementReason, PROTOCOL_VERSION};
2823
2824    struct RecordingExecutor {
2825        effects: mpsc::Sender<CoreEffect>,
2826    }
2827
2828    impl EffectExecutor for RecordingExecutor {
2829        fn execute(&self, effect: CoreEffect, _handle: ProtocolCoreHandle) -> EffectFuture {
2830            let effects = self.effects.clone();
2831            Box::pin(async move {
2832                let _ = effects.send(effect).await;
2833                None
2834            })
2835        }
2836    }
2837
2838    struct PublicationExecutor {
2839        published: Arc<AtomicBool>,
2840        effects: mpsc::Sender<bool>,
2841    }
2842
2843    struct ClosingHandler {
2844        delivered: mpsc::Sender<Envelope>,
2845        closed: mpsc::Sender<ClientOperationId>,
2846    }
2847
2848    impl SessionHandler for ClosingHandler {
2849        async fn deliver(&mut self, envelope: Envelope) {
2850            let _ = self.delivered.send(envelope).await;
2851        }
2852
2853        async fn stream_closed(&mut self, operation: ClientOperationId) {
2854            let _ = self.closed.send(operation).await;
2855        }
2856    }
2857
2858    impl EffectExecutor for PublicationExecutor {
2859        fn execute(&self, effect: CoreEffect, _handle: ProtocolCoreHandle) -> EffectFuture {
2860            let published = self.published.clone();
2861            let effects = self.effects.clone();
2862            Box::pin(async move {
2863                if matches!(effect, CoreEffect::SessionRetired { .. }) {
2864                    let _ = effects.send(published.load(Ordering::SeqCst)).await;
2865                }
2866                None
2867            })
2868        }
2869    }
2870
2871    fn local_pipe() -> (Pipe, mpsc::Sender<Envelope>, mpsc::Receiver<Envelope>) {
2872        let (incoming, rx) = mpsc::channel(16);
2873        let (tx, outgoing) = mpsc::channel(16);
2874        (
2875            Pipe::Local {
2876                rx,
2877                tx,
2878                initiator: true,
2879            },
2880            incoming,
2881            outgoing,
2882        )
2883    }
2884
2885    fn frame(kind: Kind, corr: Option<String>, payload: serde_json::Value) -> Envelope {
2886        Envelope {
2887            v: PROTOCOL_VERSION,
2888            id: format!("{kind:?}"),
2889            target: String::new(),
2890            subject: String::new(),
2891            kind,
2892            corr,
2893            seq: None,
2894            hops: None,
2895            body_token: None,
2896            payload: Envelope::encode_payload(&payload),
2897            path: Vec::new(),
2898            headers: Default::default(),
2899        }
2900    }
2901
2902    async fn establish(incoming: &mpsc::Sender<Envelope>, outgoing: &mut mpsc::Receiver<Envelope>) {
2903        assert_eq!(outgoing.recv().await.unwrap().kind, Kind::Hello);
2904        incoming
2905            .send(frame(
2906                Kind::Welcome,
2907                None,
2908                serde_json::json!({ "version": 1 }),
2909            ))
2910            .await
2911            .unwrap();
2912    }
2913
2914    fn request(target_path: &str, body: &'static [u8]) -> http::Request<Bytes> {
2915        http::Request::post(target_path)
2916            .body(Bytes::from_static(body))
2917            .unwrap()
2918    }
2919
2920    async fn attach_client(handle: &ProtocolCoreHandle, name: &str, pipe: Pipe) -> Arc<Wire> {
2921        attach_client_with_ceiling(handle, name, pipe, unb_transport::DEFAULT_MAX_FRAME_SIZE).await
2922    }
2923
2924    async fn attach_client_with_ceiling(
2925        handle: &ProtocolCoreHandle,
2926        name: &str,
2927        pipe: Pipe,
2928        ws_collect_ceiling: usize,
2929    ) -> Arc<Wire> {
2930        let session_cancellation = handle.cancellation.child_token();
2931        let (wire, directives, outcome, routes) =
2932            Wire::standalone_channel(session_cancellation.clone());
2933        let (reply, response) = oneshot::channel();
2934        handle
2935            .inputs
2936            .send(ActorInput::Attach {
2937                session: SessionId::from(name),
2938                transport: pipe,
2939                expected_peer: None,
2940                directives,
2941                handler: handler_channel(wire.observation_sender()),
2942                client: wire.client_session(),
2943                cancellation: session_cancellation,
2944                establish_peer: false,
2945                outcome,
2946                routes,
2947                ws_collect_ceiling,
2948                reply,
2949            })
2950            .await
2951            .unwrap();
2952        response.await.unwrap().unwrap();
2953        wire
2954    }
2955
2956    fn spawn_core() -> ProtocolCoreHandle {
2957        ProtocolCoreHandle::spawn(
2958            ProtocolCore::new("node"),
2959            Arc::new(RecordingExecutor {
2960                effects: mpsc::channel(1).0,
2961            }),
2962            CancellationToken::new(),
2963            &tokio::runtime::Handle::current(),
2964        )
2965    }
2966
2967    fn stalled_pipe() -> (Pipe, mpsc::Sender<Envelope>, mpsc::Receiver<Envelope>) {
2968        let (incoming, rx) = mpsc::channel(16);
2969        let (tx, outgoing) = mpsc::channel(1);
2970        (
2971            Pipe::Local {
2972                rx,
2973                tx,
2974                initiator: true,
2975            },
2976            incoming,
2977            outgoing,
2978        )
2979    }
2980
2981    fn chunk_stream(chunks: Vec<Result<Bytes, unb_core::CoreError>>) -> BodyStream {
2982        Box::pin(futures_util::stream::iter(chunks))
2983    }
2984
2985    #[tokio::test(start_paused = true)]
2986    async fn a_metered_body_stall_past_the_deadline_is_busy_shaped() {
2987        let budget = Arc::new(Semaphore::new(4));
2988        let hold = budget.clone().try_acquire_many_owned(4).unwrap();
2989        let mut body = MeteredBody::new(
2990            chunk_stream(vec![Ok(Bytes::from_static(b"x"))]),
2991            budget,
2992            CancellationToken::new(),
2993        );
2994        let outcome = body.next_chunk().await;
2995        assert!(
2996            matches!(
2997                outcome,
2998                Err(WsError::Core(unb_core::CoreError::BodyStalled))
2999            ),
3000            "budget starvation past the deadline must be BUSY-shaped: {outcome:?}"
3001        );
3002        drop(hold);
3003    }
3004
3005    #[tokio::test]
3006    async fn a_window_saturated_body_leaves_a_sibling_pump_undisturbed() {
3007        let budget = Arc::new(Semaphore::new(OUTBOUND_BYTE_BUDGET));
3008        let mut saturated = MeteredBody::new(
3009            chunk_stream(vec![
3010                Ok(Bytes::from(vec![0u8; BODY_WINDOW_BYTES])),
3011                Ok(Bytes::from_static(b"tail")),
3012            ]),
3013            budget.clone(),
3014            CancellationToken::new(),
3015        );
3016        let held = saturated.next_chunk().await.unwrap().unwrap();
3017        assert!(
3018            tokio::time::timeout(std::time::Duration::from_millis(50), saturated.next_chunk())
3019                .await
3020                .is_err(),
3021            "a full per-body window must block the next chunk"
3022        );
3023        let mut sibling = MeteredBody::new(
3024            chunk_stream(vec![Ok(Bytes::from_static(b"y"))]),
3025            budget,
3026            CancellationToken::new(),
3027        );
3028        let chunk = tokio::time::timeout(std::time::Duration::from_secs(1), sibling.next_chunk())
3029            .await
3030            .expect("a sibling body must progress while another window is saturated")
3031            .unwrap()
3032            .unwrap();
3033        assert_eq!(chunk.bytes, Bytes::from_static(b"y"));
3034        drop(held);
3035        let tail = tokio::time::timeout(std::time::Duration::from_secs(1), saturated.next_chunk())
3036            .await
3037            .expect("draining the window must unblock the body")
3038            .unwrap()
3039            .unwrap();
3040        assert_eq!(tail.bytes, Bytes::from_static(b"tail"));
3041    }
3042
3043    #[test]
3044    fn body_tokens_are_unique_and_carry_entropy() {
3045        let first = mint_body_token();
3046        let second = mint_body_token();
3047        assert_ne!(first, second);
3048        assert!(
3049            first.len() >= 33,
3050            "a guessable serial token invites body substitution: {first}"
3051        );
3052    }
3053
3054    #[tokio::test]
3055    async fn an_oversized_source_chunk_is_rejected_before_retention() {
3056        let budget = Arc::new(Semaphore::new(OUTBOUND_BYTE_BUDGET));
3057        let mut body = MeteredBody::new(
3058            chunk_stream(vec![Ok(Bytes::from(vec![
3059                0u8;
3060                unb_transport::DEFAULT_MAX_FRAME_SIZE
3061                    + 1
3062            ]))]),
3063            budget.clone(),
3064            CancellationToken::new(),
3065        );
3066        let outcome = body.next_chunk().await;
3067        assert!(
3068            matches!(
3069                outcome,
3070                Err(WsError::Core(unb_core::CoreError::BodyTooLarge(_)))
3071            ),
3072            "a chunk beyond the frame cap must be refused before retention: {outcome:?}"
3073        );
3074        assert_eq!(
3075            budget.available_permits(),
3076            OUTBOUND_BYTE_BUDGET,
3077            "a refused chunk must not consume budget"
3078        );
3079    }
3080
3081    #[tokio::test]
3082    async fn concurrent_bodies_stay_within_the_session_budget() {
3083        let budget = Arc::new(Semaphore::new(100));
3084        let mut first = MeteredBody::new(
3085            chunk_stream(vec![Ok(Bytes::from(vec![0u8; 60]))]),
3086            budget.clone(),
3087            CancellationToken::new(),
3088        );
3089        let mut second = MeteredBody::new(
3090            chunk_stream(vec![Ok(Bytes::from(vec![0u8; 60]))]),
3091            budget.clone(),
3092            CancellationToken::new(),
3093        );
3094        let held = first.next_chunk().await.unwrap().unwrap();
3095        assert_eq!(budget.available_permits(), 40);
3096        assert!(
3097            tokio::time::timeout(std::time::Duration::from_millis(50), second.next_chunk())
3098                .await
3099                .is_err(),
3100            "combined queued bytes must not exceed the session budget"
3101        );
3102        drop(held);
3103        let chunk = tokio::time::timeout(std::time::Duration::from_secs(1), second.next_chunk())
3104            .await
3105            .expect("released budget must admit the waiting body")
3106            .unwrap()
3107            .unwrap();
3108        assert_eq!(chunk.bytes.len(), 60);
3109    }
3110
3111    #[tokio::test(start_paused = true)]
3112    async fn a_steady_body_streams_past_the_terminal_deadline_without_timing_out() {
3113        let budget = Arc::new(Semaphore::new(OUTBOUND_BYTE_BUDGET));
3114        let chunks: Vec<Result<Bytes, unb_core::CoreError>> =
3115            (0..8).map(|_| Ok(Bytes::from_static(b"chunk"))).collect();
3116        let mut body = MeteredBody::new(chunk_stream(chunks), budget, CancellationToken::new());
3117        for _ in 0..8 {
3118            let chunk = body
3119                .next_chunk()
3120                .await
3121                .expect("a steadily progressing body never times out")
3122                .expect("chunks remain");
3123            drop(chunk);
3124            tokio::time::advance(std::time::Duration::from_secs(6)).await;
3125        }
3126        assert!(body.next_chunk().await.unwrap().is_none());
3127    }
3128
3129    #[tokio::test]
3130    async fn pipe_recv_yields_whole_message_envelopes_with_no_body_stream() {
3131        let mut envelope = frame(
3132            Kind::Request,
3133            Some("c1".into()),
3134            serde_json::json!({"n": 1}),
3135        );
3136        envelope.target = "server".into();
3137        envelope.subject = "echo".into();
3138        let encoded = envelope.encode();
3139        let pipe = Pipe::Piped {
3140            pipe: Box::pin(ScriptedPipe {
3141                frames: std::collections::VecDeque::from([Ok(encoded.clone())]),
3142            }),
3143            initiator: true,
3144        };
3145        let (mut reader, _writer) = pipe.split();
3146        let (received, body) = reader.recv().await.unwrap().unwrap();
3147        assert!(body.is_none(), "whole-message carriers collect the body");
3148        assert_eq!(received, Envelope::decode(encoded).unwrap());
3149
3150        let (incoming, rx) = mpsc::channel(4);
3151        let (tx, _outgoing) = mpsc::channel(4);
3152        let pipe = Pipe::Local {
3153            rx,
3154            tx,
3155            initiator: true,
3156        };
3157        let (mut reader, _writer) = pipe.split();
3158        incoming.send(envelope.clone()).await.unwrap();
3159        let (received, body) = reader.recv().await.unwrap().unwrap();
3160        assert!(body.is_none());
3161        assert_eq!(received, envelope);
3162    }
3163
3164    #[test]
3165    fn write_weight_counts_head_and_payload_and_clamps_to_the_whole_budget() {
3166        let mut envelope = frame(Kind::Request, None, serde_json::Value::Null);
3167        envelope.id = String::new();
3168        envelope.target = "server".into();
3169        envelope.subject = "echo".into();
3170        envelope.payload = Bytes::new();
3171        assert_eq!(WriteRequest::weight(&envelope), 10);
3172        envelope
3173            .headers
3174            .insert("origin".into(), serde_json::Value::String("x".repeat(64)));
3175        assert_eq!(WriteRequest::weight(&envelope), 80);
3176        envelope.payload = Bytes::from(vec![0u8; OUTBOUND_BYTE_BUDGET + 1]);
3177        assert_eq!(WriteRequest::weight(&envelope), OUTBOUND_BYTE_BUDGET as u32);
3178    }
3179
3180    #[tokio::test]
3181    async fn a_byte_saturated_outbound_ends_with_busy_and_recovers_on_drain() {
3182        let handle = spawn_core();
3183        let (pipe, incoming, mut outgoing) = stalled_pipe();
3184        let wire = attach_client(&handle, "stalled", pipe).await;
3185        establish(&incoming, &mut outgoing).await;
3186        let client = wire.client_session();
3187        let payload = Bytes::from(vec![0u8; 16 * 1024 * 1024]);
3188
3189        let mut flood = Vec::new();
3190        for _ in 0..6 {
3191            flood.push(
3192                client
3193                    .start(
3194                        "/server/flood",
3195                        Kind::Request,
3196                        payload.clone(),
3197                        None,
3198                        Default::default(),
3199                    )
3200                    .await
3201                    .unwrap(),
3202            );
3203        }
3204        let mut rejected = client
3205            .start(
3206                "/server/flood",
3207                Kind::Request,
3208                payload.clone(),
3209                None,
3210                Default::default(),
3211            )
3212            .await
3213            .unwrap();
3214
3215        let refused = rejected.operation().as_str().to_owned();
3216        let refusal = tokio::time::timeout(std::time::Duration::from_secs(2), rejected.next())
3217            .await
3218            .expect("an over-budget write must be refused promptly, not silently queued");
3219        assert!(
3220            refusal.is_err(),
3221            "queued outbound bytes past the budget must refuse the write: {refusal:?}"
3222        );
3223
3224        let mut saw_busy = false;
3225        loop {
3226            match tokio::time::timeout(std::time::Duration::from_millis(300), outgoing.recv()).await
3227            {
3228                Ok(Some(envelope)) => {
3229                    if envelope.kind == Kind::Error
3230                        && envelope.corr.as_deref() == Some(refused.as_str())
3231                    {
3232                        assert_eq!(envelope.payload_json()["code"], "BUSY");
3233                        saw_busy = true;
3234                    }
3235                }
3236                Ok(None) => panic!("the stalled pipe closed while draining"),
3237                Err(_) => break,
3238            }
3239        }
3240        assert!(saw_busy, "the refused write must surface as a BUSY error");
3241
3242        let probe = client
3243            .start(
3244                "/server/probe",
3245                Kind::Request,
3246                payload.clone(),
3247                None,
3248                Default::default(),
3249            )
3250            .await
3251            .unwrap();
3252        let opening = tokio::time::timeout(std::time::Duration::from_secs(5), async {
3253            loop {
3254                let envelope = outgoing.recv().await.unwrap();
3255                if envelope.subject == "probe" {
3256                    break envelope;
3257                }
3258            }
3259        })
3260        .await
3261        .expect("draining the writer must release budget for later writes");
3262        incoming
3263            .send(frame(
3264                Kind::Response,
3265                opening.corr,
3266                serde_json::json!({ "ok": true }),
3267            ))
3268            .await
3269            .unwrap();
3270        let mut probe = probe;
3271        let response = tokio::time::timeout(std::time::Duration::from_secs(5), probe.next())
3272            .await
3273            .expect("a post-drain write must complete end to end")
3274            .unwrap()
3275            .unwrap();
3276        assert_eq!(response.payload_json(), serde_json::json!({ "ok": true }));
3277        handle.shutdown();
3278    }
3279
3280    #[tokio::test]
3281    async fn a_byte_saturated_session_leaves_a_sibling_session_undisturbed() {
3282        let handle = spawn_core();
3283        let (saturated_pipe, stalled_incoming, mut stalled_outgoing) = stalled_pipe();
3284        let stalled = attach_client(&handle, "stalled", saturated_pipe).await;
3285        establish(&stalled_incoming, &mut stalled_outgoing).await;
3286        let (live_pipe, live_incoming, mut live_outgoing) = local_pipe();
3287        let live = attach_client(&handle, "live", live_pipe).await;
3288        establish(&live_incoming, &mut live_outgoing).await;
3289
3290        let payload = Bytes::from(vec![0u8; 16 * 1024 * 1024]);
3291        let mut flood = Vec::new();
3292        for _ in 0..6 {
3293            flood.push(
3294                stalled
3295                    .client_session()
3296                    .start(
3297                        "/server/flood",
3298                        Kind::Request,
3299                        payload.clone(),
3300                        None,
3301                        Default::default(),
3302                    )
3303                    .await
3304                    .unwrap(),
3305            );
3306        }
3307
3308        let mut sibling = live
3309            .client_session()
3310            .start(
3311                "/server/sibling",
3312                Kind::Request,
3313                Bytes::new(),
3314                None,
3315                Default::default(),
3316            )
3317            .await
3318            .unwrap();
3319        let opening = tokio::time::timeout(std::time::Duration::from_secs(1), live_outgoing.recv())
3320            .await
3321            .expect("a sibling session must keep writing while another is byte-saturated")
3322            .unwrap();
3323        assert_eq!(opening.subject, "sibling");
3324        live_incoming
3325            .send(frame(
3326                Kind::Response,
3327                opening.corr,
3328                serde_json::json!({ "ok": true }),
3329            ))
3330            .await
3331            .unwrap();
3332        let response = tokio::time::timeout(std::time::Duration::from_secs(2), sibling.next())
3333            .await
3334            .expect("a sibling response must be delivered while another session is saturated")
3335            .unwrap()
3336            .unwrap();
3337        assert_eq!(response.payload_json(), serde_json::json!({ "ok": true }));
3338        handle.shutdown();
3339    }
3340
3341    #[tokio::test]
3342    async fn terminal_client_delivery_closes_handler_stream_once() {
3343        let cancellation = CancellationToken::new();
3344        let handle = ProtocolCoreHandle::spawn(
3345            ProtocolCore::new("node"),
3346            Arc::new(RecordingExecutor {
3347                effects: mpsc::channel(1).0,
3348            }),
3349            cancellation,
3350            &tokio::runtime::Handle::current(),
3351        );
3352        let (pipe, incoming, mut outgoing) = local_pipe();
3353        let (delivered, _deliveries) = mpsc::channel(4);
3354        let (closed, mut closures) = mpsc::channel(4);
3355        let session_cancellation = handle.cancellation.child_token();
3356        let (wire, directives, outcome, routes) =
3357            Wire::standalone_channel(session_cancellation.clone());
3358        let (reply, response) = oneshot::channel();
3359        handle
3360            .inputs
3361            .send(ActorInput::Attach {
3362                session: SessionId::from("session"),
3363                transport: pipe,
3364                expected_peer: None,
3365                directives,
3366                handler: handler_channel(ClosingHandler { delivered, closed }),
3367                client: wire.client_session(),
3368                cancellation: session_cancellation,
3369                establish_peer: false,
3370                outcome,
3371                routes,
3372                ws_collect_ceiling: unb_transport::DEFAULT_MAX_FRAME_SIZE,
3373                reply,
3374            })
3375            .await
3376            .unwrap();
3377        response.await.unwrap().unwrap();
3378        establish(&incoming, &mut outgoing).await;
3379        let mut stream = wire
3380            .client_session()
3381            .start(
3382                "/server/echo",
3383                Kind::Request,
3384                Bytes::new(),
3385                None,
3386                Default::default(),
3387            )
3388            .await
3389            .unwrap();
3390        let opening = outgoing.recv().await.unwrap();
3391
3392        incoming
3393            .send(frame(
3394                Kind::Response,
3395                opening.corr,
3396                serde_json::json!({ "ok": true }),
3397            ))
3398            .await
3399            .unwrap();
3400
3401        assert!(stream.next().await.unwrap().is_some());
3402        assert_eq!(closures.recv().await, Some(stream.operation().clone()));
3403        assert!(
3404            tokio::time::timeout(std::time::Duration::from_millis(25), closures.recv())
3405                .await
3406                .is_err()
3407        );
3408        handle.shutdown();
3409    }
3410
3411    #[tokio::test]
3412    async fn generic_terminal_delivery_closes_handler_stream_once() {
3413        let cancellation = CancellationToken::new();
3414        let handle = ProtocolCoreHandle::spawn(
3415            ProtocolCore::new("node"),
3416            Arc::new(RecordingExecutor {
3417                effects: mpsc::channel(1).0,
3418            }),
3419            cancellation,
3420            &tokio::runtime::Handle::current(),
3421        );
3422        let (pipe, incoming, mut outgoing) = local_pipe();
3423        let (delivered, mut deliveries) = mpsc::channel(4);
3424        let (closed, mut closures) = mpsc::channel(4);
3425        let session_cancellation = handle.cancellation.child_token();
3426        let (wire, directives, outcome, routes) =
3427            Wire::standalone_channel(session_cancellation.clone());
3428        let (reply, response) = oneshot::channel();
3429        handle
3430            .inputs
3431            .send(ActorInput::Attach {
3432                session: SessionId::from("session"),
3433                transport: pipe,
3434                expected_peer: None,
3435                directives,
3436                handler: handler_channel(ClosingHandler { delivered, closed }),
3437                client: wire.client_session(),
3438                cancellation: session_cancellation,
3439                establish_peer: false,
3440                outcome,
3441                routes,
3442                ws_collect_ceiling: unb_transport::DEFAULT_MAX_FRAME_SIZE,
3443                reply,
3444            })
3445            .await
3446            .unwrap();
3447        response.await.unwrap().unwrap();
3448        establish(&incoming, &mut outgoing).await;
3449        let corr = wire
3450            .open_stream("/server/echo", Kind::Request, serde_json::Value::Null)
3451            .await
3452            .unwrap();
3453        let opening = outgoing.recv().await.unwrap();
3454        assert_eq!(opening.corr.as_deref(), Some(corr.as_str()));
3455
3456        incoming
3457            .send(frame(
3458                Kind::Response,
3459                Some(corr.clone()),
3460                serde_json::json!({ "ok": true }),
3461            ))
3462            .await
3463            .unwrap();
3464
3465        assert_eq!(
3466            deliveries.recv().await.unwrap().corr.as_deref(),
3467            Some(corr.as_str())
3468        );
3469        assert_eq!(closures.recv().await, Some(ClientOperationId::from(corr)));
3470        assert!(
3471            tokio::time::timeout(std::time::Duration::from_millis(25), closures.recv())
3472                .await
3473                .is_err()
3474        );
3475        handle.shutdown();
3476    }
3477
3478    #[tokio::test]
3479    async fn immediate_loopback_response_finds_registered_client_operation() {
3480        let (pipe, incoming, mut outgoing) = local_pipe();
3481        let wire = Wire::open(pipe);
3482        establish(&incoming, &mut outgoing).await;
3483        let loopback = tokio::spawn(async move {
3484            let opening = outgoing.recv().await.unwrap();
3485            let corr = opening.corr.unwrap();
3486            incoming
3487                .send(frame(
3488                    Kind::Response,
3489                    Some(corr),
3490                    serde_json::json!({ "ok": true }),
3491                ))
3492                .await
3493                .unwrap();
3494        });
3495
3496        let mut operation = wire
3497            .client_session()
3498            .start(
3499                "/server/echo",
3500                Kind::Request,
3501                Bytes::new(),
3502                None,
3503                Default::default(),
3504            )
3505            .await
3506            .unwrap();
3507
3508        loopback.await.unwrap();
3509        assert_eq!(
3510            operation
3511                .try_next()
3512                .unwrap()
3513                .unwrap()
3514                .unwrap()
3515                .payload_json(),
3516            serde_json::json!({ "ok": true })
3517        );
3518        assert_eq!(operation.try_next().unwrap(), Some(None));
3519    }
3520
3521    #[tokio::test]
3522    async fn opening_send_failure_cleans_up_client_operation() {
3523        let (pipe, incoming, mut outgoing) = local_pipe();
3524        let wire = Wire::open(pipe);
3525        establish(&incoming, &mut outgoing).await;
3526        drop(outgoing);
3527
3528        let mut operation = wire
3529            .client_session()
3530            .start(
3531                "/server/echo",
3532                Kind::Request,
3533                Bytes::new(),
3534                None,
3535                Default::default(),
3536            )
3537            .await
3538            .unwrap();
3539
3540        assert!(matches!(
3541            operation.next().await,
3542            Err(crate::ClientError::SessionClosed(_))
3543        ));
3544    }
3545
3546    #[tokio::test]
3547    async fn fetch_projects_response_and_ignores_late_terminal_frame() {
3548        let (pipe, incoming, mut outgoing) = local_pipe();
3549        let wire = Wire::open(pipe);
3550        establish(&incoming, &mut outgoing).await;
3551        let responder = tokio::spawn(async move {
3552            let opening = outgoing.recv().await.unwrap();
3553            let corr = opening.corr.unwrap();
3554            let mut response = frame(
3555                Kind::Response,
3556                Some(corr.clone()),
3557                serde_json::json!({ "ok": true }),
3558            );
3559            response.headers.insert("x-source".into(), "owner".into());
3560            incoming.send(response.clone()).await.unwrap();
3561            incoming.send(response).await.unwrap();
3562        });
3563
3564        let response = wire
3565            .client_session()
3566            .fetch(
3567                request("/server/echo", b"request"),
3568                std::time::Duration::from_secs(1),
3569            )
3570            .await
3571            .unwrap();
3572
3573        assert_eq!(response.status(), http::StatusCode::OK);
3574        assert_eq!(response.headers()["x-source"], "owner");
3575        assert_eq!(response.body(), &Bytes::from_static(br#"{"ok":true}"#));
3576        responder.await.unwrap();
3577    }
3578
3579    #[tokio::test]
3580    async fn subscribe_and_discovery_are_isolated() {
3581        let (pipe, incoming, mut outgoing) = local_pipe();
3582        let wire = Wire::open(pipe);
3583        establish(&incoming, &mut outgoing).await;
3584        let client = wire.client_session();
3585        let mut subscription = client
3586            .subscribe(request("/server/events", b""), None)
3587            .await
3588            .unwrap();
3589        let mut discovery = client
3590            .discover("/server", DiscoverPlan::decode(&Bytes::new()).unwrap())
3591            .await
3592            .unwrap();
3593        let first = outgoing.recv().await.unwrap();
3594        let second = outgoing.recv().await.unwrap();
3595        let (subscribe_corr, discover_corr) = if first.kind == Kind::Subscribe {
3596            (first.corr.unwrap(), second.corr.unwrap())
3597        } else {
3598            (second.corr.unwrap(), first.corr.unwrap())
3599        };
3600
3601        incoming
3602            .send(frame(
3603                Kind::Event,
3604                Some(discover_corr.clone()),
3605                serde_json::json!({ "discover": 1 }),
3606            ))
3607            .await
3608            .unwrap();
3609        incoming
3610            .send(frame(
3611                Kind::Event,
3612                Some(subscribe_corr.clone()),
3613                serde_json::json!({ "event": 1 }),
3614            ))
3615            .await
3616            .unwrap();
3617        incoming
3618            .send(frame(
3619                Kind::Response,
3620                Some(discover_corr),
3621                serde_json::json!({ "done": true }),
3622            ))
3623            .await
3624            .unwrap();
3625
3626        assert_eq!(
3627            subscription.next().await.unwrap().unwrap().payload_json()["event"],
3628            1
3629        );
3630        assert_eq!(
3631            discovery.next().await.unwrap().unwrap().payload_json()["discover"],
3632            1
3633        );
3634        assert_eq!(
3635            discovery.next().await.unwrap().unwrap().payload_json()["done"],
3636            true
3637        );
3638    }
3639
3640    #[tokio::test]
3641    async fn dropping_a_saturated_operation_frees_its_session_for_idle_delivery() {
3642        let (pipe, incoming, mut outgoing) = local_pipe();
3643        let wire = Wire::open(pipe);
3644        establish(&incoming, &mut outgoing).await;
3645        let client = wire.client_session();
3646        let busy = client
3647            .subscribe(request("/server/busy", b""), None)
3648            .await
3649            .unwrap();
3650        let mut idle = client
3651            .subscribe(request("/server/idle", b""), None)
3652            .await
3653            .unwrap();
3654        let busy_open = outgoing.recv().await.unwrap();
3655        let idle_open = outgoing.recv().await.unwrap();
3656
3657        let burst = tokio::spawn(async move {
3658            for sequence in 0..257 {
3659                incoming
3660                    .send(frame(
3661                        Kind::Event,
3662                        busy_open.corr.clone(),
3663                        serde_json::json!({ "sequence": sequence }),
3664                    ))
3665                    .await
3666                    .unwrap();
3667            }
3668            incoming
3669                .send(frame(
3670                    Kind::Response,
3671                    idle_open.corr,
3672                    serde_json::json!({ "done": true }),
3673                ))
3674                .await
3675                .unwrap();
3676            incoming
3677        });
3678        tokio::time::sleep(std::time::Duration::from_millis(25)).await;
3679
3680        drop(busy);
3681
3682        let _incoming = tokio::time::timeout(std::time::Duration::from_secs(5), burst)
3683            .await
3684            .expect("dropping the saturated stream must unblock its session")
3685            .unwrap();
3686        assert_eq!(
3687            idle.next().await.unwrap().unwrap().payload_json()["done"],
3688            true
3689        );
3690    }
3691
3692    #[tokio::test]
3693    async fn out_of_order_unary_responses_reach_their_operations() {
3694        let (pipe, incoming, mut outgoing) = local_pipe();
3695        let wire = Wire::open(pipe);
3696        establish(&incoming, &mut outgoing).await;
3697        let client = wire.client_session();
3698        let mut first = client
3699            .start(
3700                "/server/first",
3701                Kind::Request,
3702                Bytes::new(),
3703                None,
3704                Default::default(),
3705            )
3706            .await
3707            .unwrap();
3708        let mut second = client
3709            .start(
3710                "/server/second",
3711                Kind::Request,
3712                Bytes::new(),
3713                None,
3714                Default::default(),
3715            )
3716            .await
3717            .unwrap();
3718        let first_open = outgoing.recv().await.unwrap();
3719        let second_open = outgoing.recv().await.unwrap();
3720
3721        incoming
3722            .send(frame(
3723                Kind::Response,
3724                second_open.corr,
3725                serde_json::json!({ "response": "second" }),
3726            ))
3727            .await
3728            .unwrap();
3729        incoming
3730            .send(frame(
3731                Kind::Response,
3732                first_open.corr,
3733                serde_json::json!({ "response": "first" }),
3734            ))
3735            .await
3736            .unwrap();
3737
3738        assert_eq!(
3739            first.next().await.unwrap().unwrap().payload_json()["response"],
3740            "first"
3741        );
3742        assert_eq!(
3743            second.next().await.unwrap().unwrap().payload_json()["response"],
3744            "second"
3745        );
3746    }
3747
3748    #[tokio::test]
3749    async fn mixed_unary_subscription_and_discovery_are_isolated() {
3750        let (pipe, incoming, mut outgoing) = local_pipe();
3751        let wire = Wire::open(pipe);
3752        establish(&incoming, &mut outgoing).await;
3753        let client = wire.client_session();
3754        let mut unary = client
3755            .start(
3756                "/server/unary",
3757                Kind::Request,
3758                Bytes::new(),
3759                None,
3760                Default::default(),
3761            )
3762            .await
3763            .unwrap();
3764        let mut subscription = client
3765            .subscribe(request("/server/events", b""), None)
3766            .await
3767            .unwrap();
3768        let mut discovery = client
3769            .discover("/server", DiscoverPlan::decode(&Bytes::new()).unwrap())
3770            .await
3771            .unwrap();
3772        let mut unary_corr = None;
3773        let mut subscription_corr = None;
3774        let mut discovery_corr = None;
3775        for _ in 0..3 {
3776            let opening = outgoing.recv().await.unwrap();
3777            match opening.kind {
3778                Kind::Request => unary_corr = opening.corr,
3779                Kind::Subscribe => subscription_corr = opening.corr,
3780                Kind::Discover => discovery_corr = opening.corr,
3781                kind => panic!("unexpected opening kind {kind:?}"),
3782            }
3783        }
3784
3785        for (kind, corr, value) in [
3786            (Kind::Event, discovery_corr, "discover"),
3787            (Kind::Response, unary_corr, "unary"),
3788            (Kind::Event, subscription_corr, "subscription"),
3789        ] {
3790            incoming
3791                .send(frame(kind, corr, serde_json::json!({ "owner": value })))
3792                .await
3793                .unwrap();
3794        }
3795
3796        assert_eq!(
3797            unary.next().await.unwrap().unwrap().payload_json()["owner"],
3798            "unary"
3799        );
3800        assert_eq!(
3801            subscription.next().await.unwrap().unwrap().payload_json()["owner"],
3802            "subscription"
3803        );
3804        assert_eq!(
3805            discovery.next().await.unwrap().unwrap().payload_json()["owner"],
3806            "discover"
3807        );
3808    }
3809
3810    #[tokio::test]
3811    async fn cancelling_one_operation_does_not_complete_another() {
3812        let (pipe, incoming, mut outgoing) = local_pipe();
3813        let wire = Wire::open(pipe);
3814        establish(&incoming, &mut outgoing).await;
3815        let client = wire.client_session();
3816        let cancelled = client
3817            .subscribe(request("/server/cancelled", b""), None)
3818            .await
3819            .unwrap();
3820        let mut live = client
3821            .subscribe(request("/server/live", b""), None)
3822            .await
3823            .unwrap();
3824        let cancelled_open = outgoing.recv().await.unwrap();
3825        let live_open = outgoing.recv().await.unwrap();
3826
3827        drop(cancelled);
3828        let cancel = outgoing.recv().await.unwrap();
3829        assert_eq!(cancel.kind, Kind::Cancel);
3830        assert_eq!(cancel.corr, cancelled_open.corr);
3831        incoming
3832            .send(frame(
3833                Kind::Event,
3834                live_open.corr,
3835                serde_json::json!({ "live": true }),
3836            ))
3837            .await
3838            .unwrap();
3839        assert_eq!(
3840            live.next().await.unwrap().unwrap().payload_json()["live"],
3841            true
3842        );
3843    }
3844
3845    #[tokio::test]
3846    async fn stream_drop_sends_exactly_one_cancel() {
3847        let (pipe, incoming, mut outgoing) = local_pipe();
3848        let wire = Wire::open(pipe);
3849        establish(&incoming, &mut outgoing).await;
3850        let stream = wire
3851            .client_session()
3852            .subscribe(request("/server/events", b""), None)
3853            .await
3854            .unwrap();
3855        let opening = outgoing.recv().await.unwrap();
3856        let corr = opening.corr.unwrap();
3857
3858        drop(stream);
3859
3860        let cancel = tokio::time::timeout(std::time::Duration::from_secs(1), outgoing.recv())
3861            .await
3862            .unwrap()
3863            .unwrap();
3864        assert_eq!(cancel.kind, Kind::Cancel);
3865        assert_eq!(cancel.corr.as_deref(), Some(corr.as_str()));
3866        assert!(
3867            tokio::time::timeout(std::time::Duration::from_millis(25), outgoing.recv())
3868                .await
3869                .is_err()
3870        );
3871    }
3872
3873    #[tokio::test]
3874    async fn operation_timeout_is_typed_and_cancels_once() {
3875        let (pipe, incoming, mut outgoing) = local_pipe();
3876        let wire = Wire::open(pipe);
3877        establish(&incoming, &mut outgoing).await;
3878        let mut stream = wire
3879            .client_session()
3880            .subscribe(
3881                request("/server/events", b""),
3882                Some(std::time::Duration::from_millis(10)),
3883            )
3884            .await
3885            .unwrap();
3886        let opening = outgoing.recv().await.unwrap();
3887
3888        assert!(matches!(
3889            stream.next().await,
3890            Err(crate::ClientError::Timeout(_))
3891        ));
3892        let cancel = outgoing.recv().await.unwrap();
3893        assert_eq!(cancel.kind, Kind::Cancel);
3894        assert_eq!(cancel.corr, opening.corr);
3895        drop(stream);
3896        assert!(
3897            tokio::time::timeout(std::time::Duration::from_millis(25), outgoing.recv())
3898                .await
3899                .is_err()
3900        );
3901    }
3902
3903    #[tokio::test]
3904    async fn session_close_completes_every_operation_once() {
3905        let (pipe, incoming, mut outgoing) = local_pipe();
3906        let wire = Wire::open(pipe);
3907        establish(&incoming, &mut outgoing).await;
3908        let client = wire.client_session();
3909        let mut first = client
3910            .subscribe(request("/server/first", b""), None)
3911            .await
3912            .unwrap();
3913        let mut second = client
3914            .subscribe(request("/server/second", b""), None)
3915            .await
3916            .unwrap();
3917        outgoing.recv().await.unwrap();
3918        outgoing.recv().await.unwrap();
3919
3920        drop(incoming);
3921
3922        assert!(matches!(
3923            first.next().await,
3924            Err(crate::ClientError::SessionClosed(_))
3925        ));
3926        assert!(matches!(
3927            second.next().await,
3928            Err(crate::ClientError::SessionClosed(_))
3929        ));
3930        assert_eq!(first.next().await.unwrap(), None);
3931        assert_eq!(second.next().await.unwrap(), None);
3932    }
3933
3934    #[tokio::test]
3935    async fn a_session_the_core_rejects_closes_its_transport() {
3936        let (pipe, incoming, mut outgoing) = local_pipe();
3937        let _wire = Wire::open(pipe);
3938        assert_eq!(outgoing.recv().await.unwrap().kind, Kind::Hello);
3939        incoming
3940            .send(frame(
3941                Kind::Welcome,
3942                None,
3943                serde_json::json!({ "version": 999 }),
3944            ))
3945            .await
3946            .unwrap();
3947        let mut saw_error = false;
3948        let closed = loop {
3949            match tokio::time::timeout(std::time::Duration::from_millis(500), outgoing.recv()).await
3950            {
3951                Ok(Some(frame)) => {
3952                    if frame.kind == Kind::Error {
3953                        saw_error = true;
3954                    }
3955                }
3956                Ok(None) => break true,
3957                Err(_) => break false,
3958            }
3959        };
3960        assert!(
3961            saw_error,
3962            "the peer must receive the close-reason error frame before the transport closes"
3963        );
3964        assert!(
3965            closed,
3966            "core deciding a session invalid must close the transport, not leave it open"
3967        );
3968    }
3969
3970    struct ScriptedPipe {
3971        frames: std::collections::VecDeque<Result<Bytes, unb_transport::TransportError>>,
3972    }
3973
3974    impl futures_util::Stream for ScriptedPipe {
3975        type Item = Result<Bytes, unb_transport::TransportError>;
3976
3977        fn poll_next(
3978            mut self: Pin<&mut Self>,
3979            _cx: &mut std::task::Context<'_>,
3980        ) -> std::task::Poll<Option<Self::Item>> {
3981            std::task::Poll::Ready(self.frames.pop_front())
3982        }
3983    }
3984
3985    impl futures_util::Sink<Bytes> for ScriptedPipe {
3986        type Error = unb_transport::TransportError;
3987
3988        fn poll_ready(
3989            self: Pin<&mut Self>,
3990            _cx: &mut std::task::Context<'_>,
3991        ) -> std::task::Poll<Result<(), Self::Error>> {
3992            std::task::Poll::Ready(Ok(()))
3993        }
3994
3995        fn start_send(self: Pin<&mut Self>, _item: Bytes) -> Result<(), Self::Error> {
3996            Ok(())
3997        }
3998
3999        fn poll_flush(
4000            self: Pin<&mut Self>,
4001            _cx: &mut std::task::Context<'_>,
4002        ) -> std::task::Poll<Result<(), Self::Error>> {
4003            std::task::Poll::Ready(Ok(()))
4004        }
4005
4006        fn poll_close(
4007            self: Pin<&mut Self>,
4008            _cx: &mut std::task::Context<'_>,
4009        ) -> std::task::Poll<Result<(), Self::Error>> {
4010            std::task::Poll::Ready(Ok(()))
4011        }
4012    }
4013
4014    #[tokio::test]
4015    async fn a_malformed_frame_retires_the_session_with_a_typed_reason() {
4016        let cancellation = CancellationToken::new();
4017        let (effect_tx, mut effects) = mpsc::channel(64);
4018        let handle = ProtocolCoreHandle::spawn(
4019            ProtocolCore::new("node"),
4020            Arc::new(RecordingExecutor { effects: effect_tx }),
4021            cancellation,
4022            &tokio::runtime::Handle::current(),
4023        );
4024        let pipe = Pipe::Piped {
4025            pipe: Box::pin(ScriptedPipe {
4026                frames: std::collections::VecDeque::from([Ok(Bytes::from_static(
4027                    b"\xff\xff\xff\xffgarbage",
4028                ))]),
4029            }),
4030            initiator: true,
4031        };
4032        let (events, _events_rx) = mpsc::channel(16);
4033        handle
4034            .attach(SessionId::from("session"), pipe, None, events)
4035            .await
4036            .unwrap();
4037        let reason = tokio::time::timeout(std::time::Duration::from_secs(2), async {
4038            loop {
4039                if let CoreEffect::SessionRetired { reason, .. } = effects.recv().await.unwrap() {
4040                    break reason;
4041                }
4042            }
4043        })
4044        .await
4045        .expect("a malformed frame must retire the session");
4046        assert_eq!(reason, RetirementReason::TransportFailed);
4047        handle.shutdown();
4048    }
4049
4050    #[tokio::test]
4051    async fn a_transport_read_error_retires_the_session_with_a_typed_reason() {
4052        let cancellation = CancellationToken::new();
4053        let (effect_tx, mut effects) = mpsc::channel(64);
4054        let handle = ProtocolCoreHandle::spawn(
4055            ProtocolCore::new("node"),
4056            Arc::new(RecordingExecutor { effects: effect_tx }),
4057            cancellation,
4058            &tokio::runtime::Handle::current(),
4059        );
4060        let pipe = Pipe::Piped {
4061            pipe: Box::pin(ScriptedPipe {
4062                frames: std::collections::VecDeque::from([Err(unb_transport::TransportError::Io(
4063                    "connection reset".into(),
4064                ))]),
4065            }),
4066            initiator: true,
4067        };
4068        let (events, _events_rx) = mpsc::channel(16);
4069        handle
4070            .attach(SessionId::from("session"), pipe, None, events)
4071            .await
4072            .unwrap();
4073        let reason = tokio::time::timeout(std::time::Duration::from_secs(2), async {
4074            loop {
4075                if let CoreEffect::SessionRetired { reason, .. } = effects.recv().await.unwrap() {
4076                    break reason;
4077                }
4078            }
4079        })
4080        .await
4081        .expect("a transport read error must retire the session");
4082        assert_eq!(reason, RetirementReason::TransportFailed);
4083        handle.shutdown();
4084    }
4085
4086    #[tokio::test]
4087    async fn routes_two_sessions_and_submits_transport_closure() {
4088        let cancellation = CancellationToken::new();
4089        let (effect_tx, mut effects) = mpsc::channel(16);
4090        let handle = ProtocolCoreHandle::spawn(
4091            ProtocolCore::new("node"),
4092            Arc::new(RecordingExecutor { effects: effect_tx }),
4093            cancellation,
4094            &tokio::runtime::Handle::current(),
4095        );
4096        let (first_pipe, first_incoming, mut first_outgoing) = local_pipe();
4097        let (second_pipe, _second_incoming, mut second_outgoing) = local_pipe();
4098        let (first_events, _first_events_rx) = mpsc::channel(16);
4099        let (second_events, _second_events_rx) = mpsc::channel(16);
4100
4101        let first = handle
4102            .attach(SessionId::from("first"), first_pipe, None, first_events)
4103            .await
4104            .unwrap();
4105        let second = handle
4106            .attach(SessionId::from("second"), second_pipe, None, second_events)
4107            .await
4108            .unwrap();
4109
4110        assert!(first_outgoing.recv().await.is_some());
4111        assert!(second_outgoing.recv().await.is_some());
4112        assert!(Arc::ptr_eq(&first, &first));
4113        assert!(!Arc::ptr_eq(&first, &second));
4114
4115        drop(first_incoming);
4116        let retired = loop {
4117            let effect = effects.recv().await.unwrap();
4118            if matches!(
4119                effect,
4120                CoreEffect::SessionRetired {
4121                    ref session,
4122                    reason: RetirementReason::SessionClosed,
4123                } if session == &SessionId::from("first")
4124            ) {
4125                break effect;
4126            }
4127        };
4128        assert!(matches!(
4129            retired,
4130            CoreEffect::SessionRetired {
4131                reason: RetirementReason::SessionClosed,
4132                ..
4133            }
4134        ));
4135
4136        second.shutdown();
4137        handle.shutdown();
4138    }
4139
4140    #[tokio::test]
4141    async fn install_publishes_after_acceptance_and_before_effect_execution() {
4142        let cancellation = CancellationToken::new();
4143        let published = Arc::new(AtomicBool::new(false));
4144        let (effect_tx, mut effects) = mpsc::channel(1);
4145        let handle = ProtocolCoreHandle::spawn(
4146            ProtocolCore::new("node"),
4147            Arc::new(PublicationExecutor {
4148                published: published.clone(),
4149                effects: effect_tx,
4150            }),
4151            cancellation,
4152            &tokio::runtime::Handle::current(),
4153        );
4154        let (pipe, _incoming, mut outgoing) = local_pipe();
4155        let (events, _events_rx) = mpsc::channel(16);
4156        handle
4157            .attach(SessionId::from("session"), pipe, None, events)
4158            .await
4159            .unwrap();
4160        assert!(outgoing.recv().await.is_some());
4161
4162        handle
4163            .install(
4164                CoreInput::SessionClosed {
4165                    session: SessionId::from("session"),
4166                },
4167                {
4168                    let published = published.clone();
4169                    move || published.store(true, Ordering::SeqCst)
4170                },
4171            )
4172            .await
4173            .unwrap();
4174
4175        assert!(effects.recv().await.unwrap());
4176        handle.shutdown();
4177    }
4178
4179    #[tokio::test]
4180    async fn install_failure_does_not_publish() {
4181        let cancellation = CancellationToken::new();
4182        let (effect_tx, _effects) = mpsc::channel(1);
4183        let handle = ProtocolCoreHandle::spawn(
4184            ProtocolCore::new("node"),
4185            Arc::new(RecordingExecutor { effects: effect_tx }),
4186            cancellation,
4187            &tokio::runtime::Handle::current(),
4188        );
4189        let input = CoreInput::SessionOpened {
4190            session: SessionId::from("duplicate"),
4191            initiator: true,
4192            establish_peer: false,
4193            expected_peer: None,
4194        };
4195        handle.submit(input.clone()).await.unwrap();
4196        let published = Arc::new(AtomicBool::new(false));
4197
4198        let result = handle
4199            .install(input, {
4200                let published = published.clone();
4201                move || published.store(true, Ordering::SeqCst)
4202            })
4203            .await;
4204
4205        assert!(matches!(result, Err(WsError::Core(_))));
4206        assert!(!published.load(Ordering::SeqCst));
4207        handle.shutdown();
4208    }
4209
4210    #[test]
4211    fn operation_records_cannot_change_their_bound_correlation() {
4212        let response = frame(
4213            Kind::Response,
4214            Some("s2".into()),
4215            serde_json::json!({"ok": true}),
4216        );
4217        let error =
4218            decode_operation_message(response.encode(), "s1", OperationDirection::Return, 0)
4219                .unwrap_err();
4220        assert!(matches!(
4221            error,
4222            unb_transport::operation::OperationCodecError::Protocol(_)
4223        ));
4224        assert_eq!(error.reset_code(), unb_transport::operation::RESET_PROTOCOL);
4225    }
4226
4227    #[tokio::test]
4228    async fn streaming_operation_body_uses_head_chunks_and_no_wire_body_token() {
4229        let mut envelope = frame(Kind::Response, Some("s1".into()), serde_json::json!(null));
4230        envelope.body_token = Some("legacy-token-must-not-leak".into());
4231        let body: BodyStream = Box::pin(futures_util::stream::iter([
4232            Ok(Bytes::from_static(b"first")),
4233            Ok(Bytes::from_static(b"second")),
4234        ]));
4235        let (write, read) = tokio::io::duplex(4096);
4236        let sender = async move {
4237            let mut writer = unb_transport::operation::OperationRecordWriter::new(write);
4238            write_operation_envelope(
4239                &mut writer,
4240                envelope,
4241                Some(body),
4242                Arc::new(Semaphore::new(OUTBOUND_BYTE_BUDGET)),
4243                CancellationToken::new(),
4244            )
4245            .await
4246            .unwrap();
4247            writer.finish().await.unwrap();
4248        };
4249        let receiver = async move {
4250            let mut reader = unb_transport::operation::OperationRecordReader::new(read);
4251            let (kind, payload) = reader.read_record().await.unwrap();
4252            assert_eq!(kind, unb_transport::operation::OperationRecordKind::Head);
4253            let head = Envelope::decode(payload).unwrap();
4254            assert_eq!(head.corr.as_deref(), Some("s1"));
4255            assert!(head.body_token.is_none());
4256            assert!(head.payload.is_empty());
4257            let mut collected = Vec::new();
4258            loop {
4259                let (kind, payload) = reader.read_record().await.unwrap();
4260                match kind {
4261                    unb_transport::operation::OperationRecordKind::BodyChunk => {
4262                        collected.extend_from_slice(&payload)
4263                    }
4264                    unb_transport::operation::OperationRecordKind::BodyEnd => break,
4265                    other => panic!("unexpected body record {other:?}"),
4266                }
4267            }
4268            assert_eq!(collected, b"firstsecond");
4269            let (kind, payload) = reader.read_record().await.unwrap();
4270            assert_eq!(
4271                kind,
4272                unb_transport::operation::OperationRecordKind::DirectionEnd
4273            );
4274            assert!(payload.is_empty());
4275            reader.expect_fin().await.unwrap();
4276        };
4277        tokio::join!(sender, receiver);
4278    }
4279
4280    #[tokio::test]
4281    async fn cancelling_a_client_operation_releases_a_late_response_body() {
4282        let handle = spawn_core();
4283        let (pipe, incoming, mut outgoing) = local_pipe();
4284        let wire = attach_client(&handle, "cancel-body", pipe).await;
4285        establish(&incoming, &mut outgoing).await;
4286
4287        let call = wire
4288            .client_session()
4289            .start(
4290                "/server/echo",
4291                Kind::Request,
4292                Bytes::from_static(b"request"),
4293                None,
4294                Default::default(),
4295            )
4296            .await
4297            .unwrap();
4298        let corr = call.operation().as_str().to_owned();
4299        assert_eq!(
4300            outgoing.recv().await.unwrap().corr.as_deref(),
4301            Some(corr.as_str())
4302        );
4303        drop(call);
4304        let cancel = outgoing.recv().await.unwrap();
4305        assert_eq!(cancel.kind, Kind::Cancel);
4306        assert_eq!(cancel.corr.as_deref(), Some(corr.as_str()));
4307
4308        incoming
4309            .send(frame(
4310                Kind::Response,
4311                Some(corr),
4312                serde_json::json!({ "late": true }),
4313            ))
4314            .await
4315            .unwrap();
4316        tokio::time::timeout(std::time::Duration::from_secs(1), async {
4317            loop {
4318                let retained = handle
4319                    .token_bodies
4320                    .lock()
4321                    .expect("body token registry")
4322                    .keys()
4323                    .any(|(owner, _)| owner.as_str() == "cancel-body");
4324                if !retained {
4325                    break;
4326                }
4327                tokio::task::yield_now().await;
4328            }
4329        })
4330        .await
4331        .expect("late response body must be released");
4332        handle.shutdown();
4333    }
4334
4335    #[tokio::test]
4336    async fn session_retirement_releases_unclaimed_bodies() {
4337        let handle = spawn_core();
4338        let (pipe, incoming, mut outgoing) = local_pipe();
4339        let _wire = attach_client(&handle, "retired-body", pipe).await;
4340        establish(&incoming, &mut outgoing).await;
4341        handle
4342            .register_body(
4343                &SessionId::from("retired-body"),
4344                WireBody::Bytes(Bytes::from_static(b"retained")),
4345            )
4346            .unwrap();
4347        assert!(handle
4348            .token_bodies
4349            .lock()
4350            .expect("body token registry")
4351            .keys()
4352            .any(|(owner, _)| owner.as_str() == "retired-body"));
4353
4354        drop(incoming);
4355        tokio::time::timeout(std::time::Duration::from_secs(1), async {
4356            loop {
4357                let retained = handle
4358                    .token_bodies
4359                    .lock()
4360                    .expect("body token registry")
4361                    .keys()
4362                    .any(|(owner, _)| owner.as_str() == "retired-body");
4363                if !retained {
4364                    break;
4365                }
4366                tokio::task::yield_now().await;
4367            }
4368        })
4369        .await
4370        .expect("session retirement must release body capabilities");
4371        handle.shutdown();
4372    }
4373}