mastercache
Version:
Multi-tier cache module for Node.js. Redis, Upstash, CloudfareKV, File, in-memory and others drivers
298 lines (256 loc) • 8.04 kB
text/typescript
import { Cache } from './cache/cache';
import type { MasterStore } from './masterstore';
import type { CacheProvider } from './types/provider';
import { CacheStack } from './cache/stack/cache-stack';
import { MasterCacheOptions } from './mastercache-options';
import type {
CacheEvents,
Factory,
GetOrSetOptions,
RawMasterCacheOptions,
GetOptions,
DeleteOptions,
SetOptions,
MasterCachePlugin,
HasOptions,
ClearOptions,
GetSetFactory,
GetOrSetForeverPojoOptions,
GetOrSetPojoOptions,
GetPojoOptions,
SetPojoOptions,
HasPojoOptions,
DeletePojoOptions,
DeleteManyPojoOptions,
GetOrSetForeverOptions,
} from './types/main';
export class MasterCache<KnownCaches extends Record<string,MasterStore>> implements CacheProvider {
/**
* Name of the default cache
*/
#defaultStoreName: keyof KnownCaches;
/**
* List of registered caches
*/
#stores: KnownCaches;
/**
* Cache of already instantiated drivers
*/
#driversCache: Map<keyof KnownCaches, CacheProvider> = new Map();
/**
* Master Cache options instance
*/
#options: MasterCacheOptions;
constructor(
config: RawMasterCacheOptions & {
default: keyof KnownCaches
stores: KnownCaches
plugins?: MasterCachePlugin[]
},
) {
this.#stores = config.stores;
this.#defaultStoreName = config.default;
this.#options = new MasterCacheOptions(config);
this.#options.logger.trace('mastercache initialized');
/**
* Register plugins
*/
if (config.plugins) config.plugins.forEach((plugin) => plugin.register(this));
}
#createProvider(cacheName: string, store:MasterStore): CacheProvider {
const entry = store.entry;
const driverItemOptions = this.#options.cloneWith(entry.options);
const cacheStack = new CacheStack(cacheName, driverItemOptions, {
l1Driver: entry.l1?.factory({ prefix: driverItemOptions.prefix, ...entry.l1.options }),
l2Driver: entry.l2?.factory({ prefix: driverItemOptions.prefix, ...entry.l2.options }),
busDriver: entry.bus?.factory(entry.bus?.options),
busOptions: entry.bus?.options,
});
return new Cache(cacheName, cacheStack);
}
get defaultStoreName() {
return this.#defaultStoreName as string;
}
/**
* Use a registered cache driver
*/
use<CacheName extends keyof KnownCaches>(cache?: CacheName) {
const cacheToUse: keyof KnownCaches | undefined = cache || this.#defaultStoreName;
if (!cacheToUse) throw new Error('No cache driver selected');
/**
* Check if the cache driver was already instantiated
*/
if (this.#driversCache.has(cacheToUse)) {
return this.#driversCache.get(cacheToUse)!;
}
/**
* Otherwise create a new instance and cache it
*/
const provider = this.#createProvider(cacheToUse as string, this.#stores[cacheToUse]);
this.#driversCache.set(cacheToUse, provider);
return provider;
}
/**
* Subscribe to a given cache event
*/
on<Event extends keyof CacheEvents>(event: Event, callback: (arg: CacheEvents[Event]) => void) {
this.#options.emitter.on(event, callback);
return this;
}
/**
* Subscribe to a given cache event only once
*/
once<Event extends keyof CacheEvents>(event: Event, callback: (arg: CacheEvents[Event]) => void) {
this.#options.emitter.once(event, callback);
return this;
}
/**
* Unsubscribe the callback from the given event
*/
off<Event extends keyof CacheEvents>(event: Event, callback: (arg: CacheEvents[Event]) => void) {
this.#options.emitter.off(event, callback);
return this;
}
/**
* Returns a new instance of the driver namespaced
*/
namespace(namespace: string) {
return this.use().namespace(namespace);
}
/**
* Get a value from the cache
*/
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> {
if (typeof keyOrOptions === 'string') {
return this.use().get<T>(keyOrOptions, defaultValue, rawOptions);
}
return this.use().get<T>(keyOrOptions);
}
/**
* Put a value in the cache
* Returns true if the value was set, false otherwise
*/
async set(keyOrOptions: string | SetPojoOptions, value?: any, options?: SetOptions) {
if (typeof keyOrOptions === 'string') {
return this.use().set(keyOrOptions, value, options);
}
return this.use().set(keyOrOptions);
}
/**
* Put a value in the cache forever
* Returns true if the value was set, false otherwise
*/
async setForever(keyOrOptions: string | SetPojoOptions, value?: any, options?: SetOptions) {
if (typeof keyOrOptions === 'string') {
return this.use().setForever(keyOrOptions, value, options);
}
return this.use().setForever(keyOrOptions);
}
/**
* 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') {
return this.use().getOrSet(keyOrOptions, factory!, options);
}
return this.use().getOrSet(keyOrOptions);
}
/**
* Retrieve an item from the cache if it exists, otherwise store the value
* provided by the factory forever and return it
*/
getOrSetForever<T>(
key: string | GetOrSetForeverPojoOptions<T>,
cb?: GetSetFactory<T>,
opts?: GetOrSetForeverOptions,
): Promise<T> {
if (typeof key === 'string') {
return this.use().getOrSetForever(key, cb!, opts);
}
return this.use().getOrSetForever(key);
}
/**
* Check if a key exists in the cache
*/
async has(keyOrOptions: string | HasPojoOptions, options?: HasOptions) {
if (typeof keyOrOptions === 'string') {
return this.use().has(keyOrOptions, options);
}
return this.use().has(keyOrOptions);
}
/**
* Check if key is missing in the cache
*/
async missing(keyOrOptions: string | HasPojoOptions, options?: HasOptions) {
if (typeof keyOrOptions === 'string') {
return this.use().missing(keyOrOptions, options);
}
return this.use().missing(keyOrOptions);
}
/**
* Get the value of a key and delete it
*
* Returns the value if the key exists, undefined otherwise
*/
async pull<T = any>(key: string) {
return this.use().pull<T>(key);
}
/**
* Delete a key from the cache
* Returns true if the key was deleted, false otherwise
*/
async delete(keyOrOptions: string | DeletePojoOptions, options?: DeleteOptions) {
if (typeof keyOrOptions === 'string') {
return this.use().delete(keyOrOptions, options);
}
return this.use().delete(keyOrOptions);
}
/**
* Delete multiple keys from the cache
*/
async deleteMany(
keysOrOptions: string[] | DeleteManyPojoOptions,
options?: DeleteOptions,
): Promise<boolean> {
if (Array.isArray(keysOrOptions)) {
return this.use().deleteMany(keysOrOptions, options);
}
return this.use().deleteMany(keysOrOptions);
}
/**
* Remove all items from the cache
*/
async clear(options?: ClearOptions) {
return this.use().clear(options);
}
/**
* Remove all items from all caches
*/
async clearAll(options?: ClearOptions) {
await Promise.all(Object.keys(this.#stores).map((cache) => this.use(cache).clear(options)));
}
/**
* Closes the connection to the cache
*/
async disconnect() {
return this.use().disconnect();
}
/**
* Disconnect all cache connections created by the manager
*/
async disconnectAll(): Promise<void> {
await Promise.all(Object.keys(this.#stores).map((cache) => this.use(cache).disconnect()));
}
}