mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
5ef889addd
Summary: This moves enough server material into core to run a home server. The data engine is not yet incorporated (though in manual testing it works when ported). Test Plan: existing tests pass Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2552
148 lines
4.6 KiB
TypeScript
148 lines
4.6 KiB
TypeScript
/**
|
|
* 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<MyObject>(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<T> {
|
|
private _value?: Promise<T> = undefined;
|
|
|
|
constructor(private _createFunc: () => Promise<T>) {}
|
|
|
|
/**
|
|
* Returns createFunc() result, returning the cached promise if createFunc() succeeded, or if
|
|
* another call to it is currently pending.
|
|
*/
|
|
public get(): Promise<T> {
|
|
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<T|undefined> {
|
|
return this._value ? this._value.catch(() => undefined) : undefined;
|
|
}
|
|
|
|
// Helper which clears this AsyncCreate if the given promise is rejected.
|
|
private _clearOnError(p: Promise<T>): Promise<T> {
|
|
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<K, V>(map: Map<K, Promise<V>>, key: K, creator: (key: K) => Promise<V>): Promise<V> {
|
|
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<K, V>(map: Map<K, Promise<V>>, key: K, pvalue: Promise<V>): Promise<V> {
|
|
pvalue.catch(() => map.delete(key));
|
|
map.set(key, pvalue);
|
|
return pvalue;
|
|
}
|
|
|
|
/**
|
|
* A Map implementation that allows for expiration of old values.
|
|
*/
|
|
export class MapWithTTL<K, V> extends Map<K, V> {
|
|
private _timeouts = new Map<K, NodeJS.Timer>();
|
|
|
|
/**
|
|
* 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<T>(promise: Promise<T>): Promise<ErrorOrValue<T>> {
|
|
try {
|
|
const value = await promise;
|
|
return { unfreeze: async () => value };
|
|
} catch (error) {
|
|
return { unfreeze: async () => { throw error; } };
|
|
}
|
|
}
|
|
|
|
export interface ErrorOrValue<T> {
|
|
unfreeze(): Promise<T>;
|
|
}
|