/** * Implements a pattern for creating objects requiring asynchronous construction. The given * asynchronous createFunc() is called on the .get() call, and the result is cached on success. * On failure, the result is cleared, so that subsequent calls attempt the creation again. * * Usage: * this._obj = AsyncCreate(asyncCreateFunc); * obj = await this._obj.get(); // calls asyncCreateFunc * obj = await this._obj.get(); // uses cached object if asyncCreateFunc succeeded, else calls it again. * * Note that multiple calls while createFunc() is running will return the same promise, and will * succeed or fail together. */ export class AsyncCreate { private _value?: Promise = undefined; constructor(private _createFunc: () => Promise) {} /** * Returns createFunc() result, returning the cached promise if createFunc() succeeded, or if * another call to it is currently pending. */ public get(): Promise { return this._value || (this._value = this._clearOnError(this._createFunc.call(null))); } /** Clears the cached promise, forcing createFunc to be called again on next get(). */ public clear(): void { this._value = undefined; } /** Returns a boolean indicating whether the object is created. */ public isSet(): boolean { return Boolean(this._value); } /** Returns the value if it's set and successful, or undefined otherwise. */ public async getIfValid(): Promise { return this._value ? this._value.catch(() => undefined) : undefined; } // Helper which clears this AsyncCreate if the given promise is rejected. private _clearOnError(p: Promise): Promise { p.catch(() => this.clear()); return p; } } /** * Supports a usage similar to AsyncCreate in a Map. Returns map.get(key) if it is set to a * resolved or pending promise. Otherwise, calls creator(key) to create and return a new promise, * and sets the key to it. If the new promise is rejected, the key will be removed from the map, * so that subsequent calls would call creator() again. * * As with AsyncCreate, while the promise for a key is pending, multiple calls to that key will * return the same promise, and will succeed or fail together. */ export function mapGetOrSet(map: Map>, key: K, creator: (key: K) => Promise): Promise { return map.get(key) || mapSetOrClear(map, key, creator(key)); } /** * Supports a usage similar to AsyncCreate in a Map. Sets the given key in a map to the given * promise, and removes it later if the promise is rejected. Returns the same promise. */ export function mapSetOrClear(map: Map>, key: K, pvalue: Promise): Promise { pvalue.catch(() => map.delete(key)); map.set(key, pvalue); return pvalue; } /** * A Map implementation that allows for expiration of old values. */ export class MapWithTTL extends Map { private _timeouts = new Map(); /** * Create a map with keys that will be automatically deleted _ttlMs * milliseconds after they have been last set. Precision of timing * may vary. */ constructor(private _ttlMs: number) { super(); } /** * Set a key, with expiration. */ public set(key: K, value: V): this { return this.setWithCustomTTL(key, value, this._ttlMs); } /** * Set a key, with custom expiration. */ public setWithCustomTTL(key: K, value: V, ttlMs: number): this { const curr = this._timeouts.get(key); if (curr) { clearTimeout(curr); } super.set(key, value); this._timeouts.set(key, setTimeout(this.delete.bind(this, key), ttlMs)); return this; } /** * Remove a key. */ public delete(key: K): boolean { const result = super.delete(key); const timeout = this._timeouts.get(key); if (timeout) { clearTimeout(timeout); this._timeouts.delete(key); } return result; } /** * Forcibly expire everything. */ public clear(): void { for (const timeout of this._timeouts.values()) { clearTimeout(timeout); } this._timeouts.clear(); super.clear(); } } /** * Sometimes it is desirable to cache either fulfilled or rejected * outcomes. This method wraps a promise so that it never throws. * The result has an unfreeze method which, when called, is either * fulfilled or rejected. */ export async function freezeError(promise: Promise): Promise> { try { const value = await promise; return { unfreeze: async () => value }; } catch (error) { return { unfreeze: async () => { throw error; } }; } } export interface ErrorOrValue { unfreeze(): Promise; }