sui_rpc/client/ledger_streams/adapter/
mod.rs1use std::future::Future;
2use std::pin::Pin;
3
4use prost::Message;
5use prost::bytes::Bytes;
6use prost_types::FieldMask;
7use tonic::Request;
8use tonic::Status;
9use tonic::codegen::BoxStream;
10
11use super::super::Client;
12use super::super::Result;
13use super::observability::LedgerStreamFamily;
14use crate::proto::sui::rpc::v2::Ordering;
15use crate::proto::sui::rpc::v2::QueryEnd;
16use crate::proto::sui::rpc::v2::QueryOptions;
17use crate::proto::sui::rpc::v2::Watermark;
18
19mod checkpoint;
20mod event;
21mod progress;
22mod transaction;
23
24pub(super) use checkpoint::CheckpointAdapter;
25pub(super) use event::EventAdapter;
26pub(super) use progress::CursorDomain;
27pub(super) use progress::CursorGapUpper;
28pub(super) use progress::Progress;
29pub(super) use progress::ProgressAdvance;
30pub(super) use progress::Recovery;
31pub(super) use progress::RecoveryGap;
32pub(super) use transaction::TransactionAdapter;
33
34const CHECKPOINT_CURSOR_OVERFLOW: &str = "checkpoint subscription cursor cannot be resumed";
35
36pub(super) type RpcFuture<T> = Pin<Box<dyn Future<Output = Result<BoxStream<T>>> + Send + 'static>>;
37
38pub(super) struct PositionedItem<I, P> {
39 payload: I,
40 position: P,
41}
42
43impl<I, P> PositionedItem<I, P> {
44 fn new(payload: I, position: P) -> Self {
45 Self { payload, position }
46 }
47
48 pub(super) fn position(&self) -> &P {
49 &self.position
50 }
51
52 fn into_payload(self) -> I {
53 self.payload
54 }
55}
56
57pub(super) struct ListResponseParts<A: SubscriptionAdapter + ?Sized> {
58 pub(super) item: Option<A::Item>,
59 pub(super) watermark: Option<Watermark>,
60}
61
62pub(super) struct LiveFrame<I, P> {
63 pub(super) item: Option<I>,
64 pub(super) progress: P,
65}
66
67pub(super) fn validate_caller_read_mask<A: SubscriptionAdapter>(
68 read_mask: Option<&FieldMask>,
69) -> Result<()> {
70 let Some(read_mask) = read_mask else {
71 return Err(Status::invalid_argument(A::READ_MASK_REQUIREMENT));
72 };
73 if read_mask.paths.is_empty() {
74 return Err(Status::invalid_argument(A::READ_MASK_REQUIREMENT));
75 }
76 if read_mask.paths.iter().any(|path| path == "*") {
77 return Ok(());
78 }
79 if A::REQUIRED_READ_MASK_FIELDS.iter().all(|required| {
80 read_mask
81 .paths
82 .iter()
83 .any(|path| path.as_str() == *required)
84 }) {
85 Ok(())
86 } else {
87 Err(Status::invalid_argument(A::READ_MASK_REQUIREMENT))
88 }
89}
90
91pub(super) trait SubscriptionAdapter: Send + Sync + 'static {
93 const FAMILY: LedgerStreamFamily;
94 const REQUIRED_READ_MASK_FIELDS: &'static [&'static str];
96 const READ_MASK_REQUIREMENT: &'static str;
98
99 type Item: Send + 'static;
100 type ItemPosition: Clone + Eq + Ord + Send + 'static;
102 type Cursor: CursorDomain;
104 type Output: Send + 'static;
105 type ListRequest: Clone + Send + Sync + 'static;
106 type ListResponse: Send + 'static;
107 type SubscribeRequest: Clone + Send + 'static;
108 type SubscribeResponse: Message + Send + 'static;
109
110 fn list_read_mask(request: &Self::ListRequest) -> Option<&FieldMask>;
112 fn list_request_from_subscribe(request: &Self::SubscribeRequest) -> Self::ListRequest;
114 fn options(request: &Self::ListRequest) -> Option<&QueryOptions>;
116 fn options_mut(request: &mut Self::ListRequest) -> &mut QueryOptions;
118 fn start_checkpoint(request: &Self::ListRequest) -> Option<u64>;
120 fn set_start_checkpoint(request: &mut Self::ListRequest, checkpoint: Option<u64>);
122 fn end_checkpoint(request: &Self::ListRequest) -> Option<u64>;
124 fn set_end_checkpoint(request: &mut Self::ListRequest, checkpoint: Option<u64>);
126 fn set_ascending_resume(
128 request: &mut Self::ListRequest,
129 progress: &Progress<Self::Cursor>,
130 ) -> Result<()>;
131 fn request_resume_position(request: &Self::ListRequest) -> Option<Progress<Self::Cursor>>;
133 fn validate_checkpoint_bound(
135 request: &Self::ListRequest,
136 direction: ListScanDirection,
137 checkpoint: Option<u64>,
138 ) -> Result<()>;
139 fn item_position(item: &Self::Item) -> &Self::ItemPosition;
141 fn extract_metadata(
143 response: &Self::ListResponse,
144 ) -> (bool, Option<&Watermark>, Option<&QueryEnd>);
145 fn split_list(response: Self::ListResponse) -> Result<ListResponseParts<Self>>;
147 fn item_required(_request: &Self::SubscribeRequest) -> bool {
149 false
150 }
151 fn parse_live(
153 response: Self::SubscribeResponse,
154 item_required: bool,
155 ) -> Result<LiveFrame<Self::Item, Progress<Self::Cursor>>>;
156 fn into_output(item: Option<Self::Item>, progress: Progress<Self::Cursor>) -> Self::Output;
158 fn dispatch_list(
159 client: Client,
160 request: Request<Self::ListRequest>,
161 ) -> RpcFuture<Self::ListResponse>;
162 fn dispatch_subscribe(
163 client: Client,
164 request: Request<Self::SubscribeRequest>,
165 ) -> RpcFuture<Self::SubscribeResponse>;
166}
167
168fn parse_opaque_live_frame<I, P>(
169 item: Option<PositionedItem<I, P>>,
170 watermark: Option<Watermark>,
171) -> Result<LiveFrame<PositionedItem<I, P>, Progress<Bytes>>> {
172 let watermark = watermark
173 .ok_or_else(|| Status::data_loss("subscription frame is missing its watermark"))?;
174 let cursor = watermark
175 .cursor
176 .ok_or_else(|| Status::data_loss("subscription watermark is missing its cursor"))?;
177 Ok(LiveFrame {
178 item,
179 progress: Progress {
180 cursor,
181 checkpoint: watermark.checkpoint,
182 },
183 })
184}
185
186#[derive(Clone, Copy, Debug, PartialEq, Eq)]
187pub(super) enum ListScanDirection {
188 Ascending,
189 Descending,
190}
191
192impl ListScanDirection {
193 pub(super) fn from_request<A: SubscriptionAdapter>(request: &A::ListRequest) -> Result<Self> {
194 match A::options(request).and_then(|options| options.ordering) {
195 None => Ok(Self::Ascending),
196 Some(ordering) => match Ordering::try_from(ordering) {
197 Ok(Ordering::Ascending) => Ok(Self::Ascending),
198 Ok(Ordering::Descending) => Ok(Self::Descending),
199 Err(_) => Err(Status::invalid_argument("List ordering is unknown")),
200 },
201 }
202 }
203
204 pub(super) fn resume_bound(self, options: &QueryOptions) -> &Option<Bytes> {
205 match self {
206 Self::Ascending => &options.after,
207 Self::Descending => &options.before,
208 }
209 }
210
211 pub(super) fn set_resume_bound(self, options: &mut QueryOptions, cursor: Bytes) {
212 match self {
213 Self::Ascending => options.after = Some(cursor),
214 Self::Descending => options.before = Some(cursor),
215 }
216 }
217}
218
219pub(super) fn validate_typed_checkpoint_bound(
220 start_checkpoint: Option<u64>,
221 end_checkpoint: Option<u64>,
222 direction: ListScanDirection,
223 watermark_checkpoint: Option<u64>,
224) -> Result<()> {
225 let expected_checkpoint = match direction {
226 ListScanDirection::Ascending => end_checkpoint.and_then(|end| end.checked_sub(1)),
227 ListScanDirection::Descending => Some(start_checkpoint.unwrap_or(0)),
228 };
229 if watermark_checkpoint.is_some() && watermark_checkpoint != expected_checkpoint {
230 Err(Status::data_loss(
231 "List CheckpointBound watermark does not match the requested bound",
232 ))
233 } else {
234 Ok(())
235 }
236}