cachly
Version:
Type-safe, production-ready in-memory cache system for Node.js and TypeScript with advanced features.
694 lines • 24.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Cachly = void 0;
const compression_1 = require("./utils/compression");
const CircuitBreaker_1 = require("./utils/CircuitBreaker");
const Partitioning_1 = require("./utils/Partitioning");
const Monitoring_1 = require("./utils/Monitoring");
class Cachly {
constructor(config = {}) {
this.items = new Map();
this.eventEmitter = new (require('events').EventEmitter)();
this.circuitBreakerTrips = 0;
// New features
this.tags = new Map();
this.groups = new Map();
this.config = {
maxItems: 1000,
maxMemory: 0,
defaultTtl: 0,
staleWhileRevalidate: false,
log: false,
namespace: 'main',
persistence: 'none',
evictionPolicy: 'lru',
compression: { enabled: false, algorithm: 'gzip', threshold: 1024 },
circuitBreaker: { enabled: false, failureThreshold: 5, recoveryTimeout: 30000 },
partitioning: { enabled: false, strategy: 'hash', partitions: 4 },
monitoring: { enabled: false, metrics: [] },
distributed: { enabled: false, nodes: [], replication: false, consistency: 'eventual', partitionStrategy: 'consistent-hashing' },
onHit: () => { },
onMiss: () => { },
...config,
};
this.cacheStats = {
hits: 0,
misses: 0,
evictions: 0,
memoryUsage: 0,
keyCount: 0,
totalAccesses: 0,
hitRate: 0,
missRate: 0,
avgLoadTime: 0,
compressionRatio: 0,
};
// Initialize advanced features
if (this.config.compression.enabled) {
this.compressionUtil = new compression_1.CompressionUtil();
}
if (this.config.circuitBreaker.enabled) {
this.circuitBreaker = new CircuitBreaker_1.CircuitBreaker(this.config.circuitBreaker);
}
if (this.config.partitioning.enabled) {
this.partitioningUtil = new Partitioning_1.PartitioningUtil(this.config.partitioning);
}
if (this.config.monitoring.enabled) {
this.monitoringUtil = new Monitoring_1.MonitoringUtil(this.config.monitoring);
}
if (this.config.persistence !== 'none') {
this.persistence = this.config.persistence;
this.initializePersistence();
}
if (this.config.defaultTtl > 0) {
this.startCleanupInterval();
}
if (typeof config['onHit'] === 'function') {
this.hitHook = config['onHit'];
}
if (typeof config['onMiss'] === 'function') {
this.missHook = config['onMiss'];
}
}
async set(key, value, options = {}) {
const startTime = Date.now();
const now = Date.now();
const ttl = options.ttl ?? this.config.defaultTtl;
const expiresAt = ttl && ttl > 0 ? now + ttl : undefined;
const staleAt = options.staleTtl ? now + options.staleTtl : undefined;
let processedValue = value;
let originalSize = 0;
let compressedSize = 0;
let compressed = false;
// Apply compression if enabled
if (this.compressionUtil && this.config.compression.enabled) {
try {
const compressionResult = await compression_1.CompressionUtil.compress(value, this.config.compression);
processedValue = compressionResult.data;
originalSize = compressionResult.originalSize;
compressedSize = compressionResult.compressedSize;
compressed = originalSize !== compressedSize;
if (compressed) {
this.eventEmitter.emit('compress', key, originalSize, compressedSize);
}
}
catch (error) {
if (this.config.log) {
console.error(`[Cachly] Compression failed for ${key}:`, error);
}
}
}
const item = {
value: processedValue,
createdAt: now,
expiresAt,
staleAt,
dependsOn: new Set(options.dependsOn || []),
dependents: new Set(),
tags: new Set(options.tags || []),
accessCount: 0,
lastAccessed: now,
compressed,
originalSize,
compressedSize,
};
this.items.set(key, item);
this.updateDependencies(key, item.dependsOn);
this.updateTags(key, item.tags || new Set());
this.updateStats();
this.eventEmitter.emit('set', key, value);
if (this.config.log) {
console.log(`[Cachly] Set: ${key}`);
}
this.evictIfNeeded();
// Record load time for monitoring
if (this.monitoringUtil) {
this.monitoringUtil.recordLoadTime(Date.now() - startTime);
}
}
async get(key) {
const item = this.items.get(key);
if (!item) {
this.cacheStats.misses++;
this.cacheStats.totalAccesses++;
this.eventEmitter.emit('miss', key);
if (this.missHook)
this.missHook(key);
return undefined;
}
if (this.isExpired(item)) {
this.delete(key);
this.cacheStats.misses++;
this.cacheStats.totalAccesses++;
this.eventEmitter.emit('miss', key);
if (this.missHook)
this.missHook(key);
return undefined;
}
item.accessCount++;
item.lastAccessed = Date.now();
this.cacheStats.hits++;
this.cacheStats.totalAccesses++;
this.eventEmitter.emit('hit', key);
if (this.hitHook)
this.hitHook(key);
// Emit partition hit event
if (this.partitioningUtil) {
const partition = this.partitioningUtil.getPartition(key);
this.eventEmitter.emit('partitionHit', partition, key);
}
let result = item.value;
// Decompress if needed
if (item.compressed && this.compressionUtil) {
try {
result = await compression_1.CompressionUtil.decompress(item.value, this.config.compression.algorithm);
}
catch (error) {
if (this.config.log) {
console.error(`[Cachly] Decompression failed for ${key}:`, error);
}
return undefined;
}
}
return result;
}
async getOrCompute(key, loader, options = {}) {
const cached = await this.get(key);
if (cached !== undefined) {
return cached;
}
// Use circuit breaker if enabled
if (this.circuitBreaker) {
return this.circuitBreaker.execute(key, async () => {
const value = await loader();
await this.set(key, value, options);
return value;
}, this.config.circuitBreaker.fallback);
}
const value = await loader();
await this.set(key, value, options);
return value;
}
async getOrComputeWithStale(key, loader, options = {}) {
let item = this.items.get(key);
if (item && !this.isExpired(item)) {
item.accessCount++;
item.lastAccessed = Date.now();
this.cacheStats.hits++;
this.cacheStats.totalAccesses++;
this.eventEmitter.emit('hit', key);
let result = item.value;
if (item.compressed && this.compressionUtil) {
result = await compression_1.CompressionUtil.decompress(item.value, this.config.compression.algorithm);
}
return result;
}
if (item && this.isStale(item) && this.config.staleWhileRevalidate) {
this.cacheStats.hits++;
this.cacheStats.totalAccesses++;
this.eventEmitter.emit('hit', key);
if (options.__testAwaitBackground) {
try {
const value = await loader();
const staleTtl = options.staleTtl ?? 0;
const ttl = options.ttl ?? this.config.defaultTtl;
await this.set(key, value, { ...options, staleTtl, ttl });
// Update item reference for test determinism
item = this.items.get(key);
}
catch (error) {
if (this.config.log) {
console.error(`[Cachly] Background refresh failed for ${key}:`, error);
}
}
}
else {
setTimeout(async () => {
try {
const value = await loader();
const staleTtl = options.staleTtl ?? 0;
const ttl = options.ttl ?? this.config.defaultTtl;
await this.set(key, value, { ...options, staleTtl, ttl });
const updated = this.items.get(key);
if (updated) {
updated.staleAt = staleTtl ? Date.now() + staleTtl : undefined;
updated.expiresAt = ttl && ttl > 0 ? Date.now() + ttl : undefined;
}
}
catch (error) {
if (this.config.log) {
console.error(`[Cachly] Background refresh failed for ${key}:`, error);
}
}
}, 0);
}
let result = item ? item.value : undefined;
if (item && item.compressed && this.compressionUtil) {
result = await compression_1.CompressionUtil.decompress(item.value, this.config.compression.algorithm);
}
return result;
}
const value = await loader();
await this.set(key, value, options);
return value;
}
has(key) {
const item = this.items.get(key);
return item !== undefined && !this.isExpired(item);
}
delete(key) {
const item = this.items.get(key);
if (item) {
this.removeDependencies(key, item.dependsOn);
this.removeTags(key, item.tags);
this.invalidateDependentsOnDelete(key);
this.items.delete(key);
this.updateStats();
this.eventEmitter.emit('delete', key);
}
}
clear() {
this.items.clear();
this.updateStats();
this.eventEmitter.emit('clear');
}
stats() {
const stats = { ...this.cacheStats };
// Calculate derived stats
if (stats.totalAccesses > 0) {
stats.hitRate = (stats.hits / stats.totalAccesses) * 100;
stats.missRate = (stats.misses / stats.totalAccesses) * 100;
}
// Calculate compression ratio
let totalOriginalSize = 0;
let totalCompressedSize = 0;
let compressedItems = 0;
for (const item of this.items.values()) {
if (item.compressed && item.originalSize && item.compressedSize) {
totalOriginalSize += item.originalSize;
totalCompressedSize += item.compressedSize;
compressedItems++;
}
}
if (totalOriginalSize > 0) {
stats.compressionRatio = compression_1.CompressionUtil.calculateCompressionRatio(totalOriginalSize, totalCompressedSize);
}
// Add monitoring metrics
if (this.monitoringUtil) {
const monitoringMetrics = this.monitoringUtil.getMetrics();
stats.avgLoadTime = monitoringMetrics.avgLoadTime;
}
return stats;
}
async warm(items) {
const promises = items.map(async (item) => {
try {
const value = await item.loader();
await this.set(item.key, value, {
ttl: item.ttl,
staleTtl: item.staleTtl,
dependsOn: item.dependsOn,
});
}
catch (error) {
if (this.config.log) {
console.error(`[Cachly] Warmup failed for ${item.key}:`, error);
}
}
});
await Promise.all(promises);
}
async warmProgressive(items) {
const priorities = ['high', 'medium', 'low'];
for (const priority of priorities) {
const priorityItems = items.filter(item => item.priority === priority);
await this.warm(priorityItems);
}
}
async warmWithDependencies(items) {
// Sort items by dependency depth
const sortedItems = this.sortByDependencies(items);
await this.warm(sortedItems);
}
sortByDependencies(items) {
const dependencyMap = new Map();
const visited = new Set();
const sorted = [];
// Build dependency map
for (const item of items) {
dependencyMap.set(item.key, item);
}
const visit = (item) => {
if (visited.has(item.key))
return;
visited.add(item.key);
// Visit dependencies first
for (const dep of item.deps) {
const depItem = dependencyMap.get(dep);
if (depItem) {
visit(depItem);
}
}
sorted.push(item);
};
for (const item of items) {
visit(item);
}
return sorted;
}
health() {
if (this.monitoringUtil) {
return this.monitoringUtil.health();
}
return {
status: 'healthy',
issues: [],
lastCheck: Date.now(),
uptime: Date.now() - this.startTime || 0,
};
}
metrics() {
const stats = this.stats();
const baseMetrics = this.monitoringUtil?.getMetrics() || {
hitRate: 0,
missRate: 0,
avgLoadTime: 0,
memoryEfficiency: 0,
compressionRatio: 0,
circuitBreakerTrips: 0,
partitionDistribution: {},
};
return {
...baseMetrics,
hitRate: stats.hitRate,
missRate: stats.missRate,
avgLoadTime: stats.avgLoadTime,
memoryEfficiency: (stats.memoryUsage / (this.config.maxMemory || 1)) * 100,
compressionRatio: stats.compressionRatio,
circuitBreakerTrips: this.circuitBreakerTrips,
partitionDistribution: this.partitioningUtil?.getPartitionDistribution() || {},
};
}
getCircuitBreakerState() {
return this.circuitBreaker?.getState();
}
getPartitionInfo(partitionId) {
return this.partitioningUtil?.getPartitionInfo(partitionId);
}
getAllPartitions() {
return this.partitioningUtil?.getAllPartitions() || [];
}
isBalanced() {
return this.partitioningUtil?.isBalanced() || true;
}
isExpired(item) {
return item.expiresAt !== undefined && Date.now() > item.expiresAt;
}
isStale(item) {
return item.staleAt !== undefined && Date.now() > item.staleAt;
}
updateDependencies(key, dependencies) {
for (const dep of dependencies) {
const depItem = this.items.get(dep);
if (depItem) {
depItem.dependents.add(key);
}
}
}
removeDependencies(key, dependencies) {
for (const dep of dependencies) {
const depItem = this.items.get(dep);
if (depItem) {
depItem.dependents.delete(key);
}
}
}
invalidateDependents(key) {
const item = this.items.get(key);
if (!item)
return;
const dependents = new Set(item.dependents);
for (const dependent of dependents) {
this.delete(dependent);
}
}
invalidateDependentsOnDelete(key) {
this.invalidateDependents(key);
}
evictIfNeeded() {
while (this.config.maxItems && this.items.size > this.config.maxItems) {
this.evict();
}
}
evict() {
if (this.config.evictionPolicy === 'lru') {
this.evictLRU();
}
else if (this.config.evictionPolicy === 'ttl') {
this.evictTTL();
}
else if (this.config.evictionPolicy === 'lfu') {
this.evictLFU();
}
}
evictLRU() {
let oldestKey = null;
let oldestTime = Date.now();
for (const [key, item] of this.items) {
if (item.lastAccessed < oldestTime) {
oldestTime = item.lastAccessed;
oldestKey = key;
}
}
if (oldestKey) {
this.items.delete(oldestKey);
this.cacheStats.evictions++;
this.eventEmitter.emit('evict', oldestKey, 'lru');
this.updateStats();
}
}
evictTTL() {
const now = Date.now();
for (const [key, item] of this.items) {
if (item.expiresAt && now > item.expiresAt) {
this.items.delete(key);
this.cacheStats.evictions++;
this.eventEmitter.emit('evict', key, 'ttl');
}
}
this.updateStats();
}
evictLFU() {
let leastFrequentKey = null;
let leastFrequentCount = Infinity;
for (const [key, item] of this.items) {
if (item.accessCount < leastFrequentCount) {
leastFrequentCount = item.accessCount;
leastFrequentKey = key;
}
}
if (leastFrequentKey) {
this.items.delete(leastFrequentKey);
this.cacheStats.evictions++;
this.eventEmitter.emit('evict', leastFrequentKey, 'lfu');
this.updateStats();
}
}
updateStats() {
this.cacheStats.keyCount = this.items.size;
this.cacheStats.memoryUsage = this.estimateMemoryUsage();
}
estimateMemoryUsage() {
let total = 0;
for (const [key, item] of this.items) {
total += key.length * 2;
if (item.compressed && item.compressedSize) {
total += item.compressedSize;
}
else {
total += JSON.stringify(item.value).length * 2;
}
total += 100;
}
return total;
}
startCleanupInterval() {
this.cleanupInterval = setInterval(() => {
this.evictTTL();
}, 60000);
}
initializePersistence() {
// Initialize persistence layer if needed
if (this.persistence) {
// Future implementation for persistence initialization
}
}
destroy() {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
}
this.clear();
this.eventEmitter.removeAllListeners();
}
on(event, listener) {
this.eventEmitter.on(event, listener);
return this;
}
off(event, listener) {
this.eventEmitter.off(event, listener);
return this;
}
async invalidateByTag(tag) {
const tagData = this.tags.get(tag);
if (!tagData)
return [];
const affectedKeys = Array.from(tagData.keys);
for (const key of affectedKeys) {
this.delete(key);
}
this.tags.delete(tag);
this.eventEmitter.emit('tagInvalidated', tag, affectedKeys);
return affectedKeys;
}
async invalidateByTags(tags) {
const results = {};
for (const tag of tags) {
results[tag] = await this.invalidateByTag(tag);
}
return results;
}
getKeysByTag(tag) {
const tagData = this.tags.get(tag);
return tagData ? Array.from(tagData.keys) : [];
}
getTagsByKey(key) {
const item = this.items.get(key);
return item?.tags ? Array.from(item.tags) : [];
}
createGroup(name, config = {}) {
const group = {
name,
keys: new Set(),
config,
};
this.groups.set(name, group);
this.eventEmitter.emit('groupCreated', group);
return group;
}
addToGroup(groupName, key) {
const group = this.groups.get(groupName);
if (!group)
return false;
group.keys.add(key);
return true;
}
removeFromGroup(groupName, key) {
const group = this.groups.get(groupName);
if (!group)
return false;
return group.keys.delete(key);
}
getGroupKeys(groupName) {
const group = this.groups.get(groupName);
return group ? Array.from(group.keys) : [];
}
deleteGroup(groupName) {
const group = this.groups.get(groupName);
if (!group)
return false;
for (const key of group.keys) {
this.delete(key);
}
this.groups.delete(groupName);
this.eventEmitter.emit('groupDeleted', groupName);
return true;
}
async bulk(operation) {
const startTime = Date.now();
const results = {
get: {},
set: [],
delete: [],
invalidateByTag: {},
};
for (const key of operation.get) {
results.get[key] = await this.get(key);
}
for (const { key, value, options } of operation.set) {
await this.set(key, value, options);
results.set.push(key);
}
for (const key of operation.delete) {
this.delete(key);
results.delete.push(key);
}
for (const tag of operation.invalidateByTag) {
results.invalidateByTag[tag] = await this.invalidateByTag(tag);
}
const duration = Date.now() - startTime;
this.eventEmitter.emit('bulkOperation', operation, { results, duration });
return results;
}
getKeys(pattern) {
const keys = Array.from(this.items.keys());
if (!pattern)
return keys;
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
return keys.filter(key => regex.test(key));
}
getKeysByPattern(pattern) {
return this.getKeys(pattern);
}
getItemsByPattern(pattern) {
const keys = this.getKeysByPattern(pattern);
return keys.map(key => ({ key, item: this.items.get(key) }));
}
getTopKeys(limit = 10) {
return Array.from(this.items.entries())
.map(([key, item]) => ({ key, accessCount: item.accessCount }))
.sort((a, b) => b.accessCount - a.accessCount)
.slice(0, limit);
}
getLeastUsedKeys(limit = 10) {
return Array.from(this.items.entries())
.map(([key, item]) => ({ key, accessCount: item.accessCount }))
.sort((a, b) => a.accessCount - b.accessCount)
.slice(0, limit);
}
getKeysByAge(limit = 10) {
const now = Date.now();
return Array.from(this.items.entries())
.map(([key, item]) => ({ key, age: now - item.createdAt }))
.sort((a, b) => b.age - a.age)
.slice(0, limit);
}
updateTags(key, tags) {
for (const tag of tags) {
if (!this.tags.has(tag)) {
this.tags.set(tag, { name: tag, keys: new Set(), createdAt: Date.now() });
}
this.tags.get(tag).keys.add(key);
}
}
removeTags(key, tags) {
if (!tags)
return;
for (const tag of tags) {
const tagData = this.tags.get(tag);
if (tagData) {
tagData.keys.delete(key);
if (tagData.keys.size === 0) {
this.tags.delete(tag);
}
}
}
}
setHitHook(hook) {
this.hitHook = hook;
}
setMissHook(hook) {
this.missHook = hook;
}
}
exports.Cachly = Cachly;
//# sourceMappingURL=Cachly.js.map