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