UNPKG

@linkedmink/multilevel-aging-cache

Version:

Package provides an interface to cache and persist data to Redis, MongoDB, memory

243 lines 10.4 kB
"use strict"; 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