UNPKG

@clickup/ent-framework

Version:

A PostgreSQL graph-database-alike library with microsharding and row-level security

171 lines 7.78 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.CachedRefreshedValue = void 0; const fast_typescript_memoize_1 = require("fast-typescript-memoize"); const p_defer_1 = __importDefault(require("p-defer")); const misc_1 = require("./misc"); /** * Utility class to provide a caching layer for a resolverFn with the following * assumptions: * - The value is stable and does not change frequently. * - The resolverFn can throw or take more time to resolve (e.g. outage). In * that case, the cached value is still valid, unless a fresh value is * requested with refreshAndWait(). * * The implementation is as follows: * - Once value is accessed, we schedule an endless loop of calling resolver to * get latest value. * - The result is cached, so next calls will return it immediately in most of * the cases. * - Once every delayMs we call resolverFn to get latest value. All calls during * this time will get previous value (if available). */ class CachedRefreshedValue { /** * Initializes the instance. */ constructor(options) { this.options = options; /** Latest value pulled from the cache. */ this.latestValue = null; /** Deferred promise containing the next value. Fulfilled promises are * replaced right away. */ this.nextValue = (0, p_defer_1.default)(); /** Each time before resolverFn() is called, this value is increased. */ this.resolverFnCallCount = 0; /** Whether the instance is destroyed or not. Used to prevent memory leaks in * unit tests. */ this.destroyedError = null; /** A callback to skip the current delay() call. */ this.skipDelay = null; } /** * Returns latest cached value. If the value has been calculated at least * once, then it is guaranteed that in the worst case, it will be returned * immediately. I.e. this method never blocks once at least one calculation * succeeded in the past. (But it may block during the very first call.) */ async cached() { (0, misc_1.runInVoid)(this.refreshLoop()); return this.latestValue ?? this.nextValue.promise; } /** * Triggers the call to resolverFn() ASAP (i.e. sooner than the next interval * specified in delayMs) and waits for the next complete SUCCESSFUL cache * refresh. If the method is called during the period of time when the * resolverFn() is already running, then it waits till it finishes, and then * waits again till the next resolverFn() call finishes, so we're sure that * it's a strong barrier. */ async refreshAndWait() { (0, misc_1.runInVoid)(this.refreshLoop()); // To unfreeze, we want a completely new resolverFn() call to finish within // refreshLoop(). I.e. the call to resolverFn() must start strictly AFTER we // entered refreshAndWait(); thus, `while` loop below may spin twice. const startCallCount = this.resolverFnCallCount; while (this.resolverFnCallCount <= startCallCount) { // Skip waiting between loops. this.skipDelay?.(); // After await resolves here, it's guaranteed that this.nextValue will be // reassigned with a new pDefer, and this.latestValue will be updated (see // refreshLoop() body). await this.nextValue.promise; } } /** * Destroys the instance. Stops refreshing the value and any call to it will * result in an error. */ destroy() { this.destroyedError = Error(`${this.constructor.name}: This instance is destroyed`); } async refreshLoop() { while (!this.destroyedError) { const warningDelayMs = (0, misc_1.maybeCall)(this.options.warningTimeoutMs); const depsDelayMs = (0, misc_1.maybeCall)(this.options.deps.delayMs); const startTime = performance.now(); const warningTimeout = setTimeout(() => this.onError(Error(`${this.constructor.name}.refreshLoop: Warning: ` + `${this.options.resolverName ?? "resolverFn"}() did not complete in ${warningDelayMs} ms!`), Math.round(performance.now() - startTime)), warningDelayMs).unref(); let depsPrev = undefined; try { this.resolverFnCallCount++; depsPrev = await this.options.deps.handler(); this.latestValue = await this.options.resolverFn(); const oldNextValue = this.nextValue; this.nextValue = (0, p_defer_1.default)(); oldNextValue.resolve(this.latestValue); } catch (e) { this.onError(e, Math.round(performance.now() - startTime)); } finally { clearTimeout(warningTimeout); } // Wait for delayMs. If this.skipDelay() is called, the code unfreezes // immediately. Also, deps are rechecked every depsDelayMs, and if they // change, the code unfreezes too. const delayDefer = (0, p_defer_1.default)(); this.skipDelay = () => delayDefer.resolve(); let depsTimeoutBody = () => (0, misc_1.runInVoid)(async () => { try { const depsCurr = await this.options.deps.handler(); if (depsCurr !== depsPrev) { delayDefer.resolve(); } } catch (e) { this.onError(e, Math.round(performance.now() - startTime)); } finally { if (depsTimeoutBody) { depsTimeout = setTimeout(depsTimeoutBody, depsDelayMs).unref(); } } }); let depsTimeout = depsPrev !== undefined ? setTimeout(depsTimeoutBody, depsDelayMs).unref() : undefined; try { this.options .delay((0, misc_1.maybeCall)(this.options.delayMs)) .finally(() => delayDefer.resolve()); await delayDefer.promise; } finally { depsTimeoutBody = null; clearTimeout(depsTimeout); } } // Mark current instance as destroyed. this.latestValue = null; (0, misc_1.runInVoid)(this.nextValue.promise.catch(() => { // Stops unhandled promise rejection errors. })); this.nextValue.reject(this.destroyedError); } /** * A never throwing version of options.onError(). */ onError(...args) { try { this.options.onError(...args); } catch { // noop } } } exports.CachedRefreshedValue = CachedRefreshedValue; __decorate([ (0, fast_typescript_memoize_1.Memoize)() ], CachedRefreshedValue.prototype, "refreshLoop", null); //# sourceMappingURL=CachedRefreshedValue.js.map