@clickup/ent-framework
Version:
A PostgreSQL graph-database-alike library with microsharding and row-level security
171 lines • 7.78 kB
JavaScript
"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