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}