mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) move home server into core
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
This commit is contained in:
147
app/common/AsyncCreate.ts
Normal file
147
app/common/AsyncCreate.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* 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>;
|
||||
}
|
||||
Reference in New Issue
Block a user