Skip to main content

bern_runtime/
query.rs

1//! Asynchronous query resources and clocks for scenarios.
2//!
3//! A [`Query`] is the display state of one query subscription: first load,
4//! data, refresh and failure, so controls do not stitch booleans together.
5//! Every request gets a new parameter version; only the result for the
6//! current version is accepted. A slower answer to an older request — the
7//! first of two searches typed in a row — is discarded instead of
8//! overwriting the newer result, and the older request is cancelled. This
9//! "current result only" rule is for queries; engine commands keep their own
10//! retry and idempotency policies.
11//!
12//! [`QuerySubscriptions`] holds the live subscriptions of a UI session.
13//! Each belongs to a window instance and ends with it; a subscription whose
14//! query reads engine state is requested again when that state changes.
15//!
16//! Providers schedule delayed work through a [`Clock`]. [`SystemClock`]
17//! uses real time; [`ManualClock`] only moves when a scenario advances it,
18//! so the order of asynchronous completions is part of the test, not an
19//! accident.
20
21pub use bern_protocol::Cancellation;
22use bern_protocol::{CommandError, Value};
23use std::collections::BTreeMap;
24use std::sync::{Arc, Mutex};
25
26/// What a query shows. An empty result is `Ready` with empty data: success,
27/// not failure.
28#[derive(Clone, Debug, PartialEq)]
29pub enum Resource<T> {
30    /// Nothing requested yet.
31    Idle,
32    /// First load, no data to show.
33    Loading,
34    Ready(T),
35    /// New parameters are loading; the previous data stays visible.
36    Refreshing(T),
37    Failed {
38        error: CommandError,
39        /// Data from the last success, still shown next to the error.
40        previous: Option<T>,
41    },
42}
43
44impl<T> Resource<T> {
45    /// The data the UI can show, if any.
46    pub fn data(&self) -> Option<&T> {
47        match self {
48            Resource::Ready(data) | Resource::Refreshing(data) => Some(data),
49            Resource::Failed { previous, .. } => previous.as_ref(),
50            Resource::Idle | Resource::Loading => None,
51        }
52    }
53}
54
55/// Identity of one request: the parameter version it was made for.
56#[derive(Clone, Debug)]
57pub struct Ticket {
58    pub version: u64,
59    pub cancellation: Cancellation,
60}
61
62/// Whether a result changed what the query shows.
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub enum Resolution {
65    Applied,
66    /// The result belonged to an older version or an ended subscription.
67    Discarded,
68}
69
70/// One query subscription with its parameter versions.
71#[derive(Debug)]
72pub struct Query<P, T> {
73    params: Option<P>,
74    version: u64,
75    in_flight: Option<Cancellation>,
76    state: Resource<T>,
77    ended: bool,
78    discarded: u64,
79}
80
81impl<P, T> Default for Query<P, T> {
82    fn default() -> Self {
83        Self {
84            params: None,
85            version: 0,
86            in_flight: None,
87            state: Resource::Idle,
88            ended: false,
89            discarded: 0,
90        }
91    }
92}
93
94impl<P: Clone, T: Clone> Query<P, T> {
95    pub fn new() -> Self {
96        Self::default()
97    }
98
99    pub fn state(&self) -> &Resource<T> {
100        &self.state
101    }
102
103    pub fn params(&self) -> Option<&P> {
104        self.params.as_ref()
105    }
106
107    /// Version of the current parameters.
108    pub fn version(&self) -> u64 {
109        self.version
110    }
111
112    /// Results that arrived too late and were dropped.
113    pub fn discarded(&self) -> u64 {
114        self.discarded
115    }
116
117    /// Ask for `params`. The previous request is cancelled; data already
118    /// shown stays visible while the new request loads.
119    pub fn request(&mut self, params: P) -> Ticket {
120        if let Some(previous) = self.in_flight.take() {
121            previous.cancel();
122        }
123        self.version += 1;
124        self.params = Some(params);
125        let previous = self.state.data().cloned();
126        self.state = match previous {
127            Some(data) => Resource::Refreshing(data),
128            None => Resource::Loading,
129        };
130        let cancellation = Cancellation::new();
131        self.in_flight = Some(cancellation.clone());
132        Ticket {
133            version: self.version,
134            cancellation,
135        }
136    }
137
138    /// A provider answered `ticket`. Only the current version is applied.
139    pub fn resolve(&mut self, ticket: &Ticket, result: Result<T, CommandError>) -> Resolution {
140        if self.ended || ticket.version != self.version || ticket.cancellation.is_cancelled() {
141            self.discarded += 1;
142            return Resolution::Discarded;
143        }
144        self.in_flight = None;
145        self.state = match result {
146            Ok(data) => Resource::Ready(data),
147            Err(error) => Resource::Failed {
148                error,
149                previous: self.state.data().cloned(),
150            },
151        };
152        Resolution::Applied
153    }
154
155    /// The owner of the subscription ended, for example its page closed.
156    /// The request in flight is cancelled and later results are dropped.
157    pub fn end(&mut self) {
158        if let Some(previous) = self.in_flight.take() {
159            previous.cancel();
160        }
161        self.ended = true;
162    }
163}
164
165/// A live subscription to one catalog query, owned by a window instance.
166#[derive(Debug)]
167pub struct QuerySubscription {
168    /// Window instance that owns it, such as `main#1`.
169    pub window: String,
170    /// Catalog index of the query.
171    pub query: usize,
172    /// State fields the query reads.
173    reads: Vec<usize>,
174    resource: Query<Vec<Value>, Value>,
175}
176
177impl QuerySubscription {
178    pub fn state(&self) -> &Resource<Value> {
179        self.resource.state()
180    }
181
182    pub fn version(&self) -> u64 {
183        self.resource.version()
184    }
185
186    pub fn params(&self) -> Option<&Vec<Value>> {
187        self.resource.params()
188    }
189
190    pub fn discarded(&self) -> u64 {
191        self.resource.discarded()
192    }
193}
194
195/// The query subscriptions of one UI session.
196#[derive(Debug, Default)]
197pub struct QuerySubscriptions {
198    next: u64,
199    entries: BTreeMap<u64, QuerySubscription>,
200}
201
202impl QuerySubscriptions {
203    pub fn new() -> Self {
204        Self::default()
205    }
206
207    /// Start a subscription to `query` for `window`. Nothing is requested
208    /// until [`request`](Self::request) names the parameters.
209    pub fn open(&mut self, window: impl Into<String>, query: usize, reads: Vec<usize>) -> u64 {
210        self.next += 1;
211        self.entries.insert(
212            self.next,
213            QuerySubscription {
214                window: window.into(),
215                query,
216                reads,
217                resource: Query::new(),
218            },
219        );
220        self.next
221    }
222
223    pub fn get(&self, id: u64) -> Option<&QuerySubscription> {
224        self.entries.get(&id)
225    }
226
227    pub fn len(&self) -> usize {
228        self.entries.len()
229    }
230
231    pub fn is_empty(&self) -> bool {
232        self.entries.is_empty()
233    }
234
235    /// Request new parameters; the previous request is cancelled. `None`
236    /// when the subscription has ended.
237    pub fn request(&mut self, id: u64, params: Vec<Value>) -> Option<Ticket> {
238        self.entries
239            .get_mut(&id)
240            .map(|entry| entry.resource.request(params))
241    }
242
243    /// Apply an answer if it is still current.
244    pub fn resolve(
245        &mut self,
246        id: u64,
247        ticket: &Ticket,
248        result: Result<Value, CommandError>,
249    ) -> Resolution {
250        match self.entries.get_mut(&id) {
251            Some(entry) => entry.resource.resolve(ticket, result),
252            None => Resolution::Discarded,
253        }
254    }
255
256    /// End a subscription; its request in flight is cancelled.
257    pub fn close(&mut self, id: u64) -> bool {
258        match self.entries.remove(&id) {
259            Some(mut entry) => {
260                entry.resource.end();
261                true
262            }
263            None => false,
264        }
265    }
266
267    /// End every subscription of a closed window instance.
268    pub fn close_window(&mut self, window: &str) -> Vec<u64> {
269        let ids: Vec<u64> = self
270            .entries
271            .iter()
272            .filter(|(_, entry)| entry.window == window)
273            .map(|(id, _)| *id)
274            .collect();
275        for id in &ids {
276            self.close(*id);
277        }
278        ids
279    }
280
281    /// Engine state fields changed: every requested subscription whose
282    /// query reads one of them is requested again with its parameters.
283    /// Returns what to send to the provider.
284    pub fn refresh(&mut self, changed: &[usize]) -> Vec<(u64, Ticket, Vec<Value>)> {
285        let mut requests = Vec::new();
286        for (id, entry) in &mut self.entries {
287            if !entry.reads.iter().any(|field| changed.contains(field)) {
288                continue;
289            }
290            let Some(params) = entry.resource.params().cloned() else {
291                continue;
292            };
293            let ticket = entry.resource.request(params.clone());
294            requests.push((*id, ticket, params));
295        }
296        requests
297    }
298}
299
300type Timer = Box<dyn FnOnce() + Send>;
301
302/// Time source for providers that answer later. Scenarios inject a
303/// [`ManualClock`] to decide the order of completions.
304pub trait Clock: Send + Sync + 'static {
305    /// Milliseconds since the clock started.
306    fn now_ms(&self) -> u64;
307    /// Run `task` once `delay_ms` have passed, on a thread of the clock's
308    /// choosing.
309    fn after_ms(&self, delay_ms: u64, task: Timer);
310}
311
312/// A clock that only moves when a scenario advances it. Timers fire in
313/// due order, ties in the order they were scheduled, on the thread that
314/// advances the clock.
315#[derive(Clone, Default)]
316pub struct ManualClock {
317    inner: Arc<Mutex<ClockState>>,
318}
319
320#[derive(Default)]
321struct ClockState {
322    now: u64,
323    sequence: u64,
324    timers: Vec<(u64, u64, Timer)>,
325}
326
327impl ManualClock {
328    pub fn new() -> Self {
329        Self::default()
330    }
331
332    /// Milliseconds since the scenario started.
333    pub fn now(&self) -> u64 {
334        self.inner.lock().expect("clock lock").now
335    }
336
337    /// Run `task` once the clock has advanced by `delay_ms`.
338    pub fn after(&self, delay_ms: u64, task: impl FnOnce() + Send + 'static) {
339        let mut state = self.inner.lock().expect("clock lock");
340        let due = state.now + delay_ms;
341        state.sequence += 1;
342        let sequence = state.sequence;
343        state.timers.push((due, sequence, Box::new(task)));
344    }
345
346    /// Timers not yet fired.
347    pub fn pending(&self) -> usize {
348        self.inner.lock().expect("clock lock").timers.len()
349    }
350
351    /// Move the clock forward, firing every timer that becomes due.
352    /// Returns how many fired.
353    pub fn advance(&self, delta_ms: u64) -> usize {
354        let target = self.inner.lock().expect("clock lock").now + delta_ms;
355        let mut fired = 0;
356        loop {
357            let next = {
358                let mut state = self.inner.lock().expect("clock lock");
359                let due = state
360                    .timers
361                    .iter()
362                    .enumerate()
363                    .filter(|(_, (due, _, _))| *due <= target)
364                    .min_by_key(|(_, (due, sequence, _))| (*due, *sequence))
365                    .map(|(index, _)| index);
366                due.map(|index| {
367                    let (due, _, task) = state.timers.remove(index);
368                    state.now = due;
369                    task
370                })
371            };
372            match next {
373                Some(task) => {
374                    task();
375                    fired += 1;
376                }
377                None => break,
378            }
379        }
380        self.inner.lock().expect("clock lock").now = target;
381        fired
382    }
383}
384
385impl Clock for ManualClock {
386    fn now_ms(&self) -> u64 {
387        self.now()
388    }
389
390    fn after_ms(&self, delay_ms: u64, task: Timer) {
391        self.after(delay_ms, task);
392    }
393}
394
395#[cfg(not(target_arch = "wasm32"))]
396pub use thread_clock::SystemClock;
397
398#[cfg(not(target_arch = "wasm32"))]
399mod thread_clock {
400    use super::{Clock, Timer};
401    use std::panic::{AssertUnwindSafe, catch_unwind};
402    use std::sync::{Arc, Condvar, Mutex};
403    use std::time::{Duration, Instant};
404
405    /// Real time. One timer thread runs while timers are pending and ends when
406    /// none are left.
407    #[derive(Clone)]
408    pub struct SystemClock {
409        inner: Arc<SystemTimers>,
410    }
411
412    struct SystemTimers {
413        start: Instant,
414        queue: Mutex<TimerQueue>,
415        wake: Condvar,
416    }
417
418    #[derive(Default)]
419    struct TimerQueue {
420        sequence: u64,
421        timers: Vec<(Instant, u64, Timer)>,
422        running: bool,
423    }
424
425    impl Default for SystemClock {
426        fn default() -> Self {
427            Self::new()
428        }
429    }
430
431    impl SystemClock {
432        pub fn new() -> Self {
433            Self {
434                inner: Arc::new(SystemTimers {
435                    start: Instant::now(),
436                    queue: Mutex::new(TimerQueue::default()),
437                    wake: Condvar::new(),
438                }),
439            }
440        }
441
442        fn run(timers: Arc<SystemTimers>) {
443            let mut queue = timers.queue.lock().expect("timer lock");
444            loop {
445                let now = Instant::now();
446                let next = queue
447                    .timers
448                    .iter()
449                    .enumerate()
450                    .min_by_key(|(_, (due, sequence, _))| (*due, *sequence))
451                    .map(|(index, (due, _, _))| (index, *due));
452                match next {
453                    None => {
454                        queue.running = false;
455                        return;
456                    }
457                    Some((index, due)) if due <= now => {
458                        let (_, _, task) = queue.timers.remove(index);
459                        drop(queue);
460                        // A failing task must not stop the timers behind it.
461                        let _ = catch_unwind(AssertUnwindSafe(task));
462                        queue = timers.queue.lock().expect("timer lock");
463                    }
464                    Some((_, due)) => {
465                        queue = timers
466                            .wake
467                            .wait_timeout(queue, due - now)
468                            .expect("timer lock")
469                            .0;
470                    }
471                }
472            }
473        }
474    }
475
476    impl Clock for SystemClock {
477        fn now_ms(&self) -> u64 {
478            self.inner.start.elapsed().as_millis() as u64
479        }
480
481        fn after_ms(&self, delay_ms: u64, task: Timer) {
482            let mut queue = self.inner.queue.lock().expect("timer lock");
483            queue.sequence += 1;
484            let sequence = queue.sequence;
485            queue.timers.push((
486                Instant::now() + Duration::from_millis(delay_ms),
487                sequence,
488                task,
489            ));
490            if !queue.running {
491                queue.running = true;
492                let timers = Arc::clone(&self.inner);
493                if std::thread::Builder::new()
494                    .name("bern-timers".into())
495                    .spawn(move || SystemClock::run(timers))
496                    .is_err()
497                {
498                    queue.running = false;
499                }
500            }
501            self.inner.wake.notify_one();
502        }
503    }
504}
505
506#[cfg(target_arch = "wasm32")]
507pub use page_clock::SystemClock;
508
509#[cfg(target_arch = "wasm32")]
510mod page_clock {
511    use super::{Clock, Timer};
512    use std::collections::BTreeMap;
513    use std::sync::Mutex;
514
515    #[link(wasm_import_module = "bern")]
516    unsafe extern "C" {
517        /// Milliseconds from the page's monotonic clock.
518        fn now_ms() -> f64;
519        /// Call `bern_timer_fired(id)` once `delay_ms` have passed.
520        fn set_timer(id: u32, delay_ms: f64);
521    }
522
523    static TIMERS: Mutex<(u32, BTreeMap<u32, Timer>)> = Mutex::new((0, BTreeMap::new()));
524
525    /// Real time on a web page: timers are the page's own, so tasks run on
526    /// the page's thread between events.
527    #[derive(Clone)]
528    pub struct SystemClock {
529        start: f64,
530    }
531
532    impl Default for SystemClock {
533        fn default() -> Self {
534            Self::new()
535        }
536    }
537
538    impl SystemClock {
539        pub fn new() -> Self {
540            // SAFETY: the page provides `bern.now_ms` with this signature.
541            Self {
542                start: unsafe { now_ms() },
543            }
544        }
545    }
546
547    impl Clock for SystemClock {
548        fn now_ms(&self) -> u64 {
549            // SAFETY: as in `new`.
550            (unsafe { now_ms() } - self.start).max(0.0) as u64
551        }
552
553        fn after_ms(&self, delay_ms: u64, task: Timer) {
554            let id = {
555                let mut timers = TIMERS.lock().expect("timer lock");
556                timers.0 = timers.0.wrapping_add(1);
557                let id = timers.0;
558                timers.1.insert(id, task);
559                id
560            };
561            // SAFETY: the page provides `bern.set_timer` with this signature.
562            unsafe { set_timer(id, delay_ms as f64) };
563        }
564    }
565
566    /// The page calls this when a timer set through `bern.set_timer` is due.
567    #[unsafe(no_mangle)]
568    pub extern "C" fn bern_timer_fired(id: u32) {
569        let task = TIMERS.lock().expect("timer lock").1.remove(&id);
570        if let Some(task) = task {
571            task();
572        }
573    }
574}
575
576#[cfg(test)]
577mod tests {
578    use super::*;
579    use std::sync::mpsc::channel;
580    use std::time::Duration;
581
582    /// A search provider whose latency the scenario chooses per query.
583    struct Search {
584        clock: ManualClock,
585        query: Arc<Mutex<Query<String, Vec<String>>>>,
586        items: Vec<&'static str>,
587        cancelled: Arc<Mutex<Vec<String>>>,
588    }
589
590    impl Search {
591        fn new(clock: &ManualClock) -> Self {
592            Self {
593                clock: clock.clone(),
594                query: Arc::new(Mutex::new(Query::new())),
595                items: vec!["Documents", "Downloads", "Movies", "Music", "Photos"],
596                cancelled: Arc::default(),
597            }
598        }
599
600        /// Type `text` into the search field; the answer takes `latency_ms`.
601        fn type_text(&self, text: &str, latency_ms: u64) {
602            let ticket = self.query.lock().unwrap().request(text.to_string());
603            let query = Arc::clone(&self.query);
604            let cancelled = Arc::clone(&self.cancelled);
605            let text = text.to_lowercase();
606            let results: Vec<String> = self
607                .items
608                .iter()
609                .filter(|item| item.to_lowercase().contains(&text))
610                .map(|item| item.to_string())
611                .collect();
612            self.clock.after(latency_ms, move || {
613                if ticket.cancellation.is_cancelled() {
614                    cancelled.lock().unwrap().push(text);
615                }
616                // A provider that cannot stop in time still answers; the
617                // query drops what is no longer current.
618                query.lock().unwrap().resolve(&ticket, Ok(results));
619            });
620        }
621
622        fn state(&self) -> Resource<Vec<String>> {
623            self.query.lock().unwrap().state().clone()
624        }
625    }
626
627    #[test]
628    fn a_slow_older_search_never_overwrites_a_newer_one() {
629        let clock = ManualClock::new();
630        let search = Search::new(&clock);
631        // "m" is slow, "mo" is fast: the answers arrive out of order.
632        search.type_text("m", 300);
633        search.type_text("mo", 100);
634        assert_eq!(search.state(), Resource::Loading);
635        assert_eq!(clock.advance(100), 1);
636        assert_eq!(search.state(), Resource::Ready(vec!["Movies".to_string()]));
637        assert_eq!(clock.advance(200), 1);
638        // The late answer for "m" was dropped, and its request was cancelled.
639        assert_eq!(search.state(), Resource::Ready(vec!["Movies".to_string()]));
640        assert_eq!(search.query.lock().unwrap().discarded(), 1);
641        assert_eq!(*search.cancelled.lock().unwrap(), vec!["m".to_string()]);
642        assert_eq!(clock.now(), 300);
643    }
644
645    #[test]
646    fn in_order_answers_still_show_only_the_current_search() {
647        let clock = ManualClock::new();
648        let search = Search::new(&clock);
649        search.type_text("d", 100);
650        clock.advance(100);
651        assert_eq!(
652            search.state(),
653            Resource::Ready(vec!["Documents".to_string(), "Downloads".to_string()])
654        );
655        // While the next search loads, the previous results stay visible.
656        search.type_text("do", 200);
657        search.type_text("dow", 300);
658        assert_eq!(
659            search.state(),
660            Resource::Refreshing(vec!["Documents".to_string(), "Downloads".to_string()])
661        );
662        clock.advance(200);
663        assert!(matches!(search.state(), Resource::Refreshing(_)));
664        clock.advance(100);
665        assert_eq!(
666            search.state(),
667            Resource::Ready(vec!["Downloads".to_string()])
668        );
669        // An empty result is a successful answer, not a failure.
670        search.type_text("zzz", 50);
671        clock.advance(50);
672        assert_eq!(search.state(), Resource::Ready(vec![]));
673    }
674
675    #[test]
676    fn failures_keep_previous_data_and_ended_subscriptions_drop_results() {
677        let mut query: Query<String, Vec<i64>> = Query::new();
678        let first = query.request("a".into());
679        assert_eq!(query.resolve(&first, Ok(vec![1])), Resolution::Applied);
680        let second = query.request("b".into());
681        query.resolve(&second, Err(CommandError::new("offline", "no network")));
682        assert_eq!(query.state().data(), Some(&vec![1]));
683        assert!(matches!(query.state(), Resource::Failed { .. }));
684        // The page that owned the subscription closed.
685        let third = query.request("c".into());
686        query.end();
687        assert!(third.cancellation.is_cancelled());
688        assert_eq!(query.resolve(&third, Ok(vec![3])), Resolution::Discarded);
689    }
690
691    #[test]
692    fn subscriptions_end_with_their_window_and_refresh_when_read_state_changes() {
693        let mut subscriptions = QuerySubscriptions::new();
694        let search = subscriptions.open("main#1", 4, vec![2]);
695        let other = subscriptions.open("settings#1", 5, vec![]);
696        // Nothing requested yet: a state change has nothing to refresh.
697        assert!(subscriptions.refresh(&[2]).is_empty());
698        let first = subscriptions
699            .request(search, vec![Value::Text("m".into())])
700            .unwrap();
701        assert_eq!(
702            subscriptions.resolve(search, &first, Ok(Value::Int(1))),
703            Resolution::Applied
704        );
705        subscriptions.request(other, vec![]).unwrap();
706        // Field 2 changed: only the subscription reading it asks again,
707        // with the same parameters, keeping its data visible.
708        let refreshed = subscriptions.refresh(&[0, 2]);
709        assert_eq!(refreshed.len(), 1);
710        let (id, ticket, params) = &refreshed[0];
711        assert_eq!((*id, ticket.version), (search, 2));
712        assert_eq!(params, &vec![Value::Text("m".into())]);
713        assert_eq!(
714            subscriptions.get(search).unwrap().state(),
715            &Resource::Refreshing(Value::Int(1))
716        );
717        // The older answer no longer applies.
718        assert_eq!(
719            subscriptions.resolve(search, &first, Ok(Value::Int(9))),
720            Resolution::Discarded
721        );
722        // Closing the window ends its subscriptions and cancels requests.
723        assert_eq!(subscriptions.close_window("main#1"), vec![search]);
724        assert!(ticket.cancellation.is_cancelled());
725        assert_eq!(
726            subscriptions.resolve(search, ticket, Ok(Value::Int(2))),
727            Resolution::Discarded
728        );
729        assert_eq!(subscriptions.len(), 1);
730        assert!(subscriptions.close(other));
731        assert!(subscriptions.is_empty());
732    }
733
734    #[test]
735    fn the_system_clock_fires_timers_in_due_order() {
736        let clock = SystemClock::new();
737        let (sender, receiver) = channel();
738        for (delay, name) in [(40, "late"), (5, "early"), (20, "middle")] {
739            let sender = sender.clone();
740            clock.after_ms(delay, Box::new(move || sender.send(name).unwrap()));
741        }
742        let order: Vec<_> = (0..3)
743            .map(|_| receiver.recv_timeout(Duration::from_secs(5)).unwrap())
744            .collect();
745        assert_eq!(order, vec!["early", "middle", "late"]);
746        assert!(clock.now_ms() >= 40);
747        // The timer thread ends when idle and starts again on demand.
748        clock.after_ms(1, Box::new(move || sender.send("again").unwrap()));
749        assert_eq!(
750            receiver.recv_timeout(Duration::from_secs(5)).unwrap(),
751            "again"
752        );
753    }
754}