1#[cfg(feature = "recalc")]
2use crate::config::RecalcBackendKind;
3use crate::config::ServerConfig;
4#[cfg(feature = "recalc")]
5use crate::fork::{ForkConfig, ForkRegistry};
6use crate::model::{WorkbookId, WorkbookListResponse};
7#[cfg(feature = "recalc-formualizer")]
8use crate::recalc::FormualizerBackend;
9#[cfg(feature = "recalc")]
10use crate::recalc::{GlobalRecalcLock, GlobalScreenshotLock, RecalcBackend};
11#[cfg(feature = "recalc-libreoffice")]
12use crate::recalc::{LibreOfficeBackend, RecalcConfig};
13use crate::repository::{PathWorkspaceRepository, WorkbookRepository};
14use crate::tools::filters::WorkbookFilter;
15use crate::workbook::WorkbookContext;
16use anyhow::Result;
17use lru::LruCache;
18use parking_lot::RwLock;
19use std::num::NonZeroUsize;
20use std::path::Path;
21use std::sync::Arc;
22use tokio::task;
23
24pub struct AppState {
25 config: Arc<ServerConfig>,
26 repository: Arc<dyn WorkbookRepository>,
27 cache: RwLock<LruCache<WorkbookId, Arc<WorkbookContext>>>,
28 #[cfg(feature = "recalc")]
29 fork_registry: Option<Arc<ForkRegistry>>,
30 #[cfg(feature = "recalc")]
31 recalc_backend_preference: RecalcBackendKind,
32 #[cfg(feature = "recalc")]
33 formualizer_backend: Option<Arc<dyn RecalcBackend>>,
34 #[cfg(feature = "recalc")]
35 libreoffice_backend: Option<Arc<dyn RecalcBackend>>,
36 #[cfg(feature = "recalc")]
37 recalc_semaphore: Option<GlobalRecalcLock>,
38 #[cfg(feature = "recalc")]
39 screenshot_semaphore: Option<GlobalScreenshotLock>,
40}
41
42impl AppState {
43 pub fn new(config: Arc<ServerConfig>) -> Self {
44 #[cfg(feature = "recalc")]
45 let components = init_recalc_components(&config);
46
47 #[cfg(feature = "recalc")]
48 let repository: Arc<dyn WorkbookRepository> = Arc::new(PathWorkspaceRepository::new(
49 config.clone(),
50 components.fork_registry.clone(),
51 ));
52
53 #[cfg(not(feature = "recalc"))]
54 let repository: Arc<dyn WorkbookRepository> =
55 Arc::new(PathWorkspaceRepository::new(config.clone()));
56
57 let capacity = NonZeroUsize::new(config.cache_capacity.max(1)).unwrap();
58
59 Self {
60 config,
61 repository,
62 cache: RwLock::new(LruCache::new(capacity)),
63 #[cfg(feature = "recalc")]
64 fork_registry: components.fork_registry,
65 #[cfg(feature = "recalc")]
66 recalc_backend_preference: components.recalc_backend_preference,
67 #[cfg(feature = "recalc")]
68 formualizer_backend: components.formualizer_backend,
69 #[cfg(feature = "recalc")]
70 libreoffice_backend: components.libreoffice_backend,
71 #[cfg(feature = "recalc")]
72 recalc_semaphore: components.recalc_semaphore,
73 #[cfg(feature = "recalc")]
74 screenshot_semaphore: components.screenshot_semaphore,
75 }
76 }
77
78 pub fn new_with_repository(
79 config: Arc<ServerConfig>,
80 repository: Arc<dyn WorkbookRepository>,
81 ) -> Self {
82 let capacity = NonZeroUsize::new(config.cache_capacity.max(1)).unwrap();
83
84 #[cfg(feature = "recalc")]
85 let components = init_recalc_components(&config);
86
87 Self {
88 config,
89 repository,
90 cache: RwLock::new(LruCache::new(capacity)),
91 #[cfg(feature = "recalc")]
92 fork_registry: components.fork_registry,
93 #[cfg(feature = "recalc")]
94 recalc_backend_preference: components.recalc_backend_preference,
95 #[cfg(feature = "recalc")]
96 formualizer_backend: components.formualizer_backend,
97 #[cfg(feature = "recalc")]
98 libreoffice_backend: components.libreoffice_backend,
99 #[cfg(feature = "recalc")]
100 recalc_semaphore: components.recalc_semaphore,
101 #[cfg(feature = "recalc")]
102 screenshot_semaphore: components.screenshot_semaphore,
103 }
104 }
105
106 pub fn config(&self) -> Arc<ServerConfig> {
107 self.config.clone()
108 }
109
110 #[cfg(feature = "recalc")]
111 pub fn fork_registry(&self) -> Option<&Arc<ForkRegistry>> {
112 self.fork_registry.as_ref()
113 }
114
115 #[cfg(feature = "recalc")]
116 pub fn recalc_backend(
117 &self,
118 requested: Option<RecalcBackendKind>,
119 ) -> Option<Arc<dyn RecalcBackend>> {
120 let effective = requested.unwrap_or(self.recalc_backend_preference);
121 match effective {
122 RecalcBackendKind::Formualizer => self.formualizer_backend.clone(),
123 RecalcBackendKind::Libreoffice => self.libreoffice_backend.clone(),
124 RecalcBackendKind::Auto => self
125 .formualizer_backend
126 .clone()
127 .or_else(|| self.libreoffice_backend.clone()),
128 }
129 }
130
131 #[cfg(feature = "recalc")]
132 pub fn recalc_semaphore(&self) -> Option<&GlobalRecalcLock> {
133 self.recalc_semaphore.as_ref()
134 }
135
136 #[cfg(feature = "recalc")]
137 pub fn screenshot_semaphore(&self) -> Option<&GlobalScreenshotLock> {
138 self.screenshot_semaphore.as_ref()
139 }
140
141 pub fn list_workbooks(&self, filter: WorkbookFilter) -> Result<WorkbookListResponse> {
142 self.repository.list(&filter)
143 }
144
145 pub async fn open_workbook(&self, workbook_id: &WorkbookId) -> Result<Arc<WorkbookContext>> {
146 let resolved = self.repository.resolve(workbook_id)?;
147 let canonical = resolved.workbook_id.clone();
148 {
149 let mut cache = self.cache.write();
150 if let Some(entry) = cache.get(&canonical) {
151 return Ok(entry.clone());
152 }
153 }
154
155 let repo = self.repository.clone();
156 let workbook = task::spawn_blocking(move || repo.load_context(&resolved)).await??;
157 let workbook = Arc::new(workbook);
158
159 let mut cache = self.cache.write();
160 cache.put(canonical, workbook.clone());
161 Ok(workbook)
162 }
163
164 pub fn close_workbook(&self, workbook_id: &WorkbookId) -> Result<()> {
165 let canonical = self.repository.resolve(workbook_id)?.workbook_id;
166 let mut cache = self.cache.write();
167 cache.pop(&canonical);
168 Ok(())
169 }
170
171 pub fn evict_by_path(&self, path: &Path) {
172 let evict_ids: Vec<WorkbookId> = self
173 .cache
174 .read()
175 .iter()
176 .filter_map(|(id, ctx)| {
177 if ctx.path == path {
178 Some(id.clone())
179 } else {
180 None
181 }
182 })
183 .collect();
184
185 if evict_ids.is_empty() {
186 return;
187 }
188
189 let mut cache = self.cache.write();
190 for id in evict_ids {
191 cache.pop(&id);
192 }
193 }
194}
195
196#[cfg(feature = "recalc")]
197struct RecalcComponents {
198 fork_registry: Option<Arc<ForkRegistry>>,
199 recalc_backend_preference: RecalcBackendKind,
200 formualizer_backend: Option<Arc<dyn RecalcBackend>>,
201 libreoffice_backend: Option<Arc<dyn RecalcBackend>>,
202 recalc_semaphore: Option<GlobalRecalcLock>,
203 screenshot_semaphore: Option<GlobalScreenshotLock>,
204}
205
206#[cfg(feature = "recalc")]
207fn init_recalc_components(config: &Arc<ServerConfig>) -> RecalcComponents {
208 if !config.recalc_enabled {
209 return RecalcComponents {
210 fork_registry: None,
211 recalc_backend_preference: config.recalc_backend,
212 formualizer_backend: None,
213 libreoffice_backend: None,
214 recalc_semaphore: None,
215 screenshot_semaphore: None,
216 };
217 }
218
219 let fork_config = ForkConfig::default();
220 let registry = ForkRegistry::new(fork_config)
221 .map(Arc::new)
222 .map_err(|e| tracing::warn!("failed to init fork registry: {}", e))
223 .ok();
224
225 if let Some(registry) = ®istry {
226 registry.clone().start_cleanup_task();
227 }
228
229 #[cfg(feature = "recalc-formualizer")]
230 let formualizer_backend: Option<Arc<dyn RecalcBackend>> = Some(Arc::new(FormualizerBackend));
231 #[cfg(not(feature = "recalc-formualizer"))]
232 let formualizer_backend: Option<Arc<dyn RecalcBackend>> = None;
233
234 #[cfg(feature = "recalc-libreoffice")]
235 let libreoffice_backend: Option<Arc<dyn RecalcBackend>> = {
236 let backend: Arc<dyn RecalcBackend> =
237 Arc::new(LibreOfficeBackend::new(RecalcConfig::default()));
238 if backend.is_available() {
239 Some(backend)
240 } else {
241 tracing::warn!("libreoffice backend not available (soffice not found)");
242 None
243 }
244 };
245 #[cfg(not(feature = "recalc-libreoffice"))]
246 let libreoffice_backend: Option<Arc<dyn RecalcBackend>> = None;
247
248 let selected = match config.recalc_backend {
249 RecalcBackendKind::Auto => formualizer_backend
250 .as_ref()
251 .or(libreoffice_backend.as_ref())
252 .map(|backend| backend.name()),
253 RecalcBackendKind::Formualizer => {
254 formualizer_backend.as_ref().map(|backend| backend.name())
255 }
256 RecalcBackendKind::Libreoffice => {
257 libreoffice_backend.as_ref().map(|backend| backend.name())
258 }
259 };
260
261 if selected.is_none() {
262 tracing::warn!(
263 preferred = ?config.recalc_backend,
264 "recalc backend not available for current build/runtime"
265 );
266 }
267
268 let semaphore = GlobalRecalcLock::new(config.max_concurrent_recalcs);
269 let screenshot_semaphore = libreoffice_backend
270 .as_ref()
271 .map(|_| GlobalScreenshotLock::new());
272
273 RecalcComponents {
274 fork_registry: registry,
275 recalc_backend_preference: config.recalc_backend,
276 formualizer_backend,
277 libreoffice_backend,
278 recalc_semaphore: Some(semaphore),
279 screenshot_semaphore,
280 }
281}