Asper Header  1.0.14
The header injector extension
Loading...
Searching...
No Matches
lazyFileLoad.ts
Go to the documentation of this file.
1
76import * as fsp from 'fs/promises';
77import * as path from 'path';
78import { logger } from './logger';
79import { getMessage } from './messageProvider';
80import { parseJsonFile } from '../utils/jsoncLoader';
81
99export class LazyFileLoader<T = any> {
101 private filePath: string | undefined = undefined;
102 private alternateFilePath: string | undefined = undefined;
104 private cache: T | undefined = undefined;
106 private cwd: string = "";
108 private timeoutCheckMs: number = 5000;
110 private timeoutReadMs: number = 10000;
111
120 constructor(filePath: string | undefined = undefined, cwd: string | undefined = undefined, alternateFilePath: string | undefined = undefined) {
121 logger.debug(getMessage("inFunction", "constructor", "LazyFileLoader"));
122 if (filePath) {
123 this.filePath = filePath;
124 logger.debug(getMessage("foundFilePathToLoad", filePath));
125 }
126 if (alternateFilePath) {
127 this.alternateFilePath = alternateFilePath;
128 logger.debug(getMessage("foundAlternateFilePathToLoad", alternateFilePath));
129 }
130 if (cwd) {
131 this.cwd = cwd;
132 logger.debug(getMessage("foundCurrentWorkingDirectory", cwd));
133 }
134 }
135
140 private async withTimeout<T>(promise: Promise<T>, timeoutMs: number, label?: string): Promise<T> {
141 return Promise.race([
142 promise,
143 new Promise<T>((_, reject) =>
144 setTimeout(() => reject(new Error(`${label}`)), timeoutMs)
145 ),
146 ]);
147 }
148
158 async pathExists(filePath: string, timeoutMs: number = 5000): Promise<boolean> {
159 logger.debug(getMessage("inFunction", "pathExists", "LazyFileLoader"));
160 try {
161 await this.withTimeout(fsp.access(filePath, fsp.constants.F_OK), timeoutMs, `pathExists:'${filePath}'>${timeoutMs}ms`);
162 logger.debug(getMessage("foundFilePath", filePath));
163 return true;
164 } catch (e) {
165 logger.debug(getMessage("notFoundFilePath", String(e)));
166 return false;
167 }
168 }
169
180 private async resolveAbsolutePath(filePath: string): Promise<string> {
181 logger.debug(getMessage("inFunction", "resolveAbsolutePath", "LazyFileLoader"));
182 if (path.isAbsolute(filePath)) {
183 return filePath;
184 }
185 return path.join(this.cwd, filePath);
186 }
187
202 async get(): Promise<T | undefined> {
203 logger.debug(getMessage("inFunction", "get", "LazyFileLoader"));
204 if (this.cache) {
205 logger.info(getMessage("cacheAlreadyLoaded"));
206 return this.cache ?? undefined;
207 }
208 const tryPaths: string[] = [this.filePath, this.alternateFilePath].filter(path => path !== undefined && path !== null) as string[];
209 if (tryPaths.length === 0) {
210 logger.warning(getMessage("noFilesAvailableForLoading"));
211 return this.cache ?? undefined;
212 }
213 for (const candidate of tryPaths) {
214 logger.debug(getMessage("filePathProcessing", candidate));
215 try {
216 const absolutePath = await this.resolveAbsolutePath(candidate);
217 logger.debug(getMessage("filepathPresenceCheck", absolutePath));
218 // Check existence first (optional, or just try/catch read)
219 const exists = await this.pathExists(absolutePath, this.timeoutCheckMs);
220 if (!exists) {
221 logger.warning(getMessage("notFoundFilePath", absolutePath));
222 continue; // try next path
223 }
224 const content = await this.withTimeout(fsp.readFile(absolutePath, "utf-8"), this.timeoutReadMs, getMessage("readTimeout", this.timeoutReadMs, absolutePath));
225 logger.debug(getMessage("fileLength", absolutePath, content.length));
226 const fileExtension: string = path.extname(candidate).toLowerCase();
227 try {
228 if (fileExtension === ".json") {
229 this.cache = JSON.parse(content) as T;
230 } else if (fileExtension === ".jsonc") {
231 this.cache = await parseJsonFile(content) as T;
232 } else {
233 this.cache = content as T;
234 }
235 logger.info(getMessage("fileLoaded", absolutePath));
236 return this.cache ?? undefined;
237 } catch (err) {
238 // JSON parsing failed, but we successfully loaded the file content
239 // Return the raw content since we got the file "at all costs"
240 const errorMsg: string = getMessage("fileParseError", absolutePath, String(err));
241 logger.error(errorMsg);
242 logger.Gui.error(errorMsg);
243 this.cache = content as T;
244 logger.info(getMessage("fileLoaded", absolutePath));
245 return this.cache ?? undefined;
246 }
247 } catch (e) {
248 const errMsg: string = getMessage("fileLoadError", candidate, String(e));
249 logger.Gui.error(errMsg);
250 logger.error(errMsg);
251 this.cache = undefined;
252 // Re-throw ENOENT and EISDIR errors as expected by tests
253 if ((e as any).code === 'ENOENT' || (e as any).code === 'EISDIR') {
254 throw e;
255 }
256 }
257 }
258 return this.cache ?? undefined;
259 }
260
269 async reload(): Promise<T | undefined> {
270 logger.debug(getMessage("inFunction", "reload", "LazyFileLoader"));
271 this.unload();
272 logger.info(getMessage("fileRefreshed"));
273 return await this.get();
274 }
275
283 unload() {
284 logger.debug(getMessage("inFunction", "unload", "LazyFileLoader"));
285 this.cache = undefined;
286 logger.info(getMessage("fileUnloaded", String(this.filePath)));
287 }
288
298 async updateFilePath(filePath: string, reload: boolean = false): Promise<boolean> {
299 logger.debug(getMessage("inFunction", "updateFilePath", "LazyFileLoader"));
300 const oldFilePath = this.filePath;
301
302 // If we have cached content and are changing to a different path, validate it exists
303 if (this.cache && oldFilePath && filePath !== oldFilePath) {
304 const absolutePath = await this.resolveAbsolutePath(filePath);
305 const exists = await this.pathExists(absolutePath, this.timeoutCheckMs);
306 if (!exists) {
307 const errorMessage = getMessage("fileNotFound", absolutePath);
308 const error = new Error(errorMessage);
309 (error as any).code = 'ENOENT';
310 throw error;
311 }
312 }
313
314 this.filePath = filePath;
315 // Clear cache when file path changes to ensure fresh content
316 this.cache = undefined;
317 if (reload) {
318 const status: T | undefined = await this.reload();
319 if (status === undefined) {
320 return false;
321 }
322 }
323 logger.info(getMessage("filePathUpdated", String(oldFilePath), String(filePath)));
324 return true;
325 }
326
336 async updateAlternateFilePath(filePath: string, reload: boolean = false): Promise<boolean> {
337 logger.debug(getMessage("inFunction", "updateAlternateFilePath", "LazyFileLoader"));
338 const oldFilePath = this.alternateFilePath;
339 this.alternateFilePath = filePath;
340 if (this.cache && reload) {
341 const status: T | undefined = await this.reload();
342 if (status === undefined) {
343 return false;
344 }
345 }
346 logger.info(getMessage("alternateFilePathUpdated", String(oldFilePath), String(this.alternateFilePath)));
347 return true;
348 }
349
350
360 async updateCurrentWorkingDirectory(cwd: string): Promise<boolean> {
361 logger.debug(getMessage("inFunction", "updateCurrentWorkingDirectory", "LazyFileLoader"));
362 const oldCwd: string = this.cwd;
363 try {
364 const stats = await fsp.stat(cwd);
365 if (!stats.isDirectory()) {
366 logger.warning(getMessage("cwdDoesNotExist", cwd));
367 return false;
368 }
369 this.cwd = cwd;
370 logger.info(getMessage("cwdUpdated", String(oldCwd), String(this.cwd)));
371 return true;
372 } catch {
373 logger.warning(getMessage("cwdDoesNotExist", cwd));
374 return false;
375 }
376 }
377
385 getFilePath(): string | undefined {
386 logger.debug(getMessage("inFunction", "getFilePath", "LazyFileLoader"));
387 return this.filePath;
388 }
389
397 getAlternateFilePath(): string | undefined {
398 logger.debug(getMessage("inFunction", "getAlternateFilePath", "LazyFileLoader"));
399 return this.alternateFilePath;
400 }
401
409 setCheckTimeout(timeoutCheckMs: number): void {
410 logger.debug(getMessage("inFunction", "setCheckTimeout", "LazyFileLoader"));
411 this.timeoutCheckMs = timeoutCheckMs;
412 }
413
421 setReadTimeout(timeoutReadMs: number): void {
422 logger.debug(getMessage("inFunction", "setReadTimeout", "LazyFileLoader"));
423 this.timeoutReadMs = timeoutReadMs;
424 }
425
435 setFilePathTimeout(timeoutCheckMs: number, timeoutReadMs: number): void {
436 this.setCheckTimeout(timeoutCheckMs);
437 this.setReadTimeout(timeoutReadMs);
438 }
439}
constructor(filePath:string|undefined=undefined, cwd:string|undefined=undefined, alternateFilePath:string|undefined=undefined)
Constructor for LazyFileLoader.
Generic lazy file loader with caching and type safety @template T The expected type of the loaded fil...
function async parseJsonFile(jsonContent:string)
Parses JSONC content with comprehensive error handling and validation.
import *as path from path
export const logger
Singleton logger instance providing unified logging interface for the entire extension.
Definition logger.ts:910
export const getMessage
Exported function for direct message retrieval.
export const Record< string,(...args:any[])=> string