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
text/typescript
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(),
]);
}
}