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