@linkedmink/multilevel-aging-cache
Version:
Package provides an interface to cache and persist data to Redis, MongoDB, memory
243 lines • 10.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.StorageHierarchy = void 0;
const IAgedQueue_1 = require("../queue/IAgedQueue");
const Logger_1 = require("../shared/Logger");
const IStorageHierarchy_1 = require("./IStorageHierarchy");
const ISubscribableStorageProvider_1 = require("./ISubscribableStorageProvider");
/**
* The default storage hierarchy implementation relying on IStorageProvider for actual data access
*/
class StorageHierarchy {
/**
* @param levels The levels in the hierarchy with index 0 being the lowest level (first to read)
* @param updatePolicy How updates from subscribed higher level storage providers should be handled
*/
constructor(levels, updatePolicy = IStorageHierarchy_1.StorageHierarchyUpdatePolicy.OnlyIfKeyExist, ageCompareFunc = IAgedQueue_1.compareAscending) {
this.levels = levels;
this.updatePolicy = updatePolicy;
this.ageCompareFunc = ageCompareFunc;
this.totalLevels = this.levels.length;
this.isPersistable = this.levels[this.totalLevels - 1].isPersistable;
this.logger = Logger_1.Logger.get(StorageHierarchy.name);
this.storageChangedHandlers = new Map();
this.pendingUpdates = new Set();
this.topLevel = this.levels[this.totalLevels - 1];
if (levels.length < 2) {
throw new Error('StorageHierarchy must have at least 2 storage provider');
}
this.logger.info(`Created storage hierarchy with levels: ${this.totalLevels}`);
this.publishLevel = this.subscribeAtLevel(this.totalLevels - 1);
}
/**
* Clean up the object when it's no longer used. After a dispose(), an object
* is no longer guaranteed to be usable.
*/
dispose() {
this.storageChangedHandlers.forEach((handler, level) => {
const currentLevel = this.levels[level];
if ((0, ISubscribableStorageProvider_1.isISubscribableStorageProvider)(currentLevel)) {
currentLevel.unsubscribe(handler);
}
this.storageChangedHandlers.delete(level);
});
return Promise.all(this.pendingUpdates).then(() => undefined);
}
/**
* @param key The key to retrieve
* @param level The level at which to retrieve the key
* @param isAscending To go up the hierarchy (true) or down (false) from level
* @returns The value if it's in the hierarchy from the level going up/down or null
*/
getAtLevel(key, level, isAscending = true) {
const rLevel = this.getCurrentLevelOrNull(isAscending, level);
if (rLevel === null) {
return Promise.resolve(null);
}
return this.levels[rLevel]
.get(key)
.then(agedValue => {
if (agedValue) {
return agedValue;
}
else {
this.logger.debug(`Cache miss: level=${rLevel}, key=${key}`);
return this.getAtLevel(key, isAscending ? rLevel + 1 : rLevel - 1, isAscending);
}
})
.catch(error => {
this.logger.debug(`Failed to Get: level=${rLevel}, key=${key}, error=${error}`);
return this.getAtLevel(key, isAscending ? rLevel + 1 : rLevel - 1, isAscending);
});
}
/**
* @param key The key to set
* @param value The value to set
* @param level The level at which to set the key
* @param isAscending To go up the hierarchy (true) or down (false) from level
* @returns If the write succeeded to all levels going up/down or the error condition
*/
setAtLevel(key, value, level, isAscending = false) {
const rLevel = this.getCurrentLevelOrNull(isAscending, level);
if (rLevel === null) {
return Promise.resolve(this.getFullWriteStatus(value));
}
return this.levels[rLevel]
.set(key, value)
.then(valueWritten => {
if (valueWritten !== null) {
return this.setAtLevel(key, valueWritten, isAscending ? rLevel + 1 : rLevel - 1, isAscending);
}
return this.getPartialWriteStatus(isAscending, rLevel, value);
})
.catch(error => {
this.logger.warn(`Error setting: level=${rLevel}, key=${key}, error=${error}`);
return this.getPartialWriteStatus(isAscending, rLevel, value);
});
}
/**
* @param key The key to delete
* @param level The level at which to delete the key
* @param isAscending To go up the hierarchy (true) or down (false) from level
* @returns If the write succeeded to all levels going up/down or the error condition
*/
deleteAtLevel(key, level, isAscending = false, writtenValue) {
const rLevel = this.getCurrentLevelOrNull(isAscending, level);
if (rLevel === null) {
return Promise.resolve(this.getFullWriteStatus(writtenValue));
}
return this.levels[rLevel]
.delete(key)
.then(isSuccessful => {
if (isSuccessful) {
return this.deleteAtLevel(key, isAscending ? rLevel + 1 : rLevel - 1, isAscending);
}
return this.getPartialWriteStatus(isAscending, rLevel, writtenValue);
})
.catch(error => {
this.logger.warn(`Error deleting: level=${rLevel}, key=${key}, error=${error}`);
return this.getPartialWriteStatus(isAscending, rLevel, writtenValue);
});
}
/**
* @param level The level at which to search
* @return The number of keys at the specified level
*/
getSizeAtLevel(level) {
return this.levels[level].size();
}
/**
* @returns The keys a the top level (should be all keys across the entire hierarchy)
*/
getKeysAtTopLevel() {
return this.topLevel.keys();
}
/**
* @param key The key to retrieve
* @returns The value at the top level only or null
*/
getValueAtTopLevel(key) {
return this.getAtLevel(key, this.totalLevels - 1);
}
/**
* @param key The key to retrieve
* @returns The value at the bottom level only or null
*/
getValueAtBottomLevel(key) {
return this.getAtLevel(key, 0, false);
}
/**
* Set only the levels below the top level (for refresing from the top level for instance)
* @param key The key to set
* @param value The value to set
* @returns If the write succeeded to all levels going up/down or the error condition
*/
setBelowTopLevel(key, value) {
return this.setAtLevel(key, value, this.totalLevels - 2);
}
subscribeAtLevel(level, publishLevel) {
if (level <= 0) {
return publishLevel;
}
const nextLevel = level - 1;
const currentLevel = this.levels[level];
if ((0, ISubscribableStorageProvider_1.isISubscribableStorageProvider)(currentLevel)) {
this.logger.debug(`subscribe to level: ${level}`);
let handler = this.getUpdateHandlerAlways(nextLevel);
if (this.updatePolicy === IStorageHierarchy_1.StorageHierarchyUpdatePolicy.OnlyIfKeyExist) {
handler = this.getUpdateHandlerOnlyIfKeyExist(nextLevel, handler);
}
const wrappedHandler = this.getManagedPromiseSubscribe(handler);
currentLevel.subscribe(wrappedHandler);
this.storageChangedHandlers.set(level, wrappedHandler);
if (!publishLevel) {
publishLevel = level;
}
}
return this.subscribeAtLevel(nextLevel, publishLevel);
}
getCurrentLevelOrNull(isAscending, level) {
level = level === undefined ? (isAscending ? 0 : this.totalLevels - 1) : level;
if (isAscending && level >= this.totalLevels) {
return null;
}
else if (!isAscending && level < 0) {
return null;
}
else {
return level;
}
}
getUpdateHandlerAlways(updateLevel) {
return (key, value) => {
if (value) {
return this.setAtLevel(key, value, updateLevel).then(s => s.writtenLevels === this.totalLevels);
}
else {
return this.deleteAtLevel(key, updateLevel).then(s => s.writtenLevels === this.totalLevels);
}
};
}
getUpdateHandlerOnlyIfKeyExist(updateLevel, updateUnconditionally) {
return (key, value) => {
return this.getAtLevel(key, updateLevel, false).then(agedValue => {
if (agedValue) {
if (value !== undefined && this.ageCompareFunc(agedValue.age, value.age) >= 0) {
return Promise.resolve(true);
}
return updateUnconditionally(key, value);
}
this.logger.debug(`Key doesn't exist, ignoring subscribed update: ${key}`);
return Promise.resolve(false);
});
};
}
getManagedPromiseSubscribe(func) {
return (key, value) => {
const promise = func(key, value).then(() => {
this.pendingUpdates.delete(promise);
});
this.pendingUpdates.add(promise);
};
}
getPartialWriteStatus(isAscending, level, value) {
const writtenLevels = isAscending ? level : this.totalLevels - level - 1;
return {
isPersisted: !isAscending && level <= this.totalLevels - 2 && this.topLevel.isPersistable,
isPublished: this.publishLevel !== undefined &&
(isAscending ? level > this.publishLevel : level < this.publishLevel),
writtenLevels,
writtenValue: writtenLevels !== 0 ? value : undefined,
};
}
getFullWriteStatus(value) {
return {
isPersisted: this.topLevel.isPersistable,
isPublished: this.publishLevel !== undefined,
writtenLevels: this.totalLevels,
writtenValue: value,
};
}
}
exports.StorageHierarchy = StorageHierarchy;
//# sourceMappingURL=StorageHierarchy.js.map