UNPKG

key-file-storage

Version:

Simple key-value storage directly on file system, maps each key to a separate file.

219 lines (197 loc) 7.07 kB
export interface Cache { [key: string]: any; } interface CollectionCache { [collection: string]: string[]; } export default function createCache(cacheConfig?: number | boolean): Cache { if (cacheConfig === true || typeof cacheConfig === 'undefined') { // Unlimited cache by default return createCache_Unlimited(cacheConfig); } else if (cacheConfig === false) { // No cache return createCache_NoCache(cacheConfig); } else if (typeof cacheConfig === 'number' && cacheConfig > 0) { // Limited cache by the number of keys return createCache_LimitedByKeyCount(cacheConfig); } else { throw new Error('Invalid cache config.'); } function createCache_Unlimited(cacheConfig: true | undefined) { let collectionCache: CollectionCache = {}; return new Proxy<Cache>( { /*CACHE*/ }, { set: function (target, property, value, receiver) { const propertyName = String(property); if (propertyName.endsWith('/')) { collectionCache[propertyName] = value; return true; } target[propertyName] = value; Object.keys(collectionCache) .filter((collection) => keyInCollection(propertyName, collection)) .forEach( (collection) => collectionCache[collection].includes(propertyName) || collectionCache[collection].push(propertyName), ); return true; }, get: function (target, property, receiver) { const propertyName = String(property); if (propertyName.endsWith('/')) return collectionCache[propertyName]; return target[propertyName]; }, deleteProperty: function (target, property) { const propertyName = String(property); if (propertyName === '*') { collectionCache = {}; Object.keys(target).forEach((key) => delete target[key]); return true; } if (propertyName.endsWith('/')) return delete collectionCache[propertyName]; Object.keys(collectionCache) .filter((collection) => keyInCollection(propertyName, collection)) .forEach( (collection) => collectionCache[collection].includes(propertyName) && collectionCache[collection].splice(collectionCache[collection].indexOf(propertyName), 1), ); return delete target[propertyName]; }, has: function (target, property) { const propertyName = String(property); if (propertyName.endsWith('/')) return propertyName in collectionCache; return property in target; }, }, ); } function createCache_NoCache(cacheConfig: false) { return new Proxy<Cache>( { /*CACHE*/ }, { set: function (target, property, value, receiver) { return true; }, get: function (target, property, receiver) { return undefined; }, deleteProperty: function (target, property) { return true; }, has: function (target, property) { return false; }, }, ); } function createCache_LimitedByKeyCount(cacheConfig: number) { let collectionCache: CollectionCache = {}; let keyNumber = Math.ceil(cacheConfig), keys = Array(keyNumber), nextKeyIndex = 0, keyIndex: number; return new Proxy<Cache>( { /*CACHE*/ }, { set: function (target, property, value, receiver) { const propertyName = String(property); if (propertyName.endsWith('/')) { collectionCache[propertyName] = value; return true; } updateKeys(target, propertyName, 'SET'); target[propertyName] = value; Object.keys(collectionCache) .filter((collection) => keyInCollection(propertyName, collection)) .forEach( (collection) => collectionCache[collection].includes(propertyName) || collectionCache[collection].push(propertyName), ); return true; }, get: function (target, property, receiver) { const propertyName = String(property); if (propertyName.endsWith('/')) return collectionCache[propertyName]; updateKeys(target, propertyName, 'GET'); return target[propertyName]; }, deleteProperty: function (target, property) { const propertyName = String(property); if (propertyName === '*') { collectionCache = {}; keys = Array(keyNumber); nextKeyIndex = 0; return true; } if (propertyName.endsWith('/')) return delete collectionCache[propertyName]; Object.keys(collectionCache) .filter((collection) => keyInCollection(propertyName, collection)) .forEach( (collection) => collectionCache[collection].includes(propertyName) && collectionCache[collection].splice(collectionCache[collection].indexOf(propertyName), 1), ); updateKeys(target, propertyName, 'DELETE'); return delete target[propertyName]; }, has: function (target, property) { const propertyName = String(property); if (propertyName.endsWith('/')) return propertyName in collectionCache; return keys.indexOf(property) >= 0; }, }, ); function realIndex(i: number) { return (i + keyNumber) % keyNumber; } function updateKeys( target: { [x: string]: any }, property: string | number | symbol, mode: 'SET' | 'GET' | 'DELETE', ) { keyIndex = keys.indexOf(property); if (keyIndex < 0) { // Does not exist mode === 'SET' && addKey(); } else if (keyIndex === realIndex(nextKeyIndex - 1)) { // The latest key mode === 'DELETE' && removeKey(); } else { // Otherwise removeKey(); mode === 'DELETE' || addKey(); } function removeKey() { while (keyIndex !== nextKeyIndex && keys[keyIndex]) { keys[keyIndex] = keys[realIndex(keyIndex - 1)]; keyIndex = realIndex(keyIndex - 1); } keys[nextKeyIndex] = undefined; } function addKey() { if (keys[nextKeyIndex] !== property) { if (keys[nextKeyIndex] !== undefined) delete target[keys[nextKeyIndex]]; keys[nextKeyIndex] = property; } nextKeyIndex = realIndex(nextKeyIndex + 1); } } } function keyInCollection(key: string, collection: string): boolean { collection = collection.startsWith('./') ? collection.slice(1) : collection.startsWith('/') ? collection : '/' + collection; key = key.startsWith('./') ? key.slice(1) : key.startsWith('/') ? key : '/' + key; return key.startsWith(collection); } }