Skip to main content

arete_server/websocket/
server.rs

1use crate::bus::{BusManager, BusMessage};
2use crate::cache::{cmp_seq, EntityCache, SnapshotBatchConfig};
3use crate::compression::maybe_compress;
4use crate::view::{ViewIndex, ViewSpec};
5use crate::websocket::auth::{
6    AuthContext, AuthDecision, AuthDeny, ConnectionAuthRequest, WebSocketAuthPlugin,
7};
8use crate::websocket::client_manager::{ClientManager, RateLimitConfig};
9use crate::websocket::frame::{
10    apply_wire_format, Frame, Mode, SnapshotEntity, SnapshotFrame, SortConfig, SortOrder,
11    SubscribedFrame, UnsubscribedFrame,
12};
13use crate::websocket::subscription::{
14    ClientMessage, RefreshAuthRequest, RefreshAuthResponse, SocketIssueMessage, Subscription,
15    SubscriptionQuery, Unsubscription, PROTOCOL_VERSION,
16};
17use crate::websocket::usage::{WebSocketUsageEmitter, WebSocketUsageEvent};
18use anyhow::Result;
19use bytes::Bytes;
20use futures_util::StreamExt;
21use serde::Serialize;
22use serde_json::Value;
23use std::collections::{HashMap, HashSet};
24use std::future::Future;
25use std::net::SocketAddr;
26use std::sync::Arc;
27use std::time::Instant;
28use tokio::net::{TcpListener, TcpStream};
29use tokio::sync::{broadcast, watch};
30use tokio_tungstenite::{
31    accept_hdr_async,
32    tungstenite::{
33        handshake::server::{ErrorResponse as HandshakeErrorResponse, Request, Response},
34        http::{header::CONTENT_TYPE, StatusCode},
35        Error as WsError,
36    },
37};
38use tokio_util::sync::CancellationToken;
39use tokio_util::task::TaskTracker;
40use tracing::{debug, error, info, info_span, warn, Instrument};
41use uuid::Uuid;
42
43#[cfg(feature = "otel")]
44use crate::metrics::Metrics;
45
46#[derive(Clone, Default)]
47struct WsMetrics {
48    #[cfg(feature = "otel")]
49    inner: Option<Arc<Metrics>>,
50}
51
52impl WsMetrics {
53    #[cfg(feature = "otel")]
54    fn new(inner: Option<Arc<Metrics>>) -> Self {
55        Self { inner }
56    }
57
58    fn connection_opened(&self, metering_key: Option<&str>) {
59        #[cfg(not(feature = "otel"))]
60        let _ = metering_key;
61        #[cfg(feature = "otel")]
62        if let Some(metrics) = &self.inner {
63            if let Some(metering_key) = metering_key {
64                metrics.record_ws_connection_with_metering(metering_key);
65            } else {
66                metrics.record_ws_connection();
67            }
68        }
69    }
70
71    fn connection_closed(&self, duration_secs: f64, metering_key: Option<&str>) {
72        #[cfg(not(feature = "otel"))]
73        let _ = (duration_secs, metering_key);
74        #[cfg(feature = "otel")]
75        if let Some(metrics) = &self.inner {
76            if let Some(metering_key) = metering_key {
77                metrics.record_ws_disconnection_with_metering(duration_secs, metering_key);
78            } else {
79                metrics.record_ws_disconnection(duration_secs);
80            }
81        }
82    }
83
84    fn message_received(&self, metering_key: Option<&str>) {
85        #[cfg(not(feature = "otel"))]
86        let _ = metering_key;
87        #[cfg(feature = "otel")]
88        if let Some(metrics) = &self.inner {
89            if let Some(metering_key) = metering_key {
90                metrics.record_ws_message_received_with_metering(metering_key);
91            } else {
92                metrics.record_ws_message_received();
93            }
94        }
95    }
96
97    fn message_sent(&self) {
98        #[cfg(feature = "otel")]
99        if let Some(metrics) = &self.inner {
100            metrics.record_ws_message_sent();
101        }
102    }
103
104    fn subscription_created(&self, view: &str, metering_key: Option<&str>) {
105        #[cfg(not(feature = "otel"))]
106        let _ = (view, metering_key);
107        #[cfg(feature = "otel")]
108        if let Some(metrics) = &self.inner {
109            if let Some(metering_key) = metering_key {
110                metrics.record_subscription_created_with_metering(view, metering_key);
111            } else {
112                metrics.record_subscription_created(view);
113            }
114        }
115    }
116
117    fn subscription_removed(&self, view: &str, metering_key: Option<&str>) {
118        #[cfg(not(feature = "otel"))]
119        let _ = (view, metering_key);
120        #[cfg(feature = "otel")]
121        if let Some(metrics) = &self.inner {
122            if let Some(metering_key) = metering_key {
123                metrics.record_subscription_removed_with_metering(view, metering_key);
124            } else {
125                metrics.record_subscription_removed(view);
126            }
127        }
128    }
129
130    fn protocol_error(&self, code: &str) {
131        #[cfg(not(feature = "otel"))]
132        let _ = code;
133        #[cfg(feature = "otel")]
134        if let Some(metrics) = &self.inner {
135            metrics.record_ws_protocol_error(code);
136        }
137    }
138}
139
140async fn handle_refresh_auth(
141    client_id: Uuid,
142    refresh_req: &RefreshAuthRequest,
143    client_manager: &ClientManager,
144    auth_plugin: &Arc<dyn WebSocketAuthPlugin>,
145) {
146    let refresh_result: Result<AuthContext, String> = if let Some(signed_plugin) = auth_plugin
147        .as_any()
148        .downcast_ref::<crate::websocket::auth::SignedSessionAuthPlugin>()
149    {
150        signed_plugin
151            .verify_refresh_token(&refresh_req.token)
152            .await
153            .map_err(|error| error.reason)
154    } else {
155        Err("In-band auth refresh not supported with current auth plugin".to_string())
156    };
157
158    let response = match refresh_result {
159        Ok(new_context) => {
160            let expires_at = new_context.expires_at;
161            if client_manager.update_client_auth(client_id, new_context) {
162                RefreshAuthResponse {
163                    success: true,
164                    error: None,
165                    expires_at: Some(expires_at),
166                }
167            } else {
168                RefreshAuthResponse {
169                    success: false,
170                    error: Some("client-not-found".to_string()),
171                    expires_at: None,
172                }
173            }
174        }
175        Err(error) => {
176            let code = if error.contains("expired") {
177                "token-expired"
178            } else if error.contains("signature") {
179                "token-invalid-signature"
180            } else if error.contains("issuer") {
181                "token-invalid-issuer"
182            } else if error.contains("audience") {
183                "token-invalid-audience"
184            } else {
185                "token-invalid"
186            };
187            RefreshAuthResponse {
188                success: false,
189                error: Some(code.to_string()),
190                expires_at: None,
191            }
192        }
193    };
194
195    if let Ok(json) = serde_json::to_string(&response) {
196        let _ = client_manager.send_text_to_client(client_id, json).await;
197    }
198}
199
200async fn send_socket_issue(
201    client_id: Uuid,
202    client_manager: &ClientManager,
203    deny: &AuthDeny,
204    fatal: bool,
205    subscription_id: Option<String>,
206) {
207    let message = SocketIssueMessage::from_auth_deny(deny, fatal, subscription_id);
208    if let Ok(json) = serde_json::to_string(&message) {
209        let _ = client_manager.send_text_to_client(client_id, json).await;
210    }
211}
212
213async fn send_protocol_issue(
214    client_id: Uuid,
215    client_manager: &ClientManager,
216    metrics: &WsMetrics,
217    subscription_id: Option<String>,
218    code: &str,
219    message: impl Into<String>,
220) {
221    metrics.protocol_error(code);
222    let issue = SocketIssueMessage::protocol(subscription_id, code, message);
223    if let Ok(json) = serde_json::to_string(&issue) {
224        let _ = client_manager.send_text_to_client(client_id, json).await;
225    }
226}
227
228fn key_class_label(key_class: arete_auth::KeyClass) -> &'static str {
229    match key_class {
230        arete_auth::KeyClass::Secret => "secret",
231        arete_auth::KeyClass::Publishable => "publishable",
232    }
233}
234
235fn emit_usage_event(
236    usage_emitter: &Option<Arc<dyn WebSocketUsageEmitter>>,
237    event: WebSocketUsageEvent,
238) {
239    if let Some(emitter) = usage_emitter.clone() {
240        tokio::spawn(async move {
241            emitter.emit(event).await;
242        });
243    }
244}
245
246fn usage_identity(
247    auth_context: Option<&AuthContext>,
248) -> (
249    Option<String>,
250    Option<String>,
251    Option<String>,
252    Option<String>,
253) {
254    match auth_context {
255        Some(context) => (
256            Some(context.metering_key.clone()),
257            Some(context.subject.clone()),
258            Some(key_class_label(context.key_class).to_string()),
259            context.deployment_id.clone(),
260        ),
261        None => (None, None, None, None),
262    }
263}
264
265fn emit_update_sent_for_client(
266    usage_emitter: &Option<Arc<dyn WebSocketUsageEmitter>>,
267    client_manager: &ClientManager,
268    client_id: Uuid,
269    view_id: &str,
270    bytes: usize,
271) {
272    let auth_context = client_manager.get_auth_context(client_id);
273    let (metering_key, subject, _, deployment_id) = usage_identity(auth_context.as_ref());
274    emit_usage_event(
275        usage_emitter,
276        WebSocketUsageEvent::UpdateSent {
277            client_id: client_id.to_string(),
278            deployment_id,
279            metering_key,
280            subject,
281            view_id: view_id.to_string(),
282            messages: 1,
283            bytes: bytes as u64,
284        },
285    );
286}
287
288#[derive(Clone)]
289struct SubscriptionContext {
290    client_id: Uuid,
291    client_manager: ClientManager,
292    bus_manager: BusManager,
293    entity_cache: EntityCache,
294    view_index: Arc<ViewIndex>,
295    usage_emitter: Option<Arc<dyn WebSocketUsageEmitter>>,
296    metrics: WsMetrics,
297    /// Cancelled when the server stops; every session ends through its normal
298    /// cleanup path rather than being dropped mid-flight.
299    shutdown: CancellationToken,
300}
301
302pub struct WebSocketServer {
303    bind_addr: SocketAddr,
304    client_manager: ClientManager,
305    bus_manager: BusManager,
306    entity_cache: EntityCache,
307    view_index: Arc<ViewIndex>,
308    max_clients: usize,
309    auth_plugin: Arc<dyn WebSocketAuthPlugin>,
310    usage_emitter: Option<Arc<dyn WebSocketUsageEmitter>>,
311    rate_limit_config: Option<RateLimitConfig>,
312    #[cfg(feature = "otel")]
313    metrics: Option<Arc<Metrics>>,
314}
315
316impl WebSocketServer {
317    #[cfg(feature = "otel")]
318    pub fn new(
319        bind_addr: SocketAddr,
320        bus_manager: BusManager,
321        entity_cache: EntityCache,
322        view_index: Arc<ViewIndex>,
323        metrics: Option<Arc<Metrics>>,
324    ) -> Self {
325        Self {
326            bind_addr,
327            client_manager: ClientManager::new(),
328            bus_manager,
329            entity_cache,
330            view_index,
331            max_clients: 10_000,
332            auth_plugin: Arc::new(crate::websocket::auth::AllowAllAuthPlugin),
333            usage_emitter: None,
334            rate_limit_config: None,
335            metrics,
336        }
337    }
338
339    #[cfg(not(feature = "otel"))]
340    pub fn new(
341        bind_addr: SocketAddr,
342        bus_manager: BusManager,
343        entity_cache: EntityCache,
344        view_index: Arc<ViewIndex>,
345    ) -> Self {
346        Self {
347            bind_addr,
348            client_manager: ClientManager::new(),
349            bus_manager,
350            entity_cache,
351            view_index,
352            max_clients: 10_000,
353            auth_plugin: Arc::new(crate::websocket::auth::AllowAllAuthPlugin),
354            usage_emitter: None,
355            rate_limit_config: None,
356        }
357    }
358
359    pub fn with_max_clients(mut self, max_clients: usize) -> Self {
360        self.max_clients = max_clients;
361        self
362    }
363
364    pub fn with_auth_plugin(mut self, auth_plugin: Arc<dyn WebSocketAuthPlugin>) -> Self {
365        self.auth_plugin = auth_plugin;
366        self
367    }
368
369    pub fn with_usage_emitter(mut self, usage_emitter: Arc<dyn WebSocketUsageEmitter>) -> Self {
370        self.usage_emitter = Some(usage_emitter);
371        self
372    }
373
374    pub fn with_rate_limit_config(mut self, config: RateLimitConfig) -> Self {
375        self.rate_limit_config = Some(config);
376        self
377    }
378
379    /// Bind the configured address and serve connections until the task is
380    /// dropped. Equivalent to [`into_acceptor`](Self::into_acceptor) followed by
381    /// [`ConnectionAcceptor::serve_listener`].
382    pub async fn start(self) -> Result<()> {
383        info!(
384            "Starting WebSocket server on {} (max_clients: {})",
385            self.bind_addr, self.max_clients
386        );
387        let listener = TcpListener::bind(&self.bind_addr).await?;
388        let (acceptor, _cleanup) = self.into_acceptor();
389        acceptor.serve_listener(listener).await
390    }
391
392    /// Split this server into the part that serves connections and the
393    /// client-manager cleanup task, leaving the caller to own the listener.
394    ///
395    /// The cleanup handle is returned rather than detached so a caller that
396    /// stops serving can stop it too.
397    pub(crate) fn into_acceptor(self) -> (ConnectionAcceptor, tokio::task::JoinHandle<()>) {
398        let client_manager = self
399            .rate_limit_config
400            .map(ClientManager::with_config)
401            .unwrap_or(self.client_manager);
402        let cleanup = client_manager.start_cleanup_task();
403
404        #[cfg(feature = "otel")]
405        let metrics = WsMetrics::new(self.metrics.clone());
406        #[cfg(not(feature = "otel"))]
407        let metrics = WsMetrics::default();
408
409        let acceptor = ConnectionAcceptor {
410            client_manager,
411            bus_manager: self.bus_manager,
412            entity_cache: self.entity_cache,
413            view_index: self.view_index,
414            max_clients: self.max_clients,
415            auth_plugin: self.auth_plugin,
416            usage_emitter: self.usage_emitter,
417            metrics,
418            shutdown: CancellationToken::new(),
419            sessions: TaskTracker::new(),
420        };
421        (acceptor, cleanup)
422    }
423}
424
425/// Serves already-accepted TCP connections against one server's buses, cache
426/// and views.
427///
428/// This is what [`WebSocketServer::start`] runs behind its listener, separated
429/// so that a caller that owns the listener (an application that terminates
430/// TLS itself, a test with an ephemeral port) can hand streams in without the
431/// server binding anything.
432#[derive(Clone)]
433pub(crate) struct ConnectionAcceptor {
434    client_manager: ClientManager,
435    bus_manager: BusManager,
436    entity_cache: EntityCache,
437    view_index: Arc<ViewIndex>,
438    max_clients: usize,
439    auth_plugin: Arc<dyn WebSocketAuthPlugin>,
440    usage_emitter: Option<Arc<dyn WebSocketUsageEmitter>>,
441    metrics: WsMetrics,
442    shutdown: CancellationToken,
443    /// Sessions spawned by [`serve_listener`](Self::serve_listener), so a
444    /// stop can wait for them. Sessions a caller serves on its own tasks are
445    /// the caller's to wait for.
446    sessions: TaskTracker,
447}
448
449impl ConnectionAcceptor {
450    /// Number of clients currently connected to this server.
451    pub(crate) fn client_count(&self) -> usize {
452        self.client_manager.client_count()
453    }
454
455    /// End every session this acceptor is serving and stop accepting.
456    ///
457    /// Sessions notice on their next poll and leave through the same cleanup
458    /// as a client disconnect, so the client manager, buses and usage events
459    /// see an ordinary close.
460    pub(crate) fn shutdown(&self) {
461        self.shutdown.cancel();
462        self.sessions.close();
463    }
464
465    /// Resolves once every listener-spawned session has finished cleaning
466    /// up. Call after [`shutdown`](Self::shutdown).
467    pub(crate) async fn wait_for_sessions(&self) {
468        self.sessions.wait().await;
469    }
470
471    /// Serve one accepted connection: WebSocket handshake, authentication,
472    /// then the subscription session until the peer disconnects.
473    ///
474    /// Returns `Ok(())` without serving when the server is at its client
475    /// limit, exactly as the listener loop does.
476    pub(crate) async fn serve(&self, stream: TcpStream, remote_addr: SocketAddr) -> Result<()> {
477        if self.client_manager.client_count() >= self.max_clients {
478            warn!(
479                "Rejecting connection from {}: max clients reached",
480                remote_addr
481            );
482            return Ok(());
483        }
484
485        let context = SubscriptionContext {
486            client_id: Uuid::nil(),
487            client_manager: self.client_manager.clone(),
488            bus_manager: self.bus_manager.clone(),
489            entity_cache: self.entity_cache.clone(),
490            view_index: self.view_index.clone(),
491            usage_emitter: self.usage_emitter.clone(),
492            metrics: self.metrics.clone(),
493            shutdown: self.shutdown.clone(),
494        };
495        handle_connection(stream, context, remote_addr, self.auth_plugin.clone()).await
496    }
497
498    /// Accept from `listener` until [`shutdown`](Self::shutdown), serving each
499    /// connection on its own task.
500    pub(crate) async fn serve_listener(self, listener: TcpListener) -> Result<()> {
501        loop {
502            let accepted = tokio::select! {
503                _ = self.shutdown.cancelled() => return Ok(()),
504                accepted = listener.accept() => accepted,
505            };
506            match accepted {
507                Ok((stream, addr)) => {
508                    let acceptor = self.clone();
509                    self.sessions.spawn(
510                        async move {
511                            if let Err(error) = acceptor.serve(stream, addr).await {
512                                error!("WebSocket connection error: {}", error);
513                            }
514                        }
515                        .instrument(info_span!("ws.connection", %addr)),
516                    );
517                }
518                Err(error) => error!("Failed to accept connection: {}", error),
519            }
520        }
521    }
522}
523
524#[derive(Debug, Clone)]
525struct HandshakeReject {
526    status: StatusCode,
527    body: crate::websocket::auth::ErrorResponse,
528    error_code: String,
529    retry_after_secs: Option<u64>,
530}
531
532impl HandshakeReject {
533    fn from_deny(deny: &AuthDeny) -> Self {
534        let retry_after_secs = match deny.retry_policy {
535            crate::websocket::auth::RetryPolicy::RetryAfter(duration) => Some(duration.as_secs()),
536            _ => None,
537        };
538        Self {
539            status: StatusCode::from_u16(deny.http_status).unwrap_or(StatusCode::UNAUTHORIZED),
540            body: deny.to_error_response(),
541            error_code: deny.code.to_string(),
542            retry_after_secs,
543        }
544    }
545}
546
547fn build_handshake_error_response(
548    response: &Response,
549    reject: &HandshakeReject,
550) -> HandshakeErrorResponse {
551    let mut builder = Response::builder()
552        .status(reject.status)
553        .version(response.version())
554        .header(CONTENT_TYPE, "application/json; charset=utf-8")
555        .header("X-Error-Code", &reject.error_code)
556        .header("Cache-Control", "no-store");
557    if let Some(retry_after_secs) = reject.retry_after_secs {
558        builder = builder.header("Retry-After", retry_after_secs.to_string());
559    }
560    let body = serde_json::to_string(&reject.body).unwrap_or_else(|_| {
561        format!(
562            r#"{{"error":"{}","message":"{}","code":"{}","retryable":false}}"#,
563            reject.body.error, reject.body.message, reject.body.code
564        )
565    });
566    builder
567        .body(Some(body))
568        .expect("handshake rejection response should build")
569}
570
571#[allow(clippy::result_large_err)]
572async fn accept_authorized_connection(
573    stream: TcpStream,
574    remote_addr: SocketAddr,
575    auth_plugin: Arc<dyn WebSocketAuthPlugin>,
576    client_manager: ClientManager,
577) -> Result<Option<(tokio_tungstenite::WebSocketStream<TcpStream>, AuthContext)>> {
578    use std::sync::Mutex;
579
580    let capture: Arc<Mutex<Option<Result<AuthContext, HandshakeReject>>>> =
581        Arc::new(Mutex::new(None));
582    let capture_ref = capture.clone();
583    let auth_plugin_ref = auth_plugin.clone();
584    let manager_ref = client_manager.clone();
585
586    let handshake_result = accept_hdr_async(stream, move |request: &Request, response| {
587        let request = ConnectionAuthRequest::from_http_request(remote_addr, request);
588        let result = tokio::task::block_in_place(|| {
589            tokio::runtime::Handle::current().block_on(async {
590                match auth_plugin_ref.authorize(&request).await {
591                    AuthDecision::Allow(context) => manager_ref
592                        .check_connection_allowed(remote_addr, &Some(context.clone()))
593                        .await
594                        .map(|()| context)
595                        .map_err(|deny| HandshakeReject::from_deny(&deny)),
596                    AuthDecision::Deny(deny) => Err(HandshakeReject::from_deny(&deny)),
597                }
598            })
599        });
600        *capture_ref.lock().expect("capture lock poisoned") = Some(result.clone());
601        match result {
602            Ok(_) => Ok(response),
603            Err(reject) => Err(build_handshake_error_response(&response, &reject)),
604        }
605    })
606    .await;
607
608    let auth_result = capture.lock().expect("capture lock poisoned").take();
609    match handshake_result {
610        Ok(stream) => match auth_result {
611            Some(Ok(context)) => Ok(Some((stream, context))),
612            Some(Err(reject)) => Err(anyhow::anyhow!(
613                "handshake unexpectedly succeeded after rejection: {}",
614                reject.body.message
615            )),
616            None => Err(anyhow::anyhow!("no auth result captured during handshake")),
617        },
618        Err(WsError::Http(_)) => Ok(None),
619        Err(error) => Err(error.into()),
620    }
621}
622
623async fn handle_connection(
624    stream: TcpStream,
625    mut context: SubscriptionContext,
626    remote_addr: SocketAddr,
627    auth_plugin: Arc<dyn WebSocketAuthPlugin>,
628) -> Result<()> {
629    // The handshake is raced against shutdown too: a peer that stalls it must
630    // not keep a task alive after the server has stopped.
631    let accepted = tokio::select! {
632        _ = context.shutdown.cancelled() => return Ok(()),
633        accepted = accept_authorized_connection(
634            stream,
635            remote_addr,
636            auth_plugin.clone(),
637            context.client_manager.clone(),
638        ) => accepted?,
639    };
640    let Some((ws_stream, auth_context)) = accepted else {
641        return Ok(());
642    };
643
644    let client_id = Uuid::new_v4();
645    context.client_id = client_id;
646    let connection_start = Instant::now();
647    let (metering_key, subject, key_class, deployment_id) = usage_identity(Some(&auth_context));
648    context.metrics.connection_opened(metering_key.as_deref());
649
650    let (ws_sender, mut ws_receiver) = ws_stream.split();
651    context
652        .client_manager
653        .add_client(client_id, ws_sender, Some(auth_context), remote_addr);
654    emit_usage_event(
655        &context.usage_emitter,
656        WebSocketUsageEvent::ConnectionEstablished {
657            client_id: client_id.to_string(),
658            remote_addr: remote_addr.to_string(),
659            deployment_id: deployment_id.clone(),
660            metering_key: metering_key.clone(),
661            subject: subject.clone(),
662            key_class,
663        },
664    );
665
666    let mut active_subscriptions: HashMap<String, String> = HashMap::new();
667    loop {
668        let message = tokio::select! {
669            _ = context.shutdown.cancelled() => break,
670            next = ws_receiver.next() => match next {
671                Some(message) => message,
672                None => break,
673            },
674        };
675        let message = match message {
676            Ok(message) => message,
677            Err(error) => {
678                warn!("WebSocket error for client {}: {}", client_id, error);
679                break;
680            }
681        };
682        if message.is_close() {
683            break;
684        }
685        context.client_manager.update_client_last_seen(client_id);
686        if !message.is_text() {
687            continue;
688        }
689        if let Err(deny) = context
690            .client_manager
691            .check_inbound_message_allowed(client_id)
692        {
693            send_socket_issue(client_id, &context.client_manager, &deny, true, None).await;
694            break;
695        }
696        context.metrics.message_received(metering_key.as_deref());
697
698        let text = match message.to_text() {
699            Ok(text) => text,
700            Err(_) => continue,
701        };
702        let client_message = match serde_json::from_str::<ClientMessage>(text) {
703            Ok(message) => message,
704            Err(parse_error) => {
705                let subscription_id = extract_subscription_id(text);
706                send_protocol_issue(
707                    client_id,
708                    &context.client_manager,
709                    &context.metrics,
710                    subscription_id,
711                    "malformed-message",
712                    format!("invalid protocol v2 message: {parse_error}"),
713                )
714                .await;
715                continue;
716            }
717        };
718
719        match client_message {
720            ClientMessage::Subscribe(subscription) => {
721                let subscription_id = subscription.subscription_id.clone();
722                if let Err(message) = subscription.validate() {
723                    send_protocol_issue(
724                        client_id,
725                        &context.client_manager,
726                        &context.metrics,
727                        Some(subscription_id),
728                        "invalid-subscription",
729                        message,
730                    )
731                    .await;
732                    continue;
733                }
734                if let Err(deny) = context
735                    .client_manager
736                    .check_subscription_allowed(client_id)
737                    .await
738                {
739                    send_socket_issue(
740                        client_id,
741                        &context.client_manager,
742                        &deny,
743                        false,
744                        Some(subscription_id),
745                    )
746                    .await;
747                    continue;
748                }
749
750                let cancel_token = CancellationToken::new();
751                if !context
752                    .client_manager
753                    .add_client_subscription(
754                        client_id,
755                        subscription_id.clone(),
756                        cancel_token.clone(),
757                    )
758                    .await
759                {
760                    send_protocol_issue(
761                        client_id,
762                        &context.client_manager,
763                        &context.metrics,
764                        Some(subscription_id),
765                        "duplicate-subscription-id",
766                        "subscriptionId is already active on this connection",
767                    )
768                    .await;
769                    continue;
770                }
771
772                let view = subscription.query.view.clone();
773                if let Err(error) = attach_client_to_bus(&context, subscription, cancel_token).await
774                {
775                    context
776                        .client_manager
777                        .remove_client_subscription(client_id, &subscription_id)
778                        .await;
779                    send_protocol_issue(
780                        client_id,
781                        &context.client_manager,
782                        &context.metrics,
783                        Some(subscription_id),
784                        "subscription-rejected",
785                        error.to_string(),
786                    )
787                    .await;
788                    continue;
789                }
790
791                active_subscriptions.insert(subscription_id, view.clone());
792                context
793                    .metrics
794                    .subscription_created(&view, metering_key.as_deref());
795                emit_usage_event(
796                    &context.usage_emitter,
797                    WebSocketUsageEvent::SubscriptionCreated {
798                        client_id: client_id.to_string(),
799                        deployment_id: deployment_id.clone(),
800                        metering_key: metering_key.clone(),
801                        subject: subject.clone(),
802                        view_id: view,
803                    },
804                );
805            }
806            ClientMessage::Unsubscribe(unsubscription) => {
807                handle_unsubscribe(
808                    &context,
809                    unsubscription,
810                    &mut active_subscriptions,
811                    metering_key.as_deref(),
812                    &deployment_id,
813                    &metering_key,
814                    &subject,
815                )
816                .await;
817            }
818            ClientMessage::Ping => debug!("Received ping from client {}", client_id),
819            ClientMessage::RefreshAuth(request) => {
820                handle_refresh_auth(client_id, &request, &context.client_manager, &auth_plugin)
821                    .await;
822            }
823        }
824    }
825
826    context
827        .client_manager
828        .cancel_all_client_subscriptions(client_id)
829        .await;
830    context.client_manager.remove_client(client_id);
831    if let Some(rate_limiter) = context.client_manager.rate_limiter().cloned() {
832        rate_limiter.remove_client_buckets(client_id).await;
833    }
834    for view in active_subscriptions.values() {
835        context
836            .metrics
837            .subscription_removed(view, metering_key.as_deref());
838        emit_usage_event(
839            &context.usage_emitter,
840            WebSocketUsageEvent::SubscriptionRemoved {
841                client_id: client_id.to_string(),
842                deployment_id: deployment_id.clone(),
843                metering_key: metering_key.clone(),
844                subject: subject.clone(),
845                view_id: view.clone(),
846            },
847        );
848    }
849    let duration = connection_start.elapsed().as_secs_f64();
850    context
851        .metrics
852        .connection_closed(duration, metering_key.as_deref());
853    emit_usage_event(
854        &context.usage_emitter,
855        WebSocketUsageEvent::ConnectionClosed {
856            client_id: client_id.to_string(),
857            deployment_id,
858            metering_key,
859            subject,
860            duration_secs: Some(duration),
861            subscription_count: u32::try_from(active_subscriptions.len()).unwrap_or(u32::MAX),
862        },
863    );
864    Ok(())
865}
866
867#[allow(clippy::too_many_arguments)]
868async fn handle_unsubscribe(
869    context: &SubscriptionContext,
870    unsubscription: Unsubscription,
871    active_subscriptions: &mut HashMap<String, String>,
872    metrics_metering_key: Option<&str>,
873    deployment_id: &Option<String>,
874    usage_metering_key: &Option<String>,
875    subject: &Option<String>,
876) {
877    let subscription_id = unsubscription.subscription_id.clone();
878    if let Err(message) = unsubscription.validate() {
879        send_protocol_issue(
880            context.client_id,
881            &context.client_manager,
882            &context.metrics,
883            Some(subscription_id),
884            "invalid-unsubscription",
885            message,
886        )
887        .await;
888        return;
889    }
890
891    if !context
892        .client_manager
893        .remove_client_subscription(context.client_id, &subscription_id)
894        .await
895    {
896        send_protocol_issue(
897            context.client_id,
898            &context.client_manager,
899            &context.metrics,
900            Some(subscription_id),
901            "unknown-subscription-id",
902            "subscriptionId is not active on this connection",
903        )
904        .await;
905        return;
906    }
907
908    let Some(view) = active_subscriptions.remove(&subscription_id) else {
909        return;
910    };
911    let _ = send_control_frame(context, &UnsubscribedFrame::new(subscription_id), &view);
912    context
913        .metrics
914        .subscription_removed(&view, metrics_metering_key);
915    emit_usage_event(
916        &context.usage_emitter,
917        WebSocketUsageEvent::SubscriptionRemoved {
918            client_id: context.client_id.to_string(),
919            deployment_id: deployment_id.clone(),
920            metering_key: usage_metering_key.clone(),
921            subject: subject.clone(),
922            view_id: view,
923        },
924    );
925}
926
927fn extract_subscription_id(text: &str) -> Option<String> {
928    serde_json::from_str::<Value>(text)
929        .ok()?
930        .get("subscriptionId")?
931        .as_str()
932        .map(str::to_string)
933}
934
935struct SnapshotMetadata<'a> {
936    subscription_id: &'a str,
937    snapshot_id: &'a str,
938    authoritative: bool,
939    mode: Mode,
940    view_id: &'a str,
941    key: Option<&'a str>,
942}
943
944fn create_snapshot_batches(
945    entities: &[SnapshotEntity],
946    metadata: SnapshotMetadata<'_>,
947    batch_config: &SnapshotBatchConfig,
948) -> Vec<SnapshotFrame> {
949    if entities.is_empty() {
950        return vec![SnapshotFrame {
951            protocol_version: PROTOCOL_VERSION,
952            subscription_id: metadata.subscription_id.to_string(),
953            snapshot_id: metadata.snapshot_id.to_string(),
954            authoritative: metadata.authoritative,
955            mode: metadata.mode,
956            export: metadata.view_id.to_string(),
957            op: "snapshot",
958            key: metadata.key.map(str::to_string),
959            data: vec![],
960            complete: true,
961        }];
962    }
963
964    let mut batches = Vec::new();
965    let mut offset = 0;
966    while offset < entities.len() {
967        let configured_size = if offset == 0 {
968            batch_config.initial_batch_size
969        } else {
970            batch_config.subsequent_batch_size
971        };
972        let end = (offset + configured_size.max(1)).min(entities.len());
973        batches.push(SnapshotFrame {
974            protocol_version: PROTOCOL_VERSION,
975            subscription_id: metadata.subscription_id.to_string(),
976            snapshot_id: metadata.snapshot_id.to_string(),
977            authoritative: metadata.authoritative,
978            mode: metadata.mode,
979            export: metadata.view_id.to_string(),
980            op: "snapshot",
981            key: metadata.key.map(str::to_string),
982            data: entities[offset..end].to_vec(),
983            complete: end == entities.len(),
984        });
985        offset = end;
986    }
987    batches
988}
989
990async fn send_snapshot_batches(
991    context: &SubscriptionContext,
992    subscription: &Subscription,
993    entities: &[SnapshotEntity],
994    mode: Mode,
995    batch_config: &SnapshotBatchConfig,
996) -> Result<()> {
997    let snapshot_id = Uuid::new_v4().to_string();
998    let authoritative = subscription.query.after.is_none();
999    let frames = create_snapshot_batches(
1000        entities,
1001        SnapshotMetadata {
1002            subscription_id: &subscription.subscription_id,
1003            snapshot_id: &snapshot_id,
1004            authoritative,
1005            mode,
1006            view_id: &subscription.query.view,
1007            key: subscription.query.key.as_deref(),
1008        },
1009        batch_config,
1010    );
1011
1012    for frame in frames {
1013        let rows = frame.data.len() as u32;
1014        let json = serde_json::to_vec(&frame)?;
1015        let payload = maybe_compress(&json);
1016        let bytes = payload.as_bytes().len() as u64;
1017        context
1018            .client_manager
1019            .send_compressed_async(context.client_id, payload)
1020            .await
1021            .map_err(|error| anyhow::anyhow!("failed to send snapshot: {error}"))?;
1022        context.metrics.message_sent();
1023
1024        let auth_context = context.client_manager.get_auth_context(context.client_id);
1025        let (metering_key, subject, _, deployment_id) = usage_identity(auth_context.as_ref());
1026        emit_usage_event(
1027            &context.usage_emitter,
1028            WebSocketUsageEvent::SnapshotSent {
1029                client_id: context.client_id.to_string(),
1030                deployment_id,
1031                metering_key,
1032                subject,
1033                view_id: subscription.query.view.clone(),
1034                rows,
1035                messages: 1,
1036                bytes,
1037            },
1038        );
1039    }
1040    Ok(())
1041}
1042
1043fn extract_sort_config(view_spec: &ViewSpec) -> Option<SortConfig> {
1044    if let Some(sort) = view_spec
1045        .pipeline
1046        .as_ref()
1047        .and_then(|pipeline| pipeline.sort.as_ref())
1048    {
1049        return Some(SortConfig {
1050            field: sort.field_path.clone(),
1051            order: match sort.order {
1052                crate::materialized_view::SortOrder::Asc => SortOrder::Asc,
1053                crate::materialized_view::SortOrder::Desc => SortOrder::Desc,
1054            },
1055        });
1056    }
1057    (view_spec.mode == Mode::List).then(|| SortConfig {
1058        field: vec!["_seq".to_string()],
1059        order: SortOrder::Desc,
1060    })
1061}
1062
1063fn send_control_frame<T: Serialize>(
1064    context: &SubscriptionContext,
1065    frame: &T,
1066    view_id: &str,
1067) -> Result<()> {
1068    let json = serde_json::to_vec(frame)?;
1069    let bytes = json.len();
1070    context
1071        .client_manager
1072        .send_to_client(context.client_id, Arc::new(Bytes::from(json)))
1073        .map_err(|error| anyhow::anyhow!("failed to send control frame: {error}"))?;
1074    context.metrics.message_sent();
1075    emit_update_sent_for_client(
1076        &context.usage_emitter,
1077        &context.client_manager,
1078        context.client_id,
1079        view_id,
1080        bytes,
1081    );
1082    Ok(())
1083}
1084
1085fn send_subscribed_frame(
1086    context: &SubscriptionContext,
1087    subscription: &Subscription,
1088    view_spec: &ViewSpec,
1089) -> Result<()> {
1090    let frame = SubscribedFrame::new(
1091        subscription.subscription_id.clone(),
1092        subscription.query.clone(),
1093        view_spec.mode,
1094        extract_sort_config(view_spec),
1095    );
1096    send_control_frame(context, &frame, &subscription.query.view)
1097}
1098
1099fn enforce_snapshot_limit(context: &SubscriptionContext, rows: usize) -> Result<()> {
1100    context
1101        .client_manager
1102        .check_snapshot_allowed(context.client_id, u32::try_from(rows).unwrap_or(u32::MAX))
1103        .map_err(|deny| anyhow::anyhow!(deny.reason))
1104}
1105
1106async fn subscribe_state_then_snapshot<F, Fut, T>(
1107    bus_manager: &BusManager,
1108    view_id: &str,
1109    key: &str,
1110    snapshot: F,
1111) -> (watch::Receiver<Arc<Bytes>>, T)
1112where
1113    F: FnOnce() -> Fut,
1114    Fut: Future<Output = T>,
1115{
1116    let mut receiver = bus_manager.get_or_create_state_bus(view_id, key).await;
1117    receiver.borrow_and_update();
1118    let snapshot = snapshot().await;
1119    (receiver, snapshot)
1120}
1121
1122async fn subscribe_list_then_snapshot<F, Fut, T>(
1123    bus_manager: &BusManager,
1124    view_id: &str,
1125    snapshot: F,
1126) -> (broadcast::Receiver<Arc<BusMessage>>, T)
1127where
1128    F: FnOnce() -> Fut,
1129    Fut: Future<Output = T>,
1130{
1131    let receiver = bus_manager.get_or_create_list_bus(view_id).await;
1132    let snapshot = snapshot().await;
1133    (receiver, snapshot)
1134}
1135
1136async fn attach_client_to_bus(
1137    context: &SubscriptionContext,
1138    mut subscription: Subscription,
1139    cancel_token: CancellationToken,
1140) -> Result<()> {
1141    let view_spec = context
1142        .view_index
1143        .get_view(&subscription.query.view)
1144        .cloned()
1145        .ok_or_else(|| anyhow::anyhow!("unknown view: {}", subscription.query.view))?;
1146
1147    if view_spec.mode == Mode::State && !view_spec.is_derived() && subscription.query.key.is_none()
1148    {
1149        return Err(anyhow::anyhow!("state subscriptions require query.key"));
1150    }
1151    if view_spec.is_derived() && subscription.query.take.is_none() {
1152        subscription.query.take = view_spec
1153            .pipeline
1154            .as_ref()
1155            .and_then(|pipeline| pipeline.limit);
1156    }
1157
1158    if view_spec.mode == Mode::State && !view_spec.is_derived() {
1159        attach_state_subscription(context, subscription, view_spec, cancel_token).await
1160    } else {
1161        attach_collection_subscription(context, subscription, view_spec, cancel_token).await
1162    }
1163}
1164
1165async fn attach_state_subscription(
1166    context: &SubscriptionContext,
1167    subscription: Subscription,
1168    view_spec: ViewSpec,
1169    cancel_token: CancellationToken,
1170) -> Result<()> {
1171    let view_id = subscription.query.view.clone();
1172    let key = subscription.query.key.clone().unwrap_or_default();
1173    let query = subscription.query.clone();
1174    let cache = context.entity_cache.clone();
1175    let view_spec_for_snapshot = view_spec.clone();
1176    let (mut receiver, initial) =
1177        subscribe_state_then_snapshot(&context.bus_manager, &view_id, &key, move || async move {
1178            load_query_entities(&cache, None, &view_spec_for_snapshot, &query, false).await
1179        })
1180        .await;
1181
1182    let mut snapshot_entities = initial.clone();
1183    if let Some(limit) = subscription.query.snapshot_limit {
1184        snapshot_entities.truncate(limit);
1185    }
1186    enforce_snapshot_limit(context, snapshot_entities.len())?;
1187    send_subscribed_frame(context, &subscription, &view_spec)?;
1188    if subscription.snapshot.enabled {
1189        send_snapshot_batches(
1190            context,
1191            &subscription,
1192            &to_wire_snapshot_entities(snapshot_entities, &view_spec),
1193            view_spec.mode,
1194            &context.entity_cache.snapshot_config(),
1195        )
1196        .await?;
1197    }
1198
1199    let task_context = context.clone();
1200    let subscription_id = subscription.subscription_id.clone();
1201    let query = subscription.query.clone();
1202    let view_spec_task = view_spec.clone();
1203    let span_view = view_id.clone();
1204    let span_key = key.clone();
1205    tokio::spawn(
1206        async move {
1207            let mut member = !initial.is_empty();
1208            loop {
1209                tokio::select! {
1210                    _ = cancel_token.cancelled() => break,
1211                    changed = receiver.changed() => {
1212                        if changed.is_err() {
1213                            break;
1214                        }
1215                        let payload = receiver.borrow().clone();
1216                        let metadata = source_frame_metadata(&payload);
1217                        if metadata.op == "delete" {
1218                            task_context.entity_cache.remove(&query.view, &key).await;
1219                            if member && send_membership_frame(
1220                                &task_context,
1221                                &subscription_id,
1222                                &view_spec_task,
1223                                "delete",
1224                                &key,
1225                                Value::Null,
1226                                metadata.seq,
1227                            ).is_err() {
1228                                break;
1229                            }
1230                            member = false;
1231                            continue;
1232                        }
1233
1234                        let selected = load_query_entities(
1235                            &task_context.entity_cache,
1236                            None,
1237                            &view_spec_task,
1238                            &query,
1239                            false,
1240                        ).await;
1241                        let is_member = !selected.is_empty();
1242                        let result = match (member, is_member) {
1243                            (true, true) => send_scoped_source_payload(
1244                                &task_context,
1245                                &subscription_id,
1246                                &query.view,
1247                                payload,
1248                            ),
1249                            (false, true) => {
1250                                let (entity_key, data) = selected.into_iter().next().unwrap();
1251                                send_membership_frame(
1252                                    &task_context,
1253                                    &subscription_id,
1254                                    &view_spec_task,
1255                                    "upsert",
1256                                    &entity_key,
1257                                    data,
1258                                    metadata.seq,
1259                                )
1260                            }
1261                            (true, false) => send_membership_frame(
1262                                &task_context,
1263                                &subscription_id,
1264                                &view_spec_task,
1265                                "remove",
1266                                &key,
1267                                Value::Null,
1268                                metadata.seq,
1269                            ),
1270                            (false, false) => Ok(()),
1271                        };
1272                        if result.is_err() {
1273                            break;
1274                        }
1275                        member = is_member;
1276                    }
1277                }
1278            }
1279        }
1280        .instrument(info_span!("ws.subscribe.state", client_id = %context.client_id, view = %span_view, key = %span_key)),
1281    );
1282    Ok(())
1283}
1284
1285async fn attach_collection_subscription(
1286    context: &SubscriptionContext,
1287    subscription: Subscription,
1288    view_spec: ViewSpec,
1289    cancel_token: CancellationToken,
1290) -> Result<()> {
1291    let view_id = subscription.query.view.clone();
1292    let source_view_id = view_spec
1293        .source_view
1294        .clone()
1295        .unwrap_or_else(|| view_id.clone());
1296    let query = subscription.query.clone();
1297    let cache = context.entity_cache.clone();
1298    let sorted_caches = view_spec
1299        .is_derived()
1300        .then(|| context.view_index.sorted_caches());
1301    let view_spec_for_snapshot = view_spec.clone();
1302    let (mut receiver, initial_membership) =
1303        subscribe_list_then_snapshot(&context.bus_manager, &source_view_id, move || async move {
1304            load_query_entities(
1305                &cache,
1306                sorted_caches,
1307                &view_spec_for_snapshot,
1308                &query,
1309                false,
1310            )
1311            .await
1312        })
1313        .await;
1314
1315    let mut snapshot_entities = initial_membership.clone();
1316    if let Some(limit) = subscription.query.snapshot_limit {
1317        snapshot_entities.truncate(limit);
1318    }
1319    enforce_snapshot_limit(context, snapshot_entities.len())?;
1320    send_subscribed_frame(context, &subscription, &view_spec)?;
1321    if subscription.snapshot.enabled {
1322        send_snapshot_batches(
1323            context,
1324            &subscription,
1325            &to_wire_snapshot_entities(snapshot_entities, &view_spec),
1326            view_spec.mode,
1327            &context.entity_cache.snapshot_config(),
1328        )
1329        .await?;
1330    }
1331
1332    let task_context = context.clone();
1333    let subscription_id = subscription.subscription_id.clone();
1334    let query = subscription.query.clone();
1335    let view_spec_task = view_spec.clone();
1336    let span_view = view_id.clone();
1337    tokio::spawn(
1338        async move {
1339            let mut current = initial_membership;
1340            loop {
1341                tokio::select! {
1342                    _ = cancel_token.cancelled() => break,
1343                    received = receiver.recv() => {
1344                        let envelope = match received {
1345                            Ok(envelope) => envelope,
1346                            Err(broadcast::error::RecvError::Lagged(_)) => {
1347                                warn!("Subscription {} lagged; closing to preserve membership correctness", subscription_id);
1348                                break;
1349                            }
1350                            Err(broadcast::error::RecvError::Closed) => break,
1351                        };
1352                        let metadata = source_frame_metadata(&envelope.payload);
1353                        if metadata.op == "delete" {
1354                            task_context.entity_cache.remove(&source_view_id, &envelope.key).await;
1355                            if view_spec_task.is_derived() {
1356                                let caches = task_context.view_index.sorted_caches();
1357                                let mut guard = caches.write().await;
1358                                if let Some(cache) = guard.get_mut(&query.view) {
1359                                    cache.remove(&envelope.key);
1360                                }
1361                            }
1362                        }
1363
1364                        let sorted_caches = view_spec_task
1365                            .is_derived()
1366                            .then(|| task_context.view_index.sorted_caches());
1367                        let next = load_query_entities(
1368                            &task_context.entity_cache,
1369                            sorted_caches,
1370                            &view_spec_task,
1371                            &query,
1372                            false,
1373                        ).await;
1374                        if emit_collection_delta(
1375                            &task_context,
1376                            &subscription_id,
1377                            &view_spec_task,
1378                            &current,
1379                            &next,
1380                            &envelope,
1381                            &metadata,
1382                        ).is_err() {
1383                            break;
1384                        }
1385                        current = next;
1386                    }
1387                }
1388            }
1389        }
1390        .instrument(info_span!("ws.subscribe.collection", client_id = %context.client_id, view = %span_view)),
1391    );
1392    Ok(())
1393}
1394
1395#[derive(Default)]
1396struct SourceFrameMetadata {
1397    op: String,
1398    seq: Option<String>,
1399}
1400
1401fn source_frame_metadata(payload: &[u8]) -> SourceFrameMetadata {
1402    serde_json::from_slice::<Value>(payload)
1403        .ok()
1404        .map(|value| SourceFrameMetadata {
1405            op: value
1406                .get("op")
1407                .and_then(Value::as_str)
1408                .unwrap_or_default()
1409                .to_string(),
1410            seq: value.get("seq").and_then(Value::as_str).map(str::to_string),
1411        })
1412        .unwrap_or_default()
1413}
1414
1415fn send_scoped_source_payload(
1416    context: &SubscriptionContext,
1417    subscription_id: &str,
1418    view_id: &str,
1419    payload: Arc<Bytes>,
1420) -> Result<()> {
1421    let mut value: Value = serde_json::from_slice(&payload)?;
1422    let object = value
1423        .as_object_mut()
1424        .ok_or_else(|| anyhow::anyhow!("source frame is not an object"))?;
1425    object.insert("protocolVersion".to_string(), Value::from(PROTOCOL_VERSION));
1426    object.insert(
1427        "subscriptionId".to_string(),
1428        Value::String(subscription_id.to_string()),
1429    );
1430    let encoded = Arc::new(Bytes::from(serde_json::to_vec(&value)?));
1431    let bytes = encoded.len();
1432    context
1433        .client_manager
1434        .send_to_client(context.client_id, encoded)
1435        .map_err(|error| anyhow::anyhow!("failed to send live frame: {error}"))?;
1436    context.metrics.message_sent();
1437    emit_update_sent_for_client(
1438        &context.usage_emitter,
1439        &context.client_manager,
1440        context.client_id,
1441        view_id,
1442        bytes,
1443    );
1444    Ok(())
1445}
1446
1447fn send_membership_frame(
1448    context: &SubscriptionContext,
1449    subscription_id: &str,
1450    view_spec: &ViewSpec,
1451    op: &str,
1452    key: &str,
1453    mut data: Value,
1454    seq: Option<String>,
1455) -> Result<()> {
1456    apply_wire_format(&mut data, &view_spec.wire_format);
1457    let frame = Frame::scoped(
1458        subscription_id,
1459        view_spec.mode,
1460        &view_spec.id,
1461        op,
1462        key,
1463        data,
1464        seq,
1465    );
1466    let encoded = Arc::new(Bytes::from(serde_json::to_vec(&frame)?));
1467    let bytes = encoded.len();
1468    context
1469        .client_manager
1470        .send_to_client(context.client_id, encoded)
1471        .map_err(|error| anyhow::anyhow!("failed to send membership frame: {error}"))?;
1472    context.metrics.message_sent();
1473    emit_update_sent_for_client(
1474        &context.usage_emitter,
1475        &context.client_manager,
1476        context.client_id,
1477        &view_spec.id,
1478        bytes,
1479    );
1480    Ok(())
1481}
1482
1483fn emit_collection_delta(
1484    context: &SubscriptionContext,
1485    subscription_id: &str,
1486    view_spec: &ViewSpec,
1487    current: &[(String, Value)],
1488    next: &[(String, Value)],
1489    envelope: &BusMessage,
1490    metadata: &SourceFrameMetadata,
1491) -> Result<()> {
1492    let current_keys: Vec<&str> = current.iter().map(|(key, _)| key.as_str()).collect();
1493    let next_keys: Vec<&str> = next.iter().map(|(key, _)| key.as_str()).collect();
1494    let next_set: HashSet<&str> = next_keys.iter().copied().collect();
1495
1496    for key in current_keys
1497        .iter()
1498        .copied()
1499        .filter(|key| !next_set.contains(key))
1500    {
1501        let op = if metadata.op == "delete" && key == envelope.key {
1502            "delete"
1503        } else {
1504            "remove"
1505        };
1506        send_membership_frame(
1507            context,
1508            subscription_id,
1509            view_spec,
1510            op,
1511            key,
1512            Value::Null,
1513            metadata.seq.clone(),
1514        )?;
1515    }
1516
1517    for (position, (key, data)) in next.iter().enumerate() {
1518        let previous_position = current_keys.iter().position(|candidate| *candidate == key);
1519        let changed_position = previous_position != Some(position);
1520        if previous_position.is_none() || changed_position || key == &envelope.key {
1521            let can_forward_patch = !view_spec.is_derived()
1522                && previous_position == Some(position)
1523                && key == &envelope.key
1524                && metadata.op != "delete";
1525            if can_forward_patch {
1526                send_scoped_source_payload(
1527                    context,
1528                    subscription_id,
1529                    &view_spec.id,
1530                    envelope.payload.clone(),
1531                )?;
1532            } else {
1533                let seq = metadata
1534                    .seq
1535                    .clone()
1536                    .or_else(|| data.get("_seq").and_then(Value::as_str).map(str::to_string));
1537                send_membership_frame(
1538                    context,
1539                    subscription_id,
1540                    view_spec,
1541                    "upsert",
1542                    key,
1543                    data.clone(),
1544                    seq,
1545                )?;
1546            }
1547        }
1548    }
1549    Ok(())
1550}
1551
1552fn to_wire_snapshot_entities(
1553    entities: Vec<(String, Value)>,
1554    view_spec: &ViewSpec,
1555) -> Vec<SnapshotEntity> {
1556    entities
1557        .into_iter()
1558        .map(|(key, mut data)| {
1559            apply_wire_format(&mut data, &view_spec.wire_format);
1560            SnapshotEntity { key, data }
1561        })
1562        .collect()
1563}
1564
1565async fn load_query_entities(
1566    entity_cache: &EntityCache,
1567    sorted_caches: Option<
1568        Arc<tokio::sync::RwLock<HashMap<String, crate::sorted_cache::SortedViewCache>>>,
1569    >,
1570    view_spec: &ViewSpec,
1571    query: &SubscriptionQuery,
1572    apply_snapshot_limit: bool,
1573) -> Vec<(String, Value)> {
1574    let (entities, preordered) = if let Some(sorted_caches) = sorted_caches {
1575        let mut caches = sorted_caches.write().await;
1576        let entities = caches
1577            .get_mut(&view_spec.id)
1578            .map(|cache| cache.get_all_ordered())
1579            .unwrap_or_default();
1580        (entities, true)
1581    } else if view_spec.mode == Mode::State {
1582        let entity = match query.key.as_deref() {
1583            Some(key) => entity_cache
1584                .get(&view_spec.id, key)
1585                .await
1586                .map(|data| vec![(key.to_string(), data)])
1587                .unwrap_or_default(),
1588            None => vec![],
1589        };
1590        (entity, true)
1591    } else {
1592        (entity_cache.get_all(&view_spec.id).await, false)
1593    };
1594    select_query_entities(entities, query, preordered, apply_snapshot_limit)
1595}
1596
1597fn select_query_entities(
1598    mut entities: Vec<(String, Value)>,
1599    query: &SubscriptionQuery,
1600    preordered: bool,
1601    apply_snapshot_limit: bool,
1602) -> Vec<(String, Value)> {
1603    entities.retain(|(key, data)| query_matches_entity(query, key, data));
1604    if !preordered {
1605        entities.sort_by(|left, right| {
1606            let left_seq = left.1.get("_seq").and_then(Value::as_str).unwrap_or("");
1607            let right_seq = right.1.get("_seq").and_then(Value::as_str).unwrap_or("");
1608            let order = if query.after.is_some() {
1609                cmp_seq(left_seq, right_seq)
1610            } else {
1611                cmp_seq(right_seq, left_seq)
1612            };
1613            order.then_with(|| left.0.cmp(&right.0))
1614        });
1615    }
1616
1617    let skip = query.skip.unwrap_or(0);
1618    let take = query.take.unwrap_or(usize::MAX);
1619    let mut selected: Vec<_> = entities.into_iter().skip(skip).take(take).collect();
1620    if apply_snapshot_limit {
1621        if let Some(limit) = query.snapshot_limit {
1622            selected.truncate(limit);
1623        }
1624    }
1625    selected
1626}
1627
1628fn query_matches_entity(query: &SubscriptionQuery, key: &str, data: &Value) -> bool {
1629    if !query.matches_key(key) {
1630        return false;
1631    }
1632    if let Some(partition) = &query.partition {
1633        if value_at_dot_path(data, "_partition") != Some(&Value::String(partition.clone())) {
1634            return false;
1635        }
1636    }
1637    if let Some(after) = &query.after {
1638        let Some(seq) = data.get("_seq").and_then(Value::as_str) else {
1639            return false;
1640        };
1641        if cmp_seq(seq, after) != std::cmp::Ordering::Greater {
1642            return false;
1643        }
1644    }
1645    query
1646        .filters
1647        .iter()
1648        .all(|(path, expected)| value_at_dot_path(data, path) == Some(expected))
1649}
1650
1651fn value_at_dot_path<'a>(value: &'a Value, path: &str) -> Option<&'a Value> {
1652    path.split('.')
1653        .try_fold(value, |current, segment| current.get(segment))
1654}
1655
1656#[cfg(test)]
1657mod tests {
1658    use super::*;
1659    use crate::cache::EntityCacheConfig;
1660    use crate::view::{Delivery, Filters, Projection};
1661    use serde_json::json;
1662    use tokio::sync::oneshot;
1663
1664    fn list_spec() -> ViewSpec {
1665        ViewSpec {
1666            id: "Thing/list".to_string(),
1667            export: "Thing".to_string(),
1668            mode: Mode::List,
1669            wire_format: Default::default(),
1670            projection: Projection::all(),
1671            filters: Filters::all(),
1672            delivery: Delivery::default(),
1673            pipeline: None,
1674            source_view: None,
1675        }
1676    }
1677
1678    #[test]
1679    fn snapshot_batches_share_identity_and_completion() {
1680        let entities = ["one", "two", "three"].map(|key| SnapshotEntity {
1681            key: key.to_string(),
1682            data: json!({"key": key}),
1683        });
1684        let batches = create_snapshot_batches(
1685            &entities,
1686            SnapshotMetadata {
1687                subscription_id: "sub-1",
1688                snapshot_id: "snapshot-1",
1689                authoritative: true,
1690                mode: Mode::List,
1691                view_id: "Thing/list",
1692                key: None,
1693            },
1694            &SnapshotBatchConfig {
1695                initial_batch_size: 2,
1696                subsequent_batch_size: 1,
1697            },
1698        );
1699        assert_eq!(batches.len(), 2);
1700        assert!(batches.iter().all(|batch| batch.subscription_id == "sub-1"));
1701        assert!(batches
1702            .iter()
1703            .all(|batch| batch.snapshot_id == "snapshot-1"));
1704        assert!(!batches[0].complete);
1705        assert!(batches[1].complete);
1706        assert!(batches.iter().all(|batch| batch.authoritative));
1707    }
1708
1709    #[test]
1710    fn empty_incremental_snapshot_is_explicitly_non_authoritative() {
1711        let batches = create_snapshot_batches(
1712            &[],
1713            SnapshotMetadata {
1714                subscription_id: "sub-1",
1715                snapshot_id: "snapshot-1",
1716                authoritative: false,
1717                mode: Mode::State,
1718                view_id: "Thing/state",
1719                key: Some("missing"),
1720            },
1721            &SnapshotBatchConfig {
1722                initial_batch_size: 1,
1723                subsequent_batch_size: 1,
1724            },
1725        );
1726        assert_eq!(batches.len(), 1);
1727        assert!(!batches[0].authoritative);
1728        assert!(batches[0].complete);
1729        assert_eq!(batches[0].key.as_deref(), Some("missing"));
1730    }
1731
1732    #[test]
1733    fn dot_path_filters_are_exact_and_type_sensitive() {
1734        let mut query = SubscriptionQuery {
1735            view: "Thing/list".to_string(),
1736            ..Default::default()
1737        };
1738        query
1739            .filters
1740            .insert("state.status".to_string(), json!("open"));
1741        query.filters.insert("metrics.count".to_string(), json!(2));
1742        assert!(query_matches_entity(
1743            &query,
1744            "one",
1745            &json!({"state": {"status": "open"}, "metrics": {"count": 2}}),
1746        ));
1747        assert!(!query_matches_entity(
1748            &query,
1749            "one",
1750            &json!({"state": {"status": "open"}, "metrics": {"count": "2"}}),
1751        ));
1752    }
1753
1754    #[test]
1755    fn take_and_skip_define_independent_deterministic_windows() {
1756        let entities: Vec<_> = (1..=6)
1757            .map(|id| {
1758                (
1759                    id.to_string(),
1760                    json!({"id": id, "_seq": format!("10:{id:012}")}),
1761                )
1762            })
1763            .collect();
1764        let first = SubscriptionQuery {
1765            view: "Thing/list".to_string(),
1766            take: Some(2),
1767            skip: Some(0),
1768            ..Default::default()
1769        };
1770        let second = SubscriptionQuery {
1771            skip: Some(2),
1772            ..first.clone()
1773        };
1774        let first_keys: Vec<_> = select_query_entities(entities.clone(), &first, false, false)
1775            .into_iter()
1776            .map(|(key, _)| key)
1777            .collect();
1778        let second_keys: Vec<_> = select_query_entities(entities, &second, false, false)
1779            .into_iter()
1780            .map(|(key, _)| key)
1781            .collect();
1782        assert_eq!(first_keys, ["6", "5"]);
1783        assert_eq!(second_keys, ["4", "3"]);
1784    }
1785
1786    #[tokio::test]
1787    async fn state_receiver_is_installed_before_snapshot_awaits() {
1788        let bus = BusManager::new();
1789        let (snapshot_started_tx, snapshot_started_rx) = oneshot::channel();
1790        let (release_snapshot_tx, release_snapshot_rx) = oneshot::channel();
1791        let bus_for_task = bus.clone();
1792        let task = tokio::spawn(async move {
1793            subscribe_state_then_snapshot(&bus_for_task, "Thing/state", "one", || async move {
1794                snapshot_started_tx.send(()).unwrap();
1795                release_snapshot_rx.await.unwrap();
1796            })
1797            .await
1798            .0
1799        });
1800        snapshot_started_rx.await.unwrap();
1801        bus.publish_state(
1802            "Thing/state",
1803            "one",
1804            Arc::new(Bytes::from_static(br#"{"op":"patch"}"#)),
1805        )
1806        .await;
1807        release_snapshot_tx.send(()).unwrap();
1808        let mut receiver = task.await.unwrap();
1809        receiver.changed().await.unwrap();
1810        assert!(!receiver.borrow().is_empty());
1811    }
1812
1813    async fn assert_list_receiver_precedes_snapshot(view: &'static str) {
1814        let bus = BusManager::new();
1815        let (snapshot_started_tx, snapshot_started_rx) = oneshot::channel();
1816        let (release_snapshot_tx, release_snapshot_rx) = oneshot::channel();
1817        let bus_for_task = bus.clone();
1818        let task = tokio::spawn(async move {
1819            subscribe_list_then_snapshot(&bus_for_task, view, || async move {
1820                snapshot_started_tx.send(()).unwrap();
1821                release_snapshot_rx.await.unwrap();
1822            })
1823            .await
1824            .0
1825        });
1826        snapshot_started_rx.await.unwrap();
1827        bus.publish_list(
1828            view,
1829            Arc::new(BusMessage {
1830                key: "one".to_string(),
1831                entity: view.to_string(),
1832                payload: Arc::new(Bytes::from_static(br#"{"op":"patch"}"#)),
1833            }),
1834        )
1835        .await;
1836        release_snapshot_tx.send(()).unwrap();
1837        let mut receiver = task.await.unwrap();
1838        assert_eq!(receiver.recv().await.unwrap().key, "one");
1839    }
1840
1841    #[tokio::test]
1842    async fn list_receiver_is_installed_before_snapshot() {
1843        assert_list_receiver_precedes_snapshot("Thing/list").await;
1844    }
1845
1846    #[tokio::test]
1847    async fn append_receiver_is_installed_before_snapshot() {
1848        assert_list_receiver_precedes_snapshot("Thing/append").await;
1849    }
1850
1851    #[tokio::test]
1852    async fn derived_source_receiver_is_installed_before_snapshot() {
1853        assert_list_receiver_precedes_snapshot("Thing/list-source").await;
1854    }
1855
1856    #[tokio::test]
1857    async fn snapshot_limit_does_not_change_live_take_skip_membership() {
1858        let cache = EntityCache::with_config(EntityCacheConfig {
1859            max_entities_per_view: 10,
1860            ..Default::default()
1861        });
1862        for id in 1..=4 {
1863            cache
1864                .upsert(
1865                    "Thing/list",
1866                    &id.to_string(),
1867                    json!({"_seq": format!("10:{id:012}")}),
1868                )
1869                .await;
1870        }
1871        let query = SubscriptionQuery {
1872            view: "Thing/list".to_string(),
1873            take: Some(3),
1874            skip: Some(1),
1875            snapshot_limit: Some(1),
1876            ..Default::default()
1877        };
1878        let live = load_query_entities(&cache, None, &list_spec(), &query, false).await;
1879        let snapshot = load_query_entities(&cache, None, &list_spec(), &query, true).await;
1880        assert_eq!(live.len(), 3);
1881        assert_eq!(snapshot.len(), 1);
1882    }
1883
1884    #[test]
1885    fn fixture_manifest_covers_required_conformance_cases() {
1886        let manifest: Value = serde_json::from_str(include_str!(
1887            "../../../../tests/fixtures/websocket-v2/manifest.json"
1888        ))
1889        .unwrap();
1890        let names: HashSet<_> = manifest["fixtures"]
1891            .as_array()
1892            .unwrap()
1893            .iter()
1894            .filter_map(Value::as_str)
1895            .collect();
1896        for required in [
1897            "keyed-state.json",
1898            "list-windows.json",
1899            "filters.json",
1900            "multi-batch-authoritative.json",
1901            "empty-snapshot.json",
1902            "remove.json",
1903            "delete.json",
1904            "incremental-snapshot.json",
1905            "reconnect-replacement.json",
1906            "errors.json",
1907        ] {
1908            assert!(names.contains(required), "missing fixture {required}");
1909        }
1910
1911        for document in [
1912            include_str!("../../../../tests/fixtures/websocket-v2/keyed-state.json"),
1913            include_str!("../../../../tests/fixtures/websocket-v2/list-windows.json"),
1914            include_str!("../../../../tests/fixtures/websocket-v2/filters.json"),
1915            include_str!("../../../../tests/fixtures/websocket-v2/multi-batch-authoritative.json"),
1916            include_str!("../../../../tests/fixtures/websocket-v2/empty-snapshot.json"),
1917            include_str!("../../../../tests/fixtures/websocket-v2/remove.json"),
1918            include_str!("../../../../tests/fixtures/websocket-v2/delete.json"),
1919            include_str!("../../../../tests/fixtures/websocket-v2/incremental-snapshot.json"),
1920            include_str!("../../../../tests/fixtures/websocket-v2/reconnect-replacement.json"),
1921            include_str!("../../../../tests/fixtures/websocket-v2/errors.json"),
1922        ] {
1923            let fixture: Value = serde_json::from_str(document).unwrap();
1924            assert!(fixture["name"].is_string());
1925        }
1926    }
1927}