Skip to main content

Mountain/Environment/
StorageProvider.rs

1//! # StorageProvider (Environment)
2//!
3//! Implements the `StorageProvider` trait for `MountainEnvironment`. Contains
4//! the core logic for Memento storage: reading from and writing to JSON
5//! storage files on disk.
6//!
7//! ## Storage scopes
8//!
9//! - **Global** (`IsGlobalScope = true`) - application-level key-value store
10//!   shared across all workspaces; persisted to `GlobalMementoPath`. Used for
11//!   user preferences, extension state.
12//! - **Workspace** (`IsGlobalScope = false`) - workspace-specific state;
13//!   persisted to `WorkspaceMementoPath` (reloaded on workspace change via
14//!   `UpdateWorkspaceMementoPathAndReload`). Used for workspace configs.
15//!
16//! ## Storage operations
17//!
18//! - `GetStorageValue(scope, key)` - reads from in-memory `HashMap`; returns
19//!   `None` for missing or empty keys; rejects keys > 1 024 chars.
20//! - `UpdateStorageValue(scope, key, value)` - inserts or removes key; rejects
21//!   values > 10 MB; spawns async `SaveStorageToDisk` after each mutation.
22//! - `GetAllStorage(scope)` - returns the full in-memory map as JSON.
23//! - `SetAllStorage(scope, state)` - overwrites the full map and persists.
24//!
25//! ## Async persistence
26//!
27//! All disk writes go through `SaveStorageToDisk`, which is spawned via
28//! `tokio::spawn` so the trait call returns immediately. The function creates
29//! parent directories as needed and logs errors without propagating them
30//! (fire-and-forget pattern). Writes are NOT yet atomic (temp+rename); that
31//! is a known TODO.
32//!
33//! ## VS Code reference
34//!
35//! - `vs/platform/storage/common/storageService.ts`
36//! - `vs/platform/storage/common/memento.ts`
37
38use std::{
39	collections::HashMap,
40	path::PathBuf,
41	sync::{
42		Arc,
43		Mutex,
44		OnceLock,
45		atomic::{AtomicBool, Ordering},
46	},
47};
48
49use CommonLibrary::{Error::CommonError::CommonError, Storage::StorageProvider::StorageProvider};
50use async_trait::async_trait;
51use serde_json::Value;
52use tokio::fs;
53
54use super::{MountainEnvironment::MountainEnvironment, Utility};
55use crate::dev_log;
56
57/// Write-coalescing debouncer for storage scope.
58/// Accumulates the latest data snapshot and schedules a single
59/// disk write 100 ms after the first queued mutation in a burst.
60struct StorageWriteDebouncer {
61	Pending:Mutex<Option<(PathBuf, HashMap<String, Value>)>>,
62
63	FlushScheduled:AtomicBool,
64}
65
66impl StorageWriteDebouncer {
67	fn new() -> Arc<Self> { Arc::new(Self { Pending:Mutex::new(None), FlushScheduled:AtomicBool::new(false) }) }
68
69	fn Queue(&self, Path:PathBuf, Data:HashMap<String, Value>, Debouncer:Arc<Self>) {
70		if let Ok(mut Guard) = self.Pending.lock() {
71			*Guard = Some((Path, Data));
72		}
73
74		if !self.FlushScheduled.swap(true, Ordering::AcqRel) {
75			tokio::spawn(async move {
76				tokio::time::sleep(std::time::Duration::from_millis(100)).await;
77
78				let Item = {
79					let mut Guard = Debouncer.Pending.lock().unwrap();
80
81					let Item = Guard.take();
82
83					// Clear the flag AFTER taking the data so a concurrent
84					// Queue() call can't sneak in between store(false) and
85					// take(), see the item as "already taken", and schedule
86					// a second flush that writes nothing.
87					Debouncer.FlushScheduled.store(false, Ordering::Release);
88
89					Item
90				};
91
92				if let Some((StoragePath, StorageData)) = Item {
93					SaveStorageToDisk(StoragePath, StorageData).await;
94				}
95			});
96		}
97	}
98}
99
100static GLOBAL_DEBOUNCER:OnceLock<Arc<StorageWriteDebouncer>> = OnceLock::new();
101
102static WORKSPACE_DEBOUNCER:OnceLock<Arc<StorageWriteDebouncer>> = OnceLock::new();
103
104fn GetGlobalDebouncer() -> Arc<StorageWriteDebouncer> {
105	GLOBAL_DEBOUNCER.get_or_init(StorageWriteDebouncer::new).clone()
106}
107
108fn GetWorkspaceDebouncer() -> Arc<StorageWriteDebouncer> {
109	WORKSPACE_DEBOUNCER.get_or_init(StorageWriteDebouncer::new).clone()
110}
111
112// TODO: storage quotas per extension, encryption for sensitive values,
113// compression for large datasets, migration/versioning, atomic writes
114// (temp+rename), storage change notifications/watchers, TTL / auto-expiry,
115// binary data support, transaction (batch + rollback), sync via Air.
116#[async_trait]
117impl StorageProvider for MountainEnvironment {
118	/// Retrieves a value from either global or workspace storage.
119	/// Includes defensive validation to prevent invalid keys and invalid JSON.
120	async fn GetStorageValue(&self, IsGlobalScope:bool, Key:&str) -> Result<Option<Value>, CommonError> {
121		let ScopeName = if IsGlobalScope { "Global" } else { "Workspace" };
122
123		dev_log!(
124			"storage",
125			"[StorageProvider] Getting value from {} scope for key: {}",
126			ScopeName,
127			Key
128		);
129
130		// Validate key to prevent injection or invalid storage paths
131		if Key.is_empty() {
132			return Ok(None);
133		}
134
135		if Key.len() > 1024 {
136			return Err(CommonError::InvalidArgument {
137				ArgumentName:"Key".into(),
138				Reason:"Key length exceeds maximum allowed length of 1024 characters".into(),
139			});
140		}
141
142		let StorageMapMutex = if IsGlobalScope {
143			&self.ApplicationState.Configuration.MementoGlobalStorage
144		} else {
145			&self.ApplicationState.Configuration.MementoWorkspaceStorage
146		};
147
148		let StorageMapGuard = StorageMapMutex
149			.lock()
150			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
151
152		Ok(StorageMapGuard.get(Key).cloned())
153	}
154
155	/// Updates or deletes a value in either global or workspace storage.
156	/// Includes comprehensive validation for key length, value size, and JSON
157	/// validity.
158	async fn UpdateStorageValue(
159		&self,
160
161		IsGlobalScope:bool,
162
163		Key:String,
164
165		ValueToSet:Option<Value>,
166	) -> Result<(), CommonError> {
167		let ScopeName = if IsGlobalScope { "Global" } else { "Workspace" };
168
169		// Per-key updates fire at every workbench state change (sidebar
170		// view state, panel layout, editor tab order, telemetry opt-ins).
171		// Short-form + long-form both emit under `storage-verbose` so the
172		// default log stays clean; `Trace=storage-verbose` restores
173		// the original verbose tracing.
174		if crate::IPC::DevLog::IsShort::Fn() {
175			crate::dev_log!("storage-verbose", "update {} {}", ScopeName, Key);
176		} else {
177			dev_log!(
178				"storage-verbose",
179				"[StorageProvider] Updating value in {} scope for key: {}",
180				ScopeName,
181				Key
182			);
183		}
184
185		// Validate key to prevent injection or invalid storage paths
186		if Key.is_empty() {
187			return Err(CommonError::InvalidArgument {
188				ArgumentName:"Key".into(),
189				Reason:"Key cannot be empty".into(),
190			});
191		}
192
193		if Key.len() > 1024 {
194			return Err(CommonError::InvalidArgument {
195				ArgumentName:"Key".into(),
196				Reason:"Key length exceeds maximum allowed length of 1024 characters".into(),
197			});
198		}
199
200		// If setting a value, validate it's not too large
201		if let Some(ref value) = ValueToSet {
202			if let Ok(json_string) = serde_json::to_string(value) {
203				if json_string.len() > 10 * 1024 * 1024 {
204					// 10MB limit per value
205					return Err(CommonError::InvalidArgument {
206						ArgumentName:"ValueToSet".into(),
207						Reason:"Value size exceeds maximum allowed size of 10MB".into(),
208					});
209				}
210			}
211		}
212
213		let (StorageMapMutex, StoragePathOption) = if IsGlobalScope {
214			(
215				self.ApplicationState.Configuration.MementoGlobalStorage.clone(),
216				Some(
217					self.ApplicationState
218						.GlobalMementoPath
219						.lock()
220						.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
221						.clone(),
222				),
223			)
224		} else {
225			(
226				self.ApplicationState.Configuration.MementoWorkspaceStorage.clone(),
227				self.ApplicationState
228					.WorkspaceMementoPath
229					.lock()
230					.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
231					.clone(),
232			)
233		};
234
235		// Perform the in-memory update.
236		let DataToSave = {
237			let mut StorageMapGuard = StorageMapMutex
238				.lock()
239				.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
240
241			if let Some(Value) = ValueToSet {
242				StorageMapGuard.insert(Key, Value);
243			} else {
244				StorageMapGuard.remove(&Key);
245			}
246
247			StorageMapGuard.clone()
248		};
249
250		if let Some(StoragePath) = StoragePathOption {
251			// Coalesce rapid writes: queue the latest snapshot and let the
252			// debouncer emit a single disk write 100 ms after the burst ends.
253			let Debouncer = if IsGlobalScope { GetGlobalDebouncer() } else { GetWorkspaceDebouncer() };
254
255			Debouncer.Queue(StoragePath, DataToSave, Debouncer.clone());
256		}
257
258		Ok(())
259	}
260
261	/// Retrieves the entire storage map for a given scope.
262	async fn GetAllStorage(&self, IsGlobalScope:bool) -> Result<Value, CommonError> {
263		let ScopeName = if IsGlobalScope { "Global" } else { "Workspace" };
264
265		dev_log!(
266			"storage-verbose",
267			"[StorageProvider] Getting all values from {} scope.",
268			ScopeName
269		);
270
271		let StorageMapMutex = if IsGlobalScope {
272			&self.ApplicationState.Configuration.MementoGlobalStorage
273		} else {
274			&self.ApplicationState.Configuration.MementoWorkspaceStorage
275		};
276
277		let StorageMapGuard = StorageMapMutex
278			.lock()
279			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
280
281		Ok(serde_json::to_value(&*StorageMapGuard)?)
282	}
283
284	/// Overwrites the entire storage map for a given scope and persists it.
285	async fn SetAllStorage(&self, IsGlobalScope:bool, FullState:Value) -> Result<(), CommonError> {
286		let ScopeName = if IsGlobalScope { "Global" } else { "Workspace" };
287
288		dev_log!(
289			"storage-verbose",
290			"[StorageProvider] Setting all values for {} scope.",
291			ScopeName
292		);
293
294		let DeserializedState:HashMap<String, Value> = serde_json::from_value(FullState)?;
295
296		let (StorageMapMutex, StoragePathOption) = if IsGlobalScope {
297			(
298				self.ApplicationState.Configuration.MementoGlobalStorage.clone(),
299				Some(
300					self.ApplicationState
301						.GlobalMementoPath
302						.lock()
303						.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
304						.clone(),
305				),
306			)
307		} else {
308			(
309				self.ApplicationState.Configuration.MementoWorkspaceStorage.clone(),
310				self.ApplicationState
311					.WorkspaceMementoPath
312					.lock()
313					.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
314					.clone(),
315			)
316		};
317
318		// Update in-memory state
319		*StorageMapMutex
320			.lock()
321			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)? = DeserializedState.clone();
322
323		// Persist to disk via the scope-correct debouncer so a workspace
324		// bulk-save and a global bulk-save never share the same Pending slot.
325		if let Some(StoragePath) = StoragePathOption {
326			let Debouncer = if IsGlobalScope { GetGlobalDebouncer() } else { GetWorkspaceDebouncer() };
327
328			Debouncer.Queue(StoragePath, DeserializedState, Debouncer.clone());
329		}
330
331		Ok(())
332	}
333}
334
335// --- Internal Helper Functions ---
336
337/// An internal helper function to asynchronously write the storage map to a
338/// file.
339async fn SaveStorageToDisk(Path:PathBuf, Data:HashMap<String, Value>) {
340	// Fires on every `storage:updateItems` that mutates the global map
341	// (~50 per session during workbench boot alone). The failure path
342	// below logs unconditionally; the success path is per-call noise.
343	dev_log!(
344		"storage-verbose",
345		"[StorageProvider] Persisting storage to disk: {}",
346		Path.display()
347	);
348
349	match serde_json::to_string_pretty(&Data) {
350		Ok(JSONString) => {
351			if let Some(ParentDirectory) = Path.parent() {
352				if let Err(Error) = fs::create_dir_all(ParentDirectory).await {
353					dev_log!(
354						"storage",
355						"error: [StorageProvider] Failed to create parent directory for '{}': {}",
356						Path.display(),
357						Error
358					);
359
360					return;
361				}
362			}
363
364			if let Err(Error) = fs::write(&Path, JSONString).await {
365				dev_log!(
366					"storage",
367					"error: [StorageProvider] Failed to write storage file to '{}': {}",
368					Path.display(),
369					Error
370				);
371			}
372		},
373
374		Err(Error) => {
375			dev_log!(
376				"storage",
377				"error: [StorageProvider] Failed to serialize storage data for '{}': {}",
378				Path.display(),
379				Error
380			);
381		},
382	}
383}