@desolint/cache
v0.0.1
Published
Generic, fail-safe Redis cache wrapper (get/set/del/clear/has/getOrSet). No root export — import only what you need: @desolint/cache/config, /services.
Downloads
32
Readme
@desolint/cache
Generic, fail-safe Redis cache wrapper: get, set, del, clear, has,
getOrSet.
No root export. import ... from '@desolint/cache' resolves to
nothing — import only what you need:
@desolint/cache/config @desolint/cache/services| Subpath | What it's for |
| ----------- | ------------------------------------------------------------------------------ |
| /config | Open the Redis connection. Nothing else. |
| /services | The actual cache operations — get, set, del, clear, has, getOrSet. |
Requirements
- Node.js 22 or newer (declared in
engines) - npm 7 or newer — npm 7+ installs peer dependencies automatically
Install
npm install @desolint/cacheThat is all you need on npm 7+: ioredis is listed as a peer dependency, so npm resolves and installs it for you.
Neither installs peer dependencies automatically, so name it explicitly:
yarn add @desolint/cache ioredis
# or
pnpm add @desolint/cache ioredisWhy ioredis is a peer dependency, not a regular one
ioredis owns a connection pool to Redis. If this package carried its own copy,
your application and this cache would open two independent sets of connections to the
same Redis server — doubling connections, and making it impossible to share
configuration, event handlers or a mock between them.
Declaring it as a peer dependency means npm reuses the copy your application already has instead of nesting a second one under this package. You keep control of the version; this package just states the range it works with.
Quick start
// config/cache.js
import {initializeCache} from '@desolint/cache/config';
initializeCache({
redisUrl: process.env.REDIS_URL,
defaultTtlSeconds: 300,
onConnection: () => console.log('Cache connected'),
onError: (error) => console.error('Cache error', {error}),
});// services/permissionsServices.js
import {getOrSet, del} from '@desolint/cache/services';
const permissions = await getOrSet({
key: `permissions:${organizationId}:${userId}`,
ttlSeconds: 300,
fn: async () => await fetchPermissionsFromDb({userId, organizationId}),
});
// invalidate on write
await del({key: `permissions:${organizationId}:${userId}`});/config
initializeCache({redisUrl, defaultTtlSeconds?, options?, onConnection?, onError?})
Connects a dedicated ioredis instance and returns it — deliberately
separate from any Redis connection you already run for something else
(BullMQ, pub/sub, ...). A cache connection needs different resilience
tuning than a queue connection: fail-fast (enableOfflineQueue: false,
short commandTimeout) so a slow/unreachable Redis can never hang a
request, versus BullMQ's maxRetriesPerRequest: null. Don't share one
connection between the two.
| Param | Type | Required | Notes |
| ------------------- | ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| redisUrl | string | yes | Redis connection URI. |
| defaultTtlSeconds | number | no | Fallback used by set/getOrSet when a call omits its own ttlSeconds. See Permanent keys below. |
| options | Partial<RedisOptions> | no | Merged over the built-in fail-fast defaults (maxRetriesPerRequest: 1, enableOfflineQueue: false, commandTimeout: 1000) — override any of them. |
| onConnection | () => void | no | Fires on the connection's 'connect' event. |
| onError | (error: Error) => void | no | Fires for both connection-level Redis errors and operation-level errors that /services swallows instead of throwing (see below) — one hook covers both. |
Returns: the ioredis instance itself, if you need it directly.
/services
Every function here is fail-safe: none of them throw. get/has
return null/false on any error — indistinguishable from a genuine
cache miss, by design, so the caller always has a safe path (fall through
to the real data source). set/del/clear swallow their errors too,
reporting them through whichever onError you gave initializeCache, if
any. A down/unreachable Redis degrades your app to "always compute
fresh," never to "the request fails."
| Function | Signature | Returns | What it does |
| ---------- | --------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| get | {key} | T \| null | Reads and JSON-parses a key. null on a miss or any error. |
| set | {key, value, ttlSeconds?} | void | JSON-serializes and writes a key. See Permanent keys below for what happens when ttlSeconds is omitted. |
| del | {key} | void | Removes one key. |
| has | {key} | boolean | Whether a key exists. false on any error. |
| clear | {pattern} | void | Deletes every key matching a pattern (e.g. permissions:*) via SCAN — never KEYS, which blocks the whole Redis instance on a large keyspace. |
| getOrSet | {key, ttlSeconds?, fn} | T | Cache-aside in one call: get → miss → fn() → set → return. On a cache outage, still calls fn() and returns its result — it just won't have anywhere to cache it. |
Permanent keys
set/getOrSet resolve ttlSeconds as: the call's own ttlSeconds, else
initializeCache's defaultTtlSeconds, else no TTL at all — the key
is written with no EX flag and lives forever until explicitly del'd or
clear'd. This is a deliberate, supported way to cache something
permanently (e.g. a lookup table that only changes on deploy) — not an
oversight, and nothing enforces a TTL at any level. If you want every key
in your app to expire by default, set defaultTtlSeconds yourself.
Development
npm install # install dependencies
npm run build # type-check, then bundle each subpath into dist/
npm test # jest
npm run lint # eslintscripts/build.mjs bundles each subpath into one self-contained JS file plus a
.d.ts, then deletes everything else from dist/ — internal modules
(src/redis/*, src/shared/*) never ship, so there is nothing for an editor or a
moduleResolution: "node" consumer to resolve beyond the two public subpaths
documented above.
License
MIT © Desolint — see LICENSE.
Free to use, modify and redistribute, commercially or otherwise. Provided "as is", without warranty or liability of any kind.
