New:Microsoft Teams Notifications Are Now Available in Socket.Learn more →
Get Started

cache-hub

Package Overview
Dependencies
Maintainers
1
Versions
11
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

cache-hub

Zero-runtime-dependency multi-level caching toolkit for Node.js services.

Source
npmnpm
Version
2.0.0
Version published
Weekly downloads
129
10.26%
Maintainers
1
Weekly downloads
 
Created
Source

cache-hub

Zero-runtime-dependency multi-level caching 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, and fixed-window rate-limit primitives behind a small CacheLike contract.

Node.js License: MIT Coverage: 100%

Chinese documentation: docs/README.zh-CN.md

Table of Contents

Why cache-hub

  • Zero runtime dependencies - dependencies stays empty; Redis is an optional peer dependency.
  • Memory cache with LRU + TTL - O(1) operations with entry-count and memory-size limits.
  • Multi-level cache - L1 memory plus optional L2 remote cache, TTL-preserving backfill, timeout fallback, and configurable write policy.
  • Redis adapter - wraps ioredis as CacheLike; uses SCAN instead of KEYS for production-safe pattern operations.
  • Read-through caching - cache miss fetch, write-back, and in-flight request de-duplication.
  • Function cache - cache any async function with withCache or the FunctionCache registry.
  • Distributed invalidation - Redis Pub/Sub broadcasts cache invalidation across service instances.
  • Stable key serialization - deterministic cache keys with sorted object keys, cycle handling, and special value sentinels.
  • Rate-limit primitives - optional fixed-window stores for memory and Redis-backed HTTP middleware implementations.
  • Dual package format - ESM and CommonJS builds with subpath exports.

Installation

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.

Quick Start

Memory Cache

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

Read-through Cache

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.

Function Cache

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.

Multi-level Cache

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

Distributed Invalidation

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.

Fixed-window Rate-limit Store

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.

Module Reference

cache-hub

import { MemoryCache } from 'cache-hub';
import type { CacheLike, CacheStats, MemoryCacheOptions } from 'cache-hub';

new MemoryCache(options?)

OptionTypeDefaultDescription
maxEntriesnumber10000Maximum number of entries before LRU eviction.
maxMemorynumber0Estimated max memory in bytes. 0 disables the memory limit.
defaultTtlnumber0Default TTL in milliseconds. 0 means no expiration.
cleanupIntervalnumber0Periodic expired-entry cleanup interval in milliseconds.
enableStatsbooleantrueEnables hit/miss statistics.
enableTagsbooleanfalseEnables tag indexes and invalidateByTag.
enabledbooleantrueDisables 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.

CacheLike

Every 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-through

import { 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.
  • Same-key concurrent calls share one in-flight promise.

cache-hub/multi-level

import { MultiLevelCache } from 'cache-hub/multi-level';

new MultiLevelCache(options);
OptionTypeDefaultDescription
localCacheLikerequiredL1 local cache.
remoteCacheLikeundefinedOptional L2 remote cache.
writePolicy'both' | 'local-first-async-remote''both'Write-through or local-first async write policy.
backfillOnRemoteHitbooleantrueBackfills L1 after L2 hit. Preserves remote TTL when supported.
remoteTimeoutMsnumber50L2 get timeout in milliseconds. Timeout falls back to L1 miss behavior.
publish(msg) => voidundefinedOptional callback for distributed invalidation messages.

cache-hub/redis

import { createRedisCacheAdapter } from 'cache-hub/redis';

const adapter = createRedisCacheAdapter('redis://localhost:6379');

The Redis adapter implements CacheLike and adds:

MethodDescription
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-cache

import { 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/distributed

import { DistributedCacheInvalidator } from 'cache-hub/distributed';

const invalidator = new DistributedCacheInvalidator({
    cache,
    redisUrl: 'redis://localhost:6379',
});
OptionDescription
cacheRequired cache instance that receives delPattern(pattern) calls.
redisUrlRedis URL. Defaults to redis://localhost:6379 when neither redisUrl nor redis is provided.
redisExisting ioredis instance used for publishing.
channelPub/Sub channel. Defaults to cache-hub:invalidate.
instanceIdUnique instance id used to filter self-sent messages.

cache-hub/rate-limit

import {
    createMemoryFixedWindowRateLimitStore,
    createRedisFixedWindowRateLimitStore,
} from 'cache-hub/rate-limit';
APIDescription
MemoryFixedWindowRateLimitStoreSynchronous in-memory fixed-window counter.
RedisFixedWindowRateLimitStoreAsync Redis fixed-window counter backed by Lua scripts.
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.
reset(key)Deletes one rate-limit key.
resetPrefix(prefix)Deletes keys under a literal prefix with SCAN.

cache-hub/stringify

import { 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 Defaults

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

Testing

# 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%.

Benchmarking

# 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.

Build

# 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 Support

Node.jsStatus
18 LTSSupported
20 LTSSupported
22 LTSSupported

cache-hub requires Node.js >=18.0.0.

Troubleshooting

SymptomCheck
redis-adapter requires ioredisInstall ioredis in the consuming project: npm install ioredis.
Redis tests are skippedConfirm Redis is running and REDIS_URL points to the reachable endpoint.
Redis auth failsUse redis://:password@host:6379 or pass an already configured ioredis instance.
Pattern deletes are slower than expecteddelPattern and keys use SCAN for safety; this avoids blocking Redis like KEYS.
Cache misses after storing undefinedundefined is the miss signal. Use null or a sentinel object for cacheable empty results.

License

MIT

Keywords

cache

FAQs

Package last updated on 01 Jun 2026

Related posts