or run

tessl search
Log in

Version

Workspace
tessl
Visibility
Public
Created
Last updated
Describes
npmpkg:npm/cache-manager@7.2.x

docs

examples

edge-cases.mdintegration-patterns.mdreal-world-scenarios.md
index.md
tile.json

tessl/npm-cache-manager

tessl install tessl/npm-cache-manager@7.2.0

Cache Manager for Node.js with support for multi-store caching, background refresh, and Keyv-compatible storage adapters

edge-cases.mddocs/examples/

Edge Cases and Advanced Scenarios

Comprehensive coverage of special values, boundary conditions, and corner cases.

Special Values

Null vs Undefined

// 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

Falsy Values

// 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')); // {}

TTL Edge Cases

Zero and Negative TTL

// 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

Very Large TTL

// 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);

TTL Precision

// 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

Key Edge Cases

Empty String Key

// Empty string is valid key
await cache.set('', 'empty key value');
console.log(await cache.get('')); // 'empty key value'
await cache.del('');

Special Characters in Keys

// 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');

Value Size Edge Cases

Large Values

// 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 References

// 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

Non-Serializable Values

// 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)

Batch Operation Edge Cases

Empty Arrays

// 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

// 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

Very Large Batches

// 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);

Concurrent Operations

Simultaneous Gets

// 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)

Simultaneous Sets

// Race condition - last set wins
await Promise.all([
  cache.set('race', 'value1'),
  cache.set('race', 'value2'),
  cache.set('race', 'value3'),
]);
// Final value is unpredictable

Wrap Coalescing

let 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

Expiration Edge Cases

Just Expired

// 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 at Boundary

// 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);

Multi-Store Edge Cases

Store Inconsistency

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

Store Failure

// 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');

Error Edge Cases

Non-Existent Delete

// 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

Clear Empty Cache

// Clearing empty cache returns true
await cache.clear();
const cleared = await cache.clear();
console.log(cleared); // true

Wrap Function Errors

// 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

Type Coercion

Number to String

// 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

String to Number

// 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

Timing Edge Cases

Rapid Set-Get

// Set and immediate get
await cache.set('rapid', 'value');
const rapid = await cache.get('rapid'); // Should return 'value'
console.log(rapid); // 'value'

Rapid Wrap Calls

// 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)

Memory Management

LRU Eviction

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

Next Steps

  • Real-World Scenarios - Production implementations
  • Core Operations Reference - Complete API with all edge cases
  • Troubleshooting - Solutions to common issues