tessl install tessl/npm-cache-manager@7.2.0Cache Manager for Node.js with support for multi-store caching, background refresh, and Keyv-compatible storage adapters
Comprehensive coverage of special values, boundary conditions, and corner cases.
// null is cached (distinguishable from miss)
await cache.set('nullable', null);
console.log(await cache.get('nullable')); // null
// undefined behavior (may not be distinguishable from miss)
await cache.set('undef', undefined);
console.log(await cache.get('undef')); // undefined (could be miss or cached value)
// Cache misses always return undefined
console.log(await cache.get('nonexistent')); // undefined// Zero is cached (not confused with undefined)
await cache.set('zero', 0);
console.log(await cache.get('zero')); // 0
// False is cached
await cache.set('false', false);
console.log(await cache.get('false')); // false
// Empty string is cached
await cache.set('empty', '');
console.log(await cache.get('empty')); // ''
// Empty array is cached
await cache.set('emptyArray', []);
console.log(await cache.get('emptyArray')); // []
// Empty object is cached
await cache.set('emptyObj', {});
console.log(await cache.get('emptyObj')); // {}// Zero TTL = immediate expiration
await cache.set('instant', 'value', 0);
console.log(await cache.get('instant')); // undefined (already expired)
// Negative TTL = already expired
await cache.set('past', 'value', -1000);
console.log(await cache.get('past')); // undefined
// Infinity = no expiration (store-dependent)
await cache.set('forever', 'value', Infinity);
// May not expire, depends on store implementation// Maximum safe integer TTL
const maxTtl = Number.MAX_SAFE_INTEGER;
await cache.set('longterm', 'value', maxTtl);
// 100 years
const centuryTtl = 100 * 365 * 24 * 60 * 60 * 1000;
await cache.set('century', 'value', centuryTtl);// Millisecond precision
await cache.set('precise', 'value', 1234); // Exactly 1.234 seconds
// Sub-millisecond values rounded
await cache.set('submilli', 'value', 0.5); // Treated as 0ms = immediate expiration// Empty string is valid key
await cache.set('', 'empty key value');
console.log(await cache.get('')); // 'empty key value'
await cache.del('');// Unicode characters
await cache.set('user:日本', { name: 'Tanaka' });
console.log(await cache.get('user:日本')); // { name: 'Tanaka' }
// Special characters
await cache.set('key:with:colons', 'value');
await cache.set('key/with/slashes', 'value');
await cache.set('key with spaces', 'value');
await cache.set('key-with-dashes', 'value');
// Very long keys
const longKey = 'x'.repeat(1000);
await cache.set(longKey, 'value');// Large string (1MB)
const largeString = 'x'.repeat(1024 * 1024);
try {
await cache.set('large', largeString);
console.log('Cached 1MB string');
} catch (error) {
console.error('Value too large:', error);
}
// Very deep object
const deepObject = { a: { b: { c: { d: { e: 'deep' } } } } };
await cache.set('deep', deepObject);
// Large array
const largeArray = Array.from({ length: 10000 }, (_, i) => ({ id: i }));
await cache.set('largeArray', largeArray);// Circular reference throws error
const circular: any = { a: 1 };
circular.self = circular;
try {
await cache.set('circular', circular);
} catch (error) {
console.error('Cannot cache circular reference:', error);
// Error: Converting circular structure to JSON
}
// Break circular ref before caching
const safe = {
a: circular.a,
// Omit circular.self
};
await cache.set('safe', safe); // Works// Functions cannot be cached
try {
await cache.set('func', () => 'hello');
} catch (error) {
console.error('Cannot cache functions:', error);
}
// Symbols cannot be cached
try {
await cache.set('symbol', Symbol('test'));
} catch (error) {
console.error('Cannot cache symbols:', error);
}
// Dates are serialized as ISO strings
const date = new Date('2024-01-01');
await cache.set('date', date);
const retrieved = await cache.get('date');
console.log(typeof retrieved); // 'string' (not Date object)
console.log(retrieved); // '2024-01-01T00:00:00.000Z'
// RegExp serialization
const regex = /test/gi;
await cache.set('regex', regex);
const retrievedRegex = await cache.get('regex');
console.log(retrievedRegex); // {} (loses regex behavior)// Empty array operations
const emptyGet = await cache.mget([]);
console.log(emptyGet); // []
const emptySet = await cache.mset([]);
console.log(emptySet); // []
const emptyDel = await cache.mdel([]);
console.log(emptyDel); // true// Duplicate keys in mget
await cache.set('dup', 'value');
const dups = await cache.mget(['dup', 'dup', 'dup']);
console.log(dups); // ['value', 'value', 'value']
// Duplicate keys in mset (last wins)
await cache.mset([
{ key: 'dup', value: 'first' },
{ key: 'dup', value: 'second' },
{ key: 'dup', value: 'third' },
]);
console.log(await cache.get('dup')); // 'third'
// Duplicate keys in mdel
await cache.mdel(['key', 'key', 'key']); // Idempotent// 10,000 keys
const largeKeys = Array.from({ length: 10000 }, (_, i) => `key:${i}`);
const largeValues = await cache.mget(largeKeys);
console.log(largeValues.length); // 10000
// Set 10,000 items
await cache.mset(
largeKeys.map(key => ({ key, value: { data: 'value' } }))
);
// Delete 10,000 items
await cache.mdel(largeKeys);// Multiple concurrent gets for same key
const promises = Array.from({ length: 100 }, () => cache.get('key'));
const results = await Promise.all(promises);
// All return same value (or all undefined)// Race condition - last set wins
await Promise.all([
cache.set('race', 'value1'),
cache.set('race', 'value2'),
cache.set('race', 'value3'),
]);
// Final value is unpredictablelet execCount = 0;
async function expensiveOp() {
execCount++;
await new Promise(resolve => setTimeout(resolve, 100));
return { count: execCount };
}
// 100 concurrent wraps execute function only once
const promises = Array.from({ length: 100 }, () =>
cache.wrap('coalesce', expensiveOp, 60000)
);
const results = await Promise.all(promises);
console.log(execCount); // 1 (not 100)
console.log(results.every(r => r.count === 1)); // true// Set with 1ms TTL
await cache.set('expires', 'value', 1);
// May or may not be expired immediately
const immediate = await cache.get('expires'); // undefined or 'value'
// Definitely expired after 10ms
await new Promise(resolve => setTimeout(resolve, 10));
const expired = await cache.get('expires'); // undefined// Refresh exactly at threshold
await cache.wrap('boundary', () => 'data', 10000, 5000);
// Wait exactly 5 seconds
await new Promise(resolve => setTimeout(resolve, 5000));
// At threshold - may or may not trigger refresh
await cache.wrap('boundary', () => 'data', 10000, 5000);const cache = createCache({
stores: [memoryStore, redisStore],
nonBlocking: true,
});
// Set may succeed in memory but fail in Redis
await cache.set('inconsistent', 'value');
// Clear memory only
await cache.stores[0].clear();
// Get finds in Redis, promotes to memory
const value = await cache.get('inconsistent'); // 'value' (from Redis)
// Now in both stores again// One store fails, operation may still succeed
cache.on('set', ({ key, store, error }) => {
if (error) {
console.error(`Failed to set ${key} in ${store}:`, error);
}
});
// If memory succeeds but Redis fails, cache.set still returns
await cache.set('partial', 'value');// Deleting non-existent key returns true
const deleted = await cache.del('nonexistent');
console.log(deleted); // true
// Multiple deletes of same key
await cache.del('key');
await cache.del('key'); // Still returns true
await cache.del('key'); // Still returns true// Clearing empty cache returns true
await cache.clear();
const cleared = await cache.clear();
console.log(cleared); // true// Error in wrapped function is not cached
let attempts = 0;
async function failing() {
attempts++;
if (attempts < 3) {
throw new Error('Failure');
}
return 'success';
}
try {
await cache.wrap('failing', failing, 60000);
} catch (error) {
console.log('Attempt 1 failed');
}
try {
await cache.wrap('failing', failing, 60000);
} catch (error) {
console.log('Attempt 2 failed');
}
// Third attempt succeeds and is cached
const result = await cache.wrap('failing', failing, 60000);
console.log(result); // 'success'
console.log(attempts); // 3// Numbers stored and retrieved as-is (not coerced to string)
await cache.set('number', 42);
const num = await cache.get<number>('number');
console.log(typeof num); // 'number'
console.log(num); // 42// Strings stay strings (no auto-coercion)
await cache.set('stringNum', '42');
const str = await cache.get<string>('stringNum');
console.log(typeof str); // 'string'
console.log(str); // '42'
console.log(str === '42'); // true
console.log(str === 42); // false// Set and immediate get
await cache.set('rapid', 'value');
const rapid = await cache.get('rapid'); // Should return 'value'
console.log(rapid); // 'value'// First wrap starts, second wrap should coalece
const p1 = cache.wrap('rapid-wrap', async () => {
await new Promise(resolve => setTimeout(resolve, 100));
return 'data';
}, 60000);
// Immediate second call
const p2 = cache.wrap('rapid-wrap', async () => {
throw new Error('Should not execute');
}, 60000);
const [r1, r2] = await Promise.all([p1, p2]);
console.log(r1 === r2); // true (both get same result from single execution)import { CacheableMemory } from 'cacheable';
const cache = createCache({
stores: [
new Keyv({
store: new CacheableMemory({ lruSize: 3 }), // Only 3 items
}),
],
});
// Fill cache
await cache.set('a', 1);
await cache.set('b', 2);
await cache.set('c', 3);
// This evicts least recently used ('a')
await cache.set('d', 4);
console.log(await cache.get('a')); // undefined (evicted)
console.log(await cache.get('b')); // 2
console.log(await cache.get('c')); // 3
console.log(await cache.get('d')); // 4