Outbound WebSocket client
Connect to another Lily service and manage request/ACK correlation and connection ownership in direct, DI single, or factory mode.
lily_websocket_client connects to an external WebSocket server. It serves a different purpose from ClientProxy, which broadcasts to existing server connections. The client expects the canonical Lily v2 envelope.
Choose an ownership mode#
| Facade feature choice | Model |
|---|---|
websocket-client-di |
TokioWsClient and DI helpers; no single/factory service registration, application owns connect/disconnect |
websocket-client |
DI-managed WebSocketClientService in single mode |
websocket-client-factory |
WebSocketClientFactory for named client cells |
Single and factory modes cannot be combined; select the factory feature instead of the base feature. Direct TokioWsClient is available in every mode, but adding websocket-client to a server's dependency graph also enables single service registration and requires [websocket_client] configuration during build. This example selects the public websocket-client-di profile to demonstrate direct ownership without that side effect. The di in its name does not by itself register a managed client.
Do not use the internal __websocket-client feature. An independent client that also needs no DI helpers can alternatively depend directly on lily_websocket_client = { version = "0.1.0", default-features = false }; its imports then use the direct crate name. If another dependency enables single/factory in the same graph, those features unify. The DI model's initialize/dispose ownership applies to managed modes.
Direct client example#
lilyrs = { version = "0.1.0", features = ["websocket", "websocket-client-di", "websocket-redis"] }
serde_json = "1"
tokio-util = "0.7"This dependency replaces the first application's lilyrs line. The file's distributed function is a separate backplane-builder example; for only the direct client, remove that function, the websocket_redis import, and the websocket-redis feature. This example needs no [websocket_client] configuration or managed-client registration; it owns connect and disconnect. The server must allow the client profile without Origin.
use std::time::Duration;
use lilyrs::websocket::{BackplaneRequirement, WsAppBuilder};
use lilyrs::websocket_client::{TokioWsClient, WebSocketClientConfig, WsClient};
use lilyrs::websocket_redis::RedisWebSocketBackplane;
use tokio_util::sync::CancellationToken;
pub fn distributed(builder: WsAppBuilder) -> WsAppBuilder {
builder.backplane::<RedisWebSocketBackplane>(BackplaneRequirement::Required)
}
pub async fn direct_client() -> Result<(), Box<dyn std::error::Error>> {
let client = TokioWsClient::with_config(WebSocketClientConfig {
url: "ws://127.0.0.1:8081/ws".into(),
namespace: Some("hello".into()),
..WebSocketClientConfig::default()
})?;
client.on_async("hello:greeted", |_event, payload| async move {
println!("Yanıt: {} byte", payload.len());
});
client.connect(CancellationToken::new()).await?;
let result = client.request("hello:confirm", serde_json::json!({"name":"Ada"}),
Duration::from_secs(2)).await;
let closed = client.disconnect().await;
let _reply = result?;
closed?;
Ok(())
}Download the complete example.
The function awaits disconnect first even when the request fails. Use on_async for async I/O callbacks and on for short synchronous work. Callback queues, concurrency, and timeouts are bounded; registering the same event replaces the previous listener. * is a wildcard, and off removes a listener.
Requests and delivery#
request allocates a new ACK ID. A matched result is WebSocketReply::Acknowledgement or a correlated Rejection. Pending requests are bounded and removed on success, rejection, timeout, cancellation, or disconnect. Requests are not retried automatically.
Late/unknown ACKs or correlated errors do not reach normal event callbacks; they cause a protocol error and socket closure. Use on_error for uncorrelated errors. Direct-client send reports writer acceptance/flush, not a guarantee that the peer application processed the message. Reconnection does not replay failed writes.
DI single#
For single ownership, select websocket-client instead of websocket-client-di in the lilyrs feature list. Resolve WebSocketClientService from the container rather than constructing its generated Default manually. The following configuration is for that managed service's initialize step.
[websocket_client]
mode = "single"
url = "wss://events.example.com/ws"
namespace = "chat"
subprotocols = ["lily.v2"]
require_subprotocol = true
authorization_bearer_file = "/run/secrets/websocket-token"Initialization does not publish the service before the first connect completes. If capturing the first on_connect event is essential, register its callback on a direct client before connecting. Registering later through DI does not replay the first event.
DI factory#
[websocket_client]
mode = "factory"
[[websocket_client.cells]]
name = "primary"
url = "wss://primary.example.com/ws"
namespace = "events"
[[websocket_client.cells]]
name = "backup"
url = "wss://backup.example.com/ws"
namespace = "events"With websocket-client-factory instead of websocket-client, resolve WebSocketClientFactory and check the result of get("primary"). Empty/duplicate names are rejected before I/O; if a later cell fails to connect, previously opened cells are closed. Each client sends events for its own namespace; do not use names as shortcuts to send to another namespace.
Credentials and TLS#
AuthHeaderProvider runs before the initial handshake and reconnection. BearerTokenFile can reread a token file. This differs from refreshing identity on an already open server connection. Credentials are not carried in URL user-info or sensitive query parameters.
WSS uses hostname verification and WebPKI trust roots; the client's additional_ca_bundle adds public CAs. This differs from Redis backplane custom CAs, which replace roots. disconnect shuts down and joins the supervised runtime with bounds; the container owner closes a managed client.
Sources for this page v0.1.0
This guide is based on the reviewed framework commit.