@cacheable/node-cache
Version:
Simple and Maintained fast NodeJS internal caching
561 lines (557 loc) • 17.1 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
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 __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
NodeCache: () => NodeCache,
NodeCacheErrors: () => NodeCacheErrors,
NodeCacheStore: () => NodeCacheStore,
default: () => index_default
});
module.exports = __toCommonJS(index_exports);
var import_cacheable2 = require("cacheable");
var import_hookified2 = require("hookified");
// src/store.ts
var import_cacheable = require("cacheable");
var import_keyv = require("keyv");
var import_hookified = require("hookified");
var NodeCacheStore = class extends import_hookified.Hookified {
_maxKeys = 0;
_cache = new import_cacheable.Cacheable({ primary: new import_keyv.Keyv({ store: new import_cacheable.CacheableMemory() }) });
constructor(options) {
super();
if (options) {
const cacheOptions = {
ttl: options.ttl,
primary: options.primary,
secondary: options.secondary,
stats: options.stats ?? true
};
this._cache = new import_cacheable.Cacheable(cacheOptions);
if (options.maxKeys) {
this._maxKeys = options.maxKeys;
}
}
this._cache.on("error", (error) => {
this.emit("error", error);
});
}
/**
* Cacheable instance.
* @returns {Cacheable}
* @readonly
*/
get cache() {
return this._cache;
}
/**
* Time to live in milliseconds.
* @returns {number | string | undefined}
* @readonly
*/
get ttl() {
return this._cache.ttl;
}
/**
* Time to live in milliseconds.
* @param {number | string | undefined} ttl
*/
set ttl(ttl) {
this._cache.ttl = ttl;
}
/**
* Primary cache store.
* @returns {Keyv}
* @readonly
*/
get primary() {
return this._cache.primary;
}
/**
* Primary cache store.
* @param {Keyv} primary
*/
set primary(primary) {
this._cache.primary = primary;
}
/**
* Secondary cache store. Learn more about the secondary cache store in the
* [cacheable](https://github.com/jaredwray/cacheable/tree/main/packages/cacheable#storage-tiering-and-caching) documentation.
* @returns {Keyv | undefined}
*/
get secondary() {
return this._cache.secondary;
}
/**
* Secondary cache store. Learn more about the secondary cache store in the
* [cacheable](https://github.com/jaredwray/cacheable/tree/main/packages/cacheable#storage-tiering-and-caching) documentation.
* @param {Keyv | undefined} secondary
*/
set secondary(secondary) {
this._cache.secondary = secondary;
}
/**
* Maximum number of keys to store in the cache. if this is set to a value greater than 0,
* the cache will keep track of the number of keys and will not store more than the specified number of keys.
* @returns {number}
* @readonly
*/
get maxKeys() {
return this._maxKeys;
}
/**
* Maximum number of keys to store in the cache. if this is set to a value greater than 0,
* the cache will keep track of the number of keys and will not store more than the specified number of keys.
* @param {number} maxKeys
*/
set maxKeys(maxKeys) {
this._maxKeys = maxKeys;
if (this._maxKeys > 0) {
this._cache.stats.enabled = true;
}
}
/**
* Set a key/value pair in the cache.
* @param {string | number} key
* @param {any} value
* @param {number} [ttl]
* @returns {boolean}
*/
async set(key, value, ttl) {
if (this._maxKeys > 0) {
if (this._cache.stats.count >= this._maxKeys) {
return false;
}
}
await this._cache.set(key.toString(), value, ttl);
return true;
}
/**
* Set multiple key/value pairs in the cache.
* @param {NodeCacheItem[]} list
* @returns {void}
*/
async mset(list) {
const items = new Array();
for (const item of list) {
items.push({ key: item.key.toString(), value: item.value, ttl: item.ttl });
}
await this._cache.setMany(items);
}
/**
* Get a value from the cache.
* @param {string | number} key
* @returns {any | undefined}
*/
async get(key) {
return this._cache.get(key.toString());
}
/**
* Get multiple values from the cache.
* @param {Array<string | number>} keys
* @returns {Record<string, any | undefined>}
*/
async mget(keys) {
const result = {};
for (const key of keys) {
result[key.toString()] = await this._cache.get(key.toString());
}
return result;
}
/**
* Delete a key from the cache.
* @param {string | number} key
* @returns {boolean}
*/
async del(key) {
return this._cache.delete(key.toString());
}
/**
* Delete multiple keys from the cache.
* @param {Array<string | number>} keys
* @returns {boolean}
*/
async mdel(keys) {
return this._cache.deleteMany(keys.map((key) => key.toString()));
}
/**
* Clear the cache.
* @returns {void}
*/
async clear() {
return this._cache.clear();
}
/**
* Check if a key exists in the cache.
* @param {string | number} key
* @returns {boolean}
*/
async setTtl(key, ttl) {
const item = await this._cache.get(key.toString());
if (item) {
await this._cache.set(key.toString(), item, ttl);
return true;
}
return false;
}
/**
* Check if a key exists in the cache. If it does exist it will get the value and delete the item from the cache.
* @param {string | number} key
* @returns {any | undefined}
*/
async take(key) {
return this._cache.take(key.toString());
}
/**
* Disconnect from the cache.
* @returns {void}
*/
async disconnect() {
await this._cache.disconnect();
}
};
// src/index.ts
var NodeCacheErrors = /* @__PURE__ */ ((NodeCacheErrors2) => {
NodeCacheErrors2["ECACHEFULL"] = "Cache max keys amount exceeded";
NodeCacheErrors2["EKEYTYPE"] = "The key argument has to be of type `string` or `number`. Found: `__key`";
NodeCacheErrors2["EKEYSTYPE"] = "The keys argument has to be an array.";
NodeCacheErrors2["ETTLTYPE"] = "The ttl argument has to be a number or a string for shorthand ttl.";
return NodeCacheErrors2;
})(NodeCacheErrors || {});
var NodeCache = class extends import_hookified2.Hookified {
options = {
// eslint-disable-next-line @typescript-eslint/naming-convention
stdTTL: 0,
checkperiod: 600,
useClones: true,
deleteOnExpire: true,
maxKeys: -1
};
store = /* @__PURE__ */ new Map();
_stats = new import_cacheable2.CacheableStats({ enabled: true });
_cacheable = new import_cacheable2.CacheableMemory();
intervalId = 0;
constructor(options) {
super();
if (options) {
this.options = { ...this.options, ...options };
}
this.startInterval();
}
/**
* Sets a key value pair. It is possible to define a ttl (in seconds). Returns true on success.
* @param {string | number} key - it will convert the key to a string
* @param {any} value
* @param {number | string} [ttl] - this is in seconds and undefined will use the default ttl
* @returns {boolean}
*/
set(key, value, ttl) {
if (typeof key !== "string" && typeof key !== "number") {
throw this.createError("The key argument has to be of type `string` or `number`. Found: `__key`" /* EKEYTYPE */, key);
}
if (ttl && typeof ttl !== "number" && typeof ttl !== "string") {
throw this.createError("The ttl argument has to be a number or a string for shorthand ttl." /* ETTLTYPE */, this.formatKey(key));
}
const keyValue = this.formatKey(key);
let ttlValue = 0;
if (this.options.stdTTL) {
ttlValue = this.getExpirationTimestamp(this.options.stdTTL);
}
if (ttl) {
ttlValue = this.getExpirationTimestamp(ttl);
}
let expirationTimestamp = 0;
if (ttlValue && ttlValue > 0) {
expirationTimestamp = ttlValue;
}
if (this.options.maxKeys) {
const { maxKeys } = this.options;
if (maxKeys > -1 && this.store.size >= maxKeys) {
throw this.createError("Cache max keys amount exceeded" /* ECACHEFULL */, this.formatKey(key));
}
}
this.store.set(keyValue, { key: keyValue, value, ttl: expirationTimestamp });
this.emit("set", keyValue, value, ttlValue);
this._stats.incrementKSize(keyValue);
this._stats.incrementVSize(value);
this._stats.setCount(this.store.size);
return true;
}
/**
* Sets multiple key val pairs. It is possible to define a ttl (seconds). Returns true on success.
* @param {NodeCacheItem[]} data an array of key value pairs with optional ttl
* @returns {boolean}
*/
mset(data) {
if (!Array.isArray(data)) {
throw this.createError("The keys argument has to be an array." /* EKEYSTYPE */);
}
for (const item of data) {
this.set(item.key, item.value, item.ttl);
}
return true;
}
/**
* Gets a saved value from the cache. Returns a undefined if not found or expired. If the value was found it returns the value.
* @param {string | number} key if the key is a number it will convert it to a string
* @returns {T} the value or undefined
*/
get(key) {
const result = this.store.get(this.formatKey(key));
if (result) {
if (result.ttl > 0) {
if (result.ttl < Date.now()) {
if (this.options.deleteOnExpire) {
this.del(key);
}
this._stats.incrementMisses();
this.emit("expired", this.formatKey(key), result.value);
return void 0;
}
this._stats.incrementHits();
if (this.options.useClones) {
return this._cacheable.clone(result.value);
}
return result.value;
}
this._stats.incrementHits();
if (this.options.useClones) {
return this._cacheable.clone(result.value);
}
return result.value;
}
this._stats.incrementMisses();
return void 0;
}
/**
* Gets multiple saved values from the cache. Returns an empty object {} if not found or expired.
* If the value was found it returns an object with the key value pair.
* @param {Array<string | number} keys an array of keys
* @returns {Record<string, T | undefined>} an object with the key as a property and the value as the value
*/
mget(keys) {
const result = {};
for (const key of keys) {
const value = this.get(key);
if (value) {
result[this.formatKey(key)] = value;
}
}
return result;
}
/**
* Get the cached value and remove the key from the cache. Equivalent to calling get(key) + del(key).
* Useful for implementing single use mechanism such as OTP, where once a value is read it will become obsolete.
* @param {string | number} key
* @returns {T | undefined} the value or undefined
*/
take(key) {
const result = this.get(key);
if (result) {
this.del(key);
if (this.options.useClones) {
return this._cacheable.clone(result);
}
return result;
}
return void 0;
}
/**
* Delete a key. Returns the number of deleted entries. A delete will never fail.
* @param {string | number | Array<string | number>} key if the key is a number it will convert it to a string. if an array is passed it will delete all keys in the array.
* @returns {number} if it was successful it will return the count that was deleted
*/
del(key) {
if (Array.isArray(key)) {
return this.mdel(key);
}
const result = this.store.get(this.formatKey(key));
if (result) {
const keyValue = this.formatKey(key);
this.store.delete(keyValue);
this.emit("del", keyValue, result.value);
this._stats.decreaseKSize(keyValue);
this._stats.decreaseVSize(result.value);
this._stats.setCount(this.store.size);
return 1;
}
return 0;
}
/**
* Delete all keys in Array that exist. Returns the number of deleted entries.
* @param {Array<string | number>} keys an array of keys
* @returns {number} the count of deleted keys
*/
mdel(keys) {
let result = 0;
for (const key of keys) {
result += this.del(key);
}
return result;
}
/**
* Redefine the ttl of a key. Returns true if the key has been found and changed.
* Otherwise returns false. If the ttl-argument isn't passed the default-TTL will be used.
* @param {string | number} key if the key is a number it will convert it to a string
* @param {number | string} [ttl] the ttl in seconds if number, or a shorthand string like '1h' for 1 hour
* @returns {boolean} true if the key has been found and changed. Otherwise returns false.
*/
ttl(key, ttl) {
const result = this.store.get(this.formatKey(key));
if (result) {
const ttlValue = ttl ?? this.options.stdTTL;
result.ttl = this.getExpirationTimestamp(ttlValue);
this.store.set(this.formatKey(key), result);
return true;
}
return false;
}
/**
* Receive the ttl of a key.
* @param {string | number} key if the key is a number it will convert it to a string
* @returns {number | undefined} 0 if this key has no ttl, undefined if this key is not in the cache,
* a timestamp in ms representing the time at which this key will expire
*/
getTtl(key) {
const result = this.store.get(this.formatKey(key));
if (result) {
if (result.ttl === 0) {
return 0;
}
return result.ttl;
}
return void 0;
}
/**
* Returns an array of all existing keys. [ "all", "my", "keys", "foo", "bar" ]
* @returns {string[]} an array of all keys
*/
keys() {
const result = [];
for (const key of this.store.keys()) {
result.push(key);
}
return result;
}
/**
* Returns boolean indicating if the key is cached.
* @param {string | number} key if the key is a number it will convert it to a string
* @returns {boolean} true if the key is cached
*/
has(key) {
return this.store.has(this.formatKey(key));
}
/**
* Gets the stats of the cache
* @returns {NodeCacheStats} the stats of the cache
*/
getStats() {
const stats = {
keys: this._stats.count,
hits: this._stats.hits,
misses: this._stats.misses,
ksize: this._stats.ksize,
vsize: this._stats.vsize
};
return stats;
}
/**
* Flush the whole data.
* @returns {void}
*/
flushAll() {
this.store.clear();
this.flushStats();
this.emit("flush");
}
/**
* Flush the stats.
* @returns {void}
*/
flushStats() {
this._stats = new import_cacheable2.CacheableStats({ enabled: true });
this.emit("flush_stats");
}
/**
* Close the cache. This will clear the interval timeout which is set on check period option.
* @returns {void}
*/
close() {
this.stopInterval();
}
/**
* Get the interval id
* @returns {number | NodeJS.Timeout} the interval id
*/
getIntervalId() {
return this.intervalId;
}
formatKey(key) {
return key.toString();
}
getExpirationTimestamp(ttlInSeconds) {
if (typeof ttlInSeconds === "string") {
return (0, import_cacheable2.shorthandToTime)(ttlInSeconds);
}
const currentTimestamp = Date.now();
const ttlInMilliseconds = ttlInSeconds * 1e3;
const expirationTimestamp = currentTimestamp + ttlInMilliseconds;
return expirationTimestamp;
}
startInterval() {
if (this.options.checkperiod && this.options.checkperiod > 0) {
const checkPeriodinSeconds = this.options.checkperiod * 1e3;
this.intervalId = setInterval(() => {
this.checkData();
}, checkPeriodinSeconds).unref();
return;
}
this.intervalId = 0;
}
checkData() {
for (const [key, value] of this.store.entries()) {
if (value.ttl > 0 && value.ttl < Date.now()) {
if (this.options.deleteOnExpire) {
this.del(key);
}
this.emit("expired", this.formatKey(key), value.value);
}
}
}
stopInterval() {
if (this.intervalId !== 0) {
clearInterval(this.intervalId);
this.intervalId = 0;
}
}
createError(errorCode, key) {
let error = errorCode;
if (key) {
error = error.replace("__key", key);
}
return new Error(error);
}
};
var index_default = NodeCache;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
NodeCache,
NodeCacheErrors,
NodeCacheStore
});