1use std::cell::RefCell;
6use std::collections::{BTreeMap, BTreeSet};
7use std::fmt;
8use std::rc::Rc;
9
10use ravenrs_reactive::State;
11
12use super::model::{Row, SortDirection};
13use super::query::GridQuery;
14
15const DEFAULT_CHUNK: usize = 100;
16const DEFAULT_MAX_CHUNKS: usize = 8;
17const DEFAULT_IN_FLIGHT: usize = 2;
18const SCROLL_SETTLE: f64 = 0.12;
21const QUERY_SETTLE: f64 = 0.3;
23
24#[derive(Clone, Debug, PartialEq)]
26#[non_exhaustive]
27#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
28pub struct GridRequest {
29 pub offset: usize,
31 pub limit: usize,
33 pub sort: Vec<(usize, SortDirection)>,
35 pub filters: Vec<String>,
37 pub search: String,
39 pub after: Option<Row>,
43 pub wants_total: bool,
46}
47
48#[derive(Clone, Debug, Default, PartialEq)]
50#[non_exhaustive]
51#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
52pub struct GridPage {
53 pub rows: Vec<Row>,
55 pub total: Option<usize>,
57 pub has_more: bool,
59 pub totals: Vec<Option<f64>>,
61}
62
63#[cfg_attr(feature = "live", ravenrs_live::expose)]
64impl GridPage {
65 pub fn new(rows: Vec<Row>, has_more: bool) -> Self {
67 Self { rows, has_more, ..Self::default() }
68 }
69
70 pub fn total(mut self, total: usize) -> Self {
72 self.total = Some(total);
73 self
74 }
75
76 pub fn totals(mut self, totals: Vec<Option<f64>>) -> Self {
78 self.totals = totals;
79 self
80 }
81}
82
83struct Chunk {
84 rows: Vec<Row>,
85 used: u64,
86}
87
88#[derive(Default)]
89struct Held {
90 chunks: BTreeMap<usize, Chunk>,
91 in_flight: BTreeSet<usize>,
92 generation: u64,
94 total: Option<usize>,
95 end: usize,
97 has_more: bool,
98 totals: Vec<Option<f64>>,
99 clock: u64,
100 wanted: (usize, usize),
101 moved_at: f64,
102 query: Option<GridQuery>,
103 queried_at: f64,
104 error: Option<String>,
105 started: bool,
106 peak_rows: usize,
107}
108
109#[derive(Clone)]
112#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
113pub struct GridResponder {
114 held: Rc<RefCell<Held>>,
115 tick: State<u64>,
116 generation: u64,
117 chunk: usize,
118 chunk_size: usize,
119 max_chunks: usize,
120}
121
122impl fmt::Debug for GridResponder {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 f.debug_struct("GridResponder").field("chunk", &self.chunk).finish_non_exhaustive()
125 }
126}
127
128#[cfg_attr(feature = "live", ravenrs_live::expose)]
129impl GridResponder {
130 pub fn send(self, page: GridPage) {
132 {
133 let mut held = self.held.borrow_mut();
134 if held.generation != self.generation {
135 return;
136 }
137 held.in_flight.remove(&self.chunk);
138 held.error = None;
139 let start = self.chunk * self.chunk_size;
140 held.end = held.end.max(start + page.rows.len());
141 held.has_more = page.has_more;
142 if page.total.is_some() {
143 held.total = page.total;
144 } else if !page.has_more {
145 held.total = Some(start + page.rows.len());
146 }
147 if !page.totals.is_empty() {
148 held.totals = page.totals;
149 }
150 held.clock += 1;
151 let used = held.clock;
152 held.chunks.insert(self.chunk, Chunk { rows: page.rows, used });
153 evict(&mut held, self.max_chunks, self.chunk_size);
154 let rows: usize = held.chunks.values().map(|c| c.rows.len()).sum();
155 held.peak_rows = held.peak_rows.max(rows);
156 }
157 self.tick.update(|t| *t += 1);
158 }
159
160 pub fn fail(self, message: impl Into<String>) {
162 {
163 let mut held = self.held.borrow_mut();
164 if held.generation != self.generation {
165 return;
166 }
167 held.in_flight.remove(&self.chunk);
168 held.error = Some(message.into());
169 }
170 self.tick.update(|t| *t += 1);
171 }
172}
173
174fn evict(held: &mut Held, max_chunks: usize, chunk_size: usize) {
176 let (first, last) = (held.wanted.0 / chunk_size.max(1), held.wanted.1 / chunk_size.max(1));
177 while held.chunks.len() > max_chunks.max(last.saturating_sub(first) + 1) {
178 let oldest = held
179 .chunks
180 .iter()
181 .filter(|(chunk, _)| **chunk < first || **chunk > last)
182 .min_by_key(|(_, c)| c.used)
183 .map(|(chunk, _)| *chunk);
184 let Some(oldest) = oldest else { return };
185 held.chunks.remove(&oldest);
186 }
187}
188
189type Loader = Rc<dyn Fn(GridRequest, GridResponder)>;
190
191#[derive(Clone)]
208#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
209pub struct RemoteRows {
210 held: Rc<RefCell<Held>>,
211 tick: State<u64>,
212 loader: Loader,
213 chunk_size: usize,
214 max_chunks: usize,
215 in_flight: usize,
216}
217
218impl fmt::Debug for RemoteRows {
219 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220 f.debug_struct("RemoteRows").field("chunk_size", &self.chunk_size).finish_non_exhaustive()
221 }
222}
223
224#[cfg_attr(feature = "live", ravenrs_live::expose)]
225impl RemoteRows {
226 pub fn new(loader: impl Fn(GridRequest, GridResponder) + 'static) -> Self {
228 Self {
229 held: Rc::new(RefCell::new(Held { has_more: true, ..Held::default() })),
230 tick: State::new(0),
231 loader: Rc::new(loader),
232 chunk_size: DEFAULT_CHUNK,
233 max_chunks: DEFAULT_MAX_CHUNKS,
234 in_flight: DEFAULT_IN_FLIGHT,
235 }
236 }
237
238 pub fn chunk_size(mut self, rows: usize) -> Self {
240 self.chunk_size = rows.max(1);
241 self
242 }
243
244 pub fn max_chunks(mut self, chunks: usize) -> Self {
246 self.max_chunks = chunks.max(1);
247 self
248 }
249
250 pub fn max_in_flight(mut self, requests: usize) -> Self {
252 self.in_flight = requests.max(1);
253 self
254 }
255
256 pub fn peak_rows(&self) -> usize {
258 self.held.borrow().peak_rows
259 }
260
261 pub fn refresh(&self) {
263 {
264 let mut held = self.held.borrow_mut();
265 held.generation += 1;
266 held.chunks.clear();
267 held.in_flight.clear();
268 held.total = None;
269 held.end = 0;
270 held.has_more = true;
271 held.started = false;
272 held.error = None;
273 }
274 self.tick.update(|t| *t += 1);
275 }
276
277 pub(crate) fn watch(&self) {
279 self.tick.get();
280 }
281
282 pub(crate) fn len(&self) -> usize {
285 let held = self.held.borrow();
286 match held.total {
287 Some(total) => total,
288 None if held.has_more => held.end + self.chunk_size,
289 None => held.end,
290 }
291 }
292
293 #[cfg(test)]
294 pub(crate) fn total(&self) -> Option<usize> {
295 self.held.borrow().total
296 }
297
298 pub(crate) fn error(&self) -> Option<String> {
299 self.held.borrow().error.clone()
300 }
301
302 pub(crate) fn totals(&self) -> Vec<Option<f64>> {
303 self.held.borrow().totals.clone()
304 }
305
306 pub(crate) fn held_rows(&self) -> Vec<(usize, Row)> {
309 let held = self.held.borrow();
310 let mut rows = Vec::new();
311 for (chunk, content) in &held.chunks {
312 let start = chunk * self.chunk_size;
313 rows.extend(content.rows.iter().enumerate().map(|(i, row)| (start + i, row.clone())));
314 }
315 rows
316 }
317
318 pub(crate) fn row(&self, index: usize) -> Option<Row> {
319 let mut held = self.held.borrow_mut();
320 held.clock += 1;
321 let used = held.clock;
322 let chunk = held.chunks.get_mut(&(index / self.chunk_size))?;
323 chunk.used = used;
324 chunk.rows.get(index % self.chunk_size).cloned()
325 }
326
327 pub(crate) fn want(&self, first: usize, last: usize, query: &GridQuery, now: f64) -> Option<f64> {
330 let mut requests = Vec::new();
331 let wake = {
332 let mut held = self.held.borrow_mut();
333 let same_query = held.query.as_ref().is_some_and(|q| {
334 q.sort == query.sort && q.filters == query.filters && q.search == query.search
335 });
336 if !same_query {
337 let was_running = held.query.is_some();
338 held.query = Some(query.clone());
339 if was_running {
340 held.generation += 1;
341 held.chunks.clear();
342 held.in_flight.clear();
343 held.total = None;
344 held.end = 0;
345 held.has_more = true;
346 held.queried_at = now;
347 held.started = true;
348 held.error = None;
349 }
350 }
351 if held.wanted != (first, last) {
352 held.wanted = (first, last);
353 held.moved_at = now;
354 }
355 let ready = (!held.started)
357 || (now - held.moved_at >= SCROLL_SETTLE && now - held.queried_at >= QUERY_SETTLE);
358 if !ready {
359 Some((held.moved_at + SCROLL_SETTLE).max(held.queried_at + QUERY_SETTLE))
360 } else if held.error.is_some() {
361 None
362 } else {
363 held.started = true;
364 let size = self.chunk_size;
365 let end = last.max(first + 1) - 1;
366 let until = end / size + 1;
368 let limit = held.total.map(|t| t.div_ceil(size));
369 for chunk in first / size..=until {
370 if limit.is_some_and(|l| chunk >= l)
371 || held.chunks.contains_key(&chunk)
372 || held.in_flight.contains(&chunk)
373 || held.in_flight.len() >= self.in_flight
374 {
375 continue;
376 }
377 held.in_flight.insert(chunk);
378 let after = chunk
379 .checked_sub(1)
380 .and_then(|before| held.chunks.get(&before))
381 .and_then(|c| c.rows.last().cloned());
382 let request = GridRequest {
383 offset: chunk * size,
384 limit: size,
385 sort: query.sort.clone(),
386 filters: query.filters.clone(),
387 search: query.search.clone(),
388 after,
389 wants_total: held.total.is_none(),
390 };
391 requests.push((chunk, request, held.generation));
392 }
393 None
394 }
395 };
396 for (chunk, request, generation) in requests {
397 (self.loader)(
398 request,
399 GridResponder {
400 held: self.held.clone(),
401 tick: self.tick.clone(),
402 generation,
403 chunk,
404 chunk_size: self.chunk_size,
405 max_chunks: self.max_chunks,
406 },
407 );
408 }
409 wake
410 }
411
412 pub(crate) fn retry(&self) {
414 self.held.borrow_mut().error = None;
415 self.tick.update(|t| *t += 1);
416 }
417}
418
419#[cfg(test)]
420mod tests {
421 use std::cell::Cell;
422
423 use super::super::model::Value;
424 use super::*;
425
426 const SERVER_ROWS: usize = 1_000_000;
427
428 fn server(calls: &Rc<Cell<usize>>) -> RemoteRows {
430 let calls = calls.clone();
431 RemoteRows::new(move |request, answer| {
432 calls.set(calls.get() + 1);
433 let end = (request.offset + request.limit).min(SERVER_ROWS);
434 let rows = (request.offset..end).map(|n| vec![Value::Number(n as f64)]).collect();
435 answer.send(GridPage::new(rows, end < SERVER_ROWS));
436 })
437 }
438
439 #[test]
440 fn it_asks_only_for_what_is_on_the_screen() {
441 let calls = Rc::new(Cell::new(0));
442 let rows = server(&calls);
443 rows.want(0, 30, &GridQuery::default(), 0.0);
444 assert!(calls.get() <= 2, "{} questions", calls.get());
445 assert_eq!(rows.row(5), Some(vec![Value::Number(5.0)]));
446 assert_eq!(rows.row(5000), None);
447 }
448
449 #[test]
450 fn dragging_the_scrollbar_across_everything_asks_only_where_it_stopped() {
451 let calls = Rc::new(Cell::new(0));
452 let rows = server(&calls);
453 let q = GridQuery::default();
454 rows.want(0, 30, &q, 0.0);
455 let before = calls.get();
456 for step in 1..500 {
457 let at = step * 1000;
458 assert!(rows.want(at, at + 30, &q, f64::from(step as u32) * 0.016).is_some());
459 }
460 assert_eq!(calls.get(), before, "nothing while it moves");
461 rows.want(499_000, 499_030, &q, 100.0);
462 assert!(calls.get() - before <= 2);
463 assert!(rows.row(499_010).is_some());
464 }
465
466 #[test]
467 fn memory_stays_within_its_limit_however_far_it_goes() {
468 let calls = Rc::new(Cell::new(0));
469 let rows = server(&calls).chunk_size(100).max_chunks(4);
470 let q = GridQuery::default();
471 let mut now = 0.0;
472 for at in (0..200_000).step_by(100) {
473 now += 1.0;
474 rows.want(at, at + 30, &q, now);
475 now += 1.0;
476 rows.want(at, at + 30, &q, now);
477 }
478 assert!(rows.peak_rows() <= 400, "{} rows held", rows.peak_rows());
479 }
480
481 #[test]
482 fn typing_in_a_filter_asks_once_it_rests() {
483 let calls = Rc::new(Cell::new(0));
484 let rows = server(&calls);
485 let mut q = GridQuery::default();
486 rows.want(0, 30, &q, 0.0);
487 let before = calls.get();
488 for (i, text) in ["a", "ab", "abc"].iter().enumerate() {
489 q.search = (*text).to_owned();
490 rows.want(0, 30, &q, 1.0 + i as f64 * 0.05);
491 }
492 assert_eq!(calls.get(), before);
493 rows.want(0, 30, &q, 5.0);
494 assert!(calls.get() > before);
495 }
496
497 #[test]
498 fn an_answer_to_an_old_question_is_dropped() {
499 let waiting: Rc<RefCell<Vec<GridResponder>>> = Rc::default();
500 let held = waiting.clone();
501 let rows = RemoteRows::new(move |_, answer| held.borrow_mut().push(answer)).max_in_flight(1);
502 let mut q = GridQuery::default();
503 rows.want(0, 30, &q, 0.0);
504 let old = waiting.borrow_mut().remove(0);
505 q.search = "new".into();
506 rows.want(0, 30, &q, 1.0);
507 rows.want(0, 30, &q, 5.0);
508 old.send(GridPage::new(vec![vec![Value::Text("stale".into())]], false));
509 assert_eq!(rows.row(0), None);
510 assert_eq!(rows.total(), None);
511 }
512
513 #[test]
514 fn the_server_gets_a_limited_number_of_questions_at_once() {
515 let waiting: Rc<RefCell<Vec<GridResponder>>> = Rc::default();
516 let held = waiting.clone();
517 let rows =
518 RemoteRows::new(move |_, answer| held.borrow_mut().push(answer)).chunk_size(10).max_in_flight(2);
519 rows.want(0, 100, &GridQuery::default(), 0.0);
520 assert_eq!(waiting.borrow().len(), 2);
521 }
522
523 #[test]
524 fn without_a_total_it_grows_as_rows_arrive_and_stops_at_the_end() {
525 let rows = RemoteRows::new(|request, answer| {
526 let n = if request.offset >= 100 { 40 } else { 100 };
527 let page = (0..n).map(|i| vec![Value::Number(f64::from(i))]).collect();
528 answer.send(GridPage::new(page, n == 100));
529 });
530 let q = GridQuery::default();
531 rows.want(0, 10, &q, 0.0);
532 rows.want(95, 110, &q, 1.0);
533 rows.want(95, 110, &q, 2.0);
534 assert_eq!(rows.total(), Some(140), "a short page was the last");
535 assert_eq!(rows.len(), 140);
536 }
537
538 #[test]
539 fn it_can_continue_from_the_row_before_instead_of_counting_rows() {
540 let seen: Rc<RefCell<Vec<GridRequest>>> = Rc::default();
541 let log = seen.clone();
542 let rows = RemoteRows::new(move |request, answer| {
543 log.borrow_mut().push(request.clone());
544 let page = (request.offset..request.offset + request.limit)
545 .map(|n| vec![Value::Number(n as f64)])
546 .collect();
547 answer.send(GridPage::new(page, true));
548 });
549 let q = GridQuery::default();
550 rows.want(0, 10, &q, 0.0);
551 rows.want(150, 160, &q, 1.0);
552 rows.want(150, 160, &q, 2.0);
553 let next = seen.borrow().iter().find(|r| r.offset == 200).cloned().expect("the next chunk");
554 assert_eq!(next.after, Some(vec![Value::Number(199.0)]));
555 }
556
557 #[test]
558 fn a_failure_waits_for_someone_to_try_again() {
559 let calls = Rc::new(Cell::new(0));
560 let count = calls.clone();
561 let rows = RemoteRows::new(move |_, answer| {
562 count.set(count.get() + 1);
563 answer.fail("no connection");
564 })
565 .max_in_flight(1);
566 let q = GridQuery::default();
567 rows.want(0, 10, &q, 0.0);
568 assert_eq!(rows.error().as_deref(), Some("no connection"));
569 rows.want(0, 10, &q, 9.0);
570 assert_eq!(calls.get(), 1, "it does not hammer a server that is down");
571 rows.retry();
572 rows.want(0, 10, &q, 10.0);
573 assert_eq!(calls.get(), 2);
574 }
575}