@m4x1m1l14n/cache
Version:
Lightweight in-memory isomorphic cache implementation with TTL for browser & Node JS written in TypeScript
270 lines • 9.04 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Cache = void 0;
const helpers_1 = require("../helpers");
const MinHeap_1 = require("./MinHeap");
const detect_node_1 = __importDefault(require("detect-node"));
class Cache {
options;
cache = new Map();
timeoutHandle = null;
expirationHeap = new MinHeap_1.MinHeap();
destroyed = false;
ensureNotDestroyed() {
if (this.destroyed) {
throw new Error('Cache instance has been destroyed');
}
}
constructor(options) {
this.options = {
...{
maxItems: 1000,
resolution: 1000,
// Default timeout is infinity
defaultTTL: Number.POSITIVE_INFINITY,
},
...(options ?? {}),
};
// Note: resolution is now deprecated in favor of dynamic timeout scheduling
// Dynamic timeout scheduling provides better accuracy by scheduling cleanup
// at the exact moment items expire, rather than checking periodically
}
set(key, value, ttl, callback) {
this.ensureNotDestroyed();
const now = (0, helpers_1.getMilliseconds)();
const itemTTL = ttl ?? this.options.defaultTTL;
const wrapped = {
created: now,
value,
ttl: itemTTL,
callback,
};
// Remove any existing entries for this key from the heap
this.expirationHeap.removeByKey(key);
this.cache.set(key, wrapped);
// Add to expiration heap if item has a finite TTL
if (itemTTL !== Number.POSITIVE_INFINITY) {
this.expirationHeap.insert({
expiration: now + itemTTL,
key,
});
}
// Reschedule cleanup since we added a new item
this.scheduleCleanup();
return this;
}
/**
* Returns value by its key
*
* @param key Key of value to get
* @param refresh True to refresh item TTL or false to not
*/
get(key, refresh = false) {
this.ensureNotDestroyed();
const wrapped = this.cache.get(key);
if (!wrapped) {
return undefined;
}
const now = (0, helpers_1.getMilliseconds)();
// Check if item is expired
if (wrapped.ttl !== Number.POSITIVE_INFINITY && now > wrapped.created + wrapped.ttl) {
// Item is expired, remove it
this.cache.delete(key);
this.expirationHeap.removeByKey(key);
if (wrapped.callback) {
wrapped.callback(wrapped.value);
}
return undefined;
}
if (refresh) {
// Remove old expiration entry from heap
this.expirationHeap.removeByKey(key);
wrapped.created = now;
// Add new expiration entry if item has finite TTL
if (wrapped.ttl !== Number.POSITIVE_INFINITY) {
this.expirationHeap.insert({
expiration: now + wrapped.ttl,
key,
});
}
// Reschedule cleanup since we refreshed an item's TTL
this.scheduleCleanup();
}
return wrapped.value;
}
/**
* Takes value by its key from cache.
*
* Value is returned and key is removed from cache.
*
* @param key Key to take
* @returns Value of specified key
*/
take(key) {
const value = this.get(key);
if (value !== undefined) {
this.delete(key);
}
return value;
}
has(key) {
this.ensureNotDestroyed();
const wrapped = this.cache.get(key);
if (!wrapped) {
return false;
}
// Check if item is expired
const now = (0, helpers_1.getMilliseconds)();
if (wrapped.ttl !== Number.POSITIVE_INFINITY && now > wrapped.created + wrapped.ttl) {
// Item is expired, remove it
this.cache.delete(key);
this.expirationHeap.removeByKey(key);
if (wrapped.callback) {
wrapped.callback(wrapped.value);
}
return false;
}
return true;
}
delete(key) {
this.ensureNotDestroyed();
const result = this.cache.delete(key);
// Remove from expiration heap and reschedule cleanup since we removed an item
if (result) {
this.expirationHeap.removeByKey(key);
this.scheduleCleanup();
}
return result;
}
mdelete(keys) {
this.ensureNotDestroyed();
let deletedAny = false;
for (const key of keys) {
if (this.cache.delete(key)) {
this.expirationHeap.removeByKey(key);
deletedAny = true;
}
}
// Reschedule cleanup since we may have removed items
if (deletedAny) {
this.scheduleCleanup();
}
return this;
}
get size() {
this.ensureNotDestroyed();
return this.cache.size;
}
forEach(cb) {
this.ensureNotDestroyed();
this.cache.forEach((value, key) => {
cb(value.value, key);
});
}
cleanup() {
if (this.cache.size === 0) {
return;
}
const now = (0, helpers_1.getMilliseconds)();
// Process expired items from the heap
while (!this.expirationHeap.isEmpty) {
const nextExpiration = this.expirationHeap.peek();
if (!nextExpiration || nextExpiration.expiration > now) {
// No more expired items
break;
}
// Remove the expired entry from heap
this.expirationHeap.extractMin();
// Check if the item still exists in cache and is actually expired
const cached = this.cache.get(nextExpiration.key);
if (cached && now > cached.created + cached.ttl) {
// Item is expired, remove it
this.cache.delete(nextExpiration.key);
if (cached.callback) {
cached.callback(cached.value);
}
}
}
// Schedule the next cleanup based on remaining items
this.scheduleCleanup();
}
/**
* Finds the earliest expiration time among all cached items using the heap
* @returns The earliest expiration timestamp, or null if no items expire
*/
findEarliestExpiration() {
// Clean up any stale entries in the heap first
while (!this.expirationHeap.isEmpty) {
const peek = this.expirationHeap.peek();
const cached = this.cache.get(peek.key);
// If the item doesn't exist in cache or the expiration doesn't match, remove from heap
if (!cached || cached.created + cached.ttl !== peek.expiration) {
this.expirationHeap.extractMin();
continue;
}
// Found a valid entry
return peek.expiration;
}
return null;
}
/**
* Schedules the next cleanup based on the earliest expiration time
*/
scheduleCleanup() {
// Clear any existing timeout
this.clearScheduledCleanup();
const earliestExpiration = this.findEarliestExpiration();
if (earliestExpiration === null) {
// No items to expire
return;
}
const now = (0, helpers_1.getMilliseconds)();
const delay = Math.max(0, earliestExpiration - now);
if (detect_node_1.default) {
this.timeoutHandle = setTimeout(() => this.cleanup(), delay);
this.timeoutHandle.unref();
}
else {
this.timeoutHandle = window.setTimeout(() => this.cleanup(), delay);
}
}
/**
* Clears any scheduled cleanup timeout
*/
clearScheduledCleanup() {
if (this.timeoutHandle !== null) {
if (detect_node_1.default) {
clearTimeout(this.timeoutHandle);
}
else {
window.clearTimeout(this.timeoutHandle);
}
this.timeoutHandle = null;
}
}
destroy(invokeCallbacks = false) {
if (this.destroyed) {
return;
}
this.destroyed = true;
this.flush(invokeCallbacks);
}
flush(invokeCallback = false) {
if (invokeCallback) {
for (const [, value] of this.cache) {
if (value.callback) {
value.callback(value.value);
}
}
}
this.cache.clear();
this.expirationHeap.clear();
// Clear any scheduled cleanup since cache is empty
this.clearScheduledCleanup();
}
}
exports.Cache = Cache;
//# sourceMappingURL=Cache.js.map