npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@vita-mojo/service-cache-adapter

v0.0.7-VMOS-13194-8ec3dee-1077-rc.0

Published

A two-layer caching adapter for HTTP requests built on [cacheable](https://www.npmjs.com/package/cacheable) and [iovalkey](https://www.npmjs.com/package/iovalkey). Provides axios request caching with multiple staleness strategies, automatic Redis circuit-

Readme

service-cache-adapter

A two-layer caching adapter for HTTP requests built on cacheable and iovalkey. Provides axios request caching with multiple staleness strategies, automatic Redis circuit-breaking, and pattern-based cache invalidation.

Architecture

                        ┌──────────────────────────────────────────┐
                        │            AxiosCacheService             │
                        │  createCachedAxios() / attachCaching()   │
                        └─────────────────┬────────────────────────┘
                                          │
                        ┌─────────────────▼────────────────────────┐
                        │           CacheInterceptor               │
                        │  Axios request/response interceptors     │
                        │  Short-circuits via custom config.adapter│
                        └─────────────────┬────────────────────────┘
                                          │
                        ┌─────────────────▼────────────────────────┐
                        │        CacheStrategyExecutor             │
                        │  STALE_ON_ERROR │ FORCE_STALE │ SWU     │
                        │  fetchWithTimeout (Promise.race)         │
                        └─────────────────┬────────────────────────┘
                                          │
                        ┌─────────────────▼────────────────────────┐
                        │            CacheBackend                  │
                        │  get() → getWithTimeout (Promise.race)   │
                        │  set() → nonBlocking: true               │
                        └──────┬─────────────────────────┬─────────┘
                               │                         │
                  ┌────────────▼──────────┐  ┌───────────▼──────────┐
                  │  L1: CacheableMemory  │  │  L2: Redis (iovalkey)│
                  │  In-process, ~1min    │  │  Shared, TTL + 30d   │
                  │  LRU eviction (1000)  │  │  Circuit breaker     │
                  └───────────────────────┘  └──────────────────────┘

Request flow

  1. Axios request enters the request interceptor
  2. CacheKeyGenerator builds a deterministic key from namespace, method, URL, body, and relevant headers
  3. CacheStrategyExecutor checks L1 (memory), then L2 (Redis) via CacheBackend
  4. On cache hit (fresh): the interceptor sets a custom config.adapter that resolves immediately with cached data — no HTTP call is made
  5. On cache miss/stale: the origin fetcher executes the actual HTTP request, and the result is cached
  6. Response interceptor adds x-cache-status, x-cache-date, and x-cache-age headers

TTL model

  • defaultTTL (default: 5m) — time after which an entry is considered stale
  • minRetentionTTL (default: 30d) — how long stale entries remain in Redis for fallback
  • Effective Redis TTL = defaultTTL + minRetentionTTL — entries stay in Redis for the full retention window
  • Memory TTL (default: 1m) — short-lived L1 for hot data, LRU-evicted at 1000 items

Non-blocking guarantees

  • Writes (set): nonBlocking: true uses Promise.race() internally — returns after L1 write, L2 fires in background
  • Reads (get): L1 hit returns instantly. L1 miss awaits L2, bounded by operationTimeout (default: 800ms) via Promise.race
  • Deletes: same as writes — Promise.race(), L1 returns first

Cache strategies

| Strategy | Behaviour | | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | STALE_ON_ERROR (default) | Return fresh cache immediately. If stale, fetch origin. On 5xx/network/timeout error, fall back to stale data. 4xx errors propagate | | FORCE_STALE | Always return cached data (fresh or stale). Only fetches origin on first miss | | STALE_WHILE_UPDATE | Return stale data immediately, refresh in background (fire-and-forget) |

Redis resilience

Circuit breaker

When the Redis client emits a connection error:

  1. Suspend: disconnect the Redis client, detach L2 from cacheable. All operations fall back to L1 only.
  2. Cooldown: wait 5 minutes.
  3. Resume: create a fresh Redis client and re-attach L2.

Operation timeout

All cacheable.get() calls are wrapped in a Promise.race with operationTimeout (default: 800ms). If Redis is slow but reachable, reads fall through as cache misses instead of blocking requests.

Redis Cluster support

Enable cluster mode for AWS MemoryDB or ElastiCache Cluster by setting VM_CACHE_ADAPTER_REDIS_CLUSTER=true (or redis.cluster: true in code config).

In cluster mode:

  • The main cache client uses iovalkey Cluster, which automatically handles slot routing and MOVED/ASK redirections.
  • Reads are scaled to replica nodes (scaleReads: 'slave').
  • Cache invalidation creates standalone Redis clients per master node (since scanStream is not available on the Cluster client). Scan clients are initialized lazily on the first invalidateByPattern call to ensure the cluster topology has been discovered.
  • Invalidation uses expires=0 (logical expiry) instead of DELETE/UNLINK to avoid CROSSSLOT errors. The cacheable/keyv layer treats entries with expires=0 as expired on next read.

Installation

npm install @vita-mojo/service-cache-adapter

Usage

NestJS module

import { CacheAdapterModule } from '@vita-mojo/service-cache-adapter';

@Module({
  imports: [
    CacheAdapterModule.register({
      namespacePrefix: 'my-service',
    }),
  ],
})
export class AppModule {}

With custom config overrides:

CacheAdapterModule.registerWithConfig(
  {
    defaultTTL: ms('10m'),
    redis: { host: 'redis.internal', port: 6379, tls: true },
  },
  {
    namespacePrefix: 'my-service',
    enableInvalidationEndpoint: true,
    invalidationToken: process.env.CACHE_INVALIDATION_TOKEN,
  }
);

With async configuration (for DI-based config):

CacheAdapterModule.registerAsync({
  imports: [ConfigModule],
  useFactory: (configService: ConfigService) => ({
    defaultTTL: configService.get('CACHE_TTL'),
    defaultTimeout: 10_000,
    minRetentionTTL: ms('30d'),
    defaultStrategy: CacheStrategy.STALE_ON_ERROR,
    redis: {
      host: configService.get('REDIS_HOST'),
      port: configService.get('REDIS_PORT'),
      cluster: configService.get('REDIS_CLUSTER') === 'true',
      tls: true,
    },
  }),
  inject: [ConfigService],
  enableInvalidationEndpoint: true,
  invalidationToken: process.env.CACHE_INVALIDATION_TOKEN,
});

Creating cached axios instances

import { AxiosCacheService } from '@vita-mojo/service-cache-adapter';

@Injectable()
export class CatalogService {
  private readonly http: AxiosInstance;

  constructor(private readonly axiosCacheService: AxiosCacheService) {
    this.http = axiosCacheService.createCachedAxios({
      baseURL: 'https://api.catalog.internal',
    });
  }

  async getMenu(tenantUUID: string) {
    const response = await this.http.get('menu', {
      cache: {
        namespace: 'catalog',
        strategy: CacheStrategy.STALE_ON_ERROR,
        ttl: ms('10m'),
      },
    });

    return response.data;
  }
}

Or attach caching to an existing axios instance:

axiosCacheService.attachCaching(existingAxiosInstance);

Per-request cache options

const response = await this.http.get('/endpoint', {
  cache: {
    namespace: 'my-namespace',       // Required: cache key namespace
    strategy: CacheStrategy.FORCE_STALE, // Override default strategy
    ttl: 60_000,                     // Override TTL (ms)
    timeout: 3000,                   // Override origin fetch timeout (ms)
    bypassCache: false,              // Skip caching entirely
    relevantHeaders: ['authorization', 'tenant'], // Headers included in cache key
  },
});

Standalone (non-NestJS)

import { createCacheAdapter } from '@vita-mojo/service-cache-adapter';

const cache = createCacheAdapter({
  defaultTTL: ms('10m'),
  redis: { host: 'localhost', port: 6379 },
});

const client = cache.axiosCacheService.createCachedAxios({
  baseURL: 'https://api.example.com',
});

Cache invalidation

Programmatic:

import { CacheInvalidationService } from '@vita-mojo/service-cache-adapter';

// Invalidate by pattern
await cacheInvalidationService.invalidateByPattern('catalog:GET:*');

// Invalidate by namespace (shorthand for 'namespace:*')
await cacheInvalidationService.invalidateByNamespace('catalog');

REST endpoint (when enableInvalidationEndpoint: true):

DELETE /cache/invalidate?pattern=catalog:GET:*
Authorization: Bearer <invalidationToken>

Cache key format

{namespacePrefix}:{namespace}:{METHOD}:{url_hash}:{body_hash}:{headers_hash}
  • url_hash: SHA256 of normalized URL (trailing slash removed, query params sorted)
  • body_hash: SHA256 of JSON-stringified body, or nobody if no body
  • headers_hash: SHA256 of sorted relevant headers (optional)

Default relevant headers: authorization, accept, content-type, tenant, store, menu, x-requested-from, locale.

Response headers

| Header | Description | | ---------------- | ------------------------------------------------ | | x-cache-status | HIT, MISS, or STALE | | x-cache-date | ISO timestamp when the entry was originally cached | | x-cache-age | Age of the cached entry in seconds |

Configuration

Environment variables

| Variable | Default | Description | | ------------------------------------------ | ---------------- | -------------------------------------- | | VM_CACHE_ADAPTER_DEFAULT_TIMEOUT | 10s | Origin fetch timeout | | VM_CACHE_ADAPTER_DEFAULT_STRATEGY | stale-on-error | Default cache strategy | | VM_CACHE_ADAPTER_DEFAULT_TTL | 5m | Time before entry is considered stale | | VM_CACHE_ADAPTER_MIN_RETENTION_TTL | 30d | How long stale entries remain in Redis | | VM_CACHE_ADAPTER_NAMESPACE | cache | Default cache namespace | | VM_CACHE_ADAPTER_MEMORY_TTL | 1m | L1 memory cache TTL | | VM_CACHE_ADAPTER_MEMORY_LRU_SIZE | 1000 | Max items in L1 before LRU eviction | | VM_CACHE_ADAPTER_REDIS_HOST | localhost | Redis host | | VM_CACHE_ADAPTER_REDIS_PORT | 6379 | Redis port | | VM_CACHE_ADAPTER_REDIS_USERNAME | — | Redis username | | VM_CACHE_ADAPTER_REDIS_PASSWORD | — | Redis password | | VM_CACHE_ADAPTER_REDIS_DB | — | Redis database index | | VM_CACHE_ADAPTER_REDIS_TLS | false | Enable TLS (required for AWS MemoryDB) | | VM_CACHE_ADAPTER_REDIS_KEY_PREFIX | cache: | Key prefix for Redis | | VM_CACHE_ADAPTER_REDIS_OPERATION_TIMEOUT | 800 | Redis connect/command timeout in ms | | VM_CACHE_ADAPTER_REDIS_CLUSTER | false | Enable Redis Cluster mode |

Building

nx build service-cache-adapter

Testing

nx test service-cache-adapter