Skip to main content

arete_server/websocket/
server.rs

1use crate::bus::{BusManager, BusMessage, StateUpdate};
2use crate::cache::{cmp_seq, EntityCache, SnapshotBatchConfig};
3use crate::compression::maybe_compress;
4use crate::row_scope::{session_row_scopes, RowFilter};
5use crate::shared_entity::{lookup, EntityFields, SharedEntity};
6use crate::view::{ViewIndex, ViewSpec};
7use crate::websocket::admission::{WebSocketAdmissionProvider, WebSocketConnectionPermit};
8use crate::websocket::auth::{
9    AuthContext, AuthDecision, AuthDeny, ConnectionAuthRequest, WebSocketAuthPlugin,
10};
11use crate::websocket::client_manager::{ClientManager, RateLimitConfig};
12use crate::websocket::frame::{
13    apply_wire_format, Frame, Mode, SortConfig, SortOrder, SourceFields, SubscribedFrame,
14    UnsubscribedFrame, WireEntity, WireFormat,
15};
16use crate::websocket::subscription::{
17    ClientMessage, RefreshAuthRequest, RefreshAuthResponse, SocketIssueMessage, Subscription,
18    SubscriptionQuery, Unsubscription, PROTOCOL_VERSION,
19};
20use crate::websocket::usage::{UsageIdentity, WebSocketUsageEmitter, WebSocketUsageEvent};
21use crate::{WebSocketDeliveryConfig, WebSocketLimitsConfig};
22use anyhow::Result;
23use bytes::Bytes;
24use futures_util::StreamExt;
25use serde::Serialize;
26use serde_json::Value;
27use std::collections::{HashMap, HashSet, VecDeque};
28use std::future::Future;
29use std::net::SocketAddr;
30use std::sync::atomic::{AtomicUsize, Ordering};
31use std::sync::Arc;
32use std::time::{Duration, Instant};
33use tokio::net::{TcpListener, TcpStream};
34use tokio::sync::{broadcast, watch};
35use tokio_tungstenite::{
36    accept_hdr_async_with_config,
37    tungstenite::{
38        handshake::server::{ErrorResponse as HandshakeErrorResponse, Request, Response},
39        http::{header::CONTENT_TYPE, StatusCode},
40        protocol::WebSocketConfig,
41        Error as WsError,
42    },
43};
44use tokio_util::sync::CancellationToken;
45use tokio_util::task::TaskTracker;
46use tracing::{debug, error, info, info_span, warn, Instrument};
47use uuid::Uuid;
48
49#[cfg(feature = "otel")]
50use crate::metrics::Metrics;
51
52#[derive(Clone, Default)]
53struct WsMetrics {
54    #[cfg(feature = "otel")]
55    inner: Option<Arc<Metrics>>,
56    #[cfg(test)]
57    probe: Option<Arc<DeliveryProbe>>,
58}
59
60/// Test-only mirror of the delivery instruments recorded through
61/// [`WsMetrics`], so the real-socket load harness can report them without the
62/// `otel` feature. Each counter has the meaning of the metric named beside it.
63#[cfg(test)]
64#[derive(Debug, Default)]
65pub(crate) struct DeliveryProbe {
66    /// `arete.ws.connections.active`, by metering key
67    pub(crate) active_connections: std::sync::Mutex<std::collections::BTreeMap<String, i64>>,
68    /// `arete.ws.subscriptions.active`, by view and metering key
69    pub(crate) active_subscriptions: std::sync::Mutex<std::collections::BTreeMap<String, i64>>,
70    /// `arete.ws.messages.sent`
71    pub(crate) messages_sent: std::sync::atomic::AtomicU64,
72    /// `arete.ws.subscription.lagged`
73    pub(crate) lag_events: std::sync::atomic::AtomicU64,
74    /// `arete.ws.subscription.dropped_updates`
75    pub(crate) dropped_updates: std::sync::atomic::AtomicU64,
76    /// `arete.ws.subscription.resnapshots`
77    pub(crate) resnapshots: std::sync::atomic::AtomicU64,
78    /// `arete.ws.collection.coalesced_updates`
79    pub(crate) coalesced_updates: std::sync::atomic::AtomicU64,
80    /// `arete.ws.collection.coalesced_flushes`
81    pub(crate) coalesced_flushes: std::sync::atomic::AtomicU64,
82    /// `arete.ws.delivery.stopped`, by reason
83    pub(crate) delivery_stopped: std::sync::Mutex<std::collections::BTreeMap<&'static str, u64>>,
84}
85
86#[cfg(test)]
87impl DeliveryProbe {
88    fn add(counter: &std::sync::atomic::AtomicU64, value: u64) {
89        counter.fetch_add(value, std::sync::atomic::Ordering::Relaxed);
90    }
91
92    fn add_active(
93        counts: &std::sync::Mutex<std::collections::BTreeMap<String, i64>>,
94        key: String,
95        delta: i64,
96    ) {
97        *counts
98            .lock()
99            .expect("delivery probe lock poisoned")
100            .entry(key)
101            .or_default() += delta;
102    }
103}
104
105impl WsMetrics {
106    #[cfg(feature = "otel")]
107    fn new(inner: Option<Arc<Metrics>>) -> Self {
108        Self {
109            inner,
110            #[cfg(test)]
111            probe: None,
112        }
113    }
114
115    #[cfg(test)]
116    fn probe(&self, record: impl FnOnce(&DeliveryProbe)) {
117        if let Some(probe) = &self.probe {
118            record(probe);
119        }
120    }
121
122    fn connection_opened(&self, metering_key: Option<&str>) {
123        #[cfg(test)]
124        self.probe(|probe| {
125            DeliveryProbe::add_active(
126                &probe.active_connections,
127                metering_key.unwrap_or("<none>").to_string(),
128                1,
129            )
130        });
131        #[cfg(not(feature = "otel"))]
132        let _ = metering_key;
133        #[cfg(feature = "otel")]
134        if let Some(metrics) = &self.inner {
135            if let Some(metering_key) = metering_key {
136                metrics.record_ws_connection_with_metering(metering_key);
137            } else {
138                metrics.record_ws_connection();
139            }
140        }
141    }
142
143    fn connection_closed(&self, duration_secs: f64, metering_key: Option<&str>) {
144        #[cfg(test)]
145        self.probe(|probe| {
146            DeliveryProbe::add_active(
147                &probe.active_connections,
148                metering_key.unwrap_or("<none>").to_string(),
149                -1,
150            )
151        });
152        #[cfg(not(feature = "otel"))]
153        let _ = (duration_secs, metering_key);
154        #[cfg(feature = "otel")]
155        if let Some(metrics) = &self.inner {
156            if let Some(metering_key) = metering_key {
157                metrics.record_ws_disconnection_with_metering(duration_secs, metering_key);
158            } else {
159                metrics.record_ws_disconnection(duration_secs);
160            }
161        }
162    }
163
164    fn message_received(&self, metering_key: Option<&str>) {
165        #[cfg(not(feature = "otel"))]
166        let _ = metering_key;
167        #[cfg(feature = "otel")]
168        if let Some(metrics) = &self.inner {
169            if let Some(metering_key) = metering_key {
170                metrics.record_ws_message_received_with_metering(metering_key);
171            } else {
172                metrics.record_ws_message_received();
173            }
174        }
175    }
176
177    fn message_sent(&self) {
178        #[cfg(test)]
179        self.probe(|probe| DeliveryProbe::add(&probe.messages_sent, 1));
180        #[cfg(feature = "otel")]
181        if let Some(metrics) = &self.inner {
182            metrics.record_ws_message_sent();
183        }
184    }
185
186    fn subscription_created(&self, view: &str, metering_key: Option<&str>) {
187        #[cfg(test)]
188        self.probe(|probe| {
189            DeliveryProbe::add_active(
190                &probe.active_subscriptions,
191                format!("{view}|{}", metering_key.unwrap_or("<none>")),
192                1,
193            )
194        });
195        #[cfg(not(feature = "otel"))]
196        let _ = (view, metering_key);
197        #[cfg(feature = "otel")]
198        if let Some(metrics) = &self.inner {
199            if let Some(metering_key) = metering_key {
200                metrics.record_subscription_created_with_metering(view, metering_key);
201            } else {
202                metrics.record_subscription_created(view);
203            }
204        }
205    }
206
207    fn subscription_removed(&self, view: &str, metering_key: Option<&str>) {
208        #[cfg(test)]
209        self.probe(|probe| {
210            DeliveryProbe::add_active(
211                &probe.active_subscriptions,
212                format!("{view}|{}", metering_key.unwrap_or("<none>")),
213                -1,
214            )
215        });
216        #[cfg(not(feature = "otel"))]
217        let _ = (view, metering_key);
218        #[cfg(feature = "otel")]
219        if let Some(metrics) = &self.inner {
220            if let Some(metering_key) = metering_key {
221                metrics.record_subscription_removed_with_metering(view, metering_key);
222            } else {
223                metrics.record_subscription_removed(view);
224            }
225        }
226    }
227
228    fn protocol_error(&self, code: &str) {
229        #[cfg(not(feature = "otel"))]
230        let _ = code;
231        #[cfg(feature = "otel")]
232        if let Some(metrics) = &self.inner {
233            metrics.record_ws_protocol_error(code);
234        }
235    }
236
237    fn subscription_lagged(&self, view: &str, skipped: u64) {
238        #[cfg(test)]
239        self.probe(|probe| {
240            DeliveryProbe::add(&probe.lag_events, 1);
241            DeliveryProbe::add(&probe.dropped_updates, skipped);
242        });
243        #[cfg(not(feature = "otel"))]
244        let _ = (view, skipped);
245        #[cfg(feature = "otel")]
246        if let Some(metrics) = &self.inner {
247            metrics.record_ws_subscription_lagged(view, skipped);
248        }
249    }
250
251    fn subscription_resnapshot(&self, view: &str) {
252        #[cfg(test)]
253        self.probe(|probe| DeliveryProbe::add(&probe.resnapshots, 1));
254        #[cfg(not(feature = "otel"))]
255        let _ = view;
256        #[cfg(feature = "otel")]
257        if let Some(metrics) = &self.inner {
258            metrics.record_ws_subscription_resnapshot(view);
259        }
260    }
261
262    fn collection_coalesced(&self, view: &str, updates: u64) {
263        #[cfg(test)]
264        self.probe(|probe| {
265            DeliveryProbe::add(&probe.coalesced_updates, updates);
266            DeliveryProbe::add(&probe.coalesced_flushes, 1);
267        });
268        #[cfg(not(feature = "otel"))]
269        let _ = (view, updates);
270        #[cfg(feature = "otel")]
271        if let Some(metrics) = &self.inner {
272            metrics.record_ws_collection_coalesced(view, updates);
273        }
274    }
275
276    fn delivery_stopped(&self, view: &str, reason: &'static str) {
277        #[cfg(test)]
278        self.probe(|probe| {
279            *probe
280                .delivery_stopped
281                .lock()
282                .expect("delivery probe lock poisoned")
283                .entry(reason)
284                .or_default() += 1;
285        });
286        #[cfg(not(feature = "otel"))]
287        let _ = (view, reason);
288        #[cfg(feature = "otel")]
289        if let Some(metrics) = &self.inner {
290            metrics.record_ws_delivery_stopped(view, reason);
291        }
292    }
293}
294
295async fn handle_refresh_auth(
296    client_id: Uuid,
297    remote_addr: SocketAddr,
298    refresh_req: &RefreshAuthRequest,
299    client_manager: &ClientManager,
300    auth_plugin: &Arc<dyn WebSocketAuthPlugin>,
301) {
302    let refresh_result: Result<AuthContext, String> = if let Some(signed_plugin) = auth_plugin
303        .as_any()
304        .downcast_ref::<crate::websocket::auth::SignedSessionAuthPlugin>()
305    {
306        signed_plugin
307            .verify_refresh_token_from(&refresh_req.token, remote_addr)
308            .await
309            .map_err(|error| error.reason)
310    } else {
311        Err("In-band auth refresh not supported with current auth plugin".to_string())
312    };
313
314    let response = match refresh_result {
315        Ok(new_context) => {
316            let expires_at = new_context.expires_at;
317            match client_manager
318                .try_update_client_auth_async(client_id, new_context)
319                .await
320            {
321                Ok(true) => RefreshAuthResponse {
322                    success: true,
323                    error: None,
324                    expires_at: Some(expires_at),
325                },
326                Ok(false) => RefreshAuthResponse {
327                    success: false,
328                    error: Some("client-not-found".to_string()),
329                    expires_at: None,
330                },
331                Err(deny) => RefreshAuthResponse {
332                    success: false,
333                    error: Some(deny.code.as_str().to_string()),
334                    expires_at: None,
335                },
336            }
337        }
338        Err(error) => {
339            let code = if error.contains("expired") {
340                "token-expired"
341            } else if error.contains("signature") {
342                "token-invalid-signature"
343            } else if error.contains("issuer") {
344                "token-invalid-issuer"
345            } else if error.contains("audience") {
346                "token-invalid-audience"
347            } else {
348                "token-invalid"
349            };
350            RefreshAuthResponse {
351                success: false,
352                error: Some(code.to_string()),
353                expires_at: None,
354            }
355        }
356    };
357
358    if let Ok(json) = serde_json::to_string(&response) {
359        let _ = client_manager.send_text_to_client(client_id, json).await;
360    }
361}
362
363async fn send_socket_issue(
364    client_id: Uuid,
365    client_manager: &ClientManager,
366    deny: &AuthDeny,
367    fatal: bool,
368    subscription_id: Option<String>,
369) {
370    let message = SocketIssueMessage::from_auth_deny(deny, fatal, subscription_id);
371    if let Ok(json) = serde_json::to_string(&message) {
372        let _ = client_manager.send_text_to_client(client_id, json).await;
373    }
374}
375
376async fn send_protocol_issue(
377    client_id: Uuid,
378    client_manager: &ClientManager,
379    metrics: &WsMetrics,
380    subscription_id: Option<String>,
381    code: &str,
382    message: impl Into<String>,
383) {
384    metrics.protocol_error(code);
385    let issue = SocketIssueMessage::protocol(subscription_id, code, message);
386    if let Ok(json) = serde_json::to_string(&issue) {
387        let _ = client_manager.send_text_to_client(client_id, json).await;
388    }
389}
390
391/// Send an already-built issue, for refusals that carry structured detail.
392async fn send_prepared_issue(
393    client_id: Uuid,
394    client_manager: &ClientManager,
395    metrics: &WsMetrics,
396    issue: SocketIssueMessage,
397) {
398    metrics.protocol_error(&issue.code);
399    if let Ok(json) = serde_json::to_string(&issue) {
400        let _ = client_manager.send_text_to_client(client_id, json).await;
401    }
402}
403
404fn key_class_label(key_class: arete_auth::KeyClass) -> &'static str {
405    match key_class {
406        arete_auth::KeyClass::Secret => "secret",
407        arete_auth::KeyClass::Publishable => "publishable",
408    }
409}
410
411#[derive(Clone)]
412struct UsageEmitterHandle {
413    emitter: Arc<dyn WebSocketUsageEmitter>,
414    tasks: TaskTracker,
415}
416
417impl UsageEmitterHandle {
418    fn new(emitter: Arc<dyn WebSocketUsageEmitter>) -> Self {
419        Self {
420            emitter,
421            tasks: TaskTracker::new(),
422        }
423    }
424
425    fn emit(&self, event: WebSocketUsageEvent) {
426        let emitter = self.emitter.clone();
427        self.tasks.spawn(async move {
428            emitter.emit(event).await;
429        });
430    }
431
432    async fn close_and_wait(&self) {
433        self.tasks.close();
434        self.tasks.wait().await;
435    }
436}
437
438fn emit_usage_event(usage_emitter: &Option<UsageEmitterHandle>, event: WebSocketUsageEvent) {
439    if let Some(emitter) = usage_emitter {
440        emitter.emit(event);
441    }
442}
443
444fn usage_identity_from_context(
445    auth_context: Option<&AuthContext>,
446) -> (UsageIdentity, Option<String>) {
447    match auth_context {
448        Some(context) => {
449            let v2 = !context.is_legacy_policy();
450            (
451                UsageIdentity {
452                    metering_key: Some(context.metering_key.clone()),
453                    subject: Some(context.subject.clone()),
454                    key_class: Some(key_class_label(context.key_class).to_string()),
455                    actor_key: v2.then(|| context.actor_key.clone()).flatten(),
456                    account_key: v2.then(|| context.account_key.clone()).flatten(),
457                    consumer_key: v2.then(|| context.consumer_key.clone()).flatten(),
458                    plan_code: v2.then(|| context.plan.clone()).flatten(),
459                    policy_version: v2.then_some(context.policy_version).flatten(),
460                },
461                context.deployment_id.clone(),
462            )
463        }
464        None => (UsageIdentity::default(), None),
465    }
466}
467
468fn emit_update_sent_for_client(
469    usage_emitter: &Option<UsageEmitterHandle>,
470    client_manager: &ClientManager,
471    client_id: Uuid,
472    view_id: &str,
473    bytes: usize,
474) {
475    let auth_context = client_manager.get_auth_context(client_id);
476    let (identity, deployment_id) = usage_identity_from_context(auth_context.as_ref());
477    emit_usage_event(
478        usage_emitter,
479        WebSocketUsageEvent::UpdateSent {
480            client_id: client_id.to_string(),
481            deployment_id,
482            identity,
483            view_id: view_id.to_string(),
484            messages: 1,
485            bytes: bytes as u64,
486        },
487    );
488}
489
490#[derive(Clone)]
491struct SubscriptionContext {
492    client_id: Uuid,
493    client_manager: ClientManager,
494    bus_manager: BusManager,
495    entity_cache: EntityCache,
496    view_index: Arc<ViewIndex>,
497    usage_emitter: Option<UsageEmitterHandle>,
498    journal: Option<Arc<crate::journal::EventJournal>>,
499    metrics: WsMetrics,
500    delivery: WebSocketDeliveryConfig,
501    /// Cancelled when the server stops; every session ends through its normal
502    /// cleanup path rather than being dropped mid-flight.
503    shutdown: CancellationToken,
504    /// The connection's row-scope generation. A token refresh that changes
505    /// the session's row scopes bumps it under the write lock before it
506    /// resnapshots, which stops every subscription task still delivering on
507    /// the old scopes (see [`RowFilter::hold`]).
508    row_scope_lock: Arc<tokio::sync::RwLock<u64>>,
509    /// Set on one subscription's copy of the context when its view is
510    /// row-scoped: which rows that subscription may receive.
511    row_filter: Option<RowFilter>,
512    /// Set while re-attaching a subscription after its session's row scopes
513    /// changed: it sends no second `subscribed` frame, and its snapshot
514    /// replaces what the client holds.
515    resnapshot: bool,
516}
517
518pub struct WebSocketServer {
519    bind_addr: SocketAddr,
520    client_manager: ClientManager,
521    bus_manager: BusManager,
522    entity_cache: EntityCache,
523    view_index: Arc<ViewIndex>,
524    max_clients: usize,
525    auth_plugin: Arc<dyn WebSocketAuthPlugin>,
526    usage_emitter: Option<UsageEmitterHandle>,
527    rate_limit_config: Option<RateLimitConfig>,
528    admission_provider: Option<Arc<dyn WebSocketAdmissionProvider>>,
529    journal: Option<Arc<crate::journal::EventJournal>>,
530    delivery: WebSocketDeliveryConfig,
531    limits: WebSocketLimitsConfig,
532    #[cfg(feature = "otel")]
533    metrics: Option<Arc<Metrics>>,
534}
535
536impl WebSocketServer {
537    #[cfg(feature = "otel")]
538    pub fn new(
539        bind_addr: SocketAddr,
540        bus_manager: BusManager,
541        entity_cache: EntityCache,
542        view_index: Arc<ViewIndex>,
543        metrics: Option<Arc<Metrics>>,
544    ) -> Self {
545        Self {
546            bind_addr,
547            client_manager: ClientManager::new(),
548            bus_manager,
549            entity_cache,
550            view_index,
551            max_clients: 10_000,
552            auth_plugin: Arc::new(crate::websocket::auth::AllowAllAuthPlugin),
553            usage_emitter: None,
554            rate_limit_config: None,
555            admission_provider: None,
556            journal: None,
557            delivery: WebSocketDeliveryConfig::default(),
558            limits: WebSocketLimitsConfig::default(),
559            metrics,
560        }
561    }
562
563    #[cfg(not(feature = "otel"))]
564    pub fn new(
565        bind_addr: SocketAddr,
566        bus_manager: BusManager,
567        entity_cache: EntityCache,
568        view_index: Arc<ViewIndex>,
569    ) -> Self {
570        Self {
571            bind_addr,
572            client_manager: ClientManager::new(),
573            bus_manager,
574            entity_cache,
575            view_index,
576            max_clients: 10_000,
577            auth_plugin: Arc::new(crate::websocket::auth::AllowAllAuthPlugin),
578            usage_emitter: None,
579            rate_limit_config: None,
580            admission_provider: None,
581            journal: None,
582            delivery: WebSocketDeliveryConfig::default(),
583            limits: WebSocketLimitsConfig::default(),
584        }
585    }
586
587    pub fn with_max_clients(mut self, max_clients: usize) -> Self {
588        self.max_clients = max_clients;
589        self
590    }
591
592    pub fn with_auth_plugin(mut self, auth_plugin: Arc<dyn WebSocketAuthPlugin>) -> Self {
593        self.auth_plugin = auth_plugin;
594        self
595    }
596
597    pub fn with_usage_emitter(mut self, usage_emitter: Arc<dyn WebSocketUsageEmitter>) -> Self {
598        self.usage_emitter = Some(UsageEmitterHandle::new(usage_emitter));
599        self
600    }
601
602    /// Serve replayable append subscriptions from the retained event journal.
603    pub fn with_journal(mut self, journal: Arc<crate::journal::EventJournal>) -> Self {
604        self.journal = Some(journal);
605        self
606    }
607
608    pub fn with_rate_limit_config(mut self, config: RateLimitConfig) -> Self {
609        self.rate_limit_config = Some(config);
610        self
611    }
612
613    /// Install an embedder-owned provider for cross-runtime admission.
614    pub fn with_admission_provider(
615        mut self,
616        provider: Arc<dyn WebSocketAdmissionProvider>,
617    ) -> Self {
618        self.admission_provider = Some(provider);
619        self
620    }
621
622    /// Message, frame and handshake limits; see [`WebSocketLimitsConfig`].
623    pub fn with_limits(mut self, limits: WebSocketLimitsConfig) -> Self {
624        self.limits = limits;
625        self
626    }
627
628    pub fn with_delivery_config(mut self, config: WebSocketDeliveryConfig) -> Self {
629        self.delivery = config;
630        self
631    }
632
633    /// Bind the configured address and serve connections until the task is
634    /// dropped. Equivalent to [`into_acceptor`](Self::into_acceptor) followed by
635    /// [`ConnectionAcceptor::serve_listener`].
636    pub async fn start(self) -> Result<()> {
637        info!(
638            "Starting WebSocket server on {} (max_clients: {})",
639            self.bind_addr, self.max_clients
640        );
641        let listener = TcpListener::bind(&self.bind_addr).await?;
642        let (acceptor, _cleanup) = self.into_acceptor();
643        acceptor.serve_listener(listener).await
644    }
645
646    /// Split this server into the part that serves connections and the
647    /// client-manager cleanup task, leaving the caller to own the listener.
648    ///
649    /// The cleanup handle is returned rather than detached so a caller that
650    /// stops serving can stop it too.
651    pub(crate) fn into_acceptor(self) -> (ConnectionAcceptor, tokio::task::JoinHandle<()>) {
652        let mut client_manager = self
653            .rate_limit_config
654            .map(ClientManager::with_config)
655            .unwrap_or(self.client_manager);
656        if let Some(provider) = self.admission_provider {
657            client_manager = client_manager.with_admission_provider(provider);
658        }
659        let cleanup = client_manager.start_cleanup_task();
660
661        #[cfg(feature = "otel")]
662        let metrics = WsMetrics::new(self.metrics.clone());
663        #[cfg(not(feature = "otel"))]
664        let metrics = WsMetrics::default();
665
666        let acceptor = ConnectionAcceptor {
667            client_manager,
668            bus_manager: self.bus_manager,
669            entity_cache: self.entity_cache,
670            view_index: self.view_index,
671            max_clients: self.max_clients,
672            auth_plugin: self.auth_plugin,
673            usage_emitter: self.usage_emitter,
674            journal: self.journal,
675            delivery: self.delivery,
676            limits: self.limits,
677            pending_handshakes: Arc::new(AtomicUsize::new(0)),
678            metrics,
679            shutdown: CancellationToken::new(),
680            sessions: TaskTracker::new(),
681        };
682        (acceptor, cleanup)
683    }
684}
685
686/// Serves already-accepted TCP connections against one server's buses, cache
687/// and views.
688///
689/// This is what [`WebSocketServer::start`] runs behind its listener, separated
690/// so that a caller that owns the listener (an application that terminates
691/// TLS itself, a test with an ephemeral port) can hand streams in without the
692/// server binding anything.
693#[derive(Clone)]
694pub(crate) struct ConnectionAcceptor {
695    client_manager: ClientManager,
696    bus_manager: BusManager,
697    entity_cache: EntityCache,
698    view_index: Arc<ViewIndex>,
699    max_clients: usize,
700    auth_plugin: Arc<dyn WebSocketAuthPlugin>,
701    usage_emitter: Option<UsageEmitterHandle>,
702    journal: Option<Arc<crate::journal::EventJournal>>,
703    delivery: WebSocketDeliveryConfig,
704    limits: WebSocketLimitsConfig,
705    /// Connections accepted but not yet upgraded. They count against `max_clients` too, so
706    /// stalled handshakes cannot hold more sockets than established sessions could.
707    pending_handshakes: Arc<AtomicUsize>,
708    metrics: WsMetrics,
709    shutdown: CancellationToken,
710    /// Sessions spawned by [`serve_listener`](Self::serve_listener), so a
711    /// stop can wait for them. Sessions a caller serves on its own tasks are
712    /// the caller's to wait for.
713    sessions: TaskTracker,
714}
715
716impl ConnectionAcceptor {
717    /// Mirror this acceptor's delivery instruments into `probe`. Must be set
718    /// before serving: each session copies the metrics handle when it starts.
719    #[cfg(test)]
720    pub(crate) fn with_delivery_probe(mut self, probe: Arc<DeliveryProbe>) -> Self {
721        self.metrics.probe = Some(probe);
722        self
723    }
724
725    /// Number of clients currently connected to this server.
726    pub(crate) fn client_count(&self) -> usize {
727        self.client_manager.client_count()
728    }
729
730    /// End every session this acceptor is serving and stop accepting.
731    ///
732    /// Sessions notice on their next poll and leave through the same cleanup
733    /// as a client disconnect, so the client manager, buses and usage events
734    /// see an ordinary close.
735    pub(crate) fn shutdown(&self) {
736        self.shutdown.cancel();
737        self.sessions.close();
738    }
739
740    /// Resolves once every listener-spawned session has finished cleaning
741    /// up. Call after [`shutdown`](Self::shutdown).
742    pub(crate) async fn wait_for_sessions(&self) {
743        self.sessions.wait().await;
744    }
745
746    /// Stop accepting usage emissions and wait for every event spawned by a
747    /// completed session to enter the emitter queue.
748    pub(crate) async fn wait_for_usage_events(&self) {
749        if let Some(emitter) = &self.usage_emitter {
750            emitter.close_and_wait().await;
751        }
752    }
753
754    /// Serve one accepted connection: WebSocket handshake, authentication,
755    /// then the subscription session until the peer disconnects.
756    ///
757    /// Returns `Ok(())` without serving when the server is at its client
758    /// limit, exactly as the listener loop does.
759    pub(crate) async fn serve(&self, stream: TcpStream, remote_addr: SocketAddr) -> Result<()> {
760        let Some(pending) =
761            PendingHandshake::reserve(&self.pending_handshakes, self.limits.max_pending_handshakes)
762        else {
763            warn!(
764                "Rejecting connection from {}: too many pending handshakes",
765                remote_addr
766            );
767            return Ok(());
768        };
769        if self.client_manager.client_count() + pending.count() > self.max_clients {
770            warn!(
771                "Rejecting connection from {}: max clients reached",
772                remote_addr
773            );
774            return Ok(());
775        }
776
777        let context = SubscriptionContext {
778            client_id: Uuid::nil(),
779            client_manager: self.client_manager.clone(),
780            bus_manager: self.bus_manager.clone(),
781            entity_cache: self.entity_cache.clone(),
782            view_index: self.view_index.clone(),
783            usage_emitter: self.usage_emitter.clone(),
784            journal: self.journal.clone(),
785            delivery: self.delivery.clone(),
786            metrics: self.metrics.clone(),
787            shutdown: self.shutdown.clone(),
788            row_scope_lock: Arc::new(tokio::sync::RwLock::new(0)),
789            row_filter: None,
790            resnapshot: false,
791        };
792        handle_connection(
793            stream,
794            context,
795            remote_addr,
796            self.auth_plugin.clone(),
797            &self.limits,
798            pending,
799        )
800        .await
801    }
802
803    /// Accept from `listener` until [`shutdown`](Self::shutdown), serving each
804    /// connection on its own task.
805    pub(crate) async fn serve_listener(self, listener: TcpListener) -> Result<()> {
806        loop {
807            let accepted = tokio::select! {
808                _ = self.shutdown.cancelled() => return Ok(()),
809                accepted = listener.accept() => accepted,
810            };
811            match accepted {
812                Ok((stream, addr)) => {
813                    let acceptor = self.clone();
814                    self.sessions.spawn(
815                        async move {
816                            if let Err(error) = acceptor.serve(stream, addr).await {
817                                error!("WebSocket connection error: {}", error);
818                            }
819                        }
820                        .instrument(info_span!("ws.connection", %addr)),
821                    );
822                }
823                Err(error) => error!("Failed to accept connection: {}", error),
824            }
825        }
826    }
827}
828
829/// One connection's place among the pending handshakes, released when dropped.
830pub(crate) struct PendingHandshake {
831    counter: Arc<AtomicUsize>,
832    count: usize,
833}
834
835impl PendingHandshake {
836    fn reserve(counter: &Arc<AtomicUsize>, max: usize) -> Option<Self> {
837        let previous = counter
838            .fetch_update(Ordering::AcqRel, Ordering::Acquire, |pending| {
839                (pending < max).then_some(pending + 1)
840            })
841            .ok()?;
842        Some(Self {
843            counter: counter.clone(),
844            count: previous + 1,
845        })
846    }
847
848    /// Pending handshakes, this one included, when it was reserved.
849    fn count(&self) -> usize {
850        self.count
851    }
852}
853
854impl Drop for PendingHandshake {
855    fn drop(&mut self) {
856        self.counter.fetch_sub(1, Ordering::AcqRel);
857    }
858}
859
860#[derive(Debug, Clone)]
861struct HandshakeReject {
862    status: StatusCode,
863    body: crate::websocket::auth::ErrorResponse,
864    error_code: String,
865    retry_after_secs: Option<u64>,
866}
867
868impl HandshakeReject {
869    fn from_deny(deny: &AuthDeny) -> Self {
870        let retry_after_secs = match deny.retry_policy {
871            crate::websocket::auth::RetryPolicy::RetryAfter(duration) => Some(duration.as_secs()),
872            _ => None,
873        };
874        Self {
875            status: StatusCode::from_u16(deny.http_status).unwrap_or(StatusCode::UNAUTHORIZED),
876            body: deny.to_error_response(),
877            error_code: deny.code.to_string(),
878            retry_after_secs,
879        }
880    }
881}
882
883fn build_handshake_error_response(
884    response: &Response,
885    reject: &HandshakeReject,
886) -> HandshakeErrorResponse {
887    let mut builder = Response::builder()
888        .status(reject.status)
889        .version(response.version())
890        .header(CONTENT_TYPE, "application/json; charset=utf-8")
891        .header("X-Error-Code", &reject.error_code)
892        .header("Cache-Control", "no-store");
893    if let Some(retry_after_secs) = reject.retry_after_secs {
894        builder = builder.header("Retry-After", retry_after_secs.to_string());
895    }
896    let body = serde_json::to_string(&reject.body).unwrap_or_else(|_| {
897        format!(
898            r#"{{"error":"{}","message":"{}","code":"{}","retryable":false}}"#,
899            reject.body.error, reject.body.message, reject.body.code
900        )
901    });
902    builder
903        .body(Some(body))
904        .expect("handshake rejection response should build")
905}
906
907#[allow(clippy::result_large_err)]
908async fn accept_authorized_connection(
909    stream: TcpStream,
910    remote_addr: SocketAddr,
911    auth_plugin: Arc<dyn WebSocketAuthPlugin>,
912    client_manager: ClientManager,
913    limits: &WebSocketLimitsConfig,
914    deadline: tokio::time::Instant,
915) -> Result<
916    Option<(
917        tokio_tungstenite::WebSocketStream<TcpStream>,
918        AuthContext,
919        Option<Arc<dyn WebSocketConnectionPermit>>,
920    )>,
921> {
922    use std::sync::Mutex;
923
924    type Admission =
925        Result<(AuthContext, Option<Arc<dyn WebSocketConnectionPermit>>), HandshakeReject>;
926    let capture: Arc<Mutex<Option<Admission>>> = Arc::new(Mutex::new(None));
927    let capture_ref = capture.clone();
928    let auth_plugin_ref = auth_plugin.clone();
929    let manager_ref = client_manager.clone();
930
931    // A message or frame past these limits closes the connection rather than being buffered.
932    let config = WebSocketConfig::default()
933        .max_message_size(Some(limits.max_message_bytes))
934        .max_frame_size(Some(limits.max_frame_bytes));
935    let callback = move |request: &Request, response| {
936        let request = ConnectionAuthRequest::from_http_request(remote_addr, request);
937        let result = tokio::task::block_in_place(|| {
938            // This callback blocks its caller, so the handshake deadline
939            // outside cannot interrupt it: bound authentication and admission
940            // by the same deadline here.
941            tokio::runtime::Handle::current().block_on(async {
942                let admission = async {
943                    match auth_plugin_ref.authorize(&request).await {
944                        AuthDecision::Allow(context) => manager_ref
945                            .check_connection_allowed(remote_addr, &Some(context.clone()))
946                            .await
947                            .and_then(|()| manager_ref.reserve_connection_admission(&context))
948                            .map(|permit| (context, permit))
949                            .map_err(|deny| HandshakeReject::from_deny(&deny)),
950                        AuthDecision::Deny(deny) => Err(HandshakeReject::from_deny(&deny)),
951                    }
952                };
953                tokio::time::timeout_at(deadline, admission)
954                    .await
955                    .unwrap_or_else(|_| {
956                        Err(HandshakeReject::from_deny(&AuthDeny::new(
957                            crate::websocket::auth::AuthErrorCode::InternalError,
958                            "Authentication did not complete in time",
959                        )))
960                    })
961            })
962        });
963        *capture_ref.lock().expect("capture lock poisoned") = Some(result.clone());
964        match result {
965            Ok(_) => Ok(response),
966            Err(reject) => Err(build_handshake_error_response(&response, &reject)),
967        }
968    };
969    let handshake_result = accept_hdr_async_with_config(stream, callback, Some(config)).await;
970
971    let auth_result = capture.lock().expect("capture lock poisoned").take();
972    match handshake_result {
973        Ok(stream) => match auth_result {
974            Some(Ok((context, permit))) => Ok(Some((stream, context, permit))),
975            Some(Err(reject)) => Err(anyhow::anyhow!(
976                "handshake unexpectedly succeeded after rejection: {}",
977                reject.body.message
978            )),
979            None => Err(anyhow::anyhow!("no auth result captured during handshake")),
980        },
981        Err(WsError::Http(_)) => Ok(None),
982        Err(error) => Err(error.into()),
983    }
984}
985
986async fn handle_connection(
987    stream: TcpStream,
988    mut context: SubscriptionContext,
989    remote_addr: SocketAddr,
990    auth_plugin: Arc<dyn WebSocketAuthPlugin>,
991    limits: &WebSocketLimitsConfig,
992    pending: PendingHandshake,
993) -> Result<()> {
994    // The handshake is raced against shutdown and a deadline: a peer that
995    // stalls it must not keep a socket and a task alive.
996    let deadline = tokio::time::Instant::now() + limits.handshake_timeout;
997    let accepted = tokio::select! {
998        _ = context.shutdown.cancelled() => return Ok(()),
999        accepted = tokio::time::timeout_at(
1000            deadline,
1001            accept_authorized_connection(
1002                stream,
1003                remote_addr,
1004                auth_plugin.clone(),
1005                context.client_manager.clone(),
1006                limits,
1007                deadline,
1008            ),
1009        ) => match accepted {
1010            Ok(accepted) => accepted?,
1011            Err(_) => {
1012                debug!("WebSocket handshake from {} timed out", remote_addr);
1013                return Ok(());
1014            }
1015        },
1016    };
1017    let Some((ws_stream, auth_context, admission_permit)) = accepted else {
1018        return Ok(());
1019    };
1020
1021    let client_id = Uuid::new_v4();
1022    context.client_id = client_id;
1023    let connection_start = Instant::now();
1024    let (mut usage_identity, mut deployment_id) = usage_identity_from_context(Some(&auth_context));
1025    let mut metering_key = usage_identity.metering_key.clone();
1026    // Active gauges must be decremented with the same attributes used for
1027    // their increment. The signed usage identity may legitimately move to a
1028    // claimed owner's account during an in-band refresh, but that must not
1029    // leave the original account's active count stuck or make the new one
1030    // negative when this connection closes.
1031    let connection_metrics_metering_key = metering_key.clone();
1032    context
1033        .metrics
1034        .connection_opened(connection_metrics_metering_key.as_deref());
1035
1036    let (ws_sender, mut ws_receiver) = ws_stream.split();
1037    context.client_manager.add_client_with_admission(
1038        client_id,
1039        ws_sender,
1040        Some(auth_context),
1041        remote_addr,
1042        admission_permit,
1043    );
1044    // Cancelled when the client leaves the registry. An eviction (staleness,
1045    // backpressure, expiry, a spent budget) ends the session as well, so a
1046    // peer that ignores the close frame cannot keep an uncounted socket open.
1047    let connection_token = context.client_manager.connection_token(client_id);
1048    // Registered: the connection now counts as a client, so its pending
1049    // place can go. Released earlier, a concurrent handshake could pass the
1050    // client limit in between.
1051    drop(pending);
1052    emit_usage_event(
1053        &context.usage_emitter,
1054        WebSocketUsageEvent::ConnectionEstablished {
1055            client_id: client_id.to_string(),
1056            remote_addr: remote_addr.to_string(),
1057            deployment_id: deployment_id.clone(),
1058            identity: usage_identity.clone(),
1059        },
1060    );
1061
1062    let mut active_subscriptions: HashMap<String, ActiveSubscription> = HashMap::new();
1063    loop {
1064        let message = tokio::select! {
1065            _ = context.shutdown.cancelled() => break,
1066            _ = connection_token.cancelled() => break,
1067            next = ws_receiver.next() => match next {
1068                Some(message) => message,
1069                None => break,
1070            },
1071        };
1072        let message = match message {
1073            Ok(message) => message,
1074            Err(error) => {
1075                warn!("WebSocket error for client {}: {}", client_id, error);
1076                break;
1077            }
1078        };
1079        if message.is_close() {
1080            break;
1081        }
1082        context.client_manager.update_client_last_seen(client_id);
1083        if !message.is_text() {
1084            continue;
1085        }
1086        if let Err(deny) = context
1087            .client_manager
1088            .check_inbound_message_allowed(client_id)
1089        {
1090            send_socket_issue(client_id, &context.client_manager, &deny, true, None).await;
1091            break;
1092        }
1093        context.metrics.message_received(metering_key.as_deref());
1094
1095        let text = match message.to_text() {
1096            Ok(text) => text,
1097            Err(_) => continue,
1098        };
1099        let client_message = match serde_json::from_str::<ClientMessage>(text) {
1100            Ok(message) => message,
1101            Err(parse_error) => {
1102                let subscription_id = extract_subscription_id(text);
1103                send_protocol_issue(
1104                    client_id,
1105                    &context.client_manager,
1106                    &context.metrics,
1107                    subscription_id,
1108                    "malformed-message",
1109                    format!("invalid protocol v2 message: {parse_error}"),
1110                )
1111                .await;
1112                continue;
1113            }
1114        };
1115
1116        match client_message {
1117            ClientMessage::Subscribe(subscription) => {
1118                let subscription_id = subscription.subscription_id.clone();
1119                if let Err(message) = subscription.validate() {
1120                    send_protocol_issue(
1121                        client_id,
1122                        &context.client_manager,
1123                        &context.metrics,
1124                        Some(subscription_id),
1125                        "invalid-subscription",
1126                        message,
1127                    )
1128                    .await;
1129                    continue;
1130                }
1131                if let Err(deny) = context
1132                    .client_manager
1133                    .check_subscription_allowed(client_id)
1134                    .await
1135                {
1136                    send_socket_issue(
1137                        client_id,
1138                        &context.client_manager,
1139                        &deny,
1140                        false,
1141                        Some(subscription_id),
1142                    )
1143                    .await;
1144                    continue;
1145                }
1146
1147                // A child of the connection token: an eviction ends this
1148                // subscription's delivery task even if this loop never runs
1149                // its cleanup for the client.
1150                let cancel_token = connection_token.child_token();
1151                let added = match context
1152                    .client_manager
1153                    .try_add_client_subscription(
1154                        client_id,
1155                        subscription_id.clone(),
1156                        cancel_token.clone(),
1157                    )
1158                    .await
1159                {
1160                    Ok(added) => added,
1161                    Err(deny) => {
1162                        send_socket_issue(
1163                            client_id,
1164                            &context.client_manager,
1165                            &deny,
1166                            false,
1167                            Some(subscription_id),
1168                        )
1169                        .await;
1170                        continue;
1171                    }
1172                };
1173                if !added {
1174                    send_protocol_issue(
1175                        client_id,
1176                        &context.client_manager,
1177                        &context.metrics,
1178                        Some(subscription_id),
1179                        "duplicate-subscription-id",
1180                        "subscriptionId is already active on this connection",
1181                    )
1182                    .await;
1183                    continue;
1184                }
1185
1186                let view = subscription.query.view.clone();
1187                let original = subscription.clone();
1188                if let Err(error) = attach_client_to_bus(&context, subscription, cancel_token).await
1189                {
1190                    context
1191                        .client_manager
1192                        .remove_client_subscription(client_id, &subscription_id)
1193                        .await;
1194                    // A refusal that already knows what to tell the client
1195                    // (an expired cursor, a changed epoch) keeps its own
1196                    // frame; anything else is a generic rejection.
1197                    match error.downcast::<RejectedSubscription>() {
1198                        Ok(rejected) => {
1199                            send_prepared_issue(
1200                                client_id,
1201                                &context.client_manager,
1202                                &context.metrics,
1203                                rejected.0,
1204                            )
1205                            .await;
1206                        }
1207                        Err(error) => {
1208                            send_protocol_issue(
1209                                client_id,
1210                                &context.client_manager,
1211                                &context.metrics,
1212                                Some(subscription_id),
1213                                "subscription-rejected",
1214                                error.to_string(),
1215                            )
1216                            .await;
1217                        }
1218                    }
1219                    continue;
1220                }
1221
1222                active_subscriptions.insert(
1223                    subscription_id,
1224                    ActiveSubscription {
1225                        view: view.clone(),
1226                        metrics_metering_key: metering_key.clone(),
1227                        subscription: original,
1228                    },
1229                );
1230                context
1231                    .metrics
1232                    .subscription_created(&view, metering_key.as_deref());
1233                emit_usage_event(
1234                    &context.usage_emitter,
1235                    WebSocketUsageEvent::SubscriptionCreated {
1236                        client_id: client_id.to_string(),
1237                        deployment_id: deployment_id.clone(),
1238                        identity: usage_identity.clone(),
1239                        view_id: view,
1240                    },
1241                );
1242            }
1243            ClientMessage::Unsubscribe(unsubscription) => {
1244                handle_unsubscribe(
1245                    &context,
1246                    unsubscription,
1247                    &mut active_subscriptions,
1248                    &deployment_id,
1249                    &usage_identity,
1250                )
1251                .await;
1252            }
1253            ClientMessage::Ping => debug!("Received ping from client {}", client_id),
1254            ClientMessage::RefreshAuth(request) => {
1255                let row_scopes_before =
1256                    session_row_scopes(context.client_manager.get_auth_context(client_id).as_ref());
1257                handle_refresh_auth(
1258                    client_id,
1259                    remote_addr,
1260                    &request,
1261                    &context.client_manager,
1262                    &auth_plugin,
1263                )
1264                .await;
1265                // A successful refresh replaces the verified context. Keep
1266                // every later usage event on the same current identity used
1267                // by snapshots and updates; on failure the manager still
1268                // returns the previous context.
1269                if let Some(current_auth) = context.client_manager.get_auth_context(client_id) {
1270                    (usage_identity, deployment_id) =
1271                        usage_identity_from_context(Some(&current_auth));
1272                    metering_key = usage_identity.metering_key.clone();
1273                    if session_row_scopes(Some(&current_auth)) != row_scopes_before {
1274                        rescope_subscriptions(&context, &active_subscriptions).await;
1275                    }
1276                }
1277            }
1278        }
1279    }
1280
1281    context
1282        .client_manager
1283        .cancel_all_client_subscriptions(client_id)
1284        .await;
1285    context.client_manager.remove_client(client_id);
1286    if let Some(rate_limiter) = context.client_manager.rate_limiter().cloned() {
1287        rate_limiter.remove_client_buckets(client_id).await;
1288    }
1289    for subscription in active_subscriptions.values() {
1290        context.metrics.subscription_removed(
1291            &subscription.view,
1292            subscription.metrics_metering_key.as_deref(),
1293        );
1294        emit_usage_event(
1295            &context.usage_emitter,
1296            WebSocketUsageEvent::SubscriptionRemoved {
1297                client_id: client_id.to_string(),
1298                deployment_id: deployment_id.clone(),
1299                identity: usage_identity.clone(),
1300                view_id: subscription.view.clone(),
1301            },
1302        );
1303    }
1304    let duration = connection_start.elapsed().as_secs_f64();
1305    context
1306        .metrics
1307        .connection_closed(duration, connection_metrics_metering_key.as_deref());
1308    emit_usage_event(
1309        &context.usage_emitter,
1310        WebSocketUsageEvent::ConnectionClosed {
1311            client_id: client_id.to_string(),
1312            deployment_id,
1313            identity: usage_identity,
1314            duration_secs: Some(duration),
1315            subscription_count: u32::try_from(active_subscriptions.len()).unwrap_or(u32::MAX),
1316        },
1317    );
1318    Ok(())
1319}
1320
1321struct ActiveSubscription {
1322    view: String,
1323    /// Attribute set used for the active-gauge increment. It is immutable
1324    /// even if a later token refresh moves usage to another billing account.
1325    metrics_metering_key: Option<String>,
1326    /// The subscription as requested, to re-attach it when the session's row
1327    /// scopes change.
1328    subscription: Subscription,
1329}
1330
1331/// Re-deliver every row-scoped subscription after a token refresh changed the
1332/// session's row scopes.
1333///
1334/// The generation bump comes first, under the write lock, so no task still
1335/// running on the old scopes can send once it is done: each send holds the
1336/// read lock and checks its generation. Each subscription then re-attaches
1337/// with the new scopes: its snapshot is authoritative, so the client drops
1338/// rows it may no longer see and receives the ones it now may. A replayable
1339/// tape cannot be resnapshotted without resending records the consumer
1340/// already has, so it stops with a retryable `subscription-rescoped` issue
1341/// and the consumer resubscribes from its cursor.
1342async fn rescope_subscriptions(
1343    context: &SubscriptionContext,
1344    active_subscriptions: &HashMap<String, ActiveSubscription>,
1345) {
1346    *context.row_scope_lock.write().await += 1;
1347    for (subscription_id, active) in active_subscriptions {
1348        if context
1349            .view_index
1350            .scope_field_for_view(&active.view)
1351            .is_none()
1352        {
1353            continue;
1354        }
1355        let cancel_token = context
1356            .client_manager
1357            .connection_token(context.client_id)
1358            .child_token();
1359        if !context
1360            .client_manager
1361            .replace_client_subscription_token(
1362                context.client_id,
1363                subscription_id,
1364                cancel_token.clone(),
1365            )
1366            .await
1367        {
1368            continue;
1369        }
1370        let replayable = context.journal.as_ref().is_some_and(|journal| {
1371            journal.is_enabled()
1372                && context
1373                    .view_index
1374                    .get_view(&active.view)
1375                    .is_some_and(|spec| spec.mode == Mode::Append)
1376        });
1377        if replayable {
1378            let _ = send_control_frame(
1379                context,
1380                &SocketIssueMessage::subscription_rescoped(subscription_id.clone()),
1381                &active.view,
1382            );
1383            continue;
1384        }
1385        let mut subscription = active.subscription.clone();
1386        subscription.snapshot.enabled = true;
1387        let mut resnapshot = context.clone();
1388        resnapshot.resnapshot = true;
1389        match attach_client_to_bus(&resnapshot, subscription, cancel_token).await {
1390            Ok(()) => context.metrics.subscription_resnapshot(&active.view),
1391            Err(error) => {
1392                warn!(
1393                    "Subscription {} could not be resnapshotted for new row scopes: {error:#}",
1394                    subscription_id
1395                );
1396                let _ = send_control_frame(
1397                    context,
1398                    &SocketIssueMessage::subscription_rescoped(subscription_id.clone()),
1399                    &active.view,
1400                );
1401            }
1402        }
1403    }
1404}
1405
1406async fn handle_unsubscribe(
1407    context: &SubscriptionContext,
1408    unsubscription: Unsubscription,
1409    active_subscriptions: &mut HashMap<String, ActiveSubscription>,
1410    deployment_id: &Option<String>,
1411    usage_identity: &UsageIdentity,
1412) {
1413    let subscription_id = unsubscription.subscription_id.clone();
1414    if let Err(message) = unsubscription.validate() {
1415        send_protocol_issue(
1416            context.client_id,
1417            &context.client_manager,
1418            &context.metrics,
1419            Some(subscription_id),
1420            "invalid-unsubscription",
1421            message,
1422        )
1423        .await;
1424        return;
1425    }
1426
1427    if !context
1428        .client_manager
1429        .remove_client_subscription(context.client_id, &subscription_id)
1430        .await
1431    {
1432        send_protocol_issue(
1433            context.client_id,
1434            &context.client_manager,
1435            &context.metrics,
1436            Some(subscription_id),
1437            "unknown-subscription-id",
1438            "subscriptionId is not active on this connection",
1439        )
1440        .await;
1441        return;
1442    }
1443
1444    let Some(subscription) = active_subscriptions.remove(&subscription_id) else {
1445        return;
1446    };
1447    let _ = send_control_frame(
1448        context,
1449        &UnsubscribedFrame::new(subscription_id),
1450        &subscription.view,
1451    );
1452    context.metrics.subscription_removed(
1453        &subscription.view,
1454        subscription.metrics_metering_key.as_deref(),
1455    );
1456    emit_usage_event(
1457        &context.usage_emitter,
1458        WebSocketUsageEvent::SubscriptionRemoved {
1459            client_id: context.client_id.to_string(),
1460            deployment_id: deployment_id.clone(),
1461            identity: usage_identity.clone(),
1462            view_id: subscription.view,
1463        },
1464    );
1465}
1466
1467fn extract_subscription_id(text: &str) -> Option<String> {
1468    serde_json::from_str::<Value>(text)
1469        .ok()?
1470        .get("subscriptionId")?
1471        .as_str()
1472        .map(str::to_string)
1473}
1474
1475struct SnapshotMetadata<'a> {
1476    subscription_id: &'a str,
1477    snapshot_id: &'a str,
1478    authoritative: bool,
1479    mode: Mode,
1480    view_id: &'a str,
1481    key: Option<&'a str>,
1482}
1483
1484#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1485enum SnapshotPurpose {
1486    Initial,
1487    Recovery,
1488}
1489
1490impl SnapshotPurpose {
1491    /// The purpose of a subscription's first snapshot: a resnapshot after
1492    /// its row scopes changed replaces what the client holds, like recovery.
1493    fn first(context: &SubscriptionContext) -> Self {
1494        if context.resnapshot {
1495            Self::Recovery
1496        } else {
1497            Self::Initial
1498        }
1499    }
1500
1501    fn authoritative(self, subscription: &Subscription) -> bool {
1502        match self {
1503            Self::Initial => subscription.query.after.is_none(),
1504            // Recovery replaces the exact query membership even when `after`
1505            // made the initial snapshot incremental. A merge-only snapshot
1506            // cannot remove members whose delete frames were skipped.
1507            Self::Recovery => true,
1508        }
1509    }
1510}
1511
1512/// One snapshot frame, serialized straight from the cached entities it
1513/// carries: the same JSON as a [`SnapshotFrame`] of
1514/// [`SnapshotEntity`](crate::websocket::frame::SnapshotEntity) rows with the
1515/// view's wire format applied, without building those rows.
1516///
1517/// [`SnapshotFrame`]: crate::websocket::frame::SnapshotFrame
1518#[derive(Serialize)]
1519#[serde(rename_all = "camelCase")]
1520struct SnapshotBatch<'a> {
1521    protocol_version: u8,
1522    subscription_id: &'a str,
1523    snapshot_id: &'a str,
1524    authoritative: bool,
1525    mode: Mode,
1526    #[serde(rename = "entity")]
1527    export: &'a str,
1528    op: &'static str,
1529    #[serde(skip_serializing_if = "Option::is_none")]
1530    key: Option<&'a str>,
1531    data: SnapshotRows<'a>,
1532    complete: bool,
1533}
1534
1535struct SnapshotRows<'a> {
1536    rows: &'a [(String, SharedEntity)],
1537    wire_format: &'a WireFormat,
1538}
1539
1540impl Serialize for SnapshotRows<'_> {
1541    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1542        use serde::ser::SerializeSeq as _;
1543        let mut rows = serializer.serialize_seq(Some(self.rows.len()))?;
1544        for (key, entity) in self.rows {
1545            rows.serialize_element(&SnapshotRow {
1546                key,
1547                data: WireEntity {
1548                    entity,
1549                    wire_format: self.wire_format,
1550                },
1551            })?;
1552        }
1553        rows.end()
1554    }
1555}
1556
1557#[derive(Serialize)]
1558struct SnapshotRow<'a> {
1559    key: &'a str,
1560    data: WireEntity<'a>,
1561}
1562
1563/// Split `rows` into the snapshot's frames: the first batch for a fast first
1564/// render, then larger ones. An empty snapshot is one empty, complete frame.
1565fn create_snapshot_batches<'a>(
1566    rows: &'a [(String, SharedEntity)],
1567    wire_format: &'a WireFormat,
1568    metadata: SnapshotMetadata<'a>,
1569    batch_config: &SnapshotBatchConfig,
1570) -> Vec<SnapshotBatch<'a>> {
1571    let batch = |data: &'a [(String, SharedEntity)], complete: bool| SnapshotBatch {
1572        protocol_version: PROTOCOL_VERSION,
1573        subscription_id: metadata.subscription_id,
1574        snapshot_id: metadata.snapshot_id,
1575        authoritative: metadata.authoritative,
1576        mode: metadata.mode,
1577        export: metadata.view_id,
1578        op: "snapshot",
1579        key: metadata.key,
1580        data: SnapshotRows {
1581            rows: data,
1582            wire_format,
1583        },
1584        complete,
1585    };
1586    if rows.is_empty() {
1587        return vec![batch(&[], true)];
1588    }
1589
1590    let mut batches = Vec::new();
1591    let mut offset = 0;
1592    while offset < rows.len() {
1593        let configured_size = if offset == 0 {
1594            batch_config.initial_batch_size
1595        } else {
1596            batch_config.subsequent_batch_size
1597        };
1598        let end = (offset + configured_size.max(1)).min(rows.len());
1599        batches.push(batch(&rows[offset..end], end == rows.len()));
1600        offset = end;
1601    }
1602    batches
1603}
1604
1605/// Send `rows` as one snapshot, serializing each frame from the cached
1606/// entities as it goes: no row is copied.
1607async fn send_snapshot_batches(
1608    context: &SubscriptionContext,
1609    subscription: &Subscription,
1610    rows: &[(String, SharedEntity)],
1611    view_spec: &ViewSpec,
1612    purpose: SnapshotPurpose,
1613    batch_config: &SnapshotBatchConfig,
1614) -> Result<()> {
1615    let snapshot_id = Uuid::new_v4().to_string();
1616    let authoritative = purpose.authoritative(subscription);
1617    let mode = view_spec.mode;
1618    let frames = create_snapshot_batches(
1619        rows,
1620        &view_spec.wire_format,
1621        SnapshotMetadata {
1622            subscription_id: &subscription.subscription_id,
1623            snapshot_id: &snapshot_id,
1624            authoritative,
1625            mode,
1626            view_id: &subscription.query.view,
1627            key: subscription.query.key.as_deref(),
1628        },
1629        batch_config,
1630    );
1631
1632    for frame in frames {
1633        let rows = frame.data.rows.len() as u32;
1634        let json = serde_json::to_vec(&frame)?;
1635        let payload = maybe_compress(&json);
1636        let bytes = payload.as_bytes().len() as u64;
1637        let _held = hold_row_scopes_async(context).await?;
1638        context
1639            .client_manager
1640            .send_compressed_async(context.client_id, payload)
1641            .await
1642            .map_err(|error| anyhow::anyhow!("failed to send snapshot: {error}"))?;
1643        context.metrics.message_sent();
1644
1645        let auth_context = context.client_manager.get_auth_context(context.client_id);
1646        let (identity, deployment_id) = usage_identity_from_context(auth_context.as_ref());
1647        emit_usage_event(
1648            &context.usage_emitter,
1649            WebSocketUsageEvent::SnapshotSent {
1650                client_id: context.client_id.to_string(),
1651                deployment_id,
1652                identity,
1653                view_id: subscription.query.view.clone(),
1654                rows,
1655                messages: 1,
1656                bytes,
1657            },
1658        );
1659    }
1660    Ok(())
1661}
1662
1663fn extract_sort_config(view_spec: &ViewSpec) -> Option<SortConfig> {
1664    if let Some(sort) = view_spec
1665        .pipeline
1666        .as_ref()
1667        .and_then(|pipeline| pipeline.sort.as_ref())
1668    {
1669        return Some(SortConfig {
1670            field: sort.field_path.clone(),
1671            order: match sort.order {
1672                crate::materialized_view::SortOrder::Asc => SortOrder::Asc,
1673                crate::materialized_view::SortOrder::Desc => SortOrder::Desc,
1674            },
1675        });
1676    }
1677    (view_spec.mode == Mode::List).then(|| SortConfig {
1678        field: vec!["_seq".to_string()],
1679        order: SortOrder::Desc,
1680    })
1681}
1682
1683/// Hold a row-scoped subscription's right to send on the row scopes it
1684/// attached with, for one send: an error once a refresh changed them (see
1685/// [`RowFilter::try_hold`]). Nothing to hold for other subscriptions.
1686fn hold_row_scopes(
1687    context: &SubscriptionContext,
1688) -> Result<Option<tokio::sync::RwLockReadGuard<'_, u64>>> {
1689    match context.row_filter.as_ref() {
1690        None => Ok(None),
1691        Some(filter) => filter
1692            .try_hold()
1693            .map(Some)
1694            .ok_or_else(|| anyhow::anyhow!("the session's row scopes changed")),
1695    }
1696}
1697
1698/// [`hold_row_scopes`] for a send that awaits.
1699async fn hold_row_scopes_async(
1700    context: &SubscriptionContext,
1701) -> Result<Option<tokio::sync::RwLockReadGuard<'_, u64>>> {
1702    match context.row_filter.as_ref() {
1703        None => Ok(None),
1704        Some(filter) => filter
1705            .hold()
1706            .await
1707            .map(Some)
1708            .ok_or_else(|| anyhow::anyhow!("the session's row scopes changed")),
1709    }
1710}
1711
1712fn send_control_frame<T: Serialize>(
1713    context: &SubscriptionContext,
1714    frame: &T,
1715    view_id: &str,
1716) -> Result<()> {
1717    let json = serde_json::to_vec(frame)?;
1718    let bytes = json.len();
1719    let _held = hold_row_scopes(context)?;
1720    context
1721        .client_manager
1722        .send_to_client(context.client_id, Arc::new(Bytes::from(json)))
1723        .map_err(|error| anyhow::anyhow!("failed to send control frame: {error}"))?;
1724    context.metrics.message_sent();
1725    emit_update_sent_for_client(
1726        &context.usage_emitter,
1727        &context.client_manager,
1728        context.client_id,
1729        view_id,
1730        bytes,
1731    );
1732    Ok(())
1733}
1734
1735fn send_subscribed_frame(
1736    context: &SubscriptionContext,
1737    subscription: &Subscription,
1738    view_spec: &ViewSpec,
1739) -> Result<()> {
1740    let frame = SubscribedFrame::new(
1741        subscription.subscription_id.clone(),
1742        subscription.query.clone(),
1743        view_spec.mode,
1744        extract_sort_config(view_spec),
1745    );
1746    send_control_frame(context, &frame, &subscription.query.view)
1747}
1748
1749fn enforce_snapshot_limit(context: &SubscriptionContext, rows: usize) -> Result<()> {
1750    context
1751        .client_manager
1752        .check_snapshot_allowed(context.client_id, u32::try_from(rows).unwrap_or(u32::MAX))
1753        .map_err(|deny| anyhow::anyhow!(deny.reason))
1754}
1755
1756async fn subscribe_state_then_snapshot<F, Fut, T>(
1757    bus_manager: &BusManager,
1758    view_id: &str,
1759    key: &str,
1760    snapshot: F,
1761) -> (watch::Receiver<StateUpdate>, u64, T)
1762where
1763    F: FnOnce() -> Fut,
1764    Fut: Future<Output = T>,
1765{
1766    let mut receiver = bus_manager.get_or_create_state_bus(view_id, key).await;
1767    let published = receiver.borrow_and_update().published;
1768    let snapshot = snapshot().await;
1769    (receiver, published, snapshot)
1770}
1771
1772async fn subscribe_list_then_snapshot<F, Fut, T>(
1773    bus_manager: &BusManager,
1774    view_id: &str,
1775    snapshot: F,
1776) -> (broadcast::Receiver<Arc<BusMessage>>, T)
1777where
1778    F: FnOnce() -> Fut,
1779    Fut: Future<Output = T>,
1780{
1781    let receiver = bus_manager.get_or_create_list_bus(view_id).await;
1782    let snapshot = snapshot().await;
1783    (receiver, snapshot)
1784}
1785
1786async fn attach_client_to_bus(
1787    context: &SubscriptionContext,
1788    mut subscription: Subscription,
1789    cancel_token: CancellationToken,
1790) -> Result<()> {
1791    let view_spec = context
1792        .view_index
1793        .get_view(&subscription.query.view)
1794        .cloned()
1795        .ok_or_else(|| anyhow::anyhow!("unknown view: {}", subscription.query.view))?;
1796
1797    // A row-scoped view delivers only the rows this session's verified token
1798    // covers. The scopes are read here, once, from the token the connection
1799    // holds now; a refresh that changes them re-attaches the subscription.
1800    let mut scoped_context;
1801    let context = match context.view_index.scope_field_for_view(&view_spec.id) {
1802        Some(field) => {
1803            let generation = *context.row_scope_lock.read().await;
1804            scoped_context = context.clone();
1805            scoped_context.row_filter = Some(RowFilter::new(
1806                field.clone(),
1807                session_row_scopes(
1808                    context
1809                        .client_manager
1810                        .get_auth_context(context.client_id)
1811                        .as_ref(),
1812                ),
1813                context.row_scope_lock.clone(),
1814                generation,
1815            ));
1816            &scoped_context
1817        }
1818        None => context,
1819    };
1820
1821    if view_spec.mode == Mode::State && !view_spec.is_derived() && subscription.query.key.is_none()
1822    {
1823        return Err(anyhow::anyhow!("state subscriptions require query.key"));
1824    }
1825    if view_spec.is_derived() && subscription.query.take.is_none() {
1826        subscription.query.take = view_spec
1827            .pipeline
1828            .as_ref()
1829            .and_then(|pipeline| pipeline.limit);
1830    }
1831
1832    // A retained tape takes precedence for append views: it is the only
1833    // delivery that can honour a cursor. Without one, fall through to the
1834    // previous latest-state behaviour.
1835    let journal = context
1836        .journal
1837        .clone()
1838        .filter(|journal| journal.is_enabled() && view_spec.mode == Mode::Append);
1839    if let Some(journal) = journal {
1840        return attach_journal_subscription(
1841            context,
1842            subscription,
1843            view_spec,
1844            journal,
1845            cancel_token,
1846        )
1847        .await;
1848    }
1849
1850    if view_spec.mode == Mode::State && !view_spec.is_derived() {
1851        attach_state_subscription(context, subscription, view_spec, cancel_token).await
1852    } else {
1853        attach_collection_subscription(context, subscription, view_spec, cancel_token).await
1854    }
1855}
1856
1857async fn attach_state_subscription(
1858    context: &SubscriptionContext,
1859    subscription: Subscription,
1860    view_spec: ViewSpec,
1861    cancel_token: CancellationToken,
1862) -> Result<()> {
1863    let view_id = subscription.query.view.clone();
1864    let key = subscription.query.key.clone().unwrap_or_default();
1865    let query = subscription.query.clone();
1866    let cache = context.entity_cache.clone();
1867    let view_spec_for_snapshot = view_spec.clone();
1868    let row_filter = context.row_filter.clone();
1869    let (mut receiver, mut seen, initial) =
1870        subscribe_state_then_snapshot(&context.bus_manager, &view_id, &key, move || async move {
1871            load_query_entities(
1872                &cache,
1873                None,
1874                &view_spec_for_snapshot,
1875                &query,
1876                row_filter.as_ref(),
1877                false,
1878            )
1879            .await
1880        })
1881        .await;
1882
1883    let mut snapshot_entities = initial;
1884    if let Some(limit) = subscription.query.snapshot_limit {
1885        snapshot_entities.truncate(limit);
1886    }
1887    enforce_snapshot_limit(context, snapshot_entities.len())?;
1888    if !context.resnapshot {
1889        send_subscribed_frame(context, &subscription, &view_spec)?;
1890    }
1891    // The client holds the entity only if the snapshot actually carried it.
1892    // A disabled snapshot, or one `snapshotLimit` cut to nothing, leaves it
1893    // without a copy, so its first change must be a full `upsert`: a patch
1894    // would have nothing to merge into.
1895    let delivered = subscription.snapshot.enabled && !snapshot_entities.is_empty();
1896    // The entity as the client last received it whole, which a catch-up is
1897    // measured against.
1898    let synced = if delivered {
1899        snapshot_entities
1900            .first()
1901            .map(|(_, entity)| Synced::Entity(entity.clone()))
1902    } else {
1903        None
1904    };
1905    if subscription.snapshot.enabled {
1906        send_snapshot_batches(
1907            context,
1908            &subscription,
1909            &snapshot_entities,
1910            &view_spec,
1911            SnapshotPurpose::first(context),
1912            &context.entity_cache.snapshot_config(),
1913        )
1914        .await?;
1915    }
1916
1917    let task_context = context.clone();
1918    let subscription_id = subscription.subscription_id.clone();
1919    let query = subscription.query.clone();
1920    let view_spec_task = view_spec.clone();
1921    let span_view = view_id.clone();
1922    let span_key = key.clone();
1923    tokio::spawn(
1924        async move {
1925            // Whether the client holds this key, not whether it exists.
1926            let mut member = delivered;
1927            // The entity as the client last received it whole (its snapshot
1928            // row, an upsert or the last catch-up; see `Synced`). A catch-up
1929            // sends what changed since, and every field a patch forwarded in
1930            // between set (`touched`, see `mark_fields`): the entity may have
1931            // set it back, and a client may have dropped the patch.
1932            let mut synced = synced;
1933            let mut touched = Value::Null;
1934            // Whether the client lacks fields of frames the bus overwrote
1935            // before this task read them. The cached entity is sent in their
1936            // place, but while the cache lacks the key there is none, so the
1937            // debt waits for the next frame that finds it cached.
1938            let mut behind = false;
1939            // Whether, while behind, a frame the client missed replaced the
1940            // entity (a delete, or a whole upsert): its copy may then hold
1941            // fields the entity no longer has, which merging keeps, so the
1942            // catch-up must replace it.
1943            let mut replaced = false;
1944            loop {
1945                tokio::select! {
1946                    _ = cancel_token.cancelled() => break,
1947                    changed = receiver.changed() => {
1948                        if changed.is_err() {
1949                            break;
1950                        }
1951                        let (payload, published, replaced_at, row_scope) = {
1952                            let update = receiver.borrow_and_update();
1953                            (
1954                                update.payload.clone(),
1955                                update.published,
1956                                update.replaced,
1957                                update.row_scope.clone(),
1958                            )
1959                        };
1960                        // Authorise the frame against the scope it was
1961                        // published under. The cache may already hold a later
1962                        // state of the row in this session's scopes; a frame
1963                        // from before that is still not this session's to see.
1964                        let in_scope = frame_in_row_scopes(
1965                            task_context.row_filter.as_ref(),
1966                            row_scope.as_ref(),
1967                            &payload,
1968                        );
1969                        // The bus keeps only the latest frame. If more than one
1970                        // was published since the last read, the earlier ones
1971                        // were overwritten and the latest patch alone would
1972                        // drop their fields, so send the cached entity instead.
1973                        behind |= published > seen + 1;
1974                        replaced |= behind && replaced_at > seen;
1975                        seen = published;
1976                        let metadata = source_frame_metadata(&payload);
1977                        if metadata.op == "delete" {
1978                            if !task_context.entity_cache
1979                                .deletion_is_current(&query.view, &key)
1980                                .await
1981                            {
1982                                behind = true;
1983                                replaced = true;
1984                                continue;
1985                            }
1986                            if member && send_membership_frame(
1987                                &task_context,
1988                                &subscription_id,
1989                                &view_spec_task,
1990                                "delete",
1991                                &key,
1992                                Value::Null,
1993                                metadata.seq,
1994                            ).is_err() {
1995                                break;
1996                            }
1997                            member = false;
1998                            behind = false;
1999                            replaced = false;
2000                            synced = None;
2001                            touched = Value::Null;
2002                            continue;
2003                        }
2004
2005                        // A key missing from the cache was evicted there or
2006                        // never held whole, not deleted (deletes arrive
2007                        // above): the cache refused this patch and the whole
2008                        // entity is on its way (see `EntityResync`). The
2009                        // client's copy is still good, so a holder gets the
2010                        // patch and stays a holder; `remove` is only for an
2011                        // entity that stopped matching. If it is behind, it
2012                        // stays behind: the resend that brings the entity
2013                        // back carries the seq of the latest change, which it
2014                        // already has, so only the catch-up below reaches it.
2015                        let Some(cached) = task_context
2016                            .entity_cache
2017                            .get_shared(&view_spec_task.id, &key)
2018                            .await
2019                        else {
2020                            // A patch that moves the row out of this
2021                            // session's row scopes ends its membership.
2022                            if member
2023                                && task_context.row_filter.as_ref().is_some_and(|filter| {
2024                                    source_frame_data(&payload)
2025                                        .is_some_and(|data| filter.denies_frame_data(&data))
2026                                })
2027                            {
2028                                member = false;
2029                                synced = None;
2030                                touched = Value::Null;
2031                                if send_membership_frame(
2032                                    &task_context,
2033                                    &subscription_id,
2034                                    &view_spec_task,
2035                                    "remove",
2036                                    &key,
2037                                    Value::Null,
2038                                    metadata.seq,
2039                                ).is_err() {
2040                                    break;
2041                                }
2042                                continue;
2043                            }
2044                            // A frame outside this session's scopes is
2045                            // withheld; the client is then behind, and the
2046                            // next frame that finds the entity cached sends
2047                            // what it lacks from the cache.
2048                            if member && !in_scope {
2049                                behind = true;
2050                                continue;
2051                            }
2052                            // A copy of a replaced entity takes no patch: it
2053                            // waits for the whole entity to replace it.
2054                            if member && !replaced {
2055                                record_forwarded(&payload, &metadata.op, &mut synced, &mut touched);
2056                                if send_scoped_source_payload(
2057                                    &task_context,
2058                                    &subscription_id,
2059                                    &query.view,
2060                                    payload,
2061                                ).is_err() {
2062                                    break;
2063                                }
2064                            }
2065                            continue;
2066                        };
2067                        let selected = select_query_entities(
2068                            vec![(key.clone(), cached)],
2069                            &query,
2070                            task_context.row_filter.as_ref(),
2071                            true,
2072                            false,
2073                        );
2074                        let is_member = !selected.is_empty();
2075                        // A withheld frame is caught up from the cache, which
2076                        // `selected` has just shown to be in scope.
2077                        let caught_up = !std::mem::take(&mut behind) && in_scope;
2078                        let replace = std::mem::take(&mut replaced);
2079                        let result = match (member, selected.into_iter().next()) {
2080                            (true, Some(_)) if caught_up => {
2081                                record_forwarded(&payload, &metadata.op, &mut synced, &mut touched);
2082                                send_scoped_source_payload(
2083                                    &task_context,
2084                                    &subscription_id,
2085                                    &query.view,
2086                                    payload,
2087                                )
2088                            }
2089                            // The catch-up: a patch of the cached entity's
2090                            // fields that changed since the client last received
2091                            // it whole or that a forwarded patch set, which
2092                            // merges into what the client holds.
2093                            // If a missed frame replaced the entity, it is the
2094                            // whole entity as an upsert instead, which replaces
2095                            // the client's copy: merging would keep the fields of
2096                            // the entity that was deleted or replaced. Either
2097                            // carries no seq, because it is newer than anything
2098                            // this subscriber was sent and seqs are not ordered
2099                            // within a slot: account updates and instructions
2100                            // number themselves differently, so the latest
2101                            // patch's seq can sort below one already delivered.
2102                            (true, Some((entity_key, data))) => {
2103                                let wire_format = &view_spec_task.wire_format;
2104                                let base = synced
2105                                    .as_ref()
2106                                    .filter(|_| !replace)
2107                                    .and_then(|synced| synced.wire(wire_format));
2108                                let result = match base {
2109                                    Some(base) => match changed_fields(
2110                                        &base,
2111                                        &wire_value(&data, wire_format),
2112                                        &touched,
2113                                    ) {
2114                                        Some(body) => send_membership_frame(
2115                                            &task_context,
2116                                            &subscription_id,
2117                                            &view_spec_task,
2118                                            "patch",
2119                                            &entity_key,
2120                                            body,
2121                                            None,
2122                                        ),
2123                                        None => Ok(()),
2124                                    },
2125                                    // Nothing to measure against: the whole
2126                                    // entity, straight from the cache.
2127                                    None => send_entity_frame(
2128                                        &task_context,
2129                                        &subscription_id,
2130                                        &view_spec_task,
2131                                        if replace { "upsert" } else { "patch" },
2132                                        &entity_key,
2133                                        &data,
2134                                        None,
2135                                    ),
2136                                };
2137                                synced = Some(Synced::Entity(data));
2138                                touched = Value::Null;
2139                                result
2140                            }
2141                            (false, Some((entity_key, data))) => {
2142                                let result = send_entity_frame(
2143                                    &task_context,
2144                                    &subscription_id,
2145                                    &view_spec_task,
2146                                    "upsert",
2147                                    &entity_key,
2148                                    &data,
2149                                    metadata.seq.as_deref(),
2150                                );
2151                                synced = Some(Synced::Entity(data));
2152                                touched = Value::Null;
2153                                result
2154                            }
2155                            (true, None) => {
2156                                synced = None;
2157                                touched = Value::Null;
2158                                send_membership_frame(
2159                                &task_context,
2160                                &subscription_id,
2161                                &view_spec_task,
2162                                "remove",
2163                                &key,
2164                                Value::Null,
2165                                metadata.seq,
2166                                )
2167                            }
2168                            (false, None) => Ok(()),
2169                        };
2170                        if result.is_err() {
2171                            break;
2172                        }
2173                        member = is_member;
2174                    }
2175                }
2176            }
2177        }
2178        .instrument(info_span!("ws.subscribe.state", client_id = %context.client_id, view = %span_view, key = %span_key)),
2179    );
2180    Ok(())
2181}
2182
2183async fn attach_collection_subscription(
2184    context: &SubscriptionContext,
2185    subscription: Subscription,
2186    view_spec: ViewSpec,
2187    cancel_token: CancellationToken,
2188) -> Result<()> {
2189    let view_id = subscription.query.view.clone();
2190    let source_view_id = view_spec
2191        .source_view
2192        .clone()
2193        .unwrap_or_else(|| view_id.clone());
2194    let (mut receiver, initial_membership) = subscribe_collection_then_snapshot(
2195        context,
2196        &source_view_id,
2197        &view_spec,
2198        &subscription.query,
2199    )
2200    .await;
2201
2202    let mut snapshot_entities = initial_membership.clone();
2203    if let Some(limit) = subscription.query.snapshot_limit {
2204        snapshot_entities.truncate(limit);
2205    }
2206    enforce_snapshot_limit(context, snapshot_entities.len())?;
2207    if !context.resnapshot {
2208        send_subscribed_frame(context, &subscription, &view_spec)?;
2209    }
2210    // Window members the client was never sent: everything past a
2211    // `snapshotLimit` cut, or the whole window when the snapshot is disabled.
2212    // They stay in the window (take/skip positions still count them) but the
2213    // client holds no copy, so none of them may ever be sent a patch.
2214    let delivered_rows = if subscription.snapshot.enabled {
2215        snapshot_entities.len()
2216    } else {
2217        0
2218    };
2219    let mut undelivered: HashSet<String> = initial_membership
2220        .iter()
2221        .skip(delivered_rows)
2222        .map(|(key, _)| key.clone())
2223        .collect();
2224    let mut sent_whole = SentWhole::default();
2225    if subscription.snapshot.enabled {
2226        send_snapshot_batches(
2227            context,
2228            &subscription,
2229            &snapshot_entities,
2230            &view_spec,
2231            SnapshotPurpose::first(context),
2232            &context.entity_cache.snapshot_config(),
2233        )
2234        .await?;
2235    }
2236
2237    let task_context = context.clone();
2238    let task_subscription = subscription.clone();
2239    let subscription_id = subscription.subscription_id.clone();
2240    let query = subscription.query.clone();
2241    let view_spec_task = view_spec.clone();
2242    let span_view = view_id.clone();
2243    tokio::spawn(
2244        async move {
2245            let mut current = initial_membership;
2246            // Append views are event tapes: collapsing two records would lose
2247            // observable history. Coalescing is only valid for latest-state
2248            // list membership, where a full final entity preserves meaning.
2249            let coalesce_ms = (view_spec_task.mode == Mode::List)
2250                .then(|| {
2251                    view_spec_task
2252                        .delivery
2253                        .coalesce_ms
2254                        .or(task_context.delivery.collection_coalesce_ms)
2255                        .filter(|milliseconds| *milliseconds > 0)
2256                })
2257                .flatten();
2258            let mut flush_interval = coalesce_ms.map(|milliseconds| {
2259                let period = Duration::from_millis(milliseconds);
2260                let mut interval = tokio::time::interval_at(
2261                    tokio::time::Instant::now() + period,
2262                    period,
2263                );
2264                interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
2265                interval
2266            });
2267            let mut pending = HashMap::<String, Arc<BusMessage>>::new();
2268            let mut pending_updates = 0_u64;
2269
2270            loop {
2271                tokio::select! {
2272                    _ = cancel_token.cancelled() => break,
2273                    _ = async {
2274                        flush_interval
2275                            .as_mut()
2276                            .expect("coalescing interval is guarded")
2277                            .tick()
2278                            .await;
2279                    }, if flush_interval.is_some() => {
2280                        if pending.is_empty() {
2281                            continue;
2282                        }
2283                        let sorted_caches = view_spec_task
2284                            .is_derived()
2285                            .then(|| task_context.view_index.sorted_caches());
2286                        let next = load_query_entities(
2287                            &task_context.entity_cache,
2288                            sorted_caches,
2289                            &view_spec_task,
2290                            &query,
2291                            task_context.row_filter.as_ref(),
2292                            false,
2293                        ).await;
2294                        if emit_coalesced_collection_delta(
2295                            &task_context,
2296                            &subscription_id,
2297                            &view_spec_task,
2298                            &current,
2299                            &next,
2300                            &pending,
2301                            &mut undelivered,
2302                        ).is_err() {
2303                            task_context.metrics.delivery_stopped(&view_id, "send-failed");
2304                            break;
2305                        }
2306                        task_context
2307                            .metrics
2308                            .collection_coalesced(&view_id, pending_updates);
2309                        current = next;
2310                        pending.clear();
2311                        pending_updates = 0;
2312                    }
2313                    received = receiver.recv() => {
2314                        let envelope = match received {
2315                            Ok(envelope) => envelope,
2316                            Err(broadcast::error::RecvError::Lagged(skipped)) => {
2317                                task_context.metrics.subscription_lagged(&view_id, skipped);
2318                                warn!(
2319                                    "Subscription {} lagged by {} updates",
2320                                    subscription_id, skipped
2321                                );
2322                                if view_spec_task.mode == Mode::Append {
2323                                    let _ = send_control_frame(
2324                                        &task_context,
2325                                        &SocketIssueMessage::append_subscription_lagged(
2326                                            subscription_id.clone(),
2327                                            skipped,
2328                                        ),
2329                                        &view_id,
2330                                    );
2331                                    task_context.metrics.delivery_stopped(&view_id, "append-lagged-without-replay");
2332                                    break;
2333                                }
2334                                if !task_subscription.snapshot.enabled {
2335                                    let _ = send_control_frame(
2336                                        &task_context,
2337                                        &SocketIssueMessage::subscription_lagged(
2338                                            subscription_id.clone(),
2339                                            skipped,
2340                                        ),
2341                                        &view_id,
2342                                    );
2343                                    task_context.metrics.delivery_stopped(&view_id, "lagged-without-snapshot");
2344                                    break;
2345                                }
2346                                info!(
2347                                    "Subscription {} is recovering from an authoritative snapshot",
2348                                    subscription_id
2349                                );
2350                                match recover_collection_subscription(
2351                                    &task_context,
2352                                    &task_subscription,
2353                                    &view_spec_task,
2354                                    &source_view_id,
2355                                ).await {
2356                                    Ok((next_receiver, recovered)) => {
2357                                        receiver = next_receiver;
2358                                        current = recovered;
2359                                        // Recovery replaces the whole live
2360                                        // membership, untruncated.
2361                                        undelivered.clear();
2362                                        sent_whole.clear();
2363                                        pending.clear();
2364                                        pending_updates = 0;
2365                                        task_context.metrics.subscription_resnapshot(&view_id);
2366                                        continue;
2367                                    }
2368                                    Err(error) => {
2369                                        warn!(
2370                                            "Subscription {} failed to recover from lag: {error:#}",
2371                                            subscription_id
2372                                        );
2373                                        task_context.metrics.delivery_stopped(&view_id, "resnapshot-failed");
2374                                        break;
2375                                    }
2376                                }
2377                            }
2378                            Err(broadcast::error::RecvError::Closed) => break,
2379                        };
2380
2381                        apply_collection_source_event(
2382                            &task_context,
2383                            &source_view_id,
2384                            &view_spec_task,
2385                            &query,
2386                            &envelope,
2387                        ).await;
2388
2389                        if flush_interval.is_some() {
2390                            pending_updates = pending_updates.saturating_add(1);
2391                            pending.insert(envelope.key.clone(), envelope);
2392                            continue;
2393                        }
2394
2395                        let metadata = source_frame_metadata(&envelope.payload);
2396                        let sorted_caches = view_spec_task
2397                            .is_derived()
2398                            .then(|| task_context.view_index.sorted_caches());
2399                        let next = load_query_entities(
2400                            &task_context.entity_cache,
2401                            sorted_caches,
2402                            &view_spec_task,
2403                            &query,
2404                            task_context.row_filter.as_ref(),
2405                            false,
2406                        ).await;
2407                        if emit_collection_delta(
2408                            &task_context,
2409                            &subscription_id,
2410                            &view_spec_task,
2411                            &current,
2412                            &next,
2413                            &envelope,
2414                            &metadata,
2415                            &mut undelivered,
2416                            &mut sent_whole,
2417                        ).is_err() {
2418                            task_context.metrics.delivery_stopped(&view_id, "send-failed");
2419                            break;
2420                        }
2421                        current = next;
2422                    }
2423                }
2424            }
2425        }
2426        .instrument(info_span!("ws.subscribe.collection", client_id = %context.client_id, view = %span_view)),
2427    );
2428    Ok(())
2429}
2430
2431async fn subscribe_collection_then_snapshot(
2432    context: &SubscriptionContext,
2433    source_view_id: &str,
2434    view_spec: &ViewSpec,
2435    query: &SubscriptionQuery,
2436) -> (
2437    broadcast::Receiver<Arc<BusMessage>>,
2438    Vec<(String, SharedEntity)>,
2439) {
2440    let cache = context.entity_cache.clone();
2441    let sorted_caches = view_spec
2442        .is_derived()
2443        .then(|| context.view_index.sorted_caches());
2444    let view_spec = view_spec.clone();
2445    let query = query.clone();
2446    let row_filter = context.row_filter.clone();
2447    subscribe_list_then_snapshot(&context.bus_manager, source_view_id, move || async move {
2448        load_query_entities(
2449            &cache,
2450            sorted_caches,
2451            &view_spec,
2452            &query,
2453            row_filter.as_ref(),
2454            false,
2455        )
2456        .await
2457    })
2458    .await
2459}
2460
2461async fn recover_collection_subscription(
2462    context: &SubscriptionContext,
2463    subscription: &Subscription,
2464    view_spec: &ViewSpec,
2465    source_view_id: &str,
2466) -> Result<(
2467    broadcast::Receiver<Arc<BusMessage>>,
2468    Vec<(String, SharedEntity)>,
2469)> {
2470    let (receiver, membership) =
2471        subscribe_collection_then_snapshot(context, source_view_id, view_spec, &subscription.query)
2472            .await;
2473    // `snapshotLimit` caps only the initial transfer. Recovery has to replace
2474    // the complete live membership; truncating it would make the replacement
2475    // authoritative while immediately omitting members the server still
2476    // considers current.
2477    enforce_snapshot_limit(context, membership.len())?;
2478    send_snapshot_batches(
2479        context,
2480        subscription,
2481        &membership,
2482        view_spec,
2483        SnapshotPurpose::Recovery,
2484        &context.entity_cache.snapshot_config(),
2485    )
2486    .await?;
2487    Ok((receiver, membership))
2488}
2489
2490async fn apply_collection_source_event(
2491    context: &SubscriptionContext,
2492    source_view_id: &str,
2493    view_spec: &ViewSpec,
2494    query: &SubscriptionQuery,
2495    envelope: &BusMessage,
2496) {
2497    let metadata = source_frame_metadata(&envelope.payload);
2498    if metadata.op != "delete" {
2499        return;
2500    }
2501    // The projector changed the shared cache before publishing this frame.
2502    // A slow subscription must inspect that authoritative lifetime state, not
2503    // replay the delete against `_seq` and potentially erase a recreation.
2504    if !context
2505        .entity_cache
2506        .deletion_is_current(source_view_id, &envelope.key)
2507        .await
2508    {
2509        return;
2510    }
2511    if view_spec.is_derived() {
2512        let caches = context.view_index.sorted_caches();
2513        let mut guard = caches.write().await;
2514        if let Some(cache) = guard.get_mut(&query.view) {
2515            cache.remove_key(&envelope.key);
2516        }
2517    }
2518}
2519
2520/// A subscription refused for a reason the client needs spelled out.
2521///
2522/// Attach paths that return this get the registration released by the
2523/// connection loop, the same as any other failure, while the client still
2524/// receives the specific error rather than a generic `subscription-rejected`.
2525/// Sending the frame and returning `Ok` instead would leave a registered
2526/// subscription with nothing attached: it would hold a slot against the
2527/// client's limit and make the advertised "resubscribe" remediation fail with
2528/// `duplicate-subscription-id`.
2529#[derive(Debug)]
2530pub(crate) struct RejectedSubscription(pub SocketIssueMessage);
2531
2532impl RejectedSubscription {
2533    fn into_error(issue: SocketIssueMessage) -> anyhow::Error {
2534        anyhow::Error::new(Self(issue))
2535    }
2536}
2537
2538impl std::fmt::Display for RejectedSubscription {
2539    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2540        formatter.write_str(&self.0.message)
2541    }
2542}
2543
2544impl std::error::Error for RejectedSubscription {}
2545
2546/// Deliver an append view as an event tape: replay the retained records after
2547/// the cursor, then forward live frames.
2548///
2549/// This deliberately does not recompute membership from the entity cache the
2550/// way [`attach_collection_subscription`] does. The cache folds each patch
2551/// into the resident entity, so a membership diff cannot express "these three
2552/// events happened"; the retained records can.
2553///
2554/// For the same reason records are never promoted to a full `upsert` the
2555/// first time this subscription sees their key, the way a window entry is.
2556/// The cache only holds the latest state, not the state as of a record's
2557/// offset, so substituting it would hand a consumer state from after its
2558/// cursor. Records go out exactly as retained, `patch` included: a tape is
2559/// events, and the consumer that resumes from a cursor holds whatever came
2560/// before it. Clients therefore apply frames that carry an `offset` as events
2561/// and do not discard them as partial entities (see "Partial entities" in
2562/// `docs/websocket-v2-protocol.md`).
2563async fn attach_journal_subscription(
2564    context: &SubscriptionContext,
2565    subscription: Subscription,
2566    view_spec: ViewSpec,
2567    journal: Arc<crate::journal::EventJournal>,
2568    cancel_token: CancellationToken,
2569) -> Result<()> {
2570    let view_id = view_spec.id.clone();
2571    let subscription_id = subscription.subscription_id.clone();
2572
2573    // A tape has no membership window, so `take`/`skip` cannot mean what they
2574    // mean on a list view. Refuse them rather than accept and ignore them.
2575    if subscription.query.take.is_some()
2576        || subscription.query.skip.is_some()
2577        || subscription.query.snapshot_limit.is_some()
2578    {
2579        return Err(RejectedSubscription::into_error(SocketIssueMessage::protocol(
2580            Some(subscription_id),
2581            "invalid-subscription",
2582            format!(
2583                "take, skip and snapshotLimit are window options and do not apply to the replayable view {view_id}"
2584            ),
2585        )));
2586    }
2587
2588    // Reject a malformed cursor rather than silently replaying from the start,
2589    // which would look like success and duplicate everything.
2590    let cursor = match subscription.query.after.as_deref() {
2591        Some(raw) => match crate::journal::Cursor::parse(raw) {
2592            Some(cursor) => Some(cursor),
2593            None => {
2594                return Err(RejectedSubscription::into_error(
2595                    SocketIssueMessage::protocol(
2596                        Some(subscription_id),
2597                        "invalid-cursor",
2598                        format!(
2599                            "`after` must be an {{epoch}}:{{offset}} replay cursor for view {view_id}, got {raw:?}"
2600                        ),
2601                    ),
2602                ));
2603            }
2604        },
2605        None => None,
2606    };
2607
2608    // Subscribe before reading the journal so anything published during the
2609    // replay is still delivered; the offset filter below drops the overlap.
2610    let mut receiver = context.bus_manager.get_or_create_list_bus(&view_id).await;
2611
2612    let replayed = match journal.replay_after(&view_id, cursor.as_ref()).await {
2613        Ok(records) => records,
2614        Err(error) => {
2615            return Err(RejectedSubscription::into_error(
2616                SocketIssueMessage::replay_refused(Some(subscription_id), &error),
2617            ));
2618        }
2619    };
2620
2621    let frame = SubscribedFrame::new(
2622        subscription.subscription_id.clone(),
2623        subscription.query.clone(),
2624        view_spec.mode,
2625        extract_sort_config(&view_spec),
2626    )
2627    .with_replay_window(journal.window(&view_id).await);
2628    send_control_frame(context, &frame, &view_id)?;
2629
2630    // Everything past the acknowledgement runs on its own task. The replay can
2631    // be long and applies real backpressure, and `attach_client_to_bus` is
2632    // awaited directly on the connection's inbound loop — doing it there would
2633    // block unsubscribe, auth refresh and pong for the whole replay.
2634    let task_context = context.clone();
2635    let task_subscription_id = subscription.subscription_id.clone();
2636    let task_query = subscription.query.clone();
2637    let task_epoch = journal.epoch().await;
2638    let span_view = view_id.clone();
2639    tokio::spawn(
2640        async move {
2641            let mut last_sent = cursor.map(|cursor| cursor.offset);
2642            // Frames that published while the replay was still running. The
2643            // bus is a bounded broadcast, so it has to be drained as we go or
2644            // a busy view laps us before the replay finishes.
2645            let mut pending: VecDeque<Arc<BusMessage>> = VecDeque::new();
2646            let mut lagged: Option<u64> = None;
2647
2648            for record in replayed {
2649                if cancel_token.is_cancelled() {
2650                    return;
2651                }
2652                drain_available(&mut receiver, &mut pending, &mut lagged);
2653                if journal_record_matches(&task_query, &record)
2654                    && frame_in_row_scopes(
2655                        task_context.row_filter.as_ref(),
2656                        record.row_scope.as_ref(),
2657                        &record.payload,
2658                    )
2659                    && send_scoped_source_payload_async(
2660                        &task_context,
2661                        &task_subscription_id,
2662                        &span_view,
2663                        record.payload,
2664                    )
2665                    .await
2666                    .is_err()
2667                {
2668                    return;
2669                }
2670                // Advance past filtered records too: they were considered.
2671                last_sent = Some(record.offset);
2672            }
2673
2674            // Flush what arrived during the replay before going live, so the
2675            // handover keeps offset order.
2676            while let Some(envelope) = pending.pop_front() {
2677                if !forward_live_frame(
2678                    &task_context,
2679                    &task_subscription_id,
2680                    &span_view,
2681                    &task_query,
2682                    &envelope,
2683                    &mut last_sent,
2684                )
2685                .await
2686                {
2687                    return;
2688                }
2689            }
2690
2691            if let Some(skipped) = lagged {
2692                report_replay_gap(
2693                    &task_context,
2694                    &task_subscription_id,
2695                    &span_view,
2696                    &task_epoch,
2697                    skipped,
2698                    last_sent,
2699                );
2700                return;
2701            }
2702
2703            loop {
2704                tokio::select! {
2705                    _ = cancel_token.cancelled() => break,
2706                    received = receiver.recv() => {
2707                        let envelope = match received {
2708                            Ok(envelope) => envelope,
2709                            // A lagged tape is a gap. Report it with the last
2710                            // offset delivered *before* the gap and stop
2711                            // delivering on this subscription.
2712                            //
2713                            // Continuing would hand the consumer frames from
2714                            // after the gap, advancing its checkpoint past
2715                            // the skipped records so they could never be
2716                            // replayed. Stopping is not a silent stall: the
2717                            // consumer has an explicit error and a cursor
2718                            // that recovers exactly what it missed.
2719                            //
2720                            // The registration is deliberately left alone.
2721                            // Its lifecycle belongs to the connection loop,
2722                            // which holds the only handle to
2723                            // `active_subscriptions`; releasing half of it
2724                            // here would desynchronise unsubscribe, the
2725                            // duplicate-ID gate and close-time usage.
2726                            Err(broadcast::error::RecvError::Lagged(skipped)) => {
2727                                report_replay_gap(
2728                                    &task_context,
2729                                    &task_subscription_id,
2730                                    &span_view,
2731                                    &task_epoch,
2732                                    skipped,
2733                                    last_sent,
2734                                );
2735                                break;
2736                            }
2737                            Err(broadcast::error::RecvError::Closed) => break,
2738                        };
2739
2740                        if !forward_live_frame(
2741                            &task_context,
2742                            &task_subscription_id,
2743                            &span_view,
2744                            &task_query,
2745                            &envelope,
2746                            &mut last_sent,
2747                        )
2748                        .await
2749                        {
2750                            break;
2751                        }
2752                    }
2753                }
2754            }
2755        }
2756        .instrument(info_span!(
2757            "ws.subscribe.replay",
2758            client_id = %context.client_id,
2759            view = %view_id
2760        )),
2761    );
2762    Ok(())
2763}
2764
2765/// Take whatever the bus already has without waiting, so a long replay cannot
2766/// be lapped by a busy view.
2767fn drain_available(
2768    receiver: &mut broadcast::Receiver<Arc<BusMessage>>,
2769    pending: &mut VecDeque<Arc<BusMessage>>,
2770    lagged: &mut Option<u64>,
2771) {
2772    // Once a gap is known, everything still on the bus is on the far side of
2773    // it. Buffering it would put post-gap frames in front of the lag report,
2774    // advancing `last_sent` past the hole and making `recoverFrom` point
2775    // after the very records it is supposed to recover.
2776    if lagged.is_some() {
2777        return;
2778    }
2779    // Bounded so a view publishing faster than the client drains cannot turn
2780    // the buffer into an unbounded queue. Filling it is not itself a gap:
2781    // nothing has been skipped at that instant, the buffer simply stopped
2782    // accepting. Stop buffering and let the bus report the loss, with the
2783    // count it actually measures, when delivery reaches it.
2784    const MAX_PENDING: usize = 8_192;
2785    loop {
2786        if pending.len() >= MAX_PENDING {
2787            return;
2788        }
2789        match receiver.try_recv() {
2790            Ok(envelope) => pending.push_back(envelope),
2791            Err(broadcast::error::TryRecvError::Empty)
2792            | Err(broadcast::error::TryRecvError::Closed) => return,
2793            Err(broadcast::error::TryRecvError::Lagged(skipped)) => {
2794                *lagged = Some(skipped);
2795                return;
2796            }
2797        }
2798    }
2799}
2800
2801/// Whether a live frame was already delivered by the replay that preceded it.
2802///
2803/// The bus is subscribed before the tape is read, so a record published in
2804/// between appears on both paths; without this the consumer sees it twice.
2805/// Advances the high-water mark as a side effect.
2806fn already_delivered(offset: Option<u64>, last_sent: &mut Option<u64>) -> bool {
2807    let Some(offset) = offset else {
2808        // A frame with no offset predates the tape, so it cannot have been
2809        // replayed and must not move the mark.
2810        return false;
2811    };
2812    if last_sent.is_some_and(|last| offset <= last) {
2813        return true;
2814    }
2815    *last_sent = Some(offset);
2816    false
2817}
2818
2819/// Deliver one live frame, skipping anything the replay already sent.
2820///
2821/// Returns false when the subscription should end.
2822async fn forward_live_frame(
2823    context: &SubscriptionContext,
2824    subscription_id: &str,
2825    view_id: &str,
2826    query: &SubscriptionQuery,
2827    envelope: &Arc<BusMessage>,
2828    last_sent: &mut Option<u64>,
2829) -> bool {
2830    let metadata = source_frame_metadata(&envelope.payload);
2831    if already_delivered(metadata.offset, last_sent) {
2832        return true;
2833    }
2834    if !live_frame_matches(query, &envelope.key, &envelope.payload) {
2835        return true;
2836    }
2837    if !frame_in_row_scopes(
2838        context.row_filter.as_ref(),
2839        envelope.row_scope.as_ref(),
2840        &envelope.payload,
2841    ) {
2842        return true;
2843    }
2844    send_scoped_source_payload(context, subscription_id, view_id, envelope.payload.clone()).is_ok()
2845}
2846
2847/// Check the scope frozen with a retained or queued frame. Legacy records
2848/// may prove scope from an explicit field, but never from today's row state.
2849fn frame_in_row_scopes(
2850    filter: Option<&RowFilter>,
2851    scope: Option<&crate::row_scope::RowScope>,
2852    payload: &[u8],
2853) -> bool {
2854    let Some(filter) = filter else {
2855        return true;
2856    };
2857    if let Some(scope) = scope {
2858        return filter.allows_scope(scope);
2859    }
2860    source_frame_data(payload)
2861        .as_ref()
2862        .and_then(|data| filter.field().scope_in(data))
2863        .is_some_and(|scope| filter.allows_scope(&scope))
2864}
2865
2866fn report_replay_gap(
2867    context: &SubscriptionContext,
2868    subscription_id: &str,
2869    view_id: &str,
2870    epoch: &crate::journal::JournalEpoch,
2871    skipped: u64,
2872    last_sent: Option<u64>,
2873) {
2874    warn!(
2875        "Replay subscription {} lagged past {} records; stopping with a recovery cursor",
2876        subscription_id, skipped
2877    );
2878    let recover_from = last_sent.map(|offset| crate::journal::Cursor {
2879        epoch: epoch.clone(),
2880        offset,
2881    });
2882    let _ = send_control_frame(
2883        context,
2884        &SocketIssueMessage::replay_lagged(
2885            Some(subscription_id.to_string()),
2886            skipped,
2887            recover_from,
2888        ),
2889        view_id,
2890    );
2891}
2892
2893/// Apply the subscription's `key`, `partition` and `filters` to a retained
2894/// record. A replay must honour the same predicates a live subscription does.
2895fn journal_record_matches(
2896    query: &SubscriptionQuery,
2897    record: &crate::journal::JournalRecord,
2898) -> bool {
2899    live_frame_matches(query, &record.key, &record.payload)
2900}
2901
2902fn live_frame_matches(query: &SubscriptionQuery, key: &str, payload: &[u8]) -> bool {
2903    if !query.matches_key(key) {
2904        return false;
2905    }
2906    if query.partition.is_none() && query.filters.is_empty() {
2907        return true;
2908    }
2909    let Some(data) = source_frame_data(payload) else {
2910        return false;
2911    };
2912    let data = &data;
2913    if let Some(partition) = &query.partition {
2914        if value_at_dot_path(data, "_partition").as_deref()
2915            != Some(&Value::String(partition.clone()))
2916        {
2917            return false;
2918        }
2919    }
2920    query
2921        .filters
2922        .iter()
2923        .all(|(path, expected)| value_at_dot_path(data, path).as_deref() == Some(expected))
2924}
2925
2926/// Awaiting variant of [`send_scoped_source_payload`], for replays that can
2927/// exceed the client's send queue.
2928async fn send_scoped_source_payload_async(
2929    context: &SubscriptionContext,
2930    subscription_id: &str,
2931    view_id: &str,
2932    payload: Arc<Bytes>,
2933) -> Result<()> {
2934    let json = SourceFields::scoped(&payload, subscription_id)?;
2935    let compressed = maybe_compress(&json);
2936    let bytes = compressed.as_bytes().len();
2937    let _held = hold_row_scopes_async(context).await?;
2938    context
2939        .client_manager
2940        .send_compressed_async(context.client_id, compressed)
2941        .await
2942        .map_err(|error| anyhow::anyhow!("failed to send replayed frame: {error}"))?;
2943    context.metrics.message_sent();
2944    emit_update_sent_for_client(
2945        &context.usage_emitter,
2946        &context.client_manager,
2947        context.client_id,
2948        view_id,
2949        bytes,
2950    );
2951    Ok(())
2952}
2953
2954#[derive(Default)]
2955struct SourceFrameMetadata {
2956    op: String,
2957    seq: Option<String>,
2958    offset: Option<u64>,
2959}
2960
2961/// A source frame's `op`, `seq` and `offset`, read without building its
2962/// `data`.
2963fn source_frame_metadata(payload: &[u8]) -> SourceFrameMetadata {
2964    SourceFields::parse(payload)
2965        .map(|fields| SourceFrameMetadata {
2966            op: fields
2967                .value("op")
2968                .as_ref()
2969                .and_then(Value::as_str)
2970                .unwrap_or_default()
2971                .to_string(),
2972            seq: fields
2973                .value("seq")
2974                .as_ref()
2975                .and_then(Value::as_str)
2976                .map(str::to_string),
2977            offset: fields.value("offset").as_ref().and_then(Value::as_u64),
2978        })
2979        .unwrap_or_default()
2980}
2981
2982/// The `data` of a source frame, as a client receives it.
2983fn source_frame_data(payload: &[u8]) -> Option<Value> {
2984    SourceFields::parse(payload)?.value("data")
2985}
2986
2987/// What a state subscriber last received whole, which a catch-up is
2988/// measured against. Both kinds are shared, with the entity cache or the bus,
2989/// so a subscriber keeps no copy of its own. A subscriber does keep the
2990/// version it was sent alive while the cache moves on; subscribers sent the
2991/// same version share it.
2992enum Synced {
2993    /// The cached entity, as sent in a snapshot row, an upsert or a catch-up.
2994    Entity(SharedEntity),
2995    /// A forwarded `upsert` source frame, whose `data` the client holds.
2996    Frame(Arc<Bytes>),
2997}
2998
2999impl Synced {
3000    /// What the client holds, as it received it: in wire format.
3001    fn wire(&self, wire_format: &WireFormat) -> Option<Value> {
3002        match self {
3003            Synced::Entity(entity) => Some(wire_value(entity, wire_format)),
3004            Synced::Frame(frame) => source_frame_data(frame),
3005        }
3006    }
3007}
3008
3009/// Tracks what a client holds after a frame forwarded to it as published. An
3010/// upsert hands it the entity whole, which a catch-up is then measured
3011/// against; a patch sets fields (see [`mark_fields`]).
3012fn record_forwarded(
3013    payload: &Arc<Bytes>,
3014    op: &str,
3015    synced: &mut Option<Synced>,
3016    touched: &mut Value,
3017) {
3018    if op == "upsert" {
3019        *synced = Some(Synced::Frame(payload.clone()));
3020        *touched = Value::Null;
3021    } else if let Some(data) = source_frame_data(payload) {
3022        mark_fields(touched, &data);
3023    }
3024}
3025
3026/// Marks in `touched` the fields `patch` sets: objects field by field, any
3027/// other value whole (`true`). `null` marks nothing.
3028///
3029/// A client holds whatever a forwarded patch set, which the entity may have
3030/// set back before the next catch-up (or the client may have dropped the
3031/// patch), so a catch-up sends these fields even when they match the entity
3032/// the client last received whole.
3033fn mark_fields(touched: &mut Value, patch: &Value) {
3034    let Value::Object(fields) = patch else {
3035        *touched = Value::Bool(true);
3036        return;
3037    };
3038    if touched.is_null() {
3039        *touched = Value::Object(serde_json::Map::new());
3040    }
3041    let Value::Object(marked) = touched else {
3042        // Already marked whole.
3043        return;
3044    };
3045    for (field, value) in fields {
3046        mark_fields(marked.entry(field.clone()).or_insert(Value::Null), value);
3047    }
3048}
3049
3050/// The fields of `current` that differ from `base` or that `touched` marks
3051/// (see [`mark_fields`]), as a patch that turns what the client holds into
3052/// `current` when merged: objects compare field by field, any other value is
3053/// sent whole, and a field `current` no longer has is sent as `null`. `None`
3054/// when nothing needs sending.
3055fn changed_fields(base: &Value, current: &Value, touched: &Value) -> Option<Value> {
3056    match (base, current, touched) {
3057        (_, _, Value::Bool(true)) => Some(current.clone()),
3058        (Value::Object(base), Value::Object(current), _) => {
3059            let marked = touched.as_object();
3060            let mut patch = serde_json::Map::new();
3061            for (field, value) in current {
3062                let touched = marked
3063                    .and_then(|marked| marked.get(field))
3064                    .unwrap_or(&Value::Null);
3065                match base.get(field) {
3066                    Some(old) => {
3067                        if let Some(changed) = changed_fields(old, value, touched) {
3068                            patch.insert(field.clone(), changed);
3069                        }
3070                    }
3071                    None => {
3072                        patch.insert(field.clone(), value.clone());
3073                    }
3074                }
3075            }
3076            for field in base
3077                .keys()
3078                .chain(marked.into_iter().flat_map(|marked| marked.keys()))
3079            {
3080                if !current.contains_key(field) {
3081                    patch.insert(field.clone(), Value::Null);
3082                }
3083            }
3084            (!patch.is_empty()).then_some(Value::Object(patch))
3085        }
3086        _ => (base != current || !touched.is_null()).then(|| current.clone()),
3087    }
3088}
3089
3090fn send_scoped_source_payload(
3091    context: &SubscriptionContext,
3092    subscription_id: &str,
3093    view_id: &str,
3094    payload: Arc<Bytes>,
3095) -> Result<()> {
3096    let encoded = Arc::new(Bytes::from(SourceFields::scoped(
3097        &payload,
3098        subscription_id,
3099    )?));
3100    let bytes = encoded.len();
3101    let _held = hold_row_scopes(context)?;
3102    context
3103        .client_manager
3104        .send_to_client(context.client_id, encoded)
3105        .map_err(|error| anyhow::anyhow!("failed to send live frame: {error}"))?;
3106    context.metrics.message_sent();
3107    emit_update_sent_for_client(
3108        &context.usage_emitter,
3109        &context.client_manager,
3110        context.client_id,
3111        view_id,
3112        bytes,
3113    );
3114    Ok(())
3115}
3116
3117fn send_membership_frame(
3118    context: &SubscriptionContext,
3119    subscription_id: &str,
3120    view_spec: &ViewSpec,
3121    op: &str,
3122    key: &str,
3123    mut data: Value,
3124    seq: Option<String>,
3125) -> Result<()> {
3126    apply_wire_format(&mut data, &view_spec.wire_format);
3127    let frame = Frame::scoped(
3128        subscription_id,
3129        view_spec.mode,
3130        &view_spec.id,
3131        op,
3132        key,
3133        data,
3134        seq,
3135    );
3136    send_membership_json(context, view_spec, serde_json::to_vec(&frame)?)
3137}
3138
3139/// A scoped frame whose data is a cached entity, serialized straight from
3140/// it: the same JSON as [`Frame::scoped`] with a formatted copy of the entity.
3141#[derive(Serialize)]
3142#[serde(rename_all = "camelCase")]
3143struct ScopedEntityFrame<'a> {
3144    protocol_version: u8,
3145    subscription_id: &'a str,
3146    mode: Mode,
3147    #[serde(rename = "entity")]
3148    export: &'a str,
3149    op: &'a str,
3150    key: &'a str,
3151    data: WireEntity<'a>,
3152    #[serde(skip_serializing_if = "Option::is_none")]
3153    seq: Option<&'a str>,
3154}
3155
3156/// [`send_membership_frame`] for a cached entity, without copying it.
3157fn send_entity_frame(
3158    context: &SubscriptionContext,
3159    subscription_id: &str,
3160    view_spec: &ViewSpec,
3161    op: &str,
3162    key: &str,
3163    entity: &SharedEntity,
3164    seq: Option<&str>,
3165) -> Result<()> {
3166    let frame = ScopedEntityFrame {
3167        protocol_version: PROTOCOL_VERSION,
3168        subscription_id,
3169        mode: view_spec.mode,
3170        export: &view_spec.id,
3171        op,
3172        key,
3173        data: WireEntity {
3174            entity,
3175            wire_format: &view_spec.wire_format,
3176        },
3177        seq,
3178    };
3179    send_membership_json(context, view_spec, serde_json::to_vec(&frame)?)
3180}
3181
3182fn send_membership_json(
3183    context: &SubscriptionContext,
3184    view_spec: &ViewSpec,
3185    json: Vec<u8>,
3186) -> Result<()> {
3187    let encoded = Arc::new(Bytes::from(json));
3188    let bytes = encoded.len();
3189    let _held = hold_row_scopes(context)?;
3190    context
3191        .client_manager
3192        .send_to_client(context.client_id, encoded)
3193        .map_err(|error| anyhow::anyhow!("failed to send membership frame: {error}"))?;
3194    context.metrics.message_sent();
3195    emit_update_sent_for_client(
3196        &context.usage_emitter,
3197        &context.client_manager,
3198        context.client_id,
3199        &view_spec.id,
3200        bytes,
3201    );
3202    Ok(())
3203}
3204
3205/// Send what one source mutation owes a subscriber, and keep `undelivered`
3206/// (window members the client holds no copy of) in step with what was sent.
3207#[allow(clippy::too_many_arguments)]
3208fn emit_collection_delta(
3209    context: &SubscriptionContext,
3210    subscription_id: &str,
3211    view_spec: &ViewSpec,
3212    current: &[(String, SharedEntity)],
3213    next: &[(String, SharedEntity)],
3214    envelope: &BusMessage,
3215    metadata: &SourceFrameMetadata,
3216    undelivered: &mut HashSet<String>,
3217    sent_whole: &mut SentWhole,
3218) -> Result<()> {
3219    let current_keys: HashSet<&str> = current.iter().map(|(key, _)| key.as_str()).collect();
3220    let next_set: HashSet<&str> = next.iter().map(|(key, _)| key.as_str()).collect();
3221
3222    // In window order, as the frames go out.
3223    for key in current
3224        .iter()
3225        .map(|(key, _)| key.as_str())
3226        .filter(|key| !next_set.contains(key))
3227    {
3228        sent_whole.forget(key);
3229        // A key the client was never sent has nothing to remove.
3230        if undelivered.remove(key) {
3231            continue;
3232        }
3233        let op = if metadata.op == "delete" && key == envelope.key {
3234            "delete"
3235        } else {
3236            "remove"
3237        };
3238        send_membership_frame(
3239            context,
3240            subscription_id,
3241            view_spec,
3242            op,
3243            key,
3244            Value::Null,
3245            metadata.seq.clone(),
3246        )?;
3247    }
3248
3249    for (key, data) in next.iter() {
3250        let in_window = current_keys.contains(key.as_str());
3251        let action = match member_action(
3252            in_window,
3253            in_window && !undelivered.contains(key),
3254            key == &envelope.key,
3255            view_spec.is_derived(),
3256            &metadata.op,
3257        ) {
3258            // The row is in this session's scopes now (it is in `next`), but
3259            // the patch was published under the scope it had then. If that
3260            // scope is not this session's, send the row as it is now instead.
3261            MemberAction::ForwardPatch
3262                if !frame_in_row_scopes(
3263                    context.row_filter.as_ref(),
3264                    envelope.row_scope.as_ref(),
3265                    &envelope.payload,
3266                ) =>
3267            {
3268                MemberAction::Upsert
3269            }
3270            action => action,
3271        };
3272        match action {
3273            MemberAction::Skip => {}
3274            MemberAction::ForwardPatch => {
3275                // The upsert that brought the key into the window may already
3276                // carry this patch: it was read from the cache, which folds
3277                // a mutation in before its frame reaches this task.
3278                if sent_whole.covers(key, || {
3279                    source_frame_data(&envelope.payload)
3280                        .and_then(|data| data.get("_version").cloned())
3281                }) {
3282                    continue;
3283                }
3284                send_scoped_source_payload(
3285                    context,
3286                    subscription_id,
3287                    &view_spec.id,
3288                    envelope.payload.clone(),
3289                )?
3290            }
3291            MemberAction::Upsert => {
3292                let held = in_window && !undelivered.contains(key);
3293                // A derived view sends the mutated key whole every time; the
3294                // client already holds this exact version.
3295                if held && sent_whole.holds(key, data.version()) {
3296                    continue;
3297                }
3298                sent_whole.sent(key, data.version());
3299                let seq = metadata
3300                    .seq
3301                    .as_deref()
3302                    .or_else(|| data.field("_seq").and_then(Value::as_str));
3303                send_entity_frame(
3304                    context,
3305                    subscription_id,
3306                    view_spec,
3307                    "upsert",
3308                    key,
3309                    data,
3310                    seq,
3311                )?;
3312                undelivered.remove(key);
3313            }
3314        }
3315    }
3316    Ok(())
3317}
3318
3319/// The `_version` of each entity a list subscriber was last sent whole (an
3320/// upsert), so a source patch the upsert already carried is not sent again.
3321///
3322/// A list subscriber sends a key entering its window as the cached entity,
3323/// read when the subscriber handles the key's first mutation. The projector
3324/// folds a mutation into the cache before the subscriber handles its frame,
3325/// so when one key changes twice in quick succession (an entity created by
3326/// one instruction and completed by the next in the same transaction), the
3327/// upsert already holds the second change, and forwarding the second patch
3328/// as well repeats it. Versions are `<epoch>:<counter>`; a patch whose
3329/// version is not newer than the one sent whole is skipped.
3330///
3331/// Forgetting an entry only means a redundant patch may be forwarded, which
3332/// clients ignore by version, so the map is capped rather than kept exact.
3333#[derive(Default)]
3334struct SentWhole(HashMap<String, Value>);
3335
3336impl SentWhole {
3337    /// Entries kept per subscription; beyond it the map starts over.
3338    const MAX_ENTRIES: usize = 4096;
3339
3340    fn sent(&mut self, key: &str, version: Option<&Value>) {
3341        match version {
3342            Some(version) => {
3343                if self.0.len() >= Self::MAX_ENTRIES && !self.0.contains_key(key) {
3344                    self.0.clear();
3345                }
3346                self.0.insert(key.to_string(), version.clone());
3347            }
3348            None => {
3349                self.0.remove(key);
3350            }
3351        }
3352    }
3353
3354    fn forget(&mut self, key: &str) {
3355        self.0.remove(key);
3356    }
3357
3358    fn clear(&mut self) {
3359        self.0.clear();
3360    }
3361
3362    /// Whether the client holds exactly `version` of `key`.
3363    fn holds(&self, key: &str, version: Option<&Value>) -> bool {
3364        version.is_some() && self.0.get(key) == version
3365    }
3366
3367    /// Whether the entity sent whole for `key` already carries a patch whose
3368    /// version `patch_version` reads. Reads the patch only when `key` was
3369    /// sent whole. A newer patch (or one without a comparable version) ends
3370    /// the entry: the client then holds more than what was sent whole.
3371    fn covers(&mut self, key: &str, patch_version: impl FnOnce() -> Option<Value>) -> bool {
3372        let Some(sent) = self.0.get(key) else {
3373            return false;
3374        };
3375        let covered = match (
3376            version_parts(sent),
3377            patch_version().as_ref().and_then(version_parts),
3378        ) {
3379            (Some((sent_epoch, sent_counter)), Some((epoch, counter))) => {
3380                epoch == sent_epoch && counter <= sent_counter
3381            }
3382            _ => false,
3383        };
3384        if !covered {
3385            self.0.remove(key);
3386        }
3387        covered
3388    }
3389}
3390
3391/// `(epoch, counter)` of a `_version` (`<epoch>:<counter>`).
3392fn version_parts(version: &Value) -> Option<(String, u64)> {
3393    let (epoch, counter) = version.as_str()?.rsplit_once(':')?;
3394    Some((epoch.to_string(), counter.parse().ok()?))
3395}
3396
3397/// Emit the net effect of every source mutation seen during one coalescing
3398/// interval. Full entities are used for changed members because intermediate
3399/// sparse patches were intentionally discarded and can no longer be merged
3400/// safely by a client.
3401fn emit_coalesced_collection_delta(
3402    context: &SubscriptionContext,
3403    subscription_id: &str,
3404    view_spec: &ViewSpec,
3405    current: &[(String, SharedEntity)],
3406    next: &[(String, SharedEntity)],
3407    pending: &HashMap<String, Arc<BusMessage>>,
3408    undelivered: &mut HashSet<String>,
3409) -> Result<()> {
3410    let changes = plan_coalesced_collection_delta(current, next, pending, undelivered);
3411    // Every key that left the window or was just upserted is settled: the
3412    // client either never needs it or now holds all of it.
3413    let next_keys: HashSet<&str> = next.iter().map(|(key, _)| key.as_str()).collect();
3414    undelivered.retain(|key| next_keys.contains(key.as_str()));
3415    for change in changes {
3416        if change.op == "upsert" {
3417            undelivered.remove(&change.key);
3418        }
3419        match &change.data {
3420            Some(entity) => send_entity_frame(
3421                context,
3422                subscription_id,
3423                view_spec,
3424                change.op,
3425                &change.key,
3426                entity,
3427                change.seq.as_deref(),
3428            )?,
3429            None => send_membership_frame(
3430                context,
3431                subscription_id,
3432                view_spec,
3433                change.op,
3434                &change.key,
3435                Value::Null,
3436                change.seq,
3437            )?,
3438        }
3439    }
3440    Ok(())
3441}
3442
3443#[derive(Debug, PartialEq)]
3444struct CollectionChange {
3445    op: &'static str,
3446    key: String,
3447    /// The entity an `upsert` sends; `None` for a `remove` or `delete`.
3448    data: Option<SharedEntity>,
3449    seq: Option<String>,
3450}
3451
3452/// Plan one coalescing interval. `undelivered` holds window members the
3453/// client was never sent; they get no `remove`/`delete` when they leave, and
3454/// like every changed key they are sent whole when they change.
3455fn plan_coalesced_collection_delta(
3456    current: &[(String, SharedEntity)],
3457    next: &[(String, SharedEntity)],
3458    pending: &HashMap<String, Arc<BusMessage>>,
3459    undelivered: &HashSet<String>,
3460) -> Vec<CollectionChange> {
3461    let current_by_key: HashMap<&str, &SharedEntity> = current
3462        .iter()
3463        .map(|(key, data)| (key.as_str(), data))
3464        .collect();
3465    let next_keys: HashSet<&str> = next.iter().map(|(key, _)| key.as_str()).collect();
3466    let latest_seq = pending
3467        .values()
3468        .filter_map(|envelope| source_frame_metadata(&envelope.payload).seq)
3469        .max_by(|left, right| cmp_seq(left, right));
3470    let mut changes = Vec::new();
3471
3472    for (key, _) in current
3473        .iter()
3474        .filter(|(key, _)| !next_keys.contains(key.as_str()) && !undelivered.contains(key.as_str()))
3475    {
3476        let metadata = pending
3477            .get(key)
3478            .map(|envelope| source_frame_metadata(&envelope.payload));
3479        let op = if metadata
3480            .as_ref()
3481            .is_some_and(|metadata| metadata.op == "delete")
3482        {
3483            "delete"
3484        } else {
3485            "remove"
3486        };
3487        let seq = metadata
3488            .and_then(|metadata| metadata.seq)
3489            .or_else(|| latest_seq.clone());
3490        changes.push(CollectionChange {
3491            op,
3492            key: key.clone(),
3493            data: None,
3494            seq,
3495        });
3496    }
3497
3498    for (key, data) in next {
3499        let changed = current_by_key
3500            .get(key.as_str())
3501            .is_none_or(|previous| *previous != data);
3502        if !changed && !pending.contains_key(key) {
3503            continue;
3504        }
3505        let seq = data
3506            .field("_seq")
3507            .and_then(Value::as_str)
3508            .map(str::to_string)
3509            .or_else(|| {
3510                pending
3511                    .get(key)
3512                    .and_then(|envelope| source_frame_metadata(&envelope.payload).seq)
3513            })
3514            .or_else(|| latest_seq.clone());
3515        changes.push(CollectionChange {
3516            op: "upsert",
3517            key: key.clone(),
3518            data: Some(data.clone()),
3519            seq,
3520        });
3521    }
3522    changes
3523}
3524
3525/// What one in-window key owes a subscriber after a source mutation.
3526#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3527enum MemberAction {
3528    /// Send nothing: the subscriber already holds this entity and its data did
3529    /// not change.
3530    Skip,
3531    /// Forward the source patch verbatim.
3532    ForwardPatch,
3533    /// Send the whole entity.
3534    Upsert,
3535}
3536
3537/// Decide what to send for one key in the query window.
3538///
3539/// `in_window` says the key was in the window before this mutation; `held`
3540/// says the client also has a copy of it. The two differ after a snapshot
3541/// that `snapshotLimit` truncated or that was disabled: those keys count for
3542/// the window's positions, but the client was never sent them.
3543///
3544/// Only the mutated key's data changed, so a key that was already in the
3545/// window has at most moved index — and index is not the server's to
3546/// communicate. `subscribed` announces the window's sort (see
3547/// [`extract_sort_config`], which is `_seq` descending for a plain list) and
3548/// every SDK re-sorts locally from it, so resending an unchanged entity to
3549/// convey its new position is pure waste. This matters because on a
3550/// `_seq`-ordered list the mutated entity jumps to the front on *every*
3551/// mutation: keying the decision on position change meant rebroadcasting the
3552/// whole window each time, and the mutated entity itself never kept its
3553/// position long enough to have its patch forwarded.
3554fn member_action(
3555    in_window: bool,
3556    held: bool,
3557    is_mutated_key: bool,
3558    is_derived: bool,
3559    op: &str,
3560) -> MemberAction {
3561    if !is_mutated_key {
3562        // A key entering the window has no local state to merge into, so it
3563        // needs the whole entity. One already in the window is unchanged:
3564        // if the client holds it there is nothing new, and if it doesn't the
3565        // snapshot limit deliberately left it out, so it waits for its own
3566        // next change.
3567        return if in_window {
3568            MemberAction::Skip
3569        } else {
3570            MemberAction::Upsert
3571        };
3572    }
3573    // The mutated entity rides its own patch through untouched, but only when
3574    // the subscriber already holds a copy; a patch for a key the client never
3575    // received is not an entity. Derived views still send whole entities: the
3576    // patch on the bus is scoped to the source view, not this one (see
3577    // A4-150).
3578    if held && !is_derived && op != "delete" {
3579        MemberAction::ForwardPatch
3580    } else {
3581        MemberAction::Upsert
3582    }
3583}
3584
3585/// A copy of `entity` with the view's wire format applied, for code that
3586/// needs it as a value (a catch-up diff) rather than serialized.
3587fn wire_value(entity: &SharedEntity, wire_format: &WireFormat) -> Value {
3588    let mut value = entity.to_value();
3589    apply_wire_format(&mut value, wire_format);
3590    value
3591}
3592
3593async fn load_query_entities(
3594    entity_cache: &EntityCache,
3595    sorted_caches: Option<
3596        Arc<tokio::sync::RwLock<HashMap<String, crate::sorted_cache::SortedViewCache>>>,
3597    >,
3598    view_spec: &ViewSpec,
3599    query: &SubscriptionQuery,
3600    row_filter: Option<&RowFilter>,
3601    apply_snapshot_limit: bool,
3602) -> Vec<(String, SharedEntity)> {
3603    // Rows share their fields with the caches; nothing here copies an entity.
3604    let mut query = query.clone();
3605    if let Some(limit) = view_spec
3606        .pipeline
3607        .as_ref()
3608        .and_then(|pipeline| pipeline.limit)
3609    {
3610        query.take = Some(query.take.unwrap_or(limit).min(limit));
3611    }
3612    if let Some(sorted_caches) = sorted_caches {
3613        // A sorted cache is read in order, and only as far as the window
3614        // reaches: rows past it are neither visited nor shared.
3615        let caches = sorted_caches.read().await;
3616        if let Some(cache) = caches.get(&view_spec.id) {
3617            // A row-scoped view keeps its rows per scope: read only the
3618            // partitions this session may see, so its window is as full as
3619            // its own rows allow.
3620            let no_scopes = std::collections::BTreeSet::new();
3621            let rows: Box<dyn Iterator<Item = (&str, &SharedEntity)> + '_> = match row_filter {
3622                Some(filter) => Box::new(cache.iter_ordered_in_scopes(filter.scopes())),
3623                None if cache.is_scope_partitioned() => {
3624                    Box::new(cache.iter_ordered_in_scopes(&no_scopes))
3625                }
3626                None => Box::new(cache.iter_ordered()),
3627            };
3628            return select_window(
3629                rows.filter(|(key, data)| query_matches_entity(&query, key, *data, row_filter))
3630                    .map(|(key, data)| (key.to_string(), data.clone())),
3631                &query,
3632                apply_snapshot_limit,
3633            );
3634        }
3635    }
3636    let (entities, preordered) = if view_spec.is_derived() {
3637        // Empty and filter-only pipelines have no sorted cache. Evaluate the
3638        // source rows, retaining the pipeline predicate before query selection.
3639        let mut entities = entity_cache
3640            .get_all_shared(view_spec.source_view.as_deref().unwrap_or(&view_spec.id))
3641            .await;
3642        if let Some(filter) = view_spec
3643            .pipeline
3644            .as_ref()
3645            .and_then(|pipeline| pipeline.filter.as_ref())
3646        {
3647            entities.retain(|(_, data)| filter.matches(data));
3648        }
3649        (entities, false)
3650    } else if view_spec.mode == Mode::State {
3651        let entity = match query.key.as_deref() {
3652            Some(key) => entity_cache
3653                .get_shared(&view_spec.id, key)
3654                .await
3655                .map(|data| vec![(key.to_string(), data)])
3656                .unwrap_or_default(),
3657            None => vec![],
3658        };
3659        (entity, true)
3660    } else {
3661        (entity_cache.get_all_shared(&view_spec.id).await, false)
3662    };
3663    select_query_entities(
3664        entities,
3665        &query,
3666        row_filter,
3667        preordered,
3668        apply_snapshot_limit,
3669    )
3670}
3671
3672fn select_query_entities<E: EntityFields>(
3673    mut entities: Vec<(String, E)>,
3674    query: &SubscriptionQuery,
3675    row_filter: Option<&RowFilter>,
3676    preordered: bool,
3677    apply_snapshot_limit: bool,
3678) -> Vec<(String, E)> {
3679    entities.retain(|(key, data)| query_matches_entity(query, key, data, row_filter));
3680    if !preordered {
3681        entities.sort_by(|left, right| {
3682            let left_seq = left.1.field("_seq").and_then(Value::as_str).unwrap_or("");
3683            let right_seq = right.1.field("_seq").and_then(Value::as_str).unwrap_or("");
3684            let order = if query.after.is_some() {
3685                cmp_seq(left_seq, right_seq)
3686            } else {
3687                cmp_seq(right_seq, left_seq)
3688            };
3689            order.then_with(|| left.0.cmp(&right.0))
3690        });
3691    }
3692
3693    select_window(entities.into_iter(), query, apply_snapshot_limit)
3694}
3695
3696/// The `[skip, skip + take)` window of rows already filtered and in order,
3697/// cut to the snapshot limit when it applies. Takes no row past the window.
3698fn select_window<E>(
3699    rows: impl Iterator<Item = (String, E)>,
3700    query: &SubscriptionQuery,
3701    apply_snapshot_limit: bool,
3702) -> Vec<(String, E)> {
3703    let skip = query.skip.unwrap_or(0);
3704    let mut take = query.take.unwrap_or(usize::MAX);
3705    if apply_snapshot_limit {
3706        if let Some(limit) = query.snapshot_limit {
3707            take = take.min(limit);
3708        }
3709    }
3710    rows.skip(skip).take(take).collect()
3711}
3712
3713/// Whether a whole row belongs to the subscription: in its row scopes (when
3714/// the view is row-scoped), then its key, `partition`, `after` and `filters`.
3715/// The client-supplied predicates only narrow; none of them can admit a row
3716/// the row filter refuses.
3717fn query_matches_entity<E: EntityFields + ?Sized>(
3718    query: &SubscriptionQuery,
3719    key: &str,
3720    data: &E,
3721    row_filter: Option<&RowFilter>,
3722) -> bool {
3723    if row_filter.is_some_and(|filter| !filter.allows(data)) {
3724        return false;
3725    }
3726    if !query.matches_key(key) {
3727        return false;
3728    }
3729    if let Some(partition) = &query.partition {
3730        if value_at_dot_path(data, "_partition").as_deref()
3731            != Some(&Value::String(partition.clone()))
3732        {
3733            return false;
3734        }
3735    }
3736    if let Some(after) = &query.after {
3737        let Some(seq) = data.field("_seq").and_then(Value::as_str) else {
3738            return false;
3739        };
3740        if cmp_seq(seq, after) != std::cmp::Ordering::Greater {
3741            return false;
3742        }
3743    }
3744    query
3745        .filters
3746        .iter()
3747        .all(|(path, expected)| value_at_dot_path(data, path).as_deref() == Some(expected))
3748}
3749
3750fn value_at_dot_path<'a, E: EntityFields + ?Sized>(
3751    entity: &'a E,
3752    path: &str,
3753) -> Option<std::borrow::Cow<'a, Value>> {
3754    lookup(entity, path.split('.'))
3755}
3756
3757#[cfg(test)]
3758mod tests {
3759    use super::*;
3760    use crate::cache::{EntityCacheConfig, PatchOrigin};
3761    use crate::view::{Delivery, Filters, Projection};
3762    use serde_json::json;
3763    use tokio::sync::oneshot;
3764
3765    #[derive(Default)]
3766    struct DelayedUsageEmitter {
3767        started: tokio::sync::Notify,
3768        release: tokio::sync::Notify,
3769        events: tokio::sync::Mutex<Vec<WebSocketUsageEvent>>,
3770    }
3771
3772    #[async_trait::async_trait]
3773    impl WebSocketUsageEmitter for DelayedUsageEmitter {
3774        async fn emit(&self, event: WebSocketUsageEvent) {
3775            self.started.notify_one();
3776            self.release.notified().await;
3777            self.events.lock().await.push(event);
3778        }
3779    }
3780
3781    #[tokio::test]
3782    async fn usage_emitter_handle_waits_for_spawned_emissions() {
3783        let emitter = Arc::new(DelayedUsageEmitter::default());
3784        let handle = UsageEmitterHandle::new(emitter.clone());
3785        handle.emit(WebSocketUsageEvent::ConnectionEstablished {
3786            client_id: "client-1".to_string(),
3787            remote_addr: "127.0.0.1:1234".to_string(),
3788            deployment_id: Some("1".to_string()),
3789            identity: UsageIdentity::default(),
3790        });
3791        emitter.started.notified().await;
3792
3793        let closing_handle = handle.clone();
3794        let closing = tokio::spawn(async move {
3795            closing_handle.close_and_wait().await;
3796        });
3797        tokio::task::yield_now().await;
3798        assert!(!closing.is_finished());
3799
3800        emitter.release.notify_one();
3801        closing.await.expect("usage task wait should finish");
3802        assert_eq!(emitter.events.lock().await.len(), 1);
3803    }
3804
3805    #[test]
3806    fn a_catch_up_carries_only_the_fields_that_changed() {
3807        let base = json!({
3808            "id": 7,
3809            "state": {"deployed": [1, 2, 3], "total": 6},
3810            "results": {"square": null},
3811            "gone": true,
3812        });
3813        let current = json!({
3814            "id": 7,
3815            "state": {"deployed": [1, 2, 4], "total": 6},
3816            "results": {"square": 3},
3817            "fresh": "x",
3818        });
3819        assert_eq!(
3820            changed_fields(&base, &current, &Value::Null),
3821            Some(json!({
3822                // An array that changed is sent whole.
3823                "state": {"deployed": [1, 2, 4]},
3824                "results": {"square": 3},
3825                "fresh": "x",
3826                // A field the entity no longer has is cleared.
3827                "gone": null,
3828            }))
3829        );
3830        assert_eq!(changed_fields(&current, &current, &Value::Null), None);
3831        // Anything that is not an object on both sides is compared whole.
3832        assert_eq!(
3833            changed_fields(&json!(null), &current, &Value::Null),
3834            Some(current.clone())
3835        );
3836    }
3837
3838    #[test]
3839    fn a_catch_up_resends_the_fields_forwarded_patches_set() {
3840        let base = json!({"a": 1, "state": {"x": 1, "y": 1}, "list": [1]});
3841        let mut touched = Value::Null;
3842        mark_fields(&mut touched, &json!({"a": 2, "state": {"x": 2}}));
3843        mark_fields(&mut touched, &json!({"list": [2], "extra": true}));
3844        assert_eq!(
3845            touched,
3846            json!({"a": true, "state": {"x": true}, "list": true, "extra": true})
3847        );
3848        // The entity set `a` and `state.x` back and never kept `extra`: the
3849        // client still holds what the forwarded patches set, so a catch-up
3850        // compared with `base` alone would leave it stale.
3851        let current = json!({"a": 1, "state": {"x": 1, "y": 1}, "list": [1, 2]});
3852        assert_eq!(
3853            changed_fields(&base, &current, &Value::Null),
3854            Some(json!({"list": [1, 2]}))
3855        );
3856        assert_eq!(
3857            changed_fields(&base, &current, &touched),
3858            Some(json!({"a": 1, "state": {"x": 1}, "list": [1, 2], "extra": null}))
3859        );
3860        // A patch whose data is not an object marks everything.
3861        mark_fields(&mut touched, &json!(null));
3862        assert_eq!(
3863            changed_fields(&base, &current, &touched),
3864            Some(current.clone())
3865        );
3866    }
3867
3868    fn list_spec() -> ViewSpec {
3869        ViewSpec {
3870            id: "Thing/list".to_string(),
3871            export: "Thing".to_string(),
3872            mode: Mode::List,
3873            wire_format: Default::default(),
3874            projection: Projection::all(),
3875            filters: Filters::all(),
3876            delivery: Delivery::default(),
3877            pipeline: None,
3878            source_view: None,
3879        }
3880    }
3881
3882    /// Plan the whole window the way `emit_collection_delta` does, so a test
3883    /// can assert on what a subscriber is actually sent.
3884    fn plan_window(
3885        current_keys: &[&str],
3886        next_keys: &[&str],
3887        envelope_key: &str,
3888        is_derived: bool,
3889        op: &str,
3890    ) -> Vec<(String, MemberAction)> {
3891        plan_window_with_undelivered(current_keys, &[], next_keys, envelope_key, is_derived, op)
3892    }
3893
3894    /// [`plan_window`] for a subscriber that was never sent `undelivered`.
3895    fn plan_window_with_undelivered(
3896        current_keys: &[&str],
3897        undelivered: &[&str],
3898        next_keys: &[&str],
3899        envelope_key: &str,
3900        is_derived: bool,
3901        op: &str,
3902    ) -> Vec<(String, MemberAction)> {
3903        next_keys
3904            .iter()
3905            .map(|key| {
3906                let in_window = current_keys.contains(key);
3907                let held = in_window && !undelivered.contains(key);
3908                (
3909                    (*key).to_string(),
3910                    member_action(in_window, held, *key == envelope_key, is_derived, op),
3911                )
3912            })
3913            .collect()
3914    }
3915
3916    #[test]
3917    fn a_window_member_the_client_never_received_is_sent_whole() {
3918        // snapshotLimit cut the window [4, 3, 2, 1] to [4, 3]. "1" still
3919        // counts for positions, but a patch for it would reach a client with
3920        // nothing to merge into.
3921        let plan = plan_window_with_undelivered(
3922            &["4", "3", "2", "1"],
3923            &["2", "1"],
3924            &["1", "4", "3", "2"],
3925            "1",
3926            false,
3927            "patch",
3928        );
3929        assert_eq!(
3930            plan,
3931            vec![
3932                ("1".to_string(), MemberAction::Upsert),
3933                ("4".to_string(), MemberAction::Skip),
3934                ("3".to_string(), MemberAction::Skip),
3935                // Unchanged and never sent: it waits for its own change
3936                // rather than undoing the snapshot limit.
3937                ("2".to_string(), MemberAction::Skip),
3938            ]
3939        );
3940    }
3941
3942    #[test]
3943    fn a_held_member_still_rides_its_patch_after_a_truncated_snapshot() {
3944        let plan = plan_window_with_undelivered(
3945            &["4", "3", "2", "1"],
3946            &["2", "1"],
3947            &["4", "3", "2", "1"],
3948            "4",
3949            false,
3950            "patch",
3951        );
3952        assert_eq!(plan[0], ("4".to_string(), MemberAction::ForwardPatch));
3953    }
3954
3955    #[test]
3956    fn coalescing_sends_no_remove_for_a_key_the_client_never_received() {
3957        let current = vec![
3958            ("held".to_string(), json!({"count": 1, "_seq": "10:000001"})),
3959            (
3960                "unsent".to_string(),
3961                json!({"count": 1, "_seq": "10:000001"}),
3962            ),
3963            ("gone".to_string(), json!({"count": 1, "_seq": "10:000001"})),
3964        ];
3965        let next = vec![
3966            ("held".to_string(), json!({"count": 1, "_seq": "10:000001"})),
3967            (
3968                "unsent".to_string(),
3969                json!({"count": 2, "_seq": "10:000003"}),
3970            ),
3971        ];
3972        let pending = HashMap::from([
3973            (
3974                "unsent".to_string(),
3975                list_message("unsent", "patch", "10:000003"),
3976            ),
3977            (
3978                "gone".to_string(),
3979                list_message("gone", "delete", "10:000002"),
3980            ),
3981        ]);
3982        let undelivered = HashSet::from(["unsent".to_string(), "gone".to_string()]);
3983
3984        assert_eq!(
3985            plan_coalesced_collection_delta(
3986                &shared(&current),
3987                &shared(&next),
3988                &pending,
3989                &undelivered
3990            ),
3991            vec![CollectionChange {
3992                op: "upsert",
3993                key: "unsent".to_string(),
3994                data: Some(SharedEntity::new(json!({"count": 2, "_seq": "10:000003"}))),
3995                seq: Some("10:000003".to_string()),
3996            }]
3997        );
3998    }
3999
4000    #[test]
4001    fn a_reordered_window_sends_only_the_mutated_entity() {
4002        // A `_seq`-descending list: mutating "1" moves it to the front and
4003        // shifts every other key down one. Only "1" changed, so only "1" is
4004        // sent — and it rides its own patch, not a full entity.
4005        let current = ["4", "3", "2", "1"];
4006        let next = ["1", "4", "3", "2"];
4007        let plan = plan_window(&current, &next, "1", false, "patch");
4008
4009        assert_eq!(
4010            plan,
4011            vec![
4012                ("1".to_string(), MemberAction::ForwardPatch),
4013                ("4".to_string(), MemberAction::Skip),
4014                ("3".to_string(), MemberAction::Skip),
4015                ("2".to_string(), MemberAction::Skip),
4016            ]
4017        );
4018    }
4019
4020    #[test]
4021    fn a_patch_the_whole_entity_already_carried_is_not_sent_again() {
4022        let mut sent = SentWhole::default();
4023        sent.sent("k", Some(&json!("e1:5")));
4024        // Older or equal patches were folded into what was sent.
4025        assert!(sent.covers("k", || Some(json!("e1:4"))));
4026        assert!(sent.covers("k", || Some(json!("e1:5"))));
4027        assert!(sent.holds("k", Some(&json!("e1:5"))));
4028        // A newer one is forwarded and ends the entry.
4029        assert!(!sent.covers("k", || Some(json!("e1:6"))));
4030        assert!(!sent.covers("k", || Some(json!("e1:3"))));
4031        // Another epoch or no version is never taken as covered.
4032        sent.sent("k", Some(&json!("e1:5")));
4033        assert!(!sent.covers("k", || Some(json!("e2:1"))));
4034        sent.sent("k", Some(&json!("e1:5")));
4035        assert!(!sent.covers("k", || None));
4036        // A key never sent whole reads no patch.
4037        assert!(!sent.covers("other", || panic!("read only for keys sent whole")));
4038        // An unversioned upsert leaves nothing to compare against.
4039        sent.sent("k", Some(&json!("e1:5")));
4040        sent.sent("k", None);
4041        assert!(!sent.holds("k", None));
4042        assert!(!sent.covers("k", || Some(json!("e1:1"))));
4043    }
4044
4045    #[test]
4046    fn a_key_entering_the_window_gets_the_whole_entity() {
4047        // "5" has no local state for the subscriber to merge a patch into.
4048        let plan = plan_window(&["4", "3"], &["5", "4", "3"], "5", false, "patch");
4049        assert_eq!(
4050            plan,
4051            vec![
4052                ("5".to_string(), MemberAction::Upsert),
4053                ("4".to_string(), MemberAction::Skip),
4054                ("3".to_string(), MemberAction::Skip),
4055            ]
4056        );
4057    }
4058
4059    #[test]
4060    fn derived_views_still_send_whole_entities() {
4061        // The patch on the bus is scoped to the source view, so a derived
4062        // subscription cannot forward it verbatim (A4-150).
4063        let plan = plan_window(&["1", "2"], &["1", "2"], "1", true, "patch");
4064        assert_eq!(
4065            plan,
4066            vec![
4067                ("1".to_string(), MemberAction::Upsert),
4068                ("2".to_string(), MemberAction::Skip),
4069            ]
4070        );
4071    }
4072
4073    #[test]
4074    fn a_delete_envelope_never_forwards_a_patch() {
4075        // A surviving key on a delete envelope carries no mergeable patch.
4076        let plan = plan_window(&["1", "2"], &["1", "2"], "1", false, "delete");
4077        assert_eq!(
4078            plan,
4079            vec![
4080                ("1".to_string(), MemberAction::Upsert),
4081                ("2".to_string(), MemberAction::Skip),
4082            ]
4083        );
4084    }
4085
4086    #[test]
4087    fn an_unchanged_window_sends_one_frame_not_a_broadcast() {
4088        // The regression this guards: 500 members used to mean 500 full
4089        // entities on the wire for a single mutation.
4090        let keys: Vec<String> = (0..500).map(|index| index.to_string()).collect();
4091        let refs: Vec<&str> = keys.iter().map(String::as_str).collect();
4092        let plan = plan_window(&refs, &refs, "250", false, "patch");
4093
4094        let sent = plan
4095            .iter()
4096            .filter(|(_, action)| *action != MemberAction::Skip)
4097            .count();
4098        assert_eq!(sent, 1);
4099        assert_eq!(plan[250].1, MemberAction::ForwardPatch);
4100    }
4101
4102    fn shared(rows: &[(String, Value)]) -> Vec<(String, SharedEntity)> {
4103        rows.iter()
4104            .map(|(key, data)| (key.clone(), SharedEntity::new(data.clone())))
4105            .collect()
4106    }
4107
4108    fn list_message(key: &str, op: &str, seq: &str) -> Arc<BusMessage> {
4109        Arc::new(BusMessage {
4110            row_scope: None,
4111            key: key.to_string(),
4112            entity: "Thing/list".to_string(),
4113            payload: Arc::new(Bytes::from(
4114                serde_json::to_vec(&json!({
4115                    "entity": "Thing/list",
4116                    "op": op,
4117                    "key": key,
4118                    "seq": seq,
4119                    "data": {},
4120                }))
4121                .unwrap(),
4122            )),
4123        })
4124    }
4125
4126    #[test]
4127    fn coalescing_emits_only_the_final_full_state_per_changed_key() {
4128        let current = vec![
4129            ("a".to_string(), json!({"count": 1, "_seq": "10:000001"})),
4130            ("b".to_string(), json!({"count": 1, "_seq": "10:000001"})),
4131            (
4132                "stable".to_string(),
4133                json!({"count": 1, "_seq": "10:000001"}),
4134            ),
4135        ];
4136        let next = vec![
4137            ("a".to_string(), json!({"count": 3, "_seq": "10:000004"})),
4138            ("c".to_string(), json!({"count": 1, "_seq": "10:000003"})),
4139            (
4140                "stable".to_string(),
4141                json!({"count": 1, "_seq": "10:000001"}),
4142            ),
4143        ];
4144        let pending = HashMap::from([
4145            ("a".to_string(), list_message("a", "patch", "10:000004")),
4146            ("b".to_string(), list_message("b", "delete", "10:000002")),
4147            ("c".to_string(), list_message("c", "patch", "10:000003")),
4148        ]);
4149
4150        assert_eq!(
4151            plan_coalesced_collection_delta(
4152                &shared(&current),
4153                &shared(&next),
4154                &pending,
4155                &HashSet::new()
4156            ),
4157            vec![
4158                CollectionChange {
4159                    op: "delete",
4160                    key: "b".to_string(),
4161                    data: None,
4162                    seq: Some("10:000002".to_string()),
4163                },
4164                CollectionChange {
4165                    op: "upsert",
4166                    key: "a".to_string(),
4167                    data: Some(SharedEntity::new(json!({"count": 3, "_seq": "10:000004"}))),
4168                    seq: Some("10:000004".to_string()),
4169                },
4170                CollectionChange {
4171                    op: "upsert",
4172                    key: "c".to_string(),
4173                    data: Some(SharedEntity::new(json!({"count": 1, "_seq": "10:000003"}))),
4174                    seq: Some("10:000003".to_string()),
4175                },
4176            ]
4177        );
4178    }
4179
4180    #[test]
4181    fn a_lagged_snapshotless_subscription_gets_a_fatal_retryable_error() {
4182        let issue = SocketIssueMessage::subscription_lagged("balances".to_string(), 42);
4183        assert_eq!(issue.code, "subscription-lagged");
4184        assert!(issue.retryable);
4185        assert!(issue.fatal);
4186        assert!(issue.message.contains("42"));
4187        assert!(issue
4188            .suggested_action
4189            .unwrap()
4190            .contains("snapshots enabled"));
4191
4192        let append = SocketIssueMessage::append_subscription_lagged("trades".to_string(), 9);
4193        assert!(append.fatal);
4194        assert!(append.suggested_action.unwrap().contains("retained replay"));
4195    }
4196
4197    #[test]
4198    fn snapshot_batches_share_identity_and_completion() {
4199        let entities = ["one", "two", "three"]
4200            .map(|key| (key.to_string(), SharedEntity::new(json!({"key": key}))));
4201        let wire_format = WireFormat::default();
4202        let batches = create_snapshot_batches(
4203            &entities,
4204            &wire_format,
4205            SnapshotMetadata {
4206                subscription_id: "sub-1",
4207                snapshot_id: "snapshot-1",
4208                authoritative: true,
4209                mode: Mode::List,
4210                view_id: "Thing/list",
4211                key: None,
4212            },
4213            &SnapshotBatchConfig {
4214                initial_batch_size: 2,
4215                subsequent_batch_size: 1,
4216            },
4217        );
4218        assert_eq!(batches.len(), 2);
4219        assert!(batches.iter().all(|batch| batch.subscription_id == "sub-1"));
4220        assert!(batches
4221            .iter()
4222            .all(|batch| batch.snapshot_id == "snapshot-1"));
4223        assert!(!batches[0].complete);
4224        assert!(batches[1].complete);
4225        assert!(batches.iter().all(|batch| batch.authoritative));
4226    }
4227
4228    #[test]
4229    fn empty_incremental_snapshot_is_explicitly_non_authoritative() {
4230        let wire_format = WireFormat::default();
4231        let batches = create_snapshot_batches(
4232            &[],
4233            &wire_format,
4234            SnapshotMetadata {
4235                subscription_id: "sub-1",
4236                snapshot_id: "snapshot-1",
4237                authoritative: false,
4238                mode: Mode::State,
4239                view_id: "Thing/state",
4240                key: Some("missing"),
4241            },
4242            &SnapshotBatchConfig {
4243                initial_batch_size: 1,
4244                subsequent_batch_size: 1,
4245            },
4246        );
4247        assert_eq!(batches.len(), 1);
4248        assert!(!batches[0].authoritative);
4249        assert!(batches[0].complete);
4250        assert_eq!(batches[0].key, Some("missing"));
4251    }
4252
4253    fn wide_ints() -> WireFormat {
4254        WireFormat {
4255            wide_int_paths: vec![
4256                vec!["amount".to_string()],
4257                vec!["trades".to_string(), "price".to_string()],
4258            ],
4259        }
4260    }
4261
4262    fn sample_rows() -> Vec<(String, SharedEntity)> {
4263        (0..7)
4264            .map(|index| {
4265                (
4266                    index.to_string(),
4267                    SharedEntity::new(json!({
4268                        "_seq": format!("10:{index:012}"),
4269                        "_version": format!("e:{index}"),
4270                        "amount": u64::MAX - index,
4271                        "label": "Ï€ \"quoted\" \n",
4272                        "trades": [{"price": index, "side": "buy"}, {"price": -1}],
4273                    })),
4274                )
4275            })
4276            .collect()
4277    }
4278
4279    /// Snapshot frames serialized from shared rows are byte for byte the
4280    /// frames built from formatted copies of the rows.
4281    #[test]
4282    fn snapshot_batches_serialize_as_frames_of_formatted_copies() {
4283        use crate::websocket::frame::{SnapshotEntity, SnapshotFrame};
4284        let rows = sample_rows();
4285        for (rows, key, wire_format, (initial, subsequent)) in [
4286            (&rows[..], None, wide_ints(), (2, 3)),
4287            (&rows[..], Some("3"), WireFormat::default(), (50, 100)),
4288            (&rows[..1], Some("0"), wide_ints(), (1, 1)),
4289            (&rows[..0], None, wide_ints(), (2, 3)),
4290        ] {
4291            let metadata = SnapshotMetadata {
4292                subscription_id: "sub-1",
4293                snapshot_id: "snapshot-1",
4294                authoritative: true,
4295                mode: Mode::List,
4296                view_id: "Thing/list",
4297                key,
4298            };
4299            let config = SnapshotBatchConfig {
4300                initial_batch_size: initial,
4301                subsequent_batch_size: subsequent,
4302            };
4303            let batches = create_snapshot_batches(rows, &wire_format, metadata, &config);
4304            let mut offset = 0;
4305            for batch in &batches {
4306                let copies = rows[offset..offset + batch.data.rows.len()]
4307                    .iter()
4308                    .map(|(key, entity)| SnapshotEntity {
4309                        key: key.clone(),
4310                        data: wire_value(entity, &wire_format),
4311                    })
4312                    .collect();
4313                offset += batch.data.rows.len();
4314                let frame = SnapshotFrame {
4315                    protocol_version: PROTOCOL_VERSION,
4316                    subscription_id: "sub-1".to_string(),
4317                    snapshot_id: "snapshot-1".to_string(),
4318                    authoritative: true,
4319                    mode: Mode::List,
4320                    export: "Thing/list".to_string(),
4321                    op: "snapshot",
4322                    key: key.map(str::to_string),
4323                    data: copies,
4324                    complete: batch.complete,
4325                };
4326                assert_eq!(
4327                    serde_json::to_string(batch).unwrap(),
4328                    serde_json::to_string(&frame).unwrap()
4329                );
4330            }
4331            assert_eq!(offset, rows.len());
4332        }
4333    }
4334
4335    /// A membership frame serialized from a shared entity is byte for byte
4336    /// the frame built from a formatted copy of it.
4337    #[test]
4338    fn entity_frames_serialize_as_frames_of_formatted_copies() {
4339        for (_, entity) in sample_rows() {
4340            for (wire_format, seq) in [
4341                (wide_ints(), Some("10:000000000001")),
4342                (WireFormat::default(), None),
4343            ] {
4344                let frame = ScopedEntityFrame {
4345                    protocol_version: PROTOCOL_VERSION,
4346                    subscription_id: "sub-1",
4347                    mode: Mode::List,
4348                    export: "Thing/latest",
4349                    op: "upsert",
4350                    key: "7",
4351                    data: WireEntity {
4352                        entity: &entity,
4353                        wire_format: &wire_format,
4354                    },
4355                    seq,
4356                };
4357                let copy = Frame::scoped(
4358                    "sub-1",
4359                    Mode::List,
4360                    "Thing/latest",
4361                    "upsert",
4362                    "7",
4363                    wire_value(&entity, &wire_format),
4364                    seq.map(str::to_string),
4365                );
4366                assert_eq!(
4367                    serde_json::to_string(&frame).unwrap(),
4368                    serde_json::to_string(&copy).unwrap()
4369                );
4370            }
4371        }
4372    }
4373
4374    #[test]
4375    fn lag_recovery_is_authoritative_for_an_after_query() {
4376        let subscription = Subscription {
4377            protocol_version: PROTOCOL_VERSION,
4378            subscription_id: "sub-1".to_string(),
4379            query: SubscriptionQuery {
4380                view: "Thing/list".to_string(),
4381                after: Some("40:000000000010".to_string()),
4382                ..Default::default()
4383            },
4384            snapshot: Default::default(),
4385        };
4386
4387        assert!(!SnapshotPurpose::Initial.authoritative(&subscription));
4388        assert!(SnapshotPurpose::Recovery.authoritative(&subscription));
4389    }
4390
4391    #[test]
4392    fn dot_path_filters_are_exact_and_type_sensitive() {
4393        let mut query = SubscriptionQuery {
4394            view: "Thing/list".to_string(),
4395            ..Default::default()
4396        };
4397        query
4398            .filters
4399            .insert("state.status".to_string(), json!("open"));
4400        query.filters.insert("metrics.count".to_string(), json!(2));
4401        assert!(query_matches_entity(
4402            &query,
4403            "one",
4404            &json!({"state": {"status": "open"}, "metrics": {"count": 2}}),
4405            None,
4406        ));
4407        assert!(!query_matches_entity(
4408            &query,
4409            "one",
4410            &json!({"state": {"status": "open"}, "metrics": {"count": "2"}}),
4411            None,
4412        ));
4413    }
4414
4415    #[test]
4416    fn take_and_skip_define_independent_deterministic_windows() {
4417        let entities: Vec<_> = (1..=6)
4418            .map(|id| {
4419                (
4420                    id.to_string(),
4421                    json!({"id": id, "_seq": format!("10:{id:012}")}),
4422                )
4423            })
4424            .collect();
4425        let first = SubscriptionQuery {
4426            view: "Thing/list".to_string(),
4427            take: Some(2),
4428            skip: Some(0),
4429            ..Default::default()
4430        };
4431        let second = SubscriptionQuery {
4432            skip: Some(2),
4433            ..first.clone()
4434        };
4435        let first_keys: Vec<_> =
4436            select_query_entities(entities.clone(), &first, None, false, false)
4437                .into_iter()
4438                .map(|(key, _)| key)
4439                .collect();
4440        let second_keys: Vec<_> = select_query_entities(entities, &second, None, false, false)
4441            .into_iter()
4442            .map(|(key, _)| key)
4443            .collect();
4444        assert_eq!(first_keys, ["6", "5"]);
4445        assert_eq!(second_keys, ["4", "3"]);
4446    }
4447
4448    #[tokio::test]
4449    async fn state_receiver_is_installed_before_snapshot_awaits() {
4450        let bus = BusManager::new();
4451        let (snapshot_started_tx, snapshot_started_rx) = oneshot::channel();
4452        let (release_snapshot_tx, release_snapshot_rx) = oneshot::channel();
4453        let bus_for_task = bus.clone();
4454        let task = tokio::spawn(async move {
4455            subscribe_state_then_snapshot(&bus_for_task, "Thing/state", "one", || async move {
4456                snapshot_started_tx.send(()).unwrap();
4457                release_snapshot_rx.await.unwrap();
4458            })
4459            .await
4460            .0
4461        });
4462        snapshot_started_rx.await.unwrap();
4463        bus.publish_state(
4464            "Thing/state",
4465            "one",
4466            Arc::new(Bytes::from_static(br#"{"op":"patch"}"#)),
4467            None,
4468        )
4469        .await;
4470        release_snapshot_tx.send(()).unwrap();
4471        let mut receiver = task.await.unwrap();
4472        receiver.changed().await.unwrap();
4473        assert!(!receiver.borrow().payload.is_empty());
4474    }
4475
4476    async fn assert_list_receiver_precedes_snapshot(view: &'static str) {
4477        let bus = BusManager::new();
4478        let (snapshot_started_tx, snapshot_started_rx) = oneshot::channel();
4479        let (release_snapshot_tx, release_snapshot_rx) = oneshot::channel();
4480        let bus_for_task = bus.clone();
4481        let task = tokio::spawn(async move {
4482            subscribe_list_then_snapshot(&bus_for_task, view, || async move {
4483                snapshot_started_tx.send(()).unwrap();
4484                release_snapshot_rx.await.unwrap();
4485            })
4486            .await
4487            .0
4488        });
4489        snapshot_started_rx.await.unwrap();
4490        bus.publish_list(
4491            view,
4492            Arc::new(BusMessage {
4493                row_scope: None,
4494                key: "one".to_string(),
4495                entity: view.to_string(),
4496                payload: Arc::new(Bytes::from_static(br#"{"op":"patch"}"#)),
4497            }),
4498        )
4499        .await;
4500        release_snapshot_tx.send(()).unwrap();
4501        let mut receiver = task.await.unwrap();
4502        assert_eq!(receiver.recv().await.unwrap().key, "one");
4503    }
4504
4505    #[tokio::test]
4506    async fn list_receiver_is_installed_before_snapshot() {
4507        assert_list_receiver_precedes_snapshot("Thing/list").await;
4508    }
4509
4510    #[tokio::test]
4511    async fn append_receiver_is_installed_before_snapshot() {
4512        assert_list_receiver_precedes_snapshot("Thing/append").await;
4513    }
4514
4515    #[tokio::test]
4516    async fn derived_source_receiver_is_installed_before_snapshot() {
4517        assert_list_receiver_precedes_snapshot("Thing/list-source").await;
4518    }
4519
4520    /// A sorted derived view is read only as far as the window reaches, and
4521    /// gives exactly the rows filtering and windowing all of it gives.
4522    #[tokio::test]
4523    async fn sorted_derived_reads_take_the_window_of_the_whole_order() {
4524        use crate::materialized_view::{SortConfig, SortOrder, ViewPipeline};
4525        use crate::sorted_cache::{SortOrder as CacheOrder, SortedViewCache};
4526        let mut sorted =
4527            SortedViewCache::new("Thing/top".into(), vec!["score".into()], CacheOrder::Desc);
4528        for id in 0..40u64 {
4529            sorted.upsert(
4530                id.to_string(),
4531                json!({
4532                    "_seq": format!("{}:0", 100 + id * 7 % 40),
4533                    "score": (id * 13 % 17).to_string(),
4534                    "owner": if id % 3 == 0 { "alice" } else { "bob" },
4535                    "_partition": if id % 2 == 0 { "even" } else { "odd" },
4536                }),
4537            );
4538        }
4539        let whole = sorted.ordered_entities();
4540        let mut spec = list_spec();
4541        spec.id = "Thing/top".into();
4542        spec.source_view = Some("Thing/list".into());
4543        let caches = Arc::new(tokio::sync::RwLock::new(HashMap::from([(
4544            spec.id.clone(),
4545            sorted,
4546        )])));
4547        let cache = EntityCache::new();
4548        for limit in [None, Some(5)] {
4549            spec.pipeline = Some(ViewPipeline {
4550                filter: None,
4551                sort: Some(SortConfig {
4552                    field_path: vec!["score".into()],
4553                    order: SortOrder::Desc,
4554                }),
4555                limit,
4556            });
4557            for skip in [None, Some(0), Some(3), Some(50)] {
4558                for take in [None, Some(1), Some(4), Some(100)] {
4559                    for (owner, after, partition) in [
4560                        (None, None, None),
4561                        (Some("alice"), None, None),
4562                        (None, Some("120:0"), Some("even")),
4563                    ] {
4564                        let mut query = SubscriptionQuery {
4565                            view: spec.id.clone(),
4566                            skip,
4567                            take,
4568                            after: after.map(str::to_string),
4569                            partition: partition.map(str::to_string),
4570                            snapshot_limit: Some(2),
4571                            ..Default::default()
4572                        };
4573                        if let Some(owner) = owner {
4574                            query.filters.insert("owner".into(), json!(owner));
4575                        }
4576                        for apply_snapshot_limit in [false, true] {
4577                            let rows = load_query_entities(
4578                                &cache,
4579                                Some(caches.clone()),
4580                                &spec,
4581                                &query,
4582                                None,
4583                                apply_snapshot_limit,
4584                            )
4585                            .await;
4586                            let mut expected_query = query.clone();
4587                            if let Some(limit) = limit {
4588                                expected_query.take = Some(take.unwrap_or(limit).min(limit));
4589                            }
4590                            let expected = select_query_entities(
4591                                whole.clone(),
4592                                &expected_query,
4593                                None,
4594                                true,
4595                                apply_snapshot_limit,
4596                            );
4597                            assert_eq!(rows, expected, "{query:?} {limit:?}");
4598                        }
4599                    }
4600                }
4601            }
4602        }
4603    }
4604
4605    #[tokio::test]
4606    async fn unsorted_derived_reads_preserve_filters_order_and_snapshot_window() {
4607        use crate::materialized_view::{CompareOp, FilterConfig, ViewPipeline};
4608        let cache = EntityCache::new();
4609        for id in 1..=6 {
4610            cache
4611                .upsert(
4612                    "Thing/list",
4613                    &id.to_string(),
4614                    json!({
4615                        "_seq": format!("{}:0", id * 10), "active": id != 6,
4616                        "owner": "alice", "id": id
4617                    }),
4618                )
4619                .await;
4620        }
4621        let mut spec = list_spec();
4622        spec.id = "Thing/custom".into();
4623        spec.source_view = Some("Thing/list".into());
4624        spec.pipeline = Some(ViewPipeline::default());
4625        let caches = Arc::new(tokio::sync::RwLock::new(HashMap::new()));
4626        let mut query = SubscriptionQuery {
4627            view: spec.id.clone(),
4628            ..Default::default()
4629        };
4630        let rows =
4631            load_query_entities(&cache, Some(caches.clone()), &spec, &query, None, false).await;
4632        assert_eq!(
4633            rows.iter().map(|(key, _)| key.as_str()).collect::<Vec<_>>(),
4634            ["6", "5", "4", "3", "2", "1"]
4635        );
4636        spec.pipeline.as_mut().unwrap().filter = Some(FilterConfig {
4637            field_path: vec!["active".into()],
4638            op: CompareOp::Eq,
4639            value: json!(true),
4640        });
4641        query.filters.insert("owner".into(), json!("alice"));
4642        query.skip = Some(1);
4643        query.take = Some(3);
4644        query.snapshot_limit = Some(1);
4645        let rows =
4646            load_query_entities(&cache, Some(caches.clone()), &spec, &query, None, false).await;
4647        assert_eq!(
4648            rows.iter().map(|(key, _)| key.as_str()).collect::<Vec<_>>(),
4649            ["4", "3", "2"]
4650        );
4651        assert_eq!(
4652            load_query_entities(&cache, Some(caches.clone()), &spec, &query, None, true)
4653                .await
4654                .len(),
4655            1
4656        );
4657        query.skip = None;
4658        query.after = Some("20:0".into());
4659        let rows =
4660            load_query_entities(&cache, Some(caches.clone()), &spec, &query, None, false).await;
4661        assert_eq!(
4662            rows.iter().map(|(key, _)| key.as_str()).collect::<Vec<_>>(),
4663            ["3", "4", "5"]
4664        );
4665        query.filters.insert("owner".into(), json!("bob"));
4666        assert!(
4667            load_query_entities(&cache, Some(caches), &spec, &query, None, false)
4668                .await
4669                .is_empty()
4670        );
4671    }
4672
4673    #[tokio::test]
4674    async fn snapshot_limit_does_not_change_live_take_skip_membership() {
4675        let cache = EntityCache::with_config(EntityCacheConfig {
4676            max_entities_per_view: 10,
4677            ..Default::default()
4678        });
4679        for id in 1..=4 {
4680            cache
4681                .upsert(
4682                    "Thing/list",
4683                    &id.to_string(),
4684                    json!({"_seq": format!("10:{id:012}")}),
4685                )
4686                .await;
4687        }
4688        let query = SubscriptionQuery {
4689            view: "Thing/list".to_string(),
4690            take: Some(3),
4691            skip: Some(1),
4692            snapshot_limit: Some(1),
4693            ..Default::default()
4694        };
4695        let live = load_query_entities(&cache, None, &list_spec(), &query, None, false).await;
4696        let snapshot = load_query_entities(&cache, None, &list_spec(), &query, None, true).await;
4697        assert_eq!(live.len(), 3);
4698        assert_eq!(snapshot.len(), 1);
4699    }
4700
4701    #[test]
4702    fn fixture_manifest_covers_required_conformance_cases() {
4703        let manifest: Value = serde_json::from_str(include_str!(
4704            "../../../../tests/fixtures/websocket-v2/manifest.json"
4705        ))
4706        .unwrap();
4707        let names: HashSet<_> = manifest["fixtures"]
4708            .as_array()
4709            .unwrap()
4710            .iter()
4711            .filter_map(Value::as_str)
4712            .collect();
4713        for required in [
4714            "keyed-state.json",
4715            "list-windows.json",
4716            "filters.json",
4717            "multi-batch-authoritative.json",
4718            "empty-snapshot.json",
4719            "remove.json",
4720            "delete.json",
4721            "incremental-snapshot.json",
4722            "reconnect-replacement.json",
4723            "errors.json",
4724            "whole-entities.json",
4725        ] {
4726            assert!(names.contains(required), "missing fixture {required}");
4727        }
4728
4729        for document in [
4730            include_str!("../../../../tests/fixtures/websocket-v2/keyed-state.json"),
4731            include_str!("../../../../tests/fixtures/websocket-v2/list-windows.json"),
4732            include_str!("../../../../tests/fixtures/websocket-v2/filters.json"),
4733            include_str!("../../../../tests/fixtures/websocket-v2/multi-batch-authoritative.json"),
4734            include_str!("../../../../tests/fixtures/websocket-v2/empty-snapshot.json"),
4735            include_str!("../../../../tests/fixtures/websocket-v2/remove.json"),
4736            include_str!("../../../../tests/fixtures/websocket-v2/delete.json"),
4737            include_str!("../../../../tests/fixtures/websocket-v2/incremental-snapshot.json"),
4738            include_str!("../../../../tests/fixtures/websocket-v2/reconnect-replacement.json"),
4739            include_str!("../../../../tests/fixtures/websocket-v2/errors.json"),
4740            include_str!("../../../../tests/fixtures/websocket-v2/whole-entities.json"),
4741        ] {
4742            let fixture: Value = serde_json::from_str(document).unwrap();
4743            assert!(fixture["name"].is_string());
4744        }
4745    }
4746
4747    fn append_frame(key: &str, data: Value) -> Arc<Bytes> {
4748        let frame = json!({
4749            "entity": "Trade/append",
4750            "op": "patch",
4751            "key": key,
4752            "offset": 7,
4753            "data": data,
4754        });
4755        Arc::new(Bytes::from(serde_json::to_vec(&frame).unwrap()))
4756    }
4757
4758    /// A replayable subscription must honour the same predicates a live
4759    /// collection subscription does; otherwise a filtered consumer receives
4760    /// events outside the query it asked for.
4761    #[test]
4762    fn replay_delivery_applies_key_partition_and_filters() {
4763        let matching = append_frame("pool1", json!({"_partition": "us", "side": "buy"}));
4764        let other_partition = append_frame("pool1", json!({"_partition": "eu", "side": "buy"}));
4765        let other_side = append_frame("pool1", json!({"_partition": "us", "side": "sell"}));
4766
4767        let unfiltered = SubscriptionQuery {
4768            view: "Trade/append".to_string(),
4769            ..Default::default()
4770        };
4771        assert!(live_frame_matches(&unfiltered, "pool1", &matching));
4772        assert!(live_frame_matches(&unfiltered, "pool9", &matching));
4773
4774        let keyed = SubscriptionQuery {
4775            view: "Trade/append".to_string(),
4776            key: Some("pool1".to_string()),
4777            ..Default::default()
4778        };
4779        assert!(live_frame_matches(&keyed, "pool1", &matching));
4780        assert!(!live_frame_matches(&keyed, "pool2", &matching));
4781
4782        let partitioned = SubscriptionQuery {
4783            view: "Trade/append".to_string(),
4784            partition: Some("us".to_string()),
4785            ..Default::default()
4786        };
4787        assert!(live_frame_matches(&partitioned, "pool1", &matching));
4788        assert!(!live_frame_matches(&partitioned, "pool1", &other_partition));
4789
4790        let filtered = SubscriptionQuery {
4791            view: "Trade/append".to_string(),
4792            filters: [("side".to_string(), json!("buy"))].into_iter().collect(),
4793            ..Default::default()
4794        };
4795        assert!(live_frame_matches(&filtered, "pool1", &matching));
4796        assert!(!live_frame_matches(&filtered, "pool1", &other_side));
4797    }
4798
4799    #[test]
4800    fn a_frame_without_decodable_data_does_not_satisfy_a_filter() {
4801        let filtered = SubscriptionQuery {
4802            view: "Trade/append".to_string(),
4803            filters: [("side".to_string(), json!("buy"))].into_iter().collect(),
4804            ..Default::default()
4805        };
4806        let garbage = Arc::new(Bytes::from_static(b"not json"));
4807        assert!(!live_frame_matches(&filtered, "pool1", &garbage));
4808    }
4809
4810    /// The recovery cursor must be the last offset delivered *before* the
4811    /// gap. Reporting the newest offset seen would step the consumer over
4812    /// the skipped records permanently.
4813    #[test]
4814    fn replay_lagged_recovers_from_before_the_gap() {
4815        let epoch = crate::journal::JournalEpoch::new();
4816        let issue = SocketIssueMessage::replay_lagged(
4817            Some("trades".to_string()),
4818            37,
4819            Some(crate::journal::Cursor {
4820                epoch: epoch.clone(),
4821                offset: 4180,
4822            }),
4823        );
4824        assert_eq!(issue.code, "replay-lagged");
4825        assert_eq!(issue.recover_from, Some(format!("{epoch}:4180")));
4826        assert!(
4827            issue.suggested_action.unwrap().contains("4180"),
4828            "the consumer is told exactly which cursor recovers the gap"
4829        );
4830
4831        // Nothing delivered yet: there is no pre-gap offset, so the whole
4832        // retained window is the recovery.
4833        let from_scratch = SocketIssueMessage::replay_lagged(Some("trades".to_string()), 9, None);
4834        assert_eq!(from_scratch.recover_from, None);
4835        assert!(from_scratch
4836            .suggested_action
4837            .unwrap()
4838            .contains("without `after`"));
4839    }
4840
4841    fn bus_message(key: &str) -> Arc<BusMessage> {
4842        Arc::new(BusMessage {
4843            row_scope: None,
4844            key: key.to_string(),
4845            entity: "Trade/append".to_string(),
4846            payload: Arc::new(Bytes::from_static(b"{}")),
4847        })
4848    }
4849
4850    /// A long replay must keep the bus drained. The broadcast buffer is
4851    /// bounded, so a busy view would otherwise lap the replay and the first
4852    /// live `recv` would return `Lagged` — telling the client to resubscribe,
4853    /// starting another long replay, which laps again.
4854    #[tokio::test]
4855    async fn draining_during_a_replay_keeps_a_busy_view_from_lapping_it() {
4856        let (sender, mut receiver) = broadcast::channel::<Arc<BusMessage>>(16);
4857        let mut pending = VecDeque::new();
4858        let mut lagged = None;
4859
4860        // Publish more than the channel holds, draining as a replay would
4861        // between sends.
4862        for index in 0..48 {
4863            sender.send(bus_message(&format!("k{index}"))).unwrap();
4864            drain_available(&mut receiver, &mut pending, &mut lagged);
4865        }
4866
4867        assert_eq!(lagged, None, "draining as we go means nothing is dropped");
4868        assert_eq!(pending.len(), 48, "every published frame is buffered");
4869    }
4870
4871    #[tokio::test]
4872    async fn a_replay_that_never_drains_is_reported_as_a_gap() {
4873        let (sender, mut receiver) = broadcast::channel::<Arc<BusMessage>>(8);
4874        for index in 0..32 {
4875            sender.send(bus_message(&format!("k{index}"))).unwrap();
4876        }
4877
4878        let mut pending = VecDeque::new();
4879        let mut lagged = None;
4880        drain_available(&mut receiver, &mut pending, &mut lagged);
4881
4882        assert!(
4883            lagged.is_some(),
4884            "overflowing the bus is a gap, not silent truncation"
4885        );
4886    }
4887
4888    /// Everything still on the bus after a gap is on the far side of it.
4889    /// Buffering it would put those frames in front of the lag report and
4890    /// advance the recovery cursor past the records it is meant to recover.
4891    #[tokio::test]
4892    async fn nothing_after_a_gap_is_buffered_ahead_of_the_report() {
4893        let (sender, mut receiver) = broadcast::channel::<Arc<BusMessage>>(8);
4894        let mut pending = VecDeque::new();
4895        let mut lagged = None;
4896
4897        // Delivered and buffered normally.
4898        sender.send(bus_message("before")).unwrap();
4899        drain_available(&mut receiver, &mut pending, &mut lagged);
4900        assert_eq!(pending.len(), 1);
4901
4902        // Overflow the bus: everything published from here is past the gap.
4903        for index in 0..32 {
4904            sender.send(bus_message(&format!("lost{index}"))).unwrap();
4905        }
4906        drain_available(&mut receiver, &mut pending, &mut lagged);
4907        assert!(lagged.is_some());
4908
4909        // The bus still holds what survived the overflow, all of it past the
4910        // gap. A later iteration of the replay loop drains again.
4911        let buffered_at_gap = pending.len();
4912        drain_available(&mut receiver, &mut pending, &mut lagged);
4913        assert_eq!(
4914            pending.len(),
4915            buffered_at_gap,
4916            "post-gap frames must not join the pre-gap flush"
4917        );
4918        assert_eq!(pending.front().unwrap().key, "before");
4919    }
4920    /// The bus is subscribed before the tape is read, so a record published
4921    /// in that window arrives on both paths.
4922    #[test]
4923    fn the_seam_between_replay_and_live_neither_repeats_nor_skips() {
4924        let mut last_sent = Some(4211);
4925
4926        assert!(
4927            already_delivered(Some(4211), &mut last_sent),
4928            "the record the replay ended on must not be sent twice"
4929        );
4930        assert!(already_delivered(Some(4100), &mut last_sent));
4931        assert_eq!(last_sent, Some(4211), "a duplicate never moves the mark");
4932
4933        assert!(
4934            !already_delivered(Some(4212), &mut last_sent),
4935            "the next record is new"
4936        );
4937        assert_eq!(last_sent, Some(4212));
4938
4939        // A frame with no offset comes from a view with no tape; it cannot
4940        // have been replayed, and must not disturb the mark.
4941        assert!(!already_delivered(None, &mut last_sent));
4942        assert_eq!(last_sent, Some(4212));
4943    }
4944
4945    /// A subscription with no cursor has delivered nothing, so the first live
4946    /// frame is not a duplicate.
4947    #[test]
4948    fn a_fresh_subscription_delivers_its_first_live_frame() {
4949        let mut last_sent = None;
4950        assert!(!already_delivered(Some(0), &mut last_sent));
4951        assert_eq!(last_sent, Some(0));
4952    }
4953
4954    /// End-to-end over a real socket: the pieces above are unit-tested
4955    /// individually, but the thing a consumer actually does — reconnect with
4956    /// a stored cursor and keep reading — only exists once a subscription is
4957    /// attached to a connection.
4958    mod over_a_socket {
4959        use super::*;
4960        use crate::journal::{EventJournal, JournalConfig};
4961        use crate::projector::Projector;
4962        use crate::{MutationBatch, SlotContext};
4963        use arete_interpreter::Mutation;
4964        use futures_util::{SinkExt, StreamExt};
4965        use std::time::Duration;
4966        use tokio::net::{TcpListener, TcpStream};
4967        use tokio::sync::mpsc;
4968        use tokio_tungstenite::tungstenite::Message;
4969        use tokio_tungstenite::{client_async, WebSocketStream};
4970
4971        const RETAINED: u64 = 600;
4972
4973        fn append_index() -> ViewIndex {
4974            let mut index = ViewIndex::new();
4975            index.add_spec(ViewSpec {
4976                id: "Trade/append".to_string(),
4977                export: "Trade".to_string(),
4978                mode: Mode::Append,
4979                wire_format: Default::default(),
4980                projection: Projection::all(),
4981                filters: Filters::all(),
4982                delivery: Delivery::default(),
4983                pipeline: None,
4984                source_view: None,
4985            });
4986            index
4987        }
4988
4989        fn trade(index: u64) -> MutationBatch {
4990            MutationBatch::with_slot_context(
4991                vec![Mutation {
4992                    export: "Trade".to_string(),
4993                    key: json!(format!("pool{}", index % 4)),
4994                    patch: json!({"trade": index}),
4995                    append: vec![],
4996                }]
4997                .into_iter()
4998                .collect(),
4999                SlotContext::new(100 + index / 3, index % 3),
5000            )
5001        }
5002
5003        struct Harness {
5004            addr: SocketAddr,
5005            journal: Arc<EventJournal>,
5006            tx: mpsc::Sender<MutationBatch>,
5007        }
5008
5009        impl Harness {
5010            async fn start() -> Self {
5011                let view_index = Arc::new(append_index());
5012                let entity_cache = EntityCache::new();
5013                let bus_manager = BusManager::new();
5014                let journal = Arc::new(EventJournal::new(JournalConfig {
5015                    enabled: true,
5016                    max_bytes_per_view: u64::MAX,
5017                    max_records_per_view: 10_000,
5018                    max_age: Duration::from_secs(3_600),
5019                }));
5020
5021                let (tx, rx) = mpsc::channel::<MutationBatch>(256);
5022                tokio::spawn(
5023                    Projector::new(
5024                        view_index.clone(),
5025                        bus_manager.clone(),
5026                        entity_cache.clone(),
5027                        rx,
5028                        #[cfg(feature = "otel")]
5029                        None,
5030                    )
5031                    .with_journal(journal.clone())
5032                    .run(),
5033                );
5034
5035                let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5036                let addr = listener.local_addr().unwrap();
5037                let server = WebSocketServer::new(
5038                    addr,
5039                    bus_manager,
5040                    entity_cache,
5041                    view_index,
5042                    #[cfg(feature = "otel")]
5043                    None,
5044                )
5045                .with_journal(journal.clone());
5046                let (acceptor, _cleanup) = server.into_acceptor();
5047                tokio::spawn(async move { acceptor.serve_listener(listener).await });
5048
5049                Self { addr, journal, tx }
5050            }
5051
5052            async fn publish(&self, range: std::ops::Range<u64>) {
5053                for index in range {
5054                    self.tx.send(trade(index)).await.unwrap();
5055                }
5056                let (ack, wait) = oneshot::channel();
5057                self.tx
5058                    .send(MutationBatch::flush_marker(ack))
5059                    .await
5060                    .unwrap();
5061                wait.await.unwrap();
5062            }
5063
5064            async fn connect(&self) -> WebSocketStream<TcpStream> {
5065                let stream = TcpStream::connect(self.addr).await.unwrap();
5066                client_async(format!("ws://{}/", self.addr), stream)
5067                    .await
5068                    .unwrap()
5069                    .0
5070            }
5071        }
5072
5073        async fn next_frame(socket: &mut WebSocketStream<TcpStream>) -> Value {
5074            loop {
5075                let message = tokio::time::timeout(Duration::from_secs(10), socket.next())
5076                    .await
5077                    .expect("the server answers within the timeout")
5078                    .expect("the stream stays open")
5079                    .expect("a readable frame");
5080                // Control and data frames arrive as binary; issue frames as
5081                // text. Both are JSON.
5082                let bytes = match &message {
5083                    Message::Text(text) => text.as_bytes(),
5084                    Message::Binary(bytes) => bytes.as_ref(),
5085                    _ => continue,
5086                };
5087                return serde_json::from_slice(bytes).expect("frames are JSON");
5088            }
5089        }
5090
5091        /// Collect `count` event frames, ignoring anything else on the wire.
5092        async fn collect_trades(socket: &mut WebSocketStream<TcpStream>, count: usize) -> Vec<u64> {
5093            let mut offsets = Vec::with_capacity(count);
5094            while offsets.len() < count {
5095                let frame = next_frame(socket).await;
5096                assert_ne!(
5097                    frame["type"], "error",
5098                    "no error frame should interrupt delivery: {frame}"
5099                );
5100                if let Some(offset) = frame["offset"].as_u64() {
5101                    offsets.push(offset);
5102                }
5103            }
5104            offsets
5105        }
5106
5107        /// The headline claim: reconnecting with a stored cursor delivers every
5108        /// event published since it, in order, and then continues live without
5109        /// a duplicate or a hole at the seam.
5110        #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
5111        async fn a_reconnect_replays_from_a_cursor_and_continues_live() {
5112            let harness = Harness::start().await;
5113            harness.publish(0..RETAINED).await;
5114
5115            let cursor = harness.journal.window("Trade/append").await;
5116            let stored = format!("{}:{}", cursor.epoch, 99);
5117
5118            let mut socket = harness.connect().await;
5119            socket
5120                .send(Message::Text(
5121                    json!({
5122                        "type": "subscribe",
5123                        "protocolVersion": 2,
5124                        "subscriptionId": "trades",
5125                        "query": {"view": "Trade/append", "after": stored},
5126                    })
5127                    .to_string()
5128                    .into(),
5129                ))
5130                .await
5131                .unwrap();
5132
5133            let ack = next_frame(&mut socket).await;
5134            assert_eq!(ack["op"], "subscribed", "unexpected ack: {ack}");
5135            assert_eq!(ack["replayWindow"]["next"], json!(RETAINED));
5136
5137            // Well over the 500 a single read or buffer would cover.
5138            let replayed = collect_trades(&mut socket, (RETAINED - 100) as usize).await;
5139            assert_eq!(
5140                replayed,
5141                (100..RETAINED).collect::<Vec<_>>(),
5142                "every event after the cursor, in order, exactly once"
5143            );
5144
5145            // Published only now, so these can only arrive over the live path.
5146            harness.publish(RETAINED..RETAINED + 40).await;
5147            let live = collect_trades(&mut socket, 40).await;
5148            assert_eq!(
5149                live,
5150                (RETAINED..RETAINED + 40).collect::<Vec<_>>(),
5151                "the live stream resumes exactly where the replay stopped"
5152            );
5153
5154            socket.close(None).await.ok();
5155        }
5156
5157        /// Events published *during* the replay must still arrive. The replay
5158        /// and the live subscription are separate reads of the same tape, and
5159        /// the seam between them is where a naive implementation drops or
5160        /// repeats.
5161        #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
5162        async fn events_published_during_a_replay_are_not_lost() {
5163            let harness = Harness::start().await;
5164            harness.publish(0..RETAINED).await;
5165
5166            let epoch = harness.journal.window("Trade/append").await.epoch;
5167            let mut socket = harness.connect().await;
5168            socket
5169                .send(Message::Text(
5170                    json!({
5171                        "type": "subscribe",
5172                        "protocolVersion": 2,
5173                        "subscriptionId": "trades",
5174                        "query": {"view": "Trade/append", "after": format!("{epoch}:0")},
5175                    })
5176                    .to_string()
5177                    .into(),
5178                ))
5179                .await
5180                .unwrap();
5181            let ack = next_frame(&mut socket).await;
5182            assert_eq!(ack["op"], "subscribed", "unexpected ack: {ack}");
5183
5184            // Keep publishing while the replay is still draining.
5185            harness.publish(RETAINED..RETAINED + 200).await;
5186
5187            let total = (RETAINED + 200 - 1) as usize;
5188            let delivered = collect_trades(&mut socket, total).await;
5189            assert_eq!(
5190                delivered,
5191                (1..RETAINED + 200).collect::<Vec<_>>(),
5192                "replay and live output join without a gap or a repeat"
5193            );
5194
5195            socket.close(None).await.ok();
5196        }
5197
5198        /// A cursor from another tape lifetime is refused rather than served
5199        /// as a continuation, and the refusal releases the subscription id.
5200        #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
5201        async fn a_stale_epoch_is_refused_and_frees_the_subscription_id() {
5202            let harness = Harness::start().await;
5203            harness.publish(0..50).await;
5204
5205            let mut socket = harness.connect().await;
5206            socket
5207                .send(Message::Text(
5208                    json!({
5209                        "type": "subscribe",
5210                        "protocolVersion": 2,
5211                        "subscriptionId": "trades",
5212                        "query": {
5213                            "view": "Trade/append",
5214                            "after": format!("{}:10", crate::journal::JournalEpoch::new()),
5215                        },
5216                    })
5217                    .to_string()
5218                    .into(),
5219                ))
5220                .await
5221                .unwrap();
5222
5223            let error = next_frame(&mut socket).await;
5224            assert_eq!(error["type"], "error", "unexpected frame: {error}");
5225            assert_eq!(error["code"], "cursor-epoch-changed");
5226
5227            // The documented recovery is to resubscribe without a cursor. That
5228            // only works if the refused attempt released the id.
5229            socket
5230                .send(Message::Text(
5231                    json!({
5232                        "type": "subscribe",
5233                        "protocolVersion": 2,
5234                        "subscriptionId": "trades",
5235                        "query": {"view": "Trade/append"},
5236                    })
5237                    .to_string()
5238                    .into(),
5239                ))
5240                .await
5241                .unwrap();
5242            let ack = next_frame(&mut socket).await;
5243            assert_eq!(
5244                ack["op"], "subscribed",
5245                "the refused id must be reusable: {ack}"
5246            );
5247
5248            assert_eq!(collect_trades(&mut socket, 50).await.len(), 50);
5249            socket.close(None).await.ok();
5250        }
5251
5252        const ROUND: &str = "Round/state";
5253
5254        /// A state view with no projector: the test writes the cache and the
5255        /// bus itself, so it controls exactly when the subscriber can run.
5256        #[derive(Clone)]
5257        struct StateServer {
5258            addr: SocketAddr,
5259            bus_manager: BusManager,
5260            entity_cache: EntityCache,
5261        }
5262
5263        impl StateServer {
5264            async fn start() -> Self {
5265                let mut index = ViewIndex::new();
5266                index.add_spec(ViewSpec {
5267                    id: ROUND.to_string(),
5268                    export: "Round".to_string(),
5269                    mode: Mode::State,
5270                    wire_format: Default::default(),
5271                    projection: Projection::all(),
5272                    filters: Filters::all(),
5273                    delivery: Delivery::default(),
5274                    pipeline: None,
5275                    source_view: None,
5276                });
5277                let bus_manager = BusManager::new();
5278                let entity_cache = EntityCache::new();
5279                let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5280                let addr = listener.local_addr().unwrap();
5281                let server = WebSocketServer::new(
5282                    addr,
5283                    bus_manager.clone(),
5284                    entity_cache.clone(),
5285                    Arc::new(index),
5286                    #[cfg(feature = "otel")]
5287                    None,
5288                );
5289                let (acceptor, _cleanup) = server.into_acceptor();
5290                tokio::spawn(async move { acceptor.serve_listener(listener).await });
5291                Self {
5292                    addr,
5293                    bus_manager,
5294                    entity_cache,
5295                }
5296            }
5297
5298            /// Publish a patch the way the projector does: stamp its `_seq`,
5299            /// write the cache, then the key's bus.
5300            async fn publish(&self, key: &str, mut patch: Value, seq: &str) {
5301                patch["_seq"] = Value::String(seq.to_string());
5302                self.entity_cache
5303                    .upsert_with_append(ROUND, key, patch.clone(), &[], PatchOrigin::Unknown)
5304                    .await;
5305                self.publish_frame(key, "patch", patch, seq).await;
5306            }
5307
5308            /// A complete new lifetime after an explicit source deletion.
5309            async fn create(&self, key: &str, mut entity: Value, seq: &str) {
5310                entity["_seq"] = Value::String(seq.to_string());
5311                self.entity_cache
5312                    .upsert_with_append(ROUND, key, entity.clone(), &[], PatchOrigin::Creation)
5313                    .await;
5314                self.publish_frame(key, "upsert", entity, seq).await;
5315            }
5316
5317            /// Publish a frame to the key's bus without touching the cache.
5318            async fn publish_frame(&self, key: &str, op: &str, data: Value, seq: &str) {
5319                let frame = json!({
5320                    "mode": "state",
5321                    "entity": ROUND,
5322                    "op": op,
5323                    "key": key,
5324                    "data": data,
5325                    "seq": seq,
5326                });
5327                self.bus_manager
5328                    .publish_state(ROUND, key, Arc::new(Bytes::from(frame.to_string())), None)
5329                    .await;
5330            }
5331
5332            /// Subscribe to one key and read through its snapshot.
5333            async fn subscribe(&self, key: &str) -> WebSocketStream<TcpStream> {
5334                let stream = TcpStream::connect(self.addr).await.unwrap();
5335                let mut socket = client_async(format!("ws://{}/", self.addr), stream)
5336                    .await
5337                    .unwrap()
5338                    .0;
5339                socket
5340                    .send(Message::Text(
5341                        json!({
5342                            "type": "subscribe",
5343                            "protocolVersion": 2,
5344                            "subscriptionId": "round",
5345                            "query": {"view": ROUND, "key": key},
5346                        })
5347                        .to_string()
5348                        .into(),
5349                    ))
5350                    .await
5351                    .unwrap();
5352                let ack = next_frame(&mut socket).await;
5353                assert_eq!(ack["op"], "subscribed", "unexpected ack: {ack}");
5354                loop {
5355                    let frame = next_frame(&mut socket).await;
5356                    assert_eq!(frame["op"], "snapshot", "unexpected frame: {frame}");
5357                    if frame["complete"] == true {
5358                        return socket;
5359                    }
5360                }
5361            }
5362        }
5363
5364        /// Two patches to one key, published before the subscriber reads the
5365        /// first, must both reach it. The bus holds only the latest frame, so
5366        /// forwarding it alone drops the fields of the one it replaced.
5367        // One worker, so tasks run one at a time. The handshake needs the
5368        // multi-threaded runtime.
5369        #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
5370        async fn a_state_subscriber_keeps_the_fields_of_an_overwritten_patch() {
5371            let server = StateServer::start().await;
5372            server
5373                .publish("7", json!({"id": 7}), "100:000000000001")
5374                .await;
5375            let mut socket = server.subscribe("7").await;
5376
5377            // A lone patch is forwarded as it was published. This one is an
5378            // account update, numbered by its write version.
5379            server
5380                .publish(
5381                    "7",
5382                    json!({"results": {"slot_hash": "abc"}}),
5383                    "140:003836292257",
5384                )
5385                .await;
5386            let frame = next_frame(&mut socket).await;
5387            assert_eq!(frame["op"], "patch", "unexpected frame: {frame}");
5388            assert_eq!(frame["seq"], "140:003836292257");
5389
5390            // Two instruction patches from later in the same slot, numbered by
5391            // transaction index. Both writes happen in one task that never
5392            // yields to the subscriber, so the second replaces the first on
5393            // the bus before the subscriber wakes.
5394            let writer = server.clone();
5395            tokio::spawn(async move {
5396                writer
5397                    .publish("7", json!({"entropy": {"seed": "def"}}), "140:000000001200")
5398                    .await;
5399                writer
5400                    .publish("7", json!({"total": 1}), "140:000000001200")
5401                    .await;
5402            })
5403            .await
5404            .unwrap();
5405            let frame = next_frame(&mut socket).await;
5406            assert_eq!(frame["op"], "patch", "unexpected frame: {frame}");
5407            // Everything that changed since the snapshot, the forwarded patch
5408            // included; `id` has not.
5409            assert_eq!(
5410                frame["data"],
5411                json!({
5412                    "results": {"slot_hash": "abc"},
5413                    "entropy": {"seed": "def"},
5414                    "total": 1,
5415                    "_seq": "140:000000001200",
5416                })
5417            );
5418            // The latest seq sorts below the one already delivered, and a
5419            // client would drop the frame as stale.
5420            assert!(frame.get("seq").is_none(), "unexpected seq: {frame}");
5421
5422            // Having caught up, the subscriber forwards patches again.
5423            server
5424                .publish("7", json!({"total": 2}), "141:000000000002")
5425                .await;
5426            let frame = next_frame(&mut socket).await;
5427            assert_eq!(frame["op"], "patch", "unexpected frame: {frame}");
5428            assert_eq!(
5429                frame["data"],
5430                json!({"total": 2, "_seq": "141:000000000002"})
5431            );
5432
5433            // The next catch-up is measured against the last one: only what
5434            // changed since it, not the fields that one already carried.
5435            let writer = server.clone();
5436            tokio::spawn(async move {
5437                writer
5438                    .publish("7", json!({"total": 3}), "142:000000000001")
5439                    .await;
5440                writer
5441                    .publish("7", json!({"flag": true}), "142:000000000001")
5442                    .await;
5443            })
5444            .await
5445            .unwrap();
5446            let frame = next_frame(&mut socket).await;
5447            assert_eq!(frame["op"], "patch", "unexpected frame: {frame}");
5448            assert!(frame.get("seq").is_none(), "unexpected seq: {frame}");
5449            assert_eq!(
5450                frame["data"],
5451                json!({"total": 3, "flag": true, "_seq": "142:000000000001"})
5452            );
5453
5454            socket.close(None).await.ok();
5455        }
5456
5457        /// A whole entity forwarded as it was published replaces the client's
5458        /// copy, so a later catch-up is measured against it.
5459        #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
5460        async fn a_forwarded_whole_entity_is_what_a_catch_up_is_measured_against() {
5461            let server = StateServer::start().await;
5462            server
5463                .publish("7", json!({"id": 7, "a": 1}), "100:000000000001")
5464                .await;
5465            let mut socket = server.subscribe("7").await;
5466
5467            let whole = json!({"id": 7, "a": 1, "b": 2, "_seq": "101:000000000001"});
5468            server
5469                .entity_cache
5470                .store_whole(ROUND, "7", whole.clone())
5471                .await;
5472            server
5473                .publish_frame("7", "upsert", whole.clone(), "101:000000000001")
5474                .await;
5475            let frame = next_frame(&mut socket).await;
5476            assert_eq!(frame["op"], "upsert", "unexpected frame: {frame}");
5477            assert_eq!(frame["data"], whole);
5478
5479            let writer = server.clone();
5480            tokio::spawn(async move {
5481                writer
5482                    .publish("7", json!({"c": 3}), "102:000000000001")
5483                    .await;
5484                writer
5485                    .publish("7", json!({"d": 4}), "102:000000000002")
5486                    .await;
5487            })
5488            .await
5489            .unwrap();
5490            let frame = next_frame(&mut socket).await;
5491            assert_eq!(frame["op"], "patch", "unexpected frame: {frame}");
5492            assert_eq!(
5493                frame["data"],
5494                json!({"c": 3, "d": 4, "_seq": "102:000000000002"}),
5495                "`b` came with the whole entity, so it is not sent again"
5496            );
5497            socket.close(None).await.ok();
5498        }
5499
5500        /// Holders of the cached entity's fields beyond the cache and the
5501        /// copy this reads.
5502        async fn holders(server: &StateServer, key: &str) -> usize {
5503            let entity = server.entity_cache.get_shared(ROUND, key).await.unwrap();
5504            Arc::strong_count(entity.fields()) - 2
5505        }
5506
5507        /// Wait until `held` reports `expected`: a subscription's task takes
5508        /// over what it holds just after its snapshot reaches the client.
5509        async fn settle(mut held: impl AsyncFnMut() -> usize, expected: usize) {
5510            for _ in 0..200 {
5511                if held().await == expected {
5512                    return;
5513                }
5514                tokio::time::sleep(Duration::from_millis(5)).await;
5515            }
5516            assert_eq!(held().await, expected);
5517        }
5518
5519        /// What a state subscriber keeps to measure catch-ups against is the
5520        /// cached entity it was sent, or the frame the bus forwarded: shared,
5521        /// never a copy of its own.
5522        #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
5523        async fn state_subscribers_share_what_they_were_sent() {
5524            let server = StateServer::start().await;
5525            server
5526                .create(
5527                    "7",
5528                    json!({"id": 7, "detail": {"a": 1}}),
5529                    "100:000000000001",
5530                )
5531                .await;
5532            let mut sockets = Vec::new();
5533            for _ in 0..3 {
5534                sockets.push(server.subscribe("7").await);
5535            }
5536            settle(|| holders(&server, "7"), 3).await;
5537
5538            // A whole entity forwarded from the bus: each subscriber keeps the
5539            // frame the bus holds, not the entity it carries.
5540            let sent = server.entity_cache.get_shared(ROUND, "7").await.unwrap();
5541            let whole = json!({"id": 7, "detail": {"b": 2}, "_seq": "101:000000000001"});
5542            server
5543                .entity_cache
5544                .store_whole(ROUND, "7", whole.clone())
5545                .await;
5546            let frame = Arc::new(Bytes::from(
5547                json!({
5548                    "mode": "state",
5549                    "entity": ROUND,
5550                    "op": "upsert",
5551                    "key": "7",
5552                    "data": whole,
5553                    "seq": "101:000000000001",
5554                })
5555                .to_string(),
5556            ));
5557            server
5558                .bus_manager
5559                .publish_state(ROUND, "7", frame.clone(), None)
5560                .await;
5561            for socket in &mut sockets {
5562                assert_eq!(next_frame(socket).await["op"], "upsert");
5563            }
5564            // This test's handle, the bus's, and one per subscriber.
5565            settle(async || Arc::strong_count(&frame), 2 + sockets.len()).await;
5566            // Only this test still holds the entity they were sent before.
5567            settle(async || Arc::strong_count(sent.fields()), 1).await;
5568            for mut socket in sockets {
5569                socket.close(None).await.ok();
5570            }
5571        }
5572
5573        /// A catch-up resends a field a forwarded patch set even when the
5574        /// entity has set it back to the value the client last received
5575        /// whole: the client still holds the forwarded value.
5576        #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
5577        async fn a_catch_up_restores_a_field_a_forwarded_patch_set() {
5578            let server = StateServer::start().await;
5579            server
5580                .publish("7", json!({"id": 7, "a": 1}), "100:000000000001")
5581                .await;
5582            let mut socket = server.subscribe("7").await;
5583
5584            server
5585                .publish("7", json!({"a": 2}), "101:000000000001")
5586                .await;
5587            let frame = next_frame(&mut socket).await;
5588            assert_eq!(frame["op"], "patch", "unexpected frame: {frame}");
5589            assert_eq!(frame["data"], json!({"a": 2, "_seq": "101:000000000001"}));
5590
5591            let writer = server.clone();
5592            tokio::spawn(async move {
5593                writer
5594                    .publish("7", json!({"a": 1}), "102:000000000001")
5595                    .await;
5596                writer
5597                    .publish("7", json!({"b": 1}), "102:000000000002")
5598                    .await;
5599            })
5600            .await
5601            .unwrap();
5602            let frame = next_frame(&mut socket).await;
5603            assert_eq!(frame["op"], "patch", "unexpected frame: {frame}");
5604            assert_eq!(
5605                frame["data"],
5606                json!({"a": 1, "b": 1, "_seq": "102:000000000002"}),
5607                "`a` matches the snapshot but the client holds 2"
5608            );
5609            socket.close(None).await.ok();
5610        }
5611
5612        /// A key deleted and created again before the subscriber reads
5613        /// either frame: the bus keeps only the new entity's. Merged into the
5614        /// client's copy it would leave the deleted entity's fields in place,
5615        /// so the client gets the new entity whole, replacing its copy.
5616        #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
5617        async fn a_key_deleted_and_recreated_between_reads_replaces_the_clients_copy() {
5618            let server = StateServer::start().await;
5619            server
5620                .publish("7", json!({"id": 7, "old": true}), "100:000000000001")
5621                .await;
5622            let mut socket = server.subscribe("7").await;
5623
5624            let writer = server.clone();
5625            tokio::spawn(async move {
5626                writer.entity_cache.remove(ROUND, "7").await;
5627                writer
5628                    .publish_frame("7", "delete", Value::Null, "101:000000000001")
5629                    .await;
5630                writer
5631                    .publish("7", json!({"id": 7, "fresh": true}), "101:000000000002")
5632                    .await;
5633            })
5634            .await
5635            .unwrap();
5636            let frame = next_frame(&mut socket).await;
5637            assert_eq!(frame["op"], "upsert", "unexpected frame: {frame}");
5638            assert_eq!(
5639                frame["data"],
5640                json!({"id": 7, "fresh": true, "_seq": "101:000000000002"})
5641            );
5642            assert!(frame.get("seq").is_none(), "unexpected seq: {frame}");
5643
5644            // Deleted, with a patch overwritten before it: the delete is final.
5645            let writer = server.clone();
5646            tokio::spawn(async move {
5647                writer
5648                    .publish("7", json!({"n": 1}), "102:000000000001")
5649                    .await;
5650                writer.entity_cache.remove(ROUND, "7").await;
5651                writer
5652                    .publish_frame("7", "delete", Value::Null, "102:000000000002")
5653                    .await;
5654            })
5655            .await
5656            .unwrap();
5657            let frame = next_frame(&mut socket).await;
5658            assert_eq!(frame["op"], "delete", "unexpected frame: {frame}");
5659
5660            // Created again: the client holds nothing, so it arrives whole.
5661            server
5662                .create("7", json!({"id": 7, "third": true}), "103:000000000001")
5663                .await;
5664            let frame = next_frame(&mut socket).await;
5665            assert_eq!(frame["op"], "upsert", "unexpected frame: {frame}");
5666            assert_eq!(frame["data"]["third"], true);
5667            assert!(frame["data"].get("fresh").is_none(), "{frame}");
5668            socket.close(None).await.ok();
5669        }
5670
5671        /// A missed delete, then a patch the cache refused (it lacks the new
5672        /// entity): the client's copy is of the deleted entity, so the patch
5673        /// is not merged into it. The new entity replaces it once it is whole.
5674        #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
5675        async fn a_copy_of_a_deleted_entity_takes_no_patch_until_it_is_replaced() {
5676            let server = StateServer::start().await;
5677            server
5678                .publish("7", json!({"id": 7, "old": true}), "100:000000000001")
5679                .await;
5680            let mut socket = server.subscribe("7").await;
5681
5682            let writer = server.clone();
5683            tokio::spawn(async move {
5684                writer.entity_cache.remove(ROUND, "7").await;
5685                writer
5686                    .publish_frame("7", "delete", Value::Null, "101:000000000001")
5687                    .await;
5688                writer
5689                    .publish_frame("7", "patch", json!({"n": 1}), "101:000000000002")
5690                    .await;
5691            })
5692            .await
5693            .unwrap();
5694            // Let the subscriber read the patch, and withhold it, before the
5695            // whole entity arrives. Nothing is sent, so there is no frame to
5696            // wait for.
5697            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
5698
5699            let whole = json!({"id": 7, "n": 1, "_seq": "101:000000000002"});
5700            server
5701                .entity_cache
5702                .store_whole(ROUND, "7", whole.clone())
5703                .await;
5704            server
5705                .publish_frame("7", "upsert", whole.clone(), "101:000000000002")
5706                .await;
5707            let frame = next_frame(&mut socket).await;
5708            assert_eq!(frame["op"], "upsert", "unexpected frame: {frame}");
5709            assert_eq!(frame["data"], whole);
5710            socket.close(None).await.ok();
5711        }
5712
5713        /// The source replaced the entity whole (an `upsert`, dropping a field)
5714        /// and then patched it, both before the subscriber read either. The
5715        /// patch alone, or the entity merged in, would keep the dropped field.
5716        #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
5717        async fn a_whole_entity_among_overwritten_frames_replaces_the_clients_copy() {
5718            let server = StateServer::start().await;
5719            server
5720                .publish("7", json!({"id": 7, "dropped": 1}), "100:000000000001")
5721                .await;
5722            let mut socket = server.subscribe("7").await;
5723
5724            let writer = server.clone();
5725            tokio::spawn(async move {
5726                let whole = json!({"id": 7, "_seq": "101:000000000001"});
5727                writer
5728                    .entity_cache
5729                    .store_whole(ROUND, "7", whole.clone())
5730                    .await;
5731                writer
5732                    .publish_frame("7", "upsert", whole, "101:000000000001")
5733                    .await;
5734                writer
5735                    .publish("7", json!({"n": 1}), "101:000000000002")
5736                    .await;
5737            })
5738            .await
5739            .unwrap();
5740            let frame = next_frame(&mut socket).await;
5741            assert_eq!(frame["op"], "upsert", "unexpected frame: {frame}");
5742            assert_eq!(
5743                frame["data"],
5744                json!({"id": 7, "n": 1, "_seq": "101:000000000002"})
5745            );
5746            assert!(frame.get("seq").is_none(), "unexpected seq: {frame}");
5747            socket.close(None).await.ok();
5748        }
5749
5750        /// Patches overwritten on the bus while the cache lacks the key (it
5751        /// evicted it, and refused them) leave nothing whole to send in their
5752        /// place: the holder gets the latest patch, as a holder of an evicted
5753        /// key does, and stays behind. The resend that brings the entity back
5754        /// carries the seq of the key's latest change, which the holder
5755        /// already has and would drop as stale, so it gets the cached entity
5756        /// without a seq instead: as an upsert, since the resend is the whole
5757        /// entity and replaces what the holder has.
5758        #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
5759        async fn a_holder_behind_on_an_evicted_key_catches_up_when_it_returns() {
5760            let server = StateServer::start().await;
5761            server
5762                .publish("7", json!({"id": 7}), "100:000000000001")
5763                .await;
5764            let mut socket = server.subscribe("7").await;
5765            server.entity_cache.remove(ROUND, "7").await;
5766
5767            let writer = server.clone();
5768            tokio::spawn(async move {
5769                writer
5770                    .publish_frame("7", "patch", json!({"a": 1}), "101:000000000001")
5771                    .await;
5772                writer
5773                    .publish_frame("7", "patch", json!({"b": 2}), "101:000000000002")
5774                    .await;
5775            })
5776            .await
5777            .unwrap();
5778            let frame = next_frame(&mut socket).await;
5779            assert_eq!(frame["op"], "patch", "unexpected frame: {frame}");
5780            assert_eq!(frame["data"], json!({"b": 2}));
5781            assert_eq!(frame["seq"], "101:000000000002");
5782
5783            // The resend: the whole entity, at the latest change's position.
5784            let whole = json!({"id": 7, "a": 1, "b": 2, "_seq": "101:000000000002"});
5785            server
5786                .entity_cache
5787                .store_whole(ROUND, "7", whole.clone())
5788                .await;
5789            server
5790                .publish_frame("7", "upsert", whole.clone(), "101:000000000002")
5791                .await;
5792            let frame = next_frame(&mut socket).await;
5793            assert_eq!(frame["op"], "upsert", "unexpected frame: {frame}");
5794            assert_eq!(frame["data"], whole);
5795            assert!(frame.get("seq").is_none(), "unexpected seq: {frame}");
5796
5797            // Caught up: patches are forwarded again.
5798            server
5799                .publish("7", json!({"c": 3}), "102:000000000001")
5800                .await;
5801            let frame = next_frame(&mut socket).await;
5802            assert_eq!(frame["data"], json!({"c": 3, "_seq": "102:000000000001"}));
5803            assert_eq!(frame["seq"], "102:000000000001");
5804            socket.close(None).await.ok();
5805        }
5806    }
5807
5808    /// A key only counts as held by the client once a frame actually carried
5809    /// it. A truncated or disabled snapshot leaves keys the client never saw,
5810    /// and their first change must arrive whole.
5811    mod partial_entities_over_a_socket {
5812        use super::*;
5813        use crate::projector::Projector;
5814        use crate::{MutationBatch, SlotContext};
5815        use arete_interpreter::{AccountPosition, Mutation};
5816        use futures_util::{SinkExt, StreamExt};
5817        use std::time::Duration;
5818        use tokio::net::{TcpListener, TcpStream};
5819        use tokio::sync::mpsc;
5820        use tokio_tungstenite::tungstenite::Message;
5821        use tokio_tungstenite::{client_async, WebSocketStream};
5822
5823        fn thing_index() -> ViewIndex {
5824            let mut index = ViewIndex::new();
5825            index.add_spec(list_spec());
5826            index.add_spec(ViewSpec {
5827                id: "Thing/state".to_string(),
5828                mode: Mode::State,
5829                ..list_spec()
5830            });
5831            index
5832        }
5833
5834        struct Harness {
5835            addr: SocketAddr,
5836            tx: mpsc::Sender<MutationBatch>,
5837            slot: std::sync::atomic::AtomicU64,
5838        }
5839
5840        impl Harness {
5841            async fn start(delivery: WebSocketDeliveryConfig) -> Self {
5842                Self::start_with_cache(delivery, EntityCache::new()).await
5843            }
5844
5845            async fn start_with_cache(
5846                delivery: WebSocketDeliveryConfig,
5847                entity_cache: EntityCache,
5848            ) -> Self {
5849                Self::start_with_index(delivery, entity_cache, thing_index()).await
5850            }
5851
5852            async fn start_with_index(
5853                delivery: WebSocketDeliveryConfig,
5854                entity_cache: EntityCache,
5855                index: ViewIndex,
5856            ) -> Self {
5857                let view_index = Arc::new(index);
5858                let bus_manager = BusManager::new();
5859                let (tx, rx) = mpsc::channel::<MutationBatch>(64);
5860                // Unconstrained, so Tokio's cooperative budget never makes the
5861                // projector yield partway through a batch: a test on one
5862                // worker then knows no subscriber runs until the whole batch
5863                // is published (see
5864                // `snapshot_rows_patches_and_catch_ups_carry_the_version`).
5865                tokio::spawn(tokio::task::unconstrained(
5866                    Projector::new(
5867                        view_index.clone(),
5868                        bus_manager.clone(),
5869                        entity_cache.clone(),
5870                        rx,
5871                        #[cfg(feature = "otel")]
5872                        None,
5873                    )
5874                    .run(),
5875                ));
5876                let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5877                let addr = listener.local_addr().unwrap();
5878                let server = WebSocketServer::new(
5879                    addr,
5880                    bus_manager,
5881                    entity_cache,
5882                    view_index,
5883                    #[cfg(feature = "otel")]
5884                    None,
5885                )
5886                .with_delivery_config(delivery);
5887                let (acceptor, _cleanup) = server.into_acceptor();
5888                tokio::spawn(async move { acceptor.serve_listener(listener).await });
5889                Self {
5890                    addr,
5891                    tx,
5892                    slot: std::sync::atomic::AtomicU64::new(100),
5893                }
5894            }
5895
5896            /// Apply one source patch and wait until the projector published it.
5897            async fn patch(&self, key: &str, patch: Value) {
5898                self.mutate(Mutation {
5899                    export: "Thing".to_string(),
5900                    key: json!(key),
5901                    patch,
5902                    append: vec![],
5903                })
5904                .await;
5905            }
5906
5907            async fn mutate(&self, mutation: Mutation) {
5908                let slot = self.slot.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
5909                self.mutate_with_context(mutation, SlotContext::new(slot, 0))
5910                    .await;
5911            }
5912
5913            async fn mutate_with_context(&self, mutation: Mutation, context: SlotContext) {
5914                self.tx
5915                    .send(MutationBatch::with_slot_context(
5916                        vec![mutation].into_iter().collect(),
5917                        context,
5918                    ))
5919                    .await
5920                    .unwrap();
5921                let (ack, wait) = oneshot::channel();
5922                self.tx
5923                    .send(MutationBatch::flush_marker(ack))
5924                    .await
5925                    .unwrap();
5926                wait.await.unwrap();
5927            }
5928
5929            async fn subscribe(&self, query: Value, snapshot: bool) -> WebSocketStream<TcpStream> {
5930                let stream = TcpStream::connect(self.addr).await.unwrap();
5931                let mut socket = client_async(format!("ws://{}/", self.addr), stream)
5932                    .await
5933                    .unwrap()
5934                    .0;
5935                socket
5936                    .send(Message::Text(
5937                        json!({
5938                            "type": "subscribe",
5939                            "protocolVersion": 2,
5940                            "subscriptionId": "things",
5941                            "query": query,
5942                            "snapshot": {"enabled": snapshot},
5943                        })
5944                        .to_string()
5945                        .into(),
5946                    ))
5947                    .await
5948                    .unwrap();
5949                let ack = next_frame(&mut socket).await;
5950                assert_eq!(ack["op"], "subscribed", "unexpected ack: {ack}");
5951                assert_eq!(
5952                    ack["wholeEntities"],
5953                    json!(true),
5954                    "the ack advertises the whole-entity guarantee: {ack}"
5955                );
5956                socket
5957            }
5958        }
5959
5960        async fn next_frame(socket: &mut WebSocketStream<TcpStream>) -> Value {
5961            loop {
5962                let message = tokio::time::timeout(Duration::from_secs(10), socket.next())
5963                    .await
5964                    .expect("the server answers within the timeout")
5965                    .expect("the stream stays open")
5966                    .expect("a readable frame");
5967                // Every frame here is far below the compression threshold.
5968                let bytes = match &message {
5969                    Message::Text(text) => text.as_bytes(),
5970                    Message::Binary(bytes) => bytes.as_ref(),
5971                    _ => continue,
5972                };
5973                return serde_json::from_slice(bytes).expect("frames are JSON");
5974            }
5975        }
5976
5977        /// The snapshot rows, in order, once the snapshot completes.
5978        async fn snapshot_keys(socket: &mut WebSocketStream<TcpStream>) -> Vec<String> {
5979            let mut keys = Vec::new();
5980            loop {
5981                let frame = next_frame(socket).await;
5982                assert_eq!(frame["op"], "snapshot", "expected a snapshot: {frame}");
5983                keys.extend(
5984                    frame["data"]
5985                        .as_array()
5986                        .unwrap()
5987                        .iter()
5988                        .map(|row| row["key"].as_str().unwrap().to_string()),
5989                );
5990                if frame["complete"] == json!(true) {
5991                    return keys;
5992                }
5993            }
5994        }
5995
5996        async fn seed(harness: &Harness) {
5997            for key in ["a", "b", "c"] {
5998                harness
5999                    .patch(key, json!({"name": format!("thing-{key}"), "count": 1}))
6000                    .await;
6001            }
6002        }
6003
6004        fn derived_index() -> ViewIndex {
6005            use crate::materialized_view::{
6006                CompareOp, FilterConfig, SortConfig, SortOrder, ViewPipeline,
6007            };
6008            let mut index = thing_index();
6009            for name in ["empty", "filtered", "sorted"] {
6010                let mut pipeline = ViewPipeline::default();
6011                if name != "empty" {
6012                    pipeline.filter = Some(FilterConfig {
6013                        field_path: vec!["owner".into()],
6014                        op: CompareOp::Eq,
6015                        value: json!("alice"),
6016                    });
6017                    pipeline.limit = Some(2);
6018                }
6019                if name == "sorted" {
6020                    pipeline.sort = Some(SortConfig {
6021                        field_path: vec!["amount".into()],
6022                        order: SortOrder::Desc,
6023                    });
6024                }
6025                index.add_spec(ViewSpec {
6026                    id: format!("Thing/{name}"),
6027                    source_view: Some("Thing/list".into()),
6028                    pipeline: Some(pipeline),
6029                    ..list_spec()
6030                });
6031            }
6032            index
6033        }
6034
6035        #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6036        async fn unsorted_derived_idle_bootstrap_and_reconnect_are_authoritative() {
6037            let harness = Harness::start_with_index(
6038                WebSocketDeliveryConfig::default(),
6039                EntityCache::new(),
6040                derived_index(),
6041            )
6042            .await;
6043            harness
6044                .patch("a", json!({"owner":"alice", "amount":1}))
6045                .await;
6046            harness.patch("b", json!({"owner":"bob", "amount":2})).await;
6047            harness
6048                .patch("c", json!({"owner":"alice", "amount":3}))
6049                .await;
6050            harness
6051                .patch("d", json!({"owner":"alice", "amount":4}))
6052                .await;
6053            for _ in 0..2 {
6054                for (view, expected) in [
6055                    ("empty", vec!["d", "c", "b", "a"]),
6056                    ("filtered", vec!["d", "c"]),
6057                    ("sorted", vec!["d", "c"]),
6058                ] {
6059                    let mut socket = harness
6060                        .subscribe(json!({"view": format!("Thing/{view}"), "take":100}), true)
6061                        .await;
6062                    assert_eq!(snapshot_keys(&mut socket).await, expected);
6063                    socket.close(None).await.unwrap();
6064                }
6065            }
6066        }
6067
6068        #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6069        async fn source_delete_and_recreation_clear_all_views_across_reconnect() {
6070            let harness = Harness::start_with_index(
6071                WebSocketDeliveryConfig::default(),
6072                EntityCache::new(),
6073                derived_index(),
6074            )
6075            .await;
6076            harness
6077                .patch("a", json!({"owner":"alice", "amount":1, "oldField":true}))
6078                .await;
6079            let mut list = harness
6080                .subscribe(json!({"view":"Thing/filtered"}), true)
6081                .await;
6082            assert_eq!(snapshot_keys(&mut list).await, ["a"]);
6083            let mut state = harness
6084                .subscribe(json!({"view":"Thing/state", "key":"a"}), true)
6085                .await;
6086            assert_eq!(snapshot_keys(&mut state).await, ["a"]);
6087            // Leaving a predicate is view eviction, not chain deletion.
6088            harness.patch("a", json!({"owner":"bob"})).await;
6089            assert_eq!(next_frame(&mut list).await["op"], "remove");
6090            harness.patch("a", json!({"owner":"alice"})).await;
6091            assert_eq!(next_frame(&mut list).await["op"], "upsert");
6092            let mut deletion = Mutation::delete("Thing", json!("a"));
6093            deletion.mark_account_position(AccountPosition::new(100, 1001));
6094            harness.mutate(deletion).await;
6095            assert_eq!(next_frame(&mut list).await["op"], "delete");
6096            // State patches may precede its deletion on the socket.
6097            loop {
6098                if next_frame(&mut state).await["op"] == "delete" {
6099                    break;
6100                }
6101            }
6102            for view in [
6103                "Thing/list",
6104                "Thing/empty",
6105                "Thing/filtered",
6106                "Thing/sorted",
6107                "Thing/state",
6108            ] {
6109                let mut socket = harness
6110                    .subscribe(json!({"view":view, "key":"a"}), true)
6111                    .await;
6112                assert!(snapshot_keys(&mut socket).await.is_empty(), "{view}");
6113                socket.close(None).await.unwrap();
6114            }
6115            // A whole resend or sparse change cannot recreate a deleted row.
6116            harness
6117                .patch("a", json!({"owner":"alice", "oldField":true}))
6118                .await;
6119            let mut stale = Mutation {
6120                export: "Thing".into(),
6121                key: json!("a"),
6122                patch: json!({"owner":"alice", "oldField":true}),
6123                append: vec![],
6124            };
6125            stale.mark_whole_entity();
6126            harness.mutate(stale).await;
6127            let mut socket = harness
6128                .subscribe(json!({"view":"Thing/filtered"}), true)
6129                .await;
6130            assert!(snapshot_keys(&mut socket).await.is_empty());
6131            socket.close(None).await.unwrap();
6132            let mut recreated = Mutation {
6133                export: "Thing".into(),
6134                key: json!("a"),
6135                patch: json!({"owner":"alice", "amount":9}),
6136                append: vec![],
6137            };
6138            recreated.mark_created();
6139            recreated.mark_account_position(AccountPosition::new(100, 1002));
6140            harness.mutate(recreated).await;
6141            for view in [
6142                "Thing/list",
6143                "Thing/empty",
6144                "Thing/filtered",
6145                "Thing/sorted",
6146                "Thing/state",
6147            ] {
6148                let mut socket = harness
6149                    .subscribe(json!({"view":view, "key":"a"}), true)
6150                    .await;
6151                let frame = next_frame(&mut socket).await;
6152                assert_eq!(frame["data"][0]["key"], "a");
6153                assert_eq!(frame["data"][0]["data"]["amount"], 9);
6154                assert!(
6155                    frame["data"][0]["data"].get("oldField").is_none(),
6156                    "{frame}"
6157                );
6158                socket.close(None).await.unwrap();
6159            }
6160        }
6161
6162        #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6163        async fn resolver_offsets_do_not_suppress_later_instruction_updates() {
6164            let harness = Harness::start(WebSocketDeliveryConfig::default()).await;
6165            let mut created = Mutation {
6166                export: "Thing".into(),
6167                key: json!("a"),
6168                patch: json!({"value":"created"}),
6169                append: vec![],
6170            };
6171            created.mark_created();
6172            created.mark_account_position(AccountPosition::new(100, 9));
6173            harness
6174                .mutate_with_context(created, SlotContext::account(100, 9))
6175                .await;
6176
6177            harness
6178                .mutate_with_context(
6179                    Mutation {
6180                        export: "Thing".into(),
6181                        key: json!("a"),
6182                        patch: json!({"value":"resolver", "resolved":true}),
6183                        append: vec![],
6184                    },
6185                    SlotContext::resolver(100, 1_u64 << 63),
6186                )
6187                .await;
6188            harness
6189                .mutate_with_context(
6190                    Mutation {
6191                        export: "Thing".into(),
6192                        key: json!("a"),
6193                        patch: json!({"value":"instruction", "instruction":true}),
6194                        append: vec![],
6195                    },
6196                    SlotContext::instruction(100, 900),
6197                )
6198                .await;
6199
6200            let mut socket = harness
6201                .subscribe(json!({"view":"Thing/state", "key":"a"}), true)
6202                .await;
6203            let frame = next_frame(&mut socket).await;
6204            assert_eq!(frame["op"], "snapshot", "{frame}");
6205            let row = &frame["data"][0]["data"];
6206            assert_eq!(row["value"], "instruction", "{frame}");
6207            assert_eq!(row["resolved"], true, "{frame}");
6208            assert_eq!(row["instruction"], true, "{frame}");
6209
6210            // Recency still applies inside the instruction domain even after
6211            // activity from another source domain.
6212            harness
6213                .mutate_with_context(
6214                    Mutation {
6215                        export: "Thing".into(),
6216                        key: json!("a"),
6217                        patch: json!({"value":"stale", "stale":true}),
6218                        append: vec![],
6219                    },
6220                    SlotContext::instruction(100, 899),
6221                )
6222                .await;
6223            let mut reconnected = harness
6224                .subscribe(json!({"view":"Thing/state", "key":"a"}), true)
6225                .await;
6226            let frame = next_frame(&mut reconnected).await;
6227            let row = &frame["data"][0]["data"];
6228            assert_eq!(row["value"], "instruction", "{frame}");
6229            assert!(row.get("stale").is_none(), "{frame}");
6230        }
6231
6232        #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
6233        async fn projected_delete_and_recreation_between_reads_replace_state() {
6234            let harness = Harness::start(WebSocketDeliveryConfig::default()).await;
6235            harness.patch("a", json!({"old":true})).await;
6236            let mut socket = harness
6237                .subscribe(json!({"view":"Thing/state", "key":"a"}), true)
6238                .await;
6239            assert_eq!(snapshot_keys(&mut socket).await, ["a"]);
6240            let mut creation = Mutation {
6241                export: "Thing".into(),
6242                key: json!("a"),
6243                patch: json!({"fresh":true}),
6244                append: vec![],
6245            };
6246            creation.mark_created();
6247            // Both ready batches run before the watch receiver on this worker.
6248            for mutation in [Mutation::delete("Thing", json!("a")), creation] {
6249                let slot = harness
6250                    .slot
6251                    .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
6252                harness
6253                    .tx
6254                    .try_send(MutationBatch::with_slot_context(
6255                        vec![mutation].into_iter().collect(),
6256                        SlotContext::new(slot, 0),
6257                    ))
6258                    .unwrap();
6259            }
6260            let (ack, wait) = oneshot::channel();
6261            harness
6262                .tx
6263                .try_send(MutationBatch::flush_marker(ack))
6264                .unwrap();
6265            wait.await.unwrap();
6266            // Usually both batches land before the socket reads and arrive as
6267            // one upsert. Under load the socket can read between them, so the
6268            // delete may arrive on its own first. Either way the recreated
6269            // entity must not carry the deleted one's fields.
6270            let mut frame = next_frame(&mut socket).await;
6271            if frame["op"] == "delete" {
6272                assert_eq!(frame["key"], "a", "{frame}");
6273                frame = next_frame(&mut socket).await;
6274            }
6275            assert_eq!(frame["op"], "upsert", "{frame}");
6276            assert_eq!(frame["data"]["fresh"], true);
6277            assert!(frame["data"].get("old").is_none(), "{frame}");
6278        }
6279
6280        #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6281        async fn a_change_to_a_key_cut_by_snapshot_limit_is_an_upsert() {
6282            let harness = Harness::start(WebSocketDeliveryConfig::default()).await;
6283            seed(&harness).await;
6284
6285            let mut socket = harness
6286                .subscribe(json!({"view": "Thing/list", "snapshotLimit": 1}), true)
6287                .await;
6288            // Newest first: only "c" was sent.
6289            assert_eq!(snapshot_keys(&mut socket).await, ["c"]);
6290
6291            harness.patch("a", json!({"count": 2})).await;
6292            let frame = next_frame(&mut socket).await;
6293            assert_eq!(frame["key"], "a", "unexpected frame: {frame}");
6294            assert_eq!(frame["op"], "upsert", "a key never sent arrives whole");
6295            assert_eq!(frame["data"]["name"], "thing-a");
6296            assert_eq!(frame["data"]["count"], 2);
6297
6298            // Now the client holds "a", so its next change is a patch again.
6299            harness.patch("a", json!({"count": 3})).await;
6300            let frame = next_frame(&mut socket).await;
6301            assert_eq!(frame["key"], "a");
6302            assert_eq!(frame["op"], "patch", "unexpected frame: {frame}");
6303            assert!(frame["data"].get("name").is_none());
6304
6305            // A key the snapshot did carry still rides its patch.
6306            harness.patch("c", json!({"count": 2})).await;
6307            let frame = next_frame(&mut socket).await;
6308            assert_eq!(frame["key"], "c");
6309            assert_eq!(frame["op"], "patch", "unexpected frame: {frame}");
6310            socket.close(None).await.ok();
6311        }
6312
6313        #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6314        async fn a_list_without_a_snapshot_upserts_each_key_on_its_first_change() {
6315            let harness = Harness::start(WebSocketDeliveryConfig::default()).await;
6316            seed(&harness).await;
6317
6318            let mut socket = harness
6319                .subscribe(json!({"view": "Thing/list"}), false)
6320                .await;
6321            harness.patch("b", json!({"count": 2})).await;
6322            let frame = next_frame(&mut socket).await;
6323            assert_eq!(frame["key"], "b");
6324            assert_eq!(frame["op"], "upsert", "unexpected frame: {frame}");
6325            assert_eq!(frame["data"]["name"], "thing-b");
6326
6327            harness.patch("b", json!({"count": 3})).await;
6328            let frame = next_frame(&mut socket).await;
6329            assert_eq!(frame["key"], "b");
6330            assert_eq!(frame["op"], "patch", "unexpected frame: {frame}");
6331            socket.close(None).await.ok();
6332        }
6333
6334        /// A key created and completed by two mutations of one batch (an
6335        /// activity row and the instruction joined to it) enters a list as one
6336        /// upsert carrying both, read from the cache once both are folded in.
6337        /// The second mutation's patch is in that upsert, so it is not
6338        /// forwarded again; the key's next change is.
6339        // One worker, and a projector the cooperative budget cannot interrupt:
6340        // the whole batch is published before the subscriber runs.
6341        #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
6342        async fn a_key_entering_a_list_is_not_sent_the_patch_its_upsert_carries() {
6343            let harness = Harness::start(WebSocketDeliveryConfig::default()).await;
6344            seed(&harness).await;
6345            let mut socket = harness
6346                .subscribe(json!({"view": "Thing/list"}), false)
6347                .await;
6348
6349            let slot = harness
6350                .slot
6351                .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
6352            let mut creation = Mutation {
6353                export: "Thing".to_string(),
6354                key: json!("n"),
6355                patch: json!({"name": "thing-n"}),
6356                append: vec![],
6357            };
6358            creation.mark_created();
6359            let joined = Mutation {
6360                export: "Thing".to_string(),
6361                key: json!("n"),
6362                patch: json!({"flag": true}),
6363                append: vec![],
6364            };
6365            harness
6366                .tx
6367                .send(MutationBatch::with_slot_context(
6368                    vec![creation, joined].into_iter().collect(),
6369                    SlotContext::new(slot, 0),
6370                ))
6371                .await
6372                .unwrap();
6373
6374            let upsert = next_frame(&mut socket).await;
6375            assert_eq!(upsert["key"], "n");
6376            assert_eq!(upsert["op"], "upsert", "unexpected frame: {upsert}");
6377            assert_eq!(upsert["data"]["name"], "thing-n");
6378            assert_eq!(upsert["data"]["flag"], true);
6379            let (_, sent) = version_of(&upsert["data"]);
6380
6381            // The next frame is the key's next change, not the joined patch.
6382            harness.patch("n", json!({"count": 9})).await;
6383            let patch = next_frame(&mut socket).await;
6384            assert_eq!(patch["key"], "n");
6385            assert_eq!(patch["op"], "patch", "unexpected frame: {patch}");
6386            assert_eq!(patch["data"]["count"], 9, "unexpected frame: {patch}");
6387            let (_, patched) = version_of(&patch["data"]);
6388            assert!(patched > sent, "{patched} after {sent}");
6389            socket.close(None).await.ok();
6390        }
6391
6392        /// Eviction from the server's bounded cache is not a change to the
6393        /// entity: a client holding it keeps getting its patches, never a
6394        /// `remove`, and the whole entity replaces its copy when it returns.
6395        #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6396        async fn a_state_holder_keeps_its_patches_when_the_cache_evicts_the_key() {
6397            let harness = Harness::start_with_cache(
6398                WebSocketDeliveryConfig::default(),
6399                EntityCache::with_config(crate::cache::EntityCacheConfig {
6400                    max_entities_per_view: 2,
6401                    ..Default::default()
6402                }),
6403            )
6404            .await;
6405            harness
6406                .patch("a", json!({"name": "thing-a", "count": 1}))
6407                .await;
6408            let mut socket = harness
6409                .subscribe(json!({"view": "Thing/state", "key": "a"}), true)
6410                .await;
6411            assert_eq!(snapshot_keys(&mut socket).await, ["a"]);
6412
6413            // "b" and "c" push "a" out of the cache.
6414            for key in ["b", "c"] {
6415                harness
6416                    .patch(key, json!({"name": format!("thing-{key}"), "count": 1}))
6417                    .await;
6418            }
6419            harness.patch("a", json!({"count": 2})).await;
6420            let frame = next_frame(&mut socket).await;
6421            assert_eq!(frame["key"], "a", "unexpected frame: {frame}");
6422            assert_eq!(frame["op"], "patch", "the holder merges it: {frame}");
6423            assert_eq!(frame["data"]["count"], 2);
6424
6425            // The whole entity comes back from the VM.
6426            let mut whole = Mutation {
6427                export: "Thing".to_string(),
6428                key: json!("a"),
6429                patch: json!({"name": "thing-a", "count": 2}),
6430                append: vec![],
6431            };
6432            whole.mark_whole_entity();
6433            harness.mutate(whole).await;
6434            let frame = next_frame(&mut socket).await;
6435            assert_eq!(frame["op"], "upsert", "unexpected frame: {frame}");
6436            assert_eq!(frame["data"]["name"], "thing-a");
6437            assert_eq!(frame["data"]["count"], 2);
6438            assert!(frame["data"]
6439                .get(arete_interpreter::WHOLE_ENTITY_MARKER)
6440                .is_none());
6441
6442            harness.patch("a", json!({"count": 3})).await;
6443            let frame = next_frame(&mut socket).await;
6444            assert_eq!(frame["op"], "patch", "unexpected frame: {frame}");
6445            socket.close(None).await.ok();
6446        }
6447
6448        #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6449        async fn a_state_view_without_a_snapshot_upserts_on_the_first_change() {
6450            let harness = Harness::start(WebSocketDeliveryConfig::default()).await;
6451            seed(&harness).await;
6452
6453            let mut socket = harness
6454                .subscribe(json!({"view": "Thing/state", "key": "a"}), false)
6455                .await;
6456            harness.patch("a", json!({"count": 2})).await;
6457            let frame = next_frame(&mut socket).await;
6458            assert_eq!(frame["key"], "a");
6459            assert_eq!(frame["op"], "upsert", "unexpected frame: {frame}");
6460            assert_eq!(frame["data"]["name"], "thing-a");
6461            assert_eq!(frame["data"]["count"], 2);
6462
6463            harness.patch("a", json!({"count": 3})).await;
6464            let frame = next_frame(&mut socket).await;
6465            assert_eq!(frame["op"], "patch", "unexpected frame: {frame}");
6466            socket.close(None).await.ok();
6467        }
6468
6469        #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6470        async fn a_coalesced_list_sends_no_remove_for_a_key_it_never_sent() {
6471            let harness = Harness::start(WebSocketDeliveryConfig {
6472                collection_coalesce_ms: Some(10),
6473                ..Default::default()
6474            })
6475            .await;
6476            seed(&harness).await;
6477
6478            // A one-row window holding "c", which the disabled snapshot never
6479            // sent.
6480            let mut socket = harness
6481                .subscribe(json!({"view": "Thing/list", "take": 1}), false)
6482                .await;
6483            // "a" jumps to the front: it enters the window whole and "c",
6484            // never sent, leaves it without a `remove`.
6485            harness.patch("a", json!({"count": 2})).await;
6486            let frame = next_frame(&mut socket).await;
6487            assert_eq!(frame["key"], "a", "unexpected frame: {frame}");
6488            assert_eq!(frame["op"], "upsert");
6489            assert_eq!(frame["data"]["name"], "thing-a");
6490
6491            // "c" re-enters whole; "a", held, leaves with a `remove`.
6492            harness.patch("c", json!({"count": 2})).await;
6493            let mut frames = [next_frame(&mut socket).await, next_frame(&mut socket).await];
6494            frames.sort_by_key(|frame| frame["key"].as_str().unwrap().to_string());
6495            assert_eq!(frames[0]["key"], "a");
6496            assert_eq!(frames[0]["op"], "remove");
6497            assert_eq!(frames[1]["key"], "c");
6498            assert_eq!(frames[1]["op"], "upsert");
6499            assert_eq!(frames[1]["data"]["name"], "thing-c");
6500            socket.close(None).await.ok();
6501        }
6502
6503        /// `(epoch, counter)` from an entity's `_version`.
6504        fn version_of(data: &Value) -> (String, u64) {
6505            let version = data["_version"]
6506                .as_str()
6507                .unwrap_or_else(|| panic!("no _version in {data}"));
6508            let (epoch, counter) = version.split_once(':').expect("epoch:counter");
6509            (epoch.to_string(), counter.parse().expect("decimal counter"))
6510        }
6511
6512        /// Every entity a subscriber receives carries the projector's
6513        /// `_version`: a snapshot row, a forwarded patch, and a state
6514        /// subscription's catch-up, which sends the cached entity after the
6515        /// bus overwrote a patch.
6516        // One worker, and a projector the cooperative budget cannot interrupt:
6517        // within a batch it awaits only locks no other task holds, so it
6518        // publishes both changes before the subscriber can run, and the
6519        // second overwrites the first on the state bus.
6520        #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
6521        async fn snapshot_rows_patches_and_catch_ups_carry_the_version() {
6522            let harness = Harness::start(WebSocketDeliveryConfig::default()).await;
6523            harness.patch("7", json!({"name": "thing-7"})).await;
6524
6525            let mut list = harness.subscribe(json!({"view": "Thing/list"}), true).await;
6526            let snapshot = next_frame(&mut list).await;
6527            assert_eq!(snapshot["op"], "snapshot", "unexpected frame: {snapshot}");
6528            let (epoch, _) = version_of(&snapshot["data"][0]["data"]);
6529
6530            let mut state = harness
6531                .subscribe(json!({"view": "Thing/state", "key": "7"}), true)
6532                .await;
6533            let snapshot = next_frame(&mut state).await;
6534            assert_eq!(snapshot["op"], "snapshot", "unexpected frame: {snapshot}");
6535            let (state_epoch, seeded) = version_of(&snapshot["data"][0]["data"]);
6536            assert_eq!(state_epoch, epoch, "one projector, one epoch");
6537
6538            harness.patch("7", json!({"count": 1})).await;
6539            let patch = next_frame(&mut list).await;
6540            assert_eq!(patch["op"], "patch", "unexpected frame: {patch}");
6541            assert_eq!(version_of(&patch["data"]).0, epoch);
6542            let patch = next_frame(&mut state).await;
6543            assert_eq!(patch["op"], "patch", "unexpected frame: {patch}");
6544            let (_, patched) = version_of(&patch["data"]);
6545            assert!(patched > seeded, "{patched} after {seeded}");
6546
6547            // Two changes to the key in one batch.
6548            let slot = harness
6549                .slot
6550                .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
6551            let mutation = |patch: Value| Mutation {
6552                export: "Thing".to_string(),
6553                key: json!("7"),
6554                patch,
6555                append: vec![],
6556            };
6557            harness
6558                .tx
6559                .send(MutationBatch::with_slot_context(
6560                    vec![
6561                        mutation(json!({"count": 2})),
6562                        mutation(json!({"flag": true})),
6563                    ]
6564                    .into_iter()
6565                    .collect(),
6566                    SlotContext::new(slot, 0),
6567                ))
6568                .await
6569                .unwrap();
6570
6571            let catch_up = next_frame(&mut state).await;
6572            assert_eq!(catch_up["op"], "patch", "unexpected frame: {catch_up}");
6573            assert!(catch_up.get("seq").is_none(), "a catch-up: {catch_up}");
6574            assert_eq!(catch_up["data"]["count"], 2);
6575            assert_eq!(catch_up["data"]["flag"], true);
6576            let (_, caught_up) = version_of(&catch_up["data"]);
6577            assert!(caught_up > patched, "{caught_up} after {patched}");
6578
6579            list.close(None).await.ok();
6580            state.close(None).await.ok();
6581        }
6582    }
6583
6584    /// Session tokens that expire while their socket is open.
6585    mod session_expiry {
6586        use super::*;
6587        use crate::websocket::auth::SignedSessionAuthPlugin;
6588        use arete_auth::{KeyClass, SessionClaims, SigningKey, TokenSigner, TokenVerifier};
6589        use futures_util::{SinkExt, StreamExt};
6590        use std::time::Duration;
6591        use tokio::net::{TcpListener, TcpStream};
6592        use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
6593        use tokio_tungstenite::tungstenite::protocol::CloseFrame;
6594        use tokio_tungstenite::tungstenite::Message;
6595        use tokio_tungstenite::{client_async, WebSocketStream};
6596
6597        #[derive(Default)]
6598        struct RecordingUsageEmitter {
6599            events: tokio::sync::Mutex<Vec<WebSocketUsageEvent>>,
6600        }
6601
6602        #[async_trait::async_trait]
6603        impl WebSocketUsageEmitter for RecordingUsageEmitter {
6604            async fn emit(&self, event: WebSocketUsageEvent) {
6605                self.events.lock().await.push(event);
6606            }
6607        }
6608
6609        struct Server {
6610            addr: SocketAddr,
6611            signer: TokenSigner,
6612            usage: Arc<RecordingUsageEmitter>,
6613            metrics: Arc<DeliveryProbe>,
6614        }
6615
6616        impl Server {
6617            async fn start() -> Self {
6618                let signing_key = SigningKey::generate();
6619                let verifier =
6620                    TokenVerifier::new(signing_key.verifying_key(), "test-issuer", "test-audience");
6621                let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6622                let addr = listener.local_addr().unwrap();
6623                let usage = Arc::new(RecordingUsageEmitter::default());
6624                let server = WebSocketServer::new(
6625                    addr,
6626                    BusManager::new(),
6627                    EntityCache::new(),
6628                    Arc::new(ViewIndex::new()),
6629                    #[cfg(feature = "otel")]
6630                    None,
6631                )
6632                .with_auth_plugin(Arc::new(SignedSessionAuthPlugin::new(verifier)))
6633                .with_usage_emitter(usage.clone());
6634                let metrics = Arc::new(DeliveryProbe::default());
6635                let (acceptor, _cleanup) = server.into_acceptor();
6636                let acceptor = acceptor.with_delivery_probe(metrics.clone());
6637                tokio::spawn(async move { acceptor.serve_listener(listener).await });
6638                Self {
6639                    addr,
6640                    signer: TokenSigner::new(signing_key, "test-issuer"),
6641                    usage,
6642                    metrics,
6643                }
6644            }
6645
6646            fn token(&self, ttl_seconds: u64) -> String {
6647                let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
6648                    .with_scope("read")
6649                    .with_key_class(KeyClass::Secret)
6650                    .with_ttl(ttl_seconds)
6651                    .build();
6652                self.signer.sign(claims).unwrap()
6653            }
6654
6655            fn account_token(&self, ttl_seconds: u64, account: &str) -> String {
6656                let claims = SessionClaims::builder("test-issuer", account, "test-audience")
6657                    .with_scope("read")
6658                    .with_key_class(KeyClass::Secret)
6659                    .with_metering_key(account)
6660                    .with_plan("agent_trial")
6661                    .with_actor_key(account)
6662                    .with_account_key(account)
6663                    .with_consumer_key(format!("consumer:{account}"))
6664                    .with_policy_version(3)
6665                    .with_account_limits(Default::default())
6666                    .with_ttl(ttl_seconds)
6667                    .build();
6668                self.signer.sign(claims).unwrap()
6669            }
6670
6671            async fn connect(&self, token: &str) -> WebSocketStream<TcpStream> {
6672                let stream = TcpStream::connect(self.addr).await.unwrap();
6673                client_async(format!("ws://{}/?hs_token={token}", self.addr), stream)
6674                    .await
6675                    .unwrap()
6676                    .0
6677            }
6678        }
6679
6680        async fn send_json(socket: &mut WebSocketStream<TcpStream>, message: Value) {
6681            socket
6682                .send(Message::Text(message.to_string().into()))
6683                .await
6684                .unwrap();
6685        }
6686
6687        /// The close frame, if the server closes the socket within `wait`.
6688        async fn close_within(
6689            socket: &mut WebSocketStream<TcpStream>,
6690            wait: Duration,
6691        ) -> Option<Option<CloseFrame>> {
6692            tokio::time::timeout(wait, async {
6693                while let Some(Ok(message)) = socket.next().await {
6694                    if let Message::Close(frame) = message {
6695                        return Some(frame);
6696                    }
6697                }
6698                Some(None)
6699            })
6700            .await
6701            .ok()
6702            .flatten()
6703        }
6704
6705        #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6706        async fn an_expired_session_is_closed_with_the_reason() {
6707            let server = Server::start().await;
6708            let mut socket = server.connect(&server.token(2)).await;
6709
6710            tokio::time::sleep(Duration::from_secs(3)).await;
6711            send_json(&mut socket, json!({"type": "ping"})).await;
6712
6713            let frame = close_within(&mut socket, Duration::from_secs(5))
6714                .await
6715                .expect("the server closes the expired session")
6716                .expect("the close frame carries a reason");
6717            assert_eq!(frame.code, CloseCode::Policy);
6718            assert_eq!(
6719                frame.reason.as_str(),
6720                "token-expired: Authentication token expired"
6721            );
6722        }
6723
6724        #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6725        async fn a_session_refreshed_in_band_outlives_its_first_token() {
6726            let server = Server::start().await;
6727            let mut socket = server.connect(&server.token(2)).await;
6728
6729            send_json(
6730                &mut socket,
6731                json!({"type": "refresh_auth", "token": server.token(3_600)}),
6732            )
6733            .await;
6734            let reply = tokio::time::timeout(Duration::from_secs(5), async {
6735                while let Some(Ok(message)) = socket.next().await {
6736                    if let Message::Text(text) = message {
6737                        return serde_json::from_str::<Value>(text.as_str()).ok();
6738                    }
6739                }
6740                None
6741            })
6742            .await
6743            .expect("the server answers the refresh")
6744            .expect("the answer is JSON");
6745            assert_eq!(reply["success"], true, "refresh accepted: {reply}");
6746
6747            tokio::time::sleep(Duration::from_secs(3)).await;
6748            send_json(&mut socket, json!({"type": "ping"})).await;
6749            assert!(
6750                close_within(&mut socket, Duration::from_millis(1_500))
6751                    .await
6752                    .is_none(),
6753                "the socket stays open on the refreshed token"
6754            );
6755        }
6756
6757        #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6758        async fn usage_after_auth_refresh_uses_the_refreshed_account() {
6759            let server = Server::start().await;
6760            let mut socket = server
6761                .connect(&server.account_token(3_600, "account:1"))
6762                .await;
6763
6764            send_json(
6765                &mut socket,
6766                json!({
6767                    "type": "refresh_auth",
6768                    "token": server.account_token(3_600, "account:2")
6769                }),
6770            )
6771            .await;
6772            let reply = tokio::time::timeout(Duration::from_secs(5), async {
6773                while let Some(Ok(message)) = socket.next().await {
6774                    if let Message::Text(text) = message {
6775                        return serde_json::from_str::<Value>(text.as_str()).ok();
6776                    }
6777                }
6778                None
6779            })
6780            .await
6781            .expect("the server answers the refresh")
6782            .expect("the answer is JSON");
6783            assert_eq!(reply["success"], true, "refresh accepted: {reply}");
6784            socket.close(None).await.unwrap();
6785
6786            let account = tokio::time::timeout(Duration::from_secs(5), async {
6787                loop {
6788                    let events = server.usage.events.lock().await;
6789                    if let Some(account) = events.iter().find_map(|event| match event {
6790                        WebSocketUsageEvent::ConnectionClosed { identity, .. } => {
6791                            identity.account_key.clone()
6792                        }
6793                        _ => None,
6794                    }) {
6795                        return account;
6796                    }
6797                    drop(events);
6798                    tokio::task::yield_now().await;
6799                }
6800            })
6801            .await
6802            .expect("connection close usage event");
6803            assert_eq!(account, "account:2");
6804            let active = server
6805                .metrics
6806                .active_connections
6807                .lock()
6808                .expect("delivery probe lock poisoned");
6809            assert_eq!(active.get("account:1"), Some(&0));
6810            assert_eq!(active.get("account:2"), None);
6811        }
6812    }
6813}
6814
6815#[cfg(test)]
6816mod retained_scope_regressions {
6817    use super::*;
6818    use crate::row_scope::{RowScope, ScopeField};
6819
6820    #[test]
6821    fn sparse_private_frames_never_inherit_a_later_public_scope() {
6822        let public = RowFilter::new(
6823            ScopeField::new("meta.scope"),
6824            Default::default(),
6825            Arc::new(tokio::sync::RwLock::new(0)),
6826            0,
6827        );
6828        let entitled = RowFilter::new(
6829            ScopeField::new("meta.scope"),
6830            ["B".to_string()].into_iter().collect(),
6831            Arc::new(tokio::sync::RwLock::new(0)),
6832            0,
6833        );
6834        let patch = br#"{"data":{"secret":"private-at-the-time"}}"#;
6835        let original_scope = RowScope::Scoped("B".into());
6836        assert!(!frame_in_row_scopes(
6837            Some(&public),
6838            Some(&original_scope),
6839            patch
6840        ));
6841        assert!(frame_in_row_scopes(
6842            Some(&entitled),
6843            Some(&original_scope),
6844            patch
6845        ));
6846        // The public transition itself is delivered, without relabeling old frames.
6847        assert!(frame_in_row_scopes(
6848            Some(&public),
6849            Some(&RowScope::Public),
6850            br#"{"data":{"meta":{"scope":null}}}"#
6851        ));
6852        // Legacy sparse records have no proof of membership: fail closed.
6853        assert!(!frame_in_row_scopes(Some(&public), None, patch));
6854        assert!(!frame_in_row_scopes(Some(&entitled), None, patch));
6855        assert!(frame_in_row_scopes(None, None, patch));
6856    }
6857}
6858
6859#[cfg(test)]
6860mod limits_tests {
6861    use super::*;
6862    use futures_util::SinkExt;
6863    use tokio::io::AsyncReadExt;
6864    use tokio_tungstenite::client_async;
6865    use tokio_tungstenite::tungstenite::Message;
6866
6867    fn limits() -> WebSocketLimitsConfig {
6868        WebSocketLimitsConfig {
6869            max_message_bytes: 16 * 1024,
6870            max_frame_bytes: 16 * 1024,
6871            handshake_timeout: Duration::from_millis(300),
6872            max_pending_handshakes: 1,
6873        }
6874    }
6875
6876    async fn start(limits: WebSocketLimitsConfig) -> SocketAddr {
6877        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6878        let addr = listener.local_addr().unwrap();
6879        let server = WebSocketServer::new(
6880            addr,
6881            BusManager::new(),
6882            EntityCache::new(),
6883            Arc::new(ViewIndex::new()),
6884            #[cfg(feature = "otel")]
6885            None,
6886        )
6887        .with_limits(limits);
6888        let (acceptor, _cleanup) = server.into_acceptor();
6889        tokio::spawn(async move { acceptor.serve_listener(listener).await });
6890        addr
6891    }
6892
6893    /// Resolves once the server closes `stream`, or fails the test after `within`.
6894    async fn closed_within(stream: &mut TcpStream, within: Duration) {
6895        let mut buffer = [0; 1024];
6896        tokio::time::timeout(within, async {
6897            loop {
6898                match stream.read(&mut buffer).await {
6899                    Ok(0) | Err(_) => return,
6900                    Ok(_) => continue,
6901                }
6902            }
6903        })
6904        .await
6905        .expect("the server closes the connection");
6906    }
6907
6908    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6909    async fn a_connection_that_never_upgrades_is_dropped_at_the_deadline() {
6910        let addr = start(limits()).await;
6911        let mut stream = TcpStream::connect(addr).await.unwrap();
6912        let started = Instant::now();
6913        closed_within(&mut stream, Duration::from_secs(5)).await;
6914        assert!(started.elapsed() >= Duration::from_millis(250));
6915    }
6916
6917    /// Allows every connection, but only after `delay`.
6918    struct SlowAuthPlugin {
6919        delay: Duration,
6920    }
6921
6922    #[async_trait::async_trait]
6923    impl WebSocketAuthPlugin for SlowAuthPlugin {
6924        async fn authorize(&self, request: &ConnectionAuthRequest) -> AuthDecision {
6925            tokio::time::sleep(self.delay).await;
6926            crate::websocket::auth::AllowAllAuthPlugin
6927                .authorize(request)
6928                .await
6929        }
6930
6931        fn as_any(&self) -> &dyn std::any::Any {
6932            self
6933        }
6934    }
6935
6936    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6937    async fn authentication_slower_than_the_deadline_is_cut_off() {
6938        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6939        let addr = listener.local_addr().unwrap();
6940        let server = WebSocketServer::new(
6941            addr,
6942            BusManager::new(),
6943            EntityCache::new(),
6944            Arc::new(ViewIndex::new()),
6945            #[cfg(feature = "otel")]
6946            None,
6947        )
6948        .with_auth_plugin(Arc::new(SlowAuthPlugin {
6949            delay: Duration::from_secs(30),
6950        }))
6951        .with_limits(limits());
6952        let (acceptor, _cleanup) = server.into_acceptor();
6953        tokio::spawn(async move { acceptor.serve_listener(listener).await });
6954
6955        let started = Instant::now();
6956        let stream = TcpStream::connect(addr).await.unwrap();
6957        let handshake = tokio::time::timeout(
6958            Duration::from_secs(5),
6959            client_async(format!("ws://{addr}/"), stream),
6960        )
6961        .await
6962        .expect("the server ends the handshake before the test deadline");
6963        assert!(handshake.is_err(), "the upgrade is refused");
6964        assert!(started.elapsed() < Duration::from_secs(5));
6965    }
6966
6967    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6968    async fn pending_handshakes_past_the_cap_are_refused() {
6969        let addr = start(WebSocketLimitsConfig {
6970            handshake_timeout: Duration::from_secs(30),
6971            ..limits()
6972        })
6973        .await;
6974        let mut stalled = TcpStream::connect(addr).await.unwrap();
6975        // Let the server register the first connection as pending.
6976        tokio::time::sleep(Duration::from_millis(100)).await;
6977        let mut refused = TcpStream::connect(addr).await.unwrap();
6978        closed_within(&mut refused, Duration::from_secs(5)).await;
6979
6980        // The first is still held open within its own deadline.
6981        let mut buffer = [0; 16];
6982        assert!(
6983            tokio::time::timeout(Duration::from_millis(200), stalled.read(&mut buffer))
6984                .await
6985                .is_err()
6986        );
6987    }
6988
6989    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6990    async fn a_message_over_the_limit_closes_the_socket() {
6991        let addr = start(limits()).await;
6992        let stream = TcpStream::connect(addr).await.unwrap();
6993        let (mut socket, _) = client_async(format!("ws://{addr}/"), stream).await.unwrap();
6994        socket
6995            .send(Message::Text("x".repeat(64 * 1024).into()))
6996            .await
6997            .unwrap();
6998        let ended = tokio::time::timeout(Duration::from_secs(5), async {
6999            loop {
7000                match socket.next().await {
7001                    None | Some(Err(_)) | Some(Ok(Message::Close(_))) => return,
7002                    Some(Ok(_)) => continue,
7003                }
7004            }
7005        })
7006        .await;
7007        assert!(ended.is_ok(), "the server ends the session");
7008    }
7009
7010    /// An evicted client's session ends with the eviction. Before, the
7011    /// session waited on the peer: one that never answered the close frame
7012    /// kept its socket and session open after the registry, and every cap
7013    /// that counts the registry, had stopped counting it.
7014    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
7015    async fn an_evicted_session_ends_even_when_the_peer_ignores_the_close() {
7016        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
7017        let addr = listener.local_addr().unwrap();
7018        let server = WebSocketServer::new(
7019            addr,
7020            BusManager::new(),
7021            EntityCache::new(),
7022            Arc::new(ViewIndex::new()),
7023            #[cfg(feature = "otel")]
7024            None,
7025        )
7026        // Long enough that the server's own cleanup tick at startup cannot
7027        // evict the client before the test sees it register.
7028        .with_rate_limit_config(RateLimitConfig::default().with_timeout(Duration::from_millis(300)))
7029        .with_limits(limits());
7030        let (acceptor, _cleanup) = server.into_acceptor();
7031        let probe = acceptor.clone();
7032        tokio::spawn(async move { acceptor.serve_listener(listener).await });
7033
7034        let stream = TcpStream::connect(addr).await.unwrap();
7035        // Held but never polled again: the peer does not answer the close.
7036        let (_socket, _) = client_async(format!("ws://{addr}/"), stream).await.unwrap();
7037        tokio::time::timeout(Duration::from_secs(5), async {
7038            while probe.client_manager.client_count() == 0 {
7039                tokio::time::sleep(Duration::from_millis(10)).await;
7040            }
7041        })
7042        .await
7043        .expect("the client registers");
7044
7045        // Silent past the timeout, the client is stale. The test evicts it,
7046        // unless a late cleanup tick of the server's own already has.
7047        tokio::time::sleep(Duration::from_millis(400)).await;
7048        probe.client_manager.cleanup_stale_clients();
7049        assert_eq!(probe.client_manager.client_count(), 0);
7050        tokio::time::timeout(Duration::from_secs(5), async {
7051            while !probe.sessions.is_empty() {
7052                tokio::time::sleep(Duration::from_millis(10)).await;
7053            }
7054        })
7055        .await
7056        .expect("the evicted session ends");
7057    }
7058}