// file: crates/ksp-store-api/src/model/raw_pagination.rs // version: 1 /// Maximum opaque cursor length accepted by Store API queries. /// /// This bounds untrusted token material only. It is not a policy limit on the /// number of rows/results a backend may return. pub const MAX_RAW_PAGE_CURSOR_BYTES: usize = 4 * 1024; /// Opaque backend-owned cursor returned by one deterministic Store query. #[derive(Clone, Eq, PartialEq)] pub struct RawPageCursor(std::boxed::Box<[u8]>); impl RawPageCursor { /// Creates one non-empty bounded opaque cursor. pub fn try_new(bytes: std::boxed::Box<[u8]>) -> crate::Result { if bytes.is_empty() || bytes.len() > crate::MAX_RAW_PAGE_CURSOR_BYTES { return std::result::Result::Err(raw_query_error("cursor")); } return std::result::Result::Ok(Self(bytes)); } /// Returns the opaque cursor bytes unchanged. #[must_use] pub fn as_bytes(&self) -> &[u8] { return self.0.as_ref(); } } impl std::fmt::Debug for RawPageCursor { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter.debug_struct("RawPageCursor").field("len", &self.0.len()).finish(); } } /// Caller-requested page size without an arbitrary KSP policy ceiling. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct RawPageLimit(u64); impl RawPageLimit { /// Creates one strictly positive requested page size. /// /// `ksp-store-api` deliberately imposes no smaller functional maximum. /// Concrete backends may expose or return their real physical limitations. pub fn new(value: u64) -> crate::Result { if value == 0 { return std::result::Result::Err(raw_query_error("limit")); } return std::result::Result::Ok(Self(value)); } /// Returns the exact caller-requested item count. #[must_use] pub const fn get(&self) -> u64 { return self.0; } } /// Opaque-cursor page request used by backend-independent list operations. #[derive(Clone, Debug, Eq, PartialEq)] pub struct RawPageRequest { cursor: std::option::Option, limit: crate::RawPageLimit, } impl RawPageRequest { /// Creates one first-page request. #[must_use] pub const fn first(limit: crate::RawPageLimit) -> Self { return Self { cursor: std::option::Option::None, limit }; } /// Creates one continuation-page request with an opaque backend cursor. #[must_use] pub const fn after(limit: crate::RawPageLimit, cursor: crate::RawPageCursor) -> Self { return Self { cursor: std::option::Option::Some(cursor), limit }; } /// Returns the opaque continuation cursor when present. #[must_use] pub fn cursor(&self) -> std::option::Option<&crate::RawPageCursor> { return self.cursor.as_ref(); } /// Returns the exact caller-requested page size. #[must_use] pub const fn limit(&self) -> crate::RawPageLimit { return self.limit; } } /// One deterministic page of backend-independent Store results. #[derive(Debug)] pub struct RawPage { items: std::vec::Vec, next_cursor: std::option::Option, } impl RawPage { /// Creates one result page from backend-provided items and optional continuation cursor. #[must_use] pub fn new(items: std::vec::Vec, next_cursor: std::option::Option) -> Self { return Self { items, next_cursor }; } /// Returns the current page items. #[must_use] pub fn items(&self) -> &[T] { return self.items.as_slice(); } /// Consumes the page and returns its items. #[must_use] pub fn into_items(self) -> std::vec::Vec { return self.items; } /// Returns the next opaque cursor when more results are available. #[must_use] pub fn next_cursor(&self) -> std::option::Option<&crate::RawPageCursor> { return self.next_cursor.as_ref(); } } /// Deterministic traversal direction for Store list queries. #[non_exhaustive] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum RawSortDirection { /// Oldest/lower ordered keys first. Ascending, /// Newest/higher ordered keys first. Descending, } /// Optional inclusive Solana slot bounds for one Store query. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct RawSlotRange { end_inclusive: std::option::Option, start_inclusive: std::option::Option, } impl RawSlotRange { /// Creates one optional inclusive slot range and rejects reversed bounds. pub fn new(start_inclusive: std::option::Option, end_inclusive: std::option::Option) -> crate::Result { if let (std::option::Option::Some(start), std::option::Option::Some(end)) = (start_inclusive, end_inclusive) && start > end { return std::result::Result::Err(raw_query_error("slot_range")); } return std::result::Result::Ok(Self { end_inclusive, start_inclusive }); } /// Returns the optional inclusive ending slot. #[must_use] pub const fn end_inclusive(&self) -> std::option::Option { return self.end_inclusive; } /// Returns the optional inclusive starting slot. #[must_use] pub const fn start_inclusive(&self) -> std::option::Option { return self.start_inclusive; } } /// Backend-independent list query for canonical RAW transactions. #[derive(Clone, Debug, Eq, PartialEq)] pub struct RawTransactionQuery { direction: crate::RawSortDirection, network: crate::RawNetworkId, page: crate::RawPageRequest, slots: crate::RawSlotRange, } impl RawTransactionQuery { /// Creates one deterministic transaction-reference query. #[must_use] pub fn new(network: crate::RawNetworkId, slots: crate::RawSlotRange, direction: crate::RawSortDirection, page: crate::RawPageRequest) -> Self { return Self { direction, network, page, slots }; } /// Returns the requested deterministic traversal direction. #[must_use] pub const fn direction(&self) -> crate::RawSortDirection { return self.direction; } /// Returns the required logical network scope. #[must_use] pub fn network(&self) -> &crate::RawNetworkId { return &self.network; } /// Returns pagination inputs without exposing backend cursor contents. #[must_use] pub fn page(&self) -> &crate::RawPageRequest { return &self.page; } /// Returns optional inclusive slot bounds. #[must_use] pub const fn slots(&self) -> crate::RawSlotRange { return self.slots; } } /// Backend-independent list query for complete canonical RAW account states. #[derive(Clone, Debug, Eq, PartialEq)] pub struct RawAccountStateQuery { direction: crate::RawSortDirection, network: crate::RawNetworkId, page: crate::RawPageRequest, pubkey: std::option::Option, slots: crate::RawSlotRange, } impl RawAccountStateQuery { /// Creates one deterministic account-state-reference query. #[must_use] pub fn new( network: crate::RawNetworkId, pubkey: std::option::Option, slots: crate::RawSlotRange, direction: crate::RawSortDirection, page: crate::RawPageRequest, ) -> Self { return Self { direction, network, page, pubkey, slots }; } /// Returns the requested deterministic traversal direction. #[must_use] pub const fn direction(&self) -> crate::RawSortDirection { return self.direction; } /// Returns the required logical network scope. #[must_use] pub fn network(&self) -> &crate::RawNetworkId { return &self.network; } /// Returns pagination inputs without exposing backend cursor contents. #[must_use] pub fn page(&self) -> &crate::RawPageRequest { return &self.page; } /// Returns an optional account-address restriction. #[must_use] pub fn pubkey(&self) -> std::option::Option<&crate::Pubkey> { return self.pubkey.as_ref(); } /// Returns optional inclusive slot bounds. #[must_use] pub const fn slots(&self) -> crate::RawSlotRange { return self.slots; } } fn raw_query_error(field: &'static str) -> crate::Error { return crate::Error::new(crate::ERROR_CODE_RAW_QUERY_INVALID, "invalid backend-agnostic RAW Store query").with_context("field", field); } #[cfg(test)] #[path = "../../unit_tests/model/raw_pagination.rs"] mod tests;