@keyv/valkey
Version:
Valkey storage adapter for Keyv
883 lines (878 loc) • 26.2 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
createKeyv: () => createKeyv,
default: () => index_default
});
module.exports = __toCommonJS(index_exports);
var import_events = __toESM(require("events"), 1);
var import_iovalkey = __toESM(require("iovalkey"), 1);
// ../serialize/dist/index.js
var import_buffer = require("buffer");
var defaultSerialize = (data) => {
if (data === void 0 || data === null) {
return "null";
}
if (typeof data === "string") {
return JSON.stringify(data.startsWith(":") ? ":" + data : data);
}
if (import_buffer.Buffer.isBuffer(data)) {
return JSON.stringify(":base64:" + data.toString("base64"));
}
if (data?.toJSON) {
data = data.toJSON();
}
if (typeof data === "object") {
let s = "";
const array = Array.isArray(data);
s = array ? "[" : "{";
let first = true;
for (const k in data) {
const ignore = typeof data[k] === "function" || !array && data[k] === void 0;
if (!Object.hasOwn(data, k) || ignore) {
continue;
}
if (!first) {
s += ",";
}
first = false;
if (array) {
s += defaultSerialize(data[k]);
} else if (data[k] !== void 0) {
s += defaultSerialize(k) + ":" + defaultSerialize(data[k]);
}
}
s += array ? "]" : "}";
return s;
}
return JSON.stringify(data);
};
var defaultDeserialize = (data) => JSON.parse(data, (_, value) => {
if (typeof value === "string") {
if (value.startsWith(":base64:")) {
return import_buffer.Buffer.from(value.slice(8), "base64");
}
return value.startsWith(":") ? value.slice(1) : value;
}
return value;
});
// ../keyv/dist/index.js
var EventManager = class {
_eventListeners;
_maxListeners;
constructor() {
this._eventListeners = /* @__PURE__ */ new Map();
this._maxListeners = 100;
}
maxListeners() {
return this._maxListeners;
}
// Add an event listener
addListener(event, listener) {
this.on(event, listener);
}
on(event, listener) {
if (!this._eventListeners.has(event)) {
this._eventListeners.set(event, []);
}
const listeners = this._eventListeners.get(event);
if (listeners) {
if (listeners.length >= this._maxListeners) {
console.warn(`MaxListenersExceededWarning: Possible event memory leak detected. ${listeners.length + 1} ${event} listeners added. Use setMaxListeners() to increase limit.`);
}
listeners.push(listener);
}
return this;
}
// Remove an event listener
removeListener(event, listener) {
this.off(event, listener);
}
off(event, listener) {
const listeners = this._eventListeners.get(event) ?? [];
const index = listeners.indexOf(listener);
if (index !== -1) {
listeners.splice(index, 1);
}
if (listeners.length === 0) {
this._eventListeners.delete(event);
}
}
once(event, listener) {
const onceListener = (...arguments_) => {
listener(...arguments_);
this.off(event, onceListener);
};
this.on(event, onceListener);
}
// Emit an event
emit(event, ...arguments_) {
const listeners = this._eventListeners.get(event);
if (listeners && listeners.length > 0) {
for (const listener of listeners) {
listener(...arguments_);
}
} else if (event === "error") {
if (arguments_[0] instanceof Error) {
throw arguments_[0];
} else {
const error = new CustomError(arguments_[0]);
error.context = arguments_[0];
throw error;
}
}
}
// Get all listeners for a specific event
listeners(event) {
return this._eventListeners.get(event) ?? [];
}
// Remove all listeners for a specific event
removeAllListeners(event) {
if (event) {
this._eventListeners.delete(event);
} else {
this._eventListeners.clear();
}
}
// Set the maximum number of listeners for a single event
setMaxListeners(n) {
this._maxListeners = n;
}
};
var CustomError = class _CustomError extends Error {
context;
constructor(message, context) {
super(message);
this.context = context;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, _CustomError);
}
this.name = this.constructor.name;
}
};
var event_manager_default = EventManager;
var HooksManager = class extends event_manager_default {
_hookHandlers;
constructor() {
super();
this._hookHandlers = /* @__PURE__ */ new Map();
}
// Adds a handler function for a specific event
addHandler(event, handler) {
const eventHandlers = this._hookHandlers.get(event);
if (eventHandlers) {
eventHandlers.push(handler);
} else {
this._hookHandlers.set(event, [handler]);
}
}
// Removes a specific handler function for a specific event
removeHandler(event, handler) {
const eventHandlers = this._hookHandlers.get(event);
if (eventHandlers) {
const index = eventHandlers.indexOf(handler);
if (index !== -1) {
eventHandlers.splice(index, 1);
}
}
}
// Triggers all handlers for a specific event with provided data
trigger(event, data) {
const eventHandlers = this._hookHandlers.get(event);
if (eventHandlers) {
for (const handler of eventHandlers) {
try {
handler(data);
} catch (error) {
this.emit("error", new Error(`Error in hook handler for event "${event}": ${error.message}`));
}
}
}
}
// Provides read-only access to the current handlers
get handlers() {
return new Map(this._hookHandlers);
}
};
var hooks_manager_default = HooksManager;
var StatsManager = class extends event_manager_default {
enabled = true;
hits = 0;
misses = 0;
sets = 0;
deletes = 0;
errors = 0;
constructor(enabled) {
super();
if (enabled !== void 0) {
this.enabled = enabled;
}
this.reset();
}
hit() {
if (this.enabled) {
this.hits++;
}
}
miss() {
if (this.enabled) {
this.misses++;
}
}
set() {
if (this.enabled) {
this.sets++;
}
}
delete() {
if (this.enabled) {
this.deletes++;
}
}
reset() {
this.hits = 0;
this.misses = 0;
this.sets = 0;
this.deletes = 0;
this.errors = 0;
}
};
var stats_manager_default = StatsManager;
var iterableAdapters = [
"sqlite",
"postgres",
"mysql",
"mongo",
"redis",
"valkey",
"etcd"
];
var Keyv = class extends event_manager_default {
opts;
iterator;
hooks = new hooks_manager_default();
stats = new stats_manager_default(false);
/**
* Time to live in milliseconds
*/
_ttl;
/**
* Namespace
*/
_namespace;
/**
* Store
*/
_store = /* @__PURE__ */ new Map();
_serialize = defaultSerialize;
_deserialize = defaultDeserialize;
_compression;
_useKeyPrefix = true;
/**
* Keyv Constructor
* @param {KeyvStoreAdapter | KeyvOptions} store
* @param {Omit<KeyvOptions, 'store'>} [options] if you provide the store you can then provide the Keyv Options
*/
constructor(store, options) {
super();
options ??= {};
store ??= {};
this.opts = {
namespace: "keyv",
serialize: defaultSerialize,
deserialize: defaultDeserialize,
emitErrors: true,
// @ts-expect-error - Map is not a KeyvStoreAdapter
store: /* @__PURE__ */ new Map(),
...options
};
if (store && store.get) {
this.opts.store = store;
} else {
this.opts = {
...this.opts,
...store
};
}
this._store = this.opts.store;
this._compression = this.opts.compression;
this._serialize = this.opts.serialize;
this._deserialize = this.opts.deserialize;
if (this.opts.namespace) {
this._namespace = this.opts.namespace;
}
if (this._store) {
if (!this._isValidStorageAdapter(this._store)) {
throw new Error("Invalid storage adapter");
}
if (typeof this._store.on === "function") {
this._store.on("error", (error) => this.emit("error", error));
}
this._store.namespace = this._namespace;
if (typeof this._store[Symbol.iterator] === "function" && this._store instanceof Map) {
this.iterator = this.generateIterator(this._store);
} else if ("iterator" in this._store && this._store.opts && this._checkIterableAdapter()) {
this.iterator = this.generateIterator(this._store.iterator.bind(this._store));
}
}
if (this.opts.stats) {
this.stats.enabled = this.opts.stats;
}
if (this.opts.ttl) {
this._ttl = this.opts.ttl;
}
if (this.opts.useKeyPrefix !== void 0) {
this._useKeyPrefix = this.opts.useKeyPrefix;
}
}
/**
* Get the current store
*/
get store() {
return this._store;
}
/**
* Set the current store. This will also set the namespace, event error handler, and generate the iterator. If the store is not valid it will throw an error.
* @param {KeyvStoreAdapter | Map<any, any> | any} store the store to set
*/
set store(store) {
if (this._isValidStorageAdapter(store)) {
this._store = store;
this.opts.store = store;
if (typeof store.on === "function") {
store.on("error", (error) => this.emit("error", error));
}
if (this._namespace) {
this._store.namespace = this._namespace;
}
if (typeof store[Symbol.iterator] === "function" && store instanceof Map) {
this.iterator = this.generateIterator(store);
} else if ("iterator" in store && store.opts && this._checkIterableAdapter()) {
this.iterator = this.generateIterator(store.iterator.bind(store));
}
} else {
throw new Error("Invalid storage adapter");
}
}
/**
* Get the current compression function
* @returns {CompressionAdapter} The current compression function
*/
get compression() {
return this._compression;
}
/**
* Set the current compression function
* @param {CompressionAdapter} compress The compression function to set
*/
set compression(compress) {
this._compression = compress;
}
/**
* Get the current namespace.
* @returns {string | undefined} The current namespace.
*/
get namespace() {
return this._namespace;
}
/**
* Set the current namespace.
* @param {string | undefined} namespace The namespace to set.
*/
set namespace(namespace) {
this._namespace = namespace;
this.opts.namespace = namespace;
this._store.namespace = namespace;
if (this.opts.store) {
this.opts.store.namespace = namespace;
}
}
/**
* Get the current TTL.
* @returns {number} The current TTL.
*/
get ttl() {
return this._ttl;
}
/**
* Set the current TTL.
* @param {number} ttl The TTL to set.
*/
set ttl(ttl) {
this.opts.ttl = ttl;
this._ttl = ttl;
}
/**
* Get the current serialize function.
* @returns {Serialize} The current serialize function.
*/
get serialize() {
return this._serialize;
}
/**
* Set the current serialize function.
* @param {Serialize} serialize The serialize function to set.
*/
set serialize(serialize) {
this.opts.serialize = serialize;
this._serialize = serialize;
}
/**
* Get the current deserialize function.
* @returns {Deserialize} The current deserialize function.
*/
get deserialize() {
return this._deserialize;
}
/**
* Set the current deserialize function.
* @param {Deserialize} deserialize The deserialize function to set.
*/
set deserialize(deserialize) {
this.opts.deserialize = deserialize;
this._deserialize = deserialize;
}
/**
* Get the current useKeyPrefix value. This will enable or disable key prefixing.
* @returns {boolean} The current useKeyPrefix value.
* @default true
*/
get useKeyPrefix() {
return this._useKeyPrefix;
}
/**
* Set the current useKeyPrefix value. This will enable or disable key prefixing.
* @param {boolean} value The useKeyPrefix value to set.
*/
set useKeyPrefix(value) {
this._useKeyPrefix = value;
this.opts.useKeyPrefix = value;
}
generateIterator(iterator) {
const function_ = async function* () {
for await (const [key, raw] of typeof iterator === "function" ? iterator(this._store.namespace) : iterator) {
const data = await this.deserializeData(raw);
if (this._useKeyPrefix && this._store.namespace && !key.includes(this._store.namespace)) {
continue;
}
if (typeof data.expires === "number" && Date.now() > data.expires) {
this.delete(key);
continue;
}
yield [this._getKeyUnprefix(key), data.value];
}
};
return function_.bind(this);
}
_checkIterableAdapter() {
return iterableAdapters.includes(this._store.opts.dialect) || iterableAdapters.some((element) => this._store.opts.url.includes(element));
}
_getKeyPrefix(key) {
if (!this._useKeyPrefix) {
return key;
}
if (!this._namespace) {
return key;
}
return `${this._namespace}:${key}`;
}
_getKeyPrefixArray(keys) {
if (!this._useKeyPrefix) {
return keys;
}
if (!this._namespace) {
return keys;
}
return keys.map((key) => `${this._namespace}:${key}`);
}
_getKeyUnprefix(key) {
if (!this._useKeyPrefix) {
return key;
}
return key.split(":").splice(1).join(":");
}
_isValidStorageAdapter(store) {
return store instanceof Map || typeof store.get === "function" && typeof store.set === "function" && typeof store.delete === "function" && typeof store.clear === "function";
}
async get(key, options) {
const { store } = this.opts;
const isArray = Array.isArray(key);
const keyPrefixed = isArray ? this._getKeyPrefixArray(key) : this._getKeyPrefix(key);
const isDataExpired = (data) => typeof data.expires === "number" && Date.now() > data.expires;
if (isArray) {
this.hooks.trigger("preGetMany", { keys: keyPrefixed });
if (store.getMany === void 0) {
const promises = keyPrefixed.map(async (key2) => {
const rawData3 = await store.get(key2);
const deserializedRow = typeof rawData3 === "string" || this.opts.compression ? await this.deserializeData(rawData3) : rawData3;
if (deserializedRow === void 0 || deserializedRow === null) {
return void 0;
}
if (isDataExpired(deserializedRow)) {
await this.delete(key2);
return void 0;
}
return options?.raw ? deserializedRow : deserializedRow.value;
});
const deserializedRows = await Promise.allSettled(promises);
const result2 = deserializedRows.map((row) => row.value);
this.hooks.trigger("postGetMany", result2);
if (result2.length > 0) {
this.stats.hit();
}
return result2;
}
const rawData2 = await store.getMany(keyPrefixed);
const result = [];
for (const index in rawData2) {
let row = rawData2[index];
if (typeof row === "string") {
row = await this.deserializeData(row);
}
if (row === void 0 || row === null) {
result.push(void 0);
continue;
}
if (isDataExpired(row)) {
await this.delete(key[index]);
result.push(void 0);
continue;
}
const value = options?.raw ? row : row.value;
result.push(value);
}
this.hooks.trigger("postGetMany", result);
if (result.length > 0) {
this.stats.hit();
}
return result;
}
this.hooks.trigger("preGet", { key: keyPrefixed });
const rawData = await store.get(keyPrefixed);
const deserializedData = typeof rawData === "string" || this.opts.compression ? await this.deserializeData(rawData) : rawData;
if (deserializedData === void 0 || deserializedData === null) {
this.stats.miss();
return void 0;
}
if (isDataExpired(deserializedData)) {
await this.delete(key);
this.stats.miss();
return void 0;
}
this.hooks.trigger("postGet", { key: keyPrefixed, value: deserializedData });
this.stats.hit();
return options?.raw ? deserializedData : deserializedData.value;
}
/**
* 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
* @returns {boolean} if it sets then it will return a true. On failure will return false.
*/
async set(key, value, ttl) {
this.hooks.trigger("preSet", { key, value, ttl });
const keyPrefixed = this._getKeyPrefix(key);
if (ttl === void 0) {
ttl = this._ttl;
}
if (ttl === 0) {
ttl = void 0;
}
const { store } = this.opts;
const expires = typeof ttl === "number" ? Date.now() + ttl : null;
if (typeof value === "symbol") {
this.emit("error", "symbol cannot be serialized");
}
const formattedValue = { value, expires };
const serializedValue = await this.serializeData(formattedValue);
let result = true;
try {
const value2 = await store.set(keyPrefixed, serializedValue, ttl);
if (typeof value2 === "boolean") {
result = value2;
}
} catch (error) {
result = false;
this.emit("error", error);
}
this.hooks.trigger("postSet", { key: keyPrefixed, value: serializedValue, ttl });
this.stats.set();
return result;
}
/**
* Delete an Entry
* @param {string | string[]} key the key to be deleted. if an array it will delete many items
* @returns {boolean} will return true if item or items are deleted. false if there is an error
*/
async delete(key) {
const { store } = this.opts;
if (Array.isArray(key)) {
const keyPrefixed2 = this._getKeyPrefixArray(key);
this.hooks.trigger("preDelete", { key: keyPrefixed2 });
if (store.deleteMany !== void 0) {
return store.deleteMany(keyPrefixed2);
}
const promises = keyPrefixed2.map(async (key2) => store.delete(key2));
const results = await Promise.allSettled(promises);
const returnResult = results.every((x) => x.value === true);
this.hooks.trigger("postDelete", { key: keyPrefixed2, value: returnResult });
return returnResult;
}
const keyPrefixed = this._getKeyPrefix(key);
this.hooks.trigger("preDelete", { key: keyPrefixed });
let result = true;
try {
const value = await store.delete(keyPrefixed);
if (typeof value === "boolean") {
result = value;
}
} catch (error) {
result = false;
this.emit("error", error);
}
this.hooks.trigger("postDelete", { key: keyPrefixed, value: result });
this.stats.delete();
return result;
}
/**
* Clear the store
* @returns {void}
*/
async clear() {
this.emit("clear");
const { store } = this.opts;
try {
await store.clear();
} catch (error) {
this.emit("error", error);
}
}
/**
* Has a key
* @param {string} key the key to check
* @returns {boolean} will return true if the key exists
*/
async has(key) {
const keyPrefixed = this._getKeyPrefix(key);
const { store } = this.opts;
if (store.has !== void 0 && !(store instanceof Map)) {
return store.has(keyPrefixed);
}
let rawData;
try {
rawData = await store.get(keyPrefixed);
} catch (error) {
this.emit("error", error);
}
if (rawData) {
const data = await this.deserializeData(rawData);
if (data) {
if (data.expires === void 0 || data.expires === null) {
return true;
}
return data.expires > Date.now();
}
}
return false;
}
/**
* Will disconnect the store. This is only available if the store has a disconnect method
* @returns {Promise<void>}
*/
async disconnect() {
const { store } = this.opts;
this.emit("disconnect");
if (typeof store.disconnect === "function") {
return store.disconnect();
}
}
emit(event, ...arguments_) {
if (event === "error" && !this.opts.emitErrors) {
return;
}
super.emit(event, ...arguments_);
}
async serializeData(data) {
if (!this._serialize) {
return data;
}
if (this._compression?.compress) {
return this._serialize({ value: await this._compression.compress(data.value), expires: data.expires });
}
return this._serialize(data);
}
async deserializeData(data) {
if (!this._deserialize) {
return data;
}
if (this._compression?.decompress && typeof data === "string") {
const result = await this._deserialize(data);
return { value: await this._compression.decompress(result?.value), expires: result?.expires };
}
if (typeof data === "string") {
return this._deserialize(data);
}
return void 0;
}
};
// src/index.ts
var createKeyv = (uri, options) => new Keyv({ store: new KeyvValkey(uri, options) });
var KeyvValkey = class extends import_events.default {
ttlSupport = true;
namespace;
opts;
redis;
constructor(uri, options) {
super();
this.opts = {};
this.opts.useRedisSets = true;
this.opts.dialect = "redis";
if (typeof uri !== "string" && uri.options && ("family" in uri.options || uri.isCluster)) {
this.redis = uri;
} else {
options = { ...typeof uri === "string" ? { uri } : uri, ...options };
this.redis = new import_iovalkey.default(options.uri, options);
}
if (options !== void 0 && options.useRedisSets === false) {
this.opts.useRedisSets = false;
}
this.redis.on("error", (error) => this.emit("error", error));
}
_getNamespace() {
return `namespace:${this.namespace}`;
}
_getKeyName = (key) => {
if (!this.opts.useRedisSets) {
return `sets:${this._getNamespace()}:${key}`;
}
return key;
};
async get(key) {
key = this._getKeyName(key);
const value = await this.redis.get(key);
if (value === null) {
return void 0;
}
return value;
}
async getMany(keys) {
keys = keys.map(this._getKeyName);
return this.redis.mget(keys);
}
async set(key, value, ttl) {
if (value === void 0) {
return void 0;
}
key = this._getKeyName(key);
const set = async (redis) => {
if (typeof ttl === "number") {
await redis.set(key, value, "PX", ttl);
} else {
await redis.set(key, value);
}
};
if (this.opts.useRedisSets) {
const trx = await this.redis.multi();
await set(trx);
await trx.sadd(this._getNamespace(), key);
await trx.exec();
} else {
await set(this.redis);
}
}
async delete(key) {
key = this._getKeyName(key);
let items = 0;
const unlink = async (redis) => redis.unlink(key);
if (this.opts.useRedisSets) {
const trx = this.redis.multi();
await unlink(trx);
await trx.srem(this._getNamespace(), key);
const r = await trx.exec();
items = r[0][1];
} else {
items = await unlink(this.redis);
}
return items > 0;
}
async deleteMany(keys) {
const deletePromises = keys.map(async (key) => this.delete(key));
const results = await Promise.allSettled(deletePromises);
return results.every((result) => result.value);
}
async clear() {
if (this.opts.useRedisSets) {
const keys = await this.redis.smembers(this._getNamespace());
if (keys.length > 0) {
await Promise.all([
this.redis.unlink([...keys]),
this.redis.srem(this._getNamespace(), [...keys])
]);
}
} else {
const pattern = `sets:${this._getNamespace()}:*`;
const keys = await this.redis.keys(pattern);
if (keys.length > 0) {
await this.redis.unlink(keys);
}
}
}
async *iterator(namespace) {
const scan = this.redis.scan.bind(this.redis);
const get = this.redis.mget.bind(this.redis);
let cursor = "0";
do {
const [curs, keys] = await scan(cursor, "MATCH", `${namespace}:*`);
cursor = curs;
if (keys.length > 0) {
const values = await get(keys);
for (const [i] of keys.entries()) {
const key = keys[i];
const value = values[i];
yield [key, value];
}
}
} while (cursor !== "0");
}
async has(key) {
const value = await this.redis.exists(key);
return value !== 0;
}
async disconnect() {
return this.redis.disconnect();
}
};
var index_default = KeyvValkey;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
createKeyv
});