Skip to main content

origin_platform/
watcher.rs

1//! Workspace watcher contract (B4 of the Gitbit platform requirements).
2//!
3//! Event-driven local sync: watches a workspace for file changes so a product like
4//! Gitbit does not have to poll. Changes flow as typed events (ARCHITECTURE.md Rule 10).
5
6use crate::workspace::WorkspaceRoot;
7use async_trait::async_trait;
8use origin_domain::Result;
9use std::fmt::Debug;
10use tokio::sync::broadcast;
11
12/// How many change events to buffer while a subscriber catches up.
13pub(crate) const BUFFER_SIZE: usize = 64;
14
15/// A file system change within a watched workspace root.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum WorkspaceChange {
18    Created { path: String },
19    Modified { path: String },
20    Removed { path: String },
21}
22
23impl WorkspaceChange {
24    pub fn created(path: impl Into<String>) -> Self {
25        Self::Created { path: path.into() }
26    }
27
28    pub fn modified(path: impl Into<String>) -> Self {
29        Self::Modified { path: path.into() }
30    }
31
32    pub fn removed(path: impl Into<String>) -> Self {
33        Self::Removed { path: path.into() }
34    }
35
36    pub fn path(&self) -> &str {
37        match self {
38            Self::Created { path } | Self::Modified { path } | Self::Removed { path } => path,
39        }
40    }
41}
42
43/// A live watcher subscription. Dropping it stops receiving events.
44///
45/// Obtained from [`WorkspaceWatcher::watch`]. The adapter that implements
46/// [`WorkspaceWatcher`] maps platform-level file-system events into
47/// [`WorkspaceChange`] and publishes them to the application event bus.
48pub struct WatchHandle {
49    receiver: broadcast::Receiver<WorkspaceChange>,
50}
51
52impl Debug for WatchHandle {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        f.debug_struct("WatchHandle").finish_non_exhaustive()
55    }
56}
57
58impl WatchHandle {
59    /// Wrap a broadcast receiver. Used by [`WorkspaceWatcher`] implementations.
60    ///
61    /// Public so an adapter crate can construct the handle it returns; the shape is
62    /// otherwise opaque on purpose.
63    pub fn new(receiver: broadcast::Receiver<WorkspaceChange>) -> Self {
64        Self { receiver }
65    }
66
67    /// Wait for the next change event.
68    pub async fn recv(&mut self) -> Result<WorkspaceChange, broadcast::error::RecvError> {
69        self.receiver.recv().await
70    }
71
72    /// Returns `Some` if a change is already waiting, `None` otherwise.
73    pub fn try_recv(&mut self) -> Result<WorkspaceChange, broadcast::error::TryRecvError> {
74        self.receiver.try_recv()
75    }
76}
77
78/// Watches a workspace root for file changes.
79///
80/// Implementations publish [`WorkspaceChange`] events to a per-root broadcast
81/// channel. The memory double (for tests) pushes synthetic changes; the Tauri
82/// adapter wraps `notify`.
83#[async_trait]
84pub trait WorkspaceWatcher: Debug + Send + Sync + 'static {
85    /// Start watching `root`. The returned [`WatchHandle`] yields change events.
86    ///
87    /// Calling `watch` on an already-watched root may return a new handle to the
88    /// same channel — implementations may deduplicate internally.
89    async fn watch(&self, root: &WorkspaceRoot) -> Result<WatchHandle>;
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn workspace_change_path_accessor() {
98        let change = WorkspaceChange::modified("src/main.rs");
99        assert_eq!(change.path(), "src/main.rs");
100    }
101}