Outbound gateway
Send CRUD notifications to an external service with BaseGateway, and distinguish generated client helpers from server actions.
BaseGateway generates notification helpers for an outbound WebSocket client. It does not create automatic CRUD actions, subscribe endpoints, or rooms on the server. Write the receiving controller contract separately.
Dependencies#
Keep the first application's lilyrs, Serde, and Tokio dependencies and add:
lily_websocket_client = { version = "0.1.0", default-features = false }
lily_websocket_derive = "0.1.0"
lily_injection = "0.1.0"
lily_error = "0.1.0"
async-trait = "0.1"
tokio-util = "0.7"BaseGateway is an exception to the published 0.1.0 facade: it is not re-exported through lilyrs::websocket or lilyrs::websocket_client. This example imports the derive and client directly from their published crates. Generated lifecycle code also uses the async_trait, lily_injection, and lily_error paths; do not rely on transitive dependencies. Normal DI setup does not need these extra dependencies.
The direct client's default-features = false keeps automatic single registration disabled. If the same graph enables websocket-client on lilyrs or single on the client, Cargo feature unification overrides that choice and managed-client configuration is also required. Do not add those features to this manual gateway example.
Define a gateway#
use lily_websocket_client::{TokioWsClient, WsClient};
use lily_websocket_derive::BaseGateway;
use serde::Serialize;
use tokio_util::sync::CancellationToken;
#[derive(Serialize)]
pub struct OrderDto { pub id: String }
#[derive(BaseGateway)]
#[gateway(url = "ws://127.0.0.1:8081/ws", namespace = "orders")]
#[dto_type(OrderDto)]
pub struct OrdersGateway {
#[gateway_client]
client: TokioWsClient,
entity_name: String,
}
pub async fn publish(order: &OrderDto) -> Result<(), Box<dyn std::error::Error>> {
let gateway = OrdersGateway::new();
gateway.client().connect(CancellationToken::new()).await?;
let result = gateway.notify_created(order).await;
let closed = gateway.disconnect().await;
result?;
closed?;
Ok(())
}Download the complete example.
The example demonstrates direct client ownership. The receiving server must have an orders namespace and created action and permit clients without Origin. new() does not connect; await the connect result.
Generated notifications#
| Method | Event |
|---|---|
notify_created |
orders:created |
notify_created_many |
orders:createdMany |
notify_updated |
orders:updated |
notify_deleted, notify_deleted_id |
orders:deleted |
notify_deleted_many |
orders:deletedMany |
notify_deleted_id sends an ID as payload; the other deleted method sends a DTO. The receiving action must explicitly accept this distinction. Helpers for client, is_connected, and disconnect are also generated. Notifications are not a request/ACK flow; send success does not mean the domain operation completed.
Struct and lifecycle rules#
The struct has exactly two fields: a TokioWsClient marked with #[gateway_client], and entity_name: String. Extra fields are rejected because the generated constructor cannot initialize them. URL and namespace are literal metadata; URL credentials and sensitive queries are not accepted.
The macro generates ServiceTrait initialize/dispose. It does not automatically generate container-discovery registration; configure an appropriate service registration separately for DI-managed use. In the manual example, you own connect/disconnect. For dynamic configuration or named connections, the client single/factory model is a more direct starting point.
Sources for this page v0.1.0
This guide is based on the reviewed framework commit.