1use crate::bos::BosStr;
7use crate::deps::fluent_uri::{
8 ParseError, Uri,
9 pct_enc::{
10 EString,
11 encoder::{Data as EncData, Query},
12 },
13};
14use crate::error::DecodeError;
15use crate::stream::StreamError;
16use crate::websocket::{WebSocketClient, WebSocketConnection, WsSink, WsStream};
17use crate::{CowStr, Data, IntoStatic, RawData, WsMessage};
18use alloc::borrow::ToOwned;
19use alloc::string::String;
20use alloc::string::ToString;
21use alloc::vec::Vec;
22use core::error::Error;
23use core::future::Future;
24use core::marker::PhantomData;
25#[cfg(not(target_arch = "wasm32"))]
26use n0_future::stream::Boxed;
27#[cfg(target_arch = "wasm32")]
28use n0_future::stream::BoxedLocal as Boxed;
29use serde::de::DeserializeOwned;
30use serde::{Deserialize, Serialize};
31use smol_str::SmolStr;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum MessageEncoding {
36 Json,
38 DagCbor,
40}
41
42pub trait SubscriptionResp {
50 const NSID: &'static str;
52
53 const ENCODING: MessageEncoding;
55
56 type Message<S: BosStr>;
58
59 type Error: Error + DeserializeOwned;
61
62 fn decode_message<'de, S>(bytes: &'de [u8]) -> Result<Self::Message<S>, DecodeError>
68 where
69 S: BosStr + Deserialize<'de>,
70 Self::Message<S>: Deserialize<'de>,
71 {
72 match Self::ENCODING {
73 MessageEncoding::Json => serde_json::from_slice(bytes).map_err(DecodeError::from),
74 MessageEncoding::DagCbor => {
75 serde_ipld_dagcbor::from_slice(bytes).map_err(DecodeError::from)
76 }
77 }
78 }
79}
80
81pub trait XrpcSubscription {
88 const NSID: &'static str;
90
91 const ENCODING: MessageEncoding;
93
94 const CUSTOM_PATH: Option<&'static str> = None;
97
98 type Stream: SubscriptionResp;
100
101 fn query_params(&self) -> Vec<(String, String)>
105 where
106 Self: Serialize,
107 {
108 serde_html_form::to_string(self)
110 .ok()
111 .map(|s| {
112 s.split('&')
113 .filter_map(|pair| {
114 let mut parts = pair.splitn(2, '=');
115 Some((parts.next()?.to_string(), parts.next()?.to_string()))
116 })
117 .collect()
118 })
119 .unwrap_or_default()
120 }
121}
122
123#[derive(Debug, serde::Deserialize)]
128pub struct EventHeader {
129 pub op: i64,
131 pub t: smol_str::SmolStr,
133}
134
135#[cfg(not(feature = "std"))]
139struct SliceCursor<'a> {
140 slice: &'a [u8],
141 position: usize,
142}
143
144#[cfg(not(feature = "std"))]
145impl<'a> SliceCursor<'a> {
146 fn new(slice: &'a [u8]) -> Self {
147 Self { slice, position: 0 }
148 }
149
150 fn position(&self) -> usize {
151 self.position
152 }
153}
154
155#[cfg(not(feature = "std"))]
156impl ciborium_io::Read for SliceCursor<'_> {
157 type Error = core::convert::Infallible;
158
159 fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), Self::Error> {
160 let end = self.position + buf.len();
161 buf.copy_from_slice(&self.slice[self.position..end]);
162 self.position = end;
163 Ok(())
164 }
165}
166
167#[cfg(feature = "std")]
172pub fn parse_event_header<'a>(bytes: &'a [u8]) -> Result<(EventHeader, &'a [u8]), DecodeError> {
173 let mut cursor = std::io::Cursor::new(bytes);
174 let header: EventHeader = ciborium::de::from_reader(&mut cursor)?;
175 let position = cursor.position() as usize;
176 drop(cursor); Ok((header, &bytes[position..]))
179}
180
181#[cfg(not(feature = "std"))]
186pub fn parse_event_header<'a>(bytes: &'a [u8]) -> Result<(EventHeader, &'a [u8]), DecodeError> {
187 let mut cursor = SliceCursor::new(bytes);
188 let header: EventHeader = ciborium::de::from_reader(&mut cursor)?;
189 let position = cursor.position();
190
191 Ok((header, &bytes[position..]))
192}
193
194pub fn decode_json_msg<S: SubscriptionResp>(
196 msg_result: Result<crate::websocket::WsMessage, StreamError>,
197) -> Option<Result<StreamMessage<SmolStr, S>, StreamError>>
198where
199 StreamMessage<SmolStr, S>: DeserializeOwned,
200{
201 use crate::websocket::WsMessage;
202
203 match msg_result {
204 Ok(WsMessage::Text(text)) => {
205 Some(S::decode_message::<SmolStr>(text.as_ref()).map_err(StreamError::decode))
206 }
207 Ok(WsMessage::Binary(bytes)) => {
208 #[cfg(feature = "zstd")]
209 {
210 match decompress_zstd(&bytes) {
212 Ok(decompressed) => Some(
213 S::decode_message::<SmolStr>(&decompressed).map_err(StreamError::decode),
214 ),
215 Err(_) => {
216 Some(S::decode_message::<SmolStr>(&bytes).map_err(StreamError::decode))
218 }
219 }
220 }
221 #[cfg(not(feature = "zstd"))]
222 {
223 Some(S::decode_message::<SmolStr>(&bytes).map_err(StreamError::decode))
224 }
225 }
226 Ok(WsMessage::Close(_)) => Some(Err(StreamError::closed())),
227 Err(e) => Some(Err(e)),
228 }
229}
230
231#[cfg(feature = "zstd")]
232fn decompress_zstd(bytes: &[u8]) -> Result<Vec<u8>, std::io::Error> {
233 use std::sync::OnceLock;
234 use zstd::stream::decode_all;
235
236 static DICTIONARY: OnceLock<Vec<u8>> = OnceLock::new();
237
238 let dict = DICTIONARY.get_or_init(|| include_bytes!("../../zstd_dictionary").to_vec());
239
240 decode_all(std::io::Cursor::new(bytes)).or_else(|_| {
241 let mut decoder = zstd::Decoder::with_dictionary(std::io::Cursor::new(bytes), dict)?;
243 let mut result = Vec::new();
244 std::io::Read::read_to_end(&mut decoder, &mut result)?;
245 Ok(result)
246 })
247}
248
249pub fn decode_cbor_msg<S: SubscriptionResp>(
251 msg_result: Result<crate::websocket::WsMessage, StreamError>,
252) -> Option<Result<StreamMessage<SmolStr, S>, StreamError>>
253where
254 StreamMessage<SmolStr, S>: DeserializeOwned,
255{
256 use crate::websocket::WsMessage;
257
258 match msg_result {
259 Ok(WsMessage::Binary(bytes)) => {
260 Some(S::decode_message::<SmolStr>(&bytes).map_err(StreamError::decode))
261 }
262 Ok(WsMessage::Text(_)) => Some(Err(StreamError::wrong_message_format(
263 "expected binary frame for CBOR, got text",
264 ))),
265 Ok(WsMessage::Close(_)) => Some(Err(StreamError::closed())),
266 Err(e) => Some(Err(e)),
267 }
268}
269
270pub trait SubscriptionControlMessage: Serialize {
280 type Subscription: XrpcSubscription;
282
283 fn encode(&self) -> Result<WsMessage, StreamError> {
287 Ok(WsMessage::from(
288 serde_json::to_string(&self).map_err(StreamError::encode)?,
289 ))
290 }
291
292 fn decode<'de>(frame: &'de [u8]) -> Result<Self, StreamError>
294 where
295 Self: Deserialize<'de>,
296 {
297 Ok(serde_json::from_slice(frame).map_err(StreamError::decode)?)
298 }
299}
300
301pub struct SubscriptionController<S: SubscriptionControlMessage> {
303 controller: WsSink,
304 _marker: PhantomData<fn() -> S>,
305}
306
307impl<S: SubscriptionControlMessage> SubscriptionController<S> {
308 pub fn new(controller: WsSink) -> Self {
310 Self {
311 controller,
312 _marker: PhantomData,
313 }
314 }
315
316 pub async fn configure(&mut self, params: &S) -> Result<(), StreamError> {
318 let message = params.encode()?;
319
320 n0_future::SinkExt::send(self.controller.get_mut(), message)
321 .await
322 .map_err(StreamError::transport)
323 }
324}
325
326pub struct SubscriptionStream<S: SubscriptionResp> {
331 _marker: PhantomData<fn() -> S>,
332 connection: WebSocketConnection,
333}
334
335impl<S: SubscriptionResp> SubscriptionStream<S> {
336 pub fn new(connection: WebSocketConnection) -> Self {
338 Self {
339 _marker: PhantomData,
340 connection,
341 }
342 }
343
344 pub fn connection(&self) -> &WebSocketConnection {
346 &self.connection
347 }
348
349 pub fn connection_mut(&mut self) -> &mut WebSocketConnection {
351 &mut self.connection
352 }
353
354 pub fn into_stream(
359 self,
360 ) -> (
361 WsSink,
362 Boxed<Result<StreamMessage<SmolStr, S>, StreamError>>,
363 )
364 where
365 StreamMessage<SmolStr, S>: DeserializeOwned,
366 {
367 use n0_future::StreamExt as _;
368
369 let (tx, rx) = self.connection.split();
370
371 #[cfg(not(target_arch = "wasm32"))]
372 let stream = match S::ENCODING {
373 MessageEncoding::Json => rx
374 .into_inner()
375 .filter_map(|msg| decode_json_msg::<S>(msg))
376 .boxed(),
377 MessageEncoding::DagCbor => rx
378 .into_inner()
379 .filter_map(|msg| decode_cbor_msg::<S>(msg))
380 .boxed(),
381 };
382
383 #[cfg(target_arch = "wasm32")]
384 let stream = match S::ENCODING {
385 MessageEncoding::Json => rx
386 .into_inner()
387 .filter_map(|msg| decode_json_msg::<S>(msg))
388 .boxed_local(),
389 MessageEncoding::DagCbor => rx
390 .into_inner()
391 .filter_map(|msg| decode_cbor_msg::<S>(msg))
392 .boxed_local(),
393 };
394
395 (tx, stream)
396 }
397
398 pub fn into_raw_data_stream(self) -> (WsSink, Boxed<Result<RawData<'static>, StreamError>>) {
400 use n0_future::StreamExt as _;
401
402 let (tx, rx) = self.connection.split();
403
404 fn parse_msg<'a>(bytes: &'a [u8]) -> Result<RawData<'a>, serde_json::Error> {
405 serde_json::from_slice(bytes)
406 }
407 fn parse_cbor<'a>(
408 bytes: &'a [u8],
409 ) -> Result<RawData<'a>, serde_ipld_dagcbor::DecodeError<core::convert::Infallible>>
410 {
411 serde_ipld_dagcbor::from_slice(bytes)
412 }
413
414 #[cfg(not(target_arch = "wasm32"))]
415 let stream = match S::ENCODING {
416 MessageEncoding::Json => rx
417 .into_inner()
418 .filter_map(|msg_result| match msg_result {
419 Ok(WsMessage::Text(text)) => Some(
420 parse_msg(text.as_ref())
421 .map(|v| v.into_static())
422 .map_err(StreamError::decode),
423 ),
424 Ok(WsMessage::Binary(bytes)) => {
425 #[cfg(feature = "zstd")]
426 {
427 match decompress_zstd(&bytes) {
428 Ok(decompressed) => Some(
429 parse_msg(&decompressed)
430 .map(|v| v.into_static())
431 .map_err(StreamError::decode),
432 ),
433 Err(_) => Some(
434 parse_msg(&bytes)
435 .map(|v| v.into_static())
436 .map_err(StreamError::decode),
437 ),
438 }
439 }
440 #[cfg(not(feature = "zstd"))]
441 {
442 Some(
443 parse_msg(&bytes)
444 .map(|v| v.into_static())
445 .map_err(StreamError::decode),
446 )
447 }
448 }
449 Ok(WsMessage::Close(_)) => Some(Err(StreamError::closed())),
450 Err(e) => Some(Err(e)),
451 })
452 .boxed(),
453 MessageEncoding::DagCbor => rx
454 .into_inner()
455 .filter_map(|msg_result| match msg_result {
456 Ok(WsMessage::Binary(bytes)) => Some(
457 parse_cbor(&bytes)
458 .map(|v| v.into_static())
459 .map_err(|e| StreamError::decode(crate::error::DecodeError::from(e))),
460 ),
461 Ok(WsMessage::Text(_)) => Some(Err(StreamError::wrong_message_format(
462 "expected binary frame for CBOR, got text",
463 ))),
464 Ok(WsMessage::Close(_)) => Some(Err(StreamError::closed())),
465 Err(e) => Some(Err(e)),
466 })
467 .boxed(),
468 };
469
470 #[cfg(target_arch = "wasm32")]
471 let stream = match S::ENCODING {
472 MessageEncoding::Json => rx
473 .into_inner()
474 .filter_map(|msg_result| match msg_result {
475 Ok(WsMessage::Text(text)) => Some(
476 parse_msg(text.as_ref())
477 .map(|v| v.into_static())
478 .map_err(StreamError::decode),
479 ),
480 Ok(WsMessage::Binary(bytes)) => {
481 #[cfg(feature = "zstd")]
482 {
483 match decompress_zstd(&bytes) {
484 Ok(decompressed) => Some(
485 parse_msg(&decompressed)
486 .map(|v| v.into_static())
487 .map_err(StreamError::decode),
488 ),
489 Err(_) => Some(
490 parse_msg(&bytes)
491 .map(|v| v.into_static())
492 .map_err(StreamError::decode),
493 ),
494 }
495 }
496 #[cfg(not(feature = "zstd"))]
497 {
498 Some(
499 parse_msg(&bytes)
500 .map(|v| v.into_static())
501 .map_err(StreamError::decode),
502 )
503 }
504 }
505 Ok(WsMessage::Close(_)) => Some(Err(StreamError::closed())),
506 Err(e) => Some(Err(e)),
507 })
508 .boxed_local(),
509 MessageEncoding::DagCbor => rx
510 .into_inner()
511 .filter_map(|msg_result| match msg_result {
512 Ok(WsMessage::Binary(bytes)) => Some(
513 parse_cbor(&bytes)
514 .map(|v| v.into_static())
515 .map_err(|e| StreamError::decode(crate::error::DecodeError::from(e))),
516 ),
517 Ok(WsMessage::Text(_)) => Some(Err(StreamError::wrong_message_format(
518 "expected binary frame for CBOR, got text",
519 ))),
520 Ok(WsMessage::Close(_)) => Some(Err(StreamError::closed())),
521 Err(e) => Some(Err(e)),
522 })
523 .boxed_local(),
524 };
525
526 (tx, stream)
527 }
528
529 pub fn into_data_stream(self) -> (WsSink, Boxed<Result<Data<smol_str::SmolStr>, StreamError>>) {
531 use n0_future::StreamExt as _;
532
533 let (tx, rx) = self.connection.split();
534
535 fn parse_msg(bytes: &[u8]) -> Result<Data<smol_str::SmolStr>, serde_json::Error> {
536 serde_json::from_slice(bytes)
537 }
538 fn parse_cbor(
539 bytes: &[u8],
540 ) -> Result<
541 Data<smol_str::SmolStr>,
542 serde_ipld_dagcbor::DecodeError<core::convert::Infallible>,
543 > {
544 serde_ipld_dagcbor::from_slice(bytes)
545 }
546
547 #[cfg(not(target_arch = "wasm32"))]
548 let stream = match S::ENCODING {
549 MessageEncoding::Json => rx
550 .into_inner()
551 .filter_map(|msg_result| match msg_result {
552 Ok(WsMessage::Text(text)) => Some(
553 parse_msg(text.as_ref())
554 .map(|v| v.into_static())
555 .map_err(StreamError::decode),
556 ),
557 Ok(WsMessage::Binary(bytes)) => {
558 #[cfg(feature = "zstd")]
559 {
560 match decompress_zstd(&bytes) {
561 Ok(decompressed) => Some(
562 parse_msg(&decompressed)
563 .map(|v| v.into_static())
564 .map_err(StreamError::decode),
565 ),
566 Err(_) => Some(
567 parse_msg(&bytes)
568 .map(|v| v.into_static())
569 .map_err(StreamError::decode),
570 ),
571 }
572 }
573 #[cfg(not(feature = "zstd"))]
574 {
575 Some(
576 parse_msg(&bytes)
577 .map(|v| v.into_static())
578 .map_err(StreamError::decode),
579 )
580 }
581 }
582 Ok(WsMessage::Close(_)) => Some(Err(StreamError::closed())),
583 Err(e) => Some(Err(e)),
584 })
585 .boxed(),
586 MessageEncoding::DagCbor => rx
587 .into_inner()
588 .filter_map(|msg_result| match msg_result {
589 Ok(WsMessage::Binary(bytes)) => Some(
590 parse_cbor(&bytes)
591 .map(|v| v.into_static())
592 .map_err(|e| StreamError::decode(crate::error::DecodeError::from(e))),
593 ),
594 Ok(WsMessage::Text(_)) => Some(Err(StreamError::wrong_message_format(
595 "expected binary frame for CBOR, got text",
596 ))),
597 Ok(WsMessage::Close(_)) => Some(Err(StreamError::closed())),
598 Err(e) => Some(Err(e)),
599 })
600 .boxed(),
601 };
602
603 #[cfg(target_arch = "wasm32")]
604 let stream = match S::ENCODING {
605 MessageEncoding::Json => rx
606 .into_inner()
607 .filter_map(|msg_result| match msg_result {
608 Ok(WsMessage::Text(text)) => Some(
609 parse_msg(text.as_ref())
610 .map(|v| v.into_static())
611 .map_err(StreamError::decode),
612 ),
613 Ok(WsMessage::Binary(bytes)) => {
614 #[cfg(feature = "zstd")]
615 {
616 match decompress_zstd(&bytes) {
617 Ok(decompressed) => Some(
618 parse_msg(&decompressed)
619 .map(|v| v.into_static())
620 .map_err(StreamError::decode),
621 ),
622 Err(_) => Some(
623 parse_msg(&bytes)
624 .map(|v| v.into_static())
625 .map_err(StreamError::decode),
626 ),
627 }
628 }
629 #[cfg(not(feature = "zstd"))]
630 {
631 Some(
632 parse_msg(&bytes)
633 .map(|v| v.into_static())
634 .map_err(StreamError::decode),
635 )
636 }
637 }
638 Ok(WsMessage::Close(_)) => Some(Err(StreamError::closed())),
639 Err(e) => Some(Err(e)),
640 })
641 .boxed_local(),
642 MessageEncoding::DagCbor => rx
643 .into_inner()
644 .filter_map(|msg_result| match msg_result {
645 Ok(WsMessage::Binary(bytes)) => Some(
646 parse_cbor(&bytes)
647 .map(|v| v.into_static())
648 .map_err(|e| StreamError::decode(crate::error::DecodeError::from(e))),
649 ),
650 Ok(WsMessage::Text(_)) => Some(Err(StreamError::wrong_message_format(
651 "expected binary frame for CBOR, got text",
652 ))),
653 Ok(WsMessage::Close(_)) => Some(Err(StreamError::closed())),
654 Err(e) => Some(Err(e)),
655 })
656 .boxed_local(),
657 };
658
659 (tx, stream)
660 }
661
662 pub fn into_connection(self) -> WebSocketConnection {
664 self.connection
665 }
666
667 pub fn tee(&mut self) -> Boxed<Result<StreamMessage<SmolStr, S>, StreamError>>
673 where
674 StreamMessage<SmolStr, S>: DeserializeOwned,
675 {
676 use n0_future::StreamExt as _;
677
678 let rx = self.connection.receiver_mut();
679 let (raw_rx, typed_rx_source) =
680 core::mem::replace(rx, WsStream::new(n0_future::stream::empty())).tee();
681
682 *rx = raw_rx;
684
685 #[cfg(not(target_arch = "wasm32"))]
686 let stream = match S::ENCODING {
687 MessageEncoding::Json => typed_rx_source
688 .into_inner()
689 .filter_map(|msg| decode_json_msg::<S>(msg))
690 .boxed(),
691 MessageEncoding::DagCbor => typed_rx_source
692 .into_inner()
693 .filter_map(|msg| decode_cbor_msg::<S>(msg))
694 .boxed(),
695 };
696
697 #[cfg(target_arch = "wasm32")]
698 let stream = match S::ENCODING {
699 MessageEncoding::Json => typed_rx_source
700 .into_inner()
701 .filter_map(|msg| decode_json_msg::<S>(msg))
702 .boxed_local(),
703 MessageEncoding::DagCbor => typed_rx_source
704 .into_inner()
705 .filter_map(|msg| decode_cbor_msg::<S>(msg))
706 .boxed_local(),
707 };
708 stream
709 }
710}
711
712type StreamMessage<S, R> = <R as SubscriptionResp>::Message<S>;
713
714pub trait SubscriptionEndpoint {
722 const PATH: &'static str;
724
725 const ENCODING: MessageEncoding;
727
728 type Params<S: BosStr>: XrpcSubscription;
730
731 type Stream: SubscriptionResp;
733}
734
735#[derive(Debug, Default, Clone)]
737pub struct SubscriptionOptions<'a> {
738 pub headers: Vec<(CowStr<'a>, CowStr<'a>)>,
740}
741
742impl IntoStatic for SubscriptionOptions<'_> {
743 type Output = SubscriptionOptions<'static>;
744
745 fn into_static(self) -> Self::Output {
746 SubscriptionOptions {
747 headers: self
748 .headers
749 .into_iter()
750 .map(|(k, v)| (k.into_static(), v.into_static()))
751 .collect(),
752 }
753 }
754}
755
756pub trait SubscriptionExt: WebSocketClient {
760 fn subscription<'a>(&'a self, base: Uri<String>) -> SubscriptionCall<'a, Self>
762 where
763 Self: Sized,
764 {
765 SubscriptionCall {
766 client: self,
767 base,
768 opts: SubscriptionOptions::default(),
769 }
770 }
771}
772
773impl<T: WebSocketClient> SubscriptionExt for T {}
774
775fn build_subscription_uri(
793 base: &Uri<String>,
794 nsid: &str,
795 custom_path: Option<&str>,
796 query_params: &[(String, String)],
797) -> Result<Uri<String>, ParseError> {
798 let base_path = base.path().as_str().trim_end_matches('/');
799
800 let mut path = String::with_capacity(base_path.len() + 50);
802 path.push_str(base_path);
803 if let Some(custom_path) = custom_path {
804 path.push_str(custom_path);
805 } else {
806 path.push_str("/xrpc/");
807 path.push_str(nsid);
808 }
809
810 let query_str = if !query_params.is_empty() {
812 query_params
813 .iter()
814 .map(|(k, v)| {
815 let mut enc_k = EString::<Query>::new();
816 enc_k.encode_str::<EncData>(k.as_str());
817 let mut enc_v = EString::<Query>::new();
818 enc_v.encode_str::<EncData>(v.as_str());
819 alloc::format!("{}={}", enc_k, enc_v)
820 })
821 .collect::<Vec<_>>()
822 .join("&")
823 } else {
824 String::new()
825 };
826
827 let capacity = base.scheme().as_str().len()
829 + 3 + base.authority().map(|a| a.as_str().len()).unwrap_or(0)
831 + path.len()
832 + query_str.len()
833 + if !query_str.is_empty() { 1 } else { 0 }; let mut uri_str = String::with_capacity(capacity);
837 uri_str.push_str(base.scheme().as_str());
838 uri_str.push_str("://");
839
840 if let Some(authority) = base.authority() {
841 uri_str.push_str(authority.as_str());
842 }
843
844 uri_str.push_str(&path);
845
846 if !query_str.is_empty() {
847 uri_str.push('?');
848 uri_str.push_str(&query_str);
849 }
850
851 Uri::parse(uri_str)
852 .map(|u| u.to_owned())
853 .map_err(|(e, _)| e)
854}
855
856pub struct SubscriptionCall<'a, C: WebSocketClient> {
860 pub(crate) client: &'a C,
861 pub(crate) base: Uri<String>,
862 pub(crate) opts: SubscriptionOptions<'a>,
863}
864
865impl<'a, C: WebSocketClient> SubscriptionCall<'a, C> {
866 pub fn header(mut self, name: impl Into<CowStr<'a>>, value: impl Into<CowStr<'a>>) -> Self {
868 self.opts.headers.push((name.into(), value.into()));
869 self
870 }
871
872 pub fn with_options(mut self, opts: SubscriptionOptions<'a>) -> Self {
874 self.opts = opts;
875 self
876 }
877
878 pub async fn subscribe<Sub>(
884 self,
885 params: &Sub,
886 ) -> Result<SubscriptionStream<Sub::Stream>, C::Error>
887 where
888 Sub: XrpcSubscription + Serialize,
889 {
890 let query_params = params.query_params();
891 let uri = build_subscription_uri(&self.base, Sub::NSID, Sub::CUSTOM_PATH, &query_params)
892 .expect("subscription URI must be valid (base_uri + path always yields a valid URI)");
893
894 let connection = self
895 .client
896 .connect_with_headers(uri.borrow(), self.opts.headers)
897 .await?;
898
899 Ok(SubscriptionStream::new(connection))
900 }
901}
902
903#[cfg_attr(not(target_arch = "wasm32"), trait_variant::make(Send))]
908pub trait SubscriptionClient: WebSocketClient {
909 fn base_uri(&self) -> impl Future<Output = Uri<String>>;
911
912 fn subscription_opts(&self) -> impl Future<Output = SubscriptionOptions<'_>> {
914 async { SubscriptionOptions::default() }
915 }
916
917 #[cfg(not(target_arch = "wasm32"))]
919 fn subscribe<Sub>(
920 &self,
921 params: &Sub,
922 ) -> impl Future<Output = Result<SubscriptionStream<Sub::Stream>, Self::Error>>
923 where
924 Sub: XrpcSubscription + Serialize + Send + Sync,
925 Self: Sync;
926
927 #[cfg(target_arch = "wasm32")]
929 fn subscribe<Sub>(
930 &self,
931 params: &Sub,
932 ) -> impl Future<Output = Result<SubscriptionStream<Sub::Stream>, Self::Error>>
933 where
934 Sub: XrpcSubscription + Serialize + Send + Sync;
935
936 #[cfg(not(target_arch = "wasm32"))]
938 fn subscribe_with_opts<Sub>(
939 &self,
940 params: &Sub,
941 opts: SubscriptionOptions<'_>,
942 ) -> impl Future<Output = Result<SubscriptionStream<Sub::Stream>, Self::Error>>
943 where
944 Sub: XrpcSubscription + Serialize + Send + Sync,
945 Self: Sync;
946
947 #[cfg(target_arch = "wasm32")]
949 fn subscribe_with_opts<Sub>(
950 &self,
951 params: &Sub,
952 opts: SubscriptionOptions<'_>,
953 ) -> impl Future<Output = Result<SubscriptionStream<Sub::Stream>, Self::Error>>
954 where
955 Sub: XrpcSubscription + Serialize + Send + Sync;
956}
957
958pub struct BasicSubscriptionClient<W: WebSocketClient> {
964 client: W,
965 base_uri: Uri<String>,
966 opts: SubscriptionOptions<'static>,
967}
968
969impl<W: WebSocketClient> BasicSubscriptionClient<W> {
970 pub fn new(client: W, base_uri: Uri<String>) -> Self {
972 Self {
973 client,
974 base_uri,
975 opts: SubscriptionOptions::default(),
976 }
977 }
978
979 pub fn with_options(mut self, opts: SubscriptionOptions<'_>) -> Self {
981 self.opts = opts.into_static();
982 self
983 }
984
985 pub fn inner(&self) -> &W {
987 &self.client
988 }
989}
990
991impl<W: WebSocketClient> WebSocketClient for BasicSubscriptionClient<W> {
992 type Error = W::Error;
993
994 async fn connect(&self, uri: Uri<&str>) -> Result<WebSocketConnection, Self::Error> {
995 self.client.connect(uri).await
996 }
997
998 async fn connect_with_headers(
999 &self,
1000 uri: Uri<&str>,
1001 headers: Vec<(CowStr<'_>, CowStr<'_>)>,
1002 ) -> Result<WebSocketConnection, Self::Error> {
1003 self.client.connect_with_headers(uri, headers).await
1004 }
1005}
1006
1007impl<W: WebSocketClient> SubscriptionClient for BasicSubscriptionClient<W> {
1008 async fn base_uri(&self) -> Uri<String> {
1009 self.base_uri.clone()
1010 }
1011
1012 async fn subscription_opts(&self) -> SubscriptionOptions<'_> {
1013 self.opts.clone()
1014 }
1015
1016 #[cfg(not(target_arch = "wasm32"))]
1017 async fn subscribe<Sub>(
1018 &self,
1019 params: &Sub,
1020 ) -> Result<SubscriptionStream<Sub::Stream>, Self::Error>
1021 where
1022 Sub: XrpcSubscription + Serialize + Send + Sync,
1023 Self: Sync,
1024 {
1025 let opts = self.subscription_opts().await;
1026 self.subscribe_with_opts(params, opts).await
1027 }
1028
1029 #[cfg(target_arch = "wasm32")]
1030 async fn subscribe<Sub>(
1031 &self,
1032 params: &Sub,
1033 ) -> Result<SubscriptionStream<Sub::Stream>, Self::Error>
1034 where
1035 Sub: XrpcSubscription + Serialize + Send + Sync,
1036 {
1037 let opts = self.subscription_opts().await;
1038 self.subscribe_with_opts(params, opts).await
1039 }
1040
1041 #[cfg(not(target_arch = "wasm32"))]
1042 async fn subscribe_with_opts<Sub>(
1043 &self,
1044 params: &Sub,
1045 opts: SubscriptionOptions<'_>,
1046 ) -> Result<SubscriptionStream<Sub::Stream>, Self::Error>
1047 where
1048 Sub: XrpcSubscription + Serialize + Send + Sync,
1049 Self: Sync,
1050 {
1051 let base = self.base_uri().await;
1052 self.subscription(base)
1053 .with_options(opts)
1054 .subscribe(params)
1055 .await
1056 }
1057
1058 #[cfg(target_arch = "wasm32")]
1059 async fn subscribe_with_opts<Sub>(
1060 &self,
1061 params: &Sub,
1062 opts: SubscriptionOptions<'_>,
1063 ) -> Result<SubscriptionStream<Sub::Stream>, Self::Error>
1064 where
1065 Sub: XrpcSubscription + Serialize + Send + Sync,
1066 {
1067 let base = self.base_uri().await;
1068 self.subscription(base)
1069 .with_options(opts)
1070 .subscribe(params)
1071 .await
1072 }
1073}
1074
1075pub type TungsteniteSubscriptionClient =
1094 BasicSubscriptionClient<crate::websocket::tungstenite_client::TungsteniteClient>;
1095
1096impl TungsteniteSubscriptionClient {
1097 pub fn from_base_uri(base_uri: Uri<String>) -> Self {
1099 let client = crate::websocket::tungstenite_client::TungsteniteClient::new();
1100 BasicSubscriptionClient::new(client, base_uri)
1101 }
1102}
1103
1104#[cfg(test)]
1105mod tests {
1106 use super::*;
1107
1108 #[test]
1113 fn test_subscription_uri_with_nsid_path() {
1114 let base_uri = Uri::parse("wss://bsky.social/xrpc").unwrap().to_owned();
1115 let nsid = "com.example.subscribe";
1116 let query_params = vec![
1117 ("cursor".to_string(), "abc123".to_string()),
1118 ("filter".to_string(), "like".to_string()),
1119 ];
1120
1121 let uri = build_subscription_uri(&base_uri, nsid, None, &query_params)
1122 .expect("valid base uri and path should produce valid uri");
1123
1124 let uri_str = uri.as_str();
1126 assert!(uri_str.contains("/xrpc/com.example.subscribe"));
1127 assert!(uri_str.contains("cursor=abc123"));
1128 assert!(uri_str.contains("filter=like"));
1129 assert!(!uri_str.contains("//xrpc"));
1130 }
1131
1132 #[test]
1137 fn test_subscription_uri_with_custom_path() {
1138 let base_uri = Uri::parse("wss://jetstream.example.com")
1139 .unwrap()
1140 .to_owned();
1141 let custom_path = "/subscribe";
1142
1143 let uri = build_subscription_uri(&base_uri, "com.example.sub", Some(custom_path), &[])
1144 .expect("valid base uri and path should produce valid uri");
1145
1146 let uri_str = uri.as_str();
1148 assert!(uri_str.contains("/subscribe"));
1149 assert!(!uri_str.contains("/xrpc/"));
1150 }
1151
1152 #[test]
1157 fn test_subscription_uri_scheme_and_authority() {
1158 let base_uri = Uri::parse("wss://example.com:8080/path")
1159 .unwrap()
1160 .to_owned();
1161 let nsid = "com.example.test";
1162
1163 let uri = build_subscription_uri(&base_uri, nsid, None, &[])
1164 .expect("valid base uri and path should produce valid uri");
1165
1166 let uri_str = uri.as_str();
1168 assert!(uri_str.starts_with("wss://example.com:8080"));
1169 assert!(uri_str.contains("/path/xrpc/com.example.test"));
1170 }
1171
1172 #[test]
1174 fn test_query_parameters_encoding() {
1175 let base_uri = Uri::parse("wss://example.com").unwrap().to_owned();
1176 let params = vec![
1177 ("cursor".to_string(), "abc123".to_string()),
1178 ("filter".to_string(), "like".to_string()),
1179 ];
1180
1181 let uri = build_subscription_uri(&base_uri, "com.test", None, ¶ms)
1182 .expect("valid base uri and path should produce valid uri");
1183
1184 let uri_str = uri.as_str();
1186 assert!(uri_str.contains("?"));
1187 assert!(uri_str.contains("cursor=abc123"));
1188 assert!(uri_str.contains("filter=like"));
1189 assert!(uri_str.contains("&"));
1190 }
1191
1192 #[test]
1194 fn test_uri_trailing_slash_handling() {
1195 let base_uri = Uri::parse("wss://example.com/xrpc/").unwrap().to_owned();
1196
1197 let uri = build_subscription_uri(&base_uri, "com.example.test", None, &[])
1198 .expect("valid base uri and path should produce valid uri");
1199
1200 let uri_str = uri.as_str();
1202 assert!(!uri_str.contains("//xrpc"));
1203 assert!(uri_str.contains("/xrpc/com.example.test"));
1204 }
1205
1206 #[test]
1208 fn test_empty_query_parameters() {
1209 let base_uri = Uri::parse("wss://example.com").unwrap().to_owned();
1210
1211 let uri = build_subscription_uri(&base_uri, "com.example.test", None, &[])
1212 .expect("valid base uri and path should produce valid uri");
1213
1214 let uri_str = uri.as_str();
1216 assert!(!uri_str.contains("?"));
1217 assert!(uri_str.ends_with("com.example.test"));
1218 }
1219}