@molecule/api-cache-redis
Provider bond · cache · API (Node) · v1.1.0 · Apache-2.0
Redis cache provider for molecule.dev.
npm install @molecule/api-cache-redisnpm · Source on GitHub · Implements @molecule/api-cache
How it works
@molecule/api-cache-redis is a provider bond on the API (Node) side: it implements the cache core interface (@molecule/api-cache) with a concrete vendor or library behind it.
Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.
import { setProvider } from '@molecule/api-cache'
import { createProvider, provider } from '@molecule/api-cache-redis'
// Default provider reads REDIS_URL (or REDIS_HOST/REDIS_PORT/REDIS_PASSWORD) lazily:
setProvider(provider)
// Or an explicit server — explicit config beats ambient env:
setProvider(createProvider({ url: 'redis://my-redis:6379' }))Works with: @molecule/api-bond, @molecule/api-cache, @molecule/api-secrets
Secrets: REDIS_URL (optional)
Live demos
Running apps built with Molecule that install this package — open the app or study the template.
Reference
Redis cache provider for molecule.dev.
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
import { setProvider } from '@molecule/api-cache'
import { createProvider, provider } from '@molecule/api-cache-redis'
// Default provider reads REDIS_URL (or REDIS_HOST/REDIS_PORT/REDIS_PASSWORD) lazily:
setProvider(provider)
// Or an explicit server — explicit config beats ambient env:
setProvider(createProvider({ url: 'redis://my-redis:6379' }))
Type
provider
Installation
npm install @molecule/api-cache-redis @molecule/api-bond @molecule/api-cache @molecule/api-secrets ioredis
API
Interfaces
RedisOptions
Options for creating a Redis cache provider.
interface RedisOptions {
url?: string
host?: string
port?: number
password?: string
db?: number
keyPrefix?: string
/**
* Maximum retry attempts for a command before ioredis rejects it instead of
* continuing to retry. Forwarded to ioredis unchanged; omitted/`undefined`
* keeps ioredis' own default (`20`, each attempt backing off up to 2s — a
* down Redis can therefore take 10s+ to surface as an error). Pass a small
* value (e.g. `1`) for fail-fast cache semantics.
*/
maxRetriesPerRequest?: number
/**
* Whether ioredis queues commands issued while disconnected instead of
* rejecting them immediately. Forwarded to ioredis unchanged; omitted keeps
* ioredis' own default (`true`). Set to `false` for fail-fast cache
* semantics — commands reject immediately instead of buffering.
*/
enableOfflineQueue?: boolean
/**
* Milliseconds to wait for a command's reply before ioredis times it out.
* Forwarded to ioredis unchanged; omitted keeps ioredis' own default (no
* timeout). Set a bounded value (e.g. `2000`) to cap the worst-case stall
* from an unreachable-but-connected Redis.
*/
commandTimeout?: number
}
CacheOptions
Cache entry options.
interface CacheOptions {
/**
* Time-to-live in seconds.
*/
ttl?: number
/**
* Tags for cache invalidation.
*/
tags?: string[]
}
CacheProvider
Cache provider interface.
All cache providers must implement this interface.
interface CacheProvider {
/**
* Gets a value from the cache.
*
* @param key - Cache key
*/
get<T = unknown>(key: string): Promise<T | undefined>
/**
* Sets a value in the cache.
*
* @param key - Cache key
* @param value - Value to cache
* @param options - Cache options (ttl, tags)
*/
set<T = unknown>(key: string, value: T, options?: CacheOptions): Promise<void>
/**
* Deletes a value from the cache.
*
* @param key - Cache key
* @returns true if the key existed and was deleted
*/
delete(key: string): Promise<boolean>
/**
* Checks if a key exists in the cache.
*
* @param key - Cache key
*/
has(key: string): Promise<boolean>
/**
* Gets multiple values from the cache.
*
* @param keys - Array of cache keys
*/
getMany?<T = unknown>(keys: string[]): Promise<Map<string, T>>
/**
* Sets multiple values in the cache.
*
* @param entries - Array of [key, value] pairs
* @param options - Cache options applied to all entries
*/
setMany?<T = unknown>(entries: Array<[string, T]>, options?: CacheOptions): Promise<void>
/**
* Deletes multiple values from the cache.
*
* @param keys - Array of cache keys
*/
deleteMany?(keys: string[]): Promise<number>
/**
* Invalidates all cache entries with the given tag.
*
* @param tag - Tag to invalidate
*/
invalidateTag?(tag: string): Promise<void>
/**
* Clears all cache entries.
*/
clear?(): Promise<void>
/**
* Closes the cache connection.
*/
close?(): Promise<void>
/**
* Proves the cache backend is reachable with one cheap round trip (Redis
* `PING`, a Memcached read).
*
* Unlike the data methods, this MUST reject when the backend cannot be
* reached — `has()` and `get()` deliberately treat an outage as a cache miss,
* so neither can answer a readiness probe. Providers without an external
* backend (memory) need not implement it.
*/
ping?(): Promise<void>
/**
* Gets or sets a value using a factory function.
*
* @param key - Cache key
* @param factory - Function to generate the value if not cached
* @param options - Cache options
*/
getOrSet?<T = unknown>(key: string, factory: () => Promise<T>, options?: CacheOptions): Promise<T>
}
Functions
registerLifecycleIntegration(resources)
Register the cache readiness check and shutdown hook with the bonded process-lifecycle provider.
function registerLifecycleIntegration(resources: {
ping: () => Promise<void>
close: () => Promise<void>
created: () => boolean
}): void
resources— The probe and close functions this bond owns.
createProvider(options)
Creates a Redis-backed cache provider that implements the CacheProvider
interface using ioredis. Supports tag-based invalidation via Redis sets.
Reads REDIS_URL, REDIS_HOST, REDIS_PORT, and REDIS_PASSWORD from env.
function createProvider(options?: RedisOptions): CacheProvider
options— Redis connection and behavior options (URL, host, port, password, key prefix).
Returns: A CacheProvider backed by Redis.
defaultProviderCreated()
Whether the lazy default provider has been created.
Readiness and shutdown wiring must not FORCE a connection.
function defaultProviderCreated(): boolean
Returns: true once the default provider exists.
closeDefaultProvider()
Close the lazy default provider, if it was created.
function closeDefaultProvider(): Promise<void>
Returns: Resolves once the Redis connection has quit.
createClient(options)
Creates a raw ioredis Redis client for direct API access,
bypassing the CacheProvider abstraction.
function createClient(options?: Omit<RedisOptions, 'keyPrefix'>): Redis
options— Redis connection options (URL, host, port, password, db).
Returns: A raw ioredis Redis client instance.
Constants
provider
The provider implementation.
const provider: CacheProvider
cacheRedisSecretDefinitions
Secret definitions required by the Redis cache bond.
const cacheRedisSecretDefinitions: SecretDefinition[]
Core Interface
Implements @molecule/api-cache interface.
Bond Wiring
Setup function to register this provider with the core interface:
import { setProvider } from '@molecule/api-cache'
import { provider } from '@molecule/api-cache-redis'
export function setupCacheRedis(): void {
setProvider(provider)
}
Injection Notes
Requirements
Peer dependencies:
@molecule/api-bond^1.0.1@molecule/api-cache^1.0.1@molecule/api-secrets^1.0.1
Environment Variables
REDIS_URL(optional) — Redis connection URL — default:redis://localhost:6379- Provisioned automatically in molecule.dev sandboxes — manual setup only needed outside the platform.
- Setup: Redis connection string (redis:// or rediss:// for TLS). molecule.dev runs a Redis inside your app's container automatically (dev and production) — set this only to use an external/managed Redis; locally, the Docker Compose default works.
- Example:
redis://localhost:6379
Runtime Dependencies
-
@molecule/api-bond -
@molecule/api-cache -
@molecule/api-secrets -
ioredis -
Explicit config beats ambient env.
createProvider({ host, port, ... })connects to exactly that server even whenREDIS_URLis set in the environment — env vars only fill in options the call site left unspecified. -
clear()scans and deletes only keys under this provider'skeyPrefix(SCAN MATCH <keyPrefix>*+ batchedUNLINK) — it never runsFLUSHDB/FLUSHALL, so it is safe to call on a Redis db shared with sessions,@molecule/api-cron-bullmqqueues, or rate-limit counters from other apps. -
Reads degrade like a cache miss; writes still throw.
get()/has()/getMany()catch Redis/connection errors, log them, and returnundefined/false/an empty map (matching@molecule/api-cache-memcached's read behavior) — check the logs to tell a down Redis apart from a real miss.set()/delete()/clear()still throw: a write failure must be visible to the caller, it can't silently look like "nothing to cache." -
When Redis is unreachable, commands do not fail fast by default — ioredis queues them offline and retries (default
maxRetriesPerRequest: 20, ~10s+ per command) before rejecting. PassmaxRetriesPerRequest,enableOfflineQueue, and/orcommandTimeouttocreateProvider(options)to bound this (e.g.{ maxRetriesPerRequest: 1, commandTimeout: 2000 }surfaces a down Redis in ~2s instead of 10s+); they are forwarded to the underlying ioredis client unchanged. -
Tag membership stays exact, not "ever-tagged":
_tag:<tag>Redis SETs are kept in sync via a reverse index (_tags:<key>), sodelete()/deleteMany()and a re-set()that drops or changes a key's tags bothSREMit out of its OLD tag sets first —invalidateTag()never deletes a key that no longer carries that tag. The tag SETs themselves are still stored WITHOUT a TTL and are only cleared byinvalidateTag()orclear()(a tagged key that merely expires leaves its tag membership until then).
E2E Tests
Integration checklist — exercise the REAL behavior end-to-end (drive the app action that reads/writes this cache in the live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:
-
getOrSet(key, factory, options)computes ONCE on a miss and serves the cached copy afterward: drive the same screen/endpoint twice withinCacheOptions.ttland confirm the expensive factory (DB/upstream call) runs only on the FIRST load — the second read returns the cached value without re-running it. -
set(key, value)thenget<T>(key)round-trips the same value back into the app (the cached data renders identically to the source data). - Deletion reflects a miss: after
delete(key),has(key)isfalseandget(key)returnsundefined, so the app recomputes fresh from source instead of serving the removed entry. - TTL expiry refetches: once
CacheOptions.ttlseconds elapse, the next read recomputes fresh data — a stale value is NEVER served past its TTL. - A write that changes the underlying record invalidates its key (via
delete()or a re-set()) so the very next read reflects the change — the app never serves a stale cached copy after an update. - AUTHORIZATION: a per-user or per-tenant value is cached under a key that
INCLUDES the user/tenant id (
user:123:profile, never a globalprofile). Load user A's data, then user B's — B must receive B's own data, never A's cached copy. A shared key for per-user data is a cross-user data leak, not an optimization. - A cache backend failure or miss DEGRADES to computing from source: the request still succeeds (getProvider()/get() unavailable falls back to the real query) rather than erroring the whole request.