UNPKG

mastercache

Version:

Multi-tier cache module for Node.js. Redis, Upstash, CloudfareKV, File, in-memory and others drivers

274 lines (230 loc) 8.86 kB
import { events } from '../events/index'; import { GetSetHandler } from './get-set-handler'; import { CacheBusMessageType } from '../types/main'; import type { CacheStack } from './stack/cache-stack'; import type { CacheProvider } from '../types/provider'; import { CacheStackWriter } from './stack/cache-stack-writer'; import type { GetOrSetOptions, Factory, GetOptions, DeleteOptions, SetOptions, HasOptions, ClearOptions, GetSetFactory, GetOrSetPojoOptions, GetPojoOptions, SetPojoOptions, HasPojoOptions, DeletePojoOptions, DeleteManyPojoOptions, GetOrSetForeverPojoOptions, GetOrSetForeverOptions, } from '../types/main'; export class Cache implements CacheProvider { /** * The name of the cache */ name: string; #getSetHandler: GetSetHandler; #cacheWriter: CacheStackWriter; #stack: CacheStack; constructor(name: string, stack: CacheStack) { this.name = name; this.#stack = stack; this.#cacheWriter = new CacheStackWriter(this.#stack); this.#getSetHandler = new GetSetHandler(this.#stack, this.#cacheWriter); } #resolveDefaultValue(defaultValue?: Factory) { return typeof defaultValue === 'function' ? defaultValue() : defaultValue ?? undefined; } /** * Returns a new instance of the driver namespaced */ namespace(namespace: string) { return new Cache(this.name, this.#stack.namespace(namespace)); } get<T = any>(options: GetPojoOptions<T>): Promise<T> get<T = any>(key: string): Promise<T | null | undefined> get<T = any>(key: string, defaultValue: Factory<T>, options?: GetOptions): Promise<T> async get<T = any>( keyOrOptions: string | GetPojoOptions<T>, defaultValue?: Factory<T>, rawOptions?: GetOptions, ): Promise<T | undefined | null> { let key: string; let providedOptions: GetOptions; let defaultValueFn: Factory<T>; if (typeof keyOrOptions === 'string') { key = keyOrOptions; providedOptions = rawOptions ?? {}; defaultValueFn = this.#resolveDefaultValue(defaultValue); } else { key = keyOrOptions.key; providedOptions = keyOrOptions; defaultValueFn = this.#resolveDefaultValue(keyOrOptions.defaultValue); } const options = this.#stack.defaultOptions.cloneWith(providedOptions); const localItem = this.#stack.l1?.get(key, options); if (localItem !== undefined && !localItem.isLogicallyExpired()) { this.#stack.emit(new events.CacheHit(key, localItem.getValue(), this.name)); return localItem.getValue(); } const remoteItem = await this.#stack.l2?.get(key, options); if (remoteItem !== undefined && !remoteItem.isLogicallyExpired()) { this.#stack.l1?.set(key, remoteItem.serialize(), options); this.#stack.emit(new events.CacheHit(key, remoteItem.getValue(), this.name)); return remoteItem.getValue(); } if (!options.isGracePeriodEnabled) { this.#stack.emit(new events.CacheMiss(key, this.name)); return this.#resolveDefaultValue(defaultValueFn); } if (remoteItem) { this.#stack.l1?.set(key, remoteItem.serialize(), options); this.#stack.emit(new events.CacheHit(key, remoteItem.serialize(), this.name, true)); return remoteItem.getValue(); } if (localItem) { this.#stack.emit(new events.CacheHit(key, localItem.serialize(), this.name, true)); return localItem.getValue(); } this.#stack.emit(new events.CacheMiss(key, this.name)); return this.#resolveDefaultValue(defaultValueFn); } /** * Set a value in the cache * Returns true if the value was set, false otherwise */ async set(keyOrOptions: string | SetPojoOptions, value?: any, rawOptions?: SetOptions) { if (typeof keyOrOptions === 'string') { const options = this.#stack.defaultOptions.cloneWith(rawOptions); return this.#cacheWriter.set(keyOrOptions, value, options); } const options = this.#stack.defaultOptions.cloneWith(keyOrOptions); return this.#cacheWriter.set(keyOrOptions.key, keyOrOptions.value, options); } /** * Set a value in the cache forever * Returns true if the value was set, false otherwise */ async setForever<T>(keyOrOptions: string | SetPojoOptions, value?: T, rawOptions?: SetOptions) { return this.set(keyOrOptions, value, { ttl: null, ...rawOptions }); } /** * Retrieve an item from the cache if it exists, otherwise store the value * provided by the factory and return it */ async getOrSet<T>( keyOrOptions: string | GetOrSetPojoOptions<T>, factory?: GetSetFactory<T>, options?: GetOrSetOptions, ): Promise<T> { if (typeof keyOrOptions === 'string') { const cacheOptions = this.#stack.defaultOptions.cloneWith(options); return this.#getSetHandler.handle(keyOrOptions, factory, cacheOptions); } const cacheOptions = this.#stack.defaultOptions.cloneWith(keyOrOptions); return this.#getSetHandler.handle(keyOrOptions.key, keyOrOptions.factory, cacheOptions); } /** * Retrieve an item from the cache if it exists, otherwise store the value * provided by the factory forever and return it */ async getOrSetForever<T>( keyOrOptions: string | GetOrSetForeverPojoOptions<T>, factory?: GetSetFactory<T>, options?: GetOrSetForeverOptions, ): Promise<T> { if (typeof keyOrOptions === 'string') { const cacheOptions = this.#stack.defaultOptions.cloneWith({ ttl: null, ...options }); return this.#getSetHandler.handle(keyOrOptions, factory, cacheOptions); } const cacheOptions = this.#stack.defaultOptions.cloneWith({ ttl: null, ...keyOrOptions }); return this.#getSetHandler.handle(keyOrOptions.key, keyOrOptions.factory, cacheOptions); } /** * Check if a key exists in the cache */ async has(keyOrOptions: string | HasPojoOptions, options?: HasOptions) { const key = typeof keyOrOptions === 'string' ? keyOrOptions : keyOrOptions.key; const providedOptions = typeof keyOrOptions === 'string' ? options : keyOrOptions; const cacheOptions = this.#stack.defaultOptions.cloneWith(providedOptions); const inRemote = await this.#stack.l2?.has(key, cacheOptions); const inLocal = this.#stack.l1?.has(key); return !!(inRemote || inLocal); } /** * Check if key is missing in the cache */ async missing(keyOrOptions: string | HasPojoOptions, options?: HasOptions) { return !(await this.has(keyOrOptions, options)); } /** * Get the value of a key and delete it * Returns the value if the key exists, undefined otherwise */ async pull<T = any>(key: string): Promise<T | undefined | null> { const value = await this.get<T>(key); await this.delete(key); return value; } /** * Delete a key from the cache, emit cache:deleted event and * publish invalidation through the bus */ async delete( keyOrOptions: string | DeletePojoOptions, rawOptions?: DeleteOptions, ): Promise<boolean> { const isPojo = typeof keyOrOptions !== 'string'; const key = isPojo ? keyOrOptions.key : keyOrOptions; const options = this.#stack.defaultOptions.cloneWith(isPojo ? keyOrOptions : rawOptions); this.#stack.l1?.delete(key, options); await this.#stack.l2?.delete(key, options); this.#stack.emit(new events.CacheDeleted(key, this.name)); await this.#stack.publish({ type: CacheBusMessageType.Delete, keys: [key] }); return true; } /** * Delete multiple keys from local and remote cache * Then emit cache:deleted events for each key * And finally publish invalidation through the bus */ async deleteMany( keysOrOptions: string[] | DeleteManyPojoOptions, rawOptions?: DeleteOptions, ): Promise<boolean> { const isPojo = !Array.isArray(keysOrOptions); const options = this.#stack.defaultOptions.cloneWith(isPojo ? keysOrOptions : rawOptions); const keys = isPojo ? keysOrOptions.keys : keysOrOptions; this.#stack.l1?.deleteMany(keys, options); await this.#stack.l2?.deleteMany(keys, options); keys.forEach((key) => this.#stack.emit(new events.CacheDeleted(key, this.name))); await this.#stack.publish({ type: CacheBusMessageType.Delete, keys }); return true; } /** * Remove all items from the cache */ async clear(options?: ClearOptions) { const cacheOptions = this.#stack.defaultOptions.cloneWith(options); await Promise.all([ this.#stack.l1?.clear(), this.#stack.l2?.clear(cacheOptions), this.#stack.publish({ type: CacheBusMessageType.Clear, keys: [] }), ]); this.#stack.emit(new events.CacheCleared(this.name)); } /** * Closes the connection to the cache */ async disconnect() { await Promise.all([ this.#stack.l1?.disconnect(), this.#stack.l2?.disconnect(), this.#stack.bus?.disconnect(), ]); } }