Cirreum.RemoteConnections.WebSockets
2.0.2
dotnet add package Cirreum.RemoteConnections.WebSockets --version 2.0.2
NuGet\Install-Package Cirreum.RemoteConnections.WebSockets -Version 2.0.2
<PackageReference Include="Cirreum.RemoteConnections.WebSockets" Version="2.0.2" />
<PackageVersion Include="Cirreum.RemoteConnections.WebSockets" Version="2.0.2" />
<PackageReference Include="Cirreum.RemoteConnections.WebSockets" />
paket add Cirreum.RemoteConnections.WebSockets --version 2.0.2
#r "nuget: Cirreum.RemoteConnections.WebSockets, 2.0.2"
#:package Cirreum.RemoteConnections.WebSockets@2.0.2
#addin nuget:?package=Cirreum.RemoteConnections.WebSockets&version=2.0.2
#tool nuget:?package=Cirreum.RemoteConnections.WebSockets&version=2.0.2
Cirreum.RemoteConnections.WebSockets
Raw WebSocket transport for long-lived Cirreum client connections
Overview
Cirreum.RemoteConnections.WebSockets is the raw WebSocket implementation of Cirreum's IRemoteConnection abstraction — a typed, lifecycle-managed client connection backed by ClientWebSocket.
Raw WebSockets are the right transport when the wire format belongs to someone else — telephony media streams, realtime speech APIs, agent host sockets — or when a service needs a long-lived outbound channel with minimal overhead. The package supplies what the platform does not: a receive loop with multi-frame accumulation, a reconnect loop with capped jittered backoff, token refresh per attempt, observable state, and deterministic disposal.
Message routing is a derived-class concern. The default seam decodes Cirreum's
{ "method", "payload" } envelope; bridging a third-party protocol means overriding it and owning
the discriminator.
Usage
Derive a connection type. The framework-supplied context is its first constructor parameter; anything else resolves from the container as usual:
public sealed class VoiceConnection(WebSocketRemoteConnectionContext context)
: WebSocketRemoteConnection(context) {
public Task SendAudioAsync(ReadOnlyMemory<byte> pcm, CancellationToken ct = default) =>
this.SendBytesAsync(pcm, WebSocketMessageType.Binary, ct);
// The provider owns this wire format, so this connection reads the frames itself
// rather than through the Cirreum envelope.
protected override ValueTask OnFrameReceivedAsync(
ReadOnlyMemory<byte> payload, WebSocketMessageType messageType, CancellationToken ct) {
// decode the provider's protocol
return ValueTask.CompletedTask;
}
}
For a Cirreum server on the other end, the default routing already matches what it sends — register handlers by method name and send through the envelope:
public sealed class NotificationConnection(WebSocketRemoteConnectionContext context)
: WebSocketRemoteConnection(context) {
public IDisposable OnNotice(Func<Notice, Task> handler) => this.On("Notice", handler);
public Task AcknowledgeAsync(string id, CancellationToken ct = default) =>
this.SendAsync("Acknowledge", id, ct);
}
Register it, and connect when the caller is ready:
services.AddSingleton(sp => new VoiceConnection(
WebSocketRemoteConnectionContext.Create<VoiceConnection>(sp, new RemoteConnectionOptions("MyApp") {
EndpointUri = new Uri("wss://provider.example.com/realtime"),
Scopes = ["api://contoso/access_as_user"],
})));
For a connection whose lifetime is a session rather than the application — one per phone call, one per bridge — construct one per session and dispose it with the session:
await using var voice = new VoiceConnection(
WebSocketRemoteConnectionContext.Create<VoiceConnection>(services, options));
await voice.ConnectAsync(ct);
Applications composing through a Cirreum application builder normally register through the matching Runtime Extensions package instead, which reduces both shapes to a single builder call and owns the per-session lifetime rather than leaving each application to construct and track its own.
What the base owns
Receive loop — assembles multi-frame messages and hands each complete message to
OnFrameReceivedAsync. A fault in that method is logged, not fatal to the connection.Reconnect loop — raw WebSockets have none of their own. Retries indefinitely with capped, jittered backoff, driving the same state machine. Override
OnReconnectedAsyncto restore server-side state that does not survive a reconnect.Credentials — resolved on every connect and reconnect attempt. A
ClientWebSocketis single-use, so each attempt builds a fresh one and re-reads the credential; refresh is a consequence of that rather than extra machinery. Postures resolve in a fixed order, and the first one set wins:# Posture Set by 1 CredentialProvidera callback on the connection's options 2 AuthorizationHeadercarrying a valuethe connection's options 3 AuthorizationHeaderSettings.Nonethe connection's options, to connect deliberately without one 4 the ambient IRemoteConnectionCredentialSourcethe host runtime, or the application The ambient source is told what it is supplying for — endpoint, the
Scopesthe options declare, and the connection type — and a source registered keyed to that type is preferred over the unkeyed one, so one connection can use a different mechanism than another.A resolved credential has three answers: a value to present,
Noneto connect without one, andnullmeaning none is available — which fails the connect rather than connecting anonymously.Any scheme may be resolved per attempt, not only Bearer, because the socket's headers are set after the credential resolves. Off-browser the credential travels as an
Authorizationheader. In a browser, which cannot set headers on an upgrade, it travels as anaccess_tokenquery parameter — and only Bearer has that equivalent, so a non-Bearer credential in a browser is rejected rather than silently dropped.State and identity —
StateandStateChangedreport the connection's lifecycle;ConnectionIdis assigned by the adapter and stable across reconnects.SubProtocolreports what the server selected, re-read per connection because each reconnect negotiates afresh.Sends are serialized — a WebSocket permits one write at a time, so concurrent callers queue rather than corrupting the stream.
There is no request/response: the transport provides none, and synthesizing one would mean rebuilding correlation, timeouts and cancellation over a one-way pipe.
Documentation
Contribution Guidelines
Be conservative with new abstractions
The API surface must remain stable and meaningful.Limit dependency expansion
Only add foundational, version-stable dependencies.Favor additive, non-breaking changes
Breaking changes ripple through the entire ecosystem.Include thorough unit tests
All primitives and patterns should be independently testable.Document architectural decisions
Context and reasoning should be clear for future maintainers.Follow .NET conventions
Use established patterns from Microsoft.Extensions.* libraries.
Versioning
Cirreum.RemoteConnections.WebSockets follows Semantic Versioning:
- Major - Breaking API changes
- Minor - New features, backward compatible
- Patch - Bug fixes, backward compatible
License
This project is licensed under the MIT License - see the LICENSE file for details.
Cirreum Foundation Framework
Layered simplicity for modern .NET
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Cirreum.Domain (>= 5.0.1)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Cirreum.RemoteConnections.WebSockets:
| Package | Downloads |
|---|---|
|
Cirreum.Runtime.RemoteConnections.WebSockets
Runtime Extensions package for Cirreum raw WebSocket remote connections — app-facing AddRemoteConnection<TConnection>() and AddRemoteConnectionFactory<TConnection>() extensions that register a typed, lifecycle-managed WebSocket client connection from options. |
GitHub repositories
This package is not used by any popular GitHub repositories.