Skip to content

Commit b454c5b

Browse files
authored
feat: fetch api (#15)
1 parent 1fb0f78 commit b454c5b

File tree

5 files changed

+339
-29
lines changed

5 files changed

+339
-29
lines changed

relay_client/src/client.rs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,16 @@ use {
33
crate::{ConnectionOptions, Error},
44
relay_rpc::{
55
domain::{SubscriptionId, Topic},
6-
rpc::{BatchSubscribe, BatchUnsubscribe, Publish, Subscribe, Subscription, Unsubscribe},
6+
rpc::{
7+
BatchFetchMessages,
8+
BatchSubscribe,
9+
BatchUnsubscribe,
10+
FetchMessages,
11+
Publish,
12+
Subscribe,
13+
Subscription,
14+
Unsubscribe,
15+
},
716
},
817
std::{sync::Arc, time::Duration},
918
tokio::sync::{
@@ -12,9 +21,10 @@ use {
1221
},
1322
tokio_tungstenite::tungstenite::protocol::CloseFrame,
1423
};
15-
pub use {inbound::*, outbound::*, stream::*};
24+
pub use {fetch::*, inbound::*, outbound::*, stream::*};
1625

1726
mod connection;
27+
mod fetch;
1828
mod inbound;
1929
mod outbound;
2030
mod stream;
@@ -131,6 +141,20 @@ impl Client {
131141
EmptyResponseFuture::new(response)
132142
}
133143

144+
/// Fetch mailbox messages for a specific topic.
145+
pub fn fetch(&self, topic: Topic) -> ResponseFuture<FetchMessages> {
146+
let (request, response) = create_request(FetchMessages { topic });
147+
148+
self.request(request);
149+
150+
response
151+
}
152+
153+
/// Fetch mailbox messages for a specific topic. Returns a [`Stream`].
154+
pub fn fetch_stream(&self, topics: impl Into<Vec<Topic>>) -> FetchMessageStream {
155+
FetchMessageStream::new(self.clone(), topics.into())
156+
}
157+
134158
/// Subscribes on multiple topics to receive messages.
135159
pub fn batch_subscribe(&self, topics: impl Into<Vec<Topic>>) -> ResponseFuture<BatchSubscribe> {
136160
let (request, response) = create_request(BatchSubscribe {
@@ -156,6 +180,17 @@ impl Client {
156180
EmptyResponseFuture::new(response)
157181
}
158182

183+
/// Fetch mailbox messages for multiple topics.
184+
pub fn batch_fetch(&self, topics: impl Into<Vec<Topic>>) -> ResponseFuture<BatchFetchMessages> {
185+
let (request, response) = create_request(BatchFetchMessages {
186+
topics: topics.into(),
187+
});
188+
189+
self.request(request);
190+
191+
response
192+
}
193+
159194
/// Opens a connection to the Relay.
160195
pub async fn connect(&self, opts: ConnectionOptions) -> Result<(), Error> {
161196
let (tx, rx) = oneshot::channel();
@@ -187,7 +222,7 @@ impl Client {
187222
}
188223
}
189224

190-
fn request(&self, request: OutboundRequest) {
225+
pub(crate) fn request(&self, request: OutboundRequest) {
191226
if let Err(err) = self
192227
.control_tx
193228
.send(ConnectionControl::OutboundRequest(request))

relay_client/src/client/fetch.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use {
2+
crate::{create_request, Client, Error, ResponseFuture},
3+
futures_util::{FutureExt, Stream},
4+
relay_rpc::{
5+
domain::Topic,
6+
rpc::{BatchFetchMessages, SubscriptionData},
7+
},
8+
std::{
9+
pin::Pin,
10+
task::{Context, Poll},
11+
},
12+
};
13+
14+
/// Stream that uses the `irn_batchFetch` RPC method to retrieve messages from
15+
/// the Relay.
16+
pub struct FetchMessageStream {
17+
client: Client,
18+
request: BatchFetchMessages,
19+
batch: Option<std::vec::IntoIter<SubscriptionData>>,
20+
batch_fut: Option<ResponseFuture<BatchFetchMessages>>,
21+
has_more: bool,
22+
}
23+
24+
impl FetchMessageStream {
25+
pub(super) fn new(client: Client, topics: impl Into<Vec<Topic>>) -> Self {
26+
let request = BatchFetchMessages {
27+
topics: topics.into(),
28+
};
29+
30+
Self {
31+
client,
32+
request,
33+
batch: None,
34+
batch_fut: None,
35+
has_more: true,
36+
}
37+
}
38+
39+
/// Clears all internal state so that on the next stream poll it returns
40+
/// `None` and finishes data streaming.
41+
#[inline]
42+
fn clear(&mut self) {
43+
self.batch = None;
44+
self.batch_fut = None;
45+
self.has_more = false;
46+
}
47+
}
48+
49+
impl Stream for FetchMessageStream {
50+
type Item = Result<SubscriptionData, Error>;
51+
52+
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
53+
loop {
54+
if let Some(batch) = &mut self.batch {
55+
// Drain the items from the batch, if we have one.
56+
match batch.next() {
57+
Some(data) => {
58+
return Poll::Ready(Some(Ok(data)));
59+
}
60+
61+
None => {
62+
// No more items in the batch, fetch the next batch.
63+
self.batch = None;
64+
}
65+
}
66+
} else if let Some(batch_fut) = &mut self.batch_fut {
67+
// Waiting for the next batch to arrive.
68+
match batch_fut.poll_unpin(cx) {
69+
// The next batch is ready. Update `has_more` flag and clear the batch future.
70+
Poll::Ready(Ok(response)) => {
71+
self.batch = Some(response.messages.into_iter());
72+
self.batch_fut = None;
73+
self.has_more = response.has_more;
74+
}
75+
76+
// Error receiving the next batch. This is unrecoverable, so clear the state and
77+
// end the stream.
78+
Poll::Ready(Err(err)) => {
79+
self.clear();
80+
81+
return Poll::Ready(Some(Err(err)));
82+
}
83+
84+
// The batch is not ready yet.
85+
Poll::Pending => {
86+
return Poll::Pending;
87+
}
88+
};
89+
} else if self.has_more {
90+
// We have neither a batch, or a batch future, but `has_more` flag is set. Set
91+
// up a future to receive the next batch.
92+
let (request, batch_fut) = create_request(self.request.clone());
93+
94+
self.client.request(request);
95+
self.batch_fut = Some(batch_fut);
96+
} else {
97+
// The stream can't produce any more items, since it doesn't have neither a
98+
// batch of data or a future for receiving the next batch, and `has_more` flag
99+
// is not set.
100+
return Poll::Ready(None);
101+
}
102+
}
103+
}
104+
}

relay_rpc/src/auth.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ pub fn encode_auth_token(
158158
let iss = {
159159
let client_id = DecodedClientId(*key.public_key().as_bytes());
160160

161-
format!("{DID_PREFIX}{DID_DELIMITER}{DID_METHOD}{DID_DELIMITER}{client_id}",)
161+
format!("{DID_PREFIX}{DID_DELIMITER}{DID_METHOD}{DID_DELIMITER}{client_id}")
162162
};
163163

164164
let claims = {

relay_rpc/src/rpc.rs

Lines changed: 112 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ pub static JSON_RPC_VERSION: once_cell::sync::Lazy<Arc<str>> =
2121
/// See <https://github.com/WalletConnect/walletconnect-docs/blob/main/docs/specs/servers/relay/relay-server-rpc.md>
2222
pub const MAX_SUBSCRIPTION_BATCH_SIZE: usize = 500;
2323

24+
/// The maximum number of topics allowed for a batch fetch request.
25+
///
26+
/// See <https://github.com/WalletConnect/walletconnect-docs/blob/main/docs/specs/servers/relay/relay-server-rpc.md>
27+
pub const MAX_FETCH_BATCH_SIZE: usize = 500;
28+
2429
type BoxError = Box<dyn std::error::Error + Send + Sync>;
2530

2631
/// Errors covering payload validation problems.
@@ -35,14 +40,11 @@ pub enum ValidationError {
3540
#[error("Invalid JSON RPC version")]
3641
JsonRpcVersion,
3742

38-
#[error(
39-
"The batch contains too many items. Maximum number of subscriptions is {}",
40-
MAX_SUBSCRIPTION_BATCH_SIZE
41-
)]
42-
BatchSubscriptionLimit,
43+
#[error("The batch contains too many items ({actual}). Maximum number of items is {limit}")]
44+
BatchLimitExceeded { limit: usize, actual: usize },
4345

4446
#[error("The batch contains no items")]
45-
BatchSubscriptionListEmpty,
47+
BatchEmpty,
4648
}
4749

4850
/// Errors caught while processing the request. These are meant to be serialized
@@ -333,6 +335,42 @@ impl RequestPayload for Unsubscribe {
333335
}
334336
}
335337

338+
/// Data structure representing fetch request params.
339+
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
340+
pub struct FetchMessages {
341+
/// The topic of the messages to fetch.
342+
pub topic: Topic,
343+
}
344+
345+
impl RequestPayload for FetchMessages {
346+
type Error = GenericError;
347+
type Response = FetchResponse;
348+
349+
fn validate(&self) -> Result<(), ValidationError> {
350+
self.topic
351+
.decode()
352+
.map_err(ValidationError::TopicDecoding)?;
353+
354+
Ok(())
355+
}
356+
357+
fn into_params(self) -> Params {
358+
Params::FetchMessages(self)
359+
}
360+
}
361+
362+
/// Data structure representing fetch response.
363+
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
364+
#[serde(rename_all = "camelCase")]
365+
pub struct FetchResponse {
366+
/// Array of messages fetched from the mailbox.
367+
pub messages: Vec<SubscriptionData>,
368+
369+
/// Flag that indicates whether the client should keep fetching the
370+
/// messages.
371+
pub has_more: bool,
372+
}
373+
336374
/// Multi-topic subscription request parameters.
337375
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
338376
pub struct BatchSubscribe {
@@ -345,12 +383,17 @@ impl RequestPayload for BatchSubscribe {
345383
type Response = Vec<SubscriptionId>;
346384

347385
fn validate(&self) -> Result<(), ValidationError> {
348-
if self.topics.is_empty() {
349-
return Err(ValidationError::BatchSubscriptionListEmpty);
386+
let batch_size = self.topics.len();
387+
388+
if batch_size == 0 {
389+
return Err(ValidationError::BatchEmpty);
350390
}
351391

352-
if self.topics.len() > MAX_SUBSCRIPTION_BATCH_SIZE {
353-
return Err(ValidationError::BatchSubscriptionLimit);
392+
if batch_size > MAX_SUBSCRIPTION_BATCH_SIZE {
393+
return Err(ValidationError::BatchLimitExceeded {
394+
limit: MAX_SUBSCRIPTION_BATCH_SIZE,
395+
actual: batch_size,
396+
});
354397
}
355398

356399
for topic in &self.topics {
@@ -377,12 +420,17 @@ impl RequestPayload for BatchUnsubscribe {
377420
type Response = bool;
378421

379422
fn validate(&self) -> Result<(), ValidationError> {
380-
if self.subscriptions.is_empty() {
381-
return Err(ValidationError::BatchSubscriptionListEmpty);
423+
let batch_size = self.subscriptions.len();
424+
425+
if batch_size == 0 {
426+
return Err(ValidationError::BatchEmpty);
382427
}
383428

384-
if self.subscriptions.len() > MAX_SUBSCRIPTION_BATCH_SIZE {
385-
return Err(ValidationError::BatchSubscriptionLimit);
429+
if batch_size > MAX_SUBSCRIPTION_BATCH_SIZE {
430+
return Err(ValidationError::BatchLimitExceeded {
431+
limit: MAX_SUBSCRIPTION_BATCH_SIZE,
432+
actual: batch_size,
433+
});
386434
}
387435

388436
for sub in &self.subscriptions {
@@ -397,6 +445,43 @@ impl RequestPayload for BatchUnsubscribe {
397445
}
398446
}
399447

448+
/// Data structure representing batch fetch request params.
449+
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
450+
pub struct BatchFetchMessages {
451+
/// The topics of the messages to fetch.
452+
pub topics: Vec<Topic>,
453+
}
454+
455+
impl RequestPayload for BatchFetchMessages {
456+
type Error = GenericError;
457+
type Response = FetchResponse;
458+
459+
fn validate(&self) -> Result<(), ValidationError> {
460+
let batch_size = self.topics.len();
461+
462+
if batch_size == 0 {
463+
return Err(ValidationError::BatchEmpty);
464+
}
465+
466+
if batch_size > MAX_FETCH_BATCH_SIZE {
467+
return Err(ValidationError::BatchLimitExceeded {
468+
limit: MAX_FETCH_BATCH_SIZE,
469+
actual: batch_size,
470+
});
471+
}
472+
473+
for topic in &self.topics {
474+
topic.decode().map_err(ValidationError::TopicDecoding)?;
475+
}
476+
477+
Ok(())
478+
}
479+
480+
fn into_params(self) -> Params {
481+
Params::BatchFetchMessages(self)
482+
}
483+
}
484+
400485
/// Data structure representing publish request params.
401486
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
402487
pub struct Publish {
@@ -550,6 +635,10 @@ pub enum Params {
550635
#[serde(rename = "irn_unsubscribe", alias = "iridium_unsubscribe")]
551636
Unsubscribe(Unsubscribe),
552637

638+
/// Parameters to fetch.
639+
#[serde(rename = "irn_fetchMessages", alias = "iridium_fetchMessages")]
640+
FetchMessages(FetchMessages),
641+
553642
/// Parameters to batch subscribe.
554643
#[serde(rename = "irn_batchSubscribe", alias = "iridium_batchSubscribe")]
555644
BatchSubscribe(BatchSubscribe),
@@ -558,6 +647,13 @@ pub enum Params {
558647
#[serde(rename = "irn_batchUnsubscribe", alias = "iridium_batchUnsubscribe")]
559648
BatchUnsubscribe(BatchUnsubscribe),
560649

650+
/// Parameters to batch fetch.
651+
#[serde(
652+
rename = "irn_batchFetchMessages",
653+
alias = "iridium_batchFetchMessages"
654+
)]
655+
BatchFetchMessages(BatchFetchMessages),
656+
561657
/// Parameters to publish.
562658
#[serde(rename = "irn_publish", alias = "iridium_publish")]
563659
Publish(Publish),
@@ -603,8 +699,10 @@ impl Request {
603699
match &self.params {
604700
Params::Subscribe(params) => params.validate(),
605701
Params::Unsubscribe(params) => params.validate(),
702+
Params::FetchMessages(params) => params.validate(),
606703
Params::BatchSubscribe(params) => params.validate(),
607704
Params::BatchUnsubscribe(params) => params.validate(),
705+
Params::BatchFetchMessages(params) => params.validate(),
608706
Params::Publish(params) => params.validate(),
609707
Params::Subscription(params) => params.validate(),
610708
}

0 commit comments

Comments
 (0)