
Research
/Security News
TensorLake npm SDK Compromised in ChainDrop Shai-Hulud Credential-Stealing Attack
Tensorlake npm SDK version 0.5.144 was compromised in a ChainDrop / Shai-Hulud attack, delivering credential-stealing malware.
Zero-runtime-dependency caching and atomic state toolkit for Node.js services.
cache-hub provides an in-memory LRU + TTL cache, optional Redis integration,
read-through caching, function-level caching, distributed invalidation, stable
cache-key serialization, atomic state backends, and rate-limit state primitives
behind a small CacheLike contract.
Chinese documentation: docs/README.zh-CN.md
dependencies stays empty; Redis is an optional peer dependency.CacheLike; uses SCAN instead of KEYS for production-safe pattern operations.withCache or the FunctionCache registry.npm install cache-hub
Redis-backed features require ioredis:
npm install ioredis
ioredis is an optional peer dependency. Projects that only use memory caching
do not need to install it.
import { MemoryCache } from 'cache-hub';
const cache = new MemoryCache({
maxEntries: 1000,
defaultTtl: 60_000,
enableStats: true,
});
await cache.set('user:1', { name: 'Alice' });
const user = await cache.get<{ name: string }>('user:1');
console.log(user?.name); // Alice
const stats = cache.getStats();
console.log(stats.hitRate); // 0..1
import { MemoryCache } from 'cache-hub';
import { readThrough } from 'cache-hub/read-through';
const cache = new MemoryCache({ defaultTtl: 30_000 });
const user = await readThrough(cache, 30_000, 'user:1', async () => {
return db.findUser(1);
});
readThrough returns cached values immediately on hit. On miss, it runs the
fetcher, writes non-undefined results back to cache, and shares one in-flight
promise for concurrent calls with the same key.
import { MemoryCache } from 'cache-hub';
import { withCache } from 'cache-hub/function-cache';
const cache = new MemoryCache({ maxEntries: 500 });
const getUser = withCache(
async (userId: number) => db.findUser(userId),
{
cache,
ttl: 60_000,
namespace: 'users',
condition: (result) => result !== null,
},
);
const user = await getUser(1);
The default key builder uses stableStringify. Long keys are compressed with a
SHA-256 digest after the configured key length threshold.
import { MemoryCache } from 'cache-hub';
import { MultiLevelCache } from 'cache-hub/multi-level';
import { createRedisCacheAdapter } from 'cache-hub/redis';
const local = new MemoryCache({ maxEntries: 500, defaultTtl: 30_000 });
const remote = createRedisCacheAdapter('redis://localhost:6379');
const cache = new MultiLevelCache({
local,
remote,
remoteTimeoutMs: 50,
backfillOnRemoteHit: true,
writePolicy: 'both',
});
await cache.set('product:42', { name: 'Keyboard' }, 120_000);
const product = await cache.get<{ name: string }>('product:42');
console.log(product?.name); // Keyboard
await remote.close();
import { MemoryCache } from 'cache-hub';
import { DistributedCacheInvalidator } from 'cache-hub/distributed';
const local = new MemoryCache({ maxEntries: 1000 });
const invalidator = new DistributedCacheInvalidator({
redisUrl: process.env.REDIS_URL ?? 'redis://localhost:6379',
cache: local,
channel: 'app:cache-invalidation',
});
await invalidator.invalidate('user:*');
await invalidator.close();
Calling invalidate(pattern) first invalidates the current instance and then
broadcasts the same pattern to other subscribers.
import { createMemoryFixedWindowRateLimitStore } from 'cache-hub/rate-limit';
const store = createMemoryFixedWindowRateLimitStore();
const result = store.increment('rl:user:42', 60_000, 100);
if (result.remaining === 0) {
console.log(`Retry after ${result.retryAfterMs}ms`);
}
Redis-backed rate-limit state uses Lua scripts for atomic increment/decrement:
import { createRedisCacheAdapter } from 'cache-hub/redis';
import { createRedisFixedWindowRateLimitStore } from 'cache-hub/rate-limit';
const redisCache = createRedisCacheAdapter('redis://localhost:6379');
const store = createRedisFixedWindowRateLimitStore(redisCache);
await store.increment('rl:user:1', 60_000, 100);
await store.decrement('rl:user:1');
await store.resetPrefix('rl:user:');
await redisCache.close();
cache-hub/rate-limit is a low-level primitive for middleware authors. It does
not impose a specific HTTP framework adapter.
import { createRedisCacheAdapter } from 'cache-hub/redis';
import { createRedisAtomicStateBackend } from 'cache-hub/atomic';
const redisCache = createRedisCacheAdapter('redis://localhost:6379');
const atomic = createRedisAtomicStateBackend(redisCache);
const result = await atomic.incrementWithTtl('counter:tenant:42', 1, 60_000);
console.log(result.value, result.ttlMs);
await redisCache.close();
The Redis backend uses Lua scripts for atomic read-modify-write behavior. It is
safe to use as a storage primitive for high-concurrency counters where plain
get -> set would lose updates.
cache-hubimport { MemoryCache } from 'cache-hub';
import type { CacheLike, CacheStats, MemoryCacheOptions } from 'cache-hub';
new MemoryCache(options?)| Option | Type | Default | Description |
|---|---|---|---|
maxEntries | number | 10000 | Maximum number of entries before LRU eviction. |
maxMemory | number | 0 | Estimated max memory in bytes. 0 disables the memory limit. |
defaultTtl | number | 0 | Default TTL in milliseconds. 0 means no expiration. |
cleanupInterval | number | 0 | Periodic expired-entry cleanup interval in milliseconds. |
enableStats | boolean | true | Enables hit/miss statistics. |
enableTags | boolean | false | Enables tag indexes and invalidateByTag. |
enabled | boolean | true | Disables cache reads/writes when set to false. |
MemoryCache also exposes getRemainingTtl(key) and
getRemainingTtlMany(keys). A non-expiring existing key returns null; a
missing or expired key returns undefined.
CacheLikeEvery cache implementation can be used through this interface:
interface CacheLike {
get<T = any>(key: string): T | undefined | Promise<T | undefined>;
set(key: string, value: any, ttl?: number): void | Promise<void>;
del(key: string): boolean | Promise<boolean>;
exists(key: string): boolean | Promise<boolean>;
has(key: string): boolean | Promise<boolean>;
clear(): void | Promise<void>;
keys(pattern?: string): string[] | Promise<string[]>;
getMany(keys: string[]): Record<string, any> | Promise<Record<string, any>>;
setMany(entries: Record<string, any>, ttl?: number): boolean | Promise<boolean>;
delMany(keys: string[]): number | Promise<number>;
delPattern(pattern: string): number | Promise<number>;
getRemainingTtl?(key: string): number | null | undefined | Promise<number | null | undefined>;
getRemainingTtlMany?(keys: string[]): Record<string, number | null> | Promise<Record<string, number | null>>;
invalidateByTag?(tag: string): void | Promise<void>;
getStats?(): CacheStats;
resetStats?(): void;
destroy?(): void;
setLockManager?(lm: LockManager): void;
}
cache-hub/read-throughimport { readThrough } from 'cache-hub/read-through';
function readThrough<V>(
cache: CacheLike,
ttl: number,
key: string,
fetcher: () => Promise<V>,
): Promise<V>;
ttl <= 0 runs the fetcher without writing to cache.null is cached as a valid value.undefined is treated as a miss signal and is not cached.cache-hub/multi-levelimport { MultiLevelCache } from 'cache-hub/multi-level';
new MultiLevelCache(options);
| Option | Type | Default | Description |
|---|---|---|---|
local | CacheLike | required | L1 local cache. |
remote | CacheLike | undefined | Optional L2 remote cache. |
writePolicy | 'both' | 'local-first-async-remote' | 'both' | Write-through or local-first async write policy. |
backfillOnRemoteHit | boolean | true | Backfills L1 after L2 hit. Preserves remote TTL when supported. |
remoteTimeoutMs | number | 50 | L2 get timeout in milliseconds. Timeout falls back to L1 miss behavior. |
publish | (msg) => void | undefined | Optional callback for distributed invalidation messages. |
cache-hub/redisimport { createRedisCacheAdapter } from 'cache-hub/redis';
const adapter = createRedisCacheAdapter('redis://localhost:6379');
The Redis adapter implements CacheLike and adds:
| Method | Description |
|---|---|
getRemainingTtl(key) | Returns remaining TTL in milliseconds, null for non-expiring keys, and undefined for missing keys. |
getRemainingTtlMany(keys) | Batch TTL lookup. |
close() | Closes only the connection created by the adapter. Externally supplied ioredis instances are not closed. |
getRedisInstance() | Returns the underlying ioredis instance for advanced use cases. |
Pattern operations use SCAN with COUNT 100; KEYS is not used.
cache-hub/function-cacheimport { FunctionCache, withCache } from 'cache-hub/function-cache';
const cachedFn = withCache(asyncFn, {
cache,
ttl: 60_000,
namespace: 'users',
keyBuilder: (...args) => `custom:${args.join(':')}`,
condition: (result) => result !== null,
});
withCache(fn).invalidateAll() only deletes keys that were actually written by
that wrapped function. It does not delete unrelated manual keys that happen to
share the same prefix.
cache-hub/distributedimport { DistributedCacheInvalidator } from 'cache-hub/distributed';
const invalidator = new DistributedCacheInvalidator({
cache,
redisUrl: 'redis://localhost:6379',
});
| Option | Description |
|---|---|
cache | Required cache instance that receives delPattern(pattern) calls. |
redisUrl | Redis URL. Defaults to redis://localhost:6379 when neither redisUrl nor redis is provided. |
redis | Existing ioredis instance used for publishing. |
channel | Pub/Sub channel. Defaults to cache-hub:invalidate. |
instanceId | Unique instance id used to filter self-sent messages. |
cache-hub/atomicimport {
createMemoryAtomicStateBackend,
createRedisAtomicStateBackend,
} from 'cache-hub/atomic';
| API | Description |
|---|---|
MemoryAtomicStateBackend | Synchronous in-memory atomic counter backend. |
RedisAtomicStateBackend | Async Redis atomic counter backend backed by Lua scripts. |
incrementWithTtl(key, amount, ttlMs) | Atomically increments a counter and assigns TTL on first write. |
decrement(key, amount?) | Atomically decrements a counter while preserving TTL. |
reset(key) | Deletes one atomic state key. |
resetPrefix(prefix) | Deletes keys under a literal prefix with SCAN. |
cache-hub/rate-limitimport {
createMemoryFixedWindowRateLimitStore,
createMemoryRateLimitStateStore,
createRedisFixedWindowRateLimitStore,
createRedisRateLimitStateStore,
} from 'cache-hub/rate-limit';
| API | Description |
|---|---|
MemoryFixedWindowRateLimitStore | Synchronous in-memory fixed-window counter. |
RedisFixedWindowRateLimitStore | Async Redis fixed-window counter backed by Lua scripts. |
MemoryRateLimitStateStore | In-memory sliding-window, token-bucket, and leaky-bucket state primitives. |
RedisRateLimitStateStore | Redis Lua-backed sliding-window, token-bucket, and leaky-bucket state primitives. |
increment(key, windowMs, limit, amount?) | Increments the current window and returns hits, remaining quota, reset time, and retry-after. |
decrement(key, amount?) | Rolls back a counter, useful when downstream work fails after reservation. |
checkSlidingWindow(key, windowMs, limit, cost?) | Reserves sliding-window state and returns an opaque rollback token when allowed. |
consumeTokenBucket(key, capacity, refillPerSecond, cost?) | Atomically consumes token-bucket capacity and returns retry timing. |
consumeLeakyBucket(key, capacity, leakPerSecond, cost?) | Atomically consumes leaky-bucket capacity and returns retry timing. |
reset(key) | Deletes one rate-limit key. |
resetPrefix(prefix) | Deletes keys under a literal prefix with SCAN. |
cache-hub/stringifyimport { stableStringify } from 'cache-hub/stringify';
stableStringify({ b: 2, a: 1 }); // '{"a":1,"b":2}'
stableStringify(NaN); // '"__NaN__"'
stableStringify sorts object keys, handles cycles, supports custom
serializers, and keeps cache keys deterministic across processes.
Redis-backed examples use:
redis://localhost:6379
This URL means local Redis on port 6379 with no password. If your Redis
requires authentication, use the standard Redis URL form:
redis://:password@host:6379
For tests, set REDIS_URL when you need a non-default endpoint:
REDIS_URL=redis://127.0.0.1:6379 npm run test:integration
# All Vitest tests; Redis integration tests run when Redis is reachable
npm test
# Coverage
npm run test:coverage
# Redis integration tests only
npm run test:integration
# Skip Redis integration tests explicitly
SKIP_INTEGRATION=true npm run test:integration
npm test runs the full Vitest suite. Redis integration cases execute when a
reachable Redis server is available; otherwise they log a skip message.
Integration tests require ioredis in the development environment. The package
keeps ioredis as an optional peer dependency for consumers and as a dev
dependency for real integration coverage.
Coverage target: statements, branches, functions, and lines all at 100%.
# Build first, then print benchmark tables
npm run benchmark
# Print JSON to stdout
npm run benchmark -- --json
# Write JSON to a file
npm run benchmark -- --json --output benchmark-results.json
The benchmark script focuses on direct library hot paths. Treat the numbers as local performance signals, not as a replacement for production HTTP middleware or real Redis network benchmarks.
# Type check only
npm run typecheck
# Build ESM, CommonJS, and declaration files
npm run build
Build output:
dist/
├── esm/
├── cjs/
└── types/
The package exposes matching ESM, CJS, and type declaration paths for every public subpath export.
| Node.js | Status |
|---|---|
| 18 LTS | Supported |
| 20 LTS | Supported |
| 22 LTS | Supported |
cache-hub requires Node.js >=18.0.0.
| Symptom | Check |
|---|---|
redis-adapter requires ioredis | Install ioredis in the consuming project: npm install ioredis. |
| Redis tests are skipped | Confirm Redis is running and REDIS_URL points to the reachable endpoint. |
| Redis auth fails | Use redis://:password@host:6379 or pass an already configured ioredis instance. |
| Pattern deletes are slower than expected | delPattern and keys use SCAN for safety; this avoids blocking Redis like KEYS. |
Cache misses after storing undefined | undefined is the miss signal. Use null or a sentinel object for cacheable empty results. |
FAQs
Zero-runtime-dependency caching and atomic state toolkit for Node.js services.
The npm package cache-hub receives a total of 64 weekly downloads. As such, cache-hub popularity was classified as not popular.
We found that cache-hub demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Research
/Security News
Tensorlake npm SDK version 0.5.144 was compromised in a ChainDrop / Shai-Hulud attack, delivering credential-stealing malware.

Research
/Security News
Socket found 16 malicious Firefox extensions designed to steal crypto wallet recovery phrases and private keys using cloned Rabby and OKX interfaces.

Product
Socket now scans VS Code extensions, giving teams early detection of risky behaviors, hidden capabilities, and supply chain threats in developer tools.