another-cache
v0.1.1
Published
A lightweight, flexible cache library for JavaScript that works seamlessly in both Node.js and browser environments
Downloads
23
Maintainers
Readme
Another Cache
A lightweight, flexible cache library for JavaScript that works seamlessly in both Node.js and browser environments.
Features
- ✅ Works in Node.js and browsers
- ✅ TypeScript support with full type safety
- ✅ Configurable size limits (entries or bytes)
- ✅ Two eviction policies: FIFO (default) and LRU (Least Recently Used)
- ✅ TTL (Time To Live) support
- ✅ Automatic cleanup of expired entries
- ✅ Generic type support
- ✅ Rich mutation operations (increment, decrement, append, merge)
- ✅ Batch operations (setMany, getMany, deleteMany)
- ✅ Peek operation (check values without affecting eviction order)
- ✅ Consistent size calculations for accurate eviction decisions
- ✅ Event system for monitoring cache operations
- ✅ Zero dependencies
- ✅ Modular architecture
Installation
npm install another-cacheUsage
Basic Usage
import { Cache } from 'another-cache';
const cache = new Cache<string, number>();
// Set values
cache.set('key1', 100);
cache.set('key2', 200);
// Get values
const value = cache.get('key1'); // 100
// Check if key exists
if (cache.has('key1')) {
console.log('Key exists!');
}
// Delete a value
cache.delete('key1');
// Clear all values
cache.clear();
// Get cache size (number of entries)
const size = cache.size();
// Get cache size in bytes
const bytes = cache.sizeInBytes();⚠️ Note on sizeInBytes(): The byte size calculation is an approximation. It estimates memory usage based on JavaScript value types, but actual memory consumption can vary due to:
- V8 engine internals and object overhead
- Memory alignment and padding
- Garbage collection overhead
- Browser/Node.js implementation differences
Use sizeInBytes() as a guideline for cache size management, not as an exact memory measurement.
Size calculation details:
- The cache uses consistent size calculations for both individual entries and total cache size
- Each entry includes: key size + value size + 48 bytes (Map entry overhead) + 16 bytes (metadata: createdAt, expiresAt, lastAccessed)
- This ensures accurate eviction decisions when using
maxBytes
Note on peek() and LRU: When using LRU eviction policy, peek() does not update the access time, so it won't affect which entries are evicted. Use get() if you want to update the access time for LRU.
With Options
const cache = new Cache<string, any>({
maxEntries: 100, // Maximum number of entries
maxBytes: 2 * 1024 * 1024, // Maximum size in bytes (2MB)
ttl: 3600000, // Time to live in milliseconds (1 hour)
cleanupInterval: 60000, // Cleanup interval in milliseconds (1 minute)
evictionPolicy: 'FIFO', // Eviction policy: 'FIFO' (default) or 'LRU'
autoDeleteAfterUse: false, // Auto-delete after get() (default: false)
});Size Limits
The cache supports two eviction policies: FIFO (First In First Out) and LRU (Least Recently Used). When size limits are reached, entries are automatically removed based on the selected policy.
Eviction Policies
FIFO (Default): Removes the oldest entries (first inserted) when limits are reached. Accessing entries with get() does not affect eviction order.
LRU: Removes the least recently used entries. Accessing entries with get() updates their access time, keeping frequently used entries in cache longer.
// FIFO (default)
const fifoCache = new Cache<string, any>({
maxEntries: 10,
evictionPolicy: 'FIFO', // This is default - but could be added for clarity
});
// LRU
const lruCache = new Cache<string, any>({
maxEntries: 10,
evictionPolicy: 'LRU',
});Entry-based Limit
FIFO Example:
const cache = new Cache<string, any>({ maxEntries: 3, evictionPolicy: 'FIFO' });
cache.set('a', 1);
cache.set('b', 2);
cache.set('c', 3);
cache.get('a'); // Access 'a', but it doesn't change eviction order
cache.set('d', 4); // 'a' is removed (oldest entry)
console.log(cache.get('a')); // undefinedLRU Example:
const cache = new Cache<string, any>({ maxEntries: 3, evictionPolicy: 'LRU' });
cache.set('a', 1);
cache.set('b', 2);
cache.set('c', 3);
cache.get('a'); // Access 'a' - updates its access time
cache.set('d', 4); // 'b' is removed (least recently used, not 'a')
console.log(cache.get('a')); // 1 (still exists)
console.log(cache.get('b')); // undefined (removed)Bytes-based Limit
The cache can be limited by total size in bytes instead of (or in addition to) entry count. This provides more predictable memory usage.
How eviction works with maxBytes:
When you attempt to add an entry that would exceed maxBytes, the cache removes existing entries one by one based on your eviction policy until there's enough space:
- Calculate required space: The cache calculates the size of the new entry (key + value + overhead)
- Check current size: If
currentCacheSize + newEntrySize > maxBytes, eviction starts - Remove entries: Entries are removed one at a time based on eviction policy:
- FIFO: Removes oldest entries (first inserted) until there's space
- LRU: Removes least recently used entries (oldest access time) until there's space
- Entries accessed with
get()have their access time updated - Entries only accessed with
peek()do NOT update access time
- Entries accessed with
- Continue until space available: The process repeats until
currentSize + newEntrySize <= maxBytes - Set new entry: Once there's enough space, the new entry is added
Special case - Entry larger than maxBytes:
If the entry itself is larger than maxBytes, all existing entries are removed and the new entry is set anyway. This allows you to set large entries when needed, effectively clearing the cache.
// Limit cache to 2MB with LRU eviction
const cache = new Cache<string, any>({
maxBytes: 2 * 1024 * 1024,
evictionPolicy: 'LRU',
});
cache.set('largeKey', largeObject);
// When adding entries would exceed 2MB, least recently used entries are removedNote: In JavaScript runtimes, garbage collection is driven by heap memory pressure rather than the number of cached entries. Byte-based cache limits therefore provide more predictable memory behavior and help reduce GC pressure compared to entry-count-based limits, both in Node.js and browser environments.
TTL (Time To Live)
// Cache entries expire after 1 hour
const cache = new Cache<string, string>({ ttl: 3600000 });
cache.set('token', 'abc123');
// ... after 1 hour ...
const token = cache.get('token'); // undefined (expired)Operations
Set Operations
Set operations allow you to add values to the cache. The cache automatically manages size limits by removing entries based on the selected eviction policy (FIFO or LRU) when limits are reached.
set(key, value)
Sets a single value in the cache. If the cache is at capacity, entries are automatically removed based on the eviction policy (FIFO or LRU).
Note: This operation emits a set event. See Events section for details.
Eviction priority and behavior:
The cache uses the following priority when deciding what to remove:
Entry-based limit (
maxEntries):- When adding a new entry would exceed
maxEntries, exactly one entry is removed - FIFO: The oldest entry (first inserted) is removed
- LRU: The least recently used entry (oldest access time) is removed
- When adding a new entry would exceed
Bytes-based limit (
maxBytes):- When adding a new entry would exceed
maxBytes, entries are removed one by one until there's space - FIFO: Oldest entries are removed first (in insertion order)
- LRU: Least recently used entries are removed first (by access time)
- Multiple entries may be removed if needed to make space
- When adding a new entry would exceed
Entry larger than
maxBytes:- If the entry itself is larger than
maxBytes, all existing entries are removed - The new entry is then set anyway (allows setting large entries when needed)
- If the entry itself is larger than
Example 1: Basic usage
const cache = new Cache<string, number>();
// Set a simple value
cache.set('user:123', 42);
const value = cache.get('user:123'); // 42Example 2: Overwriting existing values
const cache = new Cache<string, string>();
// Set initial value
cache.set('status', 'pending');
console.log(cache.get('status')); // 'pending'
// Overwrite with new value
cache.set('status', 'completed');
console.log(cache.get('status')); // 'completed'Example 3: With entry-based limit
const cache = new Cache<string, number>({ maxEntries: 3 });
cache.set('a', 1);
cache.set('b', 2);
cache.set('c', 3);
console.log(cache.size()); // 3
// Adding a 4th entry removes the oldest ('a')
cache.set('d', 4);
console.log(cache.get('a')); // undefined (removed)
console.log(cache.get('d')); // 4Example 4: With bytes-based limit
const cache = new Cache<string, string>({ maxBytes: 1000 });
// Add entries until limit is reached
cache.set('key1', 'x'.repeat(200)); // ~200 bytes
cache.set('key2', 'y'.repeat(200)); // ~200 bytes
cache.set('key3', 'z'.repeat(200)); // ~200 bytes
// Adding more will remove oldest entries
cache.set('key4', 'w'.repeat(300)); // ~300 bytes
// Oldest entries are removed to make spaceExample 5: LRU eviction with maxBytes
const cache = new Cache<string, string>({
maxBytes: 800,
evictionPolicy: 'LRU',
});
// Add three entries (~312 bytes each, total ~936 bytes)
cache.set('key1', 'x'.repeat(200));
cache.set('key2', 'y'.repeat(200));
cache.set('key3', 'z'.repeat(200));
// Access key1 - this updates its access time (makes it more recently used)
cache.get('key1');
// Add new entry (~472 bytes)
// Current cache: ~936 bytes
// New entry: ~472 bytes
// Total needed: ~1408 bytes, but maxBytes is 800
//
// Eviction process:
// 1. Remove key3 (least recently used - never accessed)
// 2. Cache now: ~624 bytes, still not enough
// 3. Remove key2 (now least recently used)
// 4. Cache now: ~312 bytes
// 5. 312 + 472 = 784 bytes <= 800, enough space!
// 6. Add key4
//
// Result: key1 and key4 exist, key2 and key3 were removed
cache.set('key4', 'w'.repeat(300));Example 6: Entry larger than maxBytes
const cache = new Cache<string, string>({ maxBytes: 500 });
cache.set('key1', 'data1');
cache.set('key2', 'data2');
// Set entry larger than maxBytes (4000+ bytes)
// Since entry itself is larger than maxBytes, all existing entries are removed
// and the new entry is set anyway
cache.set('huge', 'x'.repeat(2000));
// Result: Only 'huge' exists, key1 and key2 were removedsetMany(entries)
Sets multiple key-value pairs at once. Useful for bulk operations or initializing the cache with data.
Note: This operation emits a setMany event. See Events section for details.
Example 1: Bulk initialization
const cache = new Cache<string, number>();
// Initialize cache with multiple values at once
cache.setMany([
['user:1', 100],
['user:2', 200],
['user:3', 300],
['user:4', 400],
]);
console.log(cache.size()); // 4
console.log(cache.get('user:2')); // 200Example 2: Updating multiple values
const cache = new Cache<string, { score: number; level: number }>();
// Set initial data
cache.set('player1', { score: 100, level: 1 });
cache.set('player2', { score: 200, level: 2 });
// Update multiple players at once
cache.setMany([
['player1', { score: 150, level: 2 }],
['player2', { score: 250, level: 3 }],
['player3', { score: 50, level: 1 }], // New player
]);
console.log(cache.get('player1')); // { score: 150, level: 2 }
console.log(cache.get('player3')); // { score: 50, level: 1 }Example 3: With size limits
const cache = new Cache<string, number>({ maxEntries: 5 });
// Set 7 entries, but only 5 will remain
cache.setMany([
['a', 1],
['b', 2],
['c', 3],
['d', 4],
['e', 5],
['f', 6], // 'a' is removed
['g', 7], // 'b' is removed
]);
console.log(cache.size()); // 5
console.log(cache.get('a')); // undefined (removed)
console.log(cache.get('g')); // 7Get Operations
Get operations allow you to retrieve values from the cache. The cache automatically handles expired entries by removing them when accessed.
get(key)
Retrieves a single value from the cache. Returns undefined if the key doesn't exist or if the entry has expired.
Note: This operation emits a get event. See Events section for details.
Example 1: Basic retrieval
const cache = new Cache<string, number>();
cache.set('score', 100);
const score = cache.get('score');
console.log(score); // 100Example 2: Handling non-existent keys
const cache = new Cache<string, string>();
// Get a value that doesn't exist
const value = cache.get('nonexistent');
console.log(value); // undefined
// Can be used with optional chaining or default value
const result = cache.get('key') ?? 'default';
console.log(result); // 'default'Example 3: With expired entries
const cache = new Cache<string, string>({ ttl: 1000 }); // 1 second TTL
cache.set('token', 'abc123');
console.log(cache.get('token')); // 'abc123'
// After 1 second
console.log(cache.get('token')); // undefined (expired and removed)Example 4: Retrieving objects
interface User {
id: number;
name: string;
email: string;
}
const cache = new Cache<string, User>();
cache.set('user:123', {
id: 123,
name: 'John Doe',
email: '[email protected]',
});
const user = cache.get('user:123');
if (user) {
console.log(user.name); // 'John Doe'
console.log(user.email); // '[email protected]'
}getMany(keys)
Retrieves multiple values at once. Returns an array where each element corresponds to the value for each key, or undefined if the key doesn't exist or has expired.
Note: This operation emits a getMany event. See Events section for details.
Example 1: Bulk retrieval
const cache = new Cache<string, number>();
cache.set('a', 1);
cache.set('b', 2);
cache.set('c', 3);
// Get multiple values at once
// Only returns existing values (non-existent keys are filtered out)
const values = cache.getMany(['a', 'b', 'c', 'd']);
console.log(values); // [1, 2, 3] (no undefined for 'd')Example 2: Getting subset of existing values
const cache = new Cache<string, string>();
cache.set('user:1', 'Alice');
cache.set('user:3', 'Charlie');
// user:2 doesn't exist
// Get multiple values - only existing ones are returned
const values = cache.getMany(['user:1', 'user:2', 'user:3']);
console.log(values); // ['Alice', 'Charlie'] (user:2 is not included)Example 3: Processing all retrieved values
const cache = new Cache<string, { name: string; score: number }>();
cache.set('player1', { name: 'Alice', score: 100 });
cache.set('player2', { name: 'Bob', score: 200 });
// Get all values - only existing ones are returned
const players = cache.getMany(['player1', 'player2', 'player3']);
// Process all returned values (no need to filter undefined)
players.forEach((player) => {
console.log(`${player.name}: ${player.score}`);
});
// Output:
// Alice: 100
// Bob: 200has(key)
Checks if a key exists in the cache and is not expired. Returns true if the key exists and is valid, false otherwise.
Example 1: Conditional operations
const cache = new Cache<string, number>();
cache.set('count', 42);
if (cache.has('count')) {
const value = cache.get('count');
console.log(`Count exists: ${value}`); // Count exists: 42
} else {
console.log('Count not found');
}getEntry(key)
Retrieves the full cache entry including metadata (createdAt, expiresAt, ttlLeft, age). Returns undefined if the key doesn't exist or has expired. Useful when you need to know when an entry was created, when it will expire, how much time is left, or how old it is.
Example 1: Accessing all metadata
const cache = new Cache<string, number>({ ttl: 3600000 }); // 1 hour TTL
cache.set('data', 100);
const entry = cache.getEntry('data');
if (entry) {
console.log(entry.value); // 100
console.log(entry.createdAt); // Timestamp when created
console.log(entry.expiresAt); // Timestamp when it expires
console.log(entry.ttlLeft); // Milliseconds until expiration (e.g., 3600000)
console.log(entry.age); // Age in milliseconds (e.g., 0 if just created)
}Example 2: Using ttlLeft for expiration checks
const cache = new Cache<string, string>({ ttl: 60000 }); // 1 minute TTL
cache.set('token', 'abc123');
const entry = cache.getEntry('token');
// ttlLeft kan be used as a validator because it will be 0 if it is no time left
if (entry && entry.ttlLeft) {
const secondsLeft = Math.floor(entry.ttlLeft / 1000);
console.log(`Token expires in ${secondsLeft} seconds`);
// Refresh if less than 10 seconds left
if (entry.ttlLeft < 10000) cache.set('token', 'new-token');
}Example 3: Using age to check data freshness
const cache = new Cache<string, { data: any }>();
cache.set('api-response', { data: 'some data' });
// Wait a bit, then check age
const entry = cache.getEntry('api-response');
// after a x-seconds
if (entry) {
const ageInSeconds = Math.floor(entry.age / 1000);
console.log(`Entry is ${ageInSeconds} seconds old`);
// Refresh if older than 5 minutes
if (entry.age > 300000) {
console.log('Data is stale, refreshing...');
cache.set('api-response', { data: 'fresh data' });
}
}peek(key)
Peek at a value without updating access time or triggering auto-delete. Useful for checking values without affecting eviction order. When using LRU, peek() does not update the lastAccessed timestamp, so it won't prevent the entry from being evicted.
Example 1: Check value without affecting eviction (FIFO)
const cache = new Cache<string, number>({
maxEntries: 3,
evictionPolicy: 'FIFO',
});
cache.set('a', 1);
cache.set('b', 2);
cache.set('c', 3);
// Peek at 'a' without moving it in eviction queue
const value = cache.peek('a');
console.log(value); // 1
// 'a' is still the oldest and will be evicted first
cache.set('d', 4);
console.log(cache.get('a')); // undefined (evicted)Example 1b: Peek with LRU (doesn't update access time)
const cache = new Cache<string, number>({
maxEntries: 3,
evictionPolicy: 'LRU',
});
cache.set('a', 1);
cache.set('b', 2);
cache.set('c', 3);
cache.get('a'); // Access 'a' - updates access time
cache.peek('b'); // Peek at 'b' - does NOT update access time
// 'b' is least recently used (was never accessed with get())
cache.set('d', 4);
console.log(cache.get('b')); // undefined (evicted, not 'a')
console.log(cache.get('a')); // 1 (still exists)Example 2: Peek vs get with autoDeleteAfterUse
const cache = new Cache<string, string>({ autoDeleteAfterUse: true });
cache.set('token', 'abc123');
// get() will delete the entry
const value1 = cache.get('token');
console.log(value1); // 'abc123'
console.log(cache.has('token')); // false (deleted)
// Reset
cache.set('token', 'abc123');
// peek() won't delete the entry
const value2 = cache.peek('token');
console.log(value2); // 'abc123'
console.log(cache.has('token')); // true (still exists)Example 3: Conditional check without side effects
const cache = new Cache<string, { status: string }>();
cache.set('task:1', { status: 'pending' });
// Check status without affecting cache
if (cache.peek('task:1')?.status === 'pending') {
// Process task...
// Entry remains in cache for later use
console.log('Task is pending');
}Delete Operations
Delete operations allow you to remove entries from the cache. You can delete single entries, multiple entries at once, or clear the entire cache.
delete(key)
Deletes a single entry from the cache. Returns true if the key existed and was deleted, false if the key didn't exist.
Note: This operation emits a delete event. See Events section for details.
Example 1: Basic deletion
const cache = new Cache<string, number>();
cache.set('score', 100);
console.log(cache.get('score')); // 100
const deleted = cache.delete('score');
console.log(deleted); // true
console.log(cache.get('score')); // undefinedExample 2: Deleting non-existent key
const cache = new Cache<string, string>();
// Try to delete a key that doesn't exist
const deleted = cache.delete('nonexistent');
console.log(deleted); // falseExample 3: Conditional deletion
const cache = new Cache<string, { status: string }>();
cache.set('task:1', { status: 'completed' });
cache.set('task:2', { status: 'pending' });
// Delete only if status is completed
const task = cache.get('task:1');
if (task && task.status === 'completed') cache.delete('task:1');
// Result
console.log(cache.has('task:1')); // false
console.log(cache.has('task:2')); // trueExample 4: Auto-delete after use
// Enable auto-delete after use in cache options
const cache = new Cache<string, any>({
autoDeleteAfterUse: true, // Entries are automatically deleted after get()
});
// Store temporary data
cache.set('temp:session', { userId: 123, token: 'abc' });
// Use the data - entry is automatically deleted after retrieval
const session = cache.get('temp:session');
if (session) {
// Process session...
console.log('Processing session:', session.userId);
}
// Entry is already deleted, no need to manually clean up
console.log(cache.has('temp:session')); // falseExample 5: One-time use tokens
const cache = new Cache<string, string>({
autoDeleteAfterUse: true,
});
// Store one-time tokens
cache.set('token:abc123', 'user:123');
cache.set('token:xyz789', 'user:456');
// Tokens are consumed and deleted when retrieved
const userId1 = cache.get('token:abc123');
console.log(userId1); // 'user:123'
console.log(cache.has('token:abc123')); // false (auto-deleted)
// Token can only be used once
const userId1Again = cache.get('token:abc123');
console.log(userId1Again); // undefined (already consumed)deleteMany(keys)
Deletes multiple entries at once. Returns the number of entries that were successfully deleted.
Note: This operation emits a deleteMany event. See Events section for details.
Example 1: Bulk deletion
const cache = new Cache<string, number>();
cache.set('a', 1);
cache.set('b', 2);
cache.set('c', 3);
cache.set('d', 4);
// Delete multiple keys at once
const deleted = cache.deleteMany(['a', 'b', 'e']); // 'e' doesn't exist
console.log(deleted); // 2 (only 'a' and 'b' were deleted)
console.log(cache.size()); // 2 ('c' and 'd' remain)Example 2: Removing expired or invalid entries
const cache = new Cache<string, { valid: boolean }>();
cache.set('item:1', { valid: true });
cache.set('item:2', { valid: false });
cache.set('item:3', { valid: true });
cache.set('item:4', { valid: false });
// Find and delete invalid items
const allKeys = cache.keys();
const invalidKeys = allKeys.filter((key) => {
const item = cache.get(key);
return item && !item.valid;
});
const deleted = cache.deleteMany(invalidKeys);
console.log(`Deleted ${deleted} invalid items`);
console.log(cache.size()); // 2 (only valid items remain)Example 3: Cleanup by prefix
const cache = new Cache<string, any>();
cache.set('user:1:profile', { name: 'Alice' });
cache.set('user:1:settings', { theme: 'dark' });
cache.set('user:2:profile', { name: 'Bob' });
cache.set('user:2:settings', { theme: 'light' });
cache.set('session:abc', { token: 'xyz' });
// Delete all entries for user:1
const allKeys = cache.keys();
const user1Keys = allKeys.filter((key) => key.startsWith('user:1:'));
const deleted = cache.deleteMany(user1Keys);
console.log(`Deleted ${deleted} entries for user:1`);
console.log(cache.has('user:1:profile')); // false
console.log(cache.has('user:2:profile')); // trueclear()
Removes all entries from the cache. Useful for resetting the cache or freeing memory.
Example 1: Resetting the cache
const cache = new Cache<string, number>();
cache.set('a', 1);
cache.set('b', 2);
cache.set('c', 3);
console.log(cache.size()); // 3
// Clear everything
cache.clear();
console.log(cache.size()); // 0
console.log(cache.get('a')); // undefinedExample 2: Memory cleanup
const cache = new Cache<string, any>({ maxBytes: 100 * 1024 * 1024 }); // 100MB
// Cache grows over time
for (let i = 0; i < 1000; i++) {
cache.set(`item:${i}`, { data: 'large object' });
}
console.log(cache.sizeInBytes()); // Large size
// Clear cache to free memory
cache.clear();
console.log(cache.size()); // 0
console.log(cache.sizeInBytes()); // 0Example 3: Cleanup before shutdown
const cache = new Cache<string, any>();
// Application runs and caches data
cache.set('config', { setting: 'value' });
cache.set('session', { userId: 123 });
// Before application shutdown
process.on('SIGTERM', () => {
console.log('Cleaning up cache before shutdown...');
cache.clear();
cache.destroy();
process.exit(0);
});Mutate Operations
Mutate operations allow you to update existing cache entries in place. These operations are useful for modifying values without retrieving, updating, and setting them separately.
mutate(key, updater)
Updates an existing value using a function. Returns the new value, or undefined if the key doesn't exist.
Note: This operation emits a mutate event. See Events section for details.
Example 1: Basic mutation
const cache = new Cache<string, number>();
cache.set('counter', 100);
const newValue = cache.mutate('counter', (value) => value * 2);
console.log(newValue); // 200
console.log(cache.get('counter')); // 200Example 2: Complex transformation
const cache = new Cache<string, { count: number; multiplier: number }>();
cache.set('stats', { count: 10, multiplier: 2 });
const updated = cache.mutate('stats', (value) => ({
...value,
count: value.count * value.multiplier,
}));
console.log(updated); // { count: 20, multiplier: 2 }Example 3: Conditional mutation
const cache = new Cache<string, number>();
cache.set('score', 50);
const result = cache.mutate('score', (value) => {
// Only update if value is less than 100
return value < 100 ? value + 10 : value;
});
console.log(result); // 60
// Try again
const result2 = cache.mutate('score', (value) => {
return value < 100 ? value + 10 : value;
});
console.log(result2); // 70Example 4: Mutating non-existent key
const cache = new Cache<string, number>();
// Mutate returns undefined if key doesn't exist
const result = cache.mutate('nonexistent', (value) => value + 1);
console.log(result); // undefined
console.log(cache.has('nonexistent')); // falseupsert(key, valueOrUpdater)
Updates an existing value or sets it if it doesn't exist. Can accept either a direct value or an updater function. Always returns the final value.
Note: This operation emits an upsert event. See Events section for details.
Example 1: Inserting new value
const cache = new Cache<string, number>();
// Set if doesn't exist
const value1 = cache.upsert('counter', 10);
console.log(value1); // 10
console.log(cache.get('counter')); // 10Example 2: Updating existing value with direct value
const cache = new Cache<string, number>();
cache.set('counter', 5);
const value = cache.upsert('counter', 20);
console.log(value); // 20
console.log(cache.get('counter')); // 20Example 3: Using updater function
const cache = new Cache<string, number>();
// Initialize with updater function
cache.upsert('counter', (current) => (current || 0) + 1);
console.log(cache.get('counter')); // 1
// Update existing with updater
cache.upsert('counter', (current) => (current || 0) + 5);
console.log(cache.get('counter')); // 6Example 4: Complex upsert with object
const cache = new Cache<string, { count: number; lastUpdated: number }>();
// Insert new object
cache.upsert('stats', {
count: 0,
lastUpdated: Date.now(),
});
// Update with function
cache.upsert('stats', (current) => ({
count: (current?.count || 0) + 1,
lastUpdated: Date.now(),
}));
console.log(cache.get('stats')); // { count: 1, lastUpdated: <timestamp> }increment(key, amount?)
Increments a numeric value by the specified amount (default: 1). Returns the new value, or undefined if the key doesn't exist.
⚠️ Important: increment() only works when the cache value type is number. It does NOT work on objects, arrays, or other types.
Note: This operation emits an increment event. See Events section for details.
✅ What works:
// Works: Cache with number values
const cache = new Cache<string, number>();
cache.set('views', 0);
cache.increment('views'); // ✅ Works
console.log(cache.get('views')); // 1❌ What doesn't work:
// Doesn't work: Cache with object values
const cache2 = new Cache<string, { count: number }>();
cache2.set('stats', { count: 5 });
cache2.increment('stats'); // ❌ ERROR: Cannot increment object
// Doesn't work: Cache with array values
const cache3 = new Cache<string, number[]>();
cache3.set('items', [1, 2, 3]);
cache3.increment('items'); // ❌ ERROR: Cannot increment array
// Solution: Use mutate() for objects/arrays
cache2.mutate('stats', (obj) => ({
...obj,
count: obj.count + 1,
}));Example 1: Basic increment
const cache = new Cache<string, number>();
cache.set('views', 0);
cache.increment('views');
console.log(cache.get('views')); // 1
cache.increment('views');
console.log(cache.get('views')); // 2Example 2: Increment by custom amount
const cache = new Cache<string, number>();
cache.set('score', 100);
cache.increment('score', 25);
console.log(cache.get('score')); // 125
cache.increment('score', 50);
console.log(cache.get('score')); // 175Example 3: Tracking page views
const cache = new Cache<string, number>();
// Track views for different pages
const pageId = 'page:123';
cache.set(pageId, 0);
// Increment on each view
cache.increment(pageId);
cache.increment(pageId);
cache.increment(pageId, 5); // Bulk views
console.log(cache.get(pageId)); // 7decrement(key, amount?)
Decrements a numeric value by the specified amount (default: 1). Returns the new value, or undefined if the key doesn't exist.
⚠️ Important: decrement() only works when the cache value type is number. It does NOT work on objects, arrays, or other types.
Note: This operation emits a decrement event. See Events section for details.
✅ What works:
// Works: Cache with number values
const cache = new Cache<string, number>();
cache.set('lives', 3);
cache.decrement('lives'); // ✅ Works
console.log(cache.get('lives')); // 2❌ What doesn't work:
// Doesn't work: Cache with object values containing numbers
const cache2 = new Cache<string, { lives: number; score: number }>();
cache2.set('player', { lives: 3, score: 100 });
cache2.decrement('player'); // ❌ ERROR: Cannot decrement object
// Doesn't work: Cache with array values
const cache3 = new Cache<string, number[]>();
cache3.set('items', [1, 2, 3]);
cache3.decrement('items'); // ❌ ERROR: Cannot decrement array
// Solution: Use mutate() for objects/arrays
cache2.mutate('player', (obj) => ({
...obj,
lives: obj.lives - 1,
score: obj.score - 10,
}));Example 1: Basic decrement
const cache = new Cache<string, number>();
cache.set('lives', 3);
cache.decrement('lives');
console.log(cache.get('lives')); // 2
cache.decrement('lives');
console.log(cache.get('lives')); // 1Example 2: Decrement by custom amount
const cache = new Cache<string, number>();
cache.set('balance', 1000);
cache.decrement('balance', 250);
console.log(cache.get('balance')); // 750
cache.decrement('balance', 100);
console.log(cache.get('balance')); // 650Example 3: Inventory management
const cache = new Cache<string, number>();
cache.set('item:123:stock', 50);
// Sell items
cache.decrement('item:123:stock', 5);
console.log(cache.get('item:123:stock')); // 45
// Return items
cache.increment('item:123:stock', 2);
console.log(cache.get('item:123:stock')); // 47append(key, ...items)
Appends one or more items to an array value. Returns the new array, or undefined if the key doesn't exist.
Note: This operation emits an append event. See Events section for details.
Example 1: Basic append
const cache = new Cache<string, number[]>();
cache.set('items', [1, 2, 3]);
cache.append('items', 4);
console.log(cache.get('items')); // [1, 2, 3, 4]
cache.append('items', 5, 6);
console.log(cache.get('items')); // [1, 2, 3, 4, 5, 6]Example 2: Building a list
const cache = new Cache<string, string[]>();
cache.set('log', []);
// Append log entries
cache.append('log', 'User logged in');
cache.append('log', 'User viewed page');
cache.append('log', 'User logged out');
console.log(cache.get('log'));
// ['User logged in', 'User viewed page', 'User logged out']Example 3: Appending multiple items
const cache = new Cache<string, string[]>();
cache.set('tags', ['javascript', 'typescript']);
cache.append('tags', 'nodejs', 'react', 'vue');
console.log(cache.get('tags'));
// ['javascript', 'typescript', 'nodejs', 'react', 'vue']merge(key, updates, options?)
Merges values based on their type. Returns the merged value, or undefined if the key doesn't exist.
Note: This operation emits a merge event. See Events section for details.
Supported types and behavior:
- Objects: Shallow merge properties (can add new properties)
- Arrays: Concatenate arrays (with optional duplicate filtering)
- Strings: Concatenate strings
- Numbers: Concatenate as strings then convert back to number (4 + 2 = 42, not 6)
Options:
allowDuplicates?: boolean- Allow duplicates when merging arrays (default: false, or uses cache-levelmergeAllowDuplicatessetting)
Example 1: Merging objects (shallow merge)
const cache = new Cache<string, { name: string; age: number }>();
cache.set('user', { name: 'John', age: 30 });
cache.merge('user', { age: 31 });
console.log(cache.get('user')); // { name: 'John', age: 31 }
// Add new properties
cache.merge('user', { city: 'Stockholm', country: 'Sweden' });
console.log(cache.get('user'));
// { name: 'John', age: 31, city: 'Stockholm', country: 'Sweden' }Example 2: Merging strings (concatenation)
const cache = new Cache<string, string>();
cache.set('greeting', 'Good');
cache.merge('greeting', ' Bye');
console.log(cache.get('greeting')); // 'Good Bye'
// Multiple merges
cache.set('text', 'Hello');
cache.merge('text', ' ');
cache.merge('text', 'World');
console.log(cache.get('text')); // 'Hello World'Example 3: Merging numbers (concatenation)
const cache = new Cache<string, number>();
cache.set('value', 4);
cache.merge('value', 2);
console.log(cache.get('value')); // 42 (not 6!)
// Multiple merges
cache.set('code', 1);
cache.merge('code', 2);
cache.merge('code', 3);
console.log(cache.get('code')); // 123Example 4: Merging arrays (without duplicates by default)
const cache = new Cache<string, number[]>();
cache.set('items', [1, 2, 3]);
cache.merge('items', [3, 4, 5]);
console.log(cache.get('items')); // [1, 2, 3, 4, 5] (3 is not duplicated)Example 5: Merging arrays (with duplicates allowed)
const cache = new Cache<string, number[]>();
cache.set('items', [1, 2, 3]);
cache.merge('items', [3, 4, 5], { allowDuplicates: true });
console.log(cache.get('items')); // [1, 2, 3, 3, 4, 5] (duplicates allowed)Example 6: Cache-level duplicate setting
// Set allowDuplicates at cache level
const cache = new Cache<string, number[]>({ mergeAllowDuplicates: true });
cache.set('items', [1, 2]);
cache.merge('items', [2, 3]);
console.log(cache.get('items')); // [1, 2, 2, 3] (duplicates allowed)
// Override with option parameter
cache.merge('items', [3, 4], { allowDuplicates: false });
console.log(cache.get('items')); // [1, 2, 2, 3, 4] (no new duplicates)Example 7: Shallow merge for nested objects
const cache = new Cache<
string,
{ profile: { name: string; bio: string }; settings: { theme: string } }
>();
cache.set('user', {
profile: { name: 'Bob', bio: 'Developer' },
settings: { theme: 'light' },
});
// Shallow merge: nested objects are replaced, not merged
cache.merge('user', {
profile: { name: 'Bob Smith' }, // This replaces the entire profile object
});
console.log(cache.get('user'));
// {
// profile: { name: 'Bob Smith' }, // bio is lost!
// settings: { theme: 'light' }
// }Utility Operations
Utility operations provide information about the cache state and contents.
Note: All utility operations emit events. See Events section for details on size, sizeInBytes, keys, values, entries, isEmpty, and randomKey events.
const cache = new Cache<string, number>();
cache.set('a', 1);
cache.set('b', 2);
cache.set('c', 3);
// Get all keys
const keys = cache.keys(); // ['a', 'b', 'c']
// Get all values
const values = cache.values(); // [1, 2, 3]
// Get all entries
const entries = cache.entries(); // [['a', 1], ['b', 2], ['c', 3]]
// Check if cache is empty
const isEmpty = cache.isEmpty(); // false
// Get a random key
const randomKey = cache.randomKey(); // 'a', 'b', or 'c'
// Get entry with metadata
const entry = cache.getEntry('a');
// { value: 1, createdAt: 1234567890, expiresAt: undefined }Cleanup
const cache = new Cache<string, any>({
ttl: 1000,
cleanupInterval: 500,
});
// Automatic cleanup runs every 500ms
// Expired entries are removed automatically
// Manually cleanup expired entries
const cleaned = cache.cleanupExpired(); // Returns number of cleaned entries
// Stop automatic cleanup
cache.stopCleanup();
// Destroy cache and cleanup resources
cache.destroy();Events
The cache provides a comprehensive event system that allows you to monitor and react to all cache operations. Events are emitted for every operation, giving you full visibility into cache behavior.
How Events Work
Events can be registered using the on() method with flexible patterns:
- Specific key and event:
cache.on("key", "event", handler)- Listen to a specific event on a specific key - All events for a key:
cache.on("key", handler)- Listen to all events for a specific key - All keys for an event:
cache.on("*", "event", handler)- Listen to a specific event for all keys - All keys and events:
cache.on("*", handler)- Listen to all events for all keys
Event handler signature:
type CacheEventHandler<K, V> = (
key: K | K[] | undefined, // The key(s) involved, undefined for utility operations
value: V | V[] | undefined, // The value(s) involved, undefined for delete operations
event: CacheEvent // The event name
) => void;Removing listeners:
cache.off("key", "event", handler)- Remove specific handlercache.off("key", "event")- Remove all handlers for an eventcache.off("key", handler)- Remove handler from all events on a keycache.off("key")- Remove all listeners for a key
Available Events
Set Events
set - Emitted when a single value is set
const cache = new Cache<string, number>();
cache.on('user:123', 'set', (key, value, event) => {
console.log(`Set ${key} to ${value}`); // Set user:123 to 42
});
cache.set('user:123', 42);setMany - Emitted when multiple values are set at once
const cache = new Cache<string, number>();
cache.on('*', 'setMany', (keys, values, event) => {
console.log(`Set ${keys.length} keys`); // Set 3 keys
});
cache.setMany([
['key1', 100],
['key2', 200],
['key3', 300],
]);Get Events
get - Emitted when a value is retrieved
const cache = new Cache<string, number>();
cache.set('user:123', 42);
cache.on('user:123', 'get', (key, value, event) => {
console.log(`Retrieved ${key}: ${value}`); // Retrieved user:123: 42
});
cache.get('user:123');getMany - Emitted when multiple values are retrieved
const cache = new Cache<string, number>();
cache.set('key1', 100);
cache.set('key2', 200);
cache.on('*', 'getMany', (keys, values, event) => {
console.log(`Retrieved ${values.length} values`); // Retrieved 2 values
});
cache.getMany(['key1', 'key2', 'key3']);Delete Events
delete - Emitted when a value is deleted
const cache = new Cache<string, number>();
cache.set('user:123', 42);
cache.on('user:123', 'delete', (key, value, event) => {
console.log(`Deleted ${key}`); // Deleted user:123
});
cache.delete('user:123');deleteMany - Emitted when multiple values are deleted
const cache = new Cache<string, number>();
cache.set('key1', 100);
cache.set('key2', 200);
cache.on('*', 'deleteMany', (keys, value, event) => {
console.log(`Deleted ${keys.length} keys`); // Deleted 2 keys
});
cache.deleteMany(['key1', 'key2', 'key3']);Mutate Events
mutate - Emitted when a value is updated using a function
const cache = new Cache<string, number>();
cache.set('counter', 10);
cache.on('counter', 'mutate', (key, value, event) => {
console.log(`Mutated ${key} to ${value}`); // Mutated counter to 20
});
cache.mutate('counter', (v) => v * 2);upsert - Emitted when a value is updated or inserted
const cache = new Cache<string, number>();
cache.on('counter', 'upsert', (key, value, event) => {
console.log(`Upserted ${key} to ${value}`); // Upserted counter to 5
});
cache.upsert('counter', 5);increment - Emitted when a numeric value is incremented
const cache = new Cache<string, number>();
cache.set('counter', 10);
cache.on('counter', 'increment', (key, value, event) => {
console.log(`Incremented ${key} to ${value}`); // Incremented counter to 15
});
cache.increment('counter', 5);decrement - Emitted when a numeric value is decremented
const cache = new Cache<string, number>();
cache.set('counter', 10);
cache.on('counter', 'decrement', (key, value, event) => {
console.log(`Decremented ${key} to ${value}`); // Decremented counter to 5
});
cache.decrement('counter', 5);append - Emitted when items are appended to an array
const cache = new Cache<string, number[]>();
cache.set('items', [1, 2]);
cache.on('items', 'append', (key, value, event) => {
console.log(`Appended to ${key}:`, value); // Appended to items: [1, 2, 3, 4]
});
cache.append('items', 3, 4);merge - Emitted when values are merged
const cache = new Cache<string, { a: number; b: number }>();
cache.set('obj', { a: 1, b: 2 });
cache.on('obj', 'merge', (key, value, event) => {
console.log(`Merged ${key}:`, value); // Merged obj: { a: 1, b: 3, c: 4 }
});
cache.merge('obj', { b: 3, c: 4 });Utility Events
size - Emitted when cache size is queried
const cache = new Cache<string, number>();
cache.set('key1', 100);
cache.on('*', 'size', (key, value, event) => {
console.log('Size queried');
});
const size = cache.size(); // Triggers eventsizeInBytes - Emitted when cache size in bytes is queried
const cache = new Cache<string, number>();
cache.set('key1', 100);
cache.on('*', 'sizeInBytes', (key, value, event) => {
console.log('Size in bytes queried');
});
const bytes = cache.sizeInBytes(); // Triggers eventkeys - Emitted when all keys are retrieved
const cache = new Cache<string, number>();
cache.set('key1', 100);
cache.set('key2', 200);
cache.on('*', 'keys', (key, values, event) => {
console.log('Keys retrieved:', values); // Keys retrieved: ['key1', 'key2']
});
const keys = cache.keys(); // Triggers eventvalues - Emitted when all values are retrieved
const cache = new Cache<string, number>();
cache.set('key1', 100);
cache.set('key2', 200);
cache.on('*', 'values', (key, values, event) => {
console.log('Values retrieved:', values); // Values retrieved: [100, 200]
});
const values = cache.values(); // Triggers evententries - Emitted when all entries are retrieved
const cache = new Cache<string, number>();
cache.set('key1', 100);
cache.on('*', 'entries', (key, values, event) => {
console.log('Entries retrieved:', values); // Entries retrieved: [['key1', 100]]
});
const entries = cache.entries(); // Triggers eventisEmpty - Emitted when cache emptiness is checked
const cache = new Cache<string, number>();
cache.on('*', 'isEmpty', (key, value, event) => {
console.log('Is empty checked:', value); // Is empty checked: true
});
const isEmpty = cache.isEmpty(); // Triggers eventrandomKey - Emitted when a random key is retrieved
const cache = new Cache<string, number>();
cache.set('key1', 100);
cache.set('key2', 200);
cache.on('*', 'randomKey', (key, value, event) => {
console.log('Random key queried');
});
const random = cache.randomKey(); // Triggers eventAdvanced Event Usage
Listen to all operations on a specific key:
const cache = new Cache<string, number>();
cache.on('user:123', (key, value, event) => {
console.log(`Operation ${event} on ${key}:`, value);
});
cache.set('user:123', 42); // Operation set on user:123: 42
cache.get('user:123'); // Operation get on user:123: 42
cache.delete('user:123'); // Operation delete on user:123: undefinedListen to a specific event for all keys:
const cache = new Cache<string, number>();
cache.on('*', 'set', (key, value, event) => {
console.log(`Set operation on ${key}:`, value);
});
cache.set('key1', 100); // Set operation on key1: 100
cache.set('key2', 200); // Set operation on key2: 200Error handling:
Event handlers that throw errors are caught and logged, but don't break cache operations:
const cache = new Cache<string, number>();
cache.on('*', 'set', (key, value, event) => {
throw new Error('Handler error'); // Error is caught and logged
});
cache.set('key1', 100); // Still works, error is logged to consoleAPI
Cache<K, V>
Constructor
new Cache(options?: CacheOptions)Set Operations
set(key: K, value: V): void- Set a value in the cachesetMany(entries: Array<[K, V]>): void- Set multiple values at once
Get Operations
get(key: K): V | undefined- Get a value from the cachegetMany(keys: K[]): V[]- Get multiple values at once (filters out undefined)has(key: K): boolean- Check if a key existsgetEntry(key: K): CacheEntry<V> | undefined- Get entry with metadatapeek(key: K): V | undefined- Peek at a value without affecting eviction order
Delete Operations
delete(key: K): boolean- Delete a value from the cachedeleteMany(keys: K[]): number- Delete multiple values, returns countclear(): void- Clear all entries
Mutate Operations
mutate(key: K, updater: (value: V) => V): V | undefined- Update using a functionupsert(key: K, valueOrUpdater: V | ((value: V | undefined) => V)): V- Update or insertincrement(key: K, amount?: number): number | undefined- Increment numeric valuedecrement(key: K, amount?: number): number | undefined- Decrement numeric valueappend<T>(key: K, ...items: T[]): T[] | undefined- Append to arraymerge<T>(key: K, updates: Partial<T>): T | undefined- Merge object
Utility Operations
size(): number- Get the number of entriessizeInBytes(): number- Get the size in byteskeys(): K[]- Get all keysvalues(): V[]- Get all valuesentries(): Array<[K, V]>- Get all entriesisEmpty(): boolean- Check if cache is emptyrandomKey(): K | undefined- Get a random key
Cleanup Operations
cleanupExpired(): number- Manually cleanup expired entriesstopCleanup(): void- Stop automatic cleanupdestroy(): void- Destroy the cache and cleanup resources
CacheOptions
interface CacheOptions {
maxEntries?: number; // Maximum number of entries (default: Infinity)
maxBytes?: number; // Maximum size in bytes. If entry is larger, all entries are removed and new entry is set anyway
ttl?: number; // Time to live in milliseconds
cleanupInterval?: number; // Cleanup interval in milliseconds (default: 60000)
evictionPolicy?: 'FIFO' | 'LRU'; // Eviction policy: 'FIFO' (default) or 'LRU'
autoDeleteAfterUse?: boolean; // Automatically delete entry after get() is called (default: false)
mergeAllowDuplicates?: boolean; // Allow duplicates when merging arrays (default: false)
alarm?: CacheAlarm; // Alarm callbacks for size warnings
}CacheEntry<T>
interface CacheEntry<T> {
value: T;
expiresAt?: number; // Timestamp when entry expires
createdAt: number; // Timestamp when entry was created
lastAccessed?: number; // Last access time (used for LRU eviction, set when using LRU policy)
ttlLeft?: number; // Time to live remaining in milliseconds (calculated dynamically)
age?: number; // Age of the entry in milliseconds (calculated dynamically)
}Persistence
This cache is in-memory only by design. Data is not persisted to disk or shared between browser tabs by default. If you need persistence, you can implement it yourself:
Browser: Using localStorage
import { Cache } from 'another-cache';
// Create cache with serialization helpers
const cache = new Cache<string, any>();
// Save to localStorage on changes
function saveToStorage() {
const entries = cache.entries();
localStorage.setItem('cache', JSON.stringify(entries));
}
// Load from localStorage on init
function loadFromStorage() {
const stored = localStorage.getItem('cache');
if (stored) {
const entries = JSON.parse(stored);
cache.setMany(entries);
}
}
// Load on init
loadFromStorage();
// Save on every set/delete
const originalSet = cache.set.bind(cache);
cache.set = (key, value) => {
originalSet(key, value);
saveToStorage();
};
const originalDelete = cache.delete.bind(cache);
cache.delete = (key) => {
const result = originalDelete(key);
saveToStorage();
return result;
};Node.js: Using File System
import { Cache } from 'another-cache';
import { readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
const CACHE_FILE = join(process.cwd(), 'cache.json');
const cache = new Cache<string, any>();
// Load from file
function loadCache() {
try {
const data = readFileSync(CACHE_FILE, 'utf-8');
const entries = JSON.parse(data);
cache.setMany(entries);
} catch (error) {
// File doesn't exist or is invalid
}
}
// Save to file
function saveCache() {
const entries = cache.entries();
writeFileSync(CACHE_FILE, JSON.stringify(entries, null, 2));
}
// Load on init
loadCache();
// Save periodically or on process exit
setInterval(saveCache, 60000); // Every minute
process.on('SIGINT', () => {
saveCache();
process.exit(0);
});Browser: Sharing Between Tabs (BroadcastChannel)
import { Cache } from 'another-cache';
const cache = new Cache<string, any>();
const channel = new BroadcastChannel('cache-sync');
// Listen for updates from other tabs
channel.onmessage = (event) => {
const { type, key, value } = event.data;
if (type === 'set') {
cache.set(key, value);
} else if (type === 'delete') {
cache.delete(key);
}
};
// Broadcast changes to other tabs
const originalSet = cache.set.bind(cache);
cache.set = (key, value) => {
originalSet(key, value);
channel.postMessage({ type: 'set', key, value });
};
const originalDelete = cache.delete.bind(cache);
cache.delete = (key) => {
const result = originalDelete(key);
channel.postMessage({ type: 'delete', key });
return result;
};Architecture
The library is built with a modular architecture:
src/
├── cache.ts # Main Cache class
├── types.ts # TypeScript types
├── operations/ # Modular operations
│ ├── set.ts # Set operations
│ ├── get.ts # Get operations
│ ├── delete.ts # Delete operations
│ ├── mutate.ts # Mutation operations
│ ├── utility.ts # Utility operations
│ └── cleanup.ts # Cleanup operations
└── utils/
└── size.ts # Size calculation utilitiesBuilding
# Install dependencies
npm install
# Build the project
npm run build
# Run tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:coverage
# Lint code
npm run lint
# Format code
npm run formatTesting
Tests are organized in a structured test directory:
tests/
├── cache.test.ts # Main Cache tests
├── operations/ # Operation-specific tests
│ ├── set.test.ts
│ ├── get.test.ts
│ ├── delete.test.ts
│ ├── mutate.test.ts
│ ├── utility.test.ts
│ └── cleanup.test.ts
└── utils/
└── size.test.tsLicense
MIT
