packages/web-ide-fs/src/browserfs/ParsedFileCache.ts (42 lines of code) (raw):
import { FileFlag } from 'browserfs/dist/node/core/file_flag';
import type { ReadonlyPromisifiedBrowserFS } from './types';
interface Parser<T> {
(content: string): Promise<T>;
}
export class ParsedFileCache<T> {
readonly #fs: ReadonlyPromisifiedBrowserFS;
readonly #filePath: string;
readonly #parser: Parser<T>;
// Cache is either a tuple of (cacheKey, cacheValue) or it is empty
#cache: [string, T] | undefined;
constructor(fs: ReadonlyPromisifiedBrowserFS, filePath: string, parser: Parser<T>) {
this.#fs = fs;
this.#filePath = filePath;
this.#parser = parser;
}
public async getContents(): Promise<T | null> {
const cacheKey = await this.#getCacheKey();
if (!cacheKey) {
return null;
}
if (this.#cache && cacheKey === this.#cache[0]) {
return this.#cache[1];
}
const value = await this.#parseFileContents();
this.#cache = [cacheKey, value];
return value;
}
async #getCacheKey(): Promise<string | null> {
try {
const stat = await this.#fs.stat(this.#filePath, null);
// why: Using just mtime.getTime() can be flaky in some edge cases.
// In unit tests, sometimes the time between 2 write operations
// can be less than a ms which wouldn't trigger a cache invalidation.
return `${stat.size}_${stat.mtime.getTime()}`;
} catch {
// The path was not found! Let's just assume empty.
return null;
}
}
async #parseFileContents(): Promise<T> {
// We are guaranteed this is a "string" because we passed the encoding
const content = <string>(
await this.#fs.readFile(this.#filePath, 'utf-8', FileFlag.getFileFlag('r'))
);
return this.#parser(content);
}
}