tinymist_vfs/
notify.rs

1use core::fmt;
2use std::path::Path;
3
4use rpds::RedBlackTreeMapSync;
5use typst::diag::FileResult;
6
7use crate::{Bytes, FileChangeSet, FileSnapshot, ImmutPath, PathAccessModel};
8
9/// A memory event that is notified by some external source
10#[derive(Debug, Clone)]
11pub enum MemoryEvent {
12    /// Reset all dependencies and update according to the given changeset
13    ///
14    /// We have not provided a way to reset all dependencies without updating
15    /// yet, but you can create a memory event with empty changeset to achieve
16    /// this:
17    ///
18    /// ```
19    /// use tinymist_vfs::{FileChangeSet, notify::MemoryEvent};
20    /// let event = MemoryEvent::Sync(FileChangeSet::default());
21    /// ```
22    Sync(FileChangeSet),
23    /// Update according to the given changeset
24    Update(FileChangeSet),
25}
26
27/// A upstream update event that is notified by some external source.
28///
29/// This event is used to notify some file watcher to invalidate some files
30/// before applying upstream changes. This is very important to make some atomic
31/// changes.
32#[derive(Debug)]
33pub struct UpstreamUpdateEvent {
34    /// Associated files that the event causes to invalidate
35    pub invalidates: Vec<ImmutPath>,
36    /// Opaque data that is passed to the file watcher
37    pub opaque: Box<dyn std::any::Any + Send>,
38}
39
40/// Aggregated filesystem events from some file watcher
41#[derive(Debug)]
42pub enum FilesystemEvent {
43    /// Update file system files according to the given changeset
44    Update(FileChangeSet, /* is_sync */ bool),
45    /// See [`UpstreamUpdateEvent`]
46    UpstreamUpdate {
47        /// New changeset produced by invalidation
48        changeset: FileChangeSet,
49        /// The upstream event that causes the invalidation
50        upstream_event: Option<UpstreamUpdateEvent>,
51    },
52}
53
54impl FilesystemEvent {
55    /// Splits the filesystem event into a changeset and an optional upstream
56    /// event.
57    pub fn split(self) -> (FileChangeSet, Option<UpstreamUpdateEvent>) {
58        match self {
59            FilesystemEvent::UpstreamUpdate {
60                changeset,
61                upstream_event,
62            } => (changeset, upstream_event),
63            FilesystemEvent::Update(changeset, ..) => (changeset, None),
64        }
65    }
66
67    /// Splits the filesystem event into a changeset and an optional upstream
68    /// event.
69    pub fn split_with_is_sync(self) -> (FileChangeSet, bool, Option<UpstreamUpdateEvent>) {
70        match self {
71            FilesystemEvent::UpstreamUpdate {
72                changeset,
73                upstream_event,
74            } => (changeset, false, upstream_event),
75            FilesystemEvent::Update(changeset, is_sync) => (changeset, is_sync, None),
76        }
77    }
78}
79
80/// A trait implementing dependency getter.
81pub trait NotifyDeps: fmt::Debug + Send + Sync {
82    /// Gets the dependencies recorded in the world. It is a list of
83    /// accessed file recorded during this revision, e.g. a single compilation
84    /// or other compiler tasks.
85    fn dependencies(&self, f: &mut dyn FnMut(&ImmutPath));
86}
87
88impl NotifyDeps for Vec<ImmutPath> {
89    fn dependencies(&self, f: &mut dyn FnMut(&ImmutPath)) {
90        for path in self.iter() {
91            f(path);
92        }
93    }
94}
95
96/// A message that is sent to some file watcher
97#[derive(Debug)]
98pub enum NotifyMessage {
99    /// Oettle the watching
100    Settle,
101    /// Overrides all dependencies
102    SyncDependency(Box<dyn NotifyDeps>),
103    /// upstream invalidation This is very important to make some atomic changes
104    ///
105    /// Example:
106    /// ```plain
107    ///   /// Receive memory event
108    ///   let event: MemoryEvent = retrieve();
109    ///   let invalidates = event.invalidates();
110    ///
111    ///   /// Send memory change event to [`NotifyActor`]
112    ///   let event = Box::new(event);
113    ///   self.send(NotifyMessage::UpstreamUpdate{ invalidates, opaque: event });
114    ///
115    ///   /// Wait for [`NotifyActor`] to finish
116    ///   let fs_event = self.fs_notify.block_receive();
117    ///   let event: MemoryEvent = fs_event.opaque.downcast().unwrap();
118    ///
119    ///   /// Apply changes
120    ///   self.lock();
121    ///   update_memory(event);
122    ///   apply_fs_changes(fs_event.changeset);
123    ///   self.unlock();
124    /// ```
125    UpstreamUpdate(UpstreamUpdateEvent),
126}
127
128/// Provides notify access model which retrieves file system events and changes
129/// from some notify backend.
130///
131/// It simply hold notified filesystem data in memory, but still have a fallback
132/// access model, whose the typical underlying access model is
133/// [`crate::system::SystemAccessModel`]
134#[derive(Debug, Clone)]
135pub struct NotifyAccessModel<M> {
136    files: RedBlackTreeMapSync<ImmutPath, FileSnapshot>,
137    /// The fallback access model when the file is not notified ever.
138    pub inner: M,
139}
140
141impl<M: PathAccessModel> NotifyAccessModel<M> {
142    /// Create a new notify access model
143    pub fn new(inner: M) -> Self {
144        Self {
145            files: RedBlackTreeMapSync::default(),
146            inner,
147        }
148    }
149
150    /// Notify the access model with a filesystem event
151    pub fn notify(&mut self, changeset: FileChangeSet) {
152        for path in changeset.removes {
153            self.files.remove_mut(&path);
154        }
155
156        for (path, contents) in changeset.inserts {
157            self.files.insert_mut(path, contents);
158        }
159    }
160}
161
162impl<M: PathAccessModel> PathAccessModel for NotifyAccessModel<M> {
163    #[inline]
164    fn reset(&mut self) {
165        self.inner.reset();
166    }
167
168    fn content(&self, src: &Path) -> FileResult<Bytes> {
169        if let Some(entry) = self.files.get(src) {
170            return entry.content().cloned();
171        }
172
173        self.inner.content(src)
174    }
175}