1use notify::Watcher as _;
8use pushkin_core::envelope::CheckResult;
9use pushkin_core::manifest::Manifest;
10use pushkin_core::pipeline::{check_write, WriteRequest};
11use std::collections::HashMap;
12use std::path::Path;
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::sync::{Arc, Mutex, RwLock};
15
16fn fnv1a(bytes: &[u8]) -> u64 {
20 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
21 for byte in bytes {
22 hash ^= u64::from(*byte);
23 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
24 }
25 hash
26}
27
28type Memo = HashMap<String, (u64, CheckResult)>;
29
30struct Inner {
31 manifest: RwLock<Manifest>,
32 memo: Mutex<Memo>,
33 hits: AtomicU64,
34}
35
36pub struct WarmState {
38 inner: Arc<Inner>,
39}
40
41pub struct WatchGuard {
43 _watcher: notify::RecommendedWatcher,
44}
45
46impl WarmState {
47 #[must_use]
48 pub fn new(manifest: Manifest) -> Self {
49 Self {
50 inner: Arc::new(Inner {
51 manifest: RwLock::new(manifest),
52 memo: Mutex::new(HashMap::new()),
53 hits: AtomicU64::new(0),
54 }),
55 }
56 }
57
58 #[must_use]
61 pub fn share(&self) -> Self {
62 Self {
63 inner: Arc::clone(&self.inner),
64 }
65 }
66
67 #[must_use]
71 pub fn check(&self, request: &WriteRequest) -> CheckResult {
72 let content_hash = fnv1a(request.content.as_bytes());
73 if let Ok(memo) = self.inner.memo.lock() {
74 if let Some((stored_hash, stored)) = memo.get(&request.file_path) {
75 if *stored_hash == content_hash {
76 self.inner.hits.fetch_add(1, Ordering::SeqCst);
77 return stored.clone();
78 }
79 }
80 }
81 let result = match self.inner.manifest.read() {
82 Ok(manifest) => check_write(&manifest, request),
83 Err(poisoned) => check_write(&poisoned.into_inner(), request),
84 };
85 if let Ok(mut memo) = self.inner.memo.lock() {
86 memo.insert(request.file_path.clone(), (content_hash, result.clone()));
87 }
88 result
89 }
90
91 #[must_use]
93 pub fn hits(&self) -> u64 {
94 self.inner.hits.load(Ordering::SeqCst)
95 }
96
97 pub fn swap_manifest(&self, manifest: Manifest) {
100 match self.inner.manifest.write() {
101 Ok(mut slot) => *slot = manifest,
102 Err(poisoned) => *poisoned.into_inner() = manifest,
103 }
104 if let Ok(mut memo) = self.inner.memo.lock() {
105 memo.clear();
106 }
107 }
108
109 pub fn invalidate_path(&self, path: &str) {
111 if let Ok(mut memo) = self.inner.memo.lock() {
112 memo.remove(path);
113 }
114 }
115
116 pub fn watch(&self, root: &Path) -> std::io::Result<WatchGuard> {
125 let inner = Arc::clone(&self.inner);
126 let root_buf = root.to_path_buf();
127 let mut watcher =
128 notify::recommended_watcher(move |event: Result<notify::Event, notify::Error>| {
129 let Ok(event) = event else { return };
130 for path in event.paths {
131 handle_change(&inner, &root_buf, &path);
132 }
133 })
134 .map_err(std::io::Error::other)?;
135 watcher
136 .watch(root, notify::RecursiveMode::Recursive)
137 .map_err(std::io::Error::other)?;
138 Ok(WatchGuard { _watcher: watcher })
139 }
140}
141
142fn handle_change(inner: &Inner, root: &Path, changed: &Path) {
143 let is_manifest = changed
144 .file_name()
145 .is_some_and(|name| name == "pushkin.toml");
146 if is_manifest {
147 let Ok(text) = std::fs::read_to_string(changed) else {
150 return;
151 };
152 let Ok(manifest) = Manifest::parse(&text) else {
153 return;
154 };
155 match inner.manifest.write() {
156 Ok(mut slot) => *slot = manifest,
157 Err(poisoned) => *poisoned.into_inner() = manifest,
158 }
159 if let Ok(mut memo) = inner.memo.lock() {
160 memo.clear();
161 }
162 return;
163 }
164 let key = changed
165 .strip_prefix(root)
166 .unwrap_or(changed)
167 .to_string_lossy()
168 .into_owned();
169 if let Ok(mut memo) = inner.memo.lock() {
170 memo.remove(&key);
171 }
172}