Skip to main content

vissue_tui/
backend.rs

1//! Sync board facade shared by the file-backed and socket-backed clients.
2
3use vissue_core::config::Layout;
4use vissue_core::error::Error;
5use vissue_core::views::{
6    AgendaRow, ClaimRow, Excerpt, IssueDetail, ListQuery, RelatedHit, SearchHit, TreeNode,
7};
8
9/// Which store the board is talking to.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum BackendKind {
12    Core,
13    Control,
14}
15
16/// One page of list/ready rows.
17#[derive(Debug, Clone, Default)]
18pub struct ListPage {
19    pub issues: Vec<vissue_core::views::IssueRow>,
20    pub total: u64,
21    pub matched: u64,
22    pub revision: u64,
23    pub generation: u64,
24    pub unchanged: bool,
25}
26
27/// Outcome of claim, note, or update.
28#[derive(Debug, Clone)]
29pub struct MutResult {
30    pub ok: bool,
31    pub report: String,
32    pub issue: Option<IssueDetail>,
33    pub revision: u64,
34    pub generation: u64,
35}
36
37/// Fields `issue/update` accepts.
38#[derive(Debug, Clone, Default)]
39pub struct UpdateReq {
40    pub id: String,
41    pub state: Option<String>,
42    pub priority: Option<char>,
43    pub block: Option<String>,
44    pub unblock: Option<String>,
45}
46
47/// Drops `since_revision` for one fetch after attach.
48#[derive(Debug)]
49pub struct SinceGate {
50    skip_once: std::sync::atomic::AtomicBool,
51}
52
53impl SinceGate {
54    /// After `initialize`, the next list must not send a core generation.
55    pub fn after_attach() -> Self {
56        Self {
57            skip_once: std::sync::atomic::AtomicBool::new(true),
58        }
59    }
60
61    pub fn next(&self, revision: u64) -> Option<u64> {
62        if self
63            .skip_once
64            .swap(false, std::sync::atomic::Ordering::SeqCst)
65        {
66            None
67        } else if revision > 0 {
68            Some(revision)
69        } else {
70            None
71        }
72    }
73
74    /// Next list/ready must omit `since_revision` (pane or query changed).
75    pub fn invalidate(&self) {
76        self.skip_once
77            .store(true, std::sync::atomic::Ordering::SeqCst);
78    }
79}
80
81/// Read and mutate the board. Implementations are `CoreBackend` and
82/// `ControlBackend`.
83pub trait BoardBackend: Send + Sync {
84    fn layout(&self) -> &Layout;
85    fn generation(&self) -> u64;
86    fn revision(&self) -> u64;
87    fn live(&self) -> BackendKind;
88    fn identity(&self) -> &str;
89
90    fn list(&self, q: ListQuery) -> Result<ListPage, Error>;
91    fn ready(&self, project: Option<&str>) -> Result<ListPage, Error>;
92    fn get(&self, id: &str) -> Result<IssueDetail, Error>;
93    fn excerpt(&self, id: &str) -> Result<Excerpt, Error>;
94    fn search(&self, query: &str, limit: usize) -> Result<Vec<SearchHit>, Error>;
95    fn claims(&self, holder: Option<&str>, project: Option<&str>) -> Result<Vec<ClaimRow>, Error>;
96    fn agenda(&self, days: i64, project: Option<&str>) -> Result<Vec<AgendaRow>, Error>;
97    fn tree(&self, id: &str) -> Result<TreeNode, Error>;
98    fn related(&self, id: &str, depth: usize, limit: usize) -> Result<Vec<RelatedHit>, Error>;
99    fn projects(&self) -> Result<Vec<String>, Error>;
100    fn claim(&self, id: &str, force: bool) -> Result<MutResult, Error>;
101    fn note(&self, id: &str, text: &str) -> Result<MutResult, Error>;
102    fn update(&self, req: UpdateReq) -> Result<MutResult, Error>;
103    fn open(&self, id: &str) -> Result<IssueDetail, Error>;
104
105    /// Core: wait on the file generation. Control: wait for `vault/changed`.
106    fn wait(&self, last: u64, timeout_ms: u64) -> Result<u64, Error>;
107
108    /// Last `since_revision` sent on list/ready. `None` means the field was
109    /// omitted. Default is "not recorded".
110    fn last_since_revision(&self) -> Option<Option<u64>> {
111        None
112    }
113
114    /// Drop `since_revision` on the next list/ready. Serve `unchanged` is
115    /// catalog-wide, so a pane or project change must fetch a full page.
116    fn invalidate_since(&self) {}
117
118    /// Re-read the files. Core uses this after an out-of-band write such as
119    /// `ops::create`. Control is a no-op; serve sees the file event.
120    fn refresh(&self) -> Result<(), Error> {
121        Ok(())
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::SinceGate;
128
129    #[test]
130    fn after_attach_first_list_omits_since_revision() {
131        let gate = SinceGate::after_attach();
132        assert_eq!(gate.next(7), None);
133        assert_eq!(gate.next(7), Some(7));
134        assert_eq!(gate.next(8), Some(8));
135    }
136
137    #[test]
138    fn a_zero_revision_never_sends_since() {
139        let gate = SinceGate::after_attach();
140        assert_eq!(gate.next(0), None);
141        assert_eq!(gate.next(0), None);
142    }
143
144    #[test]
145    fn invalidate_omits_the_next_since() {
146        let gate = SinceGate::after_attach();
147        assert_eq!(gate.next(7), None);
148        assert_eq!(gate.next(7), Some(7));
149        gate.invalidate();
150        assert_eq!(gate.next(7), None);
151        assert_eq!(gate.next(7), Some(7));
152    }
153}