Skip to main content

lspf/
engine.rs

1//! Connection-owned protocol engine for `Server<S>`.
2//!
3//! This slice serves a connection end to end for the lifecycle plus typed
4//! custom requests, notifications, and commands. `initialize` is the one
5//! bounded transaction that can conditionally extend the Router, freeze it,
6//! generate capabilities, establish the connection's [`Workspace`],
7//! [`Documents`], and negotiated position encoding, and run the
8//! `on_initialize` lifecycle hook — all without exposing partial state
9//! (ADR 0017, ADR 0018). The later lifecycle notifications carry the remaining
10//! hooks: the client's `initialized` runs `on_initialized` once, in the
11//! running state only, a successful `on_shutdown` gates the transition into
12//! shutting down, and the peer's `exit` runs `on_exit` before the engine
13//! computes the exit outcome (ADR 0024) — the exit hook resolves to `()`, so it
14//! cannot change the exit code the lifecycle implies. Inbound
15//! requests reserve their IDs before user work is spawned; the engine's atomic
16//! completion gate then arbitrates success, errors, and cancellation.
17//!
18//! Every way a connection can end — reader EOF, a reader error, a writer send
19//! or shutdown failure, `exit`, and the fatal termination a failed initialize
20//! transaction takes — requests the same idempotent close operation. The first
21//! requester records the [`CloseCause`] and wakes the read-loop; the engine
22//! then performs the cleanup exactly once and reports the recorded cause as an
23//! [`Outcome`] or a transport [`Error`]. The engine never terminates the
24//! process; the entry point decides what an [`Outcome`] means for a binary.
25
26use std::sync::Arc;
27use std::time::Duration;
28
29use bytes::Bytes;
30#[cfg(all(test, not(target_arch = "wasm32")))]
31use futures_channel::mpsc::UnboundedReceiver;
32use gen_lsp_types::{
33    DidChangeConfigurationParams, DidChangeNotebookDocumentParams, DidChangeTextDocumentParams,
34    DidChangeWorkspaceFoldersParams, DidCloseNotebookDocumentParams, DidCloseTextDocumentParams,
35    DidOpenNotebookDocumentParams, DidOpenTextDocumentParams, DidSaveNotebookDocumentParams,
36    DidSaveTextDocumentParams, InitializeParams, InitializedParams, NotebookDocumentCellChanges,
37    ProgressToken, Save, ServerInfo, SetTraceParams, TextDocumentItem, TextDocumentSyncKind,
38    TextDocumentSyncOptions, TraceValue, Uri, WillSaveTextDocumentParams,
39    WorkDoneProgressCancelParams, WorkspaceFoldersServerCapabilities,
40};
41use serde::Serialize;
42use tokio_util::sync::CancellationToken;
43use tracing::{Instrument, Span, debug, warn};
44
45use crate::builder::{
46    ConfigureInitialize, InitializeRegistrar, OnExit, OnInitialize, OnInitialized, OnShutdown,
47    ProtocolNotification, Registrations, Server,
48};
49use crate::capability::GeneratedCapabilities;
50use crate::client::ClientHandle;
51use crate::codec::{decode_params, decode_value, encode_body, request_token};
52use crate::context::ServerContext;
53use crate::documents::{Document, DocumentMutationError, Documents};
54use crate::error::Error;
55use crate::failure::{ConnectionDirection, ConnectionFailureCategory, FailureReporter};
56use crate::file_provider::SharedFileProvider;
57use crate::notebooks::{Notebook, Notebooks};
58use crate::progress::{ProgressCancel, ProgressRegistry};
59use crate::raw::{RawMessage, RequestId};
60use crate::runtime::{Runtime, default_runtime, ensure_runtime_available};
61use crate::service::{
62    HandlerTimeout, IncomingCall, ServiceResult, UserLayer, UserService, build_service_stack,
63};
64#[cfg(all(test, not(target_arch = "wasm32")))]
65use crate::session::send_loop as drive_send_loop;
66#[cfg(all(test, not(target_arch = "wasm32")))]
67use crate::session::{CloseSignal, OutboundQueue, OutboundRegistry, enqueue_encoded};
68use crate::session::{
69    INBOUND_CAPACITY_EXHAUSTED, InboundReserveError, ProtocolSession, Reservation, SessionInput,
70    run_handler_with_deadline,
71};
72use crate::telemetry::{Completion, ConnectionTrace, Direction, Instant};
73#[cfg(all(test, not(target_arch = "wasm32")))]
74use crate::transport::TransportWriter;
75use crate::transport::{Transport, TransportError, TransportReader};
76use crate::workspace::Workspace;
77#[cfg(all(test, not(target_arch = "wasm32")))]
78use std::sync::Mutex;
79
80#[derive(Serialize)]
81#[serde(rename_all = "camelCase")]
82struct WireInitializeResult {
83    capabilities: GeneratedCapabilities,
84    #[serde(skip_serializing_if = "Option::is_none")]
85    server_info: Option<ServerInfo>,
86}
87
88use crate::{LspError, Result};
89
90fn validate_sync_changes(
91    kind: TextDocumentSyncKind,
92    changes: &[gen_lsp_types::TextDocumentContentChangeEvent],
93) -> std::result::Result<(), LspError> {
94    if kind == TextDocumentSyncKind::Incremental {
95        return Ok(());
96    }
97    if kind == TextDocumentSyncKind::Full {
98        return if changes.iter().all(|change| {
99            matches!(
100                change,
101                gen_lsp_types::TextDocumentContentChangeEvent::TextDocumentContentChangeWholeDocument(_)
102            )
103        }) {
104            Ok(())
105        } else {
106            Err(LspError::invalid_request(
107                "range changes require incremental document synchronization",
108            ))
109        };
110    }
111    Err(LspError::invalid_request(
112        "document changes are disabled by the configured synchronization kind",
113    ))
114}
115
116/// Decode the client's `initialized` notification params.
117///
118/// LSP 3.17 defines `InitializedParams` as an empty object, and clients send
119/// either `{}` or no params at all (JSON-RPC `null`). The `lsp-types` 0.97
120/// type is a unit struct, so its derived deserializer accepts only `null`;
121/// accepting the empty object too keeps the typed hook reachable from every
122/// real client.
123fn decode_initialized_params(raw: &Bytes) -> std::result::Result<InitializedParams, LspError> {
124    match serde_json::from_slice::<serde_json::Value>(raw) {
125        Ok(serde_json::Value::Null) => Ok(InitializedParams {}),
126        Ok(serde_json::Value::Object(map)) if map.is_empty() => Ok(InitializedParams {}),
127        _ => Err(LspError::invalid_params(
128            "initialized params must be an empty object",
129        )),
130    }
131}
132
133/// How one connection ended.
134///
135/// Serving a Server or Client connection resolves to exactly one `Outcome` or
136/// to a transport [`Error`]; it never terminates the process. A server binary
137/// maps its outcome to a process disposition itself — [`Outcome::code`]
138/// reports the exit code the LSP lifecycle implies.
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub enum Outcome {
141    /// The endpoint processed `exit`. For a Server, `code` is 0 when the peer
142    /// sent `shutdown` first and 1 otherwise; a Client permits local `exit`
143    /// only after its `shutdown` request succeeds and therefore reports 0.
144    Exit {
145        /// The process exit code prescribed by the LSP lifecycle.
146        code: i32,
147    },
148    /// The transport ended before `exit`, or a Client disconnected locally.
149    TransportClosed,
150    /// The outbound path failed terminally: the writer failed, or required
151    /// protocol traffic could not fit within the configured queue budgets.
152    WriterFailed,
153    /// A failed initialize transaction terminated the connection after its
154    /// fixed error response was enqueued (ADR 0018).
155    InitializeFailed,
156}
157
158impl Outcome {
159    /// The process exit code this outcome implies for a server binary: the
160    /// LSP-defined code after `exit`, and 1 for every ending without one.
161    pub fn code(&self) -> i32 {
162        match self {
163            Self::Exit { code } => *code,
164            Self::TransportClosed | Self::WriterFailed | Self::InitializeFailed => 1,
165        }
166    }
167}
168
169/// What first requested the engine's one close operation.
170///
171/// Only the first requester's cause is recorded, so a writer failure racing
172/// reader EOF still reports a single deterministic ending.
173#[derive(Debug)]
174enum CloseCause {
175    /// An `exit` notification was processed; carries the LSP exit code.
176    Exit { code: i32 },
177    /// The reader reached end of input before `exit`.
178    ReaderEof,
179    /// The reader failed with a transport error.
180    ReaderFailed(TransportError),
181    /// The writer failed to send or shut down, or required protocol traffic
182    /// could not fit within the outbound resource policy.
183    WriterFailed,
184    /// A failed initialize transaction terminated the connection (ADR 0018).
185    InitializeFailed,
186}
187
188impl CloseCause {
189    fn writer_failed() -> Self {
190        Self::WriterFailed
191    }
192
193    fn as_str(&self) -> &'static str {
194        match self {
195            Self::Exit { .. } => "exit",
196            Self::ReaderEof => "reader_eof",
197            Self::ReaderFailed(_) => "reader_failed",
198            Self::WriterFailed => "writer_failed",
199            Self::InitializeFailed => "initialize_failed",
200        }
201    }
202
203    /// Map the recorded cause onto what serving the connection returns.
204    fn into_result(self) -> Result<Outcome> {
205        match self {
206            Self::Exit { code } => Ok(Outcome::Exit { code }),
207            Self::ReaderEof => Ok(Outcome::TransportClosed),
208            Self::ReaderFailed(error) => Err(Error::Transport(error)),
209            Self::WriterFailed => Ok(Outcome::WriterFailed),
210            Self::InitializeFailed => Ok(Outcome::InitializeFailed),
211        }
212    }
213}
214
215/// Drive a [`Server`] over `transport` until the peer exits, the transport
216/// closes, a transport error ends the session, or a failed initialize
217/// transaction enters the terminal close path.
218pub(crate) async fn run<S, T>(server: Server<S>, transport: T) -> Result<Outcome>
219where
220    S: Send + Sync + 'static,
221    T: Transport,
222{
223    ensure_runtime_available()?;
224    let connection_trace = ConnectionTrace::new();
225    let failure_reporter = FailureReporter::new(server.error_hook.clone(), connection_trace.id());
226    let connection_span = connection_trace.span();
227    let (reader, writer) = transport.split();
228    let runtime = default_runtime();
229    let (protocol, client) = ProtocolSession::start(
230        runtime,
231        server.resource_policy,
232        writer,
233        connection_trace,
234        connection_span.clone(),
235        failure_reporter.clone(),
236        CloseCause::writer_failed,
237        ClientHandle::new,
238    );
239    ProtocolEngine::new(server, protocol, client, connection_trace, failure_reporter)
240        .serve(reader)
241        .instrument(connection_span)
242        .await
243}
244
245#[derive(serde::Deserialize)]
246struct CancelParams {
247    id: RequestId,
248}
249
250/// Whether a protocol built-in's post-validation hook runs.
251///
252/// Most built-ins gate their hook on the `Result` of
253/// [`ProtocolEngine::process_protocol_notification`]; the work-done progress
254/// cancel built-in reports its own non-error registry misses at debug level
255/// and signals malformed parameters through the gate directly instead.
256enum BuiltInGate {
257    /// Decode — and any mutation — succeeded: dispatch the registered hook.
258    RunHook,
259    /// Parameters violated the protocol contract: report and skip the hook.
260    ProtocolFailure,
261}
262
263enum BuiltInError {
264    Protocol(LspError),
265    Overload(LspError),
266}
267
268struct CellOpenRollback {
269    uri: Uri,
270    previous: Option<Document>,
271}
272
273impl From<LspError> for BuiltInError {
274    fn from(error: LspError) -> Self {
275        Self::Protocol(error)
276    }
277}
278
279impl From<DocumentMutationError> for BuiltInError {
280    fn from(error: DocumentMutationError) -> Self {
281        match error {
282            DocumentMutationError::Capacity(error) => Self::Overload(error),
283            DocumentMutationError::Protocol(error) => Self::Protocol(error),
284        }
285    }
286}
287
288/// Decode and apply one `window/workDoneProgress/cancel` notification against
289/// the connection's progress registry (ADR 0018).
290///
291/// A matching active and cancellable token fires the handle's cancellation
292/// token; cancellation never sends a work-done end by itself — the
293/// application decides the final message and calls `end`. Unknown, ended, and
294/// non-cancellable tokens, like malformed params, are logged at debug level
295/// and otherwise ignored, leaving the connection usable. The hook gate opens
296/// only after a successful decode, so a registered hook always observes the
297/// updated cancellation state.
298fn gate_progress_cancel(registry: &ProgressRegistry, raw_params: &Bytes) -> BuiltInGate {
299    let params = match decode_params::<WorkDoneProgressCancelParams>(raw_params) {
300        Ok(params) => params,
301        Err(error) => {
302            debug!(%error, "ignoring malformed window/workDoneProgress/cancel");
303            return BuiltInGate::ProtocolFailure;
304        }
305    };
306    match registry.cancel(&params.token) {
307        ProgressCancel::Cancelled => {}
308        ProgressCancel::NotCancellable => debug!(
309            token = ?params.token,
310            "ignoring work-done progress cancel for a non-cancellable token"
311        ),
312        ProgressCancel::NotActive => debug!(
313            token = ?params.token,
314            "ignoring work-done progress cancel for an unknown or ended token"
315        ),
316    }
317    BuiltInGate::RunHook
318}
319
320#[cfg(all(test, not(target_arch = "wasm32")))]
321async fn send_loop<W: TransportWriter>(
322    writer: W,
323    out_rx: UnboundedReceiver<RawMessage>,
324    client: ClientHandle,
325    close: CloseSignal<CloseCause>,
326) {
327    drive_send_loop(writer, out_rx, client, close, CloseCause::writer_failed).await;
328}
329
330/// The static registrations and lifecycle callbacks awaiting the initialize
331/// transaction. Held only while the connection is [`Lifecycle::Uninitialized`];
332/// the transaction consumes it once, so it need not be `Clone`.
333struct Pending<S> {
334    registrations: Registrations<S>,
335    file_provider: SharedFileProvider,
336    configure_initialize: Option<ConfigureInitialize<S>>,
337    on_initialize: Option<OnInitialize<S>>,
338    on_initialized: Option<OnInitialized<S>>,
339    on_shutdown: Option<OnShutdown<S>>,
340    on_exit: Option<OnExit<S>>,
341    layers: Vec<UserLayer<S>>,
342    concurrency_limit: usize,
343}
344
345/// The connection's lifecycle phase. The frozen [`Router`] exists only after a
346/// successful initialize transaction, so it lives inside [`Lifecycle::Running`]
347/// rather than being available up front.
348enum Lifecycle<S> {
349    Uninitialized(Box<Pending<S>>),
350    Initializing,
351    Running(UserService<S>),
352    ShuttingDown,
353    Exited,
354}
355
356/// The single owner of mutable protocol coordination for one connection.
357///
358/// Transport code only feeds envelopes in and drains envelopes out. Lifecycle
359/// selection, request registration, cancellation, task ownership, terminal
360/// response arbitration, and session close all remain behind this boundary.
361struct ProtocolEngine<S, R> {
362    state: Arc<S>,
363    documents: Documents,
364    notebooks: Notebooks,
365    workspace: Option<Workspace>,
366    document_sync: TextDocumentSyncOptions,
367    /// Whether this connection advertised `notebookDocumentSync`, which is what
368    /// makes the four notebook built-ins reachable.
369    notebook_sync: bool,
370    lifecycle: Lifecycle<S>,
371    /// The `on_initialized` hook awaiting the client's `initialized`
372    /// notification. Lifted out of [`Pending`] when the initialize transaction
373    /// succeeds, and taken by the first running-state `initialized` message.
374    on_initialized: Option<OnInitialized<S>>,
375    /// The `on_shutdown` hook for the running state. Unlike notification hooks,
376    /// it is reusable after an error because a failed attempt leaves the
377    /// connection running and the client may send shutdown again.
378    on_shutdown: Option<OnShutdown<S>>,
379    /// The `on_exit` hook awaiting the peer's `exit` notification. Lifted out
380    /// of [`Pending`] when the initialize transaction succeeds; an `exit`
381    /// received earlier closes without a Workspace to hand it.
382    on_exit: Option<OnExit<S>>,
383    protocol: ProtocolSession<R, ClientHandle, CloseCause>,
384    client: ClientHandle,
385    trace: ConnectionTrace,
386    failure_reporter: FailureReporter,
387}
388
389impl<S, R> ProtocolEngine<S, R>
390where
391    S: Send + Sync + 'static,
392    R: Runtime,
393{
394    fn new(
395        server: Server<S>,
396        protocol: ProtocolSession<R, ClientHandle, CloseCause>,
397        client: ClientHandle,
398        trace: ConnectionTrace,
399        failure_reporter: FailureReporter,
400    ) -> Self {
401        let max_inbound_requests = server.resource_policy.max_inbound_requests;
402        Self {
403            state: server.state,
404            documents: Documents::with_resource_policy(server.resource_policy, trace),
405            notebooks: Notebooks::with_resource_policy(server.resource_policy, trace),
406            workspace: None,
407            // Document notifications are processed only after initialize has
408            // replaced this with the validated effective configuration.
409            document_sync: TextDocumentSyncOptions::default(),
410            // Likewise: notebook notifications stay unreachable until
411            // initialize reads the frozen registrations' opt-in.
412            notebook_sync: false,
413            lifecycle: Lifecycle::Uninitialized(Box::new(Pending {
414                registrations: server.registrations,
415                file_provider: server.file_provider,
416                configure_initialize: server.configure_initialize,
417                on_initialize: server.on_initialize,
418                on_initialized: server.on_initialized,
419                on_shutdown: server.on_shutdown,
420                on_exit: server.on_exit,
421                layers: server.layers,
422                concurrency_limit: max_inbound_requests,
423            })),
424            on_initialized: None,
425            on_shutdown: None,
426            on_exit: None,
427            protocol,
428            client,
429            trace,
430            failure_reporter,
431        }
432    }
433
434    /// Own the reader and process one envelope at a time until some cause
435    /// requests closure, then run the one close operation and report the
436    /// ending.
437    ///
438    /// The read-loop also waits on the close signal, so a writer failure ends
439    /// the session without waiting for the peer to send another message.
440    async fn serve<Rd>(mut self, mut reader: Rd) -> Result<Outcome>
441    where
442        Rd: TransportReader,
443    {
444        loop {
445            let msg = match self.protocol.next_input(&mut reader).await {
446                SessionInput::CloseRequested => break,
447                SessionInput::OutboundFailed => {
448                    self.protocol.request_close(CloseCause::WriterFailed);
449                    break;
450                }
451                SessionInput::Message(message) => message,
452            };
453
454            match msg {
455                Ok(msg) => {
456                    self.trace.message(Direction::Inbound, &msg);
457                    match self.dispatch(msg).await {
458                        Flow::Continue => {}
459                        Flow::Close(cause) => {
460                            self.protocol.request_close(cause);
461                            break;
462                        }
463                    }
464                }
465                Err(TransportError::Closed) => {
466                    warn!("transport closed by peer before exit notification");
467                    self.protocol.request_close(CloseCause::ReaderEof);
468                    break;
469                }
470                Err(error) => {
471                    let category = match &error {
472                        TransportError::Malformed(_) | TransportError::OversizedMessage { .. } => {
473                            ConnectionFailureCategory::Framing
474                        }
475                        TransportError::Io(_) | TransportError::Serde(_) => {
476                            ConnectionFailureCategory::Transport
477                        }
478                        TransportError::Closed => unreachable!("closed is handled above"),
479                    };
480                    self.failure_reporter.report(
481                        category,
482                        Some(ConnectionDirection::Inbound),
483                        None,
484                        None,
485                    );
486                    self.protocol.request_close(CloseCause::ReaderFailed(error));
487                    break;
488                }
489            }
490        }
491
492        self.close().await;
493        let cause = self.protocol.final_close_cause();
494        self.trace.connection_closed(cause.as_str());
495        cause.into_result()
496    }
497
498    async fn dispatch(&mut self, msg: RawMessage) -> Flow {
499        match msg {
500            RawMessage::Request { id, method, params } => {
501                let span = self.trace.request_span(method.as_ref(), &id);
502                // Admission happens before request-scoped cancellation state,
503                // parameter decoding, and runtime task creation. The owned
504                // permit remains attached to the task handle until the engine
505                // reaps it, even if cancellation or close claims the response
506                // gate first.
507                let reserved = match self.protocol.reserve_inbound(
508                    id.clone(),
509                    method.as_ref(),
510                    method != "initialize",
511                ) {
512                    Ok(reserved) => reserved,
513                    Err(InboundReserveError::DuplicateId) => {
514                        self.failure_reporter.report_unvalidated_inbound_method(
515                            ConnectionFailureCategory::Protocol,
516                            Some(&id),
517                        );
518                        self.trace.request_completed(
519                            method.as_ref(),
520                            &id,
521                            Instant::now(),
522                            Direction::Inbound,
523                            Completion::Rejected,
524                        );
525                        self.protocol
526                            .reject_inbound(id, LspError::invalid_request("duplicate request id"));
527                        return Flow::Continue;
528                    }
529                    Err(InboundReserveError::CapacityExhausted) => {
530                        self.trace.request_completed(
531                            method.as_ref(),
532                            &id,
533                            Instant::now(),
534                            Direction::Inbound,
535                            Completion::Rejected,
536                        );
537                        self.protocol.reject_inbound(
538                            id,
539                            LspError::ServerCancelled(INBOUND_CAPACITY_EXHAUSTED.to_string()),
540                        );
541                        return Flow::Continue;
542                    }
543                };
544                let reservation = reserved.reservation;
545                let cancellation = reserved.cancellation;
546
547                // Initialize precedence: until `initialize` completes, refuse
548                // every other request with `ServerNotInitialized`.
549                if method != "initialize"
550                    && matches!(
551                        self.lifecycle,
552                        Lifecycle::Uninitialized(_) | Lifecycle::Initializing
553                    )
554                {
555                    self.failure_reporter.report_unvalidated_inbound_method(
556                        ConnectionFailureCategory::Protocol,
557                        Some(&reservation.id),
558                    );
559                    self.protocol
560                        .complete_inbound(reservation, Err(LspError::ServerNotInitialized));
561                    return Flow::Continue;
562                }
563                // After `shutdown`, every request is invalid until `exit`.
564                if matches!(self.lifecycle, Lifecycle::ShuttingDown | Lifecycle::Exited) {
565                    self.failure_reporter.report_unvalidated_inbound_method(
566                        ConnectionFailureCategory::Protocol,
567                        Some(&reservation.id),
568                    );
569                    self.protocol.complete_inbound(
570                        reservation,
571                        Err(LspError::invalid_request("invalid request")),
572                    );
573                    return Flow::Continue;
574                }
575
576                match method.as_ref() {
577                    "initialize" => return self.initialize(&span, reservation, params).await,
578                    "shutdown" => {
579                        let params_result = if params.is_empty() {
580                            Ok(())
581                        } else {
582                            decode_params::<()>(&params)
583                        };
584                        if let Err(err) = params_result {
585                            self.failure_reporter.report(
586                                ConnectionFailureCategory::Protocol,
587                                Some(ConnectionDirection::Inbound),
588                                Some("shutdown"),
589                                Some(&reservation.id),
590                            );
591                            self.protocol.complete_inbound(reservation, Err(err));
592                            return Flow::Continue;
593                        }
594                        if let Some(hook) = &self.on_shutdown {
595                            let cancellation = cancellation
596                                .expect("shutdown is a cancellable non-initialize request");
597                            let ctx = ServerContext::for_request(
598                                reservation.id.clone(),
599                                span.clone(),
600                                self.client.clone(),
601                                self.established_workspace(),
602                            )
603                            .with_cancellation(cancellation.clone());
604                            if let Err(err) = hook
605                                .invoke((Arc::clone(&self.state), ctx, (), cancellation))
606                                .instrument(span.clone())
607                                .await
608                            {
609                                self.protocol.complete_inbound(reservation, Err(err));
610                                return Flow::Continue;
611                            }
612                        }
613                        // The successful shutdown request answers itself first,
614                        // so its own entry is gone before the sweep below; only
615                        // then cancel the rest of the in-flight work and enter
616                        // `ShuttingDown`.
617                        self.protocol
618                            .complete_inbound(reservation, encode_body(&serde_json::Value::Null));
619                        self.protocol.cancel_all_inbound_with_response();
620                        self.lifecycle = Lifecycle::ShuttingDown;
621                    }
622                    _other => {
623                        // Precedence guarantees the connection is running here.
624                        let service = match &self.lifecycle {
625                            Lifecycle::Running(service) => Arc::clone(service),
626                            _ => {
627                                self.protocol.complete_inbound(
628                                    reservation,
629                                    Err(LspError::ServerNotInitialized),
630                                );
631                                return Flow::Continue;
632                            }
633                        };
634                        let params = match decode_value(&params) {
635                            Ok(params) => params,
636                            Err(error) => {
637                                self.failure_reporter.report_unvalidated_inbound_method(
638                                    ConnectionFailureCategory::Protocol,
639                                    Some(&reservation.id),
640                                );
641                                self.protocol.complete_inbound(reservation, Err(error));
642                                return Flow::Continue;
643                            }
644                        };
645                        let work_done_token =
646                            match request_token::<ProgressToken>(&params, "workDoneToken") {
647                                Ok(token) => token,
648                                Err(error) => {
649                                    self.protocol.complete_inbound(
650                                        reservation,
651                                        Err(LspError::invalid_params(error)),
652                                    );
653                                    return Flow::Continue;
654                                }
655                            };
656                        let partial_result_token =
657                            if crate::partial_result::supports_method(method.as_ref()) {
658                                match request_token::<ProgressToken>(&params, "partialResultToken")
659                                {
660                                    Ok(token) => token,
661                                    Err(error) => {
662                                        self.protocol.complete_inbound(
663                                            reservation,
664                                            Err(LspError::invalid_params(error)),
665                                        );
666                                        return Flow::Continue;
667                                    }
668                                }
669                            } else {
670                                None
671                            };
672                        let method = method.into_owned();
673                        let ctx = ServerContext::for_request(
674                            reservation.id.clone(),
675                            span.clone(),
676                            self.client.clone(),
677                            self.established_workspace(),
678                        )
679                        .with_cancellation(
680                            cancellation
681                                .as_ref()
682                                .expect("non-initialize requests are cancellable")
683                                .clone(),
684                        )
685                        .with_work_done_token(work_done_token)
686                        .with_partial_result(method.clone(), partial_result_token);
687                        self.spawn_service_request(
688                            service,
689                            reservation,
690                            method,
691                            params,
692                            ctx,
693                            cancellation.expect("non-initialize requests are cancellable"),
694                            self.protocol.handler_timeout(),
695                        );
696                    }
697                }
698            }
699            RawMessage::Notification { method, params } => match method.as_ref() {
700                "exit" => {
701                    // The exit hook observes the ending first (ADR 0018,
702                    // ADR 0024): it runs after a successful initialize
703                    // transaction, before the engine computes the exit
704                    // outcome. It resolves to `()`, so it cannot change that
705                    // outcome — the LSP exit code below derives from
706                    // protocol-owned lifecycle state alone, and the hook
707                    // receives only the shared state and a live `ServerContext`.
708                    let established = matches!(
709                        self.lifecycle,
710                        Lifecycle::Running(_) | Lifecycle::ShuttingDown
711                    );
712                    if !established {
713                        self.failure_reporter.report(
714                            ConnectionFailureCategory::Protocol,
715                            Some(ConnectionDirection::Inbound),
716                            Some("exit"),
717                            None,
718                        );
719                    }
720                    if established && let Some(hook) = self.on_exit.take() {
721                        let span = self.trace.notification_span("exit");
722                        let ctx = ServerContext::for_notification(
723                            span,
724                            self.client.clone(),
725                            self.established_workspace(),
726                        );
727                        hook.invoke((Arc::clone(&self.state), ctx)).await;
728                    }
729                    // The LSP exit code comes from protocol-owned lifecycle
730                    // state: 0 only when `shutdown` completed first.
731                    let code = match self.lifecycle {
732                        Lifecycle::ShuttingDown => 0,
733                        _ => 1,
734                    };
735                    return Flow::Close(CloseCause::Exit { code });
736                }
737                "$/cancelRequest" => {
738                    let bytes: &[u8] = if params.is_empty() { b"{}" } else { &params };
739                    match serde_json::from_slice::<CancelParams>(bytes) {
740                        Ok(cancel) => {
741                            if let Some(reservation) = self.protocol.cancel_inbound(&cancel.id) {
742                                self.protocol.complete_cancelled(reservation);
743                            }
744                        }
745                        Err(error) => {
746                            self.failure_reporter.report(
747                                ConnectionFailureCategory::Protocol,
748                                Some(ConnectionDirection::Inbound),
749                                Some("$/cancelRequest"),
750                                None,
751                            );
752                            debug!(%error, "ignoring malformed $/cancelRequest");
753                        }
754                    }
755                }
756                "initialized" => {
757                    // The initialized hook runs at most once, and only after a
758                    // successful initialize transaction: outside the running
759                    // state there is no Workspace for its ServerContext, and the
760                    // notification is ignored without consuming the hook, so a
761                    // later, valid `initialized` still runs it. The params are
762                    // decoded before the hook is taken, so a malformed
763                    // notification leaves it in place too.
764                    let Lifecycle::Running(_) = &self.lifecycle else {
765                        self.failure_reporter.report(
766                            ConnectionFailureCategory::Protocol,
767                            Some(ConnectionDirection::Inbound),
768                            Some("initialized"),
769                            None,
770                        );
771                        debug!("initialized notification outside the running state ignored");
772                        return Flow::Continue;
773                    };
774                    let params = match decode_initialized_params(&params) {
775                        Ok(params) => params,
776                        Err(error) => {
777                            self.failure_reporter.report(
778                                ConnectionFailureCategory::Protocol,
779                                Some(ConnectionDirection::Inbound),
780                                Some("initialized"),
781                                None,
782                            );
783                            warn!(%error, "dropping initialized notification with malformed params");
784                            return Flow::Continue;
785                        }
786                    };
787                    let Some(hook) = self.on_initialized.take() else {
788                        return Flow::Continue;
789                    };
790                    let span = self.trace.notification_span("initialized");
791                    let ctx = ServerContext::for_notification(
792                        span,
793                        self.client.clone(),
794                        self.established_workspace(),
795                    );
796                    hook.invoke((Arc::clone(&self.state), ctx, params)).await;
797                }
798                other => {
799                    // Outside the running state only the lifecycle and
800                    // completion notifications handled above are processed:
801                    // before `initialize` there is no Router, and after
802                    // `shutdown` the connection accepts no further user work.
803                    let Lifecycle::Running(service) = &self.lifecycle else {
804                        self.failure_reporter.report_unvalidated_inbound_method(
805                            ConnectionFailureCategory::Protocol,
806                            None,
807                        );
808                        debug!(method = other, "notification outside running state ignored");
809                        return Flow::Continue;
810                    };
811                    let service = Arc::clone(service);
812
813                    // A protocol-owned notification is a built-in (ADR 0018):
814                    // its validation and any mutation run here, on the
815                    // read-loop, before anything user-registered is reached, so
816                    // the hook below — and every later message — observes the
817                    // mutated state. A failure reports the notification
818                    // error and skips the hook, leaving the connection to
819                    // process the next message; a built-in may also skip its
820                    // own hook after logging a non-error rejection at debug
821                    // level (the work-done progress cancel built-in).
822                    if let Some(built_in) = ProtocolNotification::from_method(other) {
823                        if !self.accepts_protocol_notification(built_in) {
824                            debug!(
825                                method = other,
826                                "document-sync notification disabled and ignored"
827                            );
828                            return Flow::Continue;
829                        }
830                        match self.process_protocol_notification(built_in, &params) {
831                            Ok(BuiltInGate::RunHook) => {}
832                            Ok(BuiltInGate::ProtocolFailure) => {
833                                self.failure_reporter.report(
834                                    ConnectionFailureCategory::Protocol,
835                                    Some(ConnectionDirection::Inbound),
836                                    Some(other),
837                                    None,
838                                );
839                                return Flow::Continue;
840                            }
841                            Err(error) => {
842                                let (category, error) = match error {
843                                    BuiltInError::Protocol(error) => {
844                                        (ConnectionFailureCategory::Protocol, error)
845                                    }
846                                    BuiltInError::Overload(error) => {
847                                        (ConnectionFailureCategory::Overload, error)
848                                    }
849                                };
850                                self.failure_reporter.report(
851                                    category,
852                                    Some(ConnectionDirection::Inbound),
853                                    Some(other),
854                                    None,
855                                );
856                                warn!(method = other, %error, "protocol validation skipped its hook");
857                                return Flow::Continue;
858                            }
859                        }
860                    }
861
862                    // The same bytes decode again into the method-erased value
863                    // that crosses the Service stack. For a built-in this
864                    // cannot fail — its typed decode above already succeeded.
865                    let params = match decode_value(&params) {
866                        Ok(params) => params,
867                        Err(error) => {
868                            self.failure_reporter.report_unvalidated_inbound_method(
869                                ConnectionFailureCategory::Protocol,
870                                None,
871                            );
872                            debug!(method = other, %error, "notification params ignored");
873                            return Flow::Continue;
874                        }
875                    };
876                    // A registered notification — a custom route or a built-in's
877                    // post-validation hook — dispatches with no response; an
878                    // unregistered one is ignored.
879                    self.dispatch_notification(service, other, params).await;
880                }
881            },
882            RawMessage::Response { id, result } => {
883                // Only positive numeric IDs are allocated by `OutboundRegistry`.
884                let id_num = match &id {
885                    RequestId::Number(n) if *n > 0 => Some(*n as u32),
886                    _ => None,
887                };
888                let delivered =
889                    id_num.is_some_and(|n| self.client.outbound_registry().complete(n, result));
890                if !delivered {
891                    self.failure_reporter.report(
892                        ConnectionFailureCategory::Protocol,
893                        Some(ConnectionDirection::Inbound),
894                        None,
895                        Some(&id),
896                    );
897                    debug!(?id, "ignoring response with unknown or non-numeric id");
898                }
899            }
900            RawMessage::ProtocolError { error } => {
901                self.failure_reporter.report(
902                    ConnectionFailureCategory::Protocol,
903                    Some(ConnectionDirection::Inbound),
904                    None,
905                    None,
906                );
907                self.protocol.send_protocol_error(error);
908            }
909        }
910
911        Flow::Continue
912    }
913
914    /// Run one normalized user notification through the Service stack.
915    ///
916    /// Takes `&mut self` like the rest of dispatch: the read-loop holds the
917    /// engine exclusively across this await, which is what keeps a built-in's
918    /// mutation and its hook one serial step.
919    async fn dispatch_notification(
920        &mut self,
921        service: UserService<S>,
922        method: &str,
923        params: serde_json::Value,
924    ) {
925        let span = self.trace.notification_span(method);
926        let ctx = ServerContext::for_notification(
927            span,
928            self.client.clone(),
929            self.established_workspace(),
930        );
931        let result = service
932            .call(IncomingCall::notification(
933                method.to_string(),
934                params,
935                ctx,
936                Arc::clone(&self.state),
937            ))
938            .await;
939        if !matches!(result, ServiceResult::NoResponse) {
940            warn!("notification service attempted to produce a response");
941        }
942    }
943
944    /// Validate and, where applicable, mutate for a protocol-owned notification.
945    ///
946    /// Built-in validation is what the stores themselves can establish: a
947    /// change names a document that must already be open, each of its ranges
948    /// must be applicable under the negotiated encoding, and a notebook cell
949    /// splice must land inside the cell array the notebook actually has.
950    /// Returning `Err`
951    /// is what skips the notification's hook, so nothing partial is left for a
952    /// hook to observe: a rejected `didChange` batch leaves the document at the
953    /// revision the last accepted notification produced. The returned gate says
954    /// whether the hook runs — the work-done progress cancel built-in skips it
955    /// for malformed params without an error worth a warning.
956    fn process_protocol_notification(
957        &self,
958        built_in: ProtocolNotification,
959        raw_params: &Bytes,
960    ) -> std::result::Result<BuiltInGate, BuiltInError> {
961        match built_in {
962            ProtocolNotification::Open => {
963                let params: DidOpenTextDocumentParams = decode_params(raw_params)?;
964                self.documents.open(params.text_document)?;
965            }
966            ProtocolNotification::Change => {
967                let params: DidChangeTextDocumentParams = decode_params(raw_params)?;
968                validate_sync_changes(
969                    self.document_sync
970                        .change
971                        .unwrap_or(TextDocumentSyncKind::Incremental),
972                    &params.content_changes,
973                )?;
974                self.documents.apply_changes(
975                    &params.text_document.text_document_identifier.uri,
976                    params.text_document.version,
977                    params.content_changes,
978                )?;
979            }
980            ProtocolNotification::Close => {
981                let params: DidCloseTextDocumentParams = decode_params(raw_params)?;
982                // Closing a document that was never opened breaks the LSP's
983                // ordering, but there is nothing to roll back and no response
984                // to carry a complaint. The hook still runs: it observes the
985                // same absence a real close would have left behind.
986                if self.documents.close(&params.text_document.uri).is_none() {
987                    debug!(
988                        uri = ?params.text_document.uri,
989                        "closing a document that was not open"
990                    );
991                }
992            }
993            ProtocolNotification::WillSave => {
994                let _: WillSaveTextDocumentParams = decode_params(raw_params)?;
995            }
996            ProtocolNotification::Save => {
997                let params: DidSaveTextDocumentParams = decode_params(raw_params)?;
998                if matches!(
999                    &self.document_sync.save,
1000                    Some(Save::SaveOptions(options))
1001                        if options.include_text == Some(true)
1002                ) && params.text.is_none()
1003                {
1004                    return Err(LspError::invalid_request(
1005                        "didSave text is required by textDocumentSync.save.includeText",
1006                    )
1007                    .into());
1008                }
1009            }
1010            ProtocolNotification::NotebookOpen => {
1011                let params: DidOpenNotebookDocumentParams = decode_params(raw_params)?;
1012                // Each cell counts against the connection's Document budgets,
1013                // and cells go first: a refused cell leaves the notebook
1014                // unregistered, so the skipped hook sees neither half of a
1015                // half-open notebook. The separate notebook-count budget makes
1016                // an empty notebook consume finite connection capacity too.
1017                let opened = self.open_cell_text_documents(params.cell_text_documents)?;
1018                let superseded = match self.notebooks.try_open(params.notebook_document) {
1019                    Ok(superseded) => superseded,
1020                    Err(error) => {
1021                        self.rollback_cell_opens(opened);
1022                        return Err(BuiltInError::Overload(error));
1023                    }
1024                };
1025                if let Some(superseded) = superseded {
1026                    // Re-opening a notebook breaks the LSP's ordering. There is
1027                    // no response to complain through, but the cells the
1028                    // superseded notebook listed and the new one does not would
1029                    // otherwise stay in the document store forever.
1030                    debug!(
1031                        uri = ?superseded.uri(),
1032                        "re-opening a notebook that was already open"
1033                    );
1034                    self.close_departed_cells(&superseded);
1035                }
1036            }
1037            ProtocolNotification::NotebookChange => {
1038                let params: DidChangeNotebookDocumentParams = decode_params(raw_params)?;
1039                self.apply_notebook_change(params)?;
1040            }
1041            ProtocolNotification::NotebookSave => {
1042                let params: DidSaveNotebookDocumentParams = decode_params(raw_params)?;
1043                // A notebook save carries no state beyond the notification
1044                // itself, exactly like `textDocument/didSave`. Saving a
1045                // notebook that was never opened still runs the hook: it
1046                // observes the same absence the peer left behind.
1047                if !self.notebooks.contains(&params.notebook_document.uri) {
1048                    debug!(
1049                        uri = ?params.notebook_document.uri,
1050                        "saving a notebook that is not synchronized"
1051                    );
1052                }
1053            }
1054            ProtocolNotification::NotebookClose => {
1055                let params: DidCloseNotebookDocumentParams = decode_params(raw_params)?;
1056                // Every cell text Document the notebook listed closes with it —
1057                // missing one is how those Documents leak — and so does every
1058                // cell the peer names, whether or not the two agree.
1059                match self.notebooks.close(&params.notebook_document.uri) {
1060                    Some(removed) => {
1061                        for cell in removed.cells() {
1062                            self.documents.close(&cell.document);
1063                        }
1064                    }
1065                    None => debug!(
1066                        uri = ?params.notebook_document.uri,
1067                        "closing a notebook that was not open"
1068                    ),
1069                }
1070                for cell in &params.cell_text_documents {
1071                    self.documents.close(&cell.uri);
1072                }
1073            }
1074            ProtocolNotification::WorkspaceFolders => {
1075                let params: DidChangeWorkspaceFoldersParams = decode_params(raw_params)?;
1076                self.established_workspace().apply_folder_change(params);
1077            }
1078            ProtocolNotification::Configuration => {
1079                let params: DidChangeConfigurationParams = decode_params(raw_params)?;
1080                self.established_workspace()
1081                    .set_configuration(params.settings);
1082            }
1083            ProtocolNotification::Trace => {
1084                let params: SetTraceParams = decode_params(raw_params)?;
1085                if matches!(&params.value, TraceValue::Custom(_)) {
1086                    return Err(LspError::invalid_params(
1087                        "setTrace value must be off, messages, or verbose",
1088                    )
1089                    .into());
1090                }
1091                self.established_workspace().set_trace(params.value);
1092            }
1093            ProtocolNotification::ProgressCancel => {
1094                return Ok(gate_progress_cancel(
1095                    self.client.progress_registry(),
1096                    raw_params,
1097                ));
1098            }
1099        }
1100        Ok(BuiltInGate::RunHook)
1101    }
1102
1103    /// Apply one `notebookDocument/didChange` across the notebook layer and the
1104    /// document store (ADR 0034).
1105    ///
1106    /// The order is what makes the whole notification all-or-nothing. Planning
1107    /// the notebook validates the splice without committing it; every cell text
1108    /// Document mutation that can be refused runs next, undoing its own work on
1109    /// refusal; the notebook is committed only once nothing can fail; and the
1110    /// cells that left the notebook — the one irreversible step — close last.
1111    fn apply_notebook_change(
1112        &self,
1113        params: DidChangeNotebookDocumentParams,
1114    ) -> std::result::Result<(), BuiltInError> {
1115        let planned = self.notebooks.plan_change(&params)?;
1116        let closed_cells = match params.change.cells {
1117            Some(cells) => self.apply_cell_text_documents(cells)?,
1118            None => Vec::new(),
1119        };
1120        // A cell the splice removed loses its Document here even when the peer
1121        // left it out of `didClose`. Nothing afterwards can name that cell, so
1122        // trusting `didClose` alone is how a cell text Document leaks.
1123        if let Some(superseded) = self.notebooks.commit(planned) {
1124            self.close_departed_cells(&superseded);
1125        }
1126        for uri in closed_cells {
1127            self.documents.close(&uri);
1128        }
1129        Ok(())
1130    }
1131
1132    /// Mutate the cell text Documents one cell change names — the structural
1133    /// change's opens, then every cell's text content — and return the URIs the
1134    /// peer asked to close, which the caller closes after the notebook commits.
1135    ///
1136    /// Any refusal restores what this notification already did: the cells it
1137    /// opened close again and the cells it edited return to their prior text
1138    /// and version, so the skipped hook has no partial state to observe.
1139    fn apply_cell_text_documents(
1140        &self,
1141        cells: NotebookDocumentCellChanges,
1142    ) -> std::result::Result<Vec<Uri>, BuiltInError> {
1143        let (opened_cells, closed_cells) = match cells.structure {
1144            Some(structure) => (
1145                structure.did_open.unwrap_or_default(),
1146                structure.did_close.unwrap_or_default(),
1147            ),
1148            None => (Vec::new(), Vec::new()),
1149        };
1150        let opened = self.open_cell_text_documents(opened_cells)?;
1151
1152        // Cell text reuses the document store's incremental change path, so a
1153        // cell edit behaves exactly like `textDocument/didChange` on the same
1154        // URI — including its all-or-nothing batch and its byte budget.
1155        let mut edited = Vec::new();
1156        for cell in cells.text_content.unwrap_or_default() {
1157            let previous = self
1158                .documents
1159                .get(&cell.document.text_document_identifier.uri);
1160            match self.documents.apply_changes(
1161                &cell.document.text_document_identifier.uri,
1162                cell.document.version,
1163                cell.changes,
1164            ) {
1165                Ok(()) => edited.extend(previous),
1166                Err(error) => {
1167                    for document in edited {
1168                        self.documents.restore(document);
1169                    }
1170                    self.rollback_cell_opens(opened);
1171                    return Err(error.into());
1172                }
1173            }
1174        }
1175
1176        Ok(closed_cells.into_iter().map(|cell| cell.uri).collect())
1177    }
1178
1179    /// Open every cell text Document, retaining the prior snapshot needed to
1180    /// roll back this notification.
1181    ///
1182    /// A refusal undoes the opens made so far, so a rejected notebook
1183    /// notification leaves no orphan or overwritten cell text Document behind.
1184    fn open_cell_text_documents(
1185        &self,
1186        cells: Vec<TextDocumentItem>,
1187    ) -> std::result::Result<Vec<CellOpenRollback>, DocumentMutationError> {
1188        let mut opened = Vec::new();
1189        for cell in cells {
1190            let uri = cell.uri.clone();
1191            let previous = self.documents.get(&uri);
1192            if let Err(error) = self.documents.open(cell) {
1193                self.rollback_cell_opens(opened);
1194                return Err(error);
1195            }
1196            opened.push(CellOpenRollback { uri, previous });
1197        }
1198        Ok(opened)
1199    }
1200
1201    fn rollback_cell_opens(&self, opened: Vec<CellOpenRollback>) {
1202        for opened in opened.into_iter().rev() {
1203            match opened.previous {
1204                Some(document) => self.documents.restore(document),
1205                None => {
1206                    self.documents.close(&opened.uri);
1207                }
1208            }
1209        }
1210    }
1211
1212    /// Close the cell text Documents a superseded notebook listed that no
1213    /// synchronized notebook lists any more.
1214    fn close_departed_cells(&self, superseded: &Notebook) {
1215        for cell in superseded.cells() {
1216            if self.notebooks.notebook_for_cell(&cell.document).is_none() {
1217                self.documents.close(&cell.document);
1218            }
1219        }
1220    }
1221
1222    fn accepts_protocol_notification(&self, built_in: ProtocolNotification) -> bool {
1223        match built_in {
1224            ProtocolNotification::WorkspaceFolders
1225            | ProtocolNotification::Configuration
1226            | ProtocolNotification::Trace
1227            | ProtocolNotification::ProgressCancel => true,
1228            // Notebook synchronization is its own capability in LSP, not a
1229            // mode of `textDocumentSync`, so the text-document switches below
1230            // do not gate it — its own opt-in does. A server that never called
1231            // `notebook_document_sync` advertises nothing, so a conformant
1232            // client sends no notebook notification; ignoring one that arrives
1233            // anyway keeps an unadvertised capability from mutating the
1234            // notebook layer, opening cell Documents against the connection's
1235            // budgets, or reaching a hook (ADR 0034).
1236            ProtocolNotification::NotebookOpen
1237            | ProtocolNotification::NotebookChange
1238            | ProtocolNotification::NotebookSave
1239            | ProtocolNotification::NotebookClose => self.notebook_sync,
1240            _ if self.document_sync.change == Some(TextDocumentSyncKind::None) => false,
1241            ProtocolNotification::Open | ProtocolNotification::Close => {
1242                self.document_sync.open_close == Some(true)
1243            }
1244            ProtocolNotification::Change => matches!(
1245                self.document_sync.change,
1246                Some(TextDocumentSyncKind::Full | TextDocumentSyncKind::Incremental)
1247            ),
1248            ProtocolNotification::WillSave => self.document_sync.will_save == Some(true),
1249            ProtocolNotification::Save => matches!(
1250                self.document_sync.save,
1251                Some(Save::Bool(true)) | Some(Save::SaveOptions(_))
1252            ),
1253        }
1254    }
1255
1256    /// Run the one `initialize` transaction (ADR 0017, ADR 0018).
1257    ///
1258    /// In order: validate and consume the sole `initialize`; run
1259    /// `configure_initialize` against a transactional registrar; on success
1260    /// commit and permanently freeze the Router; establish the `Workspace`,
1261    /// `Documents` encoding, and generated capabilities; park the
1262    /// `on_initialized`, `on_shutdown`, and `on_exit` hooks for the running state; run
1263    /// `on_initialize` for optional `ServerInfo`; then enter the running state
1264    /// and reply. Any configuration, validation, or `on_initialize` failure
1265    /// enqueues the fixed error and requests the terminal close rather than
1266    /// returning to uninitialized.
1267    async fn initialize(&mut self, span: &Span, reservation: Reservation, params: Bytes) -> Flow {
1268        // A second `initialize` after the transaction has run is invalid.
1269        if !matches!(self.lifecycle, Lifecycle::Uninitialized(_)) {
1270            self.failure_reporter.report(
1271                ConnectionFailureCategory::Protocol,
1272                Some(ConnectionDirection::Inbound),
1273                Some("initialize"),
1274                Some(&reservation.id),
1275            );
1276            self.protocol.complete_inbound(
1277                reservation,
1278                Err(LspError::ServerError {
1279                    code: -32600,
1280                    message: "server already initialized".into(),
1281                    data: None,
1282                }),
1283            );
1284            return Flow::Continue;
1285        }
1286
1287        // Malformed `initialize` params leave the transaction unspent: the
1288        // client may retry with a valid request, so stay uninitialized.
1289        let params = match decode_params::<InitializeParams>(&params) {
1290            Ok(params) => params,
1291            Err(err) => {
1292                self.failure_reporter.report(
1293                    ConnectionFailureCategory::Protocol,
1294                    Some(ConnectionDirection::Inbound),
1295                    Some("initialize"),
1296                    Some(&reservation.id),
1297                );
1298                self.protocol.complete_inbound(reservation, Err(err));
1299                return Flow::Continue;
1300            }
1301        };
1302
1303        // Take ownership of the pending registrations and callbacks; the
1304        // transaction consumes them exactly once.
1305        let pending = match std::mem::replace(&mut self.lifecycle, Lifecycle::Initializing) {
1306            Lifecycle::Uninitialized(pending) => *pending,
1307            // The `matches!` guard above already established this arm.
1308            _ => unreachable!("initialize runs only while uninitialized"),
1309        };
1310        let Pending {
1311            registrations,
1312            file_provider,
1313            configure_initialize,
1314            on_initialize,
1315            on_initialized,
1316            on_shutdown,
1317            on_exit,
1318            layers,
1319            concurrency_limit,
1320        } = pending;
1321
1322        // Run the conditional registration transaction against a registrar
1323        // seeded with all static registrations. A callback error or any
1324        // combined-validation conflict discards the whole transaction — the
1325        // registrar (and every static and conditional registration in it) is
1326        // dropped, so nothing partial leaks.
1327        let mut registrar = InitializeRegistrar::new(registrations);
1328        let committed = match configure_initialize {
1329            Some(callback) => callback.invoke(&params, &mut registrar),
1330            None => Ok(()),
1331        }
1332        .and_then(|()| registrar.commit().map_err(LspError::internal));
1333
1334        let registrations = match committed {
1335            Ok(registrations) => registrations,
1336            Err(_err) => {
1337                // ADR 0017's fixed error: configuration or combined-validation
1338                // failure reports InternalError and enters the close path.
1339                self.protocol.complete_inbound(
1340                    reservation,
1341                    Err(LspError::internal("initialization failed")),
1342                );
1343                return Flow::Close(CloseCause::InitializeFailed);
1344            }
1345        };
1346
1347        // Commit: permanently freeze the Router before any capability is
1348        // generated.
1349        let router = Arc::new(registrations.freeze());
1350
1351        // The post-initialize lifecycle hooks become reachable only through
1352        // dispatch in the running state, which the transaction enters at its
1353        // end; they stay parked here until then.
1354        self.on_initialized = on_initialized;
1355        self.on_shutdown = on_shutdown;
1356        self.on_exit = on_exit;
1357
1358        // Establish Workspace, Documents encoding, and generated capabilities
1359        // from InitializeParams before `on_initialize` observes them. Per
1360        // ADR 0018's precedence, the Workspace is established (step 4) before
1361        // protocol-owned fields are negotiated and capabilities generated
1362        // (step 5). The Workspace takes ownership of the connection's
1363        // Documents and Notebooks handles; the engine keeps its own clones for
1364        // built-in synchronization mutations.
1365        let established = Workspace::from_params_with_stores_and_provider(
1366            &params,
1367            self.documents.clone(),
1368            self.notebooks.clone(),
1369            file_provider,
1370            self.client.shared_trace(),
1371        );
1372        let work_done_token = params.work_done_progress_params.work_done_token.clone();
1373        self.workspace = Some(established.clone());
1374
1375        let position_encoding = self.documents.negotiate_position_encoding(&params);
1376        let mut capabilities = router.generated_capabilities();
1377        let standard_capabilities = &mut capabilities;
1378        standard_capabilities.position_encoding = Some(position_encoding);
1379        // Document sync is a protocol built-in rather than a registration
1380        // (ADR 0018): the engine applies every `didOpen`, `didChange`, and
1381        // `didClose` itself. So it advertises the sync kind those built-ins
1382        // implement, as one more protocol-owned field layered onto the frozen
1383        // catalog (ADR 0017) beside the negotiated position encoding. A client
1384        // that sees no `textDocumentSync` sends no document notification at
1385        // all, leaving the built-ins and every post-validation hook unreachable.
1386        // Nothing user-registered contributes this field, so there is no
1387        // contribution here to overwrite.
1388        let document_sync = router.document_sync();
1389        self.document_sync = document_sync.options;
1390        standard_capabilities.text_document_sync = Some(document_sync.capability);
1391        // Notebook sync carries its own capability and its own opt-in. The
1392        // generated catalog already holds whatever `notebook_document_sync`
1393        // contributed; the engine only records whether the built-ins it drives
1394        // are reachable at all on this connection (ADR 0034).
1395        self.notebook_sync = router.notebook_sync_enabled();
1396        // Workspace-folder sync is likewise a protocol built-in, so the
1397        // engine advertises its support itself. Registration-contributed
1398        // workspace fields (the file-operation families) come from the frozen
1399        // catalog and are preserved beside the protocol-owned field.
1400        let mut workspace = standard_capabilities.workspace.take().unwrap_or_default();
1401        workspace.workspace_folders = Some(WorkspaceFoldersServerCapabilities {
1402            supported: Some(true),
1403            change_notifications: Some(true.into()),
1404        });
1405        standard_capabilities.workspace = Some(workspace);
1406
1407        // `on_initialize` may contribute optional ServerInfo but cannot
1408        // register routes or replace the generated capabilities.
1409        let server_info = match on_initialize {
1410            Some(hook) => {
1411                let ctx = ServerContext::for_request(
1412                    reservation.id.clone(),
1413                    span.clone(),
1414                    self.client.clone(),
1415                    established,
1416                )
1417                .with_work_done_token(work_done_token);
1418                match hook
1419                    .invoke((
1420                        Arc::clone(&self.state),
1421                        ctx,
1422                        params,
1423                        self.protocol.cancellation_child(),
1424                    ))
1425                    .instrument(span.clone())
1426                    .await
1427                {
1428                    Ok(server_info) => server_info,
1429                    Err(err) => {
1430                        // ADR 0018: on_initialize failure sends that error, then
1431                        // enters the close path; the frozen Router and
1432                        // established Workspace are never exposed to later
1433                        // dispatch.
1434                        self.protocol.complete_inbound(reservation, Err(err));
1435                        return Flow::Close(CloseCause::InitializeFailed);
1436                    }
1437                }
1438            }
1439            None => None,
1440        };
1441
1442        self.protocol.complete_inbound(
1443            reservation,
1444            encode_body(&WireInitializeResult {
1445                capabilities,
1446                server_info,
1447            }),
1448        );
1449        self.lifecycle = Lifecycle::Running(build_service_stack(
1450            router,
1451            layers,
1452            concurrency_limit,
1453            self.failure_reporter.clone(),
1454        ));
1455        Flow::Continue
1456    }
1457
1458    /// The established [`Workspace`]. Dispatch reaches user code only in the
1459    /// running state, which the initialize transaction enters only after
1460    /// establishing the Workspace, so it is always present here.
1461    fn established_workspace(&self) -> Workspace {
1462        self.workspace.clone().expect(
1463            "user dispatch runs only after the initialize transaction establishes the workspace",
1464        )
1465    }
1466
1467    /// Spawn one user request into the engine's task group. The task races user
1468    /// dispatch against its explicit cancellation token. When cancellation
1469    /// wins, one final poll lets cooperative handler code observe the token
1470    /// before the future is dropped; the completion gate rejects any result
1471    /// produced after another path claimed the reservation.
1472    fn spawn_service_request(
1473        &mut self,
1474        service: UserService<S>,
1475        reservation: Reservation,
1476        method: String,
1477        params: serde_json::Value,
1478        ctx: ServerContext,
1479        cancellation: CancellationToken,
1480        default_timeout: Duration,
1481    ) {
1482        let state = Arc::clone(&self.state);
1483        let completion_gate = self.protocol.completion_gate();
1484        let permit = Arc::clone(&reservation._permit);
1485        let trace = self.trace;
1486        self.protocol.spawn(
1487            async move {
1488                let id = reservation.id.clone();
1489                let trace_id = id.clone();
1490                let trace_method = method.clone();
1491                let handler_timeout = HandlerTimeout::new(
1492                    default_timeout,
1493                    trace,
1494                    trace_method.clone(),
1495                    trace_id.clone(),
1496                );
1497                let call =
1498                    IncomingCall::request(method, id, params, ctx, state, handler_timeout.clone());
1499                let partial_result_scope = call.context().partial_result_scope();
1500                let result =
1501                    run_handler_with_deadline(service.call(call), cancellation, handler_timeout)
1502                        .await;
1503                if let Some(scope) = partial_result_scope {
1504                    scope.finish();
1505                }
1506                let result = match result {
1507                    ServiceResult::Response(value) => encode_body(&value),
1508                    ServiceResult::Error(error) => Err(error),
1509                    ServiceResult::NoResponse => {
1510                        Err(LspError::internal("request service returned no response"))
1511                    }
1512                };
1513                completion_gate.complete(reservation, result);
1514            },
1515            permit,
1516        );
1517    }
1518
1519    /// The engine's one close operation (ADR 0018).
1520    ///
1521    /// Every close cause runs exactly these steps, in this order, and a second
1522    /// call is a no-op: new outbound work is rejected, the session is
1523    /// cancelled, every pending `ClientHandle` request is resolved, the inbound,
1524    /// outbound, and progress registries are emptied, every handler task is
1525    /// aborted and then joined, and the outbound queue is closed before the
1526    /// writer task is joined. No task is detached and no pending `ClientHandle`
1527    /// future is left unresolved.
1528    async fn close(&mut self) {
1529        if matches!(self.lifecycle, Lifecycle::Exited) {
1530            return;
1531        }
1532        self.lifecycle = Lifecycle::Exited;
1533        self.protocol.close().await;
1534        // Documents and notebooks are Server endpoint state, so their lifecycle
1535        // stays out of the shared session even though close releases retained
1536        // snapshots.
1537        self.documents.clear();
1538        self.notebooks.clear();
1539    }
1540}
1541
1542enum Flow {
1543    Continue,
1544    /// A terminal path — `exit`, or the close a failed initialize transaction
1545    /// enters (ADR 0018) once its fixed error is enqueued — requesting the
1546    /// engine's one close operation with the cause that reached it.
1547    Close(CloseCause),
1548}
1549
1550/// Select the reported cause after close has quiesced every task that could
1551/// fail a required outbound admission (ADR 0026).
1552#[cfg(all(test, not(target_arch = "wasm32")))]
1553fn final_close_cause(close: &CloseSignal<CloseCause>, out_tx: &OutboundQueue) -> CloseCause {
1554    let recorded = close
1555        .take_cause()
1556        .expect("every path out of the read-loop records its close cause");
1557    if out_tx.failure().is_cancelled() {
1558        CloseCause::WriterFailed
1559    } else {
1560        recorded
1561    }
1562}
1563
1564#[cfg(all(test, not(target_arch = "wasm32")))]
1565mod tests {
1566    use gen_lsp_types::ProgressToken;
1567    use tracing_subscriber::layer::SubscriberExt;
1568
1569    use crate::session::{InboundRegistry, TaskGroup};
1570
1571    use super::*;
1572
1573    /// Run `gate_progress_cancel` against `registry` with `raw_params`,
1574    /// capturing every tracing event the call emits on this thread.
1575    fn gated_cancel(
1576        registry: &ProgressRegistry,
1577        raw_params: &'static [u8],
1578    ) -> (BuiltInGate, crate::test_util::EventCapture) {
1579        let _capture = crate::test_util::tracing_capture_lock();
1580        let events = crate::test_util::EventCapture::new();
1581        let subscriber = tracing_subscriber::registry().with(events.clone());
1582        let gate = tracing::subscriber::with_default(subscriber, || {
1583            gate_progress_cancel(registry, &Bytes::from_static(raw_params))
1584        });
1585        (gate, events)
1586    }
1587
1588    #[test]
1589    fn a_matching_cancellable_token_fires_and_opens_the_hook_gate() {
1590        let registry = ProgressRegistry::default();
1591        let cancellation = CancellationToken::new();
1592        registry.register(ProgressToken::Int(1), true, cancellation.clone());
1593
1594        let (gate, events) = gated_cancel(&registry, br#"{"token": 1}"#);
1595
1596        assert!(
1597            matches!(gate, BuiltInGate::RunHook),
1598            "a successful decode lets the hook run"
1599        );
1600        assert!(cancellation.is_cancelled());
1601        assert!(
1602            registry.is_active(&ProgressToken::Int(1)),
1603            "cancellation never ends the progress: the token stays registered"
1604        );
1605        assert!(
1606            events.messages().is_empty(),
1607            "a matched cancel logs nothing, got {:?}",
1608            events.messages()
1609        );
1610    }
1611
1612    #[test]
1613    fn non_cancellable_and_inactive_tokens_log_at_debug_and_keep_the_gate_open() {
1614        let registry = ProgressRegistry::default();
1615        let plain = CancellationToken::new();
1616        registry.register(ProgressToken::Int(1), false, plain.clone());
1617
1618        let (gate, events) = gated_cancel(&registry, br#"{"token": 1}"#);
1619        assert!(matches!(gate, BuiltInGate::RunHook));
1620        assert!(!plain.is_cancelled(), "a non-cancellable token never fires");
1621        assert!(
1622            events.contains_at(tracing::Level::DEBUG, "non-cancellable token"),
1623            "got {:?}",
1624            events.messages()
1625        );
1626
1627        let (gate, events) = gated_cancel(&registry, br#"{"token": 99}"#);
1628        assert!(matches!(gate, BuiltInGate::RunHook));
1629        assert!(
1630            events.contains_at(tracing::Level::DEBUG, "unknown or ended token"),
1631            "got {:?}",
1632            events.messages()
1633        );
1634        assert!(
1635            registry.is_active(&ProgressToken::Int(1)),
1636            "an unknown token leaves the registry untouched"
1637        );
1638    }
1639
1640    #[test]
1641    fn malformed_cancel_params_log_at_debug_and_close_the_hook_gate() {
1642        let registry = ProgressRegistry::default();
1643        let cancellation = CancellationToken::new();
1644        registry.register(ProgressToken::Int(1), true, cancellation.clone());
1645
1646        for raw in [
1647            br#"{"token": true}"#.as_slice(),
1648            br#"{}"#.as_slice(),
1649            b"not json".as_slice(),
1650        ] {
1651            let (gate, events) = gated_cancel(&registry, raw);
1652            assert!(
1653                matches!(gate, BuiltInGate::ProtocolFailure),
1654                "malformed params {raw:?} skip the hook"
1655            );
1656            assert!(
1657                events.contains_at(
1658                    tracing::Level::DEBUG,
1659                    "malformed window/workDoneProgress/cancel"
1660                ),
1661                "got {:?}",
1662                events.messages()
1663            );
1664        }
1665        assert!(
1666            !cancellation.is_cancelled(),
1667            "malformed params never cancel anything"
1668        );
1669    }
1670
1671    fn content_change(with_range: bool) -> gen_lsp_types::TextDocumentContentChangeEvent {
1672        if with_range {
1673            gen_lsp_types::TextDocumentContentChangePartial::new(
1674                gen_lsp_types::Range {
1675                    start: gen_lsp_types::Position::new(0, 0),
1676                    end: gen_lsp_types::Position::new(0, 1),
1677                },
1678                None,
1679                "replacement".to_string(),
1680            )
1681            .into()
1682        } else {
1683            gen_lsp_types::TextDocumentContentChangeWholeDocument {
1684                text: "replacement".to_string(),
1685            }
1686            .into()
1687        }
1688    }
1689
1690    #[test]
1691    fn sync_kind_validation_accepts_only_compatible_change_shapes() {
1692        assert!(
1693            validate_sync_changes(TextDocumentSyncKind::Full, &[content_change(false)]).is_ok()
1694        );
1695        assert!(
1696            validate_sync_changes(TextDocumentSyncKind::Full, &[content_change(true)]).is_err()
1697        );
1698        assert!(
1699            validate_sync_changes(
1700                TextDocumentSyncKind::Incremental,
1701                &[content_change(true), content_change(false)],
1702            )
1703            .is_ok()
1704        );
1705        assert!(
1706            validate_sync_changes(TextDocumentSyncKind::None, &[content_change(false)]).is_err()
1707        );
1708    }
1709
1710    #[test]
1711    fn the_first_requester_records_the_cause_and_later_ones_do_not_replace_it() {
1712        let close = CloseSignal::new();
1713        assert!(!close.requested().is_cancelled());
1714
1715        close.request(CloseCause::WriterFailed);
1716        close.request(CloseCause::Exit { code: 0 });
1717        close.request(CloseCause::ReaderEof);
1718
1719        assert!(
1720            close.requested().is_cancelled(),
1721            "requesting close wakes the read-loop"
1722        );
1723        assert!(
1724            matches!(close.take_cause(), Some(CloseCause::WriterFailed)),
1725            "the first cause requested is the one reported"
1726        );
1727        assert!(
1728            close.take_cause().is_none(),
1729            "the cause is taken once, by the read-loop that ran the close"
1730        );
1731    }
1732
1733    #[test]
1734    fn a_late_required_enqueue_failure_overrides_an_earlier_close_cause() {
1735        let message = RawMessage::Notification {
1736            method: "test/required".into(),
1737            params: Bytes::new(),
1738        };
1739        let (queue, _rx) = OutboundQueue::bounded(1, usize::MAX);
1740        queue.send(message.clone()).unwrap();
1741        let close = CloseSignal::new();
1742        close.request(CloseCause::InitializeFailed);
1743
1744        assert!(queue.send_required(message).is_err());
1745
1746        assert!(matches!(
1747            final_close_cause(&close, &queue),
1748            CloseCause::WriterFailed
1749        ));
1750    }
1751
1752    #[test]
1753    fn every_cause_maps_to_one_outcome_or_a_transport_error() {
1754        assert_eq!(
1755            CloseCause::Exit { code: 0 }.into_result().unwrap(),
1756            Outcome::Exit { code: 0 }
1757        );
1758        assert_eq!(
1759            CloseCause::ReaderEof.into_result().unwrap(),
1760            Outcome::TransportClosed
1761        );
1762        assert_eq!(
1763            CloseCause::WriterFailed.into_result().unwrap(),
1764            Outcome::WriterFailed
1765        );
1766        assert_eq!(
1767            CloseCause::InitializeFailed.into_result().unwrap(),
1768            Outcome::InitializeFailed
1769        );
1770        assert!(matches!(
1771            CloseCause::ReaderFailed(TransportError::Malformed("bad".into())).into_result(),
1772            Err(Error::Transport(_))
1773        ));
1774    }
1775
1776    /// A peer may reuse a request ID once the previous request under it has
1777    /// been answered. The completion gate is scoped to the reservation, not the
1778    /// ID, so the first request's task cannot answer the second request when it
1779    /// finishes after its own entry was claimed.
1780    #[test]
1781    fn a_stale_reservation_cannot_claim_a_reused_request_id() {
1782        let (out_tx, mut out_rx) =
1783            OutboundQueue::new(crate::ResourcePolicy::default().max_outbound_messages);
1784        let registry = InboundRegistry::new(2);
1785        let id = RequestId::Number(2);
1786        let session = CancellationToken::new();
1787
1788        let first = registry
1789            .reserve(id.clone(), Some(&session))
1790            .expect("the id is free")
1791            .reservation;
1792        assert!(
1793            matches!(
1794                registry.reserve(id.clone(), Some(&session)),
1795                Err(InboundReserveError::DuplicateId)
1796            ),
1797            "an in-flight id is not reserved twice"
1798        );
1799
1800        // `$/cancelRequest` claims the gate and answers the first request.
1801        let cancelled = registry
1802            .claim_cancellation(&id)
1803            .expect("the first request is cancellable");
1804        enqueue_encoded(&out_tx, cancelled.id, Err(LspError::RequestCancelled));
1805        // The peer then reuses the id for a new request.
1806        let second = registry
1807            .reserve(id.clone(), Some(&session))
1808            .expect("the id is free once the first request is answered")
1809            .reservation;
1810
1811        // The first request's task only now produces a result.
1812        registry.complete(&out_tx, first, encode_body(&"race"));
1813        registry.complete(&out_tx, second, encode_body(&"reused"));
1814
1815        assert_eq!(
1816            out_rx.try_recv().unwrap().id(),
1817            Some(&id),
1818            "the cancellation answers the first request"
1819        );
1820        let answer = out_rx.try_recv().expect("the second request is answered");
1821        match answer {
1822            RawMessage::Response {
1823                result: Ok(body), ..
1824            } => assert_eq!(
1825                serde_json::from_slice::<String>(&body).unwrap(),
1826                "reused",
1827                "the second request gets its own result, not the stale one"
1828            ),
1829            other => panic!("expected a success response, got {other:?}"),
1830        }
1831        assert!(
1832            out_rx.try_recv().is_err(),
1833            "the stale reservation enqueued nothing"
1834        );
1835    }
1836
1837    #[test]
1838    fn an_exhausted_registry_stays_bounded_and_cancellation_releases_its_entry() {
1839        let registry = InboundRegistry::new(1);
1840        let session = CancellationToken::new();
1841        let accepted = registry
1842            .reserve(RequestId::Number(2), Some(&session))
1843            .expect("the one slot is available");
1844
1845        for id in 3..=66 {
1846            assert!(matches!(
1847                registry.reserve(RequestId::Number(id), Some(&session)),
1848                Err(InboundReserveError::CapacityExhausted)
1849            ));
1850        }
1851        assert_eq!(
1852            registry.inner.lock().unwrap().entries.len(),
1853            1,
1854            "the flood retained only the admitted registry entry"
1855        );
1856
1857        let cancelled = registry
1858            .claim_cancellation(&RequestId::Number(2))
1859            .expect("the admitted request is cancellable");
1860        assert!(
1861            registry.inner.lock().unwrap().entries.is_empty(),
1862            "cancellation releases the registry entry"
1863        );
1864        drop(cancelled);
1865        assert!(matches!(
1866            registry.reserve(RequestId::Number(67), Some(&session)),
1867            Err(InboundReserveError::CapacityExhausted)
1868        ));
1869        drop(accepted);
1870        assert!(
1871            registry
1872                .reserve(RequestId::Number(67), Some(&session))
1873                .is_ok(),
1874            "capacity returns after the admitted task drops its reservation"
1875        );
1876    }
1877
1878    #[cfg(not(target_arch = "wasm32"))]
1879    #[tokio::test]
1880    async fn a_finished_task_holds_capacity_until_its_handle_is_reaped() {
1881        let registry = InboundRegistry::new(1);
1882        let session = CancellationToken::new();
1883        let accepted = registry
1884            .reserve(RequestId::Number(2), Some(&session))
1885            .expect("the one slot is available");
1886        let permit = Arc::clone(&accepted.reservation._permit);
1887        let (out_tx, _out_rx) =
1888            OutboundQueue::new(crate::ResourcePolicy::default().max_outbound_messages);
1889        registry.complete(&out_tx, accepted.reservation, encode_body(&"done"));
1890
1891        let mut tasks = TaskGroup::new(default_runtime());
1892        tasks.spawn(async {}, permit);
1893        while !tasks.handles[0].handle.is_finished() {
1894            tasks.runtime.yield_now().await;
1895        }
1896        assert_eq!(tasks.handles.len(), 1, "the finished handle is still owned");
1897        assert!(matches!(
1898            registry.reserve(RequestId::Number(3), Some(&session)),
1899            Err(InboundReserveError::CapacityExhausted)
1900        ));
1901
1902        tasks.reap_finished().await;
1903        assert!(tasks.handles.is_empty(), "the finished handle was reaped");
1904        assert!(
1905            registry
1906                .reserve(RequestId::Number(3), Some(&session))
1907                .is_ok(),
1908            "reaping the handle releases its admission permit"
1909        );
1910    }
1911
1912    #[cfg(not(target_arch = "wasm32"))]
1913    #[tokio::test]
1914    async fn aborting_the_task_group_releases_disconnect_capacity() {
1915        let registry = InboundRegistry::new(1);
1916        let session = CancellationToken::new();
1917        let accepted = registry
1918            .reserve(RequestId::Number(2), Some(&session))
1919            .expect("the one slot is available");
1920        let permit = Arc::clone(&accepted.reservation._permit);
1921        let mut tasks = TaskGroup::new(default_runtime());
1922        tasks.spawn(std::future::pending(), permit);
1923
1924        registry.close_all();
1925        drop(accepted);
1926        assert!(registry.inner.lock().unwrap().entries.is_empty());
1927        assert!(matches!(
1928            registry.reserve(RequestId::Number(3), Some(&session)),
1929            Err(InboundReserveError::CapacityExhausted)
1930        ));
1931
1932        tasks.abort_and_join().await;
1933        assert!(tasks.handles.is_empty(), "disconnect joined every task");
1934        assert!(
1935            registry
1936                .reserve(RequestId::Number(3), Some(&session))
1937                .is_ok(),
1938            "joining aborted tasks releases their admission permits"
1939        );
1940    }
1941
1942    #[test]
1943    fn only_a_shutdown_exit_reports_code_zero() {
1944        assert_eq!(Outcome::Exit { code: 0 }.code(), 0);
1945        assert_eq!(Outcome::Exit { code: 1 }.code(), 1);
1946        assert_eq!(Outcome::TransportClosed.code(), 1);
1947        assert_eq!(Outcome::WriterFailed.code(), 1);
1948        assert_eq!(Outcome::InitializeFailed.code(), 1);
1949    }
1950
1951    // --- Runtime presence detection -------------------------------------------
1952
1953    /// A transport that never starts: the runtime check fails before `split`
1954    /// is reached, so its halves only need to exist.
1955    struct DetectRuntimeTransport;
1956
1957    impl Transport for DetectRuntimeTransport {
1958        type Reader = DetectRuntimeReader;
1959        type Writer = DetectRuntimeWriter;
1960
1961        fn split(self) -> (Self::Reader, Self::Writer) {
1962            (DetectRuntimeReader, DetectRuntimeWriter)
1963        }
1964    }
1965
1966    struct DetectRuntimeReader;
1967
1968    impl TransportReader for DetectRuntimeReader {
1969        async fn recv(&mut self) -> std::result::Result<RawMessage, TransportError> {
1970            Err(TransportError::Closed)
1971        }
1972    }
1973
1974    struct DetectRuntimeWriter;
1975
1976    impl TransportWriter for DetectRuntimeWriter {
1977        async fn send(&mut self, _msg: RawMessage) -> std::result::Result<(), TransportError> {
1978            Ok(())
1979        }
1980
1981        async fn shutdown(self) -> std::result::Result<(), TransportError> {
1982            Ok(())
1983        }
1984    }
1985
1986    #[test]
1987    fn serving_without_a_tokio_runtime_reports_the_missing_runtime() {
1988        // A plain `#[test]` runs on a thread without a Tokio runtime. The
1989        // runtime check precedes every await, so one poll reports the error.
1990        let server = Server::builder(()).build().expect("an empty server builds");
1991        let mut serving = Box::pin(server.serve(DetectRuntimeTransport));
1992        let waker = futures_util::task::noop_waker();
1993        let mut cx = std::task::Context::from_waker(&waker);
1994        let outcome = match std::future::Future::poll(serving.as_mut(), &mut cx) {
1995            std::task::Poll::Ready(outcome) => outcome,
1996            std::task::Poll::Pending => panic!("the missing runtime is reported on the first poll"),
1997        };
1998
1999        assert!(
2000            matches!(outcome, Err(Error::RuntimeRequired)),
2001            "serving without a runtime reports the missing runtime, got {outcome:?}"
2002        );
2003    }
2004
2005    // --- Outbound queue depth observability tests ----------------------------
2006
2007    enum TestOutboundNotification {}
2008
2009    impl crate::types::notification::Notification for TestOutboundNotification {
2010        type Params = serde_json::Value;
2011        const METHOD: &'static str = "test/outbound-notification";
2012    }
2013
2014    enum TestOutboundRequest {}
2015
2016    impl crate::types::request::Request for TestOutboundRequest {
2017        type Params = serde_json::Value;
2018        type Result = String;
2019        const METHOD: &'static str = "test/outbound-request";
2020    }
2021
2022    fn send_loop_message(tag: u8) -> RawMessage {
2023        RawMessage::Notification {
2024            method: "test/send-loop".into(),
2025            params: Bytes::from(vec![tag]),
2026        }
2027    }
2028
2029    fn encoded_len(message: &RawMessage) -> usize {
2030        crate::transport::envelope::serialize(message)
2031            .expect("the test message encodes")
2032            .len()
2033    }
2034
2035    /// A writer that records what it sent and fails the `fail_on_send`-th send
2036    /// (1-based; `None` never fails), driving the send-loop through its
2037    /// success, draining, and terminal-failure paths.
2038    struct ScriptedWriter {
2039        outbox: Arc<Mutex<Vec<RawMessage>>>,
2040        fail_on_send: Option<usize>,
2041        sends: usize,
2042    }
2043
2044    struct SlowWriter {
2045        started: Arc<tokio::sync::Notify>,
2046        releases: Arc<tokio::sync::Semaphore>,
2047    }
2048
2049    impl TransportWriter for SlowWriter {
2050        async fn send(&mut self, _msg: RawMessage) -> std::result::Result<(), TransportError> {
2051            self.started.notify_one();
2052            self.releases
2053                .acquire()
2054                .await
2055                .expect("the test keeps the release gate open")
2056                .forget();
2057            Ok(())
2058        }
2059
2060        async fn shutdown(self) -> std::result::Result<(), TransportError> {
2061            Ok(())
2062        }
2063    }
2064
2065    impl TransportWriter for ScriptedWriter {
2066        async fn send(&mut self, msg: RawMessage) -> std::result::Result<(), TransportError> {
2067            self.sends += 1;
2068            if self.fail_on_send == Some(self.sends) {
2069                return Err(TransportError::Closed);
2070            }
2071            self.outbox.lock().unwrap().push(msg);
2072            Ok(())
2073        }
2074
2075        async fn shutdown(self) -> std::result::Result<(), TransportError> {
2076            Ok(())
2077        }
2078    }
2079
2080    #[tokio::test]
2081    async fn requests_responses_and_notifications_share_one_depth_counter() {
2082        let (queue, _rx) =
2083            OutboundQueue::new(crate::ResourcePolicy::default().max_outbound_messages);
2084        let client = ClientHandle::new(queue.clone(), OutboundRegistry::default(), None);
2085
2086        client
2087            .notify::<TestOutboundNotification>(serde_json::json!({}))
2088            .unwrap();
2089        enqueue_encoded(
2090            &queue,
2091            RequestId::Number(1),
2092            Ok(Bytes::from_static(b"null")),
2093        );
2094
2095        // The request future enqueues synchronously on its first poll, then
2096        // awaits the peer's response.
2097        let pending = client.request::<TestOutboundRequest>(serde_json::json!({}));
2098        futures_util::pin_mut!(pending);
2099        assert!(
2100            futures_util::poll!(pending.as_mut()).is_pending(),
2101            "the request awaits the peer's response"
2102        );
2103        assert_eq!(queue.depth(), 3);
2104        client
2105            .outbound_registry()
2106            .complete(1, Ok(Bytes::from_static(b"\"pong\"")));
2107        let answer = pending.await;
2108        assert_eq!(answer.unwrap(), "pong");
2109
2110        assert_eq!(
2111            queue.depth(),
2112            3,
2113            "a notification, a response, and a request all increment the one counter"
2114        );
2115    }
2116
2117    #[tokio::test]
2118    async fn writer_failure_releases_attempted_and_abandoned_accounting() {
2119        let (queue, rx) = OutboundQueue::new(16);
2120        let client = ClientHandle::new(queue.clone(), OutboundRegistry::default(), None);
2121        for tag in 0..3 {
2122            queue.send(send_loop_message(tag)).unwrap();
2123        }
2124        assert_eq!(queue.depth(), 3);
2125
2126        let outbox = Arc::new(Mutex::new(Vec::new()));
2127        let writer = ScriptedWriter {
2128            outbox: outbox.clone(),
2129            fail_on_send: Some(2),
2130            sends: 0,
2131        };
2132        let close = CloseSignal::new();
2133        send_loop(writer, rx, client, close.clone()).await;
2134
2135        assert_eq!(
2136            queue.depth(),
2137            0,
2138            "writer failure abandons every queued slot"
2139        );
2140        assert_eq!(
2141            queue.encoded_bytes(),
2142            0,
2143            "writer failure abandons every queued byte charge"
2144        );
2145        assert_eq!(
2146            outbox.lock().unwrap().len(),
2147            1,
2148            "only the first send landed on the transport"
2149        );
2150        assert!(
2151            matches!(close.take_cause(), Some(CloseCause::WriterFailed)),
2152            "the writer reports its terminal failure"
2153        );
2154    }
2155
2156    #[tokio::test]
2157    async fn draining_after_close_decrements_every_message_in_order() {
2158        let (queue, rx) = OutboundQueue::new(2);
2159        let client = ClientHandle::new(queue.clone(), OutboundRegistry::default(), None);
2160        for tag in 0..3 {
2161            queue.send(send_loop_message(tag)).unwrap();
2162        }
2163        client.close_outbound();
2164
2165        let outbox = Arc::new(Mutex::new(Vec::new()));
2166        let writer = ScriptedWriter {
2167            outbox: outbox.clone(),
2168            fail_on_send: None,
2169            sends: 0,
2170        };
2171        let close = CloseSignal::new();
2172        send_loop(writer, rx, client, close.clone()).await;
2173
2174        assert_eq!(
2175            queue.depth(),
2176            0,
2177            "draining decrements every queued message, above and below the threshold"
2178        );
2179        assert_eq!(
2180            queue.encoded_bytes(),
2181            0,
2182            "close releases every encoded-byte charge after draining"
2183        );
2184        let tags: Vec<u8> = outbox
2185            .lock()
2186            .unwrap()
2187            .iter()
2188            .map(|msg| match msg {
2189                RawMessage::Notification { params, .. } => params[0],
2190                other => panic!("expected notifications, got {other:?}"),
2191            })
2192            .collect();
2193        assert_eq!(
2194            tags,
2195            vec![0, 1, 2],
2196            "draining never drops or reorders a message"
2197        );
2198        assert!(
2199            close.take_cause().is_none(),
2200            "clean draining is not a writer failure"
2201        );
2202    }
2203
2204    #[tokio::test]
2205    async fn a_slow_reader_cannot_grow_count_or_bytes_past_the_policy() {
2206        let message_bytes = encoded_len(&send_loop_message(0));
2207        let (queue, rx) = OutboundQueue::bounded(2, message_bytes * 2);
2208        let client = ClientHandle::new(queue.clone(), OutboundRegistry::default(), None);
2209        queue.send(send_loop_message(0)).unwrap();
2210        queue.send(send_loop_message(1)).unwrap();
2211
2212        let started = Arc::new(tokio::sync::Notify::new());
2213        let releases = Arc::new(tokio::sync::Semaphore::new(0));
2214        let writer = SlowWriter {
2215            started: started.clone(),
2216            releases: releases.clone(),
2217        };
2218        let close = CloseSignal::new();
2219        let serving = tokio::spawn(send_loop(writer, rx, client.clone(), close));
2220
2221        started.notified().await;
2222        assert_eq!(queue.depth(), 2, "the in-flight write remains accounted");
2223        assert_eq!(queue.encoded_bytes(), message_bytes * 2);
2224        assert!(matches!(
2225            queue.send(send_loop_message(2)),
2226            Err(crate::client::OutboundSendError::Overloaded)
2227        ));
2228        assert_eq!(queue.depth(), 2, "the rejected send consumes no slot");
2229        assert_eq!(
2230            queue.encoded_bytes(),
2231            message_bytes * 2,
2232            "the rejected send consumes no bytes"
2233        );
2234
2235        releases.add_permits(1);
2236        for _ in 0..100 {
2237            if queue.depth() == 1 {
2238                break;
2239            }
2240            tokio::task::yield_now().await;
2241        }
2242        assert_eq!(queue.depth(), 1, "a successful write releases one slot");
2243        assert_eq!(
2244            queue.encoded_bytes(),
2245            message_bytes,
2246            "a successful write releases exactly its encoded bytes"
2247        );
2248
2249        client.close_outbound();
2250        releases.add_permits(1);
2251        serving.await.unwrap();
2252        assert_eq!(queue.depth(), 0, "close drains the remaining message");
2253        assert_eq!(
2254            queue.encoded_bytes(),
2255            0,
2256            "close releases all byte accounting"
2257        );
2258    }
2259}