Skip to main content

DevelopmentNodeEnvironment_MicrosoftVSCodeDependency_22NodeVersion_Bundle_Clean_Debug_ElectronProfile_EsbuildCompiler_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					Debouncer.FlushScheduled.store(false, Ordering::Release);
82
83					Guard.take()
84				};
85
86				if let Some((StoragePath, StorageData)) = Item {
87					SaveStorageToDisk(StoragePath, StorageData).await;
88				}
89			});
90		}
91	}
92}
93
94static GLOBAL_DEBOUNCER:OnceLock<Arc<StorageWriteDebouncer>> = OnceLock::new();
95
96static WORKSPACE_DEBOUNCER:OnceLock<Arc<StorageWriteDebouncer>> = OnceLock::new();
97
98fn GetGlobalDebouncer() -> Arc<StorageWriteDebouncer> {
99	GLOBAL_DEBOUNCER.get_or_init(StorageWriteDebouncer::new).clone()
100}
101
102fn GetWorkspaceDebouncer() -> Arc<StorageWriteDebouncer> {
103	WORKSPACE_DEBOUNCER.get_or_init(StorageWriteDebouncer::new).clone()
104}
105
106// TODO: storage quotas per extension, encryption for sensitive values,
107// compression for large datasets, migration/versioning, atomic writes
108// (temp+rename), storage change notifications/watchers, TTL / auto-expiry,
109// binary data support, transaction (batch + rollback), sync via Air.
110#[async_trait]
111impl StorageProvider for MountainEnvironment {
112	/// Retrieves a value from either global or workspace storage.
113	/// Includes defensive validation to prevent invalid keys and invalid JSON.
114	async fn GetStorageValue(&self, IsGlobalScope:bool, Key:&str) -> Result<Option<Value>, CommonError> {
115		let ScopeName = if IsGlobalScope { "Global" } else { "Workspace" };
116
117		dev_log!(
118			"storage",
119			"[StorageProvider] Getting value from {} scope for key: {}",
120			ScopeName,
121			Key
122		);
123
124		// Validate key to prevent injection or invalid storage paths
125		if Key.is_empty() {
126			return Ok(None);
127		}
128
129		if Key.len() > 1024 {
130			return Err(CommonError::InvalidArgument {
131				ArgumentName:"Key".into(),
132				Reason:"Key length exceeds maximum allowed length of 1024 characters".into(),
133			});
134		}
135
136		let StorageMapMutex = if IsGlobalScope {
137			&self.ApplicationState.Configuration.MementoGlobalStorage
138		} else {
139			&self.ApplicationState.Configuration.MementoWorkspaceStorage
140		};
141
142		let StorageMapGuard = StorageMapMutex
143			.lock()
144			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
145
146		Ok(StorageMapGuard.get(Key).cloned())
147	}
148
149	/// Updates or deletes a value in either global or workspace storage.
150	/// Includes comprehensive validation for key length, value size, and JSON
151	/// validity.
152	async fn UpdateStorageValue(
153		&self,
154
155		IsGlobalScope:bool,
156
157		Key:String,
158
159		ValueToSet:Option<Value>,
160	) -> Result<(), CommonError> {
161		let ScopeName = if IsGlobalScope { "Global" } else { "Workspace" };
162
163		// Per-key updates fire at every workbench state change (sidebar
164		// view state, panel layout, editor tab order, telemetry opt-ins).
165		// Short-form + long-form both emit under `storage-verbose` so the
166		// default log stays clean; `Trace=storage-verbose` restores
167		// the original verbose tracing.
168		if crate::IPC::DevLog::IsShort::Fn() {
169			crate::dev_log!("storage-verbose", "update {} {}", ScopeName, Key);
170		} else {
171			dev_log!(
172				"storage-verbose",
173				"[StorageProvider] Updating value in {} scope for key: {}",
174				ScopeName,
175				Key
176			);
177		}
178
179		// Validate key to prevent injection or invalid storage paths
180		if Key.is_empty() {
181			return Err(CommonError::InvalidArgument {
182				ArgumentName:"Key".into(),
183				Reason:"Key cannot be empty".into(),
184			});
185		}
186
187		if Key.len() > 1024 {
188			return Err(CommonError::InvalidArgument {
189				ArgumentName:"Key".into(),
190				Reason:"Key length exceeds maximum allowed length of 1024 characters".into(),
191			});
192		}
193
194		// If setting a value, validate it's not too large
195		if let Some(ref value) = ValueToSet {
196			if let Ok(json_string) = serde_json::to_string(value) {
197				if json_string.len() > 10 * 1024 * 1024 {
198					// 10MB limit per value
199					return Err(CommonError::InvalidArgument {
200						ArgumentName:"ValueToSet".into(),
201						Reason:"Value size exceeds maximum allowed size of 10MB".into(),
202					});
203				}
204			}
205		}
206
207		let (StorageMapMutex, StoragePathOption) = if IsGlobalScope {
208			(
209				self.ApplicationState.Configuration.MementoGlobalStorage.clone(),
210				Some(
211					self.ApplicationState
212						.GlobalMementoPath
213						.lock()
214						.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
215						.clone(),
216				),
217			)
218		} else {
219			(
220				self.ApplicationState.Configuration.MementoWorkspaceStorage.clone(),
221				self.ApplicationState
222					.WorkspaceMementoPath
223					.lock()
224					.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
225					.clone(),
226			)
227		};
228
229		// Perform the in-memory update.
230		let DataToSave = {
231			let mut StorageMapGuard = StorageMapMutex
232				.lock()
233				.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
234
235			if let Some(Value) = ValueToSet {
236				StorageMapGuard.insert(Key, Value);
237			} else {
238				StorageMapGuard.remove(&Key);
239			}
240
241			StorageMapGuard.clone()
242		};
243
244		if let Some(StoragePath) = StoragePathOption {
245			// Coalesce rapid writes: queue the latest snapshot and let the
246			// debouncer emit a single disk write 100 ms after the burst ends.
247			let Debouncer = if IsGlobalScope { GetGlobalDebouncer() } else { GetWorkspaceDebouncer() };
248
249			Debouncer.Queue(StoragePath, DataToSave, Debouncer.clone());
250		}
251
252		Ok(())
253	}
254
255	/// Retrieves the entire storage map for a given scope.
256	async fn GetAllStorage(&self, IsGlobalScope:bool) -> Result<Value, CommonError> {
257		let ScopeName = if IsGlobalScope { "Global" } else { "Workspace" };
258
259		dev_log!(
260			"storage-verbose",
261			"[StorageProvider] Getting all values from {} scope.",
262			ScopeName
263		);
264
265		let StorageMapMutex = if IsGlobalScope {
266			&self.ApplicationState.Configuration.MementoGlobalStorage
267		} else {
268			&self.ApplicationState.Configuration.MementoWorkspaceStorage
269		};
270
271		let StorageMapGuard = StorageMapMutex
272			.lock()
273			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
274
275		Ok(serde_json::to_value(&*StorageMapGuard)?)
276	}
277
278	/// Overwrites the entire storage map for a given scope and persists it.
279	async fn SetAllStorage(&self, IsGlobalScope:bool, FullState:Value) -> Result<(), CommonError> {
280		let ScopeName = if IsGlobalScope { "Global" } else { "Workspace" };
281
282		dev_log!(
283			"storage-verbose",
284			"[StorageProvider] Setting all values for {} scope.",
285			ScopeName
286		);
287
288		let DeserializedState:HashMap<String, Value> = serde_json::from_value(FullState)?;
289
290		let (StorageMapMutex, StoragePathOption) = if IsGlobalScope {
291			(
292				self.ApplicationState.Configuration.MementoGlobalStorage.clone(),
293				Some(
294					self.ApplicationState
295						.GlobalMementoPath
296						.lock()
297						.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
298						.clone(),
299				),
300			)
301		} else {
302			(
303				self.ApplicationState.Configuration.MementoWorkspaceStorage.clone(),
304				self.ApplicationState
305					.WorkspaceMementoPath
306					.lock()
307					.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
308					.clone(),
309			)
310		};
311
312		// Update in-memory state
313		*StorageMapMutex
314			.lock()
315			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)? = DeserializedState.clone();
316
317		// Persist to disk via debouncer (coalesces rapid calls).
318		if let Some(StoragePath) = StoragePathOption {
319			let Debouncer = GetGlobalDebouncer();
320
321			Debouncer.Queue(StoragePath, DeserializedState, Debouncer.clone());
322		}
323
324		Ok(())
325	}
326}
327
328// --- Internal Helper Functions ---
329
330/// An internal helper function to asynchronously write the storage map to a
331/// file.
332async fn SaveStorageToDisk(Path:PathBuf, Data:HashMap<String, Value>) {
333	// Fires on every `storage:updateItems` that mutates the global map
334	// (~50 per session during workbench boot alone). The failure path
335	// below logs unconditionally; the success path is per-call noise.
336	dev_log!(
337		"storage-verbose",
338		"[StorageProvider] Persisting storage to disk: {}",
339		Path.display()
340	);
341
342	match serde_json::to_string_pretty(&Data) {
343		Ok(JSONString) => {
344			if let Some(ParentDirectory) = Path.parent() {
345				if let Err(Error) = fs::create_dir_all(ParentDirectory).await {
346					dev_log!(
347						"storage",
348						"error: [StorageProvider] Failed to create parent directory for '{}': {}",
349						Path.display(),
350						Error
351					);
352
353					return;
354				}
355			}
356
357			if let Err(Error) = fs::write(&Path, JSONString).await {
358				dev_log!(
359					"storage",
360					"error: [StorageProvider] Failed to write storage file to '{}': {}",
361					Path.display(),
362					Error
363				);
364			}
365		},
366
367		Err(Error) => {
368			dev_log!(
369				"storage",
370				"error: [StorageProvider] Failed to serialize storage data for '{}': {}",
371				Path.display(),
372				Error
373			);
374		},
375	}
376}