JsonRpcSession Type
A typed JSON-RPC 2.0 conversation with a child process that speaks `Content-Length`-framed JSON — a
language server (LSP), a build server (BSP), or an MCP-style tool. It is the layer above
`ContentLengthSession`: that type frames bytes, this one serializes values, allocates and correlates
request ids, separates answers from notifications, and bounds a call end to end.
```fsharp
task {
let command = (Command.create "language-server").KeepStdinOpen()
match! command.StartAsync() with
| Error err -> eprintfn $"{err.Message}"
| Ok proc ->
use proc = proc
let session = JsonRpcSession(proc)
match! session.RequestAsync("initialize", parameters, Lsp.Default.InitializeParams, Lsp.Default.InitializeResult, TimeSpan.FromSeconds 30.0) with
| Ok result -> printfn $"{result.ServerInfo}"
| Error err -> eprintfn $"{err.Message}"
}
```
**Not a debug-adapter (DAP) client.** DAP borrows LSP's `Content-Length` framing but not its
envelope: its messages look like `{"seq":1,"type":"request","command":"next","arguments":{}}` — no
`jsonrpc`, no `method`, no `id` — so they are not JSON-RPC, and this session ends on the first one
with `ProcessError.Parse` rather than guessing. Drive a debug adapter with `ContentLengthSession`
directly and decode that envelope yourself.
**This session owns the run's framed transport.** Constructing it creates the one
`ContentLengthSession` over the handle and immediately claims its frames, so the handle's stdout
belongs to this session: capturing/streaming verbs, a `PtySession`, or a second framed session on the
same `RunningProcess` are refused afterwards, and there is deliberately no way to enumerate the raw
frames alongside it — a second reader would tear the peer's messages between two consumers. Dispose
the `RunningProcess` (or its owning `ProcessGroup`) to reap the tree; the session itself holds no OS
resource of its own. Build the command with `Command.KeepStdinOpen()`, or every send reports a typed
`ProcessError.Unsupported`.
**Concurrency.** Requests may be issued concurrently: each gets its own `id`, and the router matches
every answer against the request that is waiting for that exact `id`. Sends are serialized, so two
concurrent requests can never interleave inside one frame — and a call whose deadline or token ends
it while it is still queued behind another send fails alone, having written nothing.
**Failures are typed, never raw exceptions and never a silent hang** (see `ProcessError`):
- the peer answered with an `error` object — `ProcessError.JsonRpc`, carrying its `code`, `message`,
and the raw JSON of `data`;
- the request timed out — `ProcessError.Timeout` (per-request overloads only; the budget covers the
whole call, writing the frame included, so a peer that stopped reading its own stdin cannot hang
one. Without a timeout a request waits until the peer answers, the peer's output ends, or the
caller's token fires);
- the caller's `CancellationToken` fired — `ProcessError.Cancelled`;
- either of those two interrupted a send **that had begun writing its frame**: the frame may have
reached the peer truncated, and no peer can resynchronize from that, so the failure also ends the
conversation — pending requests fail with it and every later request/send reports it instead of
writing into a stream the peer can no longer read. A torn *outgoing* frame does not corrupt what
the peer says, so `MessagesAsync` keeps delivering incoming messages until the peer's output ends.
An interruption that landed **before** the write began — a token that was already cancelled, a
deadline that elapsed while the call was still queued behind another send, or one that elapsed
while the first send was still waiting for a `Command.Stdin(source)` feeder to hand over the pipe —
wrote nothing, so it fails only that one call and leaves the conversation usable;
- the frame backlog underneath hit a `Command.StreamBuffer` cap configured with
`StreamFullMode.Error` — `ProcessError.OutputTooLarge`, which ends the session exactly as a
protocol failure does (see the backlog note below);
- the peer's framed output ended (it exited, or closed stdout) while a request was pending —
`ProcessError.Io`, and every later verb fails the same way instead of waiting forever;
- the peer sent something that is not a JSON-RPC message — `ProcessError.Parse`, which ends the whole
session: a peer that is not speaking the protocol cannot be understood message by message. Pending
requests all fail with it, and `MessagesAsync` faults with `ProcessException` carrying it.
An answer whose `id` matches no waiting request — typically a late reply to a request that already
timed out — is discarded, since the caller has already been told the request failed.
**Two backlogs, two knobs.** `messageBacklog` (the third constructor argument, 1024 by default)
bounds the DECODED incoming messages waiting for `MessagesAsync`. Old notifications may be dropped
and are counted in `DroppedMessages`; a peer request is never silently dropped — if it would be
evicted, the conversation ends with `ProcessError.OutputTooLarge`. The decoded-message backlog does
not count a total message or byte volume, so that error carries zero totals (the `ProcessError`
convention for an unreported metric). `Command.StreamBuffer` bounds the
raw frame backlog underneath it, through the `ContentLengthSession` this session owns, and only its
two LOSSLESS full modes apply there: `Backpressure` paces the peer against the router, and `Error`
ends the conversation with `ProcessError.OutputTooLarge` at the cap. The two DROP modes are refused
when this session is constructed — the constructor raises `ProcessException` carrying
`ProcessError.Unsupported`, because silently deleting a queued frame would delete a message the peer
is correlating with a request, and no consumer could tell. Leaving `StreamBuffer` unset keeps the
default unbounded frame backlog.
Constructors
| Constructor |
Description
|
|
A session over `running` using the framing layer's default 16 MiB maximum frame size and a 1024-message inbound backlog.
|
Full Usage:
JsonRpcSession(running, maxFrameBytes, messageBacklog)
Parameters:
RunningProcess
maxFrameBytes : int
messageBacklog : int
Returns: JsonRpcSession
|
A session over `running` with custom frame and decoded-message backlog limits.
|
Full Usage:
JsonRpcSession(running, maxFrameBytes)
Parameters:
RunningProcess
maxFrameBytes : int
Returns: JsonRpcSession
|
A session over `running` with a custom maximum frame size, using the default 1024-message inbound backlog.
|
Instance members
| Instance member |
Description
|
Full Usage:
this.DroppedMessages
Returns: int64
|
How many incoming notifications were dropped because the inbound backlog was full — a consumer that is not enumerating `MessagesAsync`, or is falling behind the peer. Always `0` while the consumer keeps up. Peer requests and answers to this session's own requests are never silently dropped; evicting a peer request ends the conversation with `ProcessError.OutputTooLarge`.
|
|
Close the peer's framed input without cancellation. Equivalent to `FinishInputAsync(CancellationToken.None)`.
|
Full Usage:
this.FinishInputAsync
Parameters:
CancellationToken
Returns: Task<Result<unit, ProcessError>>
|
Close the peer's framed input so it observes EOF — the usual last step of an LSP `shutdown`/`exit` sequence. Unsupported when the command did not keep stdin open. `cancellationToken` bounds only the session gate and framing transport's pre-delivery feeder/gate waits; once EOF delivery starts, it is not cancellable.
|
Full Usage:
this.MaxFrameBytes
Returns: int
|
The maximum framed payload size in either direction, in bytes.
|
|
Enumerate the peer's notifications and its own requests, in arrival order. Answers to this session's requests never appear here — they go to the `RequestAsync` call waiting for them, so this enumeration and the request verbs never compete for the same message. The enumeration ends when the peer's framed output ends, and faults with `ProcessException` when the peer sends something that is not a JSON-RPC message or an unread peer request would overflow the bounded decoded-message backlog. This single-consumer method may be called only once.
|
Full Usage:
this.NotifyAsync
Parameters:
string
parameters : 'P
?options : JsonSerializerOptions
?cancellationToken : CancellationToken
Returns: Task<Result<unit, ProcessError>>
Type parameters: 'P |
Send a notification, serializing `parameters` via reflection-based `System.Text.Json` (`options` omitted uses the BCL defaults). **Trimming / AOT:** not trim-/AOT-safe — use the `JsonTypeInfo` overload (or `NotifyRawAsync`) in a trimmed/NativeAOT app.
|
Full Usage:
this.NotifyAsync
Parameters:
string
parameters : 'P
paramsTypeInfo : JsonTypeInfo<'P>
?cancellationToken : CancellationToken
Returns: Task<Result<unit, ProcessError>>
Type parameters: 'P |
Send a notification, serializing `parameters` with source-generated `JsonTypeInfo` metadata (the trim-/NativeAOT-safe path). A notification carries no `id`: the peer never answers it, so the returned `Result` reports only whether the frame was written.
|
Full Usage:
this.NotifyRawAsync
Parameters:
string
parametersJson : string
?cancellationToken : CancellationToken
Returns: Task<Result<unit, ProcessError>>
|
Send a notification whose `params` are already JSON text (`null` for none) — no `id`, no answer, and no serializer, so this overload is always trim-/NativeAOT-safe.
|
Full Usage:
this.RequestAsync
Parameters:
string
parameters : 'P
options : JsonSerializerOptions
timeout : TimeSpan
?cancellationToken : CancellationToken
Returns: Task<Result<'R, ProcessError>>
Type parameters: 'P, 'R |
Like the overload above, but the whole call is bounded by `timeout` — writing the frame as well as waiting for the answer (`ProcessError.Timeout` when it elapses). **Trimming / AOT:** not trim-/AOT-safe — use the `JsonTypeInfo` overloads (or `RequestRawAsync`) in a trimmed/NativeAOT app.
|
Full Usage:
this.RequestAsync
Parameters:
string
parameters : 'P
?options : JsonSerializerOptions
?cancellationToken : CancellationToken
Returns: Task<Result<'R, ProcessError>>
Type parameters: 'P, 'R |
Send a request, serializing `parameters` and deserializing the `result` via reflection-based `System.Text.Json` (`options` omitted uses the BCL defaults). Same result/error contract as the `JsonTypeInfo` overload. **Trimming / AOT:** not trim-/AOT-safe — use the `JsonTypeInfo` overloads (or `RequestRawAsync`) in a trimmed/NativeAOT app.
|
Full Usage:
this.RequestAsync
Parameters:
string
parameters : 'P
paramsTypeInfo : JsonTypeInfo<'P>
resultTypeInfo : JsonTypeInfo<'R>
timeout : TimeSpan
?cancellationToken : CancellationToken
Returns: Task<Result<'R, ProcessError>>
Type parameters: 'P, 'R |
Like the overload above, but the whole call is bounded by `timeout` — writing the frame as well as waiting for the answer (`ProcessError.Timeout` when it elapses).
|
Full Usage:
this.RequestAsync
Parameters:
string
parameters : 'P
paramsTypeInfo : JsonTypeInfo<'P>
resultTypeInfo : JsonTypeInfo<'R>
?cancellationToken : CancellationToken
Returns: Task<Result<'R, ProcessError>>
Type parameters: 'P, 'R |
Send a request, serializing `parameters` and deserializing the peer's `result` with source-generated `JsonTypeInfo` metadata — the trim-/NativeAOT-safe path. Give both type arguments explicitly when they cannot be inferred, e.g. `session.RequestAsync<InitializeParams, InitializeResult>(...)`. The peer's `error` answer is `ProcessError.JsonRpc`, never a successful result; a `result` that does not fit `'R` (including a JSON `null`) is `ProcessError.Parse`. A `parameters` value the serializer cannot encode raises, like any other invalid argument.
|
Full Usage:
this.RequestRawAsync
Parameters:
string
parametersJson : string
timeout : TimeSpan
?cancellationToken : CancellationToken
Returns: Task<Result<string, ProcessError>>
|
Like the overload above, but the whole call is bounded by `timeout` — writing the frame as well as waiting for the answer, so a peer that stopped reading its stdin cannot hang it either. The call then fails with `ProcessError.Timeout` and its waiter is dropped, so a late answer is discarded. A deadline that interrupts the frame while it is being written may have truncated it, and ends the session; one that elapses before the write begins fails only this call (see the type's docs).
|
Full Usage:
this.RequestRawAsync
Parameters:
string
parametersJson : string
?cancellationToken : CancellationToken
Returns: Task<Result<string, ProcessError>>
|
Send a request whose `params` are already JSON text (`null` for a request with none) and return the raw JSON text of the peer's `result`. The serializer-free path: nothing is reflected over and no `JsonTypeInfo` is needed, so it is always trim-/NativeAOT-safe, and a `result` of `null` comes back as the text `"null"` rather than failing a typed read. Without a timeout the call ends when the peer answers, its output ends, or `cancellationToken` fires — pass a timeout (or a token) for a peer that may go silent while still running.
|
Full Usage:
this.RespondAsync
Parameters:
JsonRpcMessage
result : 'R
?options : JsonSerializerOptions
?cancellationToken : CancellationToken
Returns: Task<Result<unit, ProcessError>>
Type parameters: 'R |
Answer a peer request with a `result` serialized via reflection-based `System.Text.Json` (`options` omitted uses the BCL defaults). Only the first response that starts writing is accepted by the session that received the request; an attempt that fails before writing may be retried, while a later or cross-session attempt is a typed `ProcessError.Unsupported`. **Trimming / AOT:** not trim-/AOT-safe — use the `JsonTypeInfo` overload (or `RespondRawAsync`) in a trimmed/NativeAOT app.
|
Full Usage:
this.RespondAsync
Parameters:
JsonRpcMessage
result : 'R
resultTypeInfo : JsonTypeInfo<'R>
?cancellationToken : CancellationToken
Returns: Task<Result<unit, ProcessError>>
Type parameters: 'R |
Answer a peer request with a `result` serialized through source-generated `JsonTypeInfo` metadata (the trim-/NativeAOT-safe path). Only the first response that starts writing is accepted by the session that received the request; an attempt that fails before writing may be retried, while a later or cross-session attempt is a typed `ProcessError.Unsupported`.
|
Full Usage:
this.RespondErrorAsync
Parameters:
JsonRpcMessage
code : int
message : string
?cancellationToken : CancellationToken
Returns: Task<Result<unit, ProcessError>>
|
Answer a peer request with a JSON-RPC `error` object — the honest reply when the peer asks for something this client cannot do (`-32601` "method not found", `-32602` "invalid params", and the rest of the reserved range are the conventional codes). Only the first response that starts writing is accepted by the session that received the request; an attempt that fails before writing may be retried, while a later or cross-session attempt is a typed `ProcessError.Unsupported`.
|
Full Usage:
this.RespondRawAsync
Parameters:
JsonRpcMessage
resultJson : string
?cancellationToken : CancellationToken
Returns: Task<Result<unit, ProcessError>>
|
Answer a peer request (`JsonRpcMessage.IsRequest`) with a `result` that is already JSON text — serializer-free, so always trim-/NativeAOT-safe. Only the first response that starts writing is accepted by the session that received the request; an attempt that fails before writing may be retried. A later attempt, an attempt through another session, or answering a notification is a typed `ProcessError.Unsupported`.
|
ProcessKit API Reference