Skip to main content

secretspec_ipc/
server.rs

1use crate::deadline::instant_from_unix_ms;
2use crate::error::{ErrorKind, RpcError};
3use crate::frame::{AsyncFrameReader, write_frame};
4use crate::jsonrpc::{Envelope, Notification, Request, RequestId, Response};
5use crate::protocol::{
6    CancelParams, EmptyParams, InitializeParams, InitializeResult, Limits, Product, rpc,
7};
8use crate::{ABSOLUTE_MAX_FRAME_BYTES, Error, Result};
9use async_trait::async_trait;
10use serde_json::{Value, json};
11use std::collections::{HashMap, HashSet};
12use std::sync::Arc;
13use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
14use std::time::Duration;
15use tokio::io::{AsyncRead, AsyncWrite};
16use tokio::sync::{Mutex, OwnedSemaphorePermit, Semaphore, mpsc, oneshot};
17use tokio::task::JoinSet;
18use tokio::time::Instant;
19use tokio_util::sync::CancellationToken;
20use zeroize::Zeroizing;
21
22pub type RpcResult<T> = std::result::Result<T, RpcError>;
23const MAX_ABANDONED_CALLBACKS: usize = crate::MAX_IN_FLIGHT * 4;
24
25/// How long an out-of-band protocol response (an initialize reply, a shutdown
26/// reply, or a value-free error) may wait for the writer to take it.
27///
28/// Deliberately not `ServerConfig::startup_timeout`: this bounds transport
29/// backpressure on a response the session owes regardless of where it is in its
30/// lifecycle, whereas `startup_timeout` bounds application startup work.
31const COMMIT_TIMEOUT: Duration = Duration::from_secs(5);
32
33#[derive(Debug, Clone)]
34pub struct RequestContext {
35    pub request_id: RequestId,
36    pub deadline: Instant,
37    pub cancellation: CancellationToken,
38    /// Calls back to the client on this same session (0.21+). A handler uses it
39    /// only for a method the client advertised; see [`Peer::supports`].
40    pub peer: Peer,
41}
42
43/// The client, as seen from inside a handler.
44///
45/// Version 1 reverses direction for exactly one purpose: the endpoint that
46/// discovers a value is missing is never the process that can ask a person for
47/// it, and a stdio endpoint has no terminal to ask on. A callback is bounded by
48/// the deadline and the cancellation of the request that raised it, so it can
49/// neither outlive its caller nor keep an in-flight slot after the caller is
50/// gone.
51#[derive(Debug, Clone)]
52pub struct Peer {
53    inner: Arc<PeerInner>,
54}
55
56#[derive(Debug)]
57struct PeerInner {
58    /// Weak on purpose. The session ends by dropping its sender so the writer
59    /// task sees the channel close and the process can exit; a strong clone
60    /// here would keep that channel open and make every session linger until
61    /// the shutdown timeout fired.
62    writer: mpsc::WeakSender<WriterCommand>,
63    calls: Mutex<PeerCalls>,
64    request_order: Mutex<()>,
65    next_id: AtomicU64,
66    limit: AtomicUsize,
67    semaphore: std::sync::RwLock<Arc<Semaphore>>,
68    capabilities: std::sync::RwLock<HashSet<String>>,
69}
70
71#[derive(Debug, Default)]
72struct PeerCalls {
73    pending: HashMap<RequestId, oneshot::Sender<Response>>,
74    // Callbacks this side gave up on that the client has not answered yet.
75    // Each keeps its in-flight permit until that answer, because the client
76    // counts the callback as active until it responds and would reject a
77    // replacement sent into a slot it still considers occupied.
78    abandoned: HashMap<RequestId, OwnedSemaphorePermit>,
79}
80
81impl Peer {
82    fn new(writer: &mpsc::Sender<WriterCommand>) -> Self {
83        Self {
84            inner: Arc::new(PeerInner {
85                writer: writer.downgrade(),
86                calls: Mutex::new(PeerCalls::default()),
87                request_order: Mutex::new(()),
88                next_id: AtomicU64::new(1),
89                limit: AtomicUsize::new(ABSOLUTE_MAX_FRAME_BYTES),
90                semaphore: std::sync::RwLock::new(Arc::new(Semaphore::new(1))),
91                capabilities: std::sync::RwLock::new(HashSet::new()),
92            }),
93        }
94    }
95
96    /// A peer that is not attached to a session: it advertises nothing, so
97    /// [`Self::supports`] is always false and no callback is ever attempted.
98    ///
99    /// This is what a handler exercised outside a live transport sees, and it
100    /// is the same answer a real session gives for a client that advertised no
101    /// callbacks, so a test never accidentally proves behavior a headless
102    /// consumer would not get.
103    pub fn detached() -> Self {
104        let (writer, _) = mpsc::channel(1);
105        Self::new(&writer)
106    }
107
108    /// Whether the client advertised one callback method. A handler MUST check
109    /// this rather than calling and handling the failure: a client that cannot
110    /// reach a person wants to be told so immediately, not after a deadline.
111    pub fn supports(&self, method: &str) -> bool {
112        self.inner
113            .capabilities
114            .read()
115            .unwrap_or_else(|poisoned| poisoned.into_inner())
116            .contains(method)
117    }
118
119    /// Call one advertised method on the client and await its response.
120    ///
121    /// The deadline and cancellation come from the request being served, so an
122    /// abandoned caller takes its callback down with it and no answer can
123    /// arrive for a request that is already terminal.
124    pub async fn call<P: serde::Serialize, R: serde::de::DeserializeOwned>(
125        &self,
126        method: &str,
127        params: &P,
128        context: &RequestContext,
129    ) -> RpcResult<R> {
130        if context.cancellation.is_cancelled() {
131            return Err(RpcError::new(ErrorKind::Cancelled));
132        }
133        if !self.supports(method) {
134            return Err(RpcError::new(ErrorKind::CapabilityRequired));
135        }
136        let semaphore = self
137            .inner
138            .semaphore
139            .read()
140            .unwrap_or_else(|poisoned| poisoned.into_inner())
141            .clone();
142        let permit = semaphore
143            .try_acquire_owned()
144            .map_err(|_| RpcError::unavailable(None))?;
145        let params =
146            serde_json::to_value(params).map_err(|_| RpcError::new(ErrorKind::Internal))?;
147        let deadline_unix_ms =
148            crate::deadline::clamp_unix_ms(crate::deadline::unix_ms_from_instant(context.deadline));
149        let order = tokio::select! {
150            biased;
151            _ = context.cancellation.cancelled() => return Err(RpcError::new(ErrorKind::Cancelled)),
152            order = tokio::time::timeout_at(context.deadline, self.inner.request_order.lock()) =>
153                order.map_err(|_| RpcError::new(ErrorKind::DeadlineExceeded))?,
154        };
155        let id = RequestId::new(self.inner.next_id.fetch_add(1, Ordering::Relaxed))
156            .map_err(|_| RpcError::new(ErrorKind::Internal))?;
157        let request = Request::new(id, method, deadline_unix_ms, params)
158            .map_err(|_| RpcError::new(ErrorKind::Internal))?
159            .with_parent_request_id(context.request_id);
160        let limit = self.inner.limit.load(Ordering::Acquire);
161        let payload = Zeroizing::new(
162            Envelope::Request(request)
163                .to_vec()
164                .map_err(|_| RpcError::new(ErrorKind::Internal))?,
165        );
166        if payload.is_empty() || payload.len() > limit {
167            return Err(RpcError::new(ErrorKind::MessageTooLarge));
168        }
169
170        // The session drops its sender to close the transport. Failing to
171        // upgrade means the session is already ending, so there is nothing left
172        // to ask and nothing that could answer.
173        let Some(writer) = self.inner.writer.upgrade() else {
174            return Err(RpcError::new(ErrorKind::Unavailable));
175        };
176        let (sender, receiver) = oneshot::channel();
177        self.inner.calls.lock().await.pending.insert(id, sender);
178        let command = WriterCommand {
179            payload,
180            limit,
181            committed: None,
182        };
183        let queued = tokio::select! {
184            biased;
185            _ = context.cancellation.cancelled() => {
186                self.inner.calls.lock().await.pending.remove(&id);
187                return Err(RpcError::new(ErrorKind::Cancelled));
188            }
189            _ = tokio::time::sleep_until(context.deadline) => {
190                self.inner.calls.lock().await.pending.remove(&id);
191                return Err(RpcError::new(ErrorKind::DeadlineExceeded));
192            }
193            queued = writer.send(command) => queued,
194        };
195        if queued.is_err() {
196            self.inner.calls.lock().await.pending.remove(&id);
197            return Err(RpcError::new(ErrorKind::Unavailable));
198        }
199
200        drop(order);
201
202        let response = tokio::select! {
203            biased;
204            _ = context.cancellation.cancelled() => {
205                self.abandon_call(id, permit).await;
206                return Err(RpcError::new(ErrorKind::Cancelled));
207            }
208            response = tokio::time::timeout_at(context.deadline, receiver) => response,
209        };
210        match response {
211            Ok(Ok(Response::Success(response))) => serde_json::from_value(response.result)
212                .map_err(|_| RpcError::new(ErrorKind::OperationFailed)),
213            Ok(Ok(Response::Error(response))) => Err(response.error),
214            Ok(Err(_)) => Err(RpcError::new(ErrorKind::Unavailable)),
215            Err(_) => {
216                self.abandon_call(id, permit).await;
217                Err(RpcError::new(ErrorKind::DeadlineExceeded))
218            }
219        }
220    }
221
222    /// Deliver one inbound response, reporting whether it matched a call this
223    /// side actually made. An unmatched response is a protocol violation.
224    async fn deliver(&self, response: Response) -> bool {
225        let Some(id) = response.id() else {
226            return false;
227        };
228        let mut calls = self.inner.calls.lock().await;
229        match calls.pending.remove(&id) {
230            Some(sender) => {
231                calls.abandoned.remove(&id);
232                let _ = sender.send(response);
233                true
234            }
235            None => calls.abandoned.remove(&id).is_some(),
236        }
237    }
238
239    async fn abandon_call(&self, id: RequestId, permit: OwnedSemaphorePermit) {
240        // Both tables change under one lock. If `deliver` already took the
241        // sender, the callback is terminal and its permit is released now;
242        // otherwise the marker keeps it until the client's answer arrives.
243        let mut calls = self.inner.calls.lock().await;
244        if calls.pending.remove(&id).is_some() && calls.abandoned.len() < MAX_ABANDONED_CALLBACKS {
245            calls.abandoned.insert(id, permit);
246        }
247    }
248
249    async fn fail_all(&self) {
250        let mut calls = self.inner.calls.lock().await;
251        calls.pending.clear();
252        calls.abandoned.clear();
253    }
254}
255
256/// Transport-independent application hook. Typed resolution and provider
257/// adapters below this layer own application parameter validation.
258#[async_trait]
259pub trait ApplicationHandler: Send + Sync + 'static {
260    fn protocol(&self) -> &'static str;
261    fn versions(&self) -> &'static [u32] {
262        &[1]
263    }
264    fn capabilities(&self) -> Vec<String>;
265
266    fn validate_capabilities(&self, _capabilities: &[String]) -> RpcResult<()> {
267        Ok(())
268    }
269
270    async fn initialize(&self, context: &RequestContext, application: Value) -> RpcResult<Value>;
271
272    async fn call(&self, context: RequestContext, method: &str, params: Value) -> RpcResult<Value>;
273
274    /// Reports whether the application's response became the terminal writer
275    /// outcome. Handlers use an uncommitted outcome to release resources that
276    /// were created while producing a response (for example resolver leases).
277    async fn request_finished(&self, _request_id: RequestId, _committed: bool) {}
278
279    async fn shutdown(&self) {}
280}
281
282#[derive(Debug, Clone)]
283pub struct ServerConfig {
284    pub product: Product,
285    pub limits: Limits,
286    pub startup_timeout: Duration,
287}
288
289impl Default for ServerConfig {
290    fn default() -> Self {
291        Self {
292            product: Product {
293                name: "secretspec-ipc".to_string(),
294                version: env!("CARGO_PKG_VERSION").to_string(),
295            },
296            limits: Limits {
297                max_frame_bytes: ABSOLUTE_MAX_FRAME_BYTES,
298                max_in_flight: 8,
299            },
300            startup_timeout: Duration::from_secs(5),
301        }
302    }
303}
304
305/// One already-serialized frame for the single writer task.
306///
307/// The payload is built by the sender rather than the writer because the
308/// too-large fallback has to inspect the encoded size before it can decide what
309/// to enqueue, and because the writer now carries outbound callback requests as
310/// well as responses.
311struct WriterCommand {
312    payload: Zeroizing<Vec<u8>>,
313    limit: usize,
314    committed: Option<oneshot::Sender<std::result::Result<(), ()>>>,
315}
316
317/// Serve exactly one application session on a private byte stream.
318///
319/// Before application initialization, a peer may issue side-effect-free
320/// `rpc.discover` requests. No application method is dispatched until
321/// `rpc.initialize` succeeds.
322pub async fn serve<R, W, H>(
323    reader: R,
324    mut writer: W,
325    handler: Arc<H>,
326    config: ServerConfig,
327) -> Result<()>
328where
329    R: AsyncRead + Unpin + Send + 'static,
330    W: AsyncWrite + Unpin + Send + 'static,
331    H: ApplicationHandler,
332{
333    config.product.validate()?;
334    config.limits.validate()?;
335    let mut reader = AsyncFrameReader::new(reader);
336
337    let (writer_tx, mut writer_rx) =
338        mpsc::channel::<WriterCommand>(config.limits.max_in_flight + 2);
339    let disconnected = CancellationToken::new();
340    let writer_disconnected = disconnected.clone();
341    let mut writer_task = tokio::spawn(async move {
342        while let Some(command) = writer_rx.recv().await {
343            let outcome = write_frame(&mut writer, &command.payload, command.limit)
344                .await
345                .map_err(|_| ());
346            if let Some(committed) = command.committed {
347                let _ = committed.send(outcome);
348            }
349            if outcome.is_err() {
350                writer_disconnected.cancel();
351                break;
352            }
353        }
354        use tokio::io::AsyncWriteExt;
355        let _ = writer.shutdown().await;
356    });
357
358    let peer = Peer::new(&writer_tx);
359    let mut active_limit = ABSOLUTE_MAX_FRAME_BYTES;
360    let mut initialized = false;
361    let mut advertised_capabilities = HashSet::new();
362    let mut last_seen_id: Option<RequestId> = None;
363    let inflight: Arc<Mutex<HashMap<RequestId, CancellationToken>>> =
364        Arc::new(Mutex::new(HashMap::new()));
365    let mut tasks = JoinSet::new();
366    // Replaced with the negotiated permit count during initialization; no
367    // application call can be dispatched before that happens.
368    let mut semaphore = Arc::new(Semaphore::new(1));
369    let mut shutdown_request: Option<(RequestId, Instant)> = None;
370    let mut drained = false;
371    // A transport or protocol failure must still cancel in-flight work, join
372    // its tasks, and run `handler.shutdown()`. Returning `?` straight out of
373    // the loop would skip all of that, so the failure is carried out instead.
374    let mut fatal: Option<Error> = None;
375
376    'session: loop {
377        if shutdown_request.is_some() && tasks.is_empty() {
378            drained = true;
379            break;
380        }
381        // Keep the read future alive while reaping tasks: read_frame may have
382        // consumed part of a frame when a handler finishes.
383        let frame = {
384            let read = reader.read_frame(active_limit);
385            tokio::pin!(read);
386            loop {
387                tokio::select! {
388                    biased;
389                    _ = disconnected.cancelled() => break 'session,
390                    _ = async {
391                        match shutdown_request {
392                            Some((_, deadline)) => tokio::time::sleep_until(deadline).await,
393                            None => std::future::pending().await,
394                        }
395                    } => {
396                        drained = true;
397                        break 'session;
398                    }
399                    joined = tasks.join_next(), if !tasks.is_empty() => {
400                        if joined.is_some_and(|result| result.is_err()) {
401                            fatal = Some(Error::Protocol("application task failed"));
402                            break 'session;
403                        }
404                        if shutdown_request.is_some() && tasks.is_empty() {
405                            drained = true;
406                            break 'session;
407                        }
408                    }
409                    frame = &mut read => match frame {
410                        Ok(frame) => break frame,
411                        Err(error) => {
412                            fatal = Some(error);
413                            break 'session;
414                        }
415                    },
416                }
417            }
418        };
419        let Some(frame) = frame else {
420            break;
421        };
422
423        let envelope = match Envelope::parse_classified(&frame) {
424            Ok(envelope) => envelope,
425            Err((_, kind)) => {
426                // The strict parser intentionally does not recover an ID from a
427                // malformed object. Emit one value-free error, then close. The
428                // parser reports which layer rejected the frame, so there is no
429                // need to re-parse it here to choose a kind.
430                let error = Response::error(None, RpcError::new(kind));
431                let _ = commit(&writer_tx, error, active_limit).await;
432                break;
433            }
434        };
435
436        match envelope {
437            // Before this session reversed direction, any response was a
438            // protocol violation. It still is unless it answers a callback this
439            // side actually made: an unmatched or duplicate response means the
440            // peer is tracking a different session state than we are.
441            Envelope::Response(response) => {
442                if !initialized || !peer.deliver(response).await {
443                    break;
444                }
445            }
446            Envelope::Notification(notification) => {
447                handle_notification(notification, &inflight).await;
448            }
449            Envelope::Request(request) => {
450                if last_seen_id.is_some_and(|last| request.id <= last) {
451                    let response =
452                        Response::error(Some(request.id), RpcError::new(ErrorKind::InvalidRequest));
453                    let _ = commit(&writer_tx, response, active_limit).await;
454                    break;
455                }
456                last_seen_id = Some(request.id);
457
458                if !initialized {
459                    if request.method == rpc::DISCOVER {
460                        if let Err(error) = discover(
461                            &request,
462                            handler.as_ref(),
463                            &config,
464                            &writer_tx,
465                            active_limit,
466                        )
467                        .await
468                        {
469                            fatal = Some(error);
470                            break;
471                        }
472                        continue;
473                    }
474                    if request.method != rpc::INITIALIZE {
475                        let response = Response::error(
476                            Some(request.id),
477                            RpcError::new(ErrorKind::InvalidRequest),
478                        );
479                        let _ = commit(&writer_tx, response, active_limit).await;
480                        break;
481                    }
482                    let selected = match initialize(
483                        &request,
484                        handler.as_ref(),
485                        &config,
486                        &writer_tx,
487                        &peer,
488                        &mut reader,
489                    )
490                    .await
491                    {
492                        Ok(selected) => selected,
493                        Err(error) => {
494                            fatal = Some(error);
495                            break;
496                        }
497                    };
498                    let Some((limits, capabilities)) = selected else {
499                        break;
500                    };
501                    peer.inner
502                        .limit
503                        .store(limits.max_frame_bytes, Ordering::Release);
504                    *peer
505                        .inner
506                        .semaphore
507                        .write()
508                        .unwrap_or_else(|poisoned| poisoned.into_inner()) =
509                        Arc::new(Semaphore::new(limits.max_in_flight));
510                    active_limit = limits.max_frame_bytes;
511                    semaphore = Arc::new(Semaphore::new(limits.max_in_flight));
512                    advertised_capabilities = capabilities.into_iter().collect();
513                    initialized = true;
514                    continue;
515                }
516
517                if request.method == rpc::INITIALIZE {
518                    let response =
519                        Response::error(Some(request.id), RpcError::new(ErrorKind::InvalidRequest));
520                    let _ = commit(&writer_tx, response, active_limit).await;
521                    break;
522                }
523
524                if request.method == rpc::DISCOVER {
525                    if let Err(error) = discover(
526                        &request,
527                        handler.as_ref(),
528                        &config,
529                        &writer_tx,
530                        active_limit,
531                    )
532                    .await
533                    {
534                        fatal = Some(error);
535                        break;
536                    }
537                    continue;
538                }
539
540                if shutdown_request.is_some() {
541                    let response = Response::error(Some(request.id), RpcError::unavailable(None));
542                    let _ = commit(&writer_tx, response, active_limit).await;
543                    continue;
544                }
545
546                if request.method == rpc::SHUTDOWN {
547                    if serde_json::from_value::<EmptyParams>(request.params.clone()).is_err() {
548                        let response = Response::error(
549                            Some(request.id),
550                            RpcError::new(ErrorKind::InvalidParams),
551                        );
552                        let _ = commit(&writer_tx, response, active_limit).await;
553                        break;
554                    }
555                    shutdown_request = Some((request.id, request_deadline(&request)));
556                    continue;
557                }
558
559                if !request.method.starts_with("resolver.")
560                    && !request.method.starts_with("provider.")
561                {
562                    let response =
563                        Response::error(Some(request.id), RpcError::new(ErrorKind::MethodNotFound));
564                    let _ = commit(&writer_tx, response, active_limit).await;
565                    continue;
566                }
567                if !advertised_capabilities.contains(&request.method) {
568                    let response = Response::error(
569                        Some(request.id),
570                        RpcError::new(ErrorKind::CapabilityRequired),
571                    );
572                    let _ = commit(&writer_tx, response, active_limit).await;
573                    continue;
574                }
575
576                let deadline = request_deadline(&request);
577                if deadline <= Instant::now() {
578                    let response = Response::error(
579                        Some(request.id),
580                        RpcError::new(ErrorKind::DeadlineExceeded),
581                    );
582                    let _ = commit(&writer_tx, response, active_limit).await;
583                    continue;
584                }
585
586                let permit = match semaphore.clone().try_acquire_owned() {
587                    Ok(permit) => permit,
588                    Err(_) => {
589                        let response =
590                            Response::error(Some(request.id), RpcError::unavailable(None));
591                        let _ = commit(&writer_tx, response, active_limit).await;
592                        continue;
593                    }
594                };
595
596                let cancellation = CancellationToken::new();
597                inflight
598                    .lock()
599                    .await
600                    .insert(request.id, cancellation.clone());
601                let context = RequestContext {
602                    request_id: request.id,
603                    deadline,
604                    cancellation: cancellation.clone(),
605                    peer: peer.clone(),
606                };
607                let task_handler = handler.clone();
608                let task_writer = writer_tx.clone();
609                let task_inflight = inflight.clone();
610                let task_disconnected = disconnected.clone();
611                tasks.spawn(async move {
612                    let _permit = permit;
613                    run_call(
614                        request,
615                        context,
616                        task_handler,
617                        task_writer,
618                        task_inflight,
619                        task_disconnected,
620                        active_limit,
621                    )
622                    .await;
623                });
624            }
625        }
626    }
627
628    // Draining preserves accepted work. Only an expired shutdown deadline
629    // turns this into cancellation; EOF remains the immediate-abort path.
630    // A callback still waiting on a client that is gone would otherwise hold
631    // its handler, and therefore its request, until the deadline.
632    peer.fail_all().await;
633    for cancellation in inflight.lock().await.values() {
634        cancellation.cancel();
635    }
636    tasks.abort_all();
637    while tasks.join_next().await.is_some() {}
638    if initialized {
639        // Resource cleanup must run even when the drain deadline expired.
640        let _ = tokio::time::timeout(config.startup_timeout, handler.shutdown()).await;
641    }
642    if drained && let Some((id, deadline)) = shutdown_request {
643        let _ = tokio::time::timeout_at(
644            deadline,
645            commit(&writer_tx, Response::success(id, json!({})), active_limit),
646        )
647        .await;
648    }
649    drop(writer_tx);
650    if tokio::time::timeout(config.startup_timeout, &mut writer_task)
651        .await
652        .is_err()
653    {
654        writer_task.abort();
655        let _ = writer_task.await;
656    }
657    match fatal {
658        Some(error) => Err(error),
659        None => Ok(()),
660    }
661}
662
663async fn discover<H: ApplicationHandler>(
664    request: &Request,
665    handler: &H,
666    config: &ServerConfig,
667    writer: &mpsc::Sender<WriterCommand>,
668    limit: usize,
669) -> Result<()> {
670    let _: EmptyParams = match serde_json::from_value(request.params.clone()) {
671        Ok(params) => params,
672        Err(_) => {
673            return commit(
674                writer,
675                Response::error(Some(request.id), RpcError::new(ErrorKind::InvalidParams)),
676                limit,
677            )
678            .await;
679        }
680    };
681    if request_deadline(request) <= Instant::now() {
682        return commit(
683            writer,
684            Response::error(Some(request.id), RpcError::new(ErrorKind::DeadlineExceeded)),
685            limit,
686        )
687        .await;
688    }
689
690    let methods = handler.capabilities();
691    if crate::protocol::validate_capabilities(&methods).is_err()
692        || handler.validate_capabilities(&methods).is_err()
693    {
694        return commit(
695            writer,
696            Response::error(Some(request.id), RpcError::new(ErrorKind::Internal)),
697            limit,
698        )
699        .await;
700    }
701    let document = crate::description::openrpc(
702        handler.protocol(),
703        handler.versions(),
704        &config.product,
705        &methods,
706    )?;
707    commit(writer, Response::success(request.id, document), limit).await
708}
709
710async fn initialize<R, H: ApplicationHandler>(
711    request: &Request,
712    handler: &H,
713    config: &ServerConfig,
714    writer: &mpsc::Sender<WriterCommand>,
715    peer: &Peer,
716    reader: &mut AsyncFrameReader<R>,
717) -> Result<Option<(Limits, Vec<String>)>>
718where
719    R: AsyncRead + Unpin,
720{
721    let params: InitializeParams<Value> = match serde_json::from_value(request.params.clone()) {
722        Ok(params) => params,
723        Err(_) => {
724            let response =
725                Response::error(Some(request.id), RpcError::new(ErrorKind::InvalidParams));
726            commit(writer, response, ABSOLUTE_MAX_FRAME_BYTES).await?;
727            return Ok(None);
728        }
729    };
730    if params.protocol != handler.protocol() {
731        let response = Response::error(
732            Some(request.id),
733            RpcError::new(ErrorKind::UnsupportedVersion),
734        );
735        commit(writer, response, ABSOLUTE_MAX_FRAME_BYTES).await?;
736        return Ok(None);
737    }
738    if params.validate_common(handler.protocol()).is_err() {
739        let response = Response::error(Some(request.id), RpcError::new(ErrorKind::InvalidParams));
740        commit(writer, response, ABSOLUTE_MAX_FRAME_BYTES).await?;
741        return Ok(None);
742    }
743    let Some(version) = params
744        .versions
745        .iter()
746        .copied()
747        .filter(|version| handler.versions().contains(version))
748        .max()
749    else {
750        let response = Response::error(
751            Some(request.id),
752            RpcError::new(ErrorKind::UnsupportedVersion),
753        );
754        commit(writer, response, ABSOLUTE_MAX_FRAME_BYTES).await?;
755        return Ok(None);
756    };
757
758    let capabilities = handler.capabilities();
759    if crate::protocol::validate_capabilities(&capabilities).is_err() {
760        let response = Response::error(Some(request.id), RpcError::new(ErrorKind::Internal));
761        commit(writer, response, ABSOLUTE_MAX_FRAME_BYTES).await?;
762        return Ok(None);
763    }
764    if let Err(error) = handler.validate_capabilities(&capabilities) {
765        commit(
766            writer,
767            Response::error(Some(request.id), error),
768            ABSOLUTE_MAX_FRAME_BYTES,
769        )
770        .await?;
771        return Ok(None);
772    }
773    let limits = config.limits.select(params.limits)?;
774    // Recorded before the application handler runs, so an initialize handler
775    // that needs to ask the client something can already see what it answers.
776    // Callbacks the client did not advertise are simply never sent.
777    *peer
778        .inner
779        .capabilities
780        .write()
781        .unwrap_or_else(|poisoned| poisoned.into_inner()) =
782        params.client_methods.iter().cloned().collect();
783    let cancellation = CancellationToken::new();
784    let context = RequestContext {
785        request_id: request.id,
786        deadline: request_deadline(request).min(Instant::now() + config.startup_timeout),
787        cancellation,
788        peer: peer.clone(),
789    };
790    if context.deadline <= Instant::now() {
791        commit(
792            writer,
793            Response::error(Some(request.id), RpcError::new(ErrorKind::DeadlineExceeded)),
794            ABSOLUTE_MAX_FRAME_BYTES,
795        )
796        .await?;
797        return Ok(None);
798    }
799    // Initialization may itself call the client (for example to request a
800    // provider credential). Keep consuming that one transport while the
801    // application future is pending; otherwise the callback response sits in
802    // the pipe behind a read loop that cannot resume until initialization has
803    // completed, producing a deadline deadlock.
804    let mut operation = Box::pin(handler.initialize(&context, params.application));
805    let application = loop {
806        let outcome = tokio::select! {
807            biased;
808            _ = tokio::time::sleep_until(context.deadline) => {
809                context.cancellation.cancel();
810                commit(
811                    writer,
812                    Response::error(
813                        Some(request.id),
814                        RpcError::new(ErrorKind::DeadlineExceeded),
815                    ),
816                    ABSOLUTE_MAX_FRAME_BYTES,
817                )
818                .await?;
819                return Ok(None);
820            }
821            _ = context.cancellation.cancelled() => {
822                commit(
823                    writer,
824                    Response::error(Some(request.id), RpcError::new(ErrorKind::Cancelled)),
825                    ABSOLUTE_MAX_FRAME_BYTES,
826                )
827                .await?;
828                return Ok(None);
829            }
830            outcome = &mut operation => Some(outcome),
831            frame = reader.read_frame(ABSOLUTE_MAX_FRAME_BYTES) => {
832                let Some(frame) = frame? else {
833                    context.cancellation.cancel();
834                    return Err(Error::Closed);
835                };
836                match Envelope::parse_classified(&frame) {
837                    Ok(Envelope::Response(response)) => {
838                        if !peer.deliver(response).await {
839                            context.cancellation.cancel();
840                            return Err(Error::Protocol(
841                                "unmatched callback response during initialization",
842                            ));
843                        }
844                        None
845                    }
846                    Ok(Envelope::Notification(notification)) => {
847                        if notification.method == rpc::CANCEL
848                            && serde_json::from_value::<CancelParams>(notification.params)
849                                .is_ok_and(|params| params.id == request.id)
850                        {
851                            context.cancellation.cancel();
852                        }
853                        None
854                    }
855                    Ok(Envelope::Request(invalid)) => {
856                        context.cancellation.cancel();
857                        commit(
858                            writer,
859                            Response::error(
860                                Some(invalid.id),
861                                RpcError::new(ErrorKind::InvalidRequest),
862                            ),
863                            ABSOLUTE_MAX_FRAME_BYTES,
864                        )
865                        .await?;
866                        return Ok(None);
867                    }
868                    Err((_, kind)) => {
869                        context.cancellation.cancel();
870                        commit(
871                            writer,
872                            Response::error(None, RpcError::new(kind)),
873                            ABSOLUTE_MAX_FRAME_BYTES,
874                        )
875                        .await?;
876                        return Ok(None);
877                    }
878                }
879            }
880        };
881        let Some(outcome) = outcome else { continue };
882        break match outcome {
883            Ok(application) => application,
884            Err(error) => {
885                // Stop every initialize-time callback before making its
886                // parent terminal on the wire.
887                context.cancellation.cancel();
888                commit(
889                    writer,
890                    Response::error(Some(request.id), error),
891                    ABSOLUTE_MAX_FRAME_BYTES,
892                )
893                .await?;
894                return Ok(None);
895            }
896        };
897    };
898    let result = InitializeResult {
899        protocol: handler.protocol().to_string(),
900        version,
901        server: config.product.clone(),
902        methods: capabilities.clone(),
903        capabilities: Default::default(),
904        limits,
905        application,
906    };
907    let value = match serde_json::to_value(result) {
908        Ok(value) => value,
909        Err(_) => {
910            context.cancellation.cancel();
911            return Err(Error::Protocol("serialize initialize result"));
912        }
913    };
914    // Negotiated limits take effect only after this response commits. Cancel
915    // initialize-time callbacks first so none can cross that boundary.
916    context.cancellation.cancel();
917    commit(
918        writer,
919        Response::success(request.id, value),
920        ABSOLUTE_MAX_FRAME_BYTES,
921    )
922    .await?;
923    Ok(Some((limits, capabilities)))
924}
925
926async fn handle_notification(
927    notification: Notification,
928    inflight: &Arc<Mutex<HashMap<RequestId, CancellationToken>>>,
929) {
930    if notification.method != rpc::CANCEL {
931        return;
932    }
933    let Ok(params) = serde_json::from_value::<CancelParams>(notification.params) else {
934        return;
935    };
936    if let Some(cancellation) = inflight.lock().await.get(&params.id) {
937        cancellation.cancel();
938    }
939}
940
941async fn run_call<H: ApplicationHandler>(
942    request: Request,
943    context: RequestContext,
944    handler: Arc<H>,
945    writer: mpsc::Sender<WriterCommand>,
946    inflight: Arc<Mutex<HashMap<RequestId, CancellationToken>>>,
947    disconnected: CancellationToken,
948    limit: usize,
949) {
950    let mut operation = Box::pin(handler.call(context.clone(), &request.method, request.params));
951    let response = tokio::select! {
952        biased;
953        _ = tokio::time::sleep_until(context.deadline) => {
954            context.cancellation.cancel();
955            let response = Response::error(Some(request.id), RpcError::new(ErrorKind::DeadlineExceeded));
956            let _ = send_terminal(&writer, response, limit).await;
957            let _ = operation.await;
958            handler.request_finished(request.id, false).await;
959            inflight.lock().await.remove(&request.id);
960            return;
961        }
962        _ = context.cancellation.cancelled() => {
963            let response = Response::error(Some(request.id), RpcError::new(ErrorKind::Cancelled));
964            let _ = send_terminal(&writer, response, limit).await;
965            // Keep the semaphore permit until a non-cooperative handler really
966            // exits. A late outcome is deliberately discarded.
967            let _ = operation.await;
968            handler.request_finished(request.id, false).await;
969            inflight.lock().await.remove(&request.id);
970            return;
971        }
972        result = &mut operation => response_from_result(request.id, result),
973    };
974    let committed = commit_application_before(
975        &writer,
976        response,
977        limit,
978        context.deadline,
979        &context.cancellation,
980        &disconnected,
981    )
982    .await;
983    handler.request_finished(request.id, committed).await;
984    inflight.lock().await.remove(&request.id);
985}
986
987/// Deadline and cancellation are themselves terminal outcomes. The channel is
988/// sized above the negotiated in-flight count, so enqueueing one response per
989/// accepted request remains bounded even when the transport is backpressured.
990async fn send_terminal(
991    writer: &mpsc::Sender<WriterCommand>,
992    response: Response,
993    limit: usize,
994) -> Result<()> {
995    let payload = encode_response(&response, limit)?;
996    writer
997        .send(WriterCommand {
998            payload,
999            limit,
1000            committed: None,
1001        })
1002        .await
1003        .map_err(|_| Error::Closed)
1004}
1005
1006/// Serialize a response, substituting `message_too_large` for one that cannot
1007/// fit. A result carrying a secret must never be truncated onto the wire, and
1008/// the caller is owed exactly one terminal frame either way.
1009fn encode_response(response: &Response, limit: usize) -> Result<Zeroizing<Vec<u8>>> {
1010    let payload = serde_json::to_vec(response)
1011        .map_err(|_| Error::Protocol("failed to serialize response"))?;
1012    if !payload.is_empty() && payload.len() <= limit {
1013        return Ok(Zeroizing::new(payload));
1014    }
1015    drop(Zeroizing::new(payload));
1016    let replacement = Response::error(response.id(), RpcError::new(ErrorKind::MessageTooLarge));
1017    serde_json::to_vec(&replacement)
1018        .map(Zeroizing::new)
1019        .map_err(|_| Error::Protocol("failed to serialize response"))
1020}
1021
1022fn response_from_result(id: RequestId, result: RpcResult<Value>) -> Response {
1023    match result {
1024        Ok(value) => Response::success(id, value),
1025        Err(error) => Response::error(Some(id), error),
1026    }
1027}
1028
1029async fn commit_application_before(
1030    writer: &mpsc::Sender<WriterCommand>,
1031    response: Response,
1032    limit: usize,
1033    deadline: Instant,
1034    cancellation: &CancellationToken,
1035    disconnected: &CancellationToken,
1036) -> bool {
1037    let encoded = serde_json::to_vec(&response).map(Zeroizing::new).ok();
1038    let application_response = encoded
1039        .as_ref()
1040        .is_some_and(|payload| !payload.is_empty() && payload.len() <= limit);
1041    let request_id = response.id();
1042    let payload = match encoded.filter(|_| application_response) {
1043        Some(payload) => payload,
1044        None => {
1045            let replacement =
1046                Response::error(request_id, RpcError::new(ErrorKind::MessageTooLarge));
1047            match serde_json::to_vec(&replacement).map(Zeroizing::new) {
1048                Ok(payload) => payload,
1049                Err(_) => return false,
1050            }
1051        }
1052    };
1053    let (committed_tx, committed_rx) = oneshot::channel();
1054    let command = WriterCommand {
1055        payload,
1056        limit,
1057        committed: Some(committed_tx),
1058    };
1059
1060    // Only cancellation/deadline that wins before enqueue may replace the
1061    // application response. Once the writer owns a frame, it is the sole
1062    // terminal outcome; a later write timeout closes the session instead of
1063    // queueing a second response.
1064    let enqueued = tokio::select! {
1065        biased;
1066        _ = tokio::time::sleep_until(deadline) => {
1067            cancellation.cancel();
1068            let response = Response::error(request_id, RpcError::new(ErrorKind::DeadlineExceeded));
1069            let _ = send_terminal(writer, response, limit).await;
1070            false
1071        }
1072        _ = cancellation.cancelled() => {
1073            let response = Response::error(request_id, RpcError::new(ErrorKind::Cancelled));
1074            let _ = send_terminal(writer, response, limit).await;
1075            false
1076        }
1077        result = writer.send(command) => result.is_ok(),
1078    };
1079    if !enqueued {
1080        return false;
1081    }
1082
1083    // The terminal response now precedes any later writer command. End the
1084    // child callback lifetime before waiting for the bytes to commit so no
1085    // detached callback can be queued behind its parent response.
1086    cancellation.cancel();
1087
1088    match tokio::time::timeout_at(deadline, committed_rx).await {
1089        Ok(Ok(Ok(()))) => application_response,
1090        Ok(Ok(Err(()))) | Ok(Err(_)) | Err(_) => {
1091            disconnected.cancel();
1092            false
1093        }
1094    }
1095}
1096
1097async fn commit(
1098    writer: &mpsc::Sender<WriterCommand>,
1099    response: Response,
1100    limit: usize,
1101) -> Result<()> {
1102    let payload = encode_response(&response, limit)?;
1103    tokio::time::timeout(COMMIT_TIMEOUT, async {
1104        let (committed_tx, committed_rx) = oneshot::channel();
1105        writer
1106            .send(WriterCommand {
1107                payload,
1108                limit,
1109                committed: Some(committed_tx),
1110            })
1111            .await
1112            .map_err(|_| Error::Closed)?;
1113        committed_rx
1114            .await
1115            .map_err(|_| Error::Closed)?
1116            .map_err(|_| Error::Closed)
1117    })
1118    .await
1119    .map_err(|_| Error::DeadlineExceeded)?
1120}
1121
1122fn request_deadline(request: &Request) -> Instant {
1123    instant_from_unix_ms(request.deadline_unix_ms())
1124}
1125
1126#[cfg(test)]
1127mod tests {
1128    use super::*;
1129
1130    /// The session ends by dropping its sender so the writer task observes the
1131    /// close and the process can exit. Holding a strong clone in the peer made
1132    /// every session linger until the shutdown timeout instead, which showed up
1133    /// as an endpoint that never exited after EOF.
1134    #[tokio::test]
1135    async fn a_peer_does_not_keep_the_writer_channel_open() {
1136        let (writer_tx, mut writer_rx) = mpsc::channel::<WriterCommand>(1);
1137        let peer = Peer::new(&writer_tx);
1138        peer.inner
1139            .capabilities
1140            .write()
1141            .unwrap()
1142            .insert("client.prompt".to_string());
1143        drop(writer_tx);
1144        assert!(
1145            writer_rx.recv().await.is_none(),
1146            "the peer kept the writer channel open after the session dropped its sender"
1147        );
1148
1149        // A callback attempted on a session that is already ending is reported
1150        // as unavailable rather than waiting for a writer that will never run.
1151        let context = RequestContext {
1152            request_id: RequestId::new(1).unwrap(),
1153            deadline: Instant::now() + Duration::from_secs(30),
1154            cancellation: CancellationToken::new(),
1155            peer: peer.clone(),
1156        };
1157        let error = peer
1158            .call::<_, Value>("client.prompt", &json!({}), &context)
1159            .await
1160            .unwrap_err();
1161        assert_eq!(error.data.kind, ErrorKind::Unavailable);
1162    }
1163
1164    #[tokio::test]
1165    async fn callbacks_obey_the_pre_negotiation_outbound_limit() {
1166        let (writer_tx, mut writer_rx) = mpsc::channel::<WriterCommand>(4);
1167        let peer = Peer::new(&writer_tx);
1168        peer.inner
1169            .capabilities
1170            .write()
1171            .unwrap()
1172            .insert("client.prompt".to_string());
1173        let cancellation = CancellationToken::new();
1174        let context = RequestContext {
1175            request_id: RequestId::new(1).unwrap(),
1176            deadline: Instant::now() + Duration::from_secs(2),
1177            cancellation: cancellation.clone(),
1178            peer: peer.clone(),
1179        };
1180
1181        let first_peer = peer.clone();
1182        let first_context = context.clone();
1183        let first = tokio::spawn(async move {
1184            first_peer
1185                .call::<_, Value>("client.prompt", &json!({}), &first_context)
1186                .await
1187        });
1188        let _first_frame = writer_rx.recv().await.expect("first callback frame");
1189
1190        let second = peer
1191            .call::<_, Value>("client.prompt", &json!({}), &context)
1192            .await;
1193        assert!(matches!(
1194            second,
1195            Err(ref error) if error.data.kind == ErrorKind::Unavailable
1196        ));
1197        assert!(writer_rx.try_recv().is_err(), "a second callback was sent");
1198
1199        cancellation.cancel();
1200        assert!(matches!(
1201            first.await.unwrap(),
1202            Err(ref error) if error.data.kind == ErrorKind::Cancelled
1203        ));
1204    }
1205
1206    #[tokio::test]
1207    async fn pre_negotiation_callbacks_use_the_absolute_frame_limit() {
1208        let (writer_tx, mut writer_rx) = mpsc::channel::<WriterCommand>(2);
1209        let peer = Peer::new(&writer_tx);
1210        peer.inner
1211            .capabilities
1212            .write()
1213            .unwrap()
1214            .insert("client.prompt".to_string());
1215        let context = RequestContext {
1216            request_id: RequestId::new(1).unwrap(),
1217            deadline: Instant::now() + Duration::from_secs(2),
1218            cancellation: CancellationToken::new(),
1219            peer: peer.clone(),
1220        };
1221
1222        let outcome = peer
1223            .call::<_, Value>(
1224                "client.prompt",
1225                &json!({"value": "x".repeat(ABSOLUTE_MAX_FRAME_BYTES)}),
1226                &context,
1227            )
1228            .await;
1229        assert!(matches!(
1230            outcome,
1231            Err(ref error) if error.data.kind == ErrorKind::MessageTooLarge
1232        ));
1233        assert!(writer_rx.try_recv().is_err(), "oversized callback was sent");
1234    }
1235
1236    #[tokio::test]
1237    async fn callbacks_switch_to_the_negotiated_outbound_limit() {
1238        let (writer_tx, mut writer_rx) = mpsc::channel::<WriterCommand>(6);
1239        let peer = Peer::new(&writer_tx);
1240        peer.inner
1241            .capabilities
1242            .write()
1243            .unwrap()
1244            .insert("client.prompt".to_string());
1245        *peer.inner.semaphore.write().unwrap() = Arc::new(Semaphore::new(2));
1246        peer.inner.limit.store(32 * 1024, Ordering::Release);
1247        let cancellation = CancellationToken::new();
1248        let context = RequestContext {
1249            request_id: RequestId::new(1).unwrap(),
1250            deadline: Instant::now() + Duration::from_secs(2),
1251            cancellation: cancellation.clone(),
1252            peer: peer.clone(),
1253        };
1254
1255        let mut calls = Vec::new();
1256        for _ in 0..2 {
1257            let peer = peer.clone();
1258            let context = context.clone();
1259            calls.push(tokio::spawn(async move {
1260                peer.call::<_, Value>("client.prompt", &json!({}), &context)
1261                    .await
1262            }));
1263            writer_rx.recv().await.expect("negotiated callback frame");
1264        }
1265        let third = peer
1266            .call::<_, Value>("client.prompt", &json!({}), &context)
1267            .await;
1268        assert!(matches!(
1269            third,
1270            Err(ref error) if error.data.kind == ErrorKind::Unavailable
1271        ));
1272
1273        cancellation.cancel();
1274        for call in calls {
1275            assert!(matches!(
1276                call.await.unwrap(),
1277                Err(ref error) if error.data.kind == ErrorKind::Cancelled
1278            ));
1279        }
1280    }
1281
1282    #[tokio::test]
1283    async fn a_callback_cannot_start_after_its_parent_is_terminal() {
1284        let (writer_tx, mut writer_rx) = mpsc::channel::<WriterCommand>(2);
1285        let peer = Peer::new(&writer_tx);
1286        peer.inner
1287            .capabilities
1288            .write()
1289            .unwrap()
1290            .insert("client.prompt".to_string());
1291        let cancellation = CancellationToken::new();
1292        cancellation.cancel();
1293        let context = RequestContext {
1294            request_id: RequestId::new(1).unwrap(),
1295            deadline: Instant::now() + Duration::from_secs(2),
1296            cancellation,
1297            peer: peer.clone(),
1298        };
1299
1300        let outcome = peer
1301            .call::<_, Value>("client.prompt", &json!({}), &context)
1302            .await;
1303        assert!(matches!(
1304            outcome,
1305            Err(ref error) if error.data.kind == ErrorKind::Cancelled
1306        ));
1307        assert!(writer_rx.try_recv().is_err(), "late callback was sent");
1308    }
1309
1310    // Most dispatcher behavior is exercised through the black-box integration
1311    // tests; keep this module focused on wall-to-monotonic conversion.
1312    #[test]
1313    fn expired_deadline_is_not_started() {
1314        let request = Request::new(RequestId::new(1).unwrap(), "test.call", 1, json!({})).unwrap();
1315        assert!(request_deadline(&request) <= Instant::now());
1316    }
1317
1318    #[tokio::test]
1319    async fn an_expired_callback_consumes_its_terminal_response() {
1320        let (writer_tx, mut writer_rx) = mpsc::channel::<WriterCommand>(1);
1321        let peer = Peer::new(&writer_tx);
1322        peer.inner
1323            .capabilities
1324            .write()
1325            .unwrap()
1326            .insert("client.prompt".to_string());
1327
1328        // More than one in-flight window proves each consumed response retires
1329        // its abandoned marker instead of slowly filling the bounded set.
1330        for raw_id in 1..=64 {
1331            let context = RequestContext {
1332                request_id: RequestId::new(10).unwrap(),
1333                deadline: Instant::now(),
1334                cancellation: CancellationToken::new(),
1335                peer: peer.clone(),
1336            };
1337            let error = peer
1338                .call::<_, Value>("client.prompt", &json!({}), &context)
1339                .await
1340                .unwrap_err();
1341            assert_eq!(error.data.kind, ErrorKind::DeadlineExceeded);
1342            let _request = writer_rx.recv().await.unwrap();
1343
1344            let id = RequestId::new(raw_id).unwrap();
1345            let terminal = Response::error(Some(id), RpcError::new(ErrorKind::DeadlineExceeded));
1346            assert!(peer.deliver(terminal.clone()).await);
1347            assert!(!peer.deliver(terminal).await, "duplicates remain invalid");
1348        }
1349        assert!(peer.inner.calls.lock().await.abandoned.is_empty());
1350    }
1351
1352    #[tokio::test]
1353    async fn an_abandoned_callback_keeps_its_slot_until_the_client_answers() {
1354        let (writer_tx, mut writer_rx) = mpsc::channel::<WriterCommand>(4);
1355        let peer = Peer::new(&writer_tx);
1356        peer.inner
1357            .capabilities
1358            .write()
1359            .unwrap()
1360            .insert("client.prompt".to_string());
1361        let context = |deadline| RequestContext {
1362            request_id: RequestId::new(10).unwrap(),
1363            deadline,
1364            cancellation: CancellationToken::new(),
1365            peer: peer.clone(),
1366        };
1367
1368        let error = peer
1369            .call::<_, Value>("client.prompt", &json!({}), &context(Instant::now()))
1370            .await
1371            .unwrap_err();
1372        assert_eq!(error.data.kind, ErrorKind::DeadlineExceeded);
1373        let _request = writer_rx.recv().await.unwrap();
1374
1375        // The client still counts callback 1 as active, so the single slot
1376        // stays taken rather than letting a second callback violate its limit.
1377        let error = peer
1378            .call::<_, Value>(
1379                "client.prompt",
1380                &json!({}),
1381                &context(Instant::now() + Duration::from_secs(300)),
1382            )
1383            .await
1384            .unwrap_err();
1385        assert_eq!(error.data.kind, ErrorKind::Unavailable);
1386
1387        let terminal = Response::error(
1388            Some(RequestId::new(1).unwrap()),
1389            RpcError::new(ErrorKind::DeadlineExceeded),
1390        );
1391        assert!(peer.deliver(terminal).await);
1392        let error = peer
1393            .call::<_, Value>("client.prompt", &json!({}), &context(Instant::now()))
1394            .await
1395            .unwrap_err();
1396        assert_eq!(error.data.kind, ErrorKind::DeadlineExceeded);
1397        let _request = writer_rx.recv().await.unwrap();
1398    }
1399
1400    #[tokio::test]
1401    async fn a_cancelled_callback_also_consumes_its_terminal_response() {
1402        let (writer_tx, mut writer_rx) = mpsc::channel::<WriterCommand>(1);
1403        let peer = Peer::new(&writer_tx);
1404        peer.inner
1405            .capabilities
1406            .write()
1407            .unwrap()
1408            .insert("client.prompt".to_string());
1409        let cancellation = CancellationToken::new();
1410        let context = RequestContext {
1411            request_id: RequestId::new(10).unwrap(),
1412            deadline: Instant::now() + Duration::from_secs(1),
1413            cancellation: cancellation.clone(),
1414            peer: peer.clone(),
1415        };
1416
1417        let call_peer = peer.clone();
1418        let call = tokio::spawn(async move {
1419            call_peer
1420                .call::<_, Value>("client.prompt", &json!({}), &context)
1421                .await
1422        });
1423        let _request = writer_rx.recv().await.unwrap();
1424        cancellation.cancel();
1425        let error = call.await.unwrap().unwrap_err();
1426        assert_eq!(error.data.kind, ErrorKind::Cancelled);
1427        let terminal = Response::error(
1428            Some(RequestId::new(1).unwrap()),
1429            RpcError::new(ErrorKind::Cancelled),
1430        );
1431        assert!(peer.deliver(terminal).await);
1432        assert!(peer.inner.calls.lock().await.abandoned.is_empty());
1433    }
1434}