Skip to main content

reflexo_vfs/
notify.rs

1use core::fmt;
2use std::path::Path;
3
4use rpds::RedBlackTreeMapSync;
5use typst::diag::{FileError, FileResult};
6
7use crate::{AccessModel, Bytes, ImmutPath};
8
9/// internal representation of [`NotifyFile`]
10#[derive(Debug, Clone)]
11struct NotifyFileRepr {
12    mtime: crate::Time,
13    content: Bytes,
14}
15
16/// A file snapshot that is notified by some external source
17///
18/// Note: The error is boxed to avoid large stack size
19#[derive(Clone)]
20pub struct FileSnapshot(Result<NotifyFileRepr, Box<FileError>>);
21
22impl fmt::Debug for FileSnapshot {
23    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24        match self.0.as_ref() {
25            Ok(v) => f
26                .debug_struct("FileSnapshot")
27                .field("mtime", &v.mtime)
28                .field(
29                    "content",
30                    &FileContent {
31                        len: v.content.len(),
32                    },
33                )
34                .finish(),
35            Err(e) => f.debug_struct("FileSnapshot").field("error", &e).finish(),
36        }
37    }
38}
39
40impl FileSnapshot {
41    /// Access the internal data of the file snapshot
42    #[inline]
43    #[track_caller]
44    fn retrieve<'a, T>(&'a self, f: impl FnOnce(&'a NotifyFileRepr) -> T) -> FileResult<T> {
45        self.0.as_ref().map(f).map_err(|e| *e.clone())
46    }
47
48    /// mtime of the file
49    pub fn mtime(&self) -> FileResult<&crate::Time> {
50        self.retrieve(|e| &e.mtime)
51    }
52
53    /// content of the file
54    pub fn content(&self) -> FileResult<&Bytes> {
55        self.retrieve(|e| &e.content)
56    }
57
58    /// Whether the related file is a file
59    pub fn is_file(&self) -> FileResult<bool> {
60        self.retrieve(|_| true)
61    }
62}
63
64/// Convenient function to create a [`FileSnapshot`] from tuple
65impl From<FileResult<(crate::Time, Bytes)>> for FileSnapshot {
66    fn from(result: FileResult<(crate::Time, Bytes)>) -> Self {
67        Self(
68            result
69                .map(|(mtime, content)| NotifyFileRepr { mtime, content })
70                .map_err(Box::new),
71        )
72    }
73}
74
75/// A set of changes to the filesystem.
76///
77/// The correct order of applying changes is:
78/// 1. Remove files
79/// 2. Upsert (Insert or Update) files
80#[derive(Debug, Clone, Default)]
81pub struct FileChangeSet {
82    /// Files to remove
83    pub removes: Vec<ImmutPath>,
84    /// Files to insert or update
85    pub inserts: Vec<(ImmutPath, FileSnapshot)>,
86}
87
88impl FileChangeSet {
89    /// Create a new empty changeset
90    pub fn is_empty(&self) -> bool {
91        self.inserts.is_empty() && self.removes.is_empty()
92    }
93
94    /// Create a new changeset with removing files
95    pub fn new_removes(removes: Vec<ImmutPath>) -> Self {
96        Self {
97            removes,
98            inserts: vec![],
99        }
100    }
101
102    /// Create a new changeset with inserting files
103    pub fn new_inserts(inserts: Vec<(ImmutPath, FileSnapshot)>) -> Self {
104        Self {
105            removes: vec![],
106            inserts,
107        }
108    }
109
110    /// Utility function to insert a possible file to insert or update
111    pub fn may_insert(&mut self, v: Option<(ImmutPath, FileSnapshot)>) {
112        if let Some(v) = v {
113            self.inserts.push(v);
114        }
115    }
116
117    /// Utility function to insert multiple possible files to insert or update
118    pub fn may_extend(&mut self, v: Option<impl Iterator<Item = (ImmutPath, FileSnapshot)>>) {
119        if let Some(v) = v {
120            self.inserts.extend(v);
121        }
122    }
123}
124
125/// A memory event that is notified by some external source
126#[derive(Debug)]
127pub enum MemoryEvent {
128    /// Reset all dependencies and update according to the given changeset
129    ///
130    /// We have not provided a way to reset all dependencies without updating
131    /// yet, but you can create a memory event with empty changeset to achieve
132    /// this:
133    ///
134    /// ```
135    /// use typst_ts_compiler::vfs::notify::{MemoryEvent, FileChangeSet};
136    /// let event = MemoryEvent::Sync(FileChangeSet::default());
137    /// ```
138    Sync(FileChangeSet),
139    /// Update according to the given changeset
140    Update(FileChangeSet),
141}
142
143/// A upstream update event that is notified by some external source.
144///
145/// This event is used to notify some file watcher to invalidate some files
146/// before applying upstream changes. This is very important to make some atomic
147/// changes.
148#[derive(Debug)]
149pub struct UpstreamUpdateEvent {
150    /// Associated files that the event causes to invalidate
151    pub invalidates: Vec<ImmutPath>,
152    /// Opaque data that is passed to the file watcher
153    pub opaque: Box<dyn std::any::Any + Send>,
154}
155
156/// Aggregated filesystem events from some file watcher
157#[derive(Debug)]
158pub enum FilesystemEvent {
159    /// Update file system files according to the given changeset
160    Update(FileChangeSet),
161    /// See [`UpstreamUpdateEvent`]
162    UpstreamUpdate {
163        /// New changeset produced by invalidation
164        changeset: FileChangeSet,
165        /// The upstream event that causes the invalidation
166        upstream_event: Option<UpstreamUpdateEvent>,
167    },
168}
169
170/// A message that is sent to some file watcher
171#[derive(Debug)]
172pub enum NotifyMessage {
173    /// Oettle the watching
174    Settle,
175    /// Overrides all dependencies
176    SyncDependency(Vec<ImmutPath>),
177    /// upstream invalidation This is very important to make some atomic changes
178    ///
179    /// Example:
180    /// ```plain
181    ///   /// Receive memory event
182    ///   let event: MemoryEvent = retrieve();
183    ///   let invalidates = event.invalidates();
184    ///
185    ///   /// Send memory change event to [`NotifyActor`]
186    ///   let event = Box::new(event);
187    ///   self.send(NotifyMessage::UpstreamUpdate{ invalidates, opaque: event });
188    ///
189    ///   /// Wait for [`NotifyActor`] to finish
190    ///   let fs_event = self.fs_notify.block_receive();
191    ///   let event: MemoryEvent = fs_event.opaque.downcast().unwrap();
192    ///
193    ///   /// Apply changes
194    ///   self.lock();
195    ///   update_memory(event);
196    ///   apply_fs_changes(fs_event.changeset);
197    ///   self.unlock();
198    /// ```
199    UpstreamUpdate(UpstreamUpdateEvent),
200}
201
202/// Provides notify access model which retrieves file system events and changes
203/// from some notify backend.
204///
205/// It simply hold notified filesystem data in memory, but still have a fallback
206/// access model, whose the typical underlying access model is
207/// [`crate::system::SystemAccessModel`]
208#[derive(Debug, Clone)]
209pub struct NotifyAccessModel<M> {
210    files: RedBlackTreeMapSync<ImmutPath, FileSnapshot>,
211    /// The fallback access model when the file is not notified ever.
212    pub inner: M,
213}
214
215impl<M: AccessModel> NotifyAccessModel<M> {
216    /// Create a new notify access model
217    pub fn new(inner: M) -> Self {
218        Self {
219            files: RedBlackTreeMapSync::default(),
220            inner,
221        }
222    }
223
224    /// Notify the access model with a filesystem event
225    pub fn notify(&mut self, event: FilesystemEvent) {
226        match event {
227            FilesystemEvent::UpstreamUpdate { changeset, .. }
228            | FilesystemEvent::Update(changeset) => {
229                for path in changeset.removes {
230                    self.files.remove_mut(&path);
231                }
232
233                for (path, contents) in changeset.inserts {
234                    self.files.insert_mut(path, contents);
235                }
236            }
237        }
238    }
239}
240
241impl<M: AccessModel> AccessModel for NotifyAccessModel<M> {
242    fn mtime(&self, src: &Path) -> FileResult<crate::Time> {
243        if let Some(entry) = self.files.get(src) {
244            return entry.mtime().cloned();
245        }
246
247        self.inner.mtime(src)
248    }
249
250    fn is_file(&self, src: &Path) -> FileResult<bool> {
251        if let Some(entry) = self.files.get(src) {
252            return entry.is_file();
253        }
254
255        self.inner.is_file(src)
256    }
257
258    fn real_path(&self, src: &Path) -> FileResult<ImmutPath> {
259        if self.files.contains_key(src) {
260            return Ok(src.into());
261        }
262
263        self.inner.real_path(src)
264    }
265
266    fn content(&self, src: &Path) -> FileResult<Bytes> {
267        if let Some(entry) = self.files.get(src) {
268            return entry.content().cloned();
269        }
270
271        self.inner.content(src)
272    }
273}
274
275#[derive(Debug)]
276#[allow(dead_code)]
277struct FileContent {
278    len: usize,
279}