keyv
Version:
Simple key-value storage with support for multiple backends
2,529 lines • 87 kB
JavaScript
import { Hookified } from "hookified";
//#region src/capabilities.ts
const keyvMethodNames = [
"get",
"set",
"delete",
"clear",
"has",
"getMany",
"setMany",
"deleteMany",
"hasMany",
"disconnect",
"getRaw",
"getManyRaw",
"setRaw",
"setManyRaw",
"iterator"
];
const keyvPropertyNames = ["hooks", "stats"];
const keyvStorageMethodNames = [
"get",
"getMany",
"has",
"hasMany",
"set",
"setMany",
"delete",
"deleteMany",
"clear",
"disconnect",
"iterator"
];
const keyvCompressionMethodNames = ["compress", "decompress"];
const keyvSerializationMethodNames = ["stringify", "parse"];
const keyvEncryptionMethodNames = ["encrypt", "decrypt"];
function isMethod(obj, name) {
return name in obj && typeof obj[name] === "function";
}
function isProperty(obj, name) {
return name in obj;
}
function resolveMethodType(obj, name) {
if (!(name in obj)) return "none";
const value = obj[name];
if (typeof value !== "function") return "none";
return value.constructor.name === "AsyncFunction" ? "async" : "sync";
}
function resolveMethod(obj, name) {
return {
exists: isMethod(obj, name),
methodType: resolveMethodType(obj, name)
};
}
const noneMethod = {
exists: false,
methodType: "none"
};
function buildMethods(obj, names) {
const methods = {};
for (const name of names) methods[name] = obj ? resolveMethod(obj, name) : { ...noneMethod };
return methods;
}
/** The methods a full storage adapter implements. */
const keyvStorageRequiredMethods = [
"get",
"has",
"hasMany",
"set",
"setMany",
"delete",
"deleteMany",
"clear"
];
/**
* Whether a detected store has every method of a full storage adapter, whether or not they are
* native `async` functions.
* @param capability - The result of {@link detectKeyvStorage}
* @returns `true` when all of the full storage adapter's methods exist
*/
function hasKeyvStorageMethods(capability) {
return keyvStorageRequiredMethods.every((m) => capability.methods[m].exists);
}
/**
* Detect whether an object implements the full Keyv interface
* @param obj - The object to check
* @returns A {@link KeyvCapability} where `compatible` is `true` only when all required capabilities are present
* @example
* ```typescript
* import Keyv, { detectKeyv } from 'keyv';
*
* const result = detectKeyv(new Keyv());
* result.compatible; // true — all capabilities present
* result.methods.get.exists; // true
* result.methods.get.methodType; // "async"
*
* const partial = detectKeyv(new Map());
* partial.compatible; // false — missing getMany, setMany, hooks, stats, etc.
* partial.methods.get.exists; // true
* ```
*/
function detectKeyv(obj) {
if (obj === null || obj === void 0 || typeof obj !== "object") return {
compatible: false,
methods: buildMethods(null, keyvMethodNames),
properties: {
hooks: false,
stats: false
}
};
const methods = buildMethods(obj, keyvMethodNames);
const properties = {
hooks: isProperty(obj, "hooks"),
stats: isProperty(obj, "stats")
};
return {
compatible: [...keyvMethodNames, ...keyvPropertyNames].every((k) => {
if (k === "hooks" || k === "stats") return properties[k];
return methods[k].exists;
}),
methods,
properties
};
}
/**
* Detect whether an object implements the Keyv storage adapter interface
* @param obj - The object to check
* @returns A {@link KeyvStorageCapability} where:
* - `compatible` is `true` when the object is a valid storage adapter (`"keyvStorage"`, `"mapLike"`, or `"asyncMap"`)
* - `store` indicates the detected store type: `"keyvStorage"`, `"mapLike"`, `"asyncMap"`, or `"none"`
* - `methods` maps each method name to `{ exists, methodType }`
* @example
* ```typescript
* import { detectKeyvStorage } from 'keyv';
*
* const map = detectKeyvStorage(new Map());
* map.compatible; // true
* map.store; // "mapLike"
* map.methods.get.exists; // true
* map.methods.get.methodType; // "sync"
*
* const adapter = detectKeyvStorage(asyncAdapter);
* adapter.compatible; // true
* adapter.store; // "keyvStorage"
* adapter.methods.get.methodType; // "async"
* ```
*/
function detectKeyvStorage(obj) {
if (obj === null || obj === void 0 || typeof obj !== "object") return {
compatible: false,
store: "none",
methods: buildMethods(null, keyvStorageMethodNames)
};
const methods = buildMethods(obj, keyvStorageMethodNames);
if (keyvStorageRequiredMethods.every((k) => methods[k].exists && methods[k].methodType === "async")) return {
compatible: true,
store: "keyvStorage",
methods
};
if ([
"get",
"set",
"delete",
"has"
].every((m) => methods[m].exists && methods[m].methodType === "sync")) return {
compatible: true,
store: "mapLike",
methods
};
if ([
"get",
"set",
"delete",
"clear"
].every((m) => methods[m].exists)) return {
compatible: true,
store: "asyncMap",
methods
};
return {
compatible: false,
store: "none",
methods
};
}
/**
* Build the capability descriptor for a v6 storage adapter: the structurally detected
* methods plus `expires: true`, declaring that the adapter accepts an absolute `expires`
* timestamp on `set`/`setMany`. First-party adapters expose this from their `capabilities`
* getter so Keyv uses them directly instead of bridging.
* @param adapter - The storage adapter to describe (typically `this`).
* @returns A {@link KeyvStorageCapability} with `expires` set to `true`.
* @example
* ```typescript
* public get capabilities(): KeyvStorageCapability {
* return keyvStorageCapability(this);
* }
* ```
*/
function keyvStorageCapability(adapter) {
return {
...detectKeyvStorage(adapter),
expires: true
};
}
/**
* Detect whether an object implements the Keyv compression adapter interface
* @param obj - The object to check
* @returns A {@link KeyvCompressionCapability} where `compatible` is `true` when both `compress` and `decompress` methods are present
* @example
* ```typescript
* import { detectKeyvCompression } from 'keyv';
*
* detectKeyvCompression({ compress: (d) => d, decompress: (d) => d });
* // { compatible: true, methods: { compress: { exists: true, methodType: "sync" }, decompress: { exists: true, methodType: "sync" } } }
* ```
*/
function detectKeyvCompression(obj) {
if (obj === null || obj === void 0 || typeof obj !== "object") return {
compatible: false,
methods: buildMethods(null, keyvCompressionMethodNames)
};
const methods = buildMethods(obj, keyvCompressionMethodNames);
return {
compatible: keyvCompressionMethodNames.every((k) => methods[k].exists),
methods
};
}
/**
* Detect whether an object implements the Keyv serialization adapter interface
* @param obj - The object to check
* @returns A {@link KeyvSerializationCapability} where `compatible` is `true` when both `stringify` and `parse` methods are present
* @example
* ```typescript
* import { detectKeyvSerialization } from 'keyv';
*
* detectKeyvSerialization(JSON);
* // { compatible: true, methods: { stringify: { exists: true, methodType: "sync" }, parse: { exists: true, methodType: "sync" } } }
* ```
*/
function detectKeyvSerialization(obj) {
if (obj === null || obj === void 0 || typeof obj !== "object") return {
compatible: false,
methods: buildMethods(null, keyvSerializationMethodNames)
};
const methods = buildMethods(obj, keyvSerializationMethodNames);
return {
compatible: keyvSerializationMethodNames.every((k) => methods[k].exists),
methods
};
}
/**
* Detect whether an object implements the Keyv encryption adapter interface
* @param obj - The object to check
* @returns A {@link KeyvEncryptionCapability} where `compatible` is `true` when both `encrypt` and `decrypt` methods are present
* @example
* ```typescript
* import { detectKeyvEncryption } from 'keyv';
*
* detectKeyvEncryption({ encrypt: (d) => d, decrypt: (d) => d });
* // { compatible: true, methods: { encrypt: { exists: true, methodType: "sync" }, decrypt: { exists: true, methodType: "sync" } } }
* ```
*/
function detectKeyvEncryption(obj) {
if (obj === null || obj === void 0 || typeof obj !== "object") return {
compatible: false,
methods: buildMethods(null, keyvEncryptionMethodNames)
};
const methods = buildMethods(obj, keyvEncryptionMethodNames);
return {
compatible: keyvEncryptionMethodNames.every((k) => methods[k].exists),
methods
};
}
//#endregion
//#region src/types/keyv.ts
/**
* Events emitted by Keyv for error handling and telemetry.
*/
let KeyvEvents = /* @__PURE__ */ function(KeyvEvents) {
/** Emitted when an error occurs in a store operation. */
KeyvEvents["ERROR"] = "error";
/** Emitted for informational messages. */
KeyvEvents["INFO"] = "info";
/** Emitted for warning messages. */
KeyvEvents["WARN"] = "warn";
/** Telemetry: cache hit. */
KeyvEvents["STAT_HIT"] = "stat:hit";
/** Telemetry: cache miss. */
KeyvEvents["STAT_MISS"] = "stat:miss";
/** Telemetry: value set. */
KeyvEvents["STAT_SET"] = "stat:set";
/** Telemetry: value deleted. */
KeyvEvents["STAT_DELETE"] = "stat:delete";
/** Telemetry: operation error. */
KeyvEvents["STAT_ERROR"] = "stat:error";
return KeyvEvents;
}({});
/**
* Hook names for intercepting Keyv operations.
* Register hooks via `keyv.on(KeyvHooks.BEFORE_SET, callback)` to run logic before/after operations.
*/
let KeyvHooks = /* @__PURE__ */ function(KeyvHooks) {
/** @deprecated Use BEFORE_SET instead */
KeyvHooks["PRE_SET"] = "preSet";
/** @deprecated Use AFTER_SET instead */
KeyvHooks["POST_SET"] = "postSet";
/** @deprecated Use BEFORE_GET instead */
KeyvHooks["PRE_GET"] = "preGet";
/** @deprecated Use AFTER_GET instead */
KeyvHooks["POST_GET"] = "postGet";
/** @deprecated Use BEFORE_GET_MANY instead */
KeyvHooks["PRE_GET_MANY"] = "preGetMany";
/** @deprecated Use AFTER_GET_MANY instead */
KeyvHooks["POST_GET_MANY"] = "postGetMany";
/** @deprecated Use BEFORE_GET_RAW instead */
KeyvHooks["PRE_GET_RAW"] = "preGetRaw";
/** @deprecated Use AFTER_GET_RAW instead */
KeyvHooks["POST_GET_RAW"] = "postGetRaw";
/** @deprecated Use BEFORE_GET_MANY_RAW instead */
KeyvHooks["PRE_GET_MANY_RAW"] = "preGetManyRaw";
/** @deprecated Use AFTER_GET_MANY_RAW instead */
KeyvHooks["POST_GET_MANY_RAW"] = "postGetManyRaw";
/** @deprecated Use BEFORE_SET_RAW instead */
KeyvHooks["PRE_SET_RAW"] = "preSetRaw";
/** @deprecated Use AFTER_SET_RAW instead */
KeyvHooks["POST_SET_RAW"] = "postSetRaw";
/** @deprecated Use BEFORE_SET_MANY_RAW instead */
KeyvHooks["PRE_SET_MANY_RAW"] = "preSetManyRaw";
/** @deprecated Use AFTER_SET_MANY_RAW instead */
KeyvHooks["POST_SET_MANY_RAW"] = "postSetManyRaw";
/** @deprecated Use BEFORE_SET_MANY instead */
KeyvHooks["PRE_SET_MANY"] = "preSetMany";
/** @deprecated Use AFTER_SET_MANY instead */
KeyvHooks["POST_SET_MANY"] = "postSetMany";
/** @deprecated Use BEFORE_DELETE instead */
KeyvHooks["PRE_DELETE"] = "preDelete";
/** @deprecated Use AFTER_DELETE instead */
KeyvHooks["POST_DELETE"] = "postDelete";
/** @deprecated Use BEFORE_DELETE_MANY instead */
KeyvHooks["PRE_DELETE_MANY"] = "preDeleteMany";
/** @deprecated Use AFTER_DELETE_MANY instead */
KeyvHooks["POST_DELETE_MANY"] = "postDeleteMany";
KeyvHooks["BEFORE_SET"] = "before:set";
KeyvHooks["AFTER_SET"] = "after:set";
KeyvHooks["BEFORE_GET"] = "before:get";
KeyvHooks["AFTER_GET"] = "after:get";
KeyvHooks["BEFORE_GET_MANY"] = "before:getMany";
KeyvHooks["AFTER_GET_MANY"] = "after:getMany";
KeyvHooks["BEFORE_GET_RAW"] = "before:getRaw";
KeyvHooks["AFTER_GET_RAW"] = "after:getRaw";
KeyvHooks["BEFORE_GET_MANY_RAW"] = "before:getManyRaw";
KeyvHooks["AFTER_GET_MANY_RAW"] = "after:getManyRaw";
KeyvHooks["BEFORE_SET_RAW"] = "before:setRaw";
KeyvHooks["AFTER_SET_RAW"] = "after:setRaw";
KeyvHooks["BEFORE_SET_MANY"] = "before:setMany";
KeyvHooks["AFTER_SET_MANY"] = "after:setMany";
KeyvHooks["BEFORE_SET_MANY_RAW"] = "before:setManyRaw";
KeyvHooks["AFTER_SET_MANY_RAW"] = "after:setManyRaw";
KeyvHooks["BEFORE_DELETE"] = "before:delete";
KeyvHooks["AFTER_DELETE"] = "after:delete";
KeyvHooks["BEFORE_DELETE_MANY"] = "before:deleteMany";
KeyvHooks["AFTER_DELETE_MANY"] = "after:deleteMany";
KeyvHooks["BEFORE_HAS"] = "before:has";
KeyvHooks["AFTER_HAS"] = "after:has";
KeyvHooks["BEFORE_HAS_MANY"] = "before:hasMany";
KeyvHooks["AFTER_HAS_MANY"] = "after:hasMany";
KeyvHooks["BEFORE_CLEAR"] = "before:clear";
KeyvHooks["AFTER_CLEAR"] = "after:clear";
KeyvHooks["BEFORE_DISCONNECT"] = "before:disconnect";
KeyvHooks["AFTER_DISCONNECT"] = "after:disconnect";
return KeyvHooks;
}({});
//#endregion
//#region src/utils.ts
/**
* Check whether a deserialized entry has expired based on its `expires` timestamp.
*/
function isDataExpired(data) {
return typeof data.expires === "number" && Date.now() > data.expires;
}
/**
* Calculate an absolute expiry timestamp from a TTL value, in whole milliseconds.
* Returns `undefined` when `ttl` is absent, zero, negative, or non-finite
* (meaning "no expiry").
*
* @param ttl - Time-to-live in milliseconds, or `undefined`
* @returns Absolute expiry timestamp (ms since epoch), or `undefined`
*/
function calculateExpires(ttl) {
if (typeof ttl !== "number" || ttl <= 0 || !Number.isFinite(ttl)) return;
return toStoreExpires(Date.now() + ttl);
}
/**
* Convert an absolute expiry into the form stores accept: whole milliseconds, rounded up so an
* entry never expires early, or `undefined` (no expiry) when it is not a finite number. Stores
* such as PostgreSQL's `BIGINT` column and Redis's `PXAT` reject a fractional timestamp.
*
* @param expires - Absolute expiry timestamp (ms since epoch), or `undefined`
* @returns The expiry in whole milliseconds, or `undefined` for no expiry
*/
function toStoreExpires(expires) {
if (typeof expires !== "number" || !Number.isFinite(expires)) return;
return Math.ceil(expires);
}
/**
* Resolve a TTL value by falling back to a default when none is given,
* then normalising zero, negative, or non-finite values to `undefined` (meaning "no expiry").
*
* @param ttl - Explicit TTL in milliseconds, or `undefined`
* @param defaultTtl - Fallback TTL (typically `Keyv._ttl`), or `undefined`
* @returns The resolved TTL in milliseconds, or `undefined` for no expiry
*/
function resolveTtl(ttl, defaultTtl) {
const resolved = ttl ?? defaultTtl;
if (resolved === void 0 || resolved <= 0 || !Number.isFinite(resolved)) return;
return resolved;
}
/**
* Derive a store-level TTL from an absolute `expires` timestamp.
* Returns `undefined` when `expires` is absent, non-finite, or when the derived
* TTL is zero or negative (i.e. the entry has already expired).
*
* @param expires - Absolute expiry timestamp in milliseconds since epoch, or `undefined`
* @returns The remaining TTL in milliseconds, or `undefined`
*/
function ttlFromExpires(expires) {
if (typeof expires !== "number" || !Number.isFinite(expires)) return;
const remaining = expires - Date.now();
return remaining > 0 ? remaining : void 0;
}
/**
* Calls a store's `has` once to see whether it returns a promise. A method that is not a native
* `async` function can still return one, such as an `async` method compiled to an older target.
* @param store - The store to check
* @returns `true` when `has` returned a promise
*/
function returnsPromise(store) {
let result;
try {
result = store.has("keyv-probe");
} catch {
return false;
}
if (typeof result?.then !== "function") return false;
Promise.resolve(result).catch(() => {});
return true;
}
/**
* Scan parallel `keys` and `data` arrays, nullify any expired entries in
* `data`, and batch-delete the corresponding keys via `keyv.deleteMany()`.
*/
async function deleteExpiredKeys(keys, data, keyv) {
const expiredKeys = [];
for (const [index, row] of data.entries()) if (row !== void 0 && row !== null && isDataExpired(row)) {
expiredKeys.push(keys[index]);
data[index] = void 0;
}
if (expiredKeys.length > 0) await keyv.deleteMany(expiredKeys);
}
/**
* Maps new hook names to their deprecated equivalents so both fire during migration.
*/
const deprecatedHookAliases = /* @__PURE__ */ new Map([
["before:set", "preSet"],
["after:set", "postSet"],
["before:get", "preGet"],
["after:get", "postGet"],
["before:getMany", "preGetMany"],
["after:getMany", "postGetMany"],
["before:getRaw", "preGetRaw"],
["after:getRaw", "postGetRaw"],
["before:getManyRaw", "preGetManyRaw"],
["after:getManyRaw", "postGetManyRaw"],
["before:setRaw", "preSetRaw"],
["after:setRaw", "postSetRaw"],
["before:setMany", "preSetMany"],
["after:setMany", "postSetMany"],
["before:setManyRaw", "preSetManyRaw"],
["after:setManyRaw", "postSetManyRaw"],
["before:delete", "preDelete"],
["after:delete", "postDelete"],
["before:deleteMany", "preDeleteMany"],
["after:deleteMany", "postDeleteMany"]
]);
/**
* Build the deprecated-hooks map used by Hookified to warn when old PRE_/POST_ hook names are registered.
*/
function buildDeprecatedHooks() {
return /* @__PURE__ */ new Map([
["preSet", "Use KeyvHooks.BEFORE_SET ('before:set') instead"],
["postSet", "Use KeyvHooks.AFTER_SET ('after:set') instead"],
["preGet", "Use KeyvHooks.BEFORE_GET ('before:get') instead"],
["postGet", "Use KeyvHooks.AFTER_GET ('after:get') instead"],
["preGetMany", "Use KeyvHooks.BEFORE_GET_MANY ('before:getMany') instead"],
["postGetMany", "Use KeyvHooks.AFTER_GET_MANY ('after:getMany') instead"],
["preGetRaw", "Use KeyvHooks.BEFORE_GET_RAW ('before:getRaw') instead"],
["postGetRaw", "Use KeyvHooks.AFTER_GET_RAW ('after:getRaw') instead"],
["preGetManyRaw", "Use KeyvHooks.BEFORE_GET_MANY_RAW ('before:getManyRaw') instead"],
["postGetManyRaw", "Use KeyvHooks.AFTER_GET_MANY_RAW ('after:getManyRaw') instead"],
["preSetRaw", "Use KeyvHooks.BEFORE_SET_RAW ('before:setRaw') instead"],
["postSetRaw", "Use KeyvHooks.AFTER_SET_RAW ('after:setRaw') instead"],
["preSetMany", "Use KeyvHooks.BEFORE_SET_MANY ('before:setMany') instead"],
["postSetMany", "Use KeyvHooks.AFTER_SET_MANY ('after:setMany') instead"],
["preSetManyRaw", "Use KeyvHooks.BEFORE_SET_MANY_RAW ('before:setManyRaw') instead"],
["postSetManyRaw", "Use KeyvHooks.AFTER_SET_MANY_RAW ('after:setManyRaw') instead"],
["preDelete", "Use KeyvHooks.BEFORE_DELETE ('before:delete') instead"],
["postDelete", "Use KeyvHooks.AFTER_DELETE ('after:delete') instead"],
["preDeleteMany", "Use KeyvHooks.BEFORE_DELETE_MANY ('before:deleteMany') instead"],
["postDeleteMany", "Use KeyvHooks.AFTER_DELETE_MANY ('after:deleteMany') instead"]
]);
}
//#endregion
//#region src/adapters/bridge.ts
/**
* A bridge storage adapter for Keyv that wraps any promise-based store.
*
* This class provides a unified interface for using various async stores
* with Keyv, handling namespace prefixing, TTL-based expiration, and batch operations.
* If the underlying store implements optional methods (has, hasMany, getMany, etc.),
* the bridge will delegate to them. Otherwise, it falls back to using primitives.
*
* @example
* ```typescript
* // Using with a promise-based store
* const bridge = new KeyvBridgeAdapter(myAsyncStore, { namespace: 'cache' });
*
* // Using with Keyv
* const keyv = new Keyv({ store: new KeyvBridgeAdapter(myAsyncStore) });
* ```
*/
var KeyvBridgeAdapter = class extends Hookified {
_store;
_namespace;
_namespaceSeparator = "::";
_capabilities;
/**
* Whether the wrapped store manages its own namespace (exposes a `namespace` property).
* When true the bridge propagates its namespace to the store and does not prefix keys,
* so the store's native, namespace-scoped operations (notably `clear()`) are used directly.
*/
_storeHandlesNamespace;
/**
* Creates a new KeyvBridgeAdapter instance.
* @param store - The underlying promise-based store to bridge
* @param options - Configuration options for the adapter
*/
constructor(store, options) {
super({ throwOnHookError: false });
this._store = store;
this._capabilities = detectKeyvStorage(store);
this._storeHandlesNamespace = hasKeyvStorageMethods(this._capabilities) && "namespace" in store;
if (options?.namespaceSeparator !== void 0) this._namespaceSeparator = options.namespaceSeparator;
if (options?.namespace) this._namespace = options.namespace;
if (this._storeHandlesNamespace) if (options?.namespace === void 0) this._namespace = this._store.namespace;
else this._store.namespace = this._namespace;
if (typeof store.on === "function") store.on("error", (error) => this.emit("error", error));
}
/**
* Gets the underlying store instance.
*/
get store() {
return this._store;
}
/**
* Sets the underlying store instance.
*/
set store(store) {
this._store = store;
}
/**
* Gets the capabilities of the underlying store, with `expires: true` to declare that
* the bridge accepts an absolute `expires` timestamp (which it converts to a ttl for the
* wrapped legacy store).
*/
get capabilities() {
return {
...this._capabilities,
expires: true
};
}
/**
* Gets the separator between the namespace and the key. Defaults to `::`.
*/
get namespaceSeparator() {
return this._namespaceSeparator;
}
/**
* Sets the separator between the namespace and the key.
*/
set namespaceSeparator(separator) {
this._namespaceSeparator = separator;
}
/**
* Gets the current namespace.
*/
get namespace() {
return this._namespace;
}
/**
* Sets the namespace. When the wrapped store manages its own namespace, the value is
* propagated to it so its native scoped operations stay in sync.
*/
set namespace(namespace) {
this._namespace = namespace;
if (this._storeHandlesNamespace) this._store.namespace = namespace;
}
/**
* Creates a prefixed key by combining the namespace and key with the separator.
* @param key - The base key
* @param namespace - Optional namespace to prefix the key with
* @returns The prefixed key if namespace is provided, otherwise the original key
*/
getKeyPrefix(key, namespace) {
if (this._storeHandlesNamespace) return key;
if (namespace) return `${namespace}${this._namespaceSeparator}${key}`;
return key;
}
/**
* Parses a prefixed key to extract the namespace and original key.
* @param key - The prefixed key to parse
* @returns An object containing the namespace (if present) and the original key
*/
getKeyPrefixData(key) {
if (this._namespace && key.startsWith(`${this._namespace}${this._namespaceSeparator}`)) return {
namespace: this._namespace,
key: key.slice(this._namespace.length + this._namespaceSeparator.length)
};
return { key };
}
/**
* Retrieves a value from the store by key.
* Automatically handles namespace prefixing and TTL expiration.
* @param key - The key to retrieve
* @returns The stored data, or undefined if not found or expired
*/
async get(key) {
const keyPrefix = this.getKeyPrefix(key, this._namespace);
const data = await this._store.get(keyPrefix);
if (data === void 0 || data === null) return;
if (isDataExpired(data)) {
await this._store.delete(keyPrefix);
return;
}
return data;
}
/**
* Retrieves multiple values from the store by their keys.
* Delegates to the store's native getMany if available.
* @param keys - Array of keys to retrieve
* @returns Array of stored data in the same order as the input keys
*/
async getMany(keys) {
if (this._capabilities.methods.getMany.exists) {
const prefixedKeys = keys.map((key) => this.getKeyPrefix(key, this._namespace));
/* v8 ignore next -- @preserve */
const results = await this._store.getMany?.(prefixedKeys) ?? [];
const values = [];
for (const [index, data] of results.entries()) {
if (data === void 0 || data === null) {
values.push(void 0);
continue;
}
if (isDataExpired(data)) {
await this._store.delete(prefixedKeys[index]);
values.push(void 0);
continue;
}
values.push(data);
}
return values;
}
const values = [];
for (const key of keys) {
const data = await this.get(key);
values.push(data);
}
return values;
}
/**
* Stores a value in the store with an optional absolute expiry.
* The wrapped store's `set(key, value, ttl?)` expects a relative duration, so the absolute
* `expires` is converted to a remaining ttl (`undefined` when already expired or absent).
* The value is passed through unchanged — the bridge does not wrap it in an envelope — so
* expiry is enforced by the wrapped legacy store's own ttl handling. (The read-side
* {@link isDataExpired} check only fires when a caller stores a raw `{ value, expires }`
* object directly; when Keyv core drives the bridge the value arrives already encoded.)
* @param key - The key to store the value under
* @param value - The value to store
* @param expires - Optional absolute expiry as Unix ms since epoch
* @returns Always returns true indicating success
*/
async set(key, value, expires) {
const keyPrefix = this.getKeyPrefix(key, this._namespace);
if (typeof expires === "number" && expires <= Date.now()) {
await this._store.delete(keyPrefix);
return true;
}
const result = await this._store.set(keyPrefix, value, ttlFromExpires(expires));
if (typeof result === "boolean") return result;
return true;
}
/**
* Stores multiple entries in the store at once.
* Delegates to the store's native setMany if available, otherwise loops over set.
* @param entries - Array of entries containing key, value, and optional absolute `expires`
* @returns One boolean per entry, `false` where the store reported that the write failed
*/
async setMany(entries) {
if (this._capabilities.methods.setMany.exists) {
const now = Date.now();
const isExpired = (entry) => typeof entry.expires === "number" && entry.expires <= now;
const live = entries.filter((entry) => !isExpired(entry));
let liveResults;
if (live.length > 0) liveResults = await this._store.setMany?.(live.map((entry) => ({
key: this.getKeyPrefix(entry.key, this._namespace),
value: entry.value,
ttl: ttlFromExpires(entry.expires)
})));
for (const entry of entries) if (isExpired(entry)) await this._store.delete(this.getKeyPrefix(entry.key, this._namespace));
let liveIndex = 0;
return entries.map((entry) => {
if (isExpired(entry)) return true;
const result = Array.isArray(liveResults) ? liveResults[liveIndex] : liveResults;
liveIndex++;
return result !== false;
});
}
const results = [];
for (const entry of entries) results.push(await this.set(entry.key, entry.value, entry.expires));
return results;
}
/**
* Checks if a key exists in the store and is not expired.
* Delegates to the store's native has if available.
* @param key - The key to check
* @returns True if the key exists and is not expired, false otherwise
*/
async has(key) {
if (this._capabilities.methods.has.exists) {
const keyPrefix = this.getKeyPrefix(key, this._namespace);
/* v8 ignore next -- @preserve */
return this._store.has?.(keyPrefix) ?? false;
}
return await this.get(key) !== void 0;
}
/**
* Checks if multiple keys exist in the store and are not expired.
* Delegates to the store's native hasMany if available.
* @param keys - Array of keys to check
* @returns Array of booleans indicating existence for each key
*/
async hasMany(keys) {
if (this._capabilities.methods.hasMany.exists) {
const prefixedKeys = keys.map((key) => this.getKeyPrefix(key, this._namespace));
/* v8 ignore next -- @preserve */
return this._store.hasMany?.(prefixedKeys) ?? [];
}
const results = [];
for (const key of keys) results.push(await this.has(key));
return results;
}
/**
* Deletes a value from the store by key.
* @param key - The key to delete
* @returns True if the key was deleted, false otherwise
*/
async delete(key) {
const keyPrefix = this.getKeyPrefix(key, this._namespace);
return this._store.delete(keyPrefix);
}
/**
* Deletes multiple keys from the store at once.
* Delegates to the store's native deleteMany if available.
* @param keys - Array of keys to delete
* @returns Array of booleans indicating success for each key
*/
async deleteMany(keys) {
if (this._capabilities.methods.deleteMany.exists) {
const prefixedKeys = keys.map((key) => this.getKeyPrefix(key, this._namespace));
/* v8 ignore next -- @preserve */
const result = await this._store.deleteMany?.(prefixedKeys) ?? [];
return Array.isArray(result) ? result : keys.map(() => result);
}
const results = [];
for (const key of keys) try {
const keyPrefix = this.getKeyPrefix(key, this._namespace);
const result = await this._store.delete(keyPrefix);
results.push(result);
} catch (error) {
this.emit("error", error);
results.push(false);
}
return results;
}
/**
* Clears entries from the store. With no namespace, the whole store is cleared. With a
* namespace, only that namespace's entries are removed: a store that manages its own
* namespace clears them itself, and otherwise the bridge finds them with the store's
* `iterator()`. A store with neither can't tell namespaces apart, so this throws rather
* than call the store's `clear()` and delete every namespace's entries.
* @throws {Error} If a namespace is set and the store neither manages it nor has `iterator()`.
*/
async clear() {
if (!this._namespace || this._storeHandlesNamespace) {
await this._store.clear();
return;
}
if (!this._capabilities.methods.iterator.exists) throw new Error(`Can't clear namespace "${this._namespace}": the store has no iterator() to find that namespace's keys, and its clear() would delete every namespace. Give the store an iterator(), or clear it without a namespace.`);
const prefix = `${this._namespace}${this._namespaceSeparator}`;
const keysToDelete = [];
/* v8 ignore next -- @preserve */
for await (const entry of this._store.iterator?.(this._namespace) ?? []) {
const key = Array.isArray(entry) ? entry[0] : entry;
if (typeof key === "string" && key.startsWith(prefix)) keysToDelete.push(key);
}
for (const key of keysToDelete) await this._store.delete(key);
}
/**
* Creates an async iterator for iterating over store entries.
* If the underlying store does not support iteration, returns an empty generator.
* @returns An async generator yielding [key, value] pairs
*/
async *iterator() {
if (!this._capabilities.methods.iterator.exists) return;
const namespace = this._namespace;
const prefix = namespace && !this._storeHandlesNamespace ? `${namespace}${this._namespaceSeparator}` : void 0;
/* v8 ignore next -- @preserve */
for await (const entry of this._store.iterator?.(this._namespace) ?? []) {
const [key, data] = Array.isArray(entry) ? entry : [entry];
if (prefix && typeof key === "string" && !key.startsWith(prefix)) continue;
if (data && isDataExpired(data)) {
await this._store.delete(key);
continue;
}
yield [prefix && typeof key === "string" ? key.slice(prefix.length) : key, data];
}
}
/**
* Disconnects from the underlying store.
* No-op if the store does not support disconnect.
*/
async disconnect() {
if (this._capabilities.methods.disconnect.exists) await this._store.disconnect?.();
}
};
//#endregion
//#region src/json-serializer.ts
function getGlobalBuffer() {
return globalThis.Buffer;
}
function bytesToBase64(bytes) {
const buffer = getGlobalBuffer();
if (buffer) return buffer.from(bytes).toString("base64");
let binary = "";
for (const byte of bytes) binary += String.fromCharCode(byte);
return btoa(binary);
}
function base64ToBytes(value) {
const buffer = getGlobalBuffer();
if (buffer) return buffer.from(value, "base64");
const binary = atob(value);
const bytes = new Uint8Array(binary.length);
for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
return bytes;
}
function isBinaryValue(value) {
if (getGlobalBuffer()?.isBuffer(value)) return true;
return value instanceof Uint8Array;
}
/**
* Pre-process a value tree, converting Buffer and BigInt to tagged strings
* before JSON.stringify can call toJSON() on them.
*/
function prepare(value) {
if (value === null || value === void 0) return value;
if (isBinaryValue(value)) return `:base64:${bytesToBase64(value)}`;
if (typeof value === "bigint") return `:bigint:${value.toString()}`;
if (typeof value === "string") return value.startsWith(":") ? `:${value}` : value;
if (Array.isArray(value)) return value.map((item) => prepare(item));
if (typeof value === "object") {
if (typeof value.toJSON === "function") return prepare(value.toJSON());
const result = Object.create(null);
for (const key of Object.keys(value)) if (value[key] !== void 0) result[key] = prepare(value[key]);
return result;
}
return value;
}
var KeyvJsonSerializer = class {
stringify(object) {
return JSON.stringify(prepare(object));
}
parse(data) {
return JSON.parse(data, (_, value) => {
if (typeof value === "string") {
if (value.startsWith(":bigint:")) return BigInt(value.slice(8));
if (value.startsWith(":base64:")) return base64ToBytes(value.slice(8));
return value.startsWith(":") ? value.slice(1) : value;
}
return value;
});
}
};
const jsonSerializer = new KeyvJsonSerializer();
//#endregion
//#region src/sanitize.ts
const categoryPatterns = {
sql: [
/;/g,
/--/g,
/\/\*/g
],
mongo: [/^\$/g, /\{\s*\$/g],
escape: [
/\0/g,
/\r/g,
/\n/g
],
path: [/\.\.\//g, /\.\.\\/g]
};
/**
* Compile an array of RegExp patterns from the enabled categories.
* Returns `undefined` when nothing is enabled.
*/
function buildPatterns(options) {
const patterns = [];
for (const [category, regexes] of Object.entries(categoryPatterns)) if (options[category] !== false) patterns.push(...regexes);
return patterns.length > 0 ? patterns : void 0;
}
/**
* Run all patterns against a string, stripping matched sequences. Stripping one match can form
* another, as `..././` becomes `../`, so when a match remains, {@link stripRemaining} finishes
* the job.
*/
function applyPatterns(value, patterns, options) {
for (const pattern of patterns) {
pattern.lastIndex = 0;
value = value.replace(pattern, "");
}
if (patterns.some((pattern) => value.search(pattern) !== -1)) return stripRemaining(value, options);
return value;
}
/**
* Strip the matches a single pass of {@link applyPatterns} left behind. It reads the string once,
* left to right, and drops a pattern as soon as its last character arrives, so a pattern formed by
* an earlier removal is caught without scanning the string again. The result contains no match.
*/
function stripRemaining(value, options) {
const kept = [];
for (const char of value) {
const length = matchLength(kept, char, options);
if (length === 0) kept.push(char);
else kept.length -= length - 1;
}
return kept.join("");
}
const whitespace = /\s/;
/**
* The length of the pattern that `char` completes at the end of `kept`, counting `char` itself, or
* 0 when it completes none. `;`, `\0`, `\r` and `\n` need no check: the single pass before this
* removes every one of them, and removing characters never adds one back.
*/
function matchLength(kept, char, options) {
const last = kept.length - 1;
if (options.sql && (char === "-" && kept[last] === "-" || char === "*" && kept[last] === "/")) return 2;
if (options.mongo && char === "$") {
if (kept.length === 0) return 1;
let start = kept.length;
while (start > 0 && whitespace.test(kept[start - 1])) start--;
if (kept[start - 1] === "{") return kept.length - start + 2;
}
if (options.path && (char === "/" || char === "\\") && kept[last] === "." && kept[last - 1] === ".") return 3;
return 0;
}
const allOn = {
escape: true,
mongo: true,
path: true,
sql: true
};
const allOff = {
escape: false,
mongo: false,
path: false,
sql: false
};
/**
* Encapsulates key and namespace sanitization with an LRU result cache.
*/
var KeyvSanitize = class {
_keys = { ...allOff };
_namespace = { ...allOff };
_keyPatterns;
_namespacePatterns;
_enabled = false;
_cacheKeys = /* @__PURE__ */ new Map();
_cacheNamespaces = /* @__PURE__ */ new Map();
_cacheMax = 1e4;
constructor(options) {
if (options !== void 0) this.updateOptions(options);
}
/**
* The key sanitization pattern configuration.
*/
get keys() {
return this._keys;
}
/**
* Whether any sanitization pattern (keys or namespace) is enabled.
*/
get enabled() {
return this._enabled;
}
/**
* The namespace sanitization pattern configuration.
*/
get namespace() {
return this._namespace;
}
/**
* Update the sanitization configuration. Recompiles patterns and clears the cache.
*/
updateOptions(options) {
this._keys = this.resolvePatterns(options.keys);
this._namespace = this.resolvePatterns(options.namespace);
this._enabled = Object.values(this._keys).some(Boolean) || Object.values(this._namespace).some(Boolean);
this._keyPatterns = buildPatterns(this._keys);
this._namespacePatterns = buildPatterns(this._namespace);
this._cacheKeys.clear();
this._cacheNamespaces.clear();
}
/**
* Sanitize a single key. Uses an LRU cache for repeated lookups.
*/
cleanKey(key) {
if (!this._keyPatterns) return key;
const cached = this._cacheKeys.get(key);
if (cached !== void 0) {
this._cacheKeys.delete(key);
this._cacheKeys.set(key, cached);
return cached;
}
const result = applyPatterns(key, this._keyPatterns, this._keys);
this._cacheKeys.set(key, result);
if (this._cacheKeys.size > this._cacheMax) {
const first = this._cacheKeys.keys().next().value;
if (first !== void 0) this._cacheKeys.delete(first);
}
return result;
}
/**
* Sanitize an array of keys.
*/
cleanKeys(keys) {
if (!this._keyPatterns) return keys;
return keys.map((k) => this.cleanKey(k));
}
/**
* Sanitize a namespace string. Uses an LRU cache for repeated lookups.
*/
cleanNamespace(ns) {
if (!this._namespacePatterns) return ns;
const cached = this._cacheNamespaces.get(ns);
if (cached !== void 0) {
this._cacheNamespaces.delete(ns);
this._cacheNamespaces.set(ns, cached);
return cached;
}
const result = applyPatterns(ns, this._namespacePatterns, this._namespace);
this._cacheNamespaces.set(ns, result);
if (this._cacheNamespaces.size > this._cacheMax) {
const first = this._cacheNamespaces.keys().next().value;
if (first !== void 0) this._cacheNamespaces.delete(first);
}
return result;
}
/**
* Clear the LRU caches.
*/
clearCache() {
this._cacheKeys.clear();
this._cacheNamespaces.clear();
}
resolvePatterns(options) {
if (options === false || options === void 0) return { ...allOff };
if (options === true) return { ...allOn };
return {
sql: options.sql !== false,
mongo: options.mongo !== false,
escape: options.escape !== false,
path: options.path !== false
};
}
};
//#endregion
//#region src/stats.ts
var KeyvStats = class {
_hits = 0;
_misses = 0;
_sets = 0;
_deletes = 0;
_errors = 0;
_maxEntries = 1e3;
_enabled = false;
hitKeysMap = /* @__PURE__ */ new Map();
missKeysMap = /* @__PURE__ */ new Map();
setKeysMap = /* @__PURE__ */ new Map();
deleteKeysMap = /* @__PURE__ */ new Map();
errorKeysMap = /* @__PURE__ */ new Map();
_emitter;
_listeners = /* @__PURE__ */ new Map();
constructor(options) {
if (options?.maxEntries !== void 0) this._maxEntries = options.maxEntries;
if (options?.enabled !== void 0) this._enabled = options.enabled;
this.reset();
if (options?.emitter) if (this._enabled === true) this.subscribe(options.emitter);
else this._emitter = options.emitter;
}
/**
* Total number of cache hits.
*/
get hits() {
return this._hits;
}
/**
* Total number of cache misses.
*/
get misses() {
return this._misses;
}
/**
* Total number of cache sets.
*/
get sets() {
return this._sets;
}
/**
* Total number of cache deletes.
*/
get deletes() {
return this._deletes;
}
/**
* Total number of cache errors.
*/
get errors() {
return this._errors;
}
/**
* LRU-bounded map of key to hit count.
*/
get hitKeys() {
return this.hitKeysMap;
}
/**
* LRU-bounded map of key to miss count.
*/
get missKeys() {
return this.missKeysMap;
}
/**
* LRU-bounded map of key to set count.
*/
get setKeys() {
return this.setKeysMap;
}
/**
* LRU-bounded map of key to delete count.
*/
get deleteKeys() {
return this.deleteKeysMap;
}
/**
* LRU-bounded map of key to error count.
*/
get errorKeys() {
return this.errorKeysMap;
}
/**
* Maximum number of entries per event-type LRU map.
* @default 1000
*/
get maxEntries() {
return this._maxEntries;
}
/**
* Set the maximum number of entries per event-type LRU map.
* @param {number} value the new maximum entries
*/
/* v8 ignore next 3 -- @preserve */
set maxEntries(value) {
this._maxEntries = value;
}
/**
* Whether stats tracking is enabled.
* @default false
*/
get enabled() {
return this._enabled;
}
/**
* Enable or disable stats tracking. If false it will unsubscribe from the events
* @param {boolean} value true to enable, false to disable
*/
set enabled(value) {
this._enabled = value;
if (this._enabled === false && this._listeners.size !== 0) {
const emitter = this._emitter;
this.unsubscribe();
this._emitter = emitter;
} else if (this._enabled === true && this._emitter && this._listeners.size === 0) this.subscribe(this._emitter);
}
/**
* Build a composite key from a telemetry event.
* Format: "namespace::key" if namespace is present, otherwise just "key".
*/
buildKeyEventName(event) {
if (event.namespace && event.key) return `${event.namespace}::${event.key}`;
return event.key ?? "";
}
/**
* Increment the count for a key in an LRU-bounded map.
* Deletes and re-inserts to maintain LRU order, evicts the oldest entry when full.
*/
incrementKeys(map, compositeKey) {
if (this.maxEntries <= 0 || !compositeKey) return;
const current = map.get(compositeKey) ?? 0;
map.delete(compositeKey);
map.set(compositeKey, current + 1);
if (map.size > this.maxEntries) {
const first = map.keys().next().value;
if (first !== void 0) map.delete(first);
}
}
/**
* Subscribe to telemetry events from an emitter (e.g. a Keyv instance).
* Automatically increments the corresponding stat counters and LRU key maps on each event.
* @param {IEventEmitter} emitter the event emitter to subscribe to
*/
subscribe(emitter) {
this.unsubscribe();
this._emitter = emitter;
const hitListener = (event) => {
this._hits++;
this.incrementKeys(this.hitKeysMap, this.buildKeyEventName(event));
};
const missListener = (event) => {
this._misses++;
this.incrementKeys(this.missKeysMap, this.buildKeyEventName(event));
};
const setListener = (event) => {
this._sets++;
this.incrementKeys(this.setKeysMap, this.buildKeyEventName(event));
};
const deleteListener = (event) => {
this._deletes++;
this.incrementKeys(this.deleteKeysMap, this.buildKeyEventName(event));
};
const errorListener = (event) => {
this._errors++;
this.incrementKeys(this.errorKeysMap, this.buildKeyEventName(event));
};
this._listeners.set("stat:hit", hitListener);
this._listeners.set("stat:miss", missListener);
this._listeners.set("stat:set", setListener);
this._listeners.set("stat:delete", deleteListener);
this._listeners.set("stat:error", errorListener);
emitter.on("stat:hit", hitListener);
emitter.on("stat:miss", missListener);
emitter.on("stat:set", setListener);
emitter.on("stat:delete", deleteListener);
emitter.on("stat:error", errorListener);
}
/**
* Unsubscribe from the currently subscribed emitter, removing all telemetry event listeners.
*/
unsubscribe() {
if (!this._emitter) return;
for (const [eventName, listener] of this._listeners) this._emitter.off(eventName, listener);
this._listeners.clear();
this._emitter = void 0;
}
/**
* Reset all counters and LRU key maps to their initial state.
*/
reset() {
this._hits = 0;
this._misses = 0;
this._sets = 0;
this._deletes = 0;
this._errors = 0;
this.hitKeysMap.clear();
this.missKeysMap.clear();
this.setKeysMap.clear();
this.deleteKeysMap.clear();
this.errorKeysMap.clear();
}
};
//#endregion
//#region src/keyv.ts
/**
* The namespace Keyv uses when sanitizing a namespace leaves nothing. An empty namespace would
* turn namespacing off, mixing the instance's keys with unrelated ones and letting `clear()`
* remove them.
*/
const emptiedNamespace = "keyv-sanitized";
var Keyv = class Keyv extends Hookified {
/**
* Keyv Constructor
* @param {KeyvStorageAdapter | KeyvOptions} store
* @param {Omit<KeyvOptions, 'store'>} [options] if you provide the store you can then provide the Keyv Options
*/
constructor(store, options) {
const mergedOptions = Keyv.resolveOptions(store, options);
super({
throwOnHookError: false,
throwOnEmptyListeners: true
});
this.deprecatedHooks = buildDeprecatedHooks();
this._compression = mergedOptions.compression;
this._encryption = mergedOptions.encryption;
this.initSerialization(mergedOptions);
this.initSanitize(mergedOptions);
this.initNamespace(mergedOptions.namespace);
this.initStats(mergedOptions);
if (mergedOptions.store) this.setStore(mergedOptions.store);
this.setTtl(mergedOptions.ttl);
this._checkExpired = mergedOptions.checkExpired ?? true;
}
/**
* Stats manager for tracking cache operation metrics (hits, misses, sets, deletes, errors).
* @default this is disabled.
*/
_stats;
/**
* Default time to live in milliseconds. Can be overridden per-key via {@link set}.
*/
_ttl;
/**
* Key prefix namespace used to isolate keys across different Keyv instances sharing the same store.
*/
_namespace;
/**
* The underlying storage adapter. Defaults to an in-memory {@link Map}.
*/
_store = new KeyvMemoryAdapter(/* @__PURE__ */ new Map());
/**
* Pluggable serialization adapter with `stringify` and `parse` methods.
* When `undefined`, the built-in {@link KeyvJsonSerializer} is used.
*/
_serialization;
/**
* Pluggable compression adapter with `compress` and `decompress` methods.
*/
_compression;
/**
* Pluggable encryption adapter with `encrypt` and `decrypt` methods.
*/
_encryption;
/**
* Sanitization handler for keys and namespaces. By default it is disabled.
*/
_sanitize;
/**
* When true, Keyv checks expiry at its layer on get/getMany/has/hasMany.
* Defaults to true so reads stay millisecond-precise even on adapters whose
* native expiry is coarse or lazily swept (e.g. Memcached, DynamoDB).
*/
_checkExpired = true;
/**
* Get the current storage adapter.
* @returns {KeyvStorageAdapter} The current storage adapter.
*/
get store() {
return this._store;
}
/**
* Set the storage adapter.
* @param {KeyvStorageAdapter | Map<any, any> | any} store The storage adapter to set.
*/
set store(store) {
this.setStore(store);
}
/**
* Get the current compression adapter.
* @returns {KeyvCompressionAdapter | undefined} The current compression adapter.
*/
get compression() {
return this._compression;
}
/**
* Set the compression adapter.
* @param {KeyvCompressionAdapter | undefined} compress The compression adapter to set.
*/
set compression(compress) {
this._compression = compress;
}
/**
* Get the current encryption adapter.
* @returns {KeyvEncryptionAdapter | undefined} The current encryption adapter.
*/
get encryption() {
return this._encryption;
}
/**
* Set the encryption adapter.
* @param {KeyvEncryptionAdapter | undefined} encryption The encryption adapter to set.
*/
set encryption(encryption) {
this._encryption = encryption;
}
/**
* Get the current namespace. When Keyv has no namespace of its own, this is the namespace
* configured on the storage adapter.
* @returns {string | undefined} The current namespace.
*/
get namespace() {
return this._namespace ?? this._store.namespace;
}
/**
* Set the current namespace.
* @param {string | undefined} namespace The namespace to set.
*/
set namespace(namespace) {
this._namespace = namespace && this._sanitize.enabled ? this.cleanNamespace(namespace) : namespace;
this._store.namespace = this._namespace;
}
/**
* Get the current TTL.
* @returns {number} The current TTL in milliseconds.
*/
get ttl() {
return this._ttl;
}
/**
* Set the current TTL.
* @param {number} ttl The TTL to set in milliseconds.
*/
set ttl(ttl) {
this.setTtl(ttl);
}
/**
* Get the current serialization adapter. If `undefined`, serialization is not enabled.
* @returns {KeyvSerializationAdapter | undefined} The current serialization adapter.
*/
get serialization() {
return this._serialization;
}
/**
* Set the current serialization adapter. Pass a `KeyvSerializationAdapter` to enable
* custom serialization, or `undefined` to disable serialization entirely.
* @param {KeyvSerializationAdapter | undefined} serialization The serialization adapter to set.
*/
set serialization(serialization) {
this._serialization = serialization === false ? void 0 : serialization;
}
/**
* Get the current sanitize adapter. Sanitization is disabled by default. To
* enable it `sanitize.keys` or `sanitize.namespace` to true or set KeyvSanitizePatterns
* to each.
* @returns {KeyvSanitizeAdapter} The current sanitize adapter.
*/
get sanitize() {
return this._sanitize;
}
/**
* Set the sanitize adapter directly and will run sanitization on namespace, including a
* namespace kept from the storage adapter, and apply the result to the adapter.
* @param {KeyvSanitizeAdapter} value The sanitize adapter to use.
*/
set sanitize(value) {
this._sanitize = value;
if (this._namespace && this._sanitize.enabled) {
this._namespace = this.cleanNamespace(this._namespace);
this._store.namespace = this._namespace;
}
this.sanitizeStoreNamespace();
}
/**
* Get the stats. This is just for this instance
* @returns {KeyvStats} The current stats.
*/
get stats() {
return this._stats;
}
/**
* Set the stats. When setting a new instance it will unsubscribe the old listeners
* and subscribe the new instance.
* @param {KeyvStats} stats The stats instance to set.
*/
set stats(stats) {
this._stats.unsubscribe();
this._stats = stats;
this._stats.subscribe(this);
}
/**
* Get whether Keyv checks expiry at its own layer on get/getMany/has/hasMany.
* When true (default), expired entries are filtered (and deleted) by Keyv regardless of
* the adapter. Set to false to trust the storage adapter to handle expiry on its own.
* @returns {boolean} `true` if Keyv checks expiry at its layer.
*/
get checkExpired() {
return this._checkExpired;
}
/**
* Resolves a store to a fully-compliant KeyvStorageAdapter:
* 1. If the store declares the v6 `capabilities.expires` contract, use it directly (this takes
* precedence over structural detection, so a full adapter whose async methods aren't written
* with the `async` keyword is not mis-bridged).
* 2. If the store implements the full async storage interface (but doesn't declare `expires`),
* treat it as a legacy relative-`ttl` adapter and wrap it in KeyvBridgeAdapter.
* 3. If the store is map-like (get/set/delete/has that aren't native `async` functions), wrap it
* in KeyvMemoryAdapter. A store other than a Map is first asked once, through `has`, whether
* it returns promises; if it does, wrap it in KeyvBridgeAdapter instead.
* 4. If the store has async get/set/delete/clear, wrap it in KeyvBridgeAdapter.
* 5. Otherwise, emit an error and fall back to a default in-memory KeyvMemoryAdapter.
*
* NOTE: this is used for internal but provided public for custom adapter testing
* @param {unknown} store The store to resolve.
* @returns {KeyvStorageAdapter} A fully-compliant storage adapter.
*/
resolveStore(store) {
if (store?.capabilities?.expires === true) return store;
const cap = detectKeyvStorage(store);
if (cap.store === "keyvStorage") return new KeyvBridgeAdapter(store);
if (cap.store === "mapLike") {
if (!(store instanceof Map) && returnsPromise(store)) return new KeyvBridgeAdapter(store);
return new KeyvMemoryAdapter(store);
}
if (cap.store === "asyncMap") return new KeyvBridgeAdapter(store);
this.emit("error", /* @__PURE__ */ new Error("Could not use the provided storage adapter, falling back to KeyvMemoryAdapter with Map"));
return new KeyvMemoryAdapter(/* @__PURE__ */ new Map());
}
/**
* Sets the storage adapter by resolving it via {@link resolveStore}, then wires up
* error forwarding and namespace propagation. Keyv's namespace, when set, is applied to the
* adapter. Without one, the adapter keeps the namespace it was configured with.
* @param {KeyvStorageAdapter | Map<any, any> | any} store The storage adapter to set.
*/
setStore(store) {
this._store = this.resolveStore(store);
if (typeof this._store.on === "function") this._store.on("error", (error) => this.emit("error", error));
if (this._namespace !== void 0) this._store.namespace = this._namespace;
else this.sanitizeStoreNamespace();
}
/**
* Sets the TTL, treating zero and negative values as undefined (no TTL).
* @param {number | undefined} ttl The TTL to set in milliseconds.
*/
setTtl(ttl) {
if (typeof ttl === "number" && ttl <= 0) {
this._ttl = void 0;
return;
}
this._ttl = ttl;
}
async get(key) {
if (Array.isArray(key)) return this.getMany(key);
key = this._sanitize.enabled ? this._sanitize.cleanKey(key) : key;
if (key === "") return;
await this.hookWithDeprecated("before:get", { key });
let rawData;
try {
rawData = await this._store.get(key);
} catch (error) {
this.emit("error", error);
this.emitTelemetry("stat:error", key);
await this.hookWithDeprecated("after:get", {
key,
value: void 0
});
return;
}
let data;
if (this._checkExpired) [data] = await this.decodeWithExpire(key, rawData);
else data = rawData === void 0 || rawData === null ? void 0 : typeof rawData === "string" ? await this.decode(rawData) : rawData;
if (data === void 0) {
await this.hookWithDeprecated("after:get", {
key,
value: void 0
});
this.emitTelemetry("stat:miss", key);
return;
}
await this.hookWithDeprecated("after:get", {
key,
value: data
});
this.emitTelemetry("stat:hit", key);
return data.value;
}
/**
* Reads many keys from the store, preferring its native `getMany` and falling back to parallel
* single `get`s when an adapter does not implement it. A directly-used v6 adapter is not
* structurally required to provide `getMany` (the bridge and memory adapters always do), so
* this keeps `getMany`/`getManyRaw` working regardless of the resolved adapter.
* @param keys - the keys to read
* @returns the raw store results in the same order as `keys`
*/
async storeGetMany(keys) {
if (typeof this._store.getMany === "function") return this._store.getMany(keys);
return Promise.all(keys.map(async (key) => this._store.get(key)));
}
/**
* Runs a batch operation without the keys or entries whose key is empty, then returns its results
* in the original order with `fallback` for each empty key. As with the single-key methods, an
* empty key never reaches hooks, telemetry or the store.
* @param items - the keys or entries of the batch
* @param fallback - the result for an item whose key is empty
* @param run - runs the batch operation on the items whose key is not empty
* @returns one result per item, in the same order as `items`
*/
async skipEmptyKeys(items, fallback, run) {
const isEmpty = (item) => (typeof item === "string" ? item : item.key) === "";
const kept = items.filter((item) => !isEmpty(item));
if (kept.length === items.length) return run(items);
const results = kept.length > 0 ? await run(kept) : [];
let next = 0;
return items.map((item) => isEmpty(item) ? fallback : results[next++]);
}
/**
* Get many values for an array of keys.
* @param {string[]} keys the keys to get
* @returns {Promise<Array<Value | undefined>>} an array of values in the same order as the
* keys, with `undefined` for keys that do not exist or are expired.
*/
async getMany(keys) {
keys = this._sanitize.enabled ? this._sanitize.cleanKeys(keys) : keys;
return this.skipEmptyKeys(keys, void 0, async (keys) => {
await this.hookWithDeprecated("before:getMany", { keys });
let rawData;
try {
rawData = await this.storeGetMany(keys);
} catch (error) {
this.emit("error", error);
this.emitTelemetry("stat:error", keys);
const failed = keys.map(() => void 0);
await this.hookWithDeprecated("after:getMany", failed);
return failed;
}
let deserialized;
if (this._checkExpired) deserialized = await this.decodeWithExpire(keys, rawData);
else deserialized = await Promise.all(rawData.map(async (row) => {
if (row === void 0 || row === null) return;
return typeof row === "string" ? this.decode(row) : row;
}));
const result = deserialized.map((row) => row !== void 0 ? row.value : void 0);
await this.hookWithDeprecated("after:getMany", result);
for (let i = 0; i < result.length; i++) if (result[i] === void 0) this.emitTelemetry("stat:miss", keys[i]);
else this.emitTelemetry("stat:hit", keys[i]);
return result;
});
}
/**
* Get the raw value of a key. This is the replacement for setting raw to true in the get() method.
* @param {string} key the key to get
* @returns {Promise<KeyvStorageGetResult<Value>>} will return a KeyvStorageGetResult<Value> or undefined
* if the key does not exist or is expired.
*/
async getRaw(key) {
key = this._sanitize.enabled ? this._sanitize.cleanKey(key) : key;
if (key === "") return;
await this.hookWithDeprecated("before:getRaw", { key });
let rawData;
try {
rawData = await this._store.get(key);
} catch (error) {
this.emit("error", error);
this.emitTelemetry("stat:error", key);
await this.hookWithDeprecated("after:getRaw", {
key,
value: void 0
});
return;
}
let data;
if (this._checkExpired) [data] = await this.decodeWithExpire(key, rawData);
else data = rawData === void 0 || rawData === null ? void 0 : typeof rawData === "string" ? await this.decode(rawData) : rawData;
if (data === void 0) {
await this.hookWithDeprecated("after:getRaw", {
key,
value: void 0
});
this.emitTelemetry("stat:miss", key);
return;
}
this.emitTelemetry("stat:hit", key);
await this.hookWithDeprecated("after:getRaw", {
key,
value: data
});
return data;
}
/**
* Get the raw values of many keys. This is the replacement for setting raw to true in the getMany() method.
* @param {string[]} keys the keys to get
* @returns {Promise<Array<KeyvStorageGetResult<Value>>>} will return an array of KeyvStorageGetResult<Value> or undefined if the key does not exist or is expired.
*/
async getManyRaw(keys) {
/* v8 ignore next -- @preserve */
keys = this._sanitize.enabled ? this._sanitize.cleanKeys(keys) : keys;
return this.skipEmptyKeys(keys, void 0, async (keys) => {
await this.hookWithDeprecated("before:getManyRaw", { keys });
if (keys.length === 0) {
const result = [];
await this.hookWithDeprecated("after:getManyRaw", {
keys,
values: result
});
return result;
}
let rawData;
try {
rawData = await this.storeGetMany(keys);
} catch (error) {
this.emit("error", error);
this.emitTelemetry("stat:error", keys);
const failed = keys.map(() => void 0);
await this.hookWithDeprecated("after:getManyRaw", {
keys,
values: failed
});
return failed;
}
let result;
if (this._checkExpired) result = await this.decodeWithExpire(keys, rawData);
else result = await Promise.all(rawData.map(async (row) => {
if (row === void 0 || row === null) return;
return typeof row === "string" ? this.decode(row) : row;
}));
for (let i = 0; i < result.length; i++) if (result[i] === void 0) this.emitTelemetry("stat:miss", keys[i]);
else this.emitTelemetry("stat:hit", keys[i]);
await this.hookWithDeprecated("after:getManyRaw", {
keys,
values: result
});
return result;
});
}
/**
* Set an item to the store
* @param {string} key the key to use
* @param {Value} value the value of the key
* @param {number} [ttl] time to live in milliseconds. Overrides the instance-level `ttl`.
* @returns {Promise<boolean>} `true` if it was set successfully, `false` on failure.
*/
async set(key, value, ttl) {
key = this._sanitize.enabled ? this._sanitize.cleanKey(key) : key;
if (key === "") return false;
const data = {
key,
value,
ttl
};
await this.hookWithDeprecated("before:set", data);
data.ttl = resolveTtl(data.ttl, this._ttl);
const expires = calculateExpires(data.ttl);
if (typeof data.value === "symbol") {
this.emit("error", "symbol cannot be serialized");
this.emitTelemetry("stat:error", key);
return false;
}
const formattedValue = {
value: data.value,
expires
};
let result = true;
let encodedValue = formattedValue;
try {
encodedValue = await this.encode(formattedValue);
result = await this._store.set(data.key, encodedValue, expires);
} catch (error) {
result = false;
this.emit("error", error);
this.emitTelemetry("stat:error", key);
}
await this.hookWithDeprecated("after:set", {
key,
value: encodedValue,
ttl
});
if (result) this.emitTelemetry("stat:set", key);
return result;
}
/**
* Set many items to the store
* @param {Array<KeyvEntry<Value>>} entries the entries to set
* @returns {Promise<boolean[]>} an array of booleans, one per entry: `true` if set successfully, `false` on failure.
*/
async setMany(entries) {
entries = entries.map((e) => ({
...e,
key: this._sanitize.enabled ? this._sanitize.cleanKey(e.key) : e.key
}));
return this.skipEmptyKeys(entries, false, async (entries) => {
const data = { entries };
await this.hookWithDeprecated("before:setMany", data);
entries = data.entries;
let results = [];
try {
const serializedEntries = await Promise.all(entries.map(async ({ key, value, ttl }) => {
ttl = resolveTtl(ttl, this._ttl);
/* v8 ignore next -- @preserve */
const expires = calculateExpires(ttl);
/* v8 ignore next -- @preserve */
if (typeof value === "symbol") {
this.emit("error", "symbol cannot be serialized");
this.emitTelemetry("stat:error", key);
throw new Error("symbol cannot be serialized");
}
const formattedValue = {
value,
expires
};
return {
key,
value: await this.encode(formattedValue),
expires
};
}));
const storeResult = await this._store.setMany(serializedEntries);
/* v8 ignore next -- @preserve */
results = Array.isArray(storeResult) ? storeResult : entries.map(() => true);
this.emitTelemetry("stat:set", entries.map((e) => e.key));
} catch (error) {
this.emit("error", error);
this.emitTelemetry("stat:error", entries.map((e) => e.key));
results = entries.map(() => false);
}
await this.hookWithDeprecated("after:setMany", {
entries,
values: results
});
return results;
});
}
/**
* Set a raw value to the store without wrapping or serialization. This is the write-side counterpart to getRaw().
* The value should be a KeyvValue object with { value, expires? }. If you need TTL-based expiration,
* set `expires` on the value directly (e.g. `{ value: 'bar', expires: Date.now() + 60000 }`).
* The store-level expiry is derived automatically from `value.expires`, rounded up to a whole millisecond
* (a non-finite `expires` means no store-level expiry).
* @param {string} key the key to set
* @param {KeyvValue<Value>} value the raw value envelope to store
* @returns {Promise<boolean>} `true` if it was set successfully, `false` on failure.
*/
async setRaw(key, value) {
key = this._sanitize.enabled ? this._sanitize.cleanKey(key) : key;
if (key === "") return false;
const data = {
key,
value
};
await this.hookWithDeprecated("before:setRaw", data);
const expires = toStoreExpires(data.value.expires);
const ttl = ttlFromExpires(expires);
let result = true;
try {
const encodedValue = await this.encode(data.value);
const storeResult = await this._store.set(data.key, encodedValue, expires);
if (typeof storeResult === "boolean") result = storeResult;
} catch (error) {
result = false;
this.emit("error", error);
this.emitTelemetry("stat:error", key);
}
await this.hookWithDeprecated("after:setRaw", {
key,
value: data.value,
ttl
});
if (result) this.emitTelemetry("stat:set", key);
return result;
}
/**
* Set many raw values to the store without wrapping or serialization. This is the write-side counterpart to getManyRaw().
* Each entry's value should be a KeyvValue object with { value, expires? }. If you need TTL-based expiration,
* set `expires` on each value directly. The store-level expiry is derived automatically from `value.expires`,
* rounded up to a whole millisecond (a non-finite `expires` means no store-level expiry).
* @param {KeyvEntry<KeyvValue<Value>>[]} entries the raw entries to set
* @returns {Promise<boolean[]>} an array of booleans, one per entry: `true` if set successfully, `false` on failure.
*/
async setManyRaw(entries) {
entries = entries.map((e) => ({
...e,
/* v8 ignore next -- @preserve */
key: this._sanitize.enabled ? this._sanitize.cleanKey(e.key) : e.key
}));
return this.skipEmptyKeys(entries, false, async (entries) => {
let results = [];
await this.hookWithDeprecated("before:setManyRaw", { entries });
try {
const rawEntries = await Promise.all(entries.map(async ({ key, value }) => {
return {
key,
value: await this.encode(value),
expires: toStoreExpires(value.expires)
};
}));
const storeResult = await this._store.setMany(rawEntries);
results = Array.isArray(storeResult) ? storeResult : entries.map(() => true);
this.emitTelemetry("stat:set", entries.map((e) => e.key));
} catch (error) {
this.emit("error", error);
this.emitTelemetry("stat:error", entries.map((e) => e.key));
results = entries.map(() => false);
}
await this.hookWithDeprecated("after:setManyRaw", {
entries,
results
});
return results;
});
}
async delete(key) {
if (Array.isArray(key)) return this.deleteMany(key);
key = this._sanitize.enabled ? this._sanitize.cleanKey(key) : key;
if (key === "") return false;
await this.hookWithDeprecated("before:delete", { key });
let result = true;
try {
result = await this._store.delete(key);
} catch (error) {
result = false;
this.emit("error", error);
this.emitTelemetry("stat:error", key);
}
await this.hookWithDeprecated("after:delete", {
key,
value: result
});
this.emitTelemetry("stat:delete", key);
return result;
}
/**
* Delete many items from the store
* @param {string[]} keys the keys to be deleted
* @returns {Promise<boolean[]>} an array of booleans indicating success for each key.
*/
async deleteMany(keys) {
/* v8 ignore next -- @preserve */
keys = this._sanitize.enabled ? this._sanitize.cleanKeys(keys) : keys;
return this.skipEmptyKeys(keys, false, async (keys) => {
await this.hookWithDeprecated("before:deleteMany", { keys });
await this.hookWithDeprecated("before:delete", { key: keys });
let results;
try {
results = await this._store.deleteMany(keys);
this.emitTelemetry("stat:delete", keys);
} catch (error) {
this.emit("error", error);
this.emitTelemetry("stat:error", keys);
results = keys.map(() => false);
}
await this.hookWithDeprecated("after:deleteMany", {
keys,
values: results
});
await this.hookWithDeprecated("after:delete", {
key: keys,
value: results
});
return results;
});
}
async has(key) {
if (Array.isArray(key)) return this.hasMany(key);
key = this._sanitize.enabled ? this._sanitize.cleanKey(key) : key;
if (key === "") return false;
await this.hookWithDeprecated("before:has", { key });
let result = false;
try {
if (this._checkExpired) {
const rawData = await this._store.get(key);
if (rawData !== void 0 && rawData !== null) {
const [data] = await this.decodeWithExpire(key, rawData);
result = data !== void 0;
}
} else result = await this._store.has(key);
} catch (error) {
this.emit("error", error);
this.emitTelemetry("stat:error", key);
}
await this.hookWithDeprecated("after:has", {
key,
value: result
});
return result;
}
/**
* Check if many keys exist
* @param {string[]} keys the keys to check
* @returns {Promise<boolean[]>} an array of booleans in the same order as the keys, `true` if the key exists.
*/
async hasMany(keys) {
keys = this._sanitize.enabled ? this._sanitize.cleanKeys(keys) : keys;
return this.skipEmptyKeys(keys, false, async (keys) => {
await this.hookWithDeprecated("before:hasMany", { keys });
let results = [];
try {
if (this._checkExpired) {
const rawData = await this.storeGetMany(keys);
results = (await this.decodeWithExpire(keys, rawData)).map((row) => row !== void 0);
} else results = await this._store.hasMany(keys);
} catch (error) {
this.emit("error", error);
this.emitTelemetry("stat:error", keys);
results = keys.map(() => false);
}
await this.hookWithDeprecated("after:hasMany", {
keys,
values: results
});
return results;
});
}
/**
* Clear the store. If a namespace is set only entries in that namespace are removed. When the
* store can't limit the removal to the namespace, nothing is removed and `error` is emitted.
* Emits a `clear` event.
* @returns {Promise<void>} resolves once the entries have been cleared.
*/
async clear() {
this.emit("clear");
await this.hook("before:clear", { namespace: this.namespace });
try {
await this._store.clear();
} catch (error) {
this.emit("error", error);
this.emitTelemetry("stat:error");
}
await this.hook("after:clear", { namespace: this.namespace });
}
/**
* Will disconnect the store. This is only available if the store has a disconnect method
* @returns {Promise<void>}
*/
async disconnect() {
this.emit("disconnect");
await this.hook("before:disconnect", { namespace: this.namespace });
try {
if (this._store.disconnect) await this._store.disconnect();
} catch (error) {
this.emit("error", error);
}
await this.hook("after:disconnect", { namespace: this.namespace });
}
/**
* Iterate over all key-value pairs in the store. Automatically deserializes values,
* filters out expired entries, and deletes them from the store.
* @returns {AsyncGenerator<Array<string | unknown>, void>} An async generator yielding `[key, value]` pairs.
*/
async *iterator() {
/* v8 ignore next 3 -- @preserve */
if (this._store.iterator === void 0) return;
try {
for await (const [key, raw] of this._store.iterator()) {
const data = await this.decode(raw);
if (this._checkExpired && data && isDataExpired(data)) {
await this.delete(key);
continue;
}
yield [key, data?.value];
}
} catch (error) {
this.emit("error", error);
this.emitTelemetry("stat:error");
}
}
/**
* Encodes a value for storage. Pipeline: serialize → compress → encrypt.
* If serialization is not configured, returns the data as-is, skipping compression. With an
* encryption adapter set, it throws instead, since the value can't be encrypted and storing it
* unencrypted would defeat the adapter.
* @param {KeyvValue<T>} data The value envelope to encode.
* @returns {Promise<unknown>} The encoded value.
* @throws {Error} If an encryption adapter is set while serialization is disabled.
*/
async encode(data) {
if (!this._serialization) {
if (this._encryption?.encrypt) throw new Error("Encryption needs serialization, which is disabled, so Keyv won't store the value unencrypted. Remove `serialization: false` or the encryption adapter.");
return data;
}
let result = await this._serialization.stringify(data);
if (this._compression?.compress) result = await this._compression.compress(result);
if (this._encryption?.encrypt) result = await this._encryption.encrypt(result);
return result;
}
/**
* Decodes a stored value. Pipeline: decrypt → decompress → deserialize (reverse of encode).
* If serialization is not configured, returns the data as a KeyvValue or undefined for strings.
* @param {unknown} data The raw data from the store.
* @returns {Promise<KeyvValue<T> | undefined>} The decoded value envelope, or undefined on failure.
*/
async decode(data) {
if (data === void 0 || data === null) return;
if (!this._serialization) return typeof data === "string" ? void 0 : data;
try {
let result = data;
if (this._encryption?.decrypt) result = await this._encryption.decrypt(result);
if (this._compression?.decompress) result = await this._compression.decompress(result);
if (typeof result === "string") return await this._serialization.parse(result);
return result;
} catch (error) {
this.emit("error", error);
return;
}
}
/**
* Deserializes raw data from the store, checks for expiry, and deletes expired keys.
* Accepts a single key/value or arrays. Returns an array of decoded KeyvValue objects
* (undefined for missing or expired entries).
* @param {string | string[]} keys the key(s) to process
* @param {unknown | unknown[]} rawData the raw data from the store
* @returns {Promise<Array<KeyvValue<Value> | undefined>>} decoded values with expired entries removed
*/
async decodeWithExpire(keys, rawData) {
const keyArray = Array.isArray(keys) ? keys : [keys];
const dataArray = Array.isArray(rawData) ? rawData : [rawData];
const results = [];
for (const row of dataArray) {
if (row === void 0 || row === null) {
results.push(void 0);
continue;
}
const deserialized = typeof row === "string" ? await this.decode(row) : row;
if (deserialized === void 0 || deserialized === null) {
results.push(void 0);
continue;
}
results.push(deserialized);
}
await deleteExpiredKeys(keyArray, results, this);
return results;
}
/**
* Fires a hook under its new name and also under the deprecated alias (if any),
* so that integrations still subscribing to the old PRE_/POST_ names keep working.
*/
async hookWithDeprecated(event, ...args) {
await this.hook(event, ...args);
const deprecated = deprecatedHookAliases.get(event);
if (deprecated && this.getHooks(deprecated)?.length) await this.hook(deprecated, ...args);
}
/**
* Emit a telemetry event for cache operations.
* @param {KeyvEvents} event the telemetry event type
* @param {string | string[]} [key] the cache key or keys (emits one event per key)
*/
emitTelemetry(event, key) {
if (key === void 0) {
this.emit(event, {
event: event.replace("stat:", ""),
namespace: this.namespace,
timestamp: Date.now()
});
return;
}
const keys = Array.isArray(key) ? key : [key];
for (const k of keys) this.emit(event, {
event: event.replace("stat:", ""),
key: k,
namespace: this.namespace,
timestamp: Date.now()
});
}
/**
* Merges the overloaded constructor arguments into a single KeyvOptions object.
*/
static resolveOptions(store, options) {
options ??= {};
store ??= {};
const merged = { ...options };
if (store && store.get) merged.store = store;
else Object.assign(merged, store);
return merged;
}
/**
* Initializes the serialization adapter from options.
*/
initSerialization(options) {
if (options.serialization === false) this._serialization = void 0;
else this._serialization = options.serialization ?? new KeyvJsonSerializer();
}
/**
* Initializes the sanitization handler from options.
*/
initSanitize(options) {
const sanitize = new KeyvSanitize();
if (options.sanitize) sanitize.updateOptions(options.sanitize);
this._sanitize = sanitize;
}
/**
* Initializes the stats manager from options.
*/
initStats(options) {
this._stats = new KeyvStats({
emitter: this,
enabled: options.stats ?? false
});
}
/**
* Initializes the namespace, applying sanitization if enabled.
*/
initNamespace(namespace) {
this._namespace = namespace;
if (this._namespace && this._sanitize.enabled) this._namespace = this.cleanNamespace(this._namespace);
}
/**
* Sanitizes a namespace. When sanitizing leaves nothing, it returns {@link emptiedNamespace} so
* the instance keeps a namespace of its own.
*/
cleanNamespace(namespace) {
return this._sanitize.cleanNamespace(namespace) || emptiedNamespace;
}
/**
* Sanitizes the namespace kept from the storage adapter when Keyv has none of its own.
*/
sanitizeStoreNamespace() {
if (this._namespace === void 0 && this._store.namespace && this._sanitize.enabled) this._store.namespace = this.cleanNamespace(this._store.namespace);
}
};
//#endregion
//#region src/adapters/memory.ts
/**
* An in-memory storage adapter for Keyv that wraps any Map-like object.
*
* This class provides a unified interface for using various Map-like stores
* with Keyv, handling namespace prefixing, TTL-based expiration, and batch operations.
*
* @example
* ```typescript
* // Using with a standard Map
* const store = new KeyvMemoryAdapter(new Map(), { namespace: 'cache' });
*
* // Using with a custom store
* const customStore = new KeyvMemoryAdapter(myCustomMapLikeStore, {
* namespace: 'tenant-123',
* namespaceSeparator: '::'
* });
* ```
*/
var KeyvMemoryAdapter = class extends Hookified {
_store;
_namespace;
_namespaceSeparator = "::";
_capabilities;
/**
* Creates a new KeyvMemoryAdapter instance.
* @param store - The underlying Map or Map-like object to use for storage
* @param options - Configuration options for the store
*/
constructor(store, options) {
super({ throwOnHookError: false });
this._store = store;
this._capabilities = detectKeyvStorage(store);
if (options?.namespaceSeparator !== void 0) this._namespaceSeparator = options.namespaceSeparator;
if (options?.namespace) this._namespace = options?.namespace;
}
/**
* Gets the capabilities of the underlying store, with `expires: true` to declare that
* this adapter accepts an absolute `expires` timestamp (the v6 storage contract).
*/
get capabilities() {
return {
...this._capabilities,
expires: true
};
}
/**
* Gets the underlying store instance.
*/
get store() {
return this._store;
}
/**
* Sets the underlying store instance.
*/
set store(store) {
this._store = store;
}
/**
* Gets the separator between the namespace and the key. Defaults to `::`.
*/
get namespaceSeparator() {
return this._namespaceSeparator;
}
/**
* Sets the separator between the namespace and the key.
*/
set namespaceSeparator(separator) {
this._namespaceSeparator = separator;
}
/**
* Gets the current namespace.
*/
get namespace() {
return this._namespace;
}
/**
* Sets the namespace.
*/
set namespace(namespace) {
this._namespace = namespace;
}
/**
* Creates a prefixed key by combining the namespace and key with the separator.
* @param key - The base key
* @param namespace - Optional namespace to prefix the key with
* @returns The prefixed key if namespace is provided, otherwise the original key
*/
getKeyPrefix(key, namespace) {
if (namespace) return `${namespace}${this._namespaceSeparator}${key}`;
return key;
}
/**
* Parses a prefixed key to extract the namespace and original key.
* @param key - The prefixed key to parse
* @returns An object containing the namespace (if present) and the original key
*/
getKeyPrefixData(key) {
if (this._namespace && key.startsWith(`${this._namespace}${this._namespaceSeparator}`)) return {
namespace: this._namespace,
key: key.slice(this._namespace.length + this._namespaceSeparator.length)
};
return { key };
}
/**
* Retrieves a value from the store by key.
* Automatically handles namespace prefixing and TTL expiration.
* @param key - The key to retrieve
* @returns The stored data, or undefined if not found or expired
*/
async get(key) {
const keyPrefix = this.getKeyPrefix(key, this._namespace);
const entry = this._store.get(keyPrefix);
if (entry === void 0 || entry === null) return;
if (entry.expires !== void 0 && Date.now() > entry.expires) {
this._store.delete(keyPrefix);
return;
}
return entry.value;
}
/**
* Stores a value in the store with an optional absolute expiry.
* @param key - The key to store the value under
* @param value - The value to store
* @param expires - Optional absolute expiry as Unix ms since epoch
* @returns Always returns true indicating success
*/
async set(key, value, expires) {
const keyPrefix = this.getKeyPrefix(key, this._namespace);
const entry = {
value,
expires: typeof expires === "number" ? expires : void 0
};
this._store.set(keyPrefix, entry, ttlFromExpires(expires));
return true;
}
/**
* Stores multiple entries in the store at once.
* @param entries - Array of entries containing key, value, and optional absolute `expires`
*/
async setMany(entries) {
const results = [];
for (const entry of entries) {
const keyPrefix = this.getKeyPrefix(entry.key, this._namespace);
const memEntry = {
value: entry.value,
expires: typeof entry.expires === "number" ? entry.expires : void 0
};
this._store.set(keyPrefix, memEntry, ttlFromExpires(entry.expires));
results.push(true);
}
return results;
}
/**
* Deletes a value from the store by key.
* @param key - The key to delete
* @returns True if the key was deleted, false otherwise
*/
async delete(key) {
const keyPrefix = this.getKeyPrefix(key, this._namespace);
return this._store.delete(keyPrefix);
}
/**
* Clears entries from the store. With no namespace, the whole store is cleared. With a
* namespace, only that namespace's entries are removed, found through the store's `keys()`.
* A store without `keys()` can't tell namespaces apart, so this throws rather than call the
* store's `clear()` and delete every namespace's entries.
* @throws {Error} If a namespace is set and the store has no `keys()`.
*/
async clear() {
if (!this._namespace) {
this._store.clear();
return;
}
if (typeof this._store.keys !== "function") throw new Error(`Can't clear namespace "${this._namespace}": the store has no keys() to find that namespace's keys, and its clear() would delete every namespace. Use a store with keys(), such as a Map, or clear it without a namespace.`);
const prefix = `${this._namespace}${this._namespaceSeparator}`;
const keysToDelete = [];
for (const key of this._store.keys()) if (key.startsWith(prefix)) keysToDelete.push(key);
for (const key of keysToDelete) this._store.delete(key);
}
/**
* Checks if a key exists in the store and is not expired.
* @param key - The key to check
* @returns True if the key exists and is not expired, false otherwise
*/
async has(key) {
const keyPrefix = this.getKeyPrefix(key, this._namespace);
const entry = this._store.get(keyPrefix);
if (entry === void 0 || entry === null) return false;
if (entry.expires !== void 0 && Date.now() > entry.expires) {
this._store.delete(keyPrefix);
return false;
}
return true;
}
/**
* Checks if multiple keys exist in the store and are not expired.
* @param keys - Array of keys to check
* @returns Array of booleans indicating existence for each key
*/
async hasMany(keys) {
const results = [];
for (const key of keys) results.push(await this.has(key));
return results;
}
/**
* Retrieves multiple values from the store by their keys.
* @param keys - Array of keys to retrieve
* @returns Array of stored data in the same order as the input keys
*/
async getMany(keys) {
const values = [];
for (const key of keys) {
const keyPrefix = this.getKeyPrefix(key, this._namespace);
const entry = this._store.get(keyPrefix);
if (entry === void 0 || entry === null) {
values.push(void 0);
continue;
}
if (entry.expires !== void 0 && Date.now() > entry.expires) {
this._store.delete(keyPrefix);
values.push(void 0);
continue;
}
values.push(entry.value);
}
return values;
}
/**
* Deletes multiple keys from the store at once.
* @param keys - Array of keys to delete
* @returns Array of booleans indicating success for each key
*/
async deleteMany(keys) {
const results = [];
for (const key of keys) try {
const keyPrefix = this.getKeyPrefix(key, this._namespace);
const existed = this._store.has(keyPrefix);
this._store.delete(keyPrefix);
results.push(existed);
} catch (error) {
this.emit("error", error);
results.push(false);
}
return results;
}
/**
* Creates an async iterator for iterating over store entries.
* If the underlying store does not support iteration, returns an empty generator.
* @returns {AsyncGenerator<Array<string | Awaited<Value> | undefined>, void>} An async generator yielding [key, value] pairs
*/
async *iterator() {
if (typeof this._store.entries !== "function") return;
const namespace = this._namespace;
for (const [key, raw] of this._store.entries()) {
if (namespace) {
if (!key.startsWith(`${namespace}${this._namespaceSeparator}`)) continue;
}
const entry = raw;
if (entry?.expires !== void 0 && Date.now() > entry.expires) {
this._store.delete(key);
continue;
}
yield [namespace ? key.slice(namespace.length + this._namespaceSeparator.length) : key, entry?.value];
}
}
/**
* No-op disconnect for in-memory stores.
*/
async disconnect() {}
};
/**
* Creates a Keyv instance with a memory adapter optimized for in-memory storage.
*
* This factory function configures Keyv to bypass serialization/deserialization
* and key prefixing, resulting in faster performance for in-memory use cases
* where data doesn't need to be persisted or transmitted.
*
* @param store - The underlying Map or Map-like object to use for storage
* @param options - Configuration options for the memory adapter
* @returns A configured Keyv instance with optimized settings for in-memory storage
*
* @example
* ```typescript
* // Create a simple in-memory cache
* const cache = createKeyv(new Map());
* await cache.set('user:1', { name: 'John' });
*
* // Create with namespace for multi-tenant scenarios
* const tenantCache = createKeyv(new Map(), {
* namespace: 'tenant-123',
* namespaceSeparator: '::'
* });
* ```
*/
function createKeyv(store, options) {
const { namespace, ...adapterOptions } = options ?? {};
return new Keyv({
store: new KeyvMemoryAdapter(store, adapterOptions),
serialization: false,
namespace
});
}
//#endregion
export { Keyv, Keyv as default, KeyvBridgeAdapter, KeyvEvents, KeyvHooks, KeyvJsonSerializer, KeyvMemoryAdapter, KeyvSanitize, KeyvStats, createKeyv, detectKeyv, detectKeyvCompression, detectKeyvEncryption, detectKeyvSerialization, detectKeyvStorage, jsonSerializer, keyvStorageCapability };