layered-loader
Version:
Data loader with support for caching and fallback data sources
184 lines • 8.19 kB
JavaScript
"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