Skip to content

Instantly share code, notes, and snippets.

@moritztim
Last active January 23, 2024 10:18
Show Gist options
  • Save moritztim/de1a520513d7a4f01bbe5c4dc768a8c5 to your computer and use it in GitHub Desktop.
Save moritztim/de1a520513d7a4f01bbe5c4dc768a8c5 to your computer and use it in GitHub Desktop.
Deno TypeScript Cache Manager
import { ReadWriteFileCacheManager } from "./FileCacheManager.ts";
/** Cache manager for raw binary files
*
* @property {string} filePath Path to the file whose contents are to be cached
* @property {Promise<Uint8Array>} dataPromise Asynchronous getter for data
* @property {Uint8Array} data Synchronous getter for data
* @property {Promise<void>} set Asynchronous setter for data
* @property {Uint8Array} data Synchronous setter for data
*/
export class BinaryFileCacheManager extends ReadWriteFileCacheManager<Uint8Array> {
protected get fileContentsPromise() {
return Deno.readFile(this.filePath);
}
protected get fileContents() {
return Deno.readFileSync(this.filePath);
}
protected set fileContents(data: Uint8Array) {
Deno.writeFileSync(this.filePath, data);
}
protected writeFileContents(data: Uint8Array) {
return Deno.writeFile(this.filePath, data);
}
}
/**
* Abstract class for handling caching of generic contents.
*
* Provides methods to retrieve cached data, as well as to invalidate the cache.
*
* @typeParam T Type of the data to be cached
*/
export default abstract class CacheManager<T> {
/** Cached data */
protected _cache?: T;
/** When the data was last fetched from the file */
protected lastFetch?: Date;
public get valid(): boolean {
return !(this.lastFetch == undefined || this._cache == undefined);
}
/**
* Retrieves the data from cache or fetches it if necessary.
* @async
* @returns {Promise<T>} Promise for data from cache or source
*/
public abstract get dataPromise(): Promise<T>;
/**
* Retrieves the data from cache or fetches it synchronously if necessary.
* @returns {T} Data from cache or source
*/
public abstract get data(): T;
/**
* Invalidates the cache, causing data to be refreshed on next access.
*/
public invalidate(): void {
this._cache = undefined;
this.lastFetch = undefined;
}
/**
* Caches the provided data and updates the last fetch timestamp.
* @param data The data to be cached.
*/
protected cache(data: T): void {
this._cache = data;
this.lastFetch = new Date();
}
}
import CacheManager from "./CacheManager.ts";
import ReadWriteCacheManager from "./ReadWriteCacheManager.ts";
/** Generic class for handling caching of file contents.
*
* Provides methods to retrieve cached data, as well as to invalidate the cache.
*
* @typeParam T Type of the data to be cached
* @property {string} filePath Path to the file whose contents are to be cached
* @property {Promise<T>} dataPromise Asynchronous getter for data
* @property {T} data Synchronous getter for data
*/
export abstract class FileCacheManager<T> extends CacheManager<T> {
constructor(protected filePath: string) { super() }
/** Promise for data from cache or, if file is newer, asynchronously retrieved from file
* @async
*/
public get dataPromise(): Promise<T> {
this.#cachedFileStatsPromise = undefined;
// if not cached, cache
if (!super.valid) {
return this.fetchAndCache();
}
// if cached, check if file modified
return this.fileStatsPromise.then((stats) => {
if (stats.mtime! > this.lastFetch!)
return this.fetchAndCache();
else
return this._cache ?? this.fetchAndCache(); // cache may have been invalidated
});
}
/** Data from cache or, if file is newer, from file */
public get data(): T {
this.#cachedFileStats = undefined;
if (!super.valid || this.fileStats.mtime! > this.lastFetch!) // if valid, lastFetch is defined
this._cache = this.fileContents;
return this._cache!; // if valid, _cache is defined, otherwise _cache just got defined
}
/** Promise of asynchronously retrieved file contents
* @async
* @returns {Promise<T>}
*/
protected abstract get fileContentsPromise(): Promise<T>;
/** @returns {T} */
protected abstract get fileContents(): T;
/** Promise for file stats
* Stored in a sort of singleton, to avoid multiple calls to Deno.stat
*/
#cachedFileStatsPromise?: ReturnType<typeof Deno.stat>;
private get fileStatsPromise() {
return this.#getCachedSimple<ReturnType<typeof Deno.stat>>(this.#cachedFileStatsPromise, Deno.stat.bind(Deno, this.filePath));
}
/** File stats, synchronous version of {@link fileStatsPromise} */
#cachedFileStats?: ReturnType<typeof Deno.statSync>;
private get fileStats(): ReturnType<typeof Deno.statSync> {
return this.#getCachedSimple<ReturnType<typeof Deno.statSync>>(this.#cachedFileStats, Deno.statSync.bind(Deno, this.filePath));
}
#getCachedSimple<T>(cached: T | undefined, get: () => T): T {
if (!cached) cached = get();
return cached;
}
private async fetchAndCache(): Promise<T> {
return await this.fileContentsPromise.then((data) => {
this.cache(data);
return data;
});
}
}
/**
* Generic class for handling caching of file contents.
*
* Provides methods to set and retrieve cached data, as well as to invalidate the cache.
*
* @typeParam T Type of the data to be cached
* @property {string} filePath Path to the file whose contents are to be cached
* @property {Promise<T>} dataPromise Asynchronous getter for data
* @property {T} data Synchronous getter for data
* @property {Promise<void>} set Asynchronous setter for data
* @property {T} data Synchronous setter for data
*/
export abstract class ReadWriteFileCacheManager<T> extends FileCacheManager<T> implements ReadWriteCacheManager<T> {
public set data(data: T) {
this.cache(data);
this.writeFileContents(data);
}
/** Asynchronously set the data in the cache and write it to file */
public async set(data: T): Promise<void> {
this.cache(data);
await this.writeFileContents(data);
}
/** Asynchronously write data to file
* @async
* @param {T} data
* @returns {Promise<void>}
*/
protected abstract writeFileContents(data: T): Promise<void>;
/** Synchronously sets data in file
* @param {T} data
*/
protected abstract set fileContents(data: T);
}
/** Cache manager for text files
*
* @property {string} filePath Path to the file whose contents are to be cached
* @property {Promise<string>} dataPromise Asynchronous getter for data
* @property {string} data Synchronous getter for data
* @property {Promise<void>} set Asynchronous setter for data
* @property {string} data Synchronous setter for data
*/
export class TextCacheManager extends ReadWriteFileCacheManager<string> {
protected get fileContentsPromise() {
return Deno.readTextFile(this.filePath);
}
protected get fileContents() {
return Deno.readTextFileSync(this.filePath);
}
protected set fileContents(data: string) {
Deno.writeTextFileSync(this.filePath, data);
}
protected writeFileContents(data: string) {
return Deno.writeTextFile(this.filePath, data);
}
}
/** Cache manager for JSON files
*
* @typeParam T Type of the data to be cached
* @property {string} filePath Path to the file whose contents are to be cached
* @property {Promise<T>} dataPromise Asynchronous getter for data
* @property {T} data Synchronous getter for data
* @property {Promise<void>} set Asynchronous setter for data
* @property {T} data Synchronous setter for data
*/
export class JSONCacheManager<T extends object> extends ReadWriteFileCacheManager<T> {
protected get fileContentsPromise() {
return Deno.readTextFile(this.filePath).then((contents) => JSON.parse(contents));
}
protected get fileContents() {
return JSON.parse(Deno.readTextFileSync(this.filePath));
}
protected set fileContents(data: T) {
Deno.writeTextFileSync(this.filePath, JSON.stringify(data));
}
protected writeFileContents(data: T) {
return Deno.writeTextFile(this.filePath, JSON.stringify(data));
}
}
/** Cache manager for raw binary files
*
* @property {string} filePath Path to the file whose contents are to be cached
* @property {Promise<Uint8Array>} dataPromise Asynchronous getter for data
* @property {Uint8Array} data Synchronous getter for data
* @property {Promise<void>} set Asynchronous setter for data
* @property {Uint8Array} data Synchronous setter for data
*/
export class BinaryCacheManager extends ReadWriteFileCacheManager<Uint8Array> {
protected get fileContentsPromise() {
return Deno.readFile(this.filePath);
}
protected get fileContents() {
return Deno.readFileSync(this.filePath);
}
protected set fileContents(data: Uint8Array) {
Deno.writeFileSync(this.filePath, data);
}
protected writeFileContents(data: Uint8Array) {
return Deno.writeFile(this.filePath, data);
}
}
import { ReadWriteFileCacheManager } from "./FileCacheManager.ts";
/** Cache manager for JSON files
*
* @typeParam T Type of the data to be cached
* @property {string} filePath Path to the file whose contents are to be cached
* @property {Promise<T>} dataPromise Asynchronous getter for data
* @property {T} data Synchronous getter for data
* @property {Promise<void>} set Asynchronous setter for data
* @property {T} data Synchronous setter for data
*/
export class JSONFileCacheManager<T extends object> extends ReadWriteFileCacheManager<T> {
protected get fileContentsPromise() {
return Deno.readTextFile(this.filePath).then((contents) => JSON.parse(contents));
}
protected get fileContents() {
return JSON.parse(Deno.readTextFileSync(this.filePath));
}
protected set fileContents(data: T) {
Deno.writeTextFileSync(this.filePath, JSON.stringify(data));
}
protected writeFileContents(data: T) {
return Deno.writeTextFile(this.filePath, JSON.stringify(data));
}
}
import CacheManager from './CacheManager.ts'
export default interface ReadWriteCacheManager<T> extends CacheManager<T> {
set data(data: T)
/** Asynchronously set the data in the cache and write it to file
* @async
*/
set(data: T): Promise<void>
}
import { ReadWriteFileCacheManager } from "./FileCacheManager.ts";
/** Cache manager for text files
*
* @property {string} filePath Path to the file whose contents are to be cached
* @property {Promise<string>} dataPromise Asynchronous getter for data
* @property {string} data Synchronous getter for data
* @property {Promise<void>} set Asynchronous setter for data
* @property {string} data Synchronous setter for data
*/
export class TextFileCacheManager extends ReadWriteFileCacheManager<string> {
protected get fileContentsPromise() {
return Deno.readTextFile(this.filePath);
}
protected get fileContents() {
return Deno.readTextFileSync(this.filePath);
}
protected set fileContents(data: string) {
Deno.writeTextFileSync(this.filePath, data);
}
protected writeFileContents(data: string) {
return Deno.writeTextFile(this.filePath, data);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment