redis-graph-cache 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +857 -0
- package/dist/core/compression.d.ts +58 -0
- package/dist/core/compression.js +115 -0
- package/dist/core/hydration-engine.d.ts +68 -0
- package/dist/core/hydration-engine.js +288 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.js +29 -0
- package/dist/core/lua-scripts.d.ts +148 -0
- package/dist/core/lua-scripts.js +259 -0
- package/dist/core/normalization-engine.d.ts +76 -0
- package/dist/core/normalization-engine.js +286 -0
- package/dist/core/redis-connection-manager.d.ts +255 -0
- package/dist/core/redis-connection-manager.js +745 -0
- package/dist/core/schema-manager.d.ts +133 -0
- package/dist/core/schema-manager.js +432 -0
- package/dist/core/serializer.d.ts +69 -0
- package/dist/core/serializer.js +187 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +32 -0
- package/dist/redis-graph-cache.d.ts +302 -0
- package/dist/redis-graph-cache.js +839 -0
- package/dist/types/config.types.d.ts +138 -0
- package/dist/types/config.types.js +5 -0
- package/dist/types/error.types.d.ts +54 -0
- package/dist/types/error.types.js +89 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.js +29 -0
- package/dist/types/internal.types.d.ts +67 -0
- package/dist/types/internal.types.js +73 -0
- package/dist/types/operation.types.d.ts +101 -0
- package/dist/types/operation.types.js +5 -0
- package/dist/types/schema.types.d.ts +104 -0
- package/dist/types/schema.types.js +5 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,857 @@
|
|
|
1
|
+
# redis-graph-cache
|
|
2
|
+
|
|
3
|
+
> A TypeScript-first Redis data layer for Node.js β schema-driven normalization, atomic concurrent writes, ZSET-backed paginated lists, automatic relationship hydration, and built-in resilience.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/redis-graph-cache)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://nodejs.org)
|
|
8
|
+
|
|
9
|
+
**π Full documentation:** [redis-graph-cache.vercel.app](https://redis-graph-cache.vercel.app)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Table of Contents
|
|
14
|
+
|
|
15
|
+
- [Why redis-graph-cache?](#why-redis-graph-cache)
|
|
16
|
+
- [Features](#features)
|
|
17
|
+
- [When to use it (and when not)](#when-to-use-it-and-when-not)
|
|
18
|
+
- [Installation](#installation)
|
|
19
|
+
- [Quick start (60 seconds)](#quick-start-60-seconds)
|
|
20
|
+
- [Core concepts](#core-concepts)
|
|
21
|
+
- [Schema design guide](#schema-design-guide)
|
|
22
|
+
- [Entity schema](#entity-schema)
|
|
23
|
+
- [List schema (plain JSON array)](#list-schema-plain-json-array)
|
|
24
|
+
- [Indexed list schema (ZSET)](#indexed-list-schema-zset)
|
|
25
|
+
- [Field types](#field-types)
|
|
26
|
+
- [Relations](#relations)
|
|
27
|
+
- [Schema design rules of thumb](#schema-design-rules-of-thumb)
|
|
28
|
+
- [Configuration](#configuration)
|
|
29
|
+
- [API reference](#api-reference)
|
|
30
|
+
- [Entity operations](#entity-operations)
|
|
31
|
+
- [Plain list operations](#plain-list-operations)
|
|
32
|
+
- [Indexed list operations](#indexed-list-operations)
|
|
33
|
+
- [Cache management](#cache-management)
|
|
34
|
+
- [Monitoring & lifecycle](#monitoring--lifecycle)
|
|
35
|
+
- [TTL semantics](#ttl-semantics)
|
|
36
|
+
- [Null vs undefined](#null-vs-undefined)
|
|
37
|
+
- [Error model](#error-model)
|
|
38
|
+
- [Resilience: circuit breaker & retries](#resilience-circuit-breaker--retries)
|
|
39
|
+
- [Compression](#compression)
|
|
40
|
+
- [Serializers](#serializers)
|
|
41
|
+
- [Production checklist](#production-checklist)
|
|
42
|
+
- [FAQ](#faq)
|
|
43
|
+
- [License](#license)
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Why redis-graph-cache?
|
|
48
|
+
|
|
49
|
+
Caching nested objects in Redis is harder than it looks. Naively storing JSON blobs creates three problems:
|
|
50
|
+
|
|
51
|
+
1. **Update one field β rewrite the whole object** (and lose any concurrent writes).
|
|
52
|
+
2. **Same entity duplicated across many keys** (post embedded in feed, in user profile, in search results β invalidating it means hunting them all down).
|
|
53
|
+
3. **Lists grow unbounded** and pagination requires reading the entire array.
|
|
54
|
+
|
|
55
|
+
`redis-graph-cache` solves these by:
|
|
56
|
+
|
|
57
|
+
- **Normalizing** nested objects into one canonical key per entity, with relationships stored as id references.
|
|
58
|
+
- **Atomic Lua scripts** for every read-modify-write path (CAS writes, list mutations, cascade invalidation).
|
|
59
|
+
- **ZSET-backed lists** with built-in pagination, scoring, size caps, and membership tracking for one-call cascade invalidation.
|
|
60
|
+
- **Automatic hydration** that walks relationships and reconstructs the full graph on read.
|
|
61
|
+
|
|
62
|
+
You declare your schema once. The library handles everything else.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Features
|
|
67
|
+
|
|
68
|
+
| Feature | Description |
|
|
69
|
+
| ------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
70
|
+
| **Schema-driven storage** | Declare entities, fields, relations once. Auto-validates at boot. |
|
|
71
|
+
| **Atomic writes** | Every mutating op is a Lua script β no read-then-write races, ever. |
|
|
72
|
+
| **Two list flavours** | `list` (JSON array) for small collections; `indexedList` (ZSET) for paginated, sorted feeds with atomic trim. |
|
|
73
|
+
| **Cascade invalidation** | One call removes an entity from all lists tracking it. |
|
|
74
|
+
| **Connection pooling** | Round-robin across N ioredis clients for high throughput. |
|
|
75
|
+
| **Optional compression** | Auto-zlib for large payloads, with backward-compatible reads. |
|
|
76
|
+
| **Lossless serializer** | Preserves `Date`, `BigInt`, `Map`, `Set`, `RegExp`, `Buffer`, `NaN`, `Β±Infinity` by default. |
|
|
77
|
+
| **Circuit breaker** | Bounded failures trip the breaker; half-open probes recovery. |
|
|
78
|
+
| **Bounded retries** | Exponential backoff with jitter on transient failures. |
|
|
79
|
+
| **Real metrics** | Hit rate, latency, error rate, Redis memory β measured at the connection boundary. |
|
|
80
|
+
| **TTL controls** | Per-schema, per-call override, cascade floor, or forced uniform TTL. |
|
|
81
|
+
| **Production safety** | `clearAllCache` requires explicit confirmation; auto-blocked in production. |
|
|
82
|
+
| **TypeScript native** | Full generic typing β `keyof TSchema` everywhere. |
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## When to use it (and when not)
|
|
87
|
+
|
|
88
|
+
### Use it when you have
|
|
89
|
+
|
|
90
|
+
- Nested entities with shared child objects (posts with authors, comments, categories).
|
|
91
|
+
- Feeds, timelines, leaderboards, "latest N" lists.
|
|
92
|
+
- Read-heavy workloads where stale data is acceptable for short TTLs.
|
|
93
|
+
- A single Redis instance (or a Redis cluster with hash tags planned per schema key).
|
|
94
|
+
- Throughput up to ~50k ops/sec on a typical 8-core Node process.
|
|
95
|
+
|
|
96
|
+
### Don't use it (yet) when you need
|
|
97
|
+
|
|
98
|
+
- **Strong consistency** β this is a cache, not a database. Use Postgres/Mongo as source of truth.
|
|
99
|
+
- **Redis Cluster with cross-slot operations** β Lua scripts assume keys hash to the same slot. Single-instance or hash-tagged keys only.
|
|
100
|
+
- **Cache stampede protection** β no built-in singleflight; combine with `p-memoize` or similar at the application level.
|
|
101
|
+
- **Off-thread JSON** β large payloads (>1MB) should be split or stored elsewhere.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Installation
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npm install redis-graph-cache
|
|
109
|
+
# or
|
|
110
|
+
pnpm add redis-graph-cache
|
|
111
|
+
# or
|
|
112
|
+
yarn add redis-graph-cache
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Requirements:** Node.js β₯ 18, Redis β₯ 6.
|
|
116
|
+
|
|
117
|
+
> `ioredis` is a peer dependency and is installed automatically. If you already use `ioredis` in your app, your existing version is reused (must be `^5.0.0`).
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Quick start (60 seconds)
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { RedisGraphCache } from 'redis-graph-cache';
|
|
125
|
+
|
|
126
|
+
// 1. Declare your schema
|
|
127
|
+
const schema = {
|
|
128
|
+
user: {
|
|
129
|
+
type: 'entity' as const,
|
|
130
|
+
id: 'id',
|
|
131
|
+
key: (id) => `user:${id}`,
|
|
132
|
+
fields: { name: { type: 'string' }, email: { type: 'string' } },
|
|
133
|
+
ttl: 7200,
|
|
134
|
+
},
|
|
135
|
+
post: {
|
|
136
|
+
type: 'entity' as const,
|
|
137
|
+
id: 'id',
|
|
138
|
+
key: (id) => `post:${id}`,
|
|
139
|
+
fields: {
|
|
140
|
+
title: { type: 'string' },
|
|
141
|
+
content: { type: 'string' },
|
|
142
|
+
createdAt: { type: 'date' },
|
|
143
|
+
},
|
|
144
|
+
relations: {
|
|
145
|
+
author: { type: 'user', kind: 'one' },
|
|
146
|
+
},
|
|
147
|
+
ttl: 3600,
|
|
148
|
+
},
|
|
149
|
+
globalFeed: {
|
|
150
|
+
type: 'indexedList' as const,
|
|
151
|
+
entityType: 'post',
|
|
152
|
+
key: () => 'feed:global',
|
|
153
|
+
idField: 'id',
|
|
154
|
+
scoreField: 'createdAt',
|
|
155
|
+
maxSize: 10_000,
|
|
156
|
+
trackMembership: true,
|
|
157
|
+
ttl: 86400,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// 2. Create the engine
|
|
162
|
+
const cache = new RedisGraphCache(schema, {
|
|
163
|
+
redis: { host: 'localhost', port: 6379, keyPrefix: 'myapp:' },
|
|
164
|
+
cache: { defaultTTL: 3600, enableCompression: true },
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// 3. Use it
|
|
168
|
+
await cache.writeEntity('post', {
|
|
169
|
+
id: 1,
|
|
170
|
+
title: 'Hello',
|
|
171
|
+
content: 'World',
|
|
172
|
+
createdAt: new Date(),
|
|
173
|
+
author: { id: 9, name: 'Ada', email: 'ada@example.com' },
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
await cache.addIndexedListItem(
|
|
177
|
+
'globalFeed',
|
|
178
|
+
{},
|
|
179
|
+
{
|
|
180
|
+
id: 1,
|
|
181
|
+
// ...full post data; engine writes both the entity and the feed entry
|
|
182
|
+
},
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const post = await cache.readEntity('post', 1);
|
|
186
|
+
// β { id: 1, title: 'Hello', content: 'World', createdAt: Date, author: { id: 9, name: 'Ada', ... } }
|
|
187
|
+
|
|
188
|
+
const feed = await cache.readIndexedList(
|
|
189
|
+
'globalFeed',
|
|
190
|
+
{},
|
|
191
|
+
{
|
|
192
|
+
limit: 50,
|
|
193
|
+
reverse: true, // newest first
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
await cache.invalidateEntity('post', 1); // also removes from feed (trackMembership)
|
|
198
|
+
await cache.disconnect();
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Core concepts
|
|
204
|
+
|
|
205
|
+
### 1. Entity
|
|
206
|
+
|
|
207
|
+
A single addressable object with an `id`. Stored as one Redis key. Nested objects are split into their own entity keys and replaced with id references.
|
|
208
|
+
|
|
209
|
+
### 2. List
|
|
210
|
+
|
|
211
|
+
An ordered collection of entity ids stored as a JSON array under one key. Best for small (< 200 items), rarely-changing collections.
|
|
212
|
+
|
|
213
|
+
### 3. Indexed list
|
|
214
|
+
|
|
215
|
+
A ZSET of entity ids with scores. Supports pagination, sorting, atomic add/remove, size caps, and cascade invalidation. Use this for anything that grows.
|
|
216
|
+
|
|
217
|
+
### 4. Normalization
|
|
218
|
+
|
|
219
|
+
When you write a post with an embedded author, the engine writes the author to its own key (`user:9`) and stores `{ authorId: 9 }` on the post. Update the author once β every post sees it.
|
|
220
|
+
|
|
221
|
+
### 5. Hydration
|
|
222
|
+
|
|
223
|
+
When you read a post, the engine fetches the post + walks every relation (author, comments) and assembles the full graph. Configurable depth and field selection.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Schema design guide
|
|
228
|
+
|
|
229
|
+
### Entity schema
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
{
|
|
233
|
+
type: 'entity',
|
|
234
|
+
id: 'id', // name of the id field on your data
|
|
235
|
+
key: (id) => `post:${id}`, // Redis key generator (must include id)
|
|
236
|
+
fields: { // optional β declared fields are advisory
|
|
237
|
+
title: { type: 'string', required: true },
|
|
238
|
+
views: { type: 'number' },
|
|
239
|
+
tags: { type: 'array' },
|
|
240
|
+
createdAt: { type: 'date' },
|
|
241
|
+
},
|
|
242
|
+
relations: { // optional β links to other entities
|
|
243
|
+
author: { type: 'user', kind: 'one' },
|
|
244
|
+
comments: { type: 'comment', kind: 'many' },
|
|
245
|
+
},
|
|
246
|
+
ttl: 3600, // seconds; falls back to cache.defaultTTL
|
|
247
|
+
version: '1.0.0', // optional, for migrations
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
| Property | Type | Required | Description |
|
|
252
|
+
| ----------- | ------------------------------------ | -------- | ------------------------------------------------------------- |
|
|
253
|
+
| `type` | `'entity'` | yes | Discriminator |
|
|
254
|
+
| `id` | `string` | yes | Name of the field on your data that holds the id |
|
|
255
|
+
| `key` | `(...args) => string` | yes | Generates the Redis key. Include the id to avoid collisions |
|
|
256
|
+
| `fields` | `Record<string, FieldDefinition>` | no | Self-documenting field types; **advisory only** (no coercion) |
|
|
257
|
+
| `relations` | `Record<string, RelationDefinition>` | no | Names of related entity types and their cardinality |
|
|
258
|
+
| `ttl` | `number` (seconds) | no | Per-schema TTL. Falls back to `cache.defaultTTL` |
|
|
259
|
+
| `version` | `string` | no | Reserved for migration tooling |
|
|
260
|
+
|
|
261
|
+
### List schema (plain JSON array)
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
{
|
|
265
|
+
type: 'list',
|
|
266
|
+
entityType: 'post', // referenced entity
|
|
267
|
+
key: (categoryId) => `posts:by-category:${categoryId}`,
|
|
268
|
+
idField: 'id', // which field on the entity is the id
|
|
269
|
+
ttl: 600,
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
| Property | Type | Required | Description |
|
|
274
|
+
| ------------ | --------------------- | -------- | -------------------------------------------- |
|
|
275
|
+
| `type` | `'list'` | yes | Discriminator |
|
|
276
|
+
| `entityType` | `string` | yes | Schema key of the entity stored in this list |
|
|
277
|
+
| `key` | `(...args) => string` | yes | Redis key generator |
|
|
278
|
+
| `idField` | `string` | yes | Field name that uniquely identifies items |
|
|
279
|
+
| `ttl` | `number` | no | Per-schema TTL |
|
|
280
|
+
|
|
281
|
+
### Indexed list schema (ZSET)
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
{
|
|
285
|
+
type: 'indexedList',
|
|
286
|
+
entityType: 'post',
|
|
287
|
+
key: (userId) => `feed:user:${userId}`,
|
|
288
|
+
idField: 'id',
|
|
289
|
+
scoreField: 'createdAt', // optional; defaults to insertion timestamp
|
|
290
|
+
maxSize: 1000, // optional; trim to N lowest-scored on insert
|
|
291
|
+
trackMembership: true, // optional; enables cascade-invalidate
|
|
292
|
+
ttl: 86400,
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
| Property | Type | Required | Description |
|
|
297
|
+
| ----------------- | --------------------- | -------- | ----------------------------------------------------------------------------------------------------------- |
|
|
298
|
+
| `type` | `'indexedList'` | yes | Discriminator |
|
|
299
|
+
| `entityType` | `string` | yes | Schema key of the entity stored |
|
|
300
|
+
| `key` | `(...args) => string` | yes | Redis key generator |
|
|
301
|
+
| `idField` | `string` | yes | Field name that uniquely identifies items |
|
|
302
|
+
| `scoreField` | `string` | no | Entity field used as ZSET score. Must be numeric, ISO date, or `Date`. Default: `Date.now()` at insert time |
|
|
303
|
+
| `maxSize` | `number` | no | Trim list to this size on every insert (drops lowest-scored) |
|
|
304
|
+
| `trackMembership` | `boolean` | no | Records back-index for cascade invalidation. Default `false` |
|
|
305
|
+
| `ttl` | `number` | no | Per-schema TTL |
|
|
306
|
+
|
|
307
|
+
### Field types
|
|
308
|
+
|
|
309
|
+
| Type | Round-trip with default serializer | With `JSON_SERIALIZER` |
|
|
310
|
+
| --------- | ---------------------------------- | ----------------------------------------- |
|
|
311
|
+
| `string` | OK | OK |
|
|
312
|
+
| `number` | OK | OK (loses `NaN`, `Β±Infinity`) |
|
|
313
|
+
| `boolean` | OK | OK |
|
|
314
|
+
| `object` | OK | OK |
|
|
315
|
+
| `array` | OK | OK |
|
|
316
|
+
| `date` | OK (preserved as `Date`) | becomes ISO string |
|
|
317
|
+
| `bigint` | OK (preserved as `BigInt`) | throws (JSON can't serialize) |
|
|
318
|
+
| `map` | OK (preserved as `Map`) | becomes `{}` |
|
|
319
|
+
| `set` | OK (preserved as `Set`) | becomes `{}` |
|
|
320
|
+
| `regexp` | OK (preserved as `RegExp`) | becomes `{}` |
|
|
321
|
+
| `buffer` | OK (preserved as `Buffer`) | becomes `{ type: 'Buffer', data: [...] }` |
|
|
322
|
+
|
|
323
|
+
> **Field types are advisory.** The engine does NOT coerce. They exist so schemas are self-documenting and obvious typos are caught early. Type fidelity is the serializer's job.
|
|
324
|
+
|
|
325
|
+
### Relations
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
relations: {
|
|
329
|
+
author: { type: 'user', kind: 'one' },
|
|
330
|
+
comments: { type: 'comment', kind: 'many', cascade: true, lazy: false },
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
| Property | Type | Description |
|
|
335
|
+
| --------- | ----------------- | ---------------------------------------------------- |
|
|
336
|
+
| `type` | `string` | Schema key of the related entity |
|
|
337
|
+
| `kind` | `'one' \| 'many'` | Cardinality. `one` β single object; `many` β array |
|
|
338
|
+
| `cascade` | `boolean` | Reserved for future cascade-delete (currently no-op) |
|
|
339
|
+
| `lazy` | `boolean` | Reserved for future lazy-loading (currently no-op) |
|
|
340
|
+
|
|
341
|
+
### Schema design rules of thumb
|
|
342
|
+
|
|
343
|
+
- **One key per entity, always.** Don't define two entity schemas pointing to the same Redis key β it breaks normalization.
|
|
344
|
+
- **Use `indexedList` over `list` whenever a collection might exceed ~200 items.** ZSET ops are O(log N); JSON-array ops rewrite the entire blob.
|
|
345
|
+
- **Set `trackMembership: true` only when you actually need cascade invalidation.** Each insert costs one extra `SADD`.
|
|
346
|
+
- **`scoreField` should be monotonic** (timestamps, sequence ids). Non-monotonic scores make pagination unstable.
|
|
347
|
+
- **`maxSize` is your friend** for unbounded feeds β caps memory at a known ceiling.
|
|
348
|
+
- **TTL hierarchy:** parent β₯ children when using `cascadeTTL`. Otherwise children may expire before the parent expects them.
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Configuration
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
const cache = new RedisGraphCache(schema, {
|
|
356
|
+
redis: {
|
|
357
|
+
/* ... */
|
|
358
|
+
},
|
|
359
|
+
cache: {
|
|
360
|
+
/* ... */
|
|
361
|
+
},
|
|
362
|
+
limits: {
|
|
363
|
+
/* ... */
|
|
364
|
+
},
|
|
365
|
+
resilience: {
|
|
366
|
+
/* ... */
|
|
367
|
+
},
|
|
368
|
+
monitoring: {
|
|
369
|
+
/* ... */
|
|
370
|
+
},
|
|
371
|
+
safety: {
|
|
372
|
+
/* ... */
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### `redis` (extends [`ioredis` RedisOptions](https://github.com/redis/ioredis#connect-to-redis))
|
|
378
|
+
|
|
379
|
+
| Option | Type | Default | Description |
|
|
380
|
+
| ---------------------------- | -------- | ------------- | ---------------------------------------------------------------- |
|
|
381
|
+
| `host` | `string` | `'localhost'` | Redis host |
|
|
382
|
+
| `port` | `number` | `6379` | Redis port |
|
|
383
|
+
| `db` | `number` | `0` | Redis database number |
|
|
384
|
+
| `password` | `string` | β | Redis auth password |
|
|
385
|
+
| `keyPrefix` | `string` | `''` | Prepended to every key. Auto-suffixes `:` if missing |
|
|
386
|
+
| `poolSize` | `number` | `1` | Number of ioredis clients (round-robined). Use 4β8 in production |
|
|
387
|
+
| ...all other ioredis options | | | TLS, sentinel, retry strategies, etc. |
|
|
388
|
+
|
|
389
|
+
### `cache`
|
|
390
|
+
|
|
391
|
+
| Option | Type | Default | Description |
|
|
392
|
+
| ---------------------- | ---------------- | ------------------- | --------------------------------------------------------- |
|
|
393
|
+
| `defaultTTL` | `number` (s) | `3600` | Fallback TTL when schema doesn't specify one |
|
|
394
|
+
| `enableCompression` | `boolean` | `false` | Auto-zlib entity payloads larger than threshold |
|
|
395
|
+
| `compressionThreshold` | `number` (bytes) | `1024` | Min payload size to compress |
|
|
396
|
+
| `enableL1Cache` | `boolean` | `false` | Reserved (in-process L1 cache, not yet implemented) |
|
|
397
|
+
| `l1CacheSize` | `number` | `1000` | Reserved |
|
|
398
|
+
| `serializer` | `Serializer` | `TAGGED_SERIALIZER` | Lossless tagged JSON. Pass `JSON_SERIALIZER` for raw JSON |
|
|
399
|
+
|
|
400
|
+
### `limits`
|
|
401
|
+
|
|
402
|
+
| Option | Type | Default | Description |
|
|
403
|
+
| ---------------------------- | ---------------- | -------- | ------------------------------------------------ |
|
|
404
|
+
| `maxHydrationDepth` | `number` | `5` | Throws `HydrationDepthExceededError` if exceeded |
|
|
405
|
+
| `maxEntitiesPerRequest` | `number` | `1000` | Cap on entities hydrated per call |
|
|
406
|
+
| `maxMemoryUsagePerOperation` | `number` (bytes) | `100 MB` | Throws `MemoryLimitError` if exceeded |
|
|
407
|
+
| `maxConcurrentOperations` | `number` | `100` | Reserved |
|
|
408
|
+
| `batchSize` | `number` | `100` | Pipeline batch size for bulk ops |
|
|
409
|
+
|
|
410
|
+
### `resilience`
|
|
411
|
+
|
|
412
|
+
```ts
|
|
413
|
+
resilience: {
|
|
414
|
+
circuitBreaker: { threshold: 5, timeout: 60000, resetTimeout: 30000 },
|
|
415
|
+
retry: { maxAttempts: 3, baseDelay: 100, maxDelay: 2000, backoffFactor: 2 },
|
|
416
|
+
fallback: { enabled: true, strategy: 'null' }, // 'null' | 'empty' | 'cached' | 'custom'
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
| Section | Option | Default | Description |
|
|
421
|
+
| ---------------- | --------------- | ---------- | ----------------------------------------- |
|
|
422
|
+
| `circuitBreaker` | `threshold` | `5` | Consecutive failures before tripping OPEN |
|
|
423
|
+
| | `timeout` | `60000` ms | Max op duration before counted as failure |
|
|
424
|
+
| | `resetTimeout` | `30000` ms | OPEN β HALF_OPEN cool-down |
|
|
425
|
+
| `retry` | `maxAttempts` | `3` | Max retries per op |
|
|
426
|
+
| | `baseDelay` | `100` ms | Initial backoff |
|
|
427
|
+
| | `maxDelay` | `2000` ms | Cap on exponential backoff |
|
|
428
|
+
| | `backoffFactor` | `2` | Multiplier per attempt |
|
|
429
|
+
|
|
430
|
+
### `monitoring`
|
|
431
|
+
|
|
432
|
+
| Option | Type | Default | Description |
|
|
433
|
+
| ----------------- | ---------------------------------------- | -------- | --------------------------- |
|
|
434
|
+
| `enableMetrics` | `boolean` | `true` | Track hits, misses, latency |
|
|
435
|
+
| `enableDebugMode` | `boolean` | `false` | Reserved |
|
|
436
|
+
| `enableAuditLog` | `boolean` | `false` | Reserved |
|
|
437
|
+
| `metricsInterval` | `number` | `60000` | Reserved |
|
|
438
|
+
| `logLevel` | `'error' \| 'warn' \| 'info' \| 'debug'` | `'info'` | Reserved |
|
|
439
|
+
|
|
440
|
+
### `safety`
|
|
441
|
+
|
|
442
|
+
| Option | Type | Default | Description |
|
|
443
|
+
| ---------------- | --------- | --------------------------- | ----------------------------------------------------- |
|
|
444
|
+
| `productionMode` | `boolean` | `NODE_ENV === 'production'` | Blocks `clearAllCache` unless `allowProduction: true` |
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
## API reference
|
|
449
|
+
|
|
450
|
+
Every write method supports **two call styles** β pick whichever you prefer:
|
|
451
|
+
|
|
452
|
+
```ts
|
|
453
|
+
// Positional (concise)
|
|
454
|
+
await cache.writeEntity('post', data, { ttl: 600 });
|
|
455
|
+
|
|
456
|
+
// Object (self-documenting, easier with many options)
|
|
457
|
+
await cache.writeEntity({ entityType: 'post', data, ttl: 600 });
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
Both are equivalent and fully type-checked.
|
|
461
|
+
|
|
462
|
+
### Entity operations
|
|
463
|
+
|
|
464
|
+
#### `writeEntity(entityType, data, options?) β WriteResult`
|
|
465
|
+
|
|
466
|
+
Writes an entity (and any nested entities via relations) atomically using compare-and-set.
|
|
467
|
+
|
|
468
|
+
```ts
|
|
469
|
+
const result = await cache.writeEntity('post', {
|
|
470
|
+
id: 1,
|
|
471
|
+
title: 'Hello',
|
|
472
|
+
author: { id: 9, name: 'Ada' }, // nested β written as user:9
|
|
473
|
+
});
|
|
474
|
+
// β { success: true, keys: ['post:1', 'user:9'], operationId, timestamp }
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
| Option | Type | Description |
|
|
478
|
+
| ------------ | --------- | -------------------------------------- |
|
|
479
|
+
| `ttl` | `number` | Override TTL for the root entity only |
|
|
480
|
+
| `cascadeTTL` | `boolean` | Use root TTL as floor for all children |
|
|
481
|
+
| `forceTTL` | `boolean` | Apply uniform TTL to entire write tree |
|
|
482
|
+
|
|
483
|
+
#### `readEntity(entityType, id, options?) β object \| null`
|
|
484
|
+
|
|
485
|
+
Reads and hydrates an entity (walks relations).
|
|
486
|
+
|
|
487
|
+
```ts
|
|
488
|
+
const post = await cache.readEntity('post', 1, {
|
|
489
|
+
maxDepth: 2,
|
|
490
|
+
selectiveFields: ['title', 'author'],
|
|
491
|
+
excludeRelations: ['comments'],
|
|
492
|
+
});
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
| Option | Type | Description |
|
|
496
|
+
| ------------------ | ---------------- | -------------------------------------------- |
|
|
497
|
+
| `maxDepth` | `number` | Override per-call hydration depth |
|
|
498
|
+
| `selectiveFields` | `string[]` | Only return these fields (still includes id) |
|
|
499
|
+
| `excludeRelations` | `string[]` | Skip these relation names |
|
|
500
|
+
| `memoryLimit` | `number` (bytes) | Per-call memory cap |
|
|
501
|
+
|
|
502
|
+
#### `updateEntityIfExists(entityType, data, options?) β WriteResult \| null`
|
|
503
|
+
|
|
504
|
+
Conditional update: writes only if the entity already exists. Returns `null` if missing.
|
|
505
|
+
|
|
506
|
+
```ts
|
|
507
|
+
const updated = await cache.updateEntityIfExists('post', { id: 1, views: 42 });
|
|
508
|
+
if (updated === null) {
|
|
509
|
+
// post doesn't exist; don't recreate
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
#### `deleteEntity(entityType, id) β boolean`
|
|
514
|
+
|
|
515
|
+
Deletes a single entity key. Returns `true` if deleted, `false` if it didn't exist.
|
|
516
|
+
|
|
517
|
+
> For cascade invalidation across lists, use `invalidateEntity` instead.
|
|
518
|
+
|
|
519
|
+
### Plain list operations
|
|
520
|
+
|
|
521
|
+
#### `writeList(listType, params, items, options?) β ListWriteResult`
|
|
522
|
+
|
|
523
|
+
Replaces the entire list with the given items. Each item is also written as an entity.
|
|
524
|
+
|
|
525
|
+
```ts
|
|
526
|
+
await cache.writeList('postsByCategory', { categoryId: 5 }, [
|
|
527
|
+
{ id: 1, title: 'A' },
|
|
528
|
+
{ id: 2, title: 'B' },
|
|
529
|
+
]);
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
#### `readList(listType, params, options?) β object[]`
|
|
533
|
+
|
|
534
|
+
Reads all items in the list and hydrates each as an entity.
|
|
535
|
+
|
|
536
|
+
```ts
|
|
537
|
+
const posts = await cache.readList('postsByCategory', { categoryId: 5 });
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
#### `addListItem(listType, params, entityData, options?) β boolean`
|
|
541
|
+
|
|
542
|
+
Appends an item to the list and writes the entity atomically.
|
|
543
|
+
|
|
544
|
+
#### `removeListItem(listType, params, entityId, options?) β boolean`
|
|
545
|
+
|
|
546
|
+
Removes an id from the list. Pass `{ deleteEntity: true }` to also delete the underlying entity key.
|
|
547
|
+
|
|
548
|
+
#### `deleteList(listType, params) β boolean`
|
|
549
|
+
|
|
550
|
+
Deletes the list key. Underlying entities are NOT deleted.
|
|
551
|
+
|
|
552
|
+
### Indexed list operations
|
|
553
|
+
|
|
554
|
+
#### `writeIndexedList(listType, params, items, options?) β ListWriteResult`
|
|
555
|
+
|
|
556
|
+
Adds items to a ZSET-backed list. Does NOT clear existing members β call `deleteIndexedList` first if you want a full replace.
|
|
557
|
+
|
|
558
|
+
#### `readIndexedList(listType, params, options?) β object[]`
|
|
559
|
+
|
|
560
|
+
Paginated, hydrated read.
|
|
561
|
+
|
|
562
|
+
```ts
|
|
563
|
+
const feed = await cache.readIndexedList(
|
|
564
|
+
'globalFeed',
|
|
565
|
+
{},
|
|
566
|
+
{
|
|
567
|
+
limit: 50,
|
|
568
|
+
offset: 0,
|
|
569
|
+
reverse: true, // highest score first
|
|
570
|
+
// plus all standard hydration options
|
|
571
|
+
excludeRelations: ['comments'],
|
|
572
|
+
},
|
|
573
|
+
);
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
| Option | Type | Description |
|
|
577
|
+
| ------------------ | ---------- | ----------------------------------------------------- |
|
|
578
|
+
| `limit` | `number` | Max items to return |
|
|
579
|
+
| `offset` | `number` | Skip this many items |
|
|
580
|
+
| `reverse` | `boolean` | Sort descending (newest-first when score = timestamp) |
|
|
581
|
+
| `maxDepth` | `number` | Hydration depth |
|
|
582
|
+
| `selectiveFields` | `string[]` | Fields to include |
|
|
583
|
+
| `excludeRelations` | `string[]` | Relations to skip |
|
|
584
|
+
|
|
585
|
+
#### `addIndexedListItem(listType, params, entityData, options?) β boolean`
|
|
586
|
+
|
|
587
|
+
Adds one entity to the indexed list. Returns `true` if newly added, `false` if score updated on existing id.
|
|
588
|
+
|
|
589
|
+
#### `removeIndexedListItem(listType, params, entityId, options?) β boolean`
|
|
590
|
+
|
|
591
|
+
Removes one id. Pass `{ deleteEntity: true }` to also cascade-invalidate the entity (removes from all tracked lists).
|
|
592
|
+
|
|
593
|
+
#### `indexedListSize(listType, params) β number`
|
|
594
|
+
|
|
595
|
+
Returns the current member count. O(1).
|
|
596
|
+
|
|
597
|
+
#### `deleteIndexedList(listType, params) β boolean`
|
|
598
|
+
|
|
599
|
+
Deletes the ZSET key.
|
|
600
|
+
|
|
601
|
+
### Cache management
|
|
602
|
+
|
|
603
|
+
#### `invalidateEntity(entityType, id) β number`
|
|
604
|
+
|
|
605
|
+
The killer method. Atomically:
|
|
606
|
+
|
|
607
|
+
1. Removes the entity key.
|
|
608
|
+
2. Looks up every indexed list with `trackMembership: true` that contains this id.
|
|
609
|
+
3. Removes the id from all of those lists in one Lua script.
|
|
610
|
+
|
|
611
|
+
Returns the number of tracked lists the entity was removed from.
|
|
612
|
+
|
|
613
|
+
```ts
|
|
614
|
+
const cleaned = await cache.invalidateEntity('post', 42);
|
|
615
|
+
// β 3 (removed from post:42 + 3 feeds)
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
#### `clearAllCache(opts) β void`
|
|
619
|
+
|
|
620
|
+
**Destructive.** Wipes all keys owned by the engine.
|
|
621
|
+
|
|
622
|
+
```ts
|
|
623
|
+
await cache.clearAllCache({ confirm: 'YES_WIPE_ALL' });
|
|
624
|
+
// in production:
|
|
625
|
+
await cache.clearAllCache({ confirm: 'YES_WIPE_ALL', allowProduction: true });
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
- With `keyPrefix` set: uses `SCAN` + `UNLINK` (safe to share Redis with other apps).
|
|
629
|
+
- Without `keyPrefix`: falls back to `FLUSHDB` (wipes the entire selected DB).
|
|
630
|
+
|
|
631
|
+
### Monitoring & lifecycle
|
|
632
|
+
|
|
633
|
+
#### `getMetrics() β CacheMetrics`
|
|
634
|
+
|
|
635
|
+
Synchronous snapshot of hit rate, latency, totals.
|
|
636
|
+
|
|
637
|
+
```ts
|
|
638
|
+
const m = cache.getMetrics();
|
|
639
|
+
// { cacheHits, cacheMisses, hitRate, totalOperations,
|
|
640
|
+
// avgResponseTime, failedOperations, activeConnections, ... }
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
#### `getHealthStatus() β Promise<HealthStatus>`
|
|
644
|
+
|
|
645
|
+
Async health check including Redis-side memory.
|
|
646
|
+
|
|
647
|
+
```ts
|
|
648
|
+
const h = await cache.getHealthStatus();
|
|
649
|
+
// { status: 'healthy' | 'degraded' | 'unhealthy',
|
|
650
|
+
// redis: { connected, latency, memoryUsage },
|
|
651
|
+
// engine: { activeOperations, errorRate, ... } }
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
`degraded` means circuit breaker is HALF_OPEN or error rate > 5%.
|
|
655
|
+
|
|
656
|
+
#### `disconnect() β Promise<void>`
|
|
657
|
+
|
|
658
|
+
Closes all pool connections gracefully. Always call this on shutdown.
|
|
659
|
+
|
|
660
|
+
---
|
|
661
|
+
|
|
662
|
+
## TTL semantics
|
|
663
|
+
|
|
664
|
+
Three knobs control TTL on every write:
|
|
665
|
+
|
|
666
|
+
| Mode | What happens |
|
|
667
|
+
| ---------------------------- | ------------------------------------------------------------------------------------------ |
|
|
668
|
+
| **default** | Each entity gets its own per-schema TTL (or `cache.defaultTTL`) |
|
|
669
|
+
| `ttl: 600` | Override applies **only to the root entity** |
|
|
670
|
+
| `ttl: 600, cascadeTTL: true` | Root TTL becomes the **floor** for all children β children with shorter TTLs get bumped up |
|
|
671
|
+
| `ttl: 600, forceTTL: true` | All entities written by this call get **exactly 600s**, regardless of schema |
|
|
672
|
+
|
|
673
|
+
```ts
|
|
674
|
+
// Children expire individually per their own schema
|
|
675
|
+
await cache.writeEntity('post', postData);
|
|
676
|
+
|
|
677
|
+
// Root pinned to 60s; children keep their own TTLs
|
|
678
|
+
await cache.writeEntity('post', postData, { ttl: 60 });
|
|
679
|
+
|
|
680
|
+
// Root + all children at least 3600s (children with longer TTLs unchanged)
|
|
681
|
+
await cache.writeEntity('post', postData, { ttl: 3600, cascadeTTL: true });
|
|
682
|
+
|
|
683
|
+
// Everything exactly 60s
|
|
684
|
+
await cache.writeEntity('post', postData, { ttl: 60, forceTTL: true });
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
---
|
|
688
|
+
|
|
689
|
+
## Null vs undefined
|
|
690
|
+
|
|
691
|
+
Important distinction in writes:
|
|
692
|
+
|
|
693
|
+
| Value | Effect |
|
|
694
|
+
| ------------ | ----------------------------------------------------------------------------------------------- |
|
|
695
|
+
| `null` | Explicitly **overwrites** the field with `null` |
|
|
696
|
+
| `undefined` | Field is **omitted** (because `JSON.stringify` drops `undefined`) β existing value is preserved |
|
|
697
|
+
| Field absent | Same as `undefined` |
|
|
698
|
+
|
|
699
|
+
```ts
|
|
700
|
+
// Existing post: { id: 1, title: 'A', subtitle: 'B' }
|
|
701
|
+
await cache.updateEntityIfExists('post', { id: 1, subtitle: undefined });
|
|
702
|
+
// β { id: 1, title: 'A', subtitle: 'B' } (unchanged)
|
|
703
|
+
|
|
704
|
+
await cache.updateEntityIfExists('post', { id: 1, subtitle: null });
|
|
705
|
+
// β { id: 1, title: 'A', subtitle: null }
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
This matches Redux/Immer/REST PATCH conventions.
|
|
709
|
+
|
|
710
|
+
---
|
|
711
|
+
|
|
712
|
+
## Error model
|
|
713
|
+
|
|
714
|
+
All errors inherit from `RedisSchemaEngineError`.
|
|
715
|
+
|
|
716
|
+
| Error | When it's thrown |
|
|
717
|
+
| ----------------------------- | -------------------------------------------------------------------- |
|
|
718
|
+
| `SchemaValidationError` | At construction β invalid schema, circular relations, duplicate keys |
|
|
719
|
+
| `EntityNotFoundError` | (Reserved; current API returns `null` for missing entities) |
|
|
720
|
+
| `InvalidOperationError` | Wrong call shape, missing confirmation, bad params |
|
|
721
|
+
| `RedisConnectionError` | Underlying Redis network/protocol failure |
|
|
722
|
+
| `MemoryLimitError` | Hydration exceeded `limits.maxMemoryUsagePerOperation` |
|
|
723
|
+
| `CircuitBreakerOpenError` | Operation rejected because the breaker is OPEN |
|
|
724
|
+
| `HydrationDepthExceededError` | Read exceeded `limits.maxHydrationDepth` |
|
|
725
|
+
|
|
726
|
+
```ts
|
|
727
|
+
import {
|
|
728
|
+
CircuitBreakerOpenError,
|
|
729
|
+
HydrationDepthExceededError,
|
|
730
|
+
} from 'redis-graph-cache';
|
|
731
|
+
|
|
732
|
+
try {
|
|
733
|
+
await cache.readEntity('post', 1);
|
|
734
|
+
} catch (err) {
|
|
735
|
+
if (err instanceof CircuitBreakerOpenError) {
|
|
736
|
+
// serve stale or fall back to DB
|
|
737
|
+
} else if (err instanceof HydrationDepthExceededError) {
|
|
738
|
+
// your relations form a cycle; reduce maxDepth or excludeRelations
|
|
739
|
+
} else {
|
|
740
|
+
throw err;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
---
|
|
746
|
+
|
|
747
|
+
## Resilience: circuit breaker & retries
|
|
748
|
+
|
|
749
|
+
Every Redis op flows through:
|
|
750
|
+
|
|
751
|
+
```
|
|
752
|
+
[ retry (exponential backoff) ] β [ circuit breaker ] β [ ioredis client ]
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
- **Retries** handle transient failures (timeouts, momentary network blips).
|
|
756
|
+
- **Circuit breaker** fails fast after sustained failures, preventing thundering herds.
|
|
757
|
+
|
|
758
|
+
### State machine
|
|
759
|
+
|
|
760
|
+
```
|
|
761
|
+
CLOSED ββ5 failuresββ> OPEN ββ30s cooldownββ> HALF_OPEN ββsuccessββ> CLOSED
|
|
762
|
+
β² β
|
|
763
|
+
βββββββ failure ββββββββββββ
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
When OPEN, all ops throw `CircuitBreakerOpenError` immediately. Configure thresholds to match your SLO.
|
|
767
|
+
|
|
768
|
+
---
|
|
769
|
+
|
|
770
|
+
## Compression
|
|
771
|
+
|
|
772
|
+
Optional zlib compression for entity payloads larger than `compressionThreshold`.
|
|
773
|
+
|
|
774
|
+
```ts
|
|
775
|
+
new RedisGraphCache(schema, {
|
|
776
|
+
cache: {
|
|
777
|
+
enableCompression: true,
|
|
778
|
+
compressionThreshold: 4096, // bytes
|
|
779
|
+
},
|
|
780
|
+
});
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
- Magic prefix (`\x00z1:`) marks compressed values.
|
|
784
|
+
- Reads transparently handle a mix of compressed and uncompressed entries β safe to enable on an existing cache.
|
|
785
|
+
- Lists (JSON-array bodies) are NOT compressed because Lua scripts parse them server-side.
|
|
786
|
+
|
|
787
|
+
---
|
|
788
|
+
|
|
789
|
+
## Serializers
|
|
790
|
+
|
|
791
|
+
The default `TAGGED_SERIALIZER` preserves rich types losslessly:
|
|
792
|
+
|
|
793
|
+
```ts
|
|
794
|
+
import { JSON_SERIALIZER } from 'redis-graph-cache';
|
|
795
|
+
|
|
796
|
+
// Opt out for max throughput (loses Date, BigInt, Map, Set, RegExp, Buffer fidelity)
|
|
797
|
+
new RedisGraphCache(schema, { cache: { serializer: JSON_SERIALIZER } });
|
|
798
|
+
|
|
799
|
+
// Or plug in a third-party codec
|
|
800
|
+
import superjson from 'superjson';
|
|
801
|
+
new RedisGraphCache(schema, {
|
|
802
|
+
cache: {
|
|
803
|
+
serializer: {
|
|
804
|
+
stringify: superjson.stringify,
|
|
805
|
+
parse: superjson.parse,
|
|
806
|
+
},
|
|
807
|
+
},
|
|
808
|
+
});
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
> Don't mix serializers against the same cache without flushing first β old entries written with one serializer may not parse correctly with another.
|
|
812
|
+
|
|
813
|
+
---
|
|
814
|
+
|
|
815
|
+
## Production checklist
|
|
816
|
+
|
|
817
|
+
- [ ] `redis.keyPrefix` set so `clearAllCache` is scoped, not a `FLUSHDB`.
|
|
818
|
+
- [ ] `redis.poolSize` β₯ 4 for production traffic.
|
|
819
|
+
- [ ] `safety.productionMode: true` (or `NODE_ENV=production`).
|
|
820
|
+
- [ ] `cache.enableCompression: true` if entities average > 1KB.
|
|
821
|
+
- [ ] `limits.maxHydrationDepth` set to the smallest value that supports your use case.
|
|
822
|
+
- [ ] `trackMembership: true` only on lists where you actually need cascade invalidation.
|
|
823
|
+
- [ ] `maxSize` set on every unbounded indexed list.
|
|
824
|
+
- [ ] `disconnect()` called in your shutdown hook (SIGTERM handler).
|
|
825
|
+
- [ ] Metrics exported to your observability stack (`getMetrics()` is sync β call it in a 30s timer).
|
|
826
|
+
- [ ] Circuit-breaker errors handled with a fallback (DB read, stale value, etc.).
|
|
827
|
+
|
|
828
|
+
---
|
|
829
|
+
|
|
830
|
+
## FAQ
|
|
831
|
+
|
|
832
|
+
**Q: Can I use it with Redis Cluster?**
|
|
833
|
+
Not yet. Lua scripts assume keys hash to the same slot. Use a single Redis instance, or design your `key` functions to use hash tags so all keys touched by one operation land on the same slot.
|
|
834
|
+
|
|
835
|
+
**Q: Does it protect against cache stampedes?**
|
|
836
|
+
No built-in singleflight. Combine with `p-memoize` or `dataloader` at the application level if you need it.
|
|
837
|
+
|
|
838
|
+
**Q: Can I store binary data?**
|
|
839
|
+
Yes β use `Buffer` fields with the default `TAGGED_SERIALIZER`. They round-trip losslessly.
|
|
840
|
+
|
|
841
|
+
**Q: How does it compare to bare ioredis?**
|
|
842
|
+
ioredis is the transport. This package adds normalization, hydration, atomic mutation Lua scripts, ZSET list management, cascade invalidation, circuit breaker, retries, metrics, compression, and TypeScript-native schema typing on top.
|
|
843
|
+
|
|
844
|
+
**Q: Can I read/write outside the schema?**
|
|
845
|
+
Yes β `RedisConnectionManager` is exported, but you lose all the safety guarantees. Prefer adding a schema entry.
|
|
846
|
+
|
|
847
|
+
**Q: What happens to nested entities when I overwrite a parent?**
|
|
848
|
+
They're written to their own keys; the parent stores only id references. So updating the parent's `title` does NOT touch the children. Updating a child by id is what changes it.
|
|
849
|
+
|
|
850
|
+
**Q: Why two list types?**
|
|
851
|
+
`list` is fast and simple for tiny collections. `indexedList` (ZSET) scales to millions, supports pagination and atomic trim, and is the right answer for any feed or timeline. New code should default to `indexedList`.
|
|
852
|
+
|
|
853
|
+
---
|
|
854
|
+
|
|
855
|
+
## License
|
|
856
|
+
|
|
857
|
+
MIT
|