1use crate::rate_limit::RateLimit;
2
3use super::{PageStrategy, PaginationBudget, PaginationError, PaginationProgress, SnapshotId};
4
5#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub struct OffsetPageMetadata {
8 offset: u64,
9 page_size: u64,
10 next_offset: Option<u64>,
11 total_entries: Option<u64>,
12}
13
14impl OffsetPageMetadata {
15 pub const fn new(
17 offset: u64,
18 page_size: u64,
19 next_offset: Option<u64>,
20 total_entries: Option<u64>,
21 ) -> Result<Self, PaginationError> {
22 if page_size == 0 {
23 return Err(PaginationError::PageSizeZero);
24 }
25 if let Some(next) = next_offset
26 && next <= offset
27 {
28 return Err(PaginationError::InvalidNextPage);
29 }
30 Ok(Self {
31 offset,
32 page_size,
33 next_offset,
34 total_entries,
35 })
36 }
37
38 #[must_use]
40 pub const fn offset(self) -> u64 {
41 self.offset
42 }
43
44 #[must_use]
46 pub const fn page_size(self) -> u64 {
47 self.page_size
48 }
49
50 #[must_use]
52 pub const fn next_offset(self) -> Option<u64> {
53 self.next_offset
54 }
55
56 #[must_use]
58 pub const fn total_entries(self) -> Option<u64> {
59 self.total_entries
60 }
61}
62
63#[derive(Clone, Copy, Debug, Eq, PartialEq)]
65pub struct OffsetPageBoundary {
66 metadata: OffsetPageMetadata,
67 entries: usize,
68 rate_limit: Option<RateLimit>,
69 progress: PaginationProgress,
70}
71
72#[derive(Clone, Copy, Debug, Eq, PartialEq)]
74pub struct OffsetPageObservation<'snapshot> {
75 metadata: OffsetPageMetadata,
76 entries: usize,
77 rate_limit: Option<RateLimit>,
78 snapshot: Option<SnapshotId<'snapshot>>,
79}
80
81impl<'snapshot> OffsetPageObservation<'snapshot> {
82 #[must_use]
84 pub const fn new(
85 metadata: OffsetPageMetadata,
86 entries: usize,
87 rate_limit: Option<RateLimit>,
88 snapshot: Option<SnapshotId<'snapshot>>,
89 ) -> Self {
90 Self {
91 metadata,
92 entries,
93 rate_limit,
94 snapshot,
95 }
96 }
97}
98
99impl OffsetPageBoundary {
100 #[must_use]
102 pub const fn metadata(self) -> OffsetPageMetadata {
103 self.metadata
104 }
105
106 #[must_use]
108 pub const fn entries(self) -> usize {
109 self.entries
110 }
111
112 #[must_use]
114 pub const fn rate_limit(self) -> Option<RateLimit> {
115 self.rate_limit
116 }
117
118 #[must_use]
120 pub const fn progress(self) -> PaginationProgress {
121 self.progress
122 }
123
124 #[must_use]
126 pub const fn is_terminal(self) -> bool {
127 self.metadata.next_offset.is_none()
128 }
129}
130
131pub struct OffsetPagination {
135 next_offset: Option<u64>,
136 expected_page_size: u64,
137 expected_total_entries: Option<u64>,
138 metadata_initialized: bool,
139 budget: PaginationBudget,
140}
141
142impl OffsetPagination {
143 pub fn new(
145 first_offset: u64,
146 expected_page_size: u64,
147 budget: PaginationBudget,
148 ) -> Result<Self, PaginationError> {
149 if expected_page_size == 0 {
150 return Err(PaginationError::PageSizeZero);
151 }
152 Ok(Self {
153 next_offset: Some(first_offset),
154 expected_page_size,
155 expected_total_entries: None,
156 metadata_initialized: false,
157 budget,
158 })
159 }
160
161 pub const fn next_offset(&self) -> Result<u64, PaginationError> {
163 match self.next_offset {
164 Some(offset) => Ok(offset),
165 None => Err(PaginationError::Complete),
166 }
167 }
168
169 #[must_use]
171 pub const fn progress(&self) -> PaginationProgress {
172 self.budget.progress()
173 }
174
175 pub fn observe(
177 &mut self,
178 metadata: OffsetPageMetadata,
179 entries: usize,
180 rate_limit: Option<RateLimit>,
181 snapshot: Option<SnapshotId<'_>>,
182 ) -> Result<OffsetPageBoundary, PaginationError> {
183 let expected = self.next_offset.ok_or(PaginationError::Complete)?;
184 if metadata.offset != expected {
185 return Err(PaginationError::UnexpectedPosition);
186 }
187 if metadata.page_size != self.expected_page_size {
188 return Err(PaginationError::PageSizeChanged);
189 }
190 if self.metadata_initialized && metadata.total_entries != self.expected_total_entries {
191 return Err(PaginationError::TraversalChanged);
192 }
193 let entry_count = u64::try_from(entries).map_err(|_| PaginationError::InvalidEntryCount)?;
194 if entry_count > metadata.page_size {
195 return Err(PaginationError::InvalidEntryCount);
196 }
197 let expected_next = metadata
198 .offset
199 .checked_add(entry_count)
200 .ok_or(PaginationError::InvalidEntryCount)?;
201 if let Some(next) = metadata.next_offset
202 && (entries == 0 || next != expected_next)
203 {
204 return Err(PaginationError::InvalidNextPage);
205 }
206 if let Some(total) = metadata.total_entries {
207 let continuation = total > expected_next;
208 if expected_next > total || continuation != metadata.next_offset.is_some() {
209 return Err(PaginationError::InvalidEntryCount);
210 }
211 }
212 let has_continuation = metadata.next_offset.is_some();
213 if entries == 0 && has_continuation {
214 return Err(PaginationError::EmptyPageWithContinuation);
215 }
216 let progress = self.budget.admit(entries, has_continuation, snapshot)?;
217 self.next_offset = metadata.next_offset;
218 if !self.metadata_initialized {
219 self.expected_total_entries = metadata.total_entries;
220 self.metadata_initialized = true;
221 }
222 Ok(OffsetPageBoundary {
223 metadata,
224 entries,
225 rate_limit,
226 progress,
227 })
228 }
229}
230
231impl core::fmt::Debug for OffsetPagination {
232 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
233 formatter
234 .debug_struct("OffsetPagination")
235 .field("next_offset", &self.next_offset)
236 .field("expected_page_size", &self.expected_page_size)
237 .field("traversal_metadata", &"[redacted]")
238 .field("budget", &self.budget)
239 .finish()
240 }
241}
242
243impl PageStrategy for OffsetPagination {
244 type Request = u64;
245 type Observation<'observation> = OffsetPageObservation<'observation>;
246 type Boundary = OffsetPageBoundary;
247
248 fn next_request(&self) -> Result<Self::Request, PaginationError> {
249 self.next_offset()
250 }
251
252 fn observe<'observation>(
253 &mut self,
254 observation: Self::Observation<'observation>,
255 ) -> Result<Self::Boundary, PaginationError> {
256 self.observe(
257 observation.metadata,
258 observation.entries,
259 observation.rate_limit,
260 observation.snapshot,
261 )
262 }
263}