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, VecDeque};
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
228/// Send an already-built issue, for refusals that carry structured detail.
229async fn send_prepared_issue(
230    client_id: Uuid,
231    client_manager: &ClientManager,
232    metrics: &WsMetrics,
233    issue: SocketIssueMessage,
234) {
235    metrics.protocol_error(&issue.code);
236    if let Ok(json) = serde_json::to_string(&issue) {
237        let _ = client_manager.send_text_to_client(client_id, json).await;
238    }
239}
240
241fn key_class_label(key_class: arete_auth::KeyClass) -> &'static str {
242    match key_class {
243        arete_auth::KeyClass::Secret => "secret",
244        arete_auth::KeyClass::Publishable => "publishable",
245    }
246}
247
248fn emit_usage_event(
249    usage_emitter: &Option<Arc<dyn WebSocketUsageEmitter>>,
250    event: WebSocketUsageEvent,
251) {
252    if let Some(emitter) = usage_emitter.clone() {
253        tokio::spawn(async move {
254            emitter.emit(event).await;
255        });
256    }
257}
258
259fn usage_identity(
260    auth_context: Option<&AuthContext>,
261) -> (
262    Option<String>,
263    Option<String>,
264    Option<String>,
265    Option<String>,
266) {
267    match auth_context {
268        Some(context) => (
269            Some(context.metering_key.clone()),
270            Some(context.subject.clone()),
271            Some(key_class_label(context.key_class).to_string()),
272            context.deployment_id.clone(),
273        ),
274        None => (None, None, None, None),
275    }
276}
277
278fn emit_update_sent_for_client(
279    usage_emitter: &Option<Arc<dyn WebSocketUsageEmitter>>,
280    client_manager: &ClientManager,
281    client_id: Uuid,
282    view_id: &str,
283    bytes: usize,
284) {
285    let auth_context = client_manager.get_auth_context(client_id);
286    let (metering_key, subject, _, deployment_id) = usage_identity(auth_context.as_ref());
287    emit_usage_event(
288        usage_emitter,
289        WebSocketUsageEvent::UpdateSent {
290            client_id: client_id.to_string(),
291            deployment_id,
292            metering_key,
293            subject,
294            view_id: view_id.to_string(),
295            messages: 1,
296            bytes: bytes as u64,
297        },
298    );
299}
300
301#[derive(Clone)]
302struct SubscriptionContext {
303    client_id: Uuid,
304    client_manager: ClientManager,
305    bus_manager: BusManager,
306    entity_cache: EntityCache,
307    view_index: Arc<ViewIndex>,
308    usage_emitter: Option<Arc<dyn WebSocketUsageEmitter>>,
309    journal: Option<Arc<crate::journal::EventJournal>>,
310    metrics: WsMetrics,
311    /// Cancelled when the server stops; every session ends through its normal
312    /// cleanup path rather than being dropped mid-flight.
313    shutdown: CancellationToken,
314}
315
316pub struct WebSocketServer {
317    bind_addr: SocketAddr,
318    client_manager: ClientManager,
319    bus_manager: BusManager,
320    entity_cache: EntityCache,
321    view_index: Arc<ViewIndex>,
322    max_clients: usize,
323    auth_plugin: Arc<dyn WebSocketAuthPlugin>,
324    usage_emitter: Option<Arc<dyn WebSocketUsageEmitter>>,
325    rate_limit_config: Option<RateLimitConfig>,
326    journal: Option<Arc<crate::journal::EventJournal>>,
327    #[cfg(feature = "otel")]
328    metrics: Option<Arc<Metrics>>,
329}
330
331impl WebSocketServer {
332    #[cfg(feature = "otel")]
333    pub fn new(
334        bind_addr: SocketAddr,
335        bus_manager: BusManager,
336        entity_cache: EntityCache,
337        view_index: Arc<ViewIndex>,
338        metrics: Option<Arc<Metrics>>,
339    ) -> Self {
340        Self {
341            bind_addr,
342            client_manager: ClientManager::new(),
343            bus_manager,
344            entity_cache,
345            view_index,
346            max_clients: 10_000,
347            auth_plugin: Arc::new(crate::websocket::auth::AllowAllAuthPlugin),
348            usage_emitter: None,
349            rate_limit_config: None,
350            journal: None,
351            metrics,
352        }
353    }
354
355    #[cfg(not(feature = "otel"))]
356    pub fn new(
357        bind_addr: SocketAddr,
358        bus_manager: BusManager,
359        entity_cache: EntityCache,
360        view_index: Arc<ViewIndex>,
361    ) -> Self {
362        Self {
363            bind_addr,
364            client_manager: ClientManager::new(),
365            bus_manager,
366            entity_cache,
367            view_index,
368            max_clients: 10_000,
369            auth_plugin: Arc::new(crate::websocket::auth::AllowAllAuthPlugin),
370            usage_emitter: None,
371            rate_limit_config: None,
372            journal: None,
373        }
374    }
375
376    pub fn with_max_clients(mut self, max_clients: usize) -> Self {
377        self.max_clients = max_clients;
378        self
379    }
380
381    pub fn with_auth_plugin(mut self, auth_plugin: Arc<dyn WebSocketAuthPlugin>) -> Self {
382        self.auth_plugin = auth_plugin;
383        self
384    }
385
386    pub fn with_usage_emitter(mut self, usage_emitter: Arc<dyn WebSocketUsageEmitter>) -> Self {
387        self.usage_emitter = Some(usage_emitter);
388        self
389    }
390
391    /// Serve replayable append subscriptions from the retained event journal.
392    pub fn with_journal(mut self, journal: Arc<crate::journal::EventJournal>) -> Self {
393        self.journal = Some(journal);
394        self
395    }
396
397    pub fn with_rate_limit_config(mut self, config: RateLimitConfig) -> Self {
398        self.rate_limit_config = Some(config);
399        self
400    }
401
402    /// Bind the configured address and serve connections until the task is
403    /// dropped. Equivalent to [`into_acceptor`](Self::into_acceptor) followed by
404    /// [`ConnectionAcceptor::serve_listener`].
405    pub async fn start(self) -> Result<()> {
406        info!(
407            "Starting WebSocket server on {} (max_clients: {})",
408            self.bind_addr, self.max_clients
409        );
410        let listener = TcpListener::bind(&self.bind_addr).await?;
411        let (acceptor, _cleanup) = self.into_acceptor();
412        acceptor.serve_listener(listener).await
413    }
414
415    /// Split this server into the part that serves connections and the
416    /// client-manager cleanup task, leaving the caller to own the listener.
417    ///
418    /// The cleanup handle is returned rather than detached so a caller that
419    /// stops serving can stop it too.
420    pub(crate) fn into_acceptor(self) -> (ConnectionAcceptor, tokio::task::JoinHandle<()>) {
421        let client_manager = self
422            .rate_limit_config
423            .map(ClientManager::with_config)
424            .unwrap_or(self.client_manager);
425        let cleanup = client_manager.start_cleanup_task();
426
427        #[cfg(feature = "otel")]
428        let metrics = WsMetrics::new(self.metrics.clone());
429        #[cfg(not(feature = "otel"))]
430        let metrics = WsMetrics::default();
431
432        let acceptor = ConnectionAcceptor {
433            client_manager,
434            bus_manager: self.bus_manager,
435            entity_cache: self.entity_cache,
436            view_index: self.view_index,
437            max_clients: self.max_clients,
438            auth_plugin: self.auth_plugin,
439            usage_emitter: self.usage_emitter,
440            journal: self.journal,
441            metrics,
442            shutdown: CancellationToken::new(),
443            sessions: TaskTracker::new(),
444        };
445        (acceptor, cleanup)
446    }
447}
448
449/// Serves already-accepted TCP connections against one server's buses, cache
450/// and views.
451///
452/// This is what [`WebSocketServer::start`] runs behind its listener, separated
453/// so that a caller that owns the listener (an application that terminates
454/// TLS itself, a test with an ephemeral port) can hand streams in without the
455/// server binding anything.
456#[derive(Clone)]
457pub(crate) struct ConnectionAcceptor {
458    client_manager: ClientManager,
459    bus_manager: BusManager,
460    entity_cache: EntityCache,
461    view_index: Arc<ViewIndex>,
462    max_clients: usize,
463    auth_plugin: Arc<dyn WebSocketAuthPlugin>,
464    usage_emitter: Option<Arc<dyn WebSocketUsageEmitter>>,
465    journal: Option<Arc<crate::journal::EventJournal>>,
466    metrics: WsMetrics,
467    shutdown: CancellationToken,
468    /// Sessions spawned by [`serve_listener`](Self::serve_listener), so a
469    /// stop can wait for them. Sessions a caller serves on its own tasks are
470    /// the caller's to wait for.
471    sessions: TaskTracker,
472}
473
474impl ConnectionAcceptor {
475    /// Number of clients currently connected to this server.
476    pub(crate) fn client_count(&self) -> usize {
477        self.client_manager.client_count()
478    }
479
480    /// End every session this acceptor is serving and stop accepting.
481    ///
482    /// Sessions notice on their next poll and leave through the same cleanup
483    /// as a client disconnect, so the client manager, buses and usage events
484    /// see an ordinary close.
485    pub(crate) fn shutdown(&self) {
486        self.shutdown.cancel();
487        self.sessions.close();
488    }
489
490    /// Resolves once every listener-spawned session has finished cleaning
491    /// up. Call after [`shutdown`](Self::shutdown).
492    pub(crate) async fn wait_for_sessions(&self) {
493        self.sessions.wait().await;
494    }
495
496    /// Serve one accepted connection: WebSocket handshake, authentication,
497    /// then the subscription session until the peer disconnects.
498    ///
499    /// Returns `Ok(())` without serving when the server is at its client
500    /// limit, exactly as the listener loop does.
501    pub(crate) async fn serve(&self, stream: TcpStream, remote_addr: SocketAddr) -> Result<()> {
502        if self.client_manager.client_count() >= self.max_clients {
503            warn!(
504                "Rejecting connection from {}: max clients reached",
505                remote_addr
506            );
507            return Ok(());
508        }
509
510        let context = SubscriptionContext {
511            client_id: Uuid::nil(),
512            client_manager: self.client_manager.clone(),
513            bus_manager: self.bus_manager.clone(),
514            entity_cache: self.entity_cache.clone(),
515            view_index: self.view_index.clone(),
516            usage_emitter: self.usage_emitter.clone(),
517            journal: self.journal.clone(),
518            metrics: self.metrics.clone(),
519            shutdown: self.shutdown.clone(),
520        };
521        handle_connection(stream, context, remote_addr, self.auth_plugin.clone()).await
522    }
523
524    /// Accept from `listener` until [`shutdown`](Self::shutdown), serving each
525    /// connection on its own task.
526    pub(crate) async fn serve_listener(self, listener: TcpListener) -> Result<()> {
527        loop {
528            let accepted = tokio::select! {
529                _ = self.shutdown.cancelled() => return Ok(()),
530                accepted = listener.accept() => accepted,
531            };
532            match accepted {
533                Ok((stream, addr)) => {
534                    let acceptor = self.clone();
535                    self.sessions.spawn(
536                        async move {
537                            if let Err(error) = acceptor.serve(stream, addr).await {
538                                error!("WebSocket connection error: {}", error);
539                            }
540                        }
541                        .instrument(info_span!("ws.connection", %addr)),
542                    );
543                }
544                Err(error) => error!("Failed to accept connection: {}", error),
545            }
546        }
547    }
548}
549
550#[derive(Debug, Clone)]
551struct HandshakeReject {
552    status: StatusCode,
553    body: crate::websocket::auth::ErrorResponse,
554    error_code: String,
555    retry_after_secs: Option<u64>,
556}
557
558impl HandshakeReject {
559    fn from_deny(deny: &AuthDeny) -> Self {
560        let retry_after_secs = match deny.retry_policy {
561            crate::websocket::auth::RetryPolicy::RetryAfter(duration) => Some(duration.as_secs()),
562            _ => None,
563        };
564        Self {
565            status: StatusCode::from_u16(deny.http_status).unwrap_or(StatusCode::UNAUTHORIZED),
566            body: deny.to_error_response(),
567            error_code: deny.code.to_string(),
568            retry_after_secs,
569        }
570    }
571}
572
573fn build_handshake_error_response(
574    response: &Response,
575    reject: &HandshakeReject,
576) -> HandshakeErrorResponse {
577    let mut builder = Response::builder()
578        .status(reject.status)
579        .version(response.version())
580        .header(CONTENT_TYPE, "application/json; charset=utf-8")
581        .header("X-Error-Code", &reject.error_code)
582        .header("Cache-Control", "no-store");
583    if let Some(retry_after_secs) = reject.retry_after_secs {
584        builder = builder.header("Retry-After", retry_after_secs.to_string());
585    }
586    let body = serde_json::to_string(&reject.body).unwrap_or_else(|_| {
587        format!(
588            r#"{{"error":"{}","message":"{}","code":"{}","retryable":false}}"#,
589            reject.body.error, reject.body.message, reject.body.code
590        )
591    });
592    builder
593        .body(Some(body))
594        .expect("handshake rejection response should build")
595}
596
597#[allow(clippy::result_large_err)]
598async fn accept_authorized_connection(
599    stream: TcpStream,
600    remote_addr: SocketAddr,
601    auth_plugin: Arc<dyn WebSocketAuthPlugin>,
602    client_manager: ClientManager,
603) -> Result<Option<(tokio_tungstenite::WebSocketStream<TcpStream>, AuthContext)>> {
604    use std::sync::Mutex;
605
606    let capture: Arc<Mutex<Option<Result<AuthContext, HandshakeReject>>>> =
607        Arc::new(Mutex::new(None));
608    let capture_ref = capture.clone();
609    let auth_plugin_ref = auth_plugin.clone();
610    let manager_ref = client_manager.clone();
611
612    let handshake_result = accept_hdr_async(stream, move |request: &Request, response| {
613        let request = ConnectionAuthRequest::from_http_request(remote_addr, request);
614        let result = tokio::task::block_in_place(|| {
615            tokio::runtime::Handle::current().block_on(async {
616                match auth_plugin_ref.authorize(&request).await {
617                    AuthDecision::Allow(context) => manager_ref
618                        .check_connection_allowed(remote_addr, &Some(context.clone()))
619                        .await
620                        .map(|()| context)
621                        .map_err(|deny| HandshakeReject::from_deny(&deny)),
622                    AuthDecision::Deny(deny) => Err(HandshakeReject::from_deny(&deny)),
623                }
624            })
625        });
626        *capture_ref.lock().expect("capture lock poisoned") = Some(result.clone());
627        match result {
628            Ok(_) => Ok(response),
629            Err(reject) => Err(build_handshake_error_response(&response, &reject)),
630        }
631    })
632    .await;
633
634    let auth_result = capture.lock().expect("capture lock poisoned").take();
635    match handshake_result {
636        Ok(stream) => match auth_result {
637            Some(Ok(context)) => Ok(Some((stream, context))),
638            Some(Err(reject)) => Err(anyhow::anyhow!(
639                "handshake unexpectedly succeeded after rejection: {}",
640                reject.body.message
641            )),
642            None => Err(anyhow::anyhow!("no auth result captured during handshake")),
643        },
644        Err(WsError::Http(_)) => Ok(None),
645        Err(error) => Err(error.into()),
646    }
647}
648
649async fn handle_connection(
650    stream: TcpStream,
651    mut context: SubscriptionContext,
652    remote_addr: SocketAddr,
653    auth_plugin: Arc<dyn WebSocketAuthPlugin>,
654) -> Result<()> {
655    // The handshake is raced against shutdown too: a peer that stalls it must
656    // not keep a task alive after the server has stopped.
657    let accepted = tokio::select! {
658        _ = context.shutdown.cancelled() => return Ok(()),
659        accepted = accept_authorized_connection(
660            stream,
661            remote_addr,
662            auth_plugin.clone(),
663            context.client_manager.clone(),
664        ) => accepted?,
665    };
666    let Some((ws_stream, auth_context)) = accepted else {
667        return Ok(());
668    };
669
670    let client_id = Uuid::new_v4();
671    context.client_id = client_id;
672    let connection_start = Instant::now();
673    let (metering_key, subject, key_class, deployment_id) = usage_identity(Some(&auth_context));
674    context.metrics.connection_opened(metering_key.as_deref());
675
676    let (ws_sender, mut ws_receiver) = ws_stream.split();
677    context
678        .client_manager
679        .add_client(client_id, ws_sender, Some(auth_context), remote_addr);
680    emit_usage_event(
681        &context.usage_emitter,
682        WebSocketUsageEvent::ConnectionEstablished {
683            client_id: client_id.to_string(),
684            remote_addr: remote_addr.to_string(),
685            deployment_id: deployment_id.clone(),
686            metering_key: metering_key.clone(),
687            subject: subject.clone(),
688            key_class,
689        },
690    );
691
692    let mut active_subscriptions: HashMap<String, String> = HashMap::new();
693    loop {
694        let message = tokio::select! {
695            _ = context.shutdown.cancelled() => break,
696            next = ws_receiver.next() => match next {
697                Some(message) => message,
698                None => break,
699            },
700        };
701        let message = match message {
702            Ok(message) => message,
703            Err(error) => {
704                warn!("WebSocket error for client {}: {}", client_id, error);
705                break;
706            }
707        };
708        if message.is_close() {
709            break;
710        }
711        context.client_manager.update_client_last_seen(client_id);
712        if !message.is_text() {
713            continue;
714        }
715        if let Err(deny) = context
716            .client_manager
717            .check_inbound_message_allowed(client_id)
718        {
719            send_socket_issue(client_id, &context.client_manager, &deny, true, None).await;
720            break;
721        }
722        context.metrics.message_received(metering_key.as_deref());
723
724        let text = match message.to_text() {
725            Ok(text) => text,
726            Err(_) => continue,
727        };
728        let client_message = match serde_json::from_str::<ClientMessage>(text) {
729            Ok(message) => message,
730            Err(parse_error) => {
731                let subscription_id = extract_subscription_id(text);
732                send_protocol_issue(
733                    client_id,
734                    &context.client_manager,
735                    &context.metrics,
736                    subscription_id,
737                    "malformed-message",
738                    format!("invalid protocol v2 message: {parse_error}"),
739                )
740                .await;
741                continue;
742            }
743        };
744
745        match client_message {
746            ClientMessage::Subscribe(subscription) => {
747                let subscription_id = subscription.subscription_id.clone();
748                if let Err(message) = subscription.validate() {
749                    send_protocol_issue(
750                        client_id,
751                        &context.client_manager,
752                        &context.metrics,
753                        Some(subscription_id),
754                        "invalid-subscription",
755                        message,
756                    )
757                    .await;
758                    continue;
759                }
760                if let Err(deny) = context
761                    .client_manager
762                    .check_subscription_allowed(client_id)
763                    .await
764                {
765                    send_socket_issue(
766                        client_id,
767                        &context.client_manager,
768                        &deny,
769                        false,
770                        Some(subscription_id),
771                    )
772                    .await;
773                    continue;
774                }
775
776                let cancel_token = CancellationToken::new();
777                if !context
778                    .client_manager
779                    .add_client_subscription(
780                        client_id,
781                        subscription_id.clone(),
782                        cancel_token.clone(),
783                    )
784                    .await
785                {
786                    send_protocol_issue(
787                        client_id,
788                        &context.client_manager,
789                        &context.metrics,
790                        Some(subscription_id),
791                        "duplicate-subscription-id",
792                        "subscriptionId is already active on this connection",
793                    )
794                    .await;
795                    continue;
796                }
797
798                let view = subscription.query.view.clone();
799                if let Err(error) = attach_client_to_bus(&context, subscription, cancel_token).await
800                {
801                    context
802                        .client_manager
803                        .remove_client_subscription(client_id, &subscription_id)
804                        .await;
805                    // A refusal that already knows what to tell the client
806                    // (an expired cursor, a changed epoch) keeps its own
807                    // frame; anything else is a generic rejection.
808                    match error.downcast::<RejectedSubscription>() {
809                        Ok(rejected) => {
810                            send_prepared_issue(
811                                client_id,
812                                &context.client_manager,
813                                &context.metrics,
814                                rejected.0,
815                            )
816                            .await;
817                        }
818                        Err(error) => {
819                            send_protocol_issue(
820                                client_id,
821                                &context.client_manager,
822                                &context.metrics,
823                                Some(subscription_id),
824                                "subscription-rejected",
825                                error.to_string(),
826                            )
827                            .await;
828                        }
829                    }
830                    continue;
831                }
832
833                active_subscriptions.insert(subscription_id, view.clone());
834                context
835                    .metrics
836                    .subscription_created(&view, metering_key.as_deref());
837                emit_usage_event(
838                    &context.usage_emitter,
839                    WebSocketUsageEvent::SubscriptionCreated {
840                        client_id: client_id.to_string(),
841                        deployment_id: deployment_id.clone(),
842                        metering_key: metering_key.clone(),
843                        subject: subject.clone(),
844                        view_id: view,
845                    },
846                );
847            }
848            ClientMessage::Unsubscribe(unsubscription) => {
849                handle_unsubscribe(
850                    &context,
851                    unsubscription,
852                    &mut active_subscriptions,
853                    metering_key.as_deref(),
854                    &deployment_id,
855                    &metering_key,
856                    &subject,
857                )
858                .await;
859            }
860            ClientMessage::Ping => debug!("Received ping from client {}", client_id),
861            ClientMessage::RefreshAuth(request) => {
862                handle_refresh_auth(client_id, &request, &context.client_manager, &auth_plugin)
863                    .await;
864            }
865        }
866    }
867
868    context
869        .client_manager
870        .cancel_all_client_subscriptions(client_id)
871        .await;
872    context.client_manager.remove_client(client_id);
873    if let Some(rate_limiter) = context.client_manager.rate_limiter().cloned() {
874        rate_limiter.remove_client_buckets(client_id).await;
875    }
876    for view in active_subscriptions.values() {
877        context
878            .metrics
879            .subscription_removed(view, metering_key.as_deref());
880        emit_usage_event(
881            &context.usage_emitter,
882            WebSocketUsageEvent::SubscriptionRemoved {
883                client_id: client_id.to_string(),
884                deployment_id: deployment_id.clone(),
885                metering_key: metering_key.clone(),
886                subject: subject.clone(),
887                view_id: view.clone(),
888            },
889        );
890    }
891    let duration = connection_start.elapsed().as_secs_f64();
892    context
893        .metrics
894        .connection_closed(duration, metering_key.as_deref());
895    emit_usage_event(
896        &context.usage_emitter,
897        WebSocketUsageEvent::ConnectionClosed {
898            client_id: client_id.to_string(),
899            deployment_id,
900            metering_key,
901            subject,
902            duration_secs: Some(duration),
903            subscription_count: u32::try_from(active_subscriptions.len()).unwrap_or(u32::MAX),
904        },
905    );
906    Ok(())
907}
908
909#[allow(clippy::too_many_arguments)]
910async fn handle_unsubscribe(
911    context: &SubscriptionContext,
912    unsubscription: Unsubscription,
913    active_subscriptions: &mut HashMap<String, String>,
914    metrics_metering_key: Option<&str>,
915    deployment_id: &Option<String>,
916    usage_metering_key: &Option<String>,
917    subject: &Option<String>,
918) {
919    let subscription_id = unsubscription.subscription_id.clone();
920    if let Err(message) = unsubscription.validate() {
921        send_protocol_issue(
922            context.client_id,
923            &context.client_manager,
924            &context.metrics,
925            Some(subscription_id),
926            "invalid-unsubscription",
927            message,
928        )
929        .await;
930        return;
931    }
932
933    if !context
934        .client_manager
935        .remove_client_subscription(context.client_id, &subscription_id)
936        .await
937    {
938        send_protocol_issue(
939            context.client_id,
940            &context.client_manager,
941            &context.metrics,
942            Some(subscription_id),
943            "unknown-subscription-id",
944            "subscriptionId is not active on this connection",
945        )
946        .await;
947        return;
948    }
949
950    let Some(view) = active_subscriptions.remove(&subscription_id) else {
951        return;
952    };
953    let _ = send_control_frame(context, &UnsubscribedFrame::new(subscription_id), &view);
954    context
955        .metrics
956        .subscription_removed(&view, metrics_metering_key);
957    emit_usage_event(
958        &context.usage_emitter,
959        WebSocketUsageEvent::SubscriptionRemoved {
960            client_id: context.client_id.to_string(),
961            deployment_id: deployment_id.clone(),
962            metering_key: usage_metering_key.clone(),
963            subject: subject.clone(),
964            view_id: view,
965        },
966    );
967}
968
969fn extract_subscription_id(text: &str) -> Option<String> {
970    serde_json::from_str::<Value>(text)
971        .ok()?
972        .get("subscriptionId")?
973        .as_str()
974        .map(str::to_string)
975}
976
977struct SnapshotMetadata<'a> {
978    subscription_id: &'a str,
979    snapshot_id: &'a str,
980    authoritative: bool,
981    mode: Mode,
982    view_id: &'a str,
983    key: Option<&'a str>,
984}
985
986fn create_snapshot_batches(
987    entities: &[SnapshotEntity],
988    metadata: SnapshotMetadata<'_>,
989    batch_config: &SnapshotBatchConfig,
990) -> Vec<SnapshotFrame> {
991    if entities.is_empty() {
992        return vec![SnapshotFrame {
993            protocol_version: PROTOCOL_VERSION,
994            subscription_id: metadata.subscription_id.to_string(),
995            snapshot_id: metadata.snapshot_id.to_string(),
996            authoritative: metadata.authoritative,
997            mode: metadata.mode,
998            export: metadata.view_id.to_string(),
999            op: "snapshot",
1000            key: metadata.key.map(str::to_string),
1001            data: vec![],
1002            complete: true,
1003        }];
1004    }
1005
1006    let mut batches = Vec::new();
1007    let mut offset = 0;
1008    while offset < entities.len() {
1009        let configured_size = if offset == 0 {
1010            batch_config.initial_batch_size
1011        } else {
1012            batch_config.subsequent_batch_size
1013        };
1014        let end = (offset + configured_size.max(1)).min(entities.len());
1015        batches.push(SnapshotFrame {
1016            protocol_version: PROTOCOL_VERSION,
1017            subscription_id: metadata.subscription_id.to_string(),
1018            snapshot_id: metadata.snapshot_id.to_string(),
1019            authoritative: metadata.authoritative,
1020            mode: metadata.mode,
1021            export: metadata.view_id.to_string(),
1022            op: "snapshot",
1023            key: metadata.key.map(str::to_string),
1024            data: entities[offset..end].to_vec(),
1025            complete: end == entities.len(),
1026        });
1027        offset = end;
1028    }
1029    batches
1030}
1031
1032async fn send_snapshot_batches(
1033    context: &SubscriptionContext,
1034    subscription: &Subscription,
1035    entities: &[SnapshotEntity],
1036    mode: Mode,
1037    batch_config: &SnapshotBatchConfig,
1038) -> Result<()> {
1039    let snapshot_id = Uuid::new_v4().to_string();
1040    let authoritative = subscription.query.after.is_none();
1041    let frames = create_snapshot_batches(
1042        entities,
1043        SnapshotMetadata {
1044            subscription_id: &subscription.subscription_id,
1045            snapshot_id: &snapshot_id,
1046            authoritative,
1047            mode,
1048            view_id: &subscription.query.view,
1049            key: subscription.query.key.as_deref(),
1050        },
1051        batch_config,
1052    );
1053
1054    for frame in frames {
1055        let rows = frame.data.len() as u32;
1056        let json = serde_json::to_vec(&frame)?;
1057        let payload = maybe_compress(&json);
1058        let bytes = payload.as_bytes().len() as u64;
1059        context
1060            .client_manager
1061            .send_compressed_async(context.client_id, payload)
1062            .await
1063            .map_err(|error| anyhow::anyhow!("failed to send snapshot: {error}"))?;
1064        context.metrics.message_sent();
1065
1066        let auth_context = context.client_manager.get_auth_context(context.client_id);
1067        let (metering_key, subject, _, deployment_id) = usage_identity(auth_context.as_ref());
1068        emit_usage_event(
1069            &context.usage_emitter,
1070            WebSocketUsageEvent::SnapshotSent {
1071                client_id: context.client_id.to_string(),
1072                deployment_id,
1073                metering_key,
1074                subject,
1075                view_id: subscription.query.view.clone(),
1076                rows,
1077                messages: 1,
1078                bytes,
1079            },
1080        );
1081    }
1082    Ok(())
1083}
1084
1085fn extract_sort_config(view_spec: &ViewSpec) -> Option<SortConfig> {
1086    if let Some(sort) = view_spec
1087        .pipeline
1088        .as_ref()
1089        .and_then(|pipeline| pipeline.sort.as_ref())
1090    {
1091        return Some(SortConfig {
1092            field: sort.field_path.clone(),
1093            order: match sort.order {
1094                crate::materialized_view::SortOrder::Asc => SortOrder::Asc,
1095                crate::materialized_view::SortOrder::Desc => SortOrder::Desc,
1096            },
1097        });
1098    }
1099    (view_spec.mode == Mode::List).then(|| SortConfig {
1100        field: vec!["_seq".to_string()],
1101        order: SortOrder::Desc,
1102    })
1103}
1104
1105fn send_control_frame<T: Serialize>(
1106    context: &SubscriptionContext,
1107    frame: &T,
1108    view_id: &str,
1109) -> Result<()> {
1110    let json = serde_json::to_vec(frame)?;
1111    let bytes = json.len();
1112    context
1113        .client_manager
1114        .send_to_client(context.client_id, Arc::new(Bytes::from(json)))
1115        .map_err(|error| anyhow::anyhow!("failed to send control frame: {error}"))?;
1116    context.metrics.message_sent();
1117    emit_update_sent_for_client(
1118        &context.usage_emitter,
1119        &context.client_manager,
1120        context.client_id,
1121        view_id,
1122        bytes,
1123    );
1124    Ok(())
1125}
1126
1127fn send_subscribed_frame(
1128    context: &SubscriptionContext,
1129    subscription: &Subscription,
1130    view_spec: &ViewSpec,
1131) -> Result<()> {
1132    let frame = SubscribedFrame::new(
1133        subscription.subscription_id.clone(),
1134        subscription.query.clone(),
1135        view_spec.mode,
1136        extract_sort_config(view_spec),
1137    );
1138    send_control_frame(context, &frame, &subscription.query.view)
1139}
1140
1141fn enforce_snapshot_limit(context: &SubscriptionContext, rows: usize) -> Result<()> {
1142    context
1143        .client_manager
1144        .check_snapshot_allowed(context.client_id, u32::try_from(rows).unwrap_or(u32::MAX))
1145        .map_err(|deny| anyhow::anyhow!(deny.reason))
1146}
1147
1148async fn subscribe_state_then_snapshot<F, Fut, T>(
1149    bus_manager: &BusManager,
1150    view_id: &str,
1151    key: &str,
1152    snapshot: F,
1153) -> (watch::Receiver<Arc<Bytes>>, T)
1154where
1155    F: FnOnce() -> Fut,
1156    Fut: Future<Output = T>,
1157{
1158    let mut receiver = bus_manager.get_or_create_state_bus(view_id, key).await;
1159    receiver.borrow_and_update();
1160    let snapshot = snapshot().await;
1161    (receiver, snapshot)
1162}
1163
1164async fn subscribe_list_then_snapshot<F, Fut, T>(
1165    bus_manager: &BusManager,
1166    view_id: &str,
1167    snapshot: F,
1168) -> (broadcast::Receiver<Arc<BusMessage>>, T)
1169where
1170    F: FnOnce() -> Fut,
1171    Fut: Future<Output = T>,
1172{
1173    let receiver = bus_manager.get_or_create_list_bus(view_id).await;
1174    let snapshot = snapshot().await;
1175    (receiver, snapshot)
1176}
1177
1178async fn attach_client_to_bus(
1179    context: &SubscriptionContext,
1180    mut subscription: Subscription,
1181    cancel_token: CancellationToken,
1182) -> Result<()> {
1183    let view_spec = context
1184        .view_index
1185        .get_view(&subscription.query.view)
1186        .cloned()
1187        .ok_or_else(|| anyhow::anyhow!("unknown view: {}", subscription.query.view))?;
1188
1189    if view_spec.mode == Mode::State && !view_spec.is_derived() && subscription.query.key.is_none()
1190    {
1191        return Err(anyhow::anyhow!("state subscriptions require query.key"));
1192    }
1193    if view_spec.is_derived() && subscription.query.take.is_none() {
1194        subscription.query.take = view_spec
1195            .pipeline
1196            .as_ref()
1197            .and_then(|pipeline| pipeline.limit);
1198    }
1199
1200    // A retained tape takes precedence for append views: it is the only
1201    // delivery that can honour a cursor. Without one, fall through to the
1202    // previous latest-state behaviour.
1203    let journal = context
1204        .journal
1205        .clone()
1206        .filter(|journal| journal.is_enabled() && view_spec.mode == Mode::Append);
1207    if let Some(journal) = journal {
1208        return attach_journal_subscription(
1209            context,
1210            subscription,
1211            view_spec,
1212            journal,
1213            cancel_token,
1214        )
1215        .await;
1216    }
1217
1218    if view_spec.mode == Mode::State && !view_spec.is_derived() {
1219        attach_state_subscription(context, subscription, view_spec, cancel_token).await
1220    } else {
1221        attach_collection_subscription(context, subscription, view_spec, cancel_token).await
1222    }
1223}
1224
1225async fn attach_state_subscription(
1226    context: &SubscriptionContext,
1227    subscription: Subscription,
1228    view_spec: ViewSpec,
1229    cancel_token: CancellationToken,
1230) -> Result<()> {
1231    let view_id = subscription.query.view.clone();
1232    let key = subscription.query.key.clone().unwrap_or_default();
1233    let query = subscription.query.clone();
1234    let cache = context.entity_cache.clone();
1235    let view_spec_for_snapshot = view_spec.clone();
1236    let (mut receiver, initial) =
1237        subscribe_state_then_snapshot(&context.bus_manager, &view_id, &key, move || async move {
1238            load_query_entities(&cache, None, &view_spec_for_snapshot, &query, false).await
1239        })
1240        .await;
1241
1242    let mut snapshot_entities = initial.clone();
1243    if let Some(limit) = subscription.query.snapshot_limit {
1244        snapshot_entities.truncate(limit);
1245    }
1246    enforce_snapshot_limit(context, snapshot_entities.len())?;
1247    send_subscribed_frame(context, &subscription, &view_spec)?;
1248    if subscription.snapshot.enabled {
1249        send_snapshot_batches(
1250            context,
1251            &subscription,
1252            &to_wire_snapshot_entities(snapshot_entities, &view_spec),
1253            view_spec.mode,
1254            &context.entity_cache.snapshot_config(),
1255        )
1256        .await?;
1257    }
1258
1259    let task_context = context.clone();
1260    let subscription_id = subscription.subscription_id.clone();
1261    let query = subscription.query.clone();
1262    let view_spec_task = view_spec.clone();
1263    let span_view = view_id.clone();
1264    let span_key = key.clone();
1265    tokio::spawn(
1266        async move {
1267            let mut member = !initial.is_empty();
1268            loop {
1269                tokio::select! {
1270                    _ = cancel_token.cancelled() => break,
1271                    changed = receiver.changed() => {
1272                        if changed.is_err() {
1273                            break;
1274                        }
1275                        let payload = receiver.borrow().clone();
1276                        let metadata = source_frame_metadata(&payload);
1277                        if metadata.op == "delete" {
1278                            task_context.entity_cache.remove(&query.view, &key).await;
1279                            if member && send_membership_frame(
1280                                &task_context,
1281                                &subscription_id,
1282                                &view_spec_task,
1283                                "delete",
1284                                &key,
1285                                Value::Null,
1286                                metadata.seq,
1287                            ).is_err() {
1288                                break;
1289                            }
1290                            member = false;
1291                            continue;
1292                        }
1293
1294                        let selected = load_query_entities(
1295                            &task_context.entity_cache,
1296                            None,
1297                            &view_spec_task,
1298                            &query,
1299                            false,
1300                        ).await;
1301                        let is_member = !selected.is_empty();
1302                        let result = match (member, is_member) {
1303                            (true, true) => send_scoped_source_payload(
1304                                &task_context,
1305                                &subscription_id,
1306                                &query.view,
1307                                payload,
1308                            ),
1309                            (false, true) => {
1310                                let (entity_key, data) = selected.into_iter().next().unwrap();
1311                                send_membership_frame(
1312                                    &task_context,
1313                                    &subscription_id,
1314                                    &view_spec_task,
1315                                    "upsert",
1316                                    &entity_key,
1317                                    data,
1318                                    metadata.seq,
1319                                )
1320                            }
1321                            (true, false) => send_membership_frame(
1322                                &task_context,
1323                                &subscription_id,
1324                                &view_spec_task,
1325                                "remove",
1326                                &key,
1327                                Value::Null,
1328                                metadata.seq,
1329                            ),
1330                            (false, false) => Ok(()),
1331                        };
1332                        if result.is_err() {
1333                            break;
1334                        }
1335                        member = is_member;
1336                    }
1337                }
1338            }
1339        }
1340        .instrument(info_span!("ws.subscribe.state", client_id = %context.client_id, view = %span_view, key = %span_key)),
1341    );
1342    Ok(())
1343}
1344
1345async fn attach_collection_subscription(
1346    context: &SubscriptionContext,
1347    subscription: Subscription,
1348    view_spec: ViewSpec,
1349    cancel_token: CancellationToken,
1350) -> Result<()> {
1351    let view_id = subscription.query.view.clone();
1352    let source_view_id = view_spec
1353        .source_view
1354        .clone()
1355        .unwrap_or_else(|| view_id.clone());
1356    let query = subscription.query.clone();
1357    let cache = context.entity_cache.clone();
1358    let sorted_caches = view_spec
1359        .is_derived()
1360        .then(|| context.view_index.sorted_caches());
1361    let view_spec_for_snapshot = view_spec.clone();
1362    let (mut receiver, initial_membership) =
1363        subscribe_list_then_snapshot(&context.bus_manager, &source_view_id, move || async move {
1364            load_query_entities(
1365                &cache,
1366                sorted_caches,
1367                &view_spec_for_snapshot,
1368                &query,
1369                false,
1370            )
1371            .await
1372        })
1373        .await;
1374
1375    let mut snapshot_entities = initial_membership.clone();
1376    if let Some(limit) = subscription.query.snapshot_limit {
1377        snapshot_entities.truncate(limit);
1378    }
1379    enforce_snapshot_limit(context, snapshot_entities.len())?;
1380    send_subscribed_frame(context, &subscription, &view_spec)?;
1381    if subscription.snapshot.enabled {
1382        send_snapshot_batches(
1383            context,
1384            &subscription,
1385            &to_wire_snapshot_entities(snapshot_entities, &view_spec),
1386            view_spec.mode,
1387            &context.entity_cache.snapshot_config(),
1388        )
1389        .await?;
1390    }
1391
1392    let task_context = context.clone();
1393    let subscription_id = subscription.subscription_id.clone();
1394    let query = subscription.query.clone();
1395    let view_spec_task = view_spec.clone();
1396    let span_view = view_id.clone();
1397    tokio::spawn(
1398        async move {
1399            let mut current = initial_membership;
1400            loop {
1401                tokio::select! {
1402                    _ = cancel_token.cancelled() => break,
1403                    received = receiver.recv() => {
1404                        let envelope = match received {
1405                            Ok(envelope) => envelope,
1406                            Err(broadcast::error::RecvError::Lagged(_)) => {
1407                                warn!("Subscription {} lagged; closing to preserve membership correctness", subscription_id);
1408                                break;
1409                            }
1410                            Err(broadcast::error::RecvError::Closed) => break,
1411                        };
1412                        let metadata = source_frame_metadata(&envelope.payload);
1413                        if metadata.op == "delete" {
1414                            task_context.entity_cache.remove(&source_view_id, &envelope.key).await;
1415                            if view_spec_task.is_derived() {
1416                                let caches = task_context.view_index.sorted_caches();
1417                                let mut guard = caches.write().await;
1418                                if let Some(cache) = guard.get_mut(&query.view) {
1419                                    cache.remove(&envelope.key);
1420                                }
1421                            }
1422                        }
1423
1424                        let sorted_caches = view_spec_task
1425                            .is_derived()
1426                            .then(|| task_context.view_index.sorted_caches());
1427                        let next = load_query_entities(
1428                            &task_context.entity_cache,
1429                            sorted_caches,
1430                            &view_spec_task,
1431                            &query,
1432                            false,
1433                        ).await;
1434                        if emit_collection_delta(
1435                            &task_context,
1436                            &subscription_id,
1437                            &view_spec_task,
1438                            &current,
1439                            &next,
1440                            &envelope,
1441                            &metadata,
1442                        ).is_err() {
1443                            break;
1444                        }
1445                        current = next;
1446                    }
1447                }
1448            }
1449        }
1450        .instrument(info_span!("ws.subscribe.collection", client_id = %context.client_id, view = %span_view)),
1451    );
1452    Ok(())
1453}
1454
1455/// A subscription refused for a reason the client needs spelled out.
1456///
1457/// Attach paths that return this get the registration released by the
1458/// connection loop, the same as any other failure, while the client still
1459/// receives the specific error rather than a generic `subscription-rejected`.
1460/// Sending the frame and returning `Ok` instead would leave a registered
1461/// subscription with nothing attached: it would hold a slot against the
1462/// client's limit and make the advertised "resubscribe" remediation fail with
1463/// `duplicate-subscription-id`.
1464#[derive(Debug)]
1465pub(crate) struct RejectedSubscription(pub SocketIssueMessage);
1466
1467impl RejectedSubscription {
1468    fn into_error(issue: SocketIssueMessage) -> anyhow::Error {
1469        anyhow::Error::new(Self(issue))
1470    }
1471}
1472
1473impl std::fmt::Display for RejectedSubscription {
1474    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1475        formatter.write_str(&self.0.message)
1476    }
1477}
1478
1479impl std::error::Error for RejectedSubscription {}
1480
1481/// Deliver an append view as an event tape: replay the retained records after
1482/// the cursor, then forward live frames.
1483///
1484/// This deliberately does not recompute membership from the entity cache the
1485/// way [`attach_collection_subscription`] does. The cache folds each patch
1486/// into the resident entity, so a membership diff cannot express "these three
1487/// events happened"; the retained records can.
1488async fn attach_journal_subscription(
1489    context: &SubscriptionContext,
1490    subscription: Subscription,
1491    view_spec: ViewSpec,
1492    journal: Arc<crate::journal::EventJournal>,
1493    cancel_token: CancellationToken,
1494) -> Result<()> {
1495    let view_id = view_spec.id.clone();
1496    let subscription_id = subscription.subscription_id.clone();
1497
1498    // A tape has no membership window, so `take`/`skip` cannot mean what they
1499    // mean on a list view. Refuse them rather than accept and ignore them.
1500    if subscription.query.take.is_some()
1501        || subscription.query.skip.is_some()
1502        || subscription.query.snapshot_limit.is_some()
1503    {
1504        return Err(RejectedSubscription::into_error(SocketIssueMessage::protocol(
1505            Some(subscription_id),
1506            "invalid-subscription",
1507            format!(
1508                "take, skip and snapshotLimit are window options and do not apply to the replayable view {view_id}"
1509            ),
1510        )));
1511    }
1512
1513    // Reject a malformed cursor rather than silently replaying from the start,
1514    // which would look like success and duplicate everything.
1515    let cursor = match subscription.query.after.as_deref() {
1516        Some(raw) => match crate::journal::Cursor::parse(raw) {
1517            Some(cursor) => Some(cursor),
1518            None => {
1519                return Err(RejectedSubscription::into_error(
1520                    SocketIssueMessage::protocol(
1521                        Some(subscription_id),
1522                        "invalid-cursor",
1523                        format!(
1524                            "`after` must be an {{epoch}}:{{offset}} replay cursor for view {view_id}, got {raw:?}"
1525                        ),
1526                    ),
1527                ));
1528            }
1529        },
1530        None => None,
1531    };
1532
1533    // Subscribe before reading the journal so anything published during the
1534    // replay is still delivered; the offset filter below drops the overlap.
1535    let mut receiver = context.bus_manager.get_or_create_list_bus(&view_id).await;
1536
1537    let replayed = match journal.replay_after(&view_id, cursor.as_ref()).await {
1538        Ok(records) => records,
1539        Err(error) => {
1540            return Err(RejectedSubscription::into_error(
1541                SocketIssueMessage::replay_refused(Some(subscription_id), &error),
1542            ));
1543        }
1544    };
1545
1546    let frame = SubscribedFrame::new(
1547        subscription.subscription_id.clone(),
1548        subscription.query.clone(),
1549        view_spec.mode,
1550        extract_sort_config(&view_spec),
1551    )
1552    .with_replay_window(journal.window(&view_id).await);
1553    send_control_frame(context, &frame, &view_id)?;
1554
1555    // Everything past the acknowledgement runs on its own task. The replay can
1556    // be long and applies real backpressure, and `attach_client_to_bus` is
1557    // awaited directly on the connection's inbound loop — doing it there would
1558    // block unsubscribe, auth refresh and pong for the whole replay.
1559    let task_context = context.clone();
1560    let task_subscription_id = subscription.subscription_id.clone();
1561    let task_query = subscription.query.clone();
1562    let task_epoch = journal.epoch().await;
1563    let span_view = view_id.clone();
1564    tokio::spawn(
1565        async move {
1566            let mut last_sent = cursor.map(|cursor| cursor.offset);
1567            // Frames that published while the replay was still running. The
1568            // bus is a bounded broadcast, so it has to be drained as we go or
1569            // a busy view laps us before the replay finishes.
1570            let mut pending: VecDeque<Arc<BusMessage>> = VecDeque::new();
1571            let mut lagged: Option<u64> = None;
1572
1573            for record in replayed {
1574                if cancel_token.is_cancelled() {
1575                    return;
1576                }
1577                drain_available(&mut receiver, &mut pending, &mut lagged);
1578                if journal_record_matches(&task_query, &record)
1579                    && send_scoped_source_payload_async(
1580                        &task_context,
1581                        &task_subscription_id,
1582                        &span_view,
1583                        record.payload,
1584                    )
1585                    .await
1586                    .is_err()
1587                {
1588                    return;
1589                }
1590                // Advance past filtered records too: they were considered.
1591                last_sent = Some(record.offset);
1592            }
1593
1594            // Flush what arrived during the replay before going live, so the
1595            // handover keeps offset order.
1596            while let Some(envelope) = pending.pop_front() {
1597                if !forward_live_frame(
1598                    &task_context,
1599                    &task_subscription_id,
1600                    &span_view,
1601                    &task_query,
1602                    &envelope,
1603                    &mut last_sent,
1604                )
1605                .await
1606                {
1607                    return;
1608                }
1609            }
1610
1611            if let Some(skipped) = lagged {
1612                report_replay_gap(
1613                    &task_context,
1614                    &task_subscription_id,
1615                    &span_view,
1616                    &task_epoch,
1617                    skipped,
1618                    last_sent,
1619                );
1620                return;
1621            }
1622
1623            loop {
1624                tokio::select! {
1625                    _ = cancel_token.cancelled() => break,
1626                    received = receiver.recv() => {
1627                        let envelope = match received {
1628                            Ok(envelope) => envelope,
1629                            // A lagged tape is a gap. Report it with the last
1630                            // offset delivered *before* the gap and stop
1631                            // delivering on this subscription.
1632                            //
1633                            // Continuing would hand the consumer frames from
1634                            // after the gap, advancing its checkpoint past
1635                            // the skipped records so they could never be
1636                            // replayed. Stopping is not a silent stall: the
1637                            // consumer has an explicit error and a cursor
1638                            // that recovers exactly what it missed.
1639                            //
1640                            // The registration is deliberately left alone.
1641                            // Its lifecycle belongs to the connection loop,
1642                            // which holds the only handle to
1643                            // `active_subscriptions`; releasing half of it
1644                            // here would desynchronise unsubscribe, the
1645                            // duplicate-ID gate and close-time usage.
1646                            Err(broadcast::error::RecvError::Lagged(skipped)) => {
1647                                report_replay_gap(
1648                                    &task_context,
1649                                    &task_subscription_id,
1650                                    &span_view,
1651                                    &task_epoch,
1652                                    skipped,
1653                                    last_sent,
1654                                );
1655                                break;
1656                            }
1657                            Err(broadcast::error::RecvError::Closed) => break,
1658                        };
1659
1660                        if !forward_live_frame(
1661                            &task_context,
1662                            &task_subscription_id,
1663                            &span_view,
1664                            &task_query,
1665                            &envelope,
1666                            &mut last_sent,
1667                        )
1668                        .await
1669                        {
1670                            break;
1671                        }
1672                    }
1673                }
1674            }
1675        }
1676        .instrument(info_span!(
1677            "ws.subscribe.replay",
1678            client_id = %context.client_id,
1679            view = %view_id
1680        )),
1681    );
1682    Ok(())
1683}
1684
1685/// Take whatever the bus already has without waiting, so a long replay cannot
1686/// be lapped by a busy view.
1687fn drain_available(
1688    receiver: &mut broadcast::Receiver<Arc<BusMessage>>,
1689    pending: &mut VecDeque<Arc<BusMessage>>,
1690    lagged: &mut Option<u64>,
1691) {
1692    // Once a gap is known, everything still on the bus is on the far side of
1693    // it. Buffering it would put post-gap frames in front of the lag report,
1694    // advancing `last_sent` past the hole and making `recoverFrom` point
1695    // after the very records it is supposed to recover.
1696    if lagged.is_some() {
1697        return;
1698    }
1699    // Bounded so a view publishing faster than the client drains cannot turn
1700    // the buffer into an unbounded queue. Filling it is not itself a gap:
1701    // nothing has been skipped at that instant, the buffer simply stopped
1702    // accepting. Stop buffering and let the bus report the loss, with the
1703    // count it actually measures, when delivery reaches it.
1704    const MAX_PENDING: usize = 8_192;
1705    loop {
1706        if pending.len() >= MAX_PENDING {
1707            return;
1708        }
1709        match receiver.try_recv() {
1710            Ok(envelope) => pending.push_back(envelope),
1711            Err(broadcast::error::TryRecvError::Empty)
1712            | Err(broadcast::error::TryRecvError::Closed) => return,
1713            Err(broadcast::error::TryRecvError::Lagged(skipped)) => {
1714                *lagged = Some(skipped);
1715                return;
1716            }
1717        }
1718    }
1719}
1720
1721/// Whether a live frame was already delivered by the replay that preceded it.
1722///
1723/// The bus is subscribed before the tape is read, so a record published in
1724/// between appears on both paths; without this the consumer sees it twice.
1725/// Advances the high-water mark as a side effect.
1726fn already_delivered(offset: Option<u64>, last_sent: &mut Option<u64>) -> bool {
1727    let Some(offset) = offset else {
1728        // A frame with no offset predates the tape, so it cannot have been
1729        // replayed and must not move the mark.
1730        return false;
1731    };
1732    if last_sent.is_some_and(|last| offset <= last) {
1733        return true;
1734    }
1735    *last_sent = Some(offset);
1736    false
1737}
1738
1739/// Deliver one live frame, skipping anything the replay already sent.
1740///
1741/// Returns false when the subscription should end.
1742async fn forward_live_frame(
1743    context: &SubscriptionContext,
1744    subscription_id: &str,
1745    view_id: &str,
1746    query: &SubscriptionQuery,
1747    envelope: &Arc<BusMessage>,
1748    last_sent: &mut Option<u64>,
1749) -> bool {
1750    let metadata = source_frame_metadata(&envelope.payload);
1751    if already_delivered(metadata.offset, last_sent) {
1752        return true;
1753    }
1754    if !live_frame_matches(query, &envelope.key, &envelope.payload) {
1755        return true;
1756    }
1757    send_scoped_source_payload(context, subscription_id, view_id, envelope.payload.clone()).is_ok()
1758}
1759
1760fn report_replay_gap(
1761    context: &SubscriptionContext,
1762    subscription_id: &str,
1763    view_id: &str,
1764    epoch: &crate::journal::JournalEpoch,
1765    skipped: u64,
1766    last_sent: Option<u64>,
1767) {
1768    warn!(
1769        "Replay subscription {} lagged past {} records; stopping with a recovery cursor",
1770        subscription_id, skipped
1771    );
1772    let recover_from = last_sent.map(|offset| crate::journal::Cursor {
1773        epoch: epoch.clone(),
1774        offset,
1775    });
1776    let _ = send_control_frame(
1777        context,
1778        &SocketIssueMessage::replay_lagged(
1779            Some(subscription_id.to_string()),
1780            skipped,
1781            recover_from,
1782        ),
1783        view_id,
1784    );
1785}
1786
1787/// Apply the subscription's `key`, `partition` and `filters` to a retained
1788/// record. A replay must honour the same predicates a live subscription does.
1789fn journal_record_matches(
1790    query: &SubscriptionQuery,
1791    record: &crate::journal::JournalRecord,
1792) -> bool {
1793    live_frame_matches(query, &record.key, &record.payload)
1794}
1795
1796fn live_frame_matches(query: &SubscriptionQuery, key: &str, payload: &[u8]) -> bool {
1797    if !query.matches_key(key) {
1798        return false;
1799    }
1800    if query.partition.is_none() && query.filters.is_empty() {
1801        return true;
1802    }
1803    let Ok(frame) = serde_json::from_slice::<Value>(payload) else {
1804        return false;
1805    };
1806    let Some(data) = frame.get("data") else {
1807        return false;
1808    };
1809    if let Some(partition) = &query.partition {
1810        if value_at_dot_path(data, "_partition") != Some(&Value::String(partition.clone())) {
1811            return false;
1812        }
1813    }
1814    query
1815        .filters
1816        .iter()
1817        .all(|(path, expected)| value_at_dot_path(data, path) == Some(expected))
1818}
1819
1820/// Awaiting variant of [`send_scoped_source_payload`], for replays that can
1821/// exceed the client's send queue.
1822async fn send_scoped_source_payload_async(
1823    context: &SubscriptionContext,
1824    subscription_id: &str,
1825    view_id: &str,
1826    payload: Arc<Bytes>,
1827) -> Result<()> {
1828    let mut value: Value = serde_json::from_slice(&payload)?;
1829    let object = value
1830        .as_object_mut()
1831        .ok_or_else(|| anyhow::anyhow!("source frame is not an object"))?;
1832    object.insert("protocolVersion".to_string(), Value::from(PROTOCOL_VERSION));
1833    object.insert(
1834        "subscriptionId".to_string(),
1835        Value::String(subscription_id.to_string()),
1836    );
1837    let json = serde_json::to_vec(&value)?;
1838    let compressed = maybe_compress(&json);
1839    let bytes = compressed.as_bytes().len();
1840    context
1841        .client_manager
1842        .send_compressed_async(context.client_id, compressed)
1843        .await
1844        .map_err(|error| anyhow::anyhow!("failed to send replayed frame: {error}"))?;
1845    context.metrics.message_sent();
1846    emit_update_sent_for_client(
1847        &context.usage_emitter,
1848        &context.client_manager,
1849        context.client_id,
1850        view_id,
1851        bytes,
1852    );
1853    Ok(())
1854}
1855
1856#[derive(Default)]
1857struct SourceFrameMetadata {
1858    op: String,
1859    seq: Option<String>,
1860    offset: Option<u64>,
1861}
1862
1863fn source_frame_metadata(payload: &[u8]) -> SourceFrameMetadata {
1864    serde_json::from_slice::<Value>(payload)
1865        .ok()
1866        .map(|value| SourceFrameMetadata {
1867            op: value
1868                .get("op")
1869                .and_then(Value::as_str)
1870                .unwrap_or_default()
1871                .to_string(),
1872            seq: value.get("seq").and_then(Value::as_str).map(str::to_string),
1873            offset: value.get("offset").and_then(Value::as_u64),
1874        })
1875        .unwrap_or_default()
1876}
1877
1878fn send_scoped_source_payload(
1879    context: &SubscriptionContext,
1880    subscription_id: &str,
1881    view_id: &str,
1882    payload: Arc<Bytes>,
1883) -> Result<()> {
1884    let mut value: Value = serde_json::from_slice(&payload)?;
1885    let object = value
1886        .as_object_mut()
1887        .ok_or_else(|| anyhow::anyhow!("source frame is not an object"))?;
1888    object.insert("protocolVersion".to_string(), Value::from(PROTOCOL_VERSION));
1889    object.insert(
1890        "subscriptionId".to_string(),
1891        Value::String(subscription_id.to_string()),
1892    );
1893    let encoded = Arc::new(Bytes::from(serde_json::to_vec(&value)?));
1894    let bytes = encoded.len();
1895    context
1896        .client_manager
1897        .send_to_client(context.client_id, encoded)
1898        .map_err(|error| anyhow::anyhow!("failed to send live frame: {error}"))?;
1899    context.metrics.message_sent();
1900    emit_update_sent_for_client(
1901        &context.usage_emitter,
1902        &context.client_manager,
1903        context.client_id,
1904        view_id,
1905        bytes,
1906    );
1907    Ok(())
1908}
1909
1910fn send_membership_frame(
1911    context: &SubscriptionContext,
1912    subscription_id: &str,
1913    view_spec: &ViewSpec,
1914    op: &str,
1915    key: &str,
1916    mut data: Value,
1917    seq: Option<String>,
1918) -> Result<()> {
1919    apply_wire_format(&mut data, &view_spec.wire_format);
1920    let frame = Frame::scoped(
1921        subscription_id,
1922        view_spec.mode,
1923        &view_spec.id,
1924        op,
1925        key,
1926        data,
1927        seq,
1928    );
1929    let encoded = Arc::new(Bytes::from(serde_json::to_vec(&frame)?));
1930    let bytes = encoded.len();
1931    context
1932        .client_manager
1933        .send_to_client(context.client_id, encoded)
1934        .map_err(|error| anyhow::anyhow!("failed to send membership frame: {error}"))?;
1935    context.metrics.message_sent();
1936    emit_update_sent_for_client(
1937        &context.usage_emitter,
1938        &context.client_manager,
1939        context.client_id,
1940        &view_spec.id,
1941        bytes,
1942    );
1943    Ok(())
1944}
1945
1946fn emit_collection_delta(
1947    context: &SubscriptionContext,
1948    subscription_id: &str,
1949    view_spec: &ViewSpec,
1950    current: &[(String, Value)],
1951    next: &[(String, Value)],
1952    envelope: &BusMessage,
1953    metadata: &SourceFrameMetadata,
1954) -> Result<()> {
1955    let current_keys: Vec<&str> = current.iter().map(|(key, _)| key.as_str()).collect();
1956    let next_keys: Vec<&str> = next.iter().map(|(key, _)| key.as_str()).collect();
1957    let next_set: HashSet<&str> = next_keys.iter().copied().collect();
1958
1959    for key in current_keys
1960        .iter()
1961        .copied()
1962        .filter(|key| !next_set.contains(key))
1963    {
1964        let op = if metadata.op == "delete" && key == envelope.key {
1965            "delete"
1966        } else {
1967            "remove"
1968        };
1969        send_membership_frame(
1970            context,
1971            subscription_id,
1972            view_spec,
1973            op,
1974            key,
1975            Value::Null,
1976            metadata.seq.clone(),
1977        )?;
1978    }
1979
1980    for (key, data) in next.iter() {
1981        let was_member = current_keys.iter().any(|candidate| *candidate == key);
1982        match member_action(
1983            was_member,
1984            key == &envelope.key,
1985            view_spec.is_derived(),
1986            &metadata.op,
1987        ) {
1988            MemberAction::Skip => {}
1989            MemberAction::ForwardPatch => send_scoped_source_payload(
1990                context,
1991                subscription_id,
1992                &view_spec.id,
1993                envelope.payload.clone(),
1994            )?,
1995            MemberAction::Upsert => {
1996                let seq = metadata
1997                    .seq
1998                    .clone()
1999                    .or_else(|| data.get("_seq").and_then(Value::as_str).map(str::to_string));
2000                send_membership_frame(
2001                    context,
2002                    subscription_id,
2003                    view_spec,
2004                    "upsert",
2005                    key,
2006                    data.clone(),
2007                    seq,
2008                )?;
2009            }
2010        }
2011    }
2012    Ok(())
2013}
2014
2015/// What one in-window key owes a subscriber after a source mutation.
2016#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2017enum MemberAction {
2018    /// Send nothing: the subscriber already holds this entity and its data did
2019    /// not change.
2020    Skip,
2021    /// Forward the source patch verbatim.
2022    ForwardPatch,
2023    /// Send the whole entity.
2024    Upsert,
2025}
2026
2027/// Decide what to send for one key in the query window.
2028///
2029/// Only the mutated key's data changed, so a key that was already a member has
2030/// at most moved index — and index is not the server's to communicate.
2031/// `subscribed` announces the window's sort (see [`extract_sort_config`], which
2032/// is `_seq` descending for a plain list) and every SDK re-sorts locally from
2033/// it, so resending an unchanged entity to convey its new position is pure
2034/// waste. This matters because on a `_seq`-ordered list the mutated entity
2035/// jumps to the front on *every* mutation: keying the decision on position
2036/// change meant rebroadcasting the whole window each time, and the mutated
2037/// entity itself never kept its position long enough to have its patch
2038/// forwarded.
2039fn member_action(
2040    was_member: bool,
2041    is_mutated_key: bool,
2042    is_derived: bool,
2043    op: &str,
2044) -> MemberAction {
2045    if !is_mutated_key {
2046        // A key entering the window has no local state to merge into, so it
2047        // needs the whole entity; one already held is unchanged.
2048        return if was_member {
2049            MemberAction::Skip
2050        } else {
2051            MemberAction::Upsert
2052        };
2053    }
2054    // The mutated entity rides its own patch through untouched, but only when
2055    // the subscriber already holds a copy. Derived views still send whole
2056    // entities: the patch on the bus is scoped to the source view, not this
2057    // one (see A4-150).
2058    if was_member && !is_derived && op != "delete" {
2059        MemberAction::ForwardPatch
2060    } else {
2061        MemberAction::Upsert
2062    }
2063}
2064
2065fn to_wire_snapshot_entities(
2066    entities: Vec<(String, Value)>,
2067    view_spec: &ViewSpec,
2068) -> Vec<SnapshotEntity> {
2069    entities
2070        .into_iter()
2071        .map(|(key, mut data)| {
2072            apply_wire_format(&mut data, &view_spec.wire_format);
2073            SnapshotEntity { key, data }
2074        })
2075        .collect()
2076}
2077
2078async fn load_query_entities(
2079    entity_cache: &EntityCache,
2080    sorted_caches: Option<
2081        Arc<tokio::sync::RwLock<HashMap<String, crate::sorted_cache::SortedViewCache>>>,
2082    >,
2083    view_spec: &ViewSpec,
2084    query: &SubscriptionQuery,
2085    apply_snapshot_limit: bool,
2086) -> Vec<(String, Value)> {
2087    let (entities, preordered) = if let Some(sorted_caches) = sorted_caches {
2088        let mut caches = sorted_caches.write().await;
2089        let entities = caches
2090            .get_mut(&view_spec.id)
2091            .map(|cache| cache.get_all_ordered())
2092            .unwrap_or_default();
2093        (entities, true)
2094    } else if view_spec.mode == Mode::State {
2095        let entity = match query.key.as_deref() {
2096            Some(key) => entity_cache
2097                .get(&view_spec.id, key)
2098                .await
2099                .map(|data| vec![(key.to_string(), data)])
2100                .unwrap_or_default(),
2101            None => vec![],
2102        };
2103        (entity, true)
2104    } else {
2105        (entity_cache.get_all(&view_spec.id).await, false)
2106    };
2107    select_query_entities(entities, query, preordered, apply_snapshot_limit)
2108}
2109
2110fn select_query_entities(
2111    mut entities: Vec<(String, Value)>,
2112    query: &SubscriptionQuery,
2113    preordered: bool,
2114    apply_snapshot_limit: bool,
2115) -> Vec<(String, Value)> {
2116    entities.retain(|(key, data)| query_matches_entity(query, key, data));
2117    if !preordered {
2118        entities.sort_by(|left, right| {
2119            let left_seq = left.1.get("_seq").and_then(Value::as_str).unwrap_or("");
2120            let right_seq = right.1.get("_seq").and_then(Value::as_str).unwrap_or("");
2121            let order = if query.after.is_some() {
2122                cmp_seq(left_seq, right_seq)
2123            } else {
2124                cmp_seq(right_seq, left_seq)
2125            };
2126            order.then_with(|| left.0.cmp(&right.0))
2127        });
2128    }
2129
2130    let skip = query.skip.unwrap_or(0);
2131    let take = query.take.unwrap_or(usize::MAX);
2132    let mut selected: Vec<_> = entities.into_iter().skip(skip).take(take).collect();
2133    if apply_snapshot_limit {
2134        if let Some(limit) = query.snapshot_limit {
2135            selected.truncate(limit);
2136        }
2137    }
2138    selected
2139}
2140
2141fn query_matches_entity(query: &SubscriptionQuery, key: &str, data: &Value) -> bool {
2142    if !query.matches_key(key) {
2143        return false;
2144    }
2145    if let Some(partition) = &query.partition {
2146        if value_at_dot_path(data, "_partition") != Some(&Value::String(partition.clone())) {
2147            return false;
2148        }
2149    }
2150    if let Some(after) = &query.after {
2151        let Some(seq) = data.get("_seq").and_then(Value::as_str) else {
2152            return false;
2153        };
2154        if cmp_seq(seq, after) != std::cmp::Ordering::Greater {
2155            return false;
2156        }
2157    }
2158    query
2159        .filters
2160        .iter()
2161        .all(|(path, expected)| value_at_dot_path(data, path) == Some(expected))
2162}
2163
2164fn value_at_dot_path<'a>(value: &'a Value, path: &str) -> Option<&'a Value> {
2165    path.split('.')
2166        .try_fold(value, |current, segment| current.get(segment))
2167}
2168
2169#[cfg(test)]
2170mod tests {
2171    use super::*;
2172    use crate::cache::EntityCacheConfig;
2173    use crate::view::{Delivery, Filters, Projection};
2174    use serde_json::json;
2175    use tokio::sync::oneshot;
2176
2177    fn list_spec() -> ViewSpec {
2178        ViewSpec {
2179            id: "Thing/list".to_string(),
2180            export: "Thing".to_string(),
2181            mode: Mode::List,
2182            wire_format: Default::default(),
2183            projection: Projection::all(),
2184            filters: Filters::all(),
2185            delivery: Delivery::default(),
2186            pipeline: None,
2187            source_view: None,
2188        }
2189    }
2190
2191    /// Plan the whole window the way `emit_collection_delta` does, so a test
2192    /// can assert on what a subscriber is actually sent.
2193    fn plan_window(
2194        current_keys: &[&str],
2195        next_keys: &[&str],
2196        envelope_key: &str,
2197        is_derived: bool,
2198        op: &str,
2199    ) -> Vec<(String, MemberAction)> {
2200        next_keys
2201            .iter()
2202            .map(|key| {
2203                let was_member = current_keys.contains(key);
2204                (
2205                    (*key).to_string(),
2206                    member_action(was_member, *key == envelope_key, is_derived, op),
2207                )
2208            })
2209            .collect()
2210    }
2211
2212    #[test]
2213    fn a_reordered_window_sends_only_the_mutated_entity() {
2214        // A `_seq`-descending list: mutating "1" moves it to the front and
2215        // shifts every other key down one. Only "1" changed, so only "1" is
2216        // sent — and it rides its own patch, not a full entity.
2217        let current = ["4", "3", "2", "1"];
2218        let next = ["1", "4", "3", "2"];
2219        let plan = plan_window(&current, &next, "1", false, "patch");
2220
2221        assert_eq!(
2222            plan,
2223            vec![
2224                ("1".to_string(), MemberAction::ForwardPatch),
2225                ("4".to_string(), MemberAction::Skip),
2226                ("3".to_string(), MemberAction::Skip),
2227                ("2".to_string(), MemberAction::Skip),
2228            ]
2229        );
2230    }
2231
2232    #[test]
2233    fn a_key_entering_the_window_gets_the_whole_entity() {
2234        // "5" has no local state for the subscriber to merge a patch into.
2235        let plan = plan_window(&["4", "3"], &["5", "4", "3"], "5", false, "patch");
2236        assert_eq!(
2237            plan,
2238            vec![
2239                ("5".to_string(), MemberAction::Upsert),
2240                ("4".to_string(), MemberAction::Skip),
2241                ("3".to_string(), MemberAction::Skip),
2242            ]
2243        );
2244    }
2245
2246    #[test]
2247    fn derived_views_still_send_whole_entities() {
2248        // The patch on the bus is scoped to the source view, so a derived
2249        // subscription cannot forward it verbatim (A4-150).
2250        let plan = plan_window(&["1", "2"], &["1", "2"], "1", true, "patch");
2251        assert_eq!(
2252            plan,
2253            vec![
2254                ("1".to_string(), MemberAction::Upsert),
2255                ("2".to_string(), MemberAction::Skip),
2256            ]
2257        );
2258    }
2259
2260    #[test]
2261    fn a_delete_envelope_never_forwards_a_patch() {
2262        // A surviving key on a delete envelope carries no mergeable patch.
2263        let plan = plan_window(&["1", "2"], &["1", "2"], "1", false, "delete");
2264        assert_eq!(
2265            plan,
2266            vec![
2267                ("1".to_string(), MemberAction::Upsert),
2268                ("2".to_string(), MemberAction::Skip),
2269            ]
2270        );
2271    }
2272
2273    #[test]
2274    fn an_unchanged_window_sends_one_frame_not_a_broadcast() {
2275        // The regression this guards: 500 members used to mean 500 full
2276        // entities on the wire for a single mutation.
2277        let keys: Vec<String> = (0..500).map(|index| index.to_string()).collect();
2278        let refs: Vec<&str> = keys.iter().map(String::as_str).collect();
2279        let plan = plan_window(&refs, &refs, "250", false, "patch");
2280
2281        let sent = plan
2282            .iter()
2283            .filter(|(_, action)| *action != MemberAction::Skip)
2284            .count();
2285        assert_eq!(sent, 1);
2286        assert_eq!(plan[250].1, MemberAction::ForwardPatch);
2287    }
2288
2289    #[test]
2290    fn snapshot_batches_share_identity_and_completion() {
2291        let entities = ["one", "two", "three"].map(|key| SnapshotEntity {
2292            key: key.to_string(),
2293            data: json!({"key": key}),
2294        });
2295        let batches = create_snapshot_batches(
2296            &entities,
2297            SnapshotMetadata {
2298                subscription_id: "sub-1",
2299                snapshot_id: "snapshot-1",
2300                authoritative: true,
2301                mode: Mode::List,
2302                view_id: "Thing/list",
2303                key: None,
2304            },
2305            &SnapshotBatchConfig {
2306                initial_batch_size: 2,
2307                subsequent_batch_size: 1,
2308            },
2309        );
2310        assert_eq!(batches.len(), 2);
2311        assert!(batches.iter().all(|batch| batch.subscription_id == "sub-1"));
2312        assert!(batches
2313            .iter()
2314            .all(|batch| batch.snapshot_id == "snapshot-1"));
2315        assert!(!batches[0].complete);
2316        assert!(batches[1].complete);
2317        assert!(batches.iter().all(|batch| batch.authoritative));
2318    }
2319
2320    #[test]
2321    fn empty_incremental_snapshot_is_explicitly_non_authoritative() {
2322        let batches = create_snapshot_batches(
2323            &[],
2324            SnapshotMetadata {
2325                subscription_id: "sub-1",
2326                snapshot_id: "snapshot-1",
2327                authoritative: false,
2328                mode: Mode::State,
2329                view_id: "Thing/state",
2330                key: Some("missing"),
2331            },
2332            &SnapshotBatchConfig {
2333                initial_batch_size: 1,
2334                subsequent_batch_size: 1,
2335            },
2336        );
2337        assert_eq!(batches.len(), 1);
2338        assert!(!batches[0].authoritative);
2339        assert!(batches[0].complete);
2340        assert_eq!(batches[0].key.as_deref(), Some("missing"));
2341    }
2342
2343    #[test]
2344    fn dot_path_filters_are_exact_and_type_sensitive() {
2345        let mut query = SubscriptionQuery {
2346            view: "Thing/list".to_string(),
2347            ..Default::default()
2348        };
2349        query
2350            .filters
2351            .insert("state.status".to_string(), json!("open"));
2352        query.filters.insert("metrics.count".to_string(), json!(2));
2353        assert!(query_matches_entity(
2354            &query,
2355            "one",
2356            &json!({"state": {"status": "open"}, "metrics": {"count": 2}}),
2357        ));
2358        assert!(!query_matches_entity(
2359            &query,
2360            "one",
2361            &json!({"state": {"status": "open"}, "metrics": {"count": "2"}}),
2362        ));
2363    }
2364
2365    #[test]
2366    fn take_and_skip_define_independent_deterministic_windows() {
2367        let entities: Vec<_> = (1..=6)
2368            .map(|id| {
2369                (
2370                    id.to_string(),
2371                    json!({"id": id, "_seq": format!("10:{id:012}")}),
2372                )
2373            })
2374            .collect();
2375        let first = SubscriptionQuery {
2376            view: "Thing/list".to_string(),
2377            take: Some(2),
2378            skip: Some(0),
2379            ..Default::default()
2380        };
2381        let second = SubscriptionQuery {
2382            skip: Some(2),
2383            ..first.clone()
2384        };
2385        let first_keys: Vec<_> = select_query_entities(entities.clone(), &first, false, false)
2386            .into_iter()
2387            .map(|(key, _)| key)
2388            .collect();
2389        let second_keys: Vec<_> = select_query_entities(entities, &second, false, false)
2390            .into_iter()
2391            .map(|(key, _)| key)
2392            .collect();
2393        assert_eq!(first_keys, ["6", "5"]);
2394        assert_eq!(second_keys, ["4", "3"]);
2395    }
2396
2397    #[tokio::test]
2398    async fn state_receiver_is_installed_before_snapshot_awaits() {
2399        let bus = BusManager::new();
2400        let (snapshot_started_tx, snapshot_started_rx) = oneshot::channel();
2401        let (release_snapshot_tx, release_snapshot_rx) = oneshot::channel();
2402        let bus_for_task = bus.clone();
2403        let task = tokio::spawn(async move {
2404            subscribe_state_then_snapshot(&bus_for_task, "Thing/state", "one", || async move {
2405                snapshot_started_tx.send(()).unwrap();
2406                release_snapshot_rx.await.unwrap();
2407            })
2408            .await
2409            .0
2410        });
2411        snapshot_started_rx.await.unwrap();
2412        bus.publish_state(
2413            "Thing/state",
2414            "one",
2415            Arc::new(Bytes::from_static(br#"{"op":"patch"}"#)),
2416        )
2417        .await;
2418        release_snapshot_tx.send(()).unwrap();
2419        let mut receiver = task.await.unwrap();
2420        receiver.changed().await.unwrap();
2421        assert!(!receiver.borrow().is_empty());
2422    }
2423
2424    async fn assert_list_receiver_precedes_snapshot(view: &'static str) {
2425        let bus = BusManager::new();
2426        let (snapshot_started_tx, snapshot_started_rx) = oneshot::channel();
2427        let (release_snapshot_tx, release_snapshot_rx) = oneshot::channel();
2428        let bus_for_task = bus.clone();
2429        let task = tokio::spawn(async move {
2430            subscribe_list_then_snapshot(&bus_for_task, view, || async move {
2431                snapshot_started_tx.send(()).unwrap();
2432                release_snapshot_rx.await.unwrap();
2433            })
2434            .await
2435            .0
2436        });
2437        snapshot_started_rx.await.unwrap();
2438        bus.publish_list(
2439            view,
2440            Arc::new(BusMessage {
2441                key: "one".to_string(),
2442                entity: view.to_string(),
2443                payload: Arc::new(Bytes::from_static(br#"{"op":"patch"}"#)),
2444            }),
2445        )
2446        .await;
2447        release_snapshot_tx.send(()).unwrap();
2448        let mut receiver = task.await.unwrap();
2449        assert_eq!(receiver.recv().await.unwrap().key, "one");
2450    }
2451
2452    #[tokio::test]
2453    async fn list_receiver_is_installed_before_snapshot() {
2454        assert_list_receiver_precedes_snapshot("Thing/list").await;
2455    }
2456
2457    #[tokio::test]
2458    async fn append_receiver_is_installed_before_snapshot() {
2459        assert_list_receiver_precedes_snapshot("Thing/append").await;
2460    }
2461
2462    #[tokio::test]
2463    async fn derived_source_receiver_is_installed_before_snapshot() {
2464        assert_list_receiver_precedes_snapshot("Thing/list-source").await;
2465    }
2466
2467    #[tokio::test]
2468    async fn snapshot_limit_does_not_change_live_take_skip_membership() {
2469        let cache = EntityCache::with_config(EntityCacheConfig {
2470            max_entities_per_view: 10,
2471            ..Default::default()
2472        });
2473        for id in 1..=4 {
2474            cache
2475                .upsert(
2476                    "Thing/list",
2477                    &id.to_string(),
2478                    json!({"_seq": format!("10:{id:012}")}),
2479                )
2480                .await;
2481        }
2482        let query = SubscriptionQuery {
2483            view: "Thing/list".to_string(),
2484            take: Some(3),
2485            skip: Some(1),
2486            snapshot_limit: Some(1),
2487            ..Default::default()
2488        };
2489        let live = load_query_entities(&cache, None, &list_spec(), &query, false).await;
2490        let snapshot = load_query_entities(&cache, None, &list_spec(), &query, true).await;
2491        assert_eq!(live.len(), 3);
2492        assert_eq!(snapshot.len(), 1);
2493    }
2494
2495    #[test]
2496    fn fixture_manifest_covers_required_conformance_cases() {
2497        let manifest: Value = serde_json::from_str(include_str!(
2498            "../../../../tests/fixtures/websocket-v2/manifest.json"
2499        ))
2500        .unwrap();
2501        let names: HashSet<_> = manifest["fixtures"]
2502            .as_array()
2503            .unwrap()
2504            .iter()
2505            .filter_map(Value::as_str)
2506            .collect();
2507        for required in [
2508            "keyed-state.json",
2509            "list-windows.json",
2510            "filters.json",
2511            "multi-batch-authoritative.json",
2512            "empty-snapshot.json",
2513            "remove.json",
2514            "delete.json",
2515            "incremental-snapshot.json",
2516            "reconnect-replacement.json",
2517            "errors.json",
2518        ] {
2519            assert!(names.contains(required), "missing fixture {required}");
2520        }
2521
2522        for document in [
2523            include_str!("../../../../tests/fixtures/websocket-v2/keyed-state.json"),
2524            include_str!("../../../../tests/fixtures/websocket-v2/list-windows.json"),
2525            include_str!("../../../../tests/fixtures/websocket-v2/filters.json"),
2526            include_str!("../../../../tests/fixtures/websocket-v2/multi-batch-authoritative.json"),
2527            include_str!("../../../../tests/fixtures/websocket-v2/empty-snapshot.json"),
2528            include_str!("../../../../tests/fixtures/websocket-v2/remove.json"),
2529            include_str!("../../../../tests/fixtures/websocket-v2/delete.json"),
2530            include_str!("../../../../tests/fixtures/websocket-v2/incremental-snapshot.json"),
2531            include_str!("../../../../tests/fixtures/websocket-v2/reconnect-replacement.json"),
2532            include_str!("../../../../tests/fixtures/websocket-v2/errors.json"),
2533        ] {
2534            let fixture: Value = serde_json::from_str(document).unwrap();
2535            assert!(fixture["name"].is_string());
2536        }
2537    }
2538
2539    fn append_frame(key: &str, data: Value) -> Arc<Bytes> {
2540        let frame = json!({
2541            "entity": "Trade/append",
2542            "op": "patch",
2543            "key": key,
2544            "offset": 7,
2545            "data": data,
2546        });
2547        Arc::new(Bytes::from(serde_json::to_vec(&frame).unwrap()))
2548    }
2549
2550    /// A replayable subscription must honour the same predicates a live
2551    /// collection subscription does; otherwise a filtered consumer receives
2552    /// events outside the query it asked for.
2553    #[test]
2554    fn replay_delivery_applies_key_partition_and_filters() {
2555        let matching = append_frame("pool1", json!({"_partition": "us", "side": "buy"}));
2556        let other_partition = append_frame("pool1", json!({"_partition": "eu", "side": "buy"}));
2557        let other_side = append_frame("pool1", json!({"_partition": "us", "side": "sell"}));
2558
2559        let unfiltered = SubscriptionQuery {
2560            view: "Trade/append".to_string(),
2561            ..Default::default()
2562        };
2563        assert!(live_frame_matches(&unfiltered, "pool1", &matching));
2564        assert!(live_frame_matches(&unfiltered, "pool9", &matching));
2565
2566        let keyed = SubscriptionQuery {
2567            view: "Trade/append".to_string(),
2568            key: Some("pool1".to_string()),
2569            ..Default::default()
2570        };
2571        assert!(live_frame_matches(&keyed, "pool1", &matching));
2572        assert!(!live_frame_matches(&keyed, "pool2", &matching));
2573
2574        let partitioned = SubscriptionQuery {
2575            view: "Trade/append".to_string(),
2576            partition: Some("us".to_string()),
2577            ..Default::default()
2578        };
2579        assert!(live_frame_matches(&partitioned, "pool1", &matching));
2580        assert!(!live_frame_matches(&partitioned, "pool1", &other_partition));
2581
2582        let filtered = SubscriptionQuery {
2583            view: "Trade/append".to_string(),
2584            filters: [("side".to_string(), json!("buy"))].into_iter().collect(),
2585            ..Default::default()
2586        };
2587        assert!(live_frame_matches(&filtered, "pool1", &matching));
2588        assert!(!live_frame_matches(&filtered, "pool1", &other_side));
2589    }
2590
2591    #[test]
2592    fn a_frame_without_decodable_data_does_not_satisfy_a_filter() {
2593        let filtered = SubscriptionQuery {
2594            view: "Trade/append".to_string(),
2595            filters: [("side".to_string(), json!("buy"))].into_iter().collect(),
2596            ..Default::default()
2597        };
2598        let garbage = Arc::new(Bytes::from_static(b"not json"));
2599        assert!(!live_frame_matches(&filtered, "pool1", &garbage));
2600    }
2601
2602    /// The recovery cursor must be the last offset delivered *before* the
2603    /// gap. Reporting the newest offset seen would step the consumer over
2604    /// the skipped records permanently.
2605    #[test]
2606    fn replay_lagged_recovers_from_before_the_gap() {
2607        let epoch = crate::journal::JournalEpoch::new();
2608        let issue = SocketIssueMessage::replay_lagged(
2609            Some("trades".to_string()),
2610            37,
2611            Some(crate::journal::Cursor {
2612                epoch: epoch.clone(),
2613                offset: 4180,
2614            }),
2615        );
2616        assert_eq!(issue.code, "replay-lagged");
2617        assert_eq!(issue.recover_from, Some(format!("{epoch}:4180")));
2618        assert!(
2619            issue.suggested_action.unwrap().contains("4180"),
2620            "the consumer is told exactly which cursor recovers the gap"
2621        );
2622
2623        // Nothing delivered yet: there is no pre-gap offset, so the whole
2624        // retained window is the recovery.
2625        let from_scratch = SocketIssueMessage::replay_lagged(Some("trades".to_string()), 9, None);
2626        assert_eq!(from_scratch.recover_from, None);
2627        assert!(from_scratch
2628            .suggested_action
2629            .unwrap()
2630            .contains("without `after`"));
2631    }
2632
2633    fn bus_message(key: &str) -> Arc<BusMessage> {
2634        Arc::new(BusMessage {
2635            key: key.to_string(),
2636            entity: "Trade/append".to_string(),
2637            payload: Arc::new(Bytes::from_static(b"{}")),
2638        })
2639    }
2640
2641    /// A long replay must keep the bus drained. The broadcast buffer is
2642    /// bounded, so a busy view would otherwise lap the replay and the first
2643    /// live `recv` would return `Lagged` — telling the client to resubscribe,
2644    /// starting another long replay, which laps again.
2645    #[tokio::test]
2646    async fn draining_during_a_replay_keeps_a_busy_view_from_lapping_it() {
2647        let (sender, mut receiver) = broadcast::channel::<Arc<BusMessage>>(16);
2648        let mut pending = VecDeque::new();
2649        let mut lagged = None;
2650
2651        // Publish more than the channel holds, draining as a replay would
2652        // between sends.
2653        for index in 0..48 {
2654            sender.send(bus_message(&format!("k{index}"))).unwrap();
2655            drain_available(&mut receiver, &mut pending, &mut lagged);
2656        }
2657
2658        assert_eq!(lagged, None, "draining as we go means nothing is dropped");
2659        assert_eq!(pending.len(), 48, "every published frame is buffered");
2660    }
2661
2662    #[tokio::test]
2663    async fn a_replay_that_never_drains_is_reported_as_a_gap() {
2664        let (sender, mut receiver) = broadcast::channel::<Arc<BusMessage>>(8);
2665        for index in 0..32 {
2666            sender.send(bus_message(&format!("k{index}"))).unwrap();
2667        }
2668
2669        let mut pending = VecDeque::new();
2670        let mut lagged = None;
2671        drain_available(&mut receiver, &mut pending, &mut lagged);
2672
2673        assert!(
2674            lagged.is_some(),
2675            "overflowing the bus is a gap, not silent truncation"
2676        );
2677    }
2678
2679    /// Everything still on the bus after a gap is on the far side of it.
2680    /// Buffering it would put those frames in front of the lag report and
2681    /// advance the recovery cursor past the records it is meant to recover.
2682    #[tokio::test]
2683    async fn nothing_after_a_gap_is_buffered_ahead_of_the_report() {
2684        let (sender, mut receiver) = broadcast::channel::<Arc<BusMessage>>(8);
2685        let mut pending = VecDeque::new();
2686        let mut lagged = None;
2687
2688        // Delivered and buffered normally.
2689        sender.send(bus_message("before")).unwrap();
2690        drain_available(&mut receiver, &mut pending, &mut lagged);
2691        assert_eq!(pending.len(), 1);
2692
2693        // Overflow the bus: everything published from here is past the gap.
2694        for index in 0..32 {
2695            sender.send(bus_message(&format!("lost{index}"))).unwrap();
2696        }
2697        drain_available(&mut receiver, &mut pending, &mut lagged);
2698        assert!(lagged.is_some());
2699
2700        // The bus still holds what survived the overflow, all of it past the
2701        // gap. A later iteration of the replay loop drains again.
2702        let buffered_at_gap = pending.len();
2703        drain_available(&mut receiver, &mut pending, &mut lagged);
2704        assert_eq!(
2705            pending.len(),
2706            buffered_at_gap,
2707            "post-gap frames must not join the pre-gap flush"
2708        );
2709        assert_eq!(pending.front().unwrap().key, "before");
2710    }
2711    /// The bus is subscribed before the tape is read, so a record published
2712    /// in that window arrives on both paths.
2713    #[test]
2714    fn the_seam_between_replay_and_live_neither_repeats_nor_skips() {
2715        let mut last_sent = Some(4211);
2716
2717        assert!(
2718            already_delivered(Some(4211), &mut last_sent),
2719            "the record the replay ended on must not be sent twice"
2720        );
2721        assert!(already_delivered(Some(4100), &mut last_sent));
2722        assert_eq!(last_sent, Some(4211), "a duplicate never moves the mark");
2723
2724        assert!(
2725            !already_delivered(Some(4212), &mut last_sent),
2726            "the next record is new"
2727        );
2728        assert_eq!(last_sent, Some(4212));
2729
2730        // A frame with no offset comes from a view with no tape; it cannot
2731        // have been replayed, and must not disturb the mark.
2732        assert!(!already_delivered(None, &mut last_sent));
2733        assert_eq!(last_sent, Some(4212));
2734    }
2735
2736    /// A subscription with no cursor has delivered nothing, so the first live
2737    /// frame is not a duplicate.
2738    #[test]
2739    fn a_fresh_subscription_delivers_its_first_live_frame() {
2740        let mut last_sent = None;
2741        assert!(!already_delivered(Some(0), &mut last_sent));
2742        assert_eq!(last_sent, Some(0));
2743    }
2744
2745    /// End-to-end over a real socket: the pieces above are unit-tested
2746    /// individually, but the thing a consumer actually does — reconnect with
2747    /// a stored cursor and keep reading — only exists once a subscription is
2748    /// attached to a connection.
2749    mod over_a_socket {
2750        use super::*;
2751        use crate::journal::{EventJournal, JournalConfig};
2752        use crate::projector::Projector;
2753        use crate::{MutationBatch, SlotContext};
2754        use arete_interpreter::Mutation;
2755        use futures_util::{SinkExt, StreamExt};
2756        use std::time::Duration;
2757        use tokio::net::{TcpListener, TcpStream};
2758        use tokio::sync::mpsc;
2759        use tokio_tungstenite::tungstenite::Message;
2760        use tokio_tungstenite::{client_async, WebSocketStream};
2761
2762        const RETAINED: u64 = 600;
2763
2764        fn append_index() -> ViewIndex {
2765            let mut index = ViewIndex::new();
2766            index.add_spec(ViewSpec {
2767                id: "Trade/append".to_string(),
2768                export: "Trade".to_string(),
2769                mode: Mode::Append,
2770                wire_format: Default::default(),
2771                projection: Projection::all(),
2772                filters: Filters::all(),
2773                delivery: Delivery::default(),
2774                pipeline: None,
2775                source_view: None,
2776            });
2777            index
2778        }
2779
2780        fn trade(index: u64) -> MutationBatch {
2781            MutationBatch::with_slot_context(
2782                vec![Mutation {
2783                    export: "Trade".to_string(),
2784                    key: json!(format!("pool{}", index % 4)),
2785                    patch: json!({"trade": index}),
2786                    append: vec![],
2787                }]
2788                .into_iter()
2789                .collect(),
2790                SlotContext::new(100 + index / 3, index % 3),
2791            )
2792        }
2793
2794        struct Harness {
2795            addr: SocketAddr,
2796            journal: Arc<EventJournal>,
2797            tx: mpsc::Sender<MutationBatch>,
2798        }
2799
2800        impl Harness {
2801            async fn start() -> Self {
2802                let view_index = Arc::new(append_index());
2803                let entity_cache = EntityCache::new();
2804                let bus_manager = BusManager::new();
2805                let journal = Arc::new(EventJournal::new(JournalConfig {
2806                    enabled: true,
2807                    max_bytes_per_view: u64::MAX,
2808                    max_records_per_view: 10_000,
2809                    max_age: Duration::from_secs(3_600),
2810                }));
2811
2812                let (tx, rx) = mpsc::channel::<MutationBatch>(256);
2813                tokio::spawn(
2814                    Projector::new(
2815                        view_index.clone(),
2816                        bus_manager.clone(),
2817                        entity_cache.clone(),
2818                        rx,
2819                        #[cfg(feature = "otel")]
2820                        None,
2821                    )
2822                    .with_journal(journal.clone())
2823                    .run(),
2824                );
2825
2826                let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2827                let addr = listener.local_addr().unwrap();
2828                let server = WebSocketServer::new(
2829                    addr,
2830                    bus_manager,
2831                    entity_cache,
2832                    view_index,
2833                    #[cfg(feature = "otel")]
2834                    None,
2835                )
2836                .with_journal(journal.clone());
2837                let (acceptor, _cleanup) = server.into_acceptor();
2838                tokio::spawn(async move { acceptor.serve_listener(listener).await });
2839
2840                Self { addr, journal, tx }
2841            }
2842
2843            async fn publish(&self, range: std::ops::Range<u64>) {
2844                for index in range {
2845                    self.tx.send(trade(index)).await.unwrap();
2846                }
2847                let (ack, wait) = oneshot::channel();
2848                self.tx
2849                    .send(MutationBatch::flush_marker(ack))
2850                    .await
2851                    .unwrap();
2852                wait.await.unwrap();
2853            }
2854
2855            async fn connect(&self) -> WebSocketStream<TcpStream> {
2856                let stream = TcpStream::connect(self.addr).await.unwrap();
2857                client_async(format!("ws://{}/", self.addr), stream)
2858                    .await
2859                    .unwrap()
2860                    .0
2861            }
2862        }
2863
2864        async fn next_frame(socket: &mut WebSocketStream<TcpStream>) -> Value {
2865            loop {
2866                let message = tokio::time::timeout(Duration::from_secs(10), socket.next())
2867                    .await
2868                    .expect("the server answers within the timeout")
2869                    .expect("the stream stays open")
2870                    .expect("a readable frame");
2871                // Control and data frames arrive as binary; issue frames as
2872                // text. Both are JSON.
2873                let bytes = match &message {
2874                    Message::Text(text) => text.as_bytes(),
2875                    Message::Binary(bytes) => bytes.as_ref(),
2876                    _ => continue,
2877                };
2878                return serde_json::from_slice(bytes).expect("frames are JSON");
2879            }
2880        }
2881
2882        /// Collect `count` event frames, ignoring anything else on the wire.
2883        async fn collect_trades(socket: &mut WebSocketStream<TcpStream>, count: usize) -> Vec<u64> {
2884            let mut offsets = Vec::with_capacity(count);
2885            while offsets.len() < count {
2886                let frame = next_frame(socket).await;
2887                assert_ne!(
2888                    frame["type"], "error",
2889                    "no error frame should interrupt delivery: {frame}"
2890                );
2891                if let Some(offset) = frame["offset"].as_u64() {
2892                    offsets.push(offset);
2893                }
2894            }
2895            offsets
2896        }
2897
2898        /// The headline claim: reconnecting with a stored cursor delivers every
2899        /// event published since it, in order, and then continues live without
2900        /// a duplicate or a hole at the seam.
2901        #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
2902        async fn a_reconnect_replays_from_a_cursor_and_continues_live() {
2903            let harness = Harness::start().await;
2904            harness.publish(0..RETAINED).await;
2905
2906            let cursor = harness.journal.window("Trade/append").await;
2907            let stored = format!("{}:{}", cursor.epoch, 99);
2908
2909            let mut socket = harness.connect().await;
2910            socket
2911                .send(Message::Text(
2912                    json!({
2913                        "type": "subscribe",
2914                        "protocolVersion": 2,
2915                        "subscriptionId": "trades",
2916                        "query": {"view": "Trade/append", "after": stored},
2917                    })
2918                    .to_string()
2919                    .into(),
2920                ))
2921                .await
2922                .unwrap();
2923
2924            let ack = next_frame(&mut socket).await;
2925            assert_eq!(ack["op"], "subscribed", "unexpected ack: {ack}");
2926            assert_eq!(ack["replayWindow"]["next"], json!(RETAINED));
2927
2928            // Well over the 500 a single read or buffer would cover.
2929            let replayed = collect_trades(&mut socket, (RETAINED - 100) as usize).await;
2930            assert_eq!(
2931                replayed,
2932                (100..RETAINED).collect::<Vec<_>>(),
2933                "every event after the cursor, in order, exactly once"
2934            );
2935
2936            // Published only now, so these can only arrive over the live path.
2937            harness.publish(RETAINED..RETAINED + 40).await;
2938            let live = collect_trades(&mut socket, 40).await;
2939            assert_eq!(
2940                live,
2941                (RETAINED..RETAINED + 40).collect::<Vec<_>>(),
2942                "the live stream resumes exactly where the replay stopped"
2943            );
2944
2945            socket.close(None).await.ok();
2946        }
2947
2948        /// Events published *during* the replay must still arrive. The replay
2949        /// and the live subscription are separate reads of the same tape, and
2950        /// the seam between them is where a naive implementation drops or
2951        /// repeats.
2952        #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
2953        async fn events_published_during_a_replay_are_not_lost() {
2954            let harness = Harness::start().await;
2955            harness.publish(0..RETAINED).await;
2956
2957            let epoch = harness.journal.window("Trade/append").await.epoch;
2958            let mut socket = harness.connect().await;
2959            socket
2960                .send(Message::Text(
2961                    json!({
2962                        "type": "subscribe",
2963                        "protocolVersion": 2,
2964                        "subscriptionId": "trades",
2965                        "query": {"view": "Trade/append", "after": format!("{epoch}:0")},
2966                    })
2967                    .to_string()
2968                    .into(),
2969                ))
2970                .await
2971                .unwrap();
2972            let ack = next_frame(&mut socket).await;
2973            assert_eq!(ack["op"], "subscribed", "unexpected ack: {ack}");
2974
2975            // Keep publishing while the replay is still draining.
2976            harness.publish(RETAINED..RETAINED + 200).await;
2977
2978            let total = (RETAINED + 200 - 1) as usize;
2979            let delivered = collect_trades(&mut socket, total).await;
2980            assert_eq!(
2981                delivered,
2982                (1..RETAINED + 200).collect::<Vec<_>>(),
2983                "replay and live output join without a gap or a repeat"
2984            );
2985
2986            socket.close(None).await.ok();
2987        }
2988
2989        /// A cursor from another tape lifetime is refused rather than served
2990        /// as a continuation, and the refusal releases the subscription id.
2991        #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2992        async fn a_stale_epoch_is_refused_and_frees_the_subscription_id() {
2993            let harness = Harness::start().await;
2994            harness.publish(0..50).await;
2995
2996            let mut socket = harness.connect().await;
2997            socket
2998                .send(Message::Text(
2999                    json!({
3000                        "type": "subscribe",
3001                        "protocolVersion": 2,
3002                        "subscriptionId": "trades",
3003                        "query": {
3004                            "view": "Trade/append",
3005                            "after": format!("{}:10", crate::journal::JournalEpoch::new()),
3006                        },
3007                    })
3008                    .to_string()
3009                    .into(),
3010                ))
3011                .await
3012                .unwrap();
3013
3014            let error = next_frame(&mut socket).await;
3015            assert_eq!(error["type"], "error", "unexpected frame: {error}");
3016            assert_eq!(error["code"], "cursor-epoch-changed");
3017
3018            // The documented recovery is to resubscribe without a cursor. That
3019            // only works if the refused attempt released the id.
3020            socket
3021                .send(Message::Text(
3022                    json!({
3023                        "type": "subscribe",
3024                        "protocolVersion": 2,
3025                        "subscriptionId": "trades",
3026                        "query": {"view": "Trade/append"},
3027                    })
3028                    .to_string()
3029                    .into(),
3030                ))
3031                .await
3032                .unwrap();
3033            let ack = next_frame(&mut socket).await;
3034            assert_eq!(
3035                ack["op"], "subscribed",
3036                "the refused id must be reusable: {ack}"
3037            );
3038
3039            assert_eq!(collect_trades(&mut socket, 50).await.len(), 50);
3040            socket.close(None).await.ok();
3041        }
3042    }
3043}