mastercache
Version:
Multi-tier cache module for Node.js. Redis, Upstash, CloudfareKV, File, in-memory and others drivers
1,015 lines (999 loc) • 31.4 kB
JavaScript
// src/events/cache/cache-hit.ts
var CacheHit = class {
constructor(key, value, store, graced = false) {
this.key = key;
this.value = value;
this.store = store;
this.graced = graced;
}
name = "cache:hit";
toJSON() {
return {
key: this.key,
value: this.value,
store: this.store,
graced: this.graced
};
}
};
// src/events/cache/cache-miss.ts
var CacheMiss = class {
constructor(key, store) {
this.key = key;
this.store = store;
}
name = "cache:miss";
toJSON() {
return {
key: this.key,
store: this.store
};
}
};
// src/events/cache/cache-cleared.ts
var CacheCleared = class {
constructor(store) {
this.store = store;
}
name = "cache:cleared";
toJSON() {
return {
store: this.store
};
}
};
// src/events/cache/cache-deleted.ts
var CacheDeleted = class {
constructor(key, store) {
this.key = key;
this.store = store;
}
name = "cache:deleted";
toJSON() {
return {
key: this.key,
store: this.store
};
}
};
// src/events/cache/cache-written.ts
var CacheWritten = class {
constructor(key, value, store) {
this.key = key;
this.value = value;
this.store = store;
}
name = "cache:written";
toJSON() {
return {
key: this.key,
store: this.store,
value: this.value
};
}
};
// src/events/bus/bus-message-received.ts
var BusMessageReceived = class {
constructor(message) {
this.message = message;
}
name = "bus:message:received";
toJSON() {
return {
keys: this.message.keys,
type: this.message.type
};
}
};
// src/events/bus/bus-message-published.ts
var BusMessagePublished = class {
constructor(message) {
this.message = message;
}
name = "bus:message:published";
toJSON() {
return {
keys: this.message.keys,
type: this.message.type
};
}
};
// src/events/index.ts
var events = {
BusMessagePublished,
BusMessageReceived,
CacheHit,
CacheMiss,
CacheCleared,
CacheDeleted,
CacheWritten
};
// ../../node_modules/.pnpm/async-mutex@0.5.0/node_modules/async-mutex/index.mjs
var E_TIMEOUT = new Error("timeout while waiting for mutex to become available");
var E_ALREADY_LOCKED = new Error("mutex already locked");
var E_CANCELED = new Error("request for lock canceled");
var __awaiter$2 = function(thisArg, _arguments, P, generator) {
function adopt(value) {
return value instanceof P ? value : new P(function(resolve) {
resolve(value);
});
}
return new (P || (P = Promise))(function(resolve, reject) {
function fulfilled(value) {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
}
function rejected(value) {
try {
step(generator["throw"](value));
} catch (e) {
reject(e);
}
}
function step(result) {
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
}
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var Semaphore = class {
constructor(_value, _cancelError = E_CANCELED) {
this._value = _value;
this._cancelError = _cancelError;
this._queue = [];
this._weightedWaiters = [];
}
acquire(weight = 1, priority = 0) {
if (weight <= 0)
throw new Error(`invalid weight ${weight}: must be positive`);
return new Promise((resolve, reject) => {
const task = { resolve, reject, weight, priority };
const i = findIndexFromEnd(this._queue, (other) => priority <= other.priority);
if (i === -1 && weight <= this._value) {
this._dispatchItem(task);
} else {
this._queue.splice(i + 1, 0, task);
}
});
}
runExclusive(callback_1) {
return __awaiter$2(this, arguments, void 0, function* (callback, weight = 1, priority = 0) {
const [value, release] = yield this.acquire(weight, priority);
try {
return yield callback(value);
} finally {
release();
}
});
}
waitForUnlock(weight = 1, priority = 0) {
if (weight <= 0)
throw new Error(`invalid weight ${weight}: must be positive`);
if (this._couldLockImmediately(weight, priority)) {
return Promise.resolve();
} else {
return new Promise((resolve) => {
if (!this._weightedWaiters[weight - 1])
this._weightedWaiters[weight - 1] = [];
insertSorted(this._weightedWaiters[weight - 1], { resolve, priority });
});
}
}
isLocked() {
return this._value <= 0;
}
getValue() {
return this._value;
}
setValue(value) {
this._value = value;
this._dispatchQueue();
}
release(weight = 1) {
if (weight <= 0)
throw new Error(`invalid weight ${weight}: must be positive`);
this._value += weight;
this._dispatchQueue();
}
cancel() {
this._queue.forEach((entry) => entry.reject(this._cancelError));
this._queue = [];
}
_dispatchQueue() {
this._drainUnlockWaiters();
while (this._queue.length > 0 && this._queue[0].weight <= this._value) {
this._dispatchItem(this._queue.shift());
this._drainUnlockWaiters();
}
}
_dispatchItem(item) {
const previousValue = this._value;
this._value -= item.weight;
item.resolve([previousValue, this._newReleaser(item.weight)]);
}
_newReleaser(weight) {
let called = false;
return () => {
if (called)
return;
called = true;
this.release(weight);
};
}
_drainUnlockWaiters() {
if (this._queue.length === 0) {
for (let weight = this._value; weight > 0; weight--) {
const waiters = this._weightedWaiters[weight - 1];
if (!waiters)
continue;
waiters.forEach((waiter) => waiter.resolve());
this._weightedWaiters[weight - 1] = [];
}
} else {
const queuedPriority = this._queue[0].priority;
for (let weight = this._value; weight > 0; weight--) {
const waiters = this._weightedWaiters[weight - 1];
if (!waiters)
continue;
const i = waiters.findIndex((waiter) => waiter.priority <= queuedPriority);
(i === -1 ? waiters : waiters.splice(0, i)).forEach((waiter) => waiter.resolve());
}
}
}
_couldLockImmediately(weight, priority) {
return (this._queue.length === 0 || this._queue[0].priority < priority) && weight <= this._value;
}
};
function insertSorted(a, v) {
const i = findIndexFromEnd(a, (other) => v.priority <= other.priority);
a.splice(i + 1, 0, v);
}
function findIndexFromEnd(a, predicate) {
for (let i = a.length - 1; i >= 0; i--) {
if (predicate(a[i])) {
return i;
}
}
return -1;
}
var __awaiter$1 = function(thisArg, _arguments, P, generator) {
function adopt(value) {
return value instanceof P ? value : new P(function(resolve) {
resolve(value);
});
}
return new (P || (P = Promise))(function(resolve, reject) {
function fulfilled(value) {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
}
function rejected(value) {
try {
step(generator["throw"](value));
} catch (e) {
reject(e);
}
}
function step(result) {
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
}
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var Mutex = class {
constructor(cancelError) {
this._semaphore = new Semaphore(1, cancelError);
}
acquire() {
return __awaiter$1(this, arguments, void 0, function* (priority = 0) {
const [, releaser] = yield this._semaphore.acquire(1, priority);
return releaser;
});
}
runExclusive(callback, priority = 0) {
return this._semaphore.runExclusive(() => callback(), 1, priority);
}
isLocked() {
return this._semaphore.isLocked();
}
waitForUnlock(priority = 0) {
return this._semaphore.waitForUnlock(1, priority);
}
release() {
if (this._semaphore.isLocked())
this._semaphore.release();
}
cancel() {
return this._semaphore.cancel();
}
};
var __awaiter = function(thisArg, _arguments, P, generator) {
function adopt(value) {
return value instanceof P ? value : new P(function(resolve) {
resolve(value);
});
}
return new (P || (P = Promise))(function(resolve, reject) {
function fulfilled(value) {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
}
function rejected(value) {
try {
step(generator["throw"](value));
} catch (e) {
reject(e);
}
}
function step(result) {
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
}
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
function withTimeout(sync, timeout, timeoutError = E_TIMEOUT) {
return {
acquire: (weightOrPriority, priority) => {
let weight;
if (isSemaphore(sync)) {
weight = weightOrPriority;
} else {
weight = void 0;
priority = weightOrPriority;
}
if (weight !== void 0 && weight <= 0) {
throw new Error(`invalid weight ${weight}: must be positive`);
}
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
let isTimeout = false;
const handle = setTimeout(() => {
isTimeout = true;
reject(timeoutError);
}, timeout);
try {
const ticket = yield isSemaphore(sync) ? sync.acquire(weight, priority) : sync.acquire(priority);
if (isTimeout) {
const release = Array.isArray(ticket) ? ticket[1] : ticket;
release();
} else {
clearTimeout(handle);
resolve(ticket);
}
} catch (e) {
if (!isTimeout) {
clearTimeout(handle);
reject(e);
}
}
}));
},
runExclusive(callback, weight, priority) {
return __awaiter(this, void 0, void 0, function* () {
let release = () => void 0;
try {
const ticket = yield this.acquire(weight, priority);
if (Array.isArray(ticket)) {
release = ticket[1];
return yield callback(ticket[0]);
} else {
release = ticket;
return yield callback();
}
} finally {
release();
}
});
},
release(weight) {
sync.release(weight);
},
cancel() {
return sync.cancel();
},
waitForUnlock: (weightOrPriority, priority) => {
let weight;
if (isSemaphore(sync)) {
weight = weightOrPriority;
} else {
weight = void 0;
priority = weightOrPriority;
}
if (weight !== void 0 && weight <= 0) {
throw new Error(`invalid weight ${weight}: must be positive`);
}
return new Promise((resolve, reject) => {
const handle = setTimeout(() => reject(timeoutError), timeout);
(isSemaphore(sync) ? sync.waitForUnlock(weight, priority) : sync.waitForUnlock(priority)).then(() => {
clearTimeout(handle);
resolve();
});
});
},
isLocked: () => sync.isLocked(),
getValue: () => sync.getValue(),
setValue: (value) => sync.setValue(value)
};
}
function isSemaphore(sync) {
return sync.getValue !== void 0;
}
// src/cache/locks.ts
var Locks = class {
/**
* A map that will hold active locks for each key
*/
#locks = /* @__PURE__ */ new Map();
/**
* For a given key, get or create a new lock
*
* @param key Key to get or create a lock for
* @param timeout Time to wait to acquire the lock
*/
getOrCreateForKey(key, timeout) {
let lock = this.#locks.get(key);
if (!lock) {
lock = new Mutex();
this.#locks.set(key, lock);
}
return timeout ? withTimeout(lock, timeout) : lock;
}
release(key, releaser) {
releaser();
this.#locks.delete(key);
}
};
// ../../node_modules/.pnpm/p-timeout@6.1.3/node_modules/p-timeout/index.js
var TimeoutError = class extends Error {
constructor(message) {
super(message);
this.name = "TimeoutError";
}
};
var AbortError = class extends Error {
constructor(message) {
super();
this.name = "AbortError";
this.message = message;
}
};
var getDOMException = (errorMessage) => globalThis.DOMException === void 0 ? new AbortError(errorMessage) : new DOMException(errorMessage);
var getAbortedReason = (signal) => {
const reason = signal.reason === void 0 ? getDOMException("This operation was aborted.") : signal.reason;
return reason instanceof Error ? reason : getDOMException(reason);
};
function pTimeout(promise, options) {
const {
milliseconds,
fallback,
message,
customTimers = { setTimeout, clearTimeout }
} = options;
let timer;
const wrappedPromise = new Promise((resolve, reject) => {
if (typeof milliseconds !== "number" || Math.sign(milliseconds) !== 1) {
throw new TypeError(`Expected \`milliseconds\` to be a positive number, got \`${milliseconds}\``);
}
if (options.signal) {
const { signal } = options;
if (signal.aborted) {
reject(getAbortedReason(signal));
}
const abortHandler = () => {
reject(getAbortedReason(signal));
};
signal.addEventListener("abort", abortHandler, { once: true });
promise.finally(() => {
signal.removeEventListener("abort", abortHandler);
});
}
if (milliseconds === Number.POSITIVE_INFINITY) {
promise.then(resolve, reject);
return;
}
const timeoutError = new TimeoutError();
timer = customTimers.setTimeout.call(void 0, () => {
if (fallback) {
try {
resolve(fallback());
} catch (error) {
reject(error);
}
return;
}
if (typeof promise.cancel === "function") {
promise.cancel();
}
if (message === false) {
resolve();
} else if (message instanceof Error) {
reject(message);
} else {
timeoutError.message = message ?? `Promise timed out after ${milliseconds} milliseconds`;
reject(timeoutError);
}
}, milliseconds);
(async () => {
try {
resolve(await promise);
} catch (error) {
reject(error);
}
})();
});
const cancelablePromise = wrappedPromise.finally(() => {
cancelablePromise.clear();
});
cancelablePromise.clear = () => {
customTimers.clearTimeout.call(void 0, timer);
timer = void 0;
};
return cancelablePromise;
}
// src/libs/exception.ts
import { format } from "node:util";
var Exception = class extends Error {
/**
* Name of the class that raised the exception.
*/
name;
/**
* A status code for the error. Usually helpful when converting errors
* to HTTP responses.
*/
status;
constructor(message, options) {
super(message, options);
const ErrorConstructor = this.constructor;
this.name = ErrorConstructor.name;
this.message = message || ErrorConstructor.message || "";
this.status = options?.status || ErrorConstructor.status || 500;
const code = options?.code || ErrorConstructor.code;
if (code !== void 0) {
this.code = code;
}
const help = ErrorConstructor.help;
if (help !== void 0) {
this.help = help;
}
Error.captureStackTrace(this, ErrorConstructor);
}
get [Symbol.toStringTag]() {
return this.constructor.name;
}
toString() {
if (this.code) {
return `${this.name} [${this.code}]: ${this.message}`;
}
return `${this.name}: ${this.message}`;
}
};
function createError(message, code, status) {
return class extends Exception {
static message = message;
static code = code;
static status = status;
constructor(args, options) {
super(format(message, ...args || []), options);
this.name = "Exception";
}
};
}
// src/errors.ts
var E_FACTORY_SOFT_TIMEOUT = createError(
"Factory has timed out after waiting for soft timeout",
"E_FACTORY_SOFT_TIMEOUT"
);
var E_FACTORY_HARD_TIMEOUT = createError(
"Factory has timed out after waiting for hard timeout",
"E_FACTORY_HARD_TIMEOUT"
);
// src/cache/factory-runner.ts
var FactoryRunner = class {
#stack;
#stackWriter;
#locks;
constructor(stack, stackWriter, locks) {
this.#stack = stack;
this.#stackWriter = stackWriter;
this.#locks = locks;
}
async saveBackgroundFactoryResult(key, factoryResult, options, lockReleaser) {
await this.#stackWriter.set(key, factoryResult, options);
this.#locks.release(key, lockReleaser);
}
async writeFactoryResult(key, item, options, lockReleaser) {
await this.#stackWriter.set(key, item, options);
this.#stack.emit(new events.CacheMiss(key, this.#stack.name));
this.#stack.logger.trace({ key, cache: this.#stack.name, opId: options.id }, "cache miss");
this.#locks.release(key, lockReleaser);
}
async run(key, factory, hasFallback, options, lockReleaser) {
const timeoutDuration = options.factoryTimeout(hasFallback);
const timeoutException = timeoutDuration === options.timeouts?.hard ? E_FACTORY_HARD_TIMEOUT : E_FACTORY_SOFT_TIMEOUT;
const promisifiedFactory = async () => {
return await factory({ setTtl: (ttl) => options.setLogicalTtl(ttl) });
};
const factoryPromise = promisifiedFactory();
const factoryResult = await pTimeout(factoryPromise, {
milliseconds: timeoutDuration ?? Number.POSITIVE_INFINITY,
fallback: async () => {
factoryPromise.then((result) => this.saveBackgroundFactoryResult(key, result, options, lockReleaser)).catch(() => {
}).finally(() => this.#locks.release(key, lockReleaser));
throw new timeoutException();
}
});
await this.writeFactoryResult(key, factoryResult, options, lockReleaser);
return factoryResult;
}
};
// src/cache/get-set-handler.ts
var GetSetHandler = class {
constructor(stack, stackWriter) {
this.stack = stack;
this.stackWriter = stackWriter;
this.#factoryRunner = new FactoryRunner(this.stack, this.stackWriter, this.#locks);
}
/**
* A map that will hold active locks for each key
*/
#locks = new Locks();
#factoryRunner;
get logger() {
return this.stack.logger;
}
get emitter() {
return this.stack.emitter;
}
/**
* Emit a CacheEvent using the emitter
*/
#emit(event) {
return this.stack.emitter.emit(event.name, event.toJSON());
}
/**
* Refresh a cache item before it expires
*/
async #earlyExpirationRefresh(key, factory, options) {
this.logger.debug({ key, name: this.stack.name, opId: options.id }, "try to early refresh");
const lock = this.#locks.getOrCreateForKey(key);
if (lock.isLocked()) {
return;
}
await lock.runExclusive(async () => {
this.logger.trace(
{ key, cache: this.stack.name, opId: options.id },
"acquired lock for refresh"
);
await this.stackWriter.set(key, await factory(), options);
}).catch((error) => {
const msg = "factory error in early refresh";
this.logger.error({ key, cache: this.stack.name, opId: options.id, error }, msg);
throw error;
});
}
/**
* Returns a value from the local cache and emit a CacheHit event
*/
#returnLocalCacheValue(key, item, options, logMsg) {
const isLogicallyExpired = item.isLogicallyExpired();
logMsg = logMsg ?? "local cache hit";
this.#emit(new events.CacheHit(key, item.getValue(), this.stack.name, isLogicallyExpired));
this.logger.trace({ key, cache: this.stack.name, opId: options.id }, logMsg);
return item.getValue();
}
/**
* Returns a value from the remote cache and emit a CacheHit event
*/
async #returnRemoteCacheValue(key, item, options) {
this.logger.trace({ key, cache: this.stack.name, opId: options.id }, "remote cache hit");
this.stack.l1?.set(key, item.serialize(), options);
this.#emit(new events.CacheHit(key, item.getValue(), this.stack.name));
return item.getValue();
}
/**
* Try acquiring a lock for a key
*
* If we have a fallback value, grace period enabled, and a soft timeout configured
* we will wait at most the soft timeout to acquire the lock
*/
#acquireLock(key, hasFallback, options) {
const lock = this.#locks.getOrCreateForKey(key, options.getApplicableLockTimeout(hasFallback));
return lock.acquire();
}
#returnGracedValueOrThrow(key, item, options, err) {
if (options.isGracePeriodEnabled && item) {
return this.#returnLocalCacheValue(key, item, options, "local cache hit (graced)");
}
throw err;
}
async #applyFallbackAndReturnGracedValue(key, item, options) {
if (options.gracePeriod.enabled && options.gracePeriod.fallbackDuration) {
this.logger.trace(
{ key, cache: this.stack.name, opId: options.id },
"apply fallback duration"
);
this.stack.l1?.set(
key,
item.applyFallbackDuration(options.gracePeriod.fallbackDuration).serialize(),
options
);
}
this.logger.trace({ key, cache: this.stack.name, opId: options.id }, "returns stale value");
this.#emit(new events.CacheHit(key, item.getValue(), this.stack.name, true));
return item.getValue();
}
/**
* Check if a cache item is not undefined and not logically expired
*/
#isItemValid(item) {
return !!item && !item.isLogicallyExpired();
}
async handle(key, factory, options) {
let localItem;
localItem = this.stack.l1?.get(key, options);
if (this.#isItemValid(localItem)) {
if (localItem?.isEarlyExpired()) this.#earlyExpirationRefresh(key, factory, options);
return this.#returnLocalCacheValue(key, localItem, options);
}
let releaser;
try {
releaser = await this.#acquireLock(key, !!localItem, options);
} catch (err) {
return this.#returnGracedValueOrThrow(key, localItem, options, err);
}
this.logger.trace({ key, cache: this.stack.name, opId: options.id }, "acquired lock");
localItem = this.stack.l1?.get(key, options);
if (this.#isItemValid(localItem)) {
this.#locks.release(key, releaser);
return this.#returnLocalCacheValue(key, localItem, options, "local cache hit after lock");
}
const remoteItem = await this.stack.l2?.get(key, options);
if (this.#isItemValid(remoteItem)) {
this.#locks.release(key, releaser);
return this.#returnRemoteCacheValue(key, remoteItem, options);
}
try {
const hasFallback = !!localItem || !!remoteItem;
return await this.#factoryRunner.run(key, factory, hasFallback, options, releaser);
} catch (err) {
const staleItem = remoteItem ?? localItem;
if (err instanceof E_FACTORY_SOFT_TIMEOUT && staleItem) {
return this.#returnGracedValueOrThrow(key, staleItem, options, err);
}
this.logger.trace(
{ key, cache: this.stack.name, opId: options.id, error: err },
"factory error"
);
if (staleItem && options.isGracePeriodEnabled) {
this.#locks.release(key, releaser);
return this.#applyFallbackAndReturnGracedValue(key, staleItem, options);
}
this.#locks.release(key, releaser);
throw err;
}
}
};
// src/cache/stack/cache-stack-writer.ts
var CacheStackWriter = class {
constructor(cacheStack) {
this.cacheStack = cacheStack;
}
/**
* Write a value in the cache stack
* - Set value in local cache
* - Set value in remote cache
* - Publish a message to the bus
* - Emit a CacheWritten event
*/
async set(key, value, options) {
const item = this.cacheStack.serialize({
value,
logicalExpiration: options.logicalTtlFromNow(),
earlyExpiration: options.earlyExpireTtlFromNow()
});
this.cacheStack.l1?.set(key, item, options);
await this.cacheStack.l2?.set(key, item, options);
await this.cacheStack.publish({ type: "set" /* Set */, keys: [key] });
this.cacheStack.emit(new CacheWritten(key, value, this.cacheStack.name));
return true;
}
};
// src/cache/cache.ts
var Cache = class _Cache {
/**
* The name of the cache
*/
name;
#getSetHandler;
#cacheWriter;
#stack;
constructor(name, stack) {
this.name = name;
this.#stack = stack;
this.#cacheWriter = new CacheStackWriter(this.#stack);
this.#getSetHandler = new GetSetHandler(this.#stack, this.#cacheWriter);
}
#resolveDefaultValue(defaultValue) {
return typeof defaultValue === "function" ? defaultValue() : defaultValue ?? void 0;
}
/**
* Returns a new instance of the driver namespaced
*/
namespace(namespace) {
return new _Cache(this.name, this.#stack.namespace(namespace));
}
async get(keyOrOptions, defaultValue, rawOptions) {
let key;
let providedOptions;
let defaultValueFn;
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 !== void 0 && !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 !== void 0 && !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, value, rawOptions) {
if (typeof keyOrOptions === "string") {
const options2 = this.#stack.defaultOptions.cloneWith(rawOptions);
return this.#cacheWriter.set(keyOrOptions, value, options2);
}
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(keyOrOptions, value, rawOptions) {
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(keyOrOptions, factory, options) {
if (typeof keyOrOptions === "string") {
const cacheOptions2 = this.#stack.defaultOptions.cloneWith(options);
return this.#getSetHandler.handle(keyOrOptions, factory, cacheOptions2);
}
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(keyOrOptions, factory, options) {
if (typeof keyOrOptions === "string") {
const cacheOptions2 = this.#stack.defaultOptions.cloneWith({ ttl: null, ...options });
return this.#getSetHandler.handle(keyOrOptions, factory, cacheOptions2);
}
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, options) {
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, options) {
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(key) {
const value = await this.get(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, rawOptions) {
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: "delete" /* 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, rawOptions) {
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: "delete" /* Delete */, keys });
return true;
}
/**
* Remove all items from the cache
*/
async clear(options) {
const cacheOptions = this.#stack.defaultOptions.cloneWith(options);
await Promise.all([
this.#stack.l1?.clear(),
this.#stack.l2?.clear(cacheOptions),
this.#stack.publish({ type: "clear" /* 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()
]);
}
};
export {
Cache
};
//# sourceMappingURL=cache.js.map