DevelopmentNodeEnvironment_MicrosoftVSCodeDependency_22NodeVersion_Bundle_Clean_Debug_ElectronProfile_EsbuildCompiler_Mountain/ApplicationState/Internal/ExtensionScanner/
LoadFromCache.rs1use std::{collections::HashMap, path::PathBuf, time::Duration};
34
35use CommonLibrary::Error::CommonError::CommonError;
36use serde::Deserialize;
37use serde_json::Value;
38
39use crate::{ApplicationState::DTO::ExtensionDescriptionStateDTO::ExtensionDescriptionStateDTO, dev_log};
40
41#[derive(Debug, Deserialize)]
43struct CachedEntry {
44 id:String,
45
46 path:String,
47
48 manifest:Value,
49}
50
51#[derive(Debug, Deserialize)]
53struct CacheBlob {
54 version:u32,
55
56 count:u32,
57
58 extensions:Vec<CachedEntry>,
59}
60
61const MAX_CACHE_AGE:Duration = Duration::from_secs(86_400);
65
66pub async fn Fn(BinaryDir:&PathBuf) -> Result<Option<HashMap<String, ExtensionDescriptionStateDTO>>, CommonError> {
79 let DevCachePath = BinaryDir.join("extensions.manifest.json");
81
82 let BundleCachePath = BinaryDir.join("../Resources/extensions.manifest.json");
84
85 let (CachePath, IsBundled) = if tokio::fs::metadata(&DevCachePath).await.is_ok() {
87 (DevCachePath, false)
88 } else if tokio::fs::metadata(&BundleCachePath).await.is_ok() {
89 (BundleCachePath, true)
90 } else {
91 dev_log!("extensions", "[ExtensionCache] Cache not found at {}", DevCachePath.display());
92
93 return Ok(None);
94 };
95
96 let Age = if IsBundled {
98 Duration::ZERO
99 } else {
100 let Metadata = tokio::fs::metadata(&CachePath)
101 .await
102 .map_err(|_| CommonError::Unknown { Description:"cache stat failed".into() })?;
103
104 Metadata.modified().ok().and_then(|T| T.elapsed().ok()).unwrap_or(Duration::MAX)
105 };
106
107 if !IsBundled && Age > MAX_CACHE_AGE {
108 dev_log!(
109 "extensions",
110 "[ExtensionCache] Cache is stale ({:.0}s > {:.0}s), falling back to live scan",
111 Age.as_secs_f32(),
112 MAX_CACHE_AGE.as_secs_f32()
113 );
114
115 return Ok(None);
116 }
117
118 let Bytes = match tokio::fs::read(&CachePath).await {
120 Ok(B) => B,
121
122 Err(E) => {
123 dev_log!(
124 "extensions",
125 "warn: [ExtensionCache] Read failed: {}; falling back to live scan",
126 E
127 );
128
129 return Ok(None);
130 },
131 };
132
133 let Blob:CacheBlob = match serde_json::from_slice(&Bytes) {
134 Ok(B) => B,
135
136 Err(E) => {
137 dev_log!(
138 "extensions",
139 "warn: [ExtensionCache] Parse error: {}; falling back to live scan",
140 E
141 );
142
143 return Ok(None);
144 },
145 };
146
147 if Blob.version != 1 {
148 dev_log!(
149 "extensions",
150 "[ExtensionCache] Unsupported cache version {}; falling back to live scan",
151 Blob.version
152 );
153
154 return Ok(None);
155 }
156
157 if Blob.extensions.is_empty() {
161 dev_log!(
162 "extensions",
163 "[ExtensionCache] Empty cache (count=0), falling back to live scan"
164 );
165
166 return Ok(None);
167 }
168
169 let mut Map:HashMap<String, ExtensionDescriptionStateDTO> = HashMap::with_capacity(Blob.extensions.len());
171
172 for Entry in Blob.extensions {
173 let Manifest = &Entry.manifest;
174
175 let Path = &Entry.path;
176
177 let str = |k:&str| Manifest.get(k).and_then(Value::as_str).map(str::to_string);
179
180 let str_or = |k:&str, d:&str| Manifest.get(k).and_then(Value::as_str).unwrap_or(d).to_string();
181
182 let arr =
183 |k:&str| -> Option<Vec<String>> { Manifest.get(k).and_then(|V| serde_json::from_value(V.clone()).ok()) };
184
185 let ExtId = Entry.id.clone();
186
187 let Publisher = Manifest
188 .get("publisher")
189 .and_then(Value::as_str)
190 .unwrap_or_else(|| Entry.id.split('.').next().unwrap_or("unknown"))
191 .to_string();
192
193 let IsBuiltin = PathBuf::from(Path)
195 .parent()
196 .and_then(|P| P.file_name())
197 .and_then(|N| N.to_str())
198 .map(|N| N == "extensions")
199 .unwrap_or(false);
200
201 let Dto = ExtensionDescriptionStateDTO {
202 Identifier:serde_json::json!({ "value": ExtId }),
203
204 Name:str_or("name", ""),
205
206 Version:str_or("version", "0.0.0"),
207
208 Publisher,
209
210 Engines:Manifest.get("engines").cloned().unwrap_or(serde_json::json!({})),
211
212 Main:str("main"),
213
214 Browser:str("browser"),
215
216 ModuleType:str("type"),
217
218 IsBuiltin,
219
220 IsUnderDevelopment:false,
221
222 ExtensionLocation:Value::String(format!("file://{}", Path)),
225
226 ActivationEvents:arr("activationEvents"),
227
228 Contributes:Manifest.get("contributes").cloned(),
229
230 Categories:arr("categories"),
231
232 DisplayName:str("displayName"),
233
234 Description:str("description"),
235
236 Keywords:arr("keywords"),
237
238 Repository:Manifest.get("repository").cloned(),
239
240 Bugs:Manifest.get("bugs").cloned(),
241
242 Homepage:str("homepage"),
243
244 License:str("license"),
245
246 Icon:str("icon"),
247
248 AiKey:str("aiKey"),
249
250 ExtensionKind:Manifest.get("extensionKind").cloned(),
251
252 Capabilities:Manifest.get("capabilities").cloned(),
253
254 ExtensionDependencies:arr("extensionDependencies"),
255
256 ExtensionPack:arr("extensionPack"),
257 };
258
259 Map.insert(ExtId, Dto);
260 }
261
262 dev_log!(
263 "extensions",
264 "[ExtensionCache] Loaded {} extensions from {} cache ({} bytes{})",
265 Map.len(),
266 if IsBundled { "bundled" } else { "dev" },
267 Bytes.len(),
268 if IsBundled {
269 String::new()
270 } else {
271 format!(", {:.0}s old", Age.as_secs_f32())
272 }
273 );
274
275 Ok(Some(Map))
276}