o_sfu/lib.rs
1//! A Selective Forwarding Unit (SFU) for audio/video calls.
2//!
3//! A SFU receives each participant's media once and selectively forwards it to
4//! the others, so an N-party call costs one upload per sender rather than one
5//! per listener and no stream is transcoded or mixed. `o-sfu` runs this model as
6//! a dedicated server that handles room admission, routing topology, media
7//! policy, packet forwarding, signaling and telemetry. Applications provision
8//! rooms over HTTP, browsers connect over WebSocket and media travels over UDP
9//! with `str0m` terminating ICE, DTLS and SRTP.
10//!
11//! # Core Concepts
12//!
13//! `o-sfu` separates the **control plane** for admission and room policy, the
14//! **routing plane** for user-to-connection placement and the **packet plane**
15//! for RTP forwarding on worker loops.
16//!
17//! - **[`Runtime`]**: Owns the process lifecycle, the HTTP/WebSocket servers and graceful shutdown.
18//! - **[`core::server::room::Room`]**: The control plane boundary for a set of participants. It commits membership and media relationships.
19//! - **[`o_sfu_router::Router`]**: The routing plane, a pure, sans-I/O engine owning the placement graph that maps users to connections.
20//! - **[`core::prelude::MediaSession`]**: Orchestrates a user's connection, bridging room intent to transport effects.
21//! - **[`core::server::transport::MediaTransport`]**: Owns the media workers and hides their threading model. Each worker holds a packet loop and applies projected routes to incoming datagrams.
22//!
23//! # Architecture
24//!
25//! Control and routing decisions happen above the packet loops, which apply the
26//! resulting transport state to UDP datagrams.
27//!
28//! ```text
29//! HTTP control API ----------------------> RoomManager
30//! |
31//! WebSocket session -> SfuCore -> MediaSession |
32//! \ /
33//! +----> Room <----+
34//! |
35//! +<----> Router
36//! |
37//! v
38//! MediaTransport
39//! | | |
40//! v v v
41//! RTC workers / packet loops
42//! | | |
43//! UDP:40001 UDP:40002 UDP:40003
44//! | | |
45//! v v v
46//! fanout fanout fanout
47//! ```
48//!
49//! # Admission Edge
50//!
51//! Applications provision rooms through
52//! [`RoomManager::serve_room`](core::server::room::RoomManager::serve_room).
53//! WebSocket clients join through
54//! [`SfuCore::admit_user`](core::prelude::SfuCore::admit_user). Both paths require
55//! JWT authentication before admission.
56//!
57//! ```text
58//! +----------------------------------------+
59//! | Incoming HTTP / WebSocket I/O |
60//! +----------------------------------------+
61//! |
62//! v
63//! +----------------------------------------+
64//! | Axum Router |
65//! +----------------------------------------+
66//! |
67//! +--------------------+-----------+-----------+--------------------+
68//! | | | |
69//! v v v v
70//! [ GET /v1/noop ] [ GET /v1/channel ] [ WebSocket / ] [ Operator Routes ] <-- Routes
71//! (Public Liveness) [ POST /v1/disconnect ] (Signaling Path) - GET /v1/stats
72//! | | | - GET /metrics
73//! | | | - GET /internal/...
74//! | v v |
75//! | +---------------+ +---------------+ +---------------+
76//! | | VerifiedRoom | | Upgrade | | OperatorAccess| <-- Extractors
77//! | | VerifiedClaims| | ConnectInfo | | (Route Layer) |
78//! | | (JWT Header) | | 1st-Frame JWT | | (Bearer/Local)|
79//! | +---------------+ +---------------+ +---------------+
80//! | | | |
81//! v v v v
82//! +---------------+ +---------------+ +---------------+ +---------------+
83//! | noop | | room | | upgrade | | stats | <-- Handlers
84//! | | | disconnect | | -> admit_user | | metrics |
85//! | -> 200 JSON | | -> Room State | | -> WS Session | | diagnostics_* |
86//! +---------------+ +---------------+ +---------------+ +---------------+
87//! ```
88//!
89//! - **HTTP**: Parses server-to-server requests using [`http::CreateRoomQuery`]. Verifies [`auth::HttpRoomClaims`]. The request that creates the current room fixes its signing key.
90//! - **WebSocket**: Client connection frames are decoded by [`websocket::decode_auth_payload_text`]. A hint selects a candidate key. Claims are verified, normalized into [`auth::WebSocketConnectClaims`] and trusted for access.
91//!
92//! # Security Model
93//!
94//! `o-sfu` secures two planes independently. Application-layer JWTs gate room
95//! admission on the control plane. `str0m` encrypts media on the packet plane
96//! with DTLS-SRTP. Signaling transport confidentiality is terminated at the
97//! deployment edge rather than in process.
98//!
99//! ```text
100//! control plane JWT HS256 admission trust
101//! packet plane DTLS-SRTP (str0m) media confidentiality
102//! signaling wire TLS at edge transport confidentiality
103//! ```
104//!
105//! ## JWT Admission
106//!
107//! Tokens are `HS256` only. [`auth::verify`] rejects any other `alg`, checks the
108//! HMAC in constant time and enforces `exp`, `nbf` plus an `iat` future-skew
109//! bound. It caps token size at [`auth::MAX_JWT_TOKEN_BYTES`]. Two keys scope
110//! trust:
111//!
112//! - **Server-to-server key**: `AUTH_KEY` (base64, at least 32 bytes) verifies
113//! the HTTP [`http::CreateRoomQuery`] path through [`auth::HttpRoomClaims`] and
114//! [`auth::HttpDisconnectClaims`]. See [`config`].
115//! - **Per-room key**: the request that creates the current room pins the signing
116//! key from the `key` or `keySeed` claim in [`auth::HttpRoomClaims`]. For more
117//! security, prefer the `keySeed` claim, which derives a per-room key with the
118//! `AUTH_KEY` and provided seed using the following KDF:
119//! ```text
120//! room_key = Base64StdPad(HMAC-SHA256(
121//! key = Base64Decode(AUTH_KEY),
122//! message = Base64Decode(keySeed)
123//! ))
124//! ```
125//! WebSocket [`auth::WebSocketConnectClaims`] verify against that room key,
126//! never against `AUTH_KEY`.
127//!
128//! Token carriage differs per surface: HTTP room creation uses the
129//! `Authorization` header, HTTP disconnect uses the request body and the
130//! WebSocket client sends a first-frame auth envelope decoded by
131//! [`websocket::decode_auth_payload_text`]. An unverified room id selects only a
132//! candidate key, then the same token is re-verified against it. Modern
133//! [`auth::WebSocketConnectClaims`] must name the selected room. Legacy Odoo
134//! tokens select it through the auth envelope's `channel` and are normalized
135//! only after verification with that room's key.
136//!
137//! Admission establishes identity and room scope. It does not enforce the
138//! per-user `permissions` claim, which room state collapses to a marker.
139//!
140//! ## Signaling Ingress
141//!
142//! Every authenticated client frame passes through the same decoder as the auth
143//! frame, [`websocket::decode_client_batch`], which bounds parser work with
144//! static caps: [`websocket::MAX_CLIENT_FRAME_BYTES`] per frame and
145//! [`websocket::MAX_CLIENT_BATCH_ENVELOPES`] per batch. The Axum upgrade applies
146//! the frame cap at the socket and the decoder re-checks it. Oversized frames,
147//! oversized batches, malformed JSON, ambiguous routing metadata and unknown
148//! protocol tags reject as [`websocket::ClientBatchDecodeError`] and close the
149//! socket with a protocol-error code.
150//!
151//! Two further bounds guard against resource exhaustion:
152//!
153//! - **Pre-auth admission**: global and per-origin permits cap concurrent
154//! unauthenticated sockets and return `503` once exhausted. See [`config`].
155//! - **Outbound backpressure**: per-user fanout is a bounded queue by message
156//! count and by bytes. A consumer that falls behind is closed rather than
157//! buffered without limit.
158//!
159//! A first-frame auth timeout rejects clients that never authenticate. A
160//! ping/pong health loop closes clients that stop responding. There is no
161//! per-session request-rate budget: the size, count and backpressure caps bound
162//! the work rather than metering a rate.
163//!
164//! ## Media Transport
165//!
166//! `str0m` terminates ICE, DTLS and SRTP over UDP with the `aws-lc-rs` crypto
167//! backend. `o-sfu` builds and drives the `str0m` session but implements no DTLS
168//! or SRTP itself: it forwards already-decrypted RTP between sessions and hands
169//! outbound RTP back to `str0m` for SRTP protection.
170//!
171//! - **Keying**: the DTLS handshake derives SRTP keys per RFC 5764 DTLS-SRTP.
172//! - **Certificate**: `str0m` generates a self-signed certificate when each RTC
173//! session is built and advertises its SHA-256 fingerprint in the SDP offer.
174//! Accepting the answer stores the expected remote fingerprint. The DTLS
175//! handshake verifies it against the peer certificate.
176//! - **ICE**: `o-sfu` runs ICE-lite with `a=setup:actpass` and advertises
177//! `ANNOUNCED_IP`, so media UDP must reach the host directly.
178//!
179//! ## Signaling Transport
180//!
181//! HTTP and WebSocket are served in plaintext in process. HTTPS and WSS are
182//! terminated by an external reverse proxy, so a forwarded scheme and client
183//! address are trusted only when the proxy is trusted through [`config`]
184//! (`PROXY`). Operator route access is documented by [`http`].
185//!
186//! # Room and Router Ownership
187//!
188//! Room transitions produce typed commits while holding short exclusive state
189//! locks. `RoomEffects` consumes deferred transport, source-policy and WebSocket
190//! output work after the lock is released.
191//!
192//! ```text
193//! room state lock held lock released
194//! +--------------------------------+ +------------------------------+
195//! | validate user and connection | | MediaTransport commands |
196//! | commit room topology | | source-policy turn |
197//! | capture transition commit |--->| websocket output |
198//! | | | idempotent teardown |
199//! +--------------------------------+ +------------------------------+
200//! ```
201//!
202//! [`o_sfu_router::Router`] owns exact user-to-connection placement. Receiver shadows are foreign local sessions derived from active consumer dependencies, disappearing with their final consumer.
203//!
204//! # Signaling and Client Bundle
205//!
206//! Browsers use `SfuClient` for connection, publication, subscription and room
207//! control. Signaling state stays in [`o_sfu_protocol::host::ProtocolCore`] and
208//! yields ordered [`o_sfu_protocol::host::Command`] values. The WASM bridge
209//! serializes those commands then maps protocol events to Odoo bundle updates
210//! that drive browser `WebSocket`, `RTCPeerConnection` and timer APIs.
211//!
212//! ```text
213//! SfuClient (public API)
214//! |
215//! v
216//! BrowserRuntime
217//! |
218//! v
219//! ProtocolCore (sans-I/O) -> Vec<Command>
220//! |
221//! v
222//! WASM serialization -> TypeScript command union
223//! |
224//! v
225//! BrowserRuntime -> WebSocket, RTCPeerConnection, timers
226//! ^ |
227//! +--------------- browser events ---------+
228//! ```
229//!
230//! # Packet Path
231//!
232//! [`core::server::transport::MediaTransport`] owns the media workers, which hold the packet loops. These loops receive UDP datagrams, drive WebRTC state (`str0m`), apply route tables and forward RTP.
233//!
234//! ```text
235//! UDP datagram
236//! |
237//! v
238//! worker ingress and `str0m` drain
239//! |
240//! v
241//! packet facts (source, RID and codec)
242//! |
243//! +-> origin packet sinks
244//! |
245//! +-> source and destination packet gates
246//! |
247//! +-> relay fanout
248//! +-> local RTC -> RTP identity and codec rewrite
249//! ```
250//!
251//! `str0m` handles ICE, DTLS and SRTP. The private `rtc::codec` boundary keeps
252//! codec branching out of route planning. Origin packet sinks precede route
253//! gates so recording can observe a publisher without active receivers. Source,
254//! relay and receiver gates then narrow routed fanout. Same-process relays share
255//! payload data with another worker for local delivery.
256//!
257//! Worker BWE and audio observations feed into room source policy, which updates route gates for later packets.
258//!
259//! ```text
260//! worker BWE and audio observations
261//! |
262//! v
263//! room source policy
264//! |
265//! v
266//! route gates for later packets
267//! ```
268//!
269//! # Shutdown and Teardown
270//!
271//! Teardown is explicit async work. [`Runtime::serve_listener`] stops listener acceptance, drains tracked web sockets and stops background tasks within [`config::HttpConfig::shutdown_timeout_ms`].
272//!
273//! ```text
274//! Runtime::serve_listener
275//! |
276//! +-> stop listener
277//! +-> close tracker and cancel sessions
278//! +-> wait for tracker emptiness
279//! +-> stop source-policy sync and media workers
280//! ```
281//!
282//! Missing worker-local sessions or media during teardown are successful no-ops. Unavailable workers or ownership mismatches are terminal.
283//!
284//! # Observability
285//!
286//! Monitored through the [`o_sfu_telemetry`] sub-crate. See [`http::telemetry`] for the HTTP contracts.
287//!
288//! - **Metrics**: [`http::telemetry::metrics`] exposes Prometheus text exposition.
289//! - **Diagnostics**: [`http::telemetry::diagnostics`] exposes JSON state summaries.
290//!
291//! # Scaling
292//!
293//! Rooms use one [`o_sfu_router::Router`] facade and can opt into additional same-process local routers through [`config::RoomWorkerPolicy`].
294//! Joins stay on an assigned healthy packet loop. When no assigned worker has a
295//! known delay below the configured threshold, a join can attach a healthy
296//! worker not yet assigned to the room.
297//!
298//! # Feature Flags
299//!
300//! Core media behavior is configured at runtime through [`config`], not Cargo features.
301//! The default feature `otel-tracing` enables OpenTelemetry tracing support through [`o_sfu_telemetry::TraceExportConfig`]. Other features are used strictly for tests and benchmarking.
302//!
303//! # Sub-crates
304//!
305//! | Crate | Role |
306//! | --- | --- |
307//! | [`o_sfu_rfc`] | RFC-backed JWT, RTP, RTCP, SDP and WebRTC consts/types |
308//! | `o-sfu-model` | Shared call data ([`o_sfu_protocol::wire::UserId`], etc.) |
309//! | [`o_sfu_router`] | Sans-I/O [`o_sfu_router::Router`] facade for room placement and routed media lifetimes |
310//! | [`o_sfu_core`] | Room engine, [`core::prelude::SourcePolicy`], recording taps and [`core::server::transport::MediaTransport`] projection |
311//! | [`o_sfu_protocol`] | Sans-I/O [`o_sfu_protocol::host::ProtocolCore`] and typed commands |
312//! | [`o_sfu_telemetry`] | Tracing setup, metrics, diagnostics response types and graph payloads |
313//!
314//! # Reading Map
315//!
316//! - [`run`] and [`Runtime`] own boot, serving, background tasks and shutdown.
317//! - [`config`] is the environment-to-runtime boundary.
318//! - [`auth`], [`http`] and [`websocket`] form the admission edge.
319//! - [`crate::core`] turns accepted control-plane intent into room mutations and transport effects.
320//! - [`o_sfu_protocol::host::ProtocolCore`] keeps browser signaling sans-I/O.
321//! - [`core::server::transport::MediaTransport`] owns the media workers and hides their threading model.
322pub mod config;
323pub mod core {
324 pub use o_sfu_core::{prelude, server};
325}
326pub(crate) mod application;
327mod runtime;
328
329pub mod auth {
330 pub use crate::runtime::auth::{
331 AuthenticationError, HttpDisconnectClaims, HttpRoomClaims, MAX_JWT_TOKEN_BYTES,
332 RegisteredJwtClaims, WebSocketConnectClaims, sign, verify,
333 };
334}
335
336/// HTTP route and payload contracts.
337///
338/// `/v1/stats`, `/metrics` and diagnostics require the configured
339/// [`crate::config::DiagnosticsConfig::auth_token`] on every listener. Without
340/// one, the actual listener must be loopback. Missing or invalid tokens return
341/// `401 Unauthorized`. Tokenless non-loopback access returns `403 Forbidden`.
342pub mod http {
343 pub use crate::runtime::{
344 http_server::contract::{
345 CreateRoomQuery, IncomingBitRateStatsResponse, NoopResponse, RoomResponse,
346 StatsResponse, route,
347 },
348 request_origin::{RequestOrigin, resolve_request_origin},
349 };
350
351 /// Operator-facing metrics and diagnostics contracts.
352 pub mod telemetry {
353 /// Prometheus metric scrape contract.
354 ///
355 /// `GET` [`metrics::PATH`] returns `200 OK` Prometheus text exposition
356 /// with [`metrics::CONTENT_TYPE`].
357 ///
358 /// This is a scrape endpoint.
359 /// Configure Prometheus to scrape [`metrics::PATH`], then issue `PromQL`
360 /// queries to Prometheus.
361 /// Histogram families render `<name>_bucket` with the additional `le`
362 /// label plus `<name>_sum` and `<name>_count`.
363 ///
364 /// # Scrape and Query
365 ///
366 /// ```yaml
367 /// scrape_configs:
368 /// - job_name: o-sfu
369 /// scheme: https
370 /// metrics_path: /metrics
371 /// authorization:
372 /// type: Bearer
373 /// credentials_file: /run/secrets/o_sfu_diagnostics_token
374 /// tls_config:
375 /// ca_file: /run/secrets/o_sfu_observability_ca
376 /// server_name: o-sfu-observability.internal
377 /// static_configs:
378 /// - targets: ["o-sfu-observability.internal:443"]
379 /// ```
380 ///
381 /// The endpoint returns Prometheus text exposition.
382 ///
383 /// ```text
384 /// # HELP osfu_rooms_active Current number of active rooms owned by this runtime.
385 /// # TYPE osfu_rooms_active gauge
386 /// osfu_rooms_active 3
387 /// # TYPE osfu_worker_rtp_packets_total counter
388 /// osfu_worker_rtp_packets_total{media_worker_id="0",direction="ingress"} 1240
389 /// ```
390 ///
391 /// Query clients send `PromQL` to the Prometheus-compatible backend
392 /// rather than to o-sfu.
393 /// These examples cover a gauge, counter and histogram.
394 ///
395 /// ```promql
396 /// sum(osfu_users_active)
397 /// sum by (stage) (rate(osfu_ws_connections_total[5m]))
398 /// histogram_quantile(
399 /// 0.95,
400 /// sum by (le, route) (rate(osfu_http_request_duration_seconds_bucket[10m]))
401 /// )
402 /// ```
403 ///
404 /// Read the [`metrics::MetricName`] variants below to find every
405 /// exported name and its meaning.
406 /// Query [`metrics::PATH`] to see each family's `HELP`, `TYPE`, label
407 /// keys and current label values before building selectors.
408 pub mod metrics {
409 pub use o_sfu_telemetry::{
410 metrics::MetricName, prometheus::PROMETHEUS_CONTENT_TYPE as CONTENT_TYPE,
411 };
412
413 pub use crate::http::route::METRICS as PATH;
414 }
415
416 /// JSON diagnostics contract.
417 ///
418 /// Every constant in [`diagnostics::route`] is a `GET` endpoint returning
419 /// `200 OK` JSON on success.
420 ///
421 /// # Routes and Parameters
422 ///
423 /// | request | JSON response | parameter source |
424 /// | --- | --- | --- |
425 /// | `GET /internal/diagnostics/summary` | one [`diagnostics::DiagnosticsSummaryResponse`] | none |
426 /// | `GET /internal/diagnostics/rooms` | array of [`diagnostics::DiagnosticsRoomSummary`] | none |
427 /// | `GET /internal/diagnostics/workers` | array of [`diagnostics::DiagnosticsWorkerSummary`] | none |
428 /// | `GET /internal/diagnostics/rooms/{uuid}` | one [`diagnostics::DiagnosticsRoomDetail`] | `uuid` from the rooms response |
429 /// | `GET /internal/diagnostics/rooms/{uuid}/users` | array of [`diagnostics::DiagnosticsUserSummary`] | `uuid` from the rooms response |
430 /// | `GET /internal/diagnostics/rooms/{uuid}/users/{id}` | one [`diagnostics::DiagnosticsUserDetail`] | `uuid` from rooms and `userKey` from room users |
431 /// | `GET /internal/diagnostics/node-graph/rooms/{uuid}` | `{ "nodes": [], "edges": [] }` | `uuid` from the rooms response |
432 /// | `GET /internal/diagnostics/node-graph/rooms/{uuid}/users/{id}` | `{ "nodes": [], "edges": [] }` | `uuid` from rooms and `userKey` from room users |
433 ///
434 /// `userId` may be a JSON number or string.
435 /// `userKey` is always the string to put into `{id}`.
436 /// URL-encode both path values before substitution.
437 ///
438 /// # Summary Request and Response over HTTPS
439 ///
440 /// ```text
441 /// GET /internal/diagnostics/summary HTTP/1.1
442 /// Host: o-sfu-observability.internal
443 /// Authorization: Bearer <diagnostics-token>
444 /// Accept: application/json
445 ///
446 /// HTTP/1.1 200 OK
447 /// Content-Type: application/json
448 ///
449 /// {
450 /// "roomsActive": 1,
451 /// "publicationsActive": 1,
452 /// "recordingRoomsActive": 0,
453 /// "usersActive": 2,
454 /// "subscriptionsActive": 1,
455 /// "transport": {
456 /// "connectedUsers": 2,
457 /// "disconnectedUsers": 0,
458 /// "totalUsers": 2,
459 /// "unknownUsers": 0
460 /// }
461 /// }
462 /// ```
463 ///
464 /// # JavaScript Fetch Example
465 ///
466 /// ```javascript
467 /// const origin = "https://o-sfu-observability.internal";
468 /// const headers = {
469 /// Authorization: `Bearer ${process.env.DIAGNOSTICS_AUTH_TOKEN}`,
470 /// };
471 ///
472 /// async function getJson(path) {
473 /// const response = await fetch(`${origin}${path}`, { headers });
474 /// if (!response.ok) {
475 /// throw new Error(`${response.status} ${await response.text()}`);
476 /// }
477 /// return response.json();
478 /// }
479 ///
480 /// async function main() {
481 /// const rooms = await getJson("/internal/diagnostics/rooms");
482 /// const roomUuid = encodeURIComponent(rooms[0].uuid);
483 /// const room = await getJson(`/internal/diagnostics/rooms/${roomUuid}`);
484 /// const users = await getJson(`/internal/diagnostics/rooms/${roomUuid}/users`);
485 /// const userKey = encodeURIComponent(users[0].userKey);
486 /// const graph = await getJson(
487 /// `/internal/diagnostics/node-graph/rooms/${roomUuid}/users/${userKey}`,
488 /// );
489 ///
490 /// console.log(room.summary, room.users, room.sources);
491 /// console.log(graph.nodes, graph.edges);
492 /// }
493 ///
494 /// main().catch((error) => {
495 /// console.error(error);
496 /// process.exitCode = 1;
497 /// });
498 /// ```
499 ///
500 /// The rooms response has this shape.
501 ///
502 /// ```json
503 /// [
504 /// {
505 /// "createDate": "2026-07-15T10:20:30.000Z",
506 /// "mediaWorkerId": 0,
507 /// "publicationCount": 1,
508 /// "recordingState": {
509 /// "recording": false,
510 /// "audio": false,
511 /// "transcription": false,
512 /// "video": false
513 /// },
514 /// "remoteAddress": "203.0.113.10",
515 /// "sourceCount": 1,
516 /// "userCount": 2,
517 /// "subscriptionCount": 1,
518 /// "transport": {
519 /// "connectedUsers": 2,
520 /// "disconnectedUsers": 0,
521 /// "totalUsers": 2,
522 /// "unknownUsers": 0
523 /// },
524 /// "uuid": "550e8400-e29b-41d4-a716-446655440000",
525 /// "webRtcEnabled": true
526 /// }
527 /// ]
528 /// ```
529 ///
530 /// The room users response has this shape.
531 ///
532 /// ```json
533 /// [
534 /// {
535 /// "audioIncomingBitrateBps": 32000,
536 /// "cameraIncomingBitrateBps": 600000,
537 /// "connectionId": 91,
538 /// "health": "connected",
539 /// "incomingBitrateBps": 632000,
540 /// "mediaWorkerId": 0,
541 /// "publicationCount": 2,
542 /// "roomId": "550e8400-e29b-41d4-a716-446655440000",
543 /// "screenIncomingBitrateBps": 0,
544 /// "subscriptionCount": 1,
545 /// "userId": 42,
546 /// "userKey": "42"
547 /// }
548 /// ]
549 /// ```
550 ///
551 /// The response structs below list every field in each payload.
552 /// Wire names are `camelCase` unless a field documents an exception.
553 ///
554 /// User detail is room-scoped because the same user key can be active
555 /// in several rooms.
556 pub mod diagnostics {
557 pub use o_sfu_telemetry::diagnostics::{
558 DiagnosticsRoomDetail, DiagnosticsRoomSummary, DiagnosticsSummaryResponse,
559 DiagnosticsUserDetail, DiagnosticsUserSummary, DiagnosticsWorkerSummary,
560 };
561
562 pub use crate::http::route::diagnostics as route;
563 }
564 }
565}
566
567pub mod websocket {
568 pub use crate::runtime::websocket_server::{
569 ClientBatchDecodeError, ClientBatchDecodeFailureKind, MAX_CLIENT_BATCH_ENVELOPES,
570 MAX_CLIENT_FRAME_BYTES, decode_auth_payload_text, decode_client_batch,
571 };
572}
573
574pub use self::runtime::{Runtime, ServeError, run};