UNPKG

layered-loader

Version:

Data loader with support for caching and fallback data sources

184 lines 8.19 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Loader = void 0; const AbstractFlatCache_1 = require("./AbstractFlatCache"); const GeneratedDataSource_1 = require("./GeneratedDataSource"); class Loader extends AbstractFlatCache_1.AbstractFlatCache { dataSources; isKeyRefreshing; throwIfLoadError; throwIfUnresolved; constructor(config) { super(config); // generated datasource if (config.dataSourceGetManyFn || config.dataSourceGetOneFn) { if (config.dataSources) { throw new Error('Cannot set both "dataSources" and "dataSourceGetManyFn"/"dataSourceGetOneFn" parameters.'); } this.dataSources = [ new GeneratedDataSource_1.GeneratedDataSource({ dataSourceGetOneFn: config.dataSourceGetOneFn, dataSourceGetManyFn: config.dataSourceGetManyFn, name: config.dataSourceName, }), ]; } // defined datasource else if (config.dataSources) { this.dataSources = config.dataSources; } // no datasource else { this.dataSources = []; } this.throwIfLoadError = config.throwIfLoadError ?? true; this.throwIfUnresolved = config.throwIfUnresolved ?? false; this.isKeyRefreshing = new Set(); } async forceSetValue(key, newValue) { this.inMemoryCache.set(key, newValue); /* v8 ignore next 3 */ if (this.runningLoads.has(key)) { this.runningLoads.delete(key); } if (this.asyncCache) { await this.asyncCache.set(key, newValue).catch((err) => { /* v8 ignore next 1 */ this.cacheUpdateErrorHandler(err, key, this.asyncCache, this.logger); }); } /* v8 ignore next 5 */ if (this.notificationPublisher) { this.notificationPublisher.set(key, newValue).catch((err) => { this.notificationPublisher.errorHandler(err, this.notificationPublisher.channel, this.logger); }); } } forceRefresh(loadParams) { const key = this.cacheKeyFromLoadParamsResolver(loadParams); return this.loadFromLoaders(key, loadParams).then((finalValue) => { if (finalValue !== undefined) { this.inMemoryCache.set(key, finalValue); /* v8 ignore next 3 */ if (this.runningLoads.has(key)) { this.runningLoads.delete(key); } } // In order to keep other cluster nodes in-sync with potentially changed entry, we force them to refresh too /* v8 ignore next 5 */ if (this.notificationPublisher) { this.notificationPublisher.delete(key).catch((err) => { this.notificationPublisher.errorHandler(err, this.notificationPublisher.channel, this.logger); }); } return finalValue; }); } resolveValue(key, loadParams) { return super.resolveValue(key, loadParams).then((cachedValue) => { // value resolved from cache if (cachedValue !== undefined) { if (this.asyncCache?.ttlLeftBeforeRefreshInMsecs) { if (!this.isKeyRefreshing.has(key)) { this.asyncCache.expirationTimeLoadingOperation.get(key).then((expirationTime) => { if (expirationTime && expirationTime - Date.now() < this.asyncCache.ttlLeftBeforeRefreshInMsecs) { // check second time, maybe someone obtained the lock while we were checking the expiration date if (!this.isKeyRefreshing.has(key)) { this.isKeyRefreshing.add(key); this.loadFromLoaders(key, loadParams) .catch((err) => { this.logger.error(err.message); }) .finally(() => { this.isKeyRefreshing.delete(key); }); } } }); } } return cachedValue; } // No cached value, we have to load instead return this.loadFromLoaders(key, loadParams).then((finalValue) => { if (finalValue !== undefined) { return finalValue; } if (this.throwIfUnresolved) { throw new Error(`Failed to resolve value for key "${key}"`); } return undefined; }); }); } async loadFromLoaders(key, loadParams) { for (let index = 0; index < this.dataSources.length; index++) { const resolvedValue = await this.dataSources[index].get(loadParams).catch((err) => { this.loadErrorHandler(err, key, this.dataSources[index], this.logger); if (this.throwIfLoadError) { throw err; } }); if (resolvedValue !== undefined || index === this.dataSources.length - 1) { if (resolvedValue === undefined && this.throwIfUnresolved) { throw new Error(`Failed to resolve value for key "${key}"`); } const finalValue = resolvedValue ?? null; if (this.asyncCache) { await this.asyncCache.set(key, finalValue).catch((err) => { this.cacheUpdateErrorHandler(err, key, this.asyncCache, this.logger); }); } return finalValue; } } return undefined; } async resolveManyValues(keys, loadParams) { // load what is available from async cache const cachedValues = await super.resolveManyValues(keys, loadParams); // everything was cached, no need to load anything if (cachedValues.unresolvedKeys.length === 0) { return cachedValues; } const loadValues = await this.loadManyFromLoaders(cachedValues.unresolvedKeys, loadParams); if (this.asyncCache) { const cacheEntries = loadValues.map((loadValue) => { return { key: this.cacheKeyFromValueResolver(loadValue), value: loadValue, }; }); await this.asyncCache.setMany(cacheEntries).catch((err) => { this.cacheUpdateErrorHandler(err, cacheEntries.map((entry) => entry.key).join(', '), this.asyncCache, this.logger); }); } return { resolvedValues: [...cachedValues.resolvedValues, ...loadValues], // there actually may still be some unresolved keys, but we no longer know that unresolvedKeys: [], }; } async loadManyFromLoaders(keys, loadParams) { let lastResolvedValues; for (let index = 0; index < this.dataSources.length; index++) { lastResolvedValues = await this.dataSources[index].getMany(keys, loadParams).catch((err) => { this.loadErrorHandler(err, keys.toString(), this.dataSources[index], this.logger); if (this.throwIfLoadError) { throw err; } return []; }); if (lastResolvedValues.length === keys.length) { return lastResolvedValues; } } if (this.throwIfUnresolved) { throw new Error(`Failed to resolve value for some of the keys: ${keys.join(', ')}`); } // ToDo do we want to return results of a query that returned the most amount of entities? return lastResolvedValues ?? []; } } exports.Loader = Loader; //# sourceMappingURL=Loader.js.map