@break-limits/mongoose-cache 0.1.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 +344 -0
- package/dist/index.cjs +953 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +296 -0
- package/dist/index.d.ts +296 -0
- package/dist/index.js +911 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Break Limits
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# @break-limits/mongoose-cache
|
|
4
|
+
|
|
5
|
+
**A transparent, correctness-first caching layer for Mongoose, backed by Redis.**
|
|
6
|
+
|
|
7
|
+
Install it once and every read is cached and automatically invalidated โ with **zero changes** to your models or queries.
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/@break-limits/mongoose-cache)
|
|
10
|
+
[](#license)
|
|
11
|
+
[](#correctness--testing)
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
> **The promise:** this library may serve fewer cache hits than a naive wrapper, but it **never serves stale data**. When it cannot *prove* a cached value is fresh, it doesn't cache it โ it falls back to MongoDB. Correctness is the product.
|
|
18
|
+
|
|
19
|
+
## Table of contents
|
|
20
|
+
|
|
21
|
+
- [Why](#why)
|
|
22
|
+
- [Features](#features)
|
|
23
|
+
- [Install](#install)
|
|
24
|
+
- [Quick start](#quick-start)
|
|
25
|
+
- [How it works](#how-it-works)
|
|
26
|
+
- [Configuration](#configuration)
|
|
27
|
+
- [What gets cached & invalidated](#what-gets-cached--invalidated)
|
|
28
|
+
- [Supported query operators](#supported-query-operators)
|
|
29
|
+
- [Examples](#examples)
|
|
30
|
+
- [Events](#events)
|
|
31
|
+
- [Correctness & testing](#correctness--testing)
|
|
32
|
+
- [Limitations](#limitations)
|
|
33
|
+
- [Requirements & compatibility](#requirements--compatibility)
|
|
34
|
+
- [Roadmap](#roadmap)
|
|
35
|
+
- [API reference](#api-reference)
|
|
36
|
+
- [Development](#development)
|
|
37
|
+
- [License](#license)
|
|
38
|
+
|
|
39
|
+
## Why
|
|
40
|
+
|
|
41
|
+
Most Mongoose caching libraries make you choose: cache aggressively and risk serving stale data, or invalidate by hand and get it wrong. This library takes a different stance โ it classifies every query by how safely it can be invalidated, caches precisely where it can prove freshness, conservatively where it can't, and never where it would be wrong.
|
|
42
|
+
|
|
43
|
+
The result is a cache you can put in front of production traffic without auditing every query for staleness bugs.
|
|
44
|
+
|
|
45
|
+
## Features
|
|
46
|
+
|
|
47
|
+
- ๐ง **Transparent** โ no changes to your models or queries. `createCache({ mongoose, redis })` and you're done.
|
|
48
|
+
- ๐ฏ **Precise invalidation** โ point reads and predicate queries are invalidated per-document via a membership-transition engine (enter / leave / change / top-N edges).
|
|
49
|
+
- ๐งฎ **Aggregations & joins** โ `aggregate()` (including `$lookup`, `$unionWith`, `$graphLookup`) is cached and invalidated by every collection it touches.
|
|
50
|
+
- ๐ **Everything else too** โ `find`, `findOne`, `countDocuments`, `distinct`, `estimatedDocumentCount`, and populated queries are all covered.
|
|
51
|
+
- โ๏ธ **Every write path invalidates** โ query writes, `save`/`create`, `insertMany`, `replaceOne`, upserts, and `bulkWrite`.
|
|
52
|
+
- ๐ก๏ธ **Never stale** โ verified by a differential fuzzer that runs thousands of random ops against real MongoDB and asserts the cache always matches.
|
|
53
|
+
- โก **Stampede-safe** โ in-process single-flight collapses concurrent misses for the same key.
|
|
54
|
+
- ๐ **Race-safe** โ a version-token guard prevents caching a value that a concurrent write invalidated mid-load.
|
|
55
|
+
- ๐๏ธ **Degrade, never fail** โ if Redis is down, reads fall through to MongoDB and writes still succeed.
|
|
56
|
+
- ๐ข **Multi-tenant** โ first-class tenant keyspace isolation.
|
|
57
|
+
- ๐ฆ **Lossless** โ BSON-aware serialization preserves `ObjectId`, `Date`, `Decimal128`, and `Buffer`.
|
|
58
|
+
- ๐ **Observable** โ `hit` / `miss` / `invalidate` / `error` events.
|
|
59
|
+
- ๐งฉ **TypeScript-first** โ ships ESM + CJS with full type definitions.
|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm install @break-limits/mongoose-cache ioredis mongoose
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`mongoose` (>= 7) and `ioredis` (>= 5) are peer dependencies.
|
|
68
|
+
|
|
69
|
+
## Quick start
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import mongoose from "mongoose";
|
|
73
|
+
import Redis from "ioredis";
|
|
74
|
+
import { createCache } from "@break-limits/mongoose-cache";
|
|
75
|
+
|
|
76
|
+
await mongoose.connect(process.env.MONGO_URI!);
|
|
77
|
+
const redis = new Redis(process.env.REDIS_URL!);
|
|
78
|
+
|
|
79
|
+
const cache = createCache({
|
|
80
|
+
mongoose,
|
|
81
|
+
redis,
|
|
82
|
+
models: {
|
|
83
|
+
Product: { ttlMs: 1_800_000 }, // opt-in, optional TTL backstop
|
|
84
|
+
User: { ttlMs: 300_000 },
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// From here on, reads are cached and invalidated automatically.
|
|
89
|
+
await Product.find({ status: "active" }).lean(); // miss โ MongoDB โ cached
|
|
90
|
+
await Product.find({ status: "active" }).lean(); // hit โ Redis
|
|
91
|
+
|
|
92
|
+
await Product.findOneAndUpdate({ name: "Widget" }, { status: "active" });
|
|
93
|
+
// ^ "Widget" enters the "active" set โ the cached query is invalidated.
|
|
94
|
+
|
|
95
|
+
await Product.find({ status: "active" }).lean(); // miss โ reloads with Widget
|
|
96
|
+
|
|
97
|
+
// Observe what's happening:
|
|
98
|
+
cache.on("hit", ({ key }) => {});
|
|
99
|
+
cache.on("miss", ({ key }) => {});
|
|
100
|
+
cache.on("invalidate", ({ keys }) => {});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## How it works
|
|
104
|
+
|
|
105
|
+
Every query is classified into a **cacheability tier** that determines how it is cached and invalidated:
|
|
106
|
+
|
|
107
|
+
| Tier | Query shape | Caching | Invalidation |
|
|
108
|
+
|------|-------------|---------|--------------|
|
|
109
|
+
| **T0** | point read (`findById`, `findOne({_id})`, `find({_id:{$in:[โฆ]}})`) | by document id | **precise** โ surgical by id |
|
|
110
|
+
| **T1** | predicate query the engine can evaluate in memory | result + predicate | **precise** โ membership transitions |
|
|
111
|
+
| **T2** | `$where` / `$text` / regex / geo, `distinct`, `estimatedDocumentCount`, populated queries | result + collection tags | **conservative** โ any write to a tagged collection |
|
|
112
|
+
| **T3** | aggregations (incl. `$lookup` / `$unionWith` / `$graphLookup`) | result + collection tags | **conservative** โ any write to any touched collection |
|
|
113
|
+
| **T4** | inside a transaction / cursor / `$out` / `$merge` | not cached | n/a |
|
|
114
|
+
|
|
115
|
+
Two complementary invalidation mechanisms keep both correct:
|
|
116
|
+
|
|
117
|
+
**Precise (T0/T1).** On every write, the engine takes the changed document's *before-* and *after-images* and evaluates each cached query's predicate against them. It invalidates a cached result when the document:
|
|
118
|
+
- was in the result set and changed (direct membership),
|
|
119
|
+
- newly matches the predicate (entering),
|
|
120
|
+
- no longer matches (leaving), or
|
|
121
|
+
- could affect a `limit`ed top-N window.
|
|
122
|
+
|
|
123
|
+
A predicate is only handled this way if every operator in it is one the engine can faithfully evaluate (the [supported operators](#supported-query-operators)). Anything else is downgraded to conservative โ we never run precise invalidation on a predicate we can't reproduce exactly.
|
|
124
|
+
|
|
125
|
+
**Conservative (T2/T3).** Each entry is tagged with the collection(s) it reads โ including foreign collections pulled in by `$lookup` or `populate`. Any write to a tagged collection drains all of its entries. Lower hit rate, still never stale.
|
|
126
|
+
|
|
127
|
+
Underneath, the cache is protected against the two classic correctness bugs:
|
|
128
|
+
|
|
129
|
+
- **Cache stampede** โ concurrent misses for the same key collapse into a single MongoDB load (in-process single-flight).
|
|
130
|
+
- **Write-after-read race** โ a per-model version token is captured before the DB read and re-checked before the cache write; if a write bumped it mid-load, the (possibly stale) value is not cached.
|
|
131
|
+
|
|
132
|
+
## Configuration
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
createCache({
|
|
136
|
+
mongoose, // your Mongoose instance (required)
|
|
137
|
+
redis, // an ioredis client (required)
|
|
138
|
+
|
|
139
|
+
// Opt-in model map. Omit to cache every registered model.
|
|
140
|
+
models: {
|
|
141
|
+
Product: { ttlMs: 1_800_000 },
|
|
142
|
+
Invoice: { enabled: false }, // never cache this model
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
defaults: { ttlMs: 600_000 }, // fallback TTL backstop for all models
|
|
146
|
+
|
|
147
|
+
tenant: () => getTenantId(), // keyspace isolation (see below)
|
|
148
|
+
|
|
149
|
+
store, // optional: override the storage backend
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Options
|
|
154
|
+
|
|
155
|
+
| Option | Type | Description |
|
|
156
|
+
|--------|------|-------------|
|
|
157
|
+
| `mongoose` | `Mongoose` | **Required.** Your Mongoose instance. |
|
|
158
|
+
| `redis` | `Redis` | **Required.** An `ioredis` client. |
|
|
159
|
+
| `models` | `Record<string, ModelConfig>` | Opt-in model map. Omit to cache all registered models. |
|
|
160
|
+
| `defaults` | `{ ttlMs?: number }` | Default TTL backstop applied to every model. |
|
|
161
|
+
| `tenant` | `() => string \| undefined` | Resolves the current tenant id for keyspace isolation. |
|
|
162
|
+
| `store` | `CacheStore` | Override the storage backend (defaults to Redis). |
|
|
163
|
+
|
|
164
|
+
### Per-model config (`ModelConfig`)
|
|
165
|
+
|
|
166
|
+
| Field | Type | Description |
|
|
167
|
+
|-------|------|-------------|
|
|
168
|
+
| `ttlMs` | `number` | TTL backstop in milliseconds. TTL is a *safety net*, not the freshness mechanism โ precise/tag invalidation is. |
|
|
169
|
+
| `enabled` | `boolean` | Set `false` to bypass caching for this model entirely. |
|
|
170
|
+
|
|
171
|
+
> **TTL is a backstop, not the source of truth.** Freshness comes from invalidation; TTL only bounds how long an entry can live if something is ever missed. Models that must never be slightly stale (e.g. `Invoice`) can set `enabled: false`.
|
|
172
|
+
|
|
173
|
+
### Cleanup
|
|
174
|
+
|
|
175
|
+
`createCache` patches Mongoose interception points on the instance. Call `cache.close()` to restore them (useful in tests or on graceful shutdown):
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
const cache = createCache({ mongoose, redis });
|
|
179
|
+
// ...
|
|
180
|
+
cache.close();
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## What gets cached & invalidated
|
|
184
|
+
|
|
185
|
+
**Read operations cached**
|
|
186
|
+
|
|
187
|
+
| Operation | Tier | Notes |
|
|
188
|
+
|-----------|------|-------|
|
|
189
|
+
| `find`, `findOne`, `findById` | T0 / T1 | precise; `.lean()` is ideal, hydrated docs are re-hydrated on hit |
|
|
190
|
+
| `countDocuments` | T1 | precise |
|
|
191
|
+
| `distinct` | T2 | conservative (depends on field values) |
|
|
192
|
+
| `estimatedDocumentCount` | T2 | conservative (collection metadata) |
|
|
193
|
+
| `aggregate` | T3 | conservative; tagged with every touched collection |
|
|
194
|
+
| `find().populate(โฆ)` | T2 | conservative; tagged with root + foreign collections |
|
|
195
|
+
|
|
196
|
+
Cursor/streaming reads and any query bound to a session/transaction are **never cached**.
|
|
197
|
+
|
|
198
|
+
**Write operations that invalidate**
|
|
199
|
+
|
|
200
|
+
`save` ยท `create` ยท `insertMany` ยท `updateOne` ยท `updateMany` ยท `findOneAndUpdate` ยท `replaceOne` ยท `findOneAndReplace` ยท `deleteOne` ยท `deleteMany` ยท `findOneAndDelete` ยท upserts ยท `bulkWrite`
|
|
201
|
+
|
|
202
|
+
`bulkWrite` and upsert-created documents can't be individually imaged, so they fall back to a conservative model-wide flush โ coarse, but never stale.
|
|
203
|
+
|
|
204
|
+
## Supported query operators
|
|
205
|
+
|
|
206
|
+
A predicate is invalidated **precisely (T1)** only if every operator in it is one the engine can evaluate exactly. The supported set is:
|
|
207
|
+
|
|
208
|
+
- **Comparison:** `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`
|
|
209
|
+
- **Logical:** `$and`, `$or`, `$nor`, `$not`
|
|
210
|
+
- **Element:** `$exists`
|
|
211
|
+
- Implicit equality, dot-path nesting (`"a.b"`), and array-contains semantics
|
|
212
|
+
|
|
213
|
+
Anything else โ `$where`, `$text`, `$regex`/RegExp literals, geo operators, `$expr`, a RegExp inside `$in`/`$nin` โ is sound to *run* but not to invalidate precisely, so such queries are transparently downgraded to conservative (collection-tag) caching. You don't lose caching; you lose only per-document precision.
|
|
214
|
+
|
|
215
|
+
## Examples
|
|
216
|
+
|
|
217
|
+
### Aggregation with a join
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
const cache = createCache({ mongoose, redis, models: { Book: {}, Author: {} } });
|
|
221
|
+
|
|
222
|
+
const byAuthor = await Book.aggregate([
|
|
223
|
+
{ $lookup: { from: "authors", localField: "author", foreignField: "_id", as: "a" } },
|
|
224
|
+
{ $unwind: "$a" },
|
|
225
|
+
{ $group: { _id: "$a.name", count: { $sum: 1 } } },
|
|
226
|
+
]);
|
|
227
|
+
// Cached, tagged with both `books` and `authors`.
|
|
228
|
+
|
|
229
|
+
await Author.updateOne({ _id }, { name: "New Name" });
|
|
230
|
+
// A write to the authors collection invalidates the cached aggregation.
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Multi-tenant isolation
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
237
|
+
|
|
238
|
+
const als = new AsyncLocalStorage<{ tenantId: string }>();
|
|
239
|
+
const cache = createCache({
|
|
240
|
+
mongoose,
|
|
241
|
+
redis,
|
|
242
|
+
models: { Order: {} },
|
|
243
|
+
tenant: () => als.getStore()?.tenantId,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Each tenant gets an isolated cache keyspace; the same query under a different
|
|
247
|
+
// tenant is a separate cache entry.
|
|
248
|
+
als.run({ tenantId: "acme" }, () => Order.find({ status: "open" }).lean());
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Metrics from events
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
let hits = 0, misses = 0;
|
|
255
|
+
cache.on("hit", () => hits++);
|
|
256
|
+
cache.on("miss", () => misses++);
|
|
257
|
+
|
|
258
|
+
setInterval(() => {
|
|
259
|
+
const total = hits + misses;
|
|
260
|
+
console.log(`hit rate: ${total ? ((hits / total) * 100).toFixed(1) : 0}%`);
|
|
261
|
+
}, 10_000);
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Events
|
|
265
|
+
|
|
266
|
+
`createCache` returns an `EventEmitter`. All payloads are plain objects.
|
|
267
|
+
|
|
268
|
+
| Event | Payload | Emitted when |
|
|
269
|
+
|-------|---------|--------------|
|
|
270
|
+
| `hit` | `{ key, model }` | a read is served from cache |
|
|
271
|
+
| `miss` | `{ key, model }` | a read falls through to MongoDB |
|
|
272
|
+
| `invalidate` | `{ keys, model? , collection? }` | cache keys are deleted by a write |
|
|
273
|
+
| `error` | `Error` | a cache (Redis) operation fails โ the request still succeeds against MongoDB |
|
|
274
|
+
|
|
275
|
+
> The `error` event is only emitted if you attach a listener, so an unhandled cache error never crashes your process.
|
|
276
|
+
|
|
277
|
+
## Correctness & testing
|
|
278
|
+
|
|
279
|
+
Correctness is treated as the product, so it's tested like one โ **183 tests**, including a differential fuzzer that is the strongest evidence of the never-stale guarantee:
|
|
280
|
+
|
|
281
|
+
- **Differential fuzzer** โ 6 seeds ร 200 random operations against **real MongoDB**. Every random read (`find`, `count`, `findOne`, `findById`, `distinct`, order-sensitive top-N) is compared against the *same query run un-cached*; any divergence fails the test with a reproducible seed. Writes cover every path including upserts and `bulkWrite`.
|
|
282
|
+
- **Concurrency stress** โ interleaved parallel reads and writes, asserting no permanently-stale entry survives.
|
|
283
|
+
- **Membership-transition matrix** โ entering / leaving / direct / top-N invalidation edges.
|
|
284
|
+
- **No-stale-read race** โ a write landing mid-load must not be cached.
|
|
285
|
+
- **Degradation** โ Redis failures fall through to MongoDB without throwing.
|
|
286
|
+
- **Full integration** โ aggregation, `$lookup`, populate, distinct, `bulkWrite`, sessions, multi-tenant, pagination โ all against `mongodb-memory-server`.
|
|
287
|
+
|
|
288
|
+
Run them yourself:
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
npm test
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Limitations
|
|
295
|
+
|
|
296
|
+
By design, we cache less rather than cache wrong:
|
|
297
|
+
|
|
298
|
+
1. **Aggregations and T2 queries are invalidated conservatively** (by collection), never per-document. High write volume on involved collections means a lower hit rate for those entries.
|
|
299
|
+
2. **Out-of-band writes** โ changes made by another service or the Mongo shell are not yet synced (MongoDB Change Streams are on the roadmap). Today, invalidation covers writes made through the Mongoose instance you passed to `createCache`.
|
|
300
|
+
3. **Single-node focus** โ a shared in-process L1 layer and cross-node pub/sub are on the roadmap. The Redis layer is already shared and safe across nodes; only the optional in-process layer and an atomic Lua version-guard remain.
|
|
301
|
+
4. **Conservative entries have a small cross-collection write-race window** โ the version guard covers the root collection only; a subsequent write to either collection clears it. Precise (T0/T1) entries have no such window.
|
|
302
|
+
|
|
303
|
+
## Requirements & compatibility
|
|
304
|
+
|
|
305
|
+
| | |
|
|
306
|
+
|--|--|
|
|
307
|
+
| Node.js | >= 18 |
|
|
308
|
+
| Mongoose | >= 7 (peer) |
|
|
309
|
+
| ioredis | >= 5 (peer) |
|
|
310
|
+
| Module formats | ESM + CommonJS, with type definitions |
|
|
311
|
+
|
|
312
|
+
## Roadmap
|
|
313
|
+
|
|
314
|
+
- [ ] MongoDB Change Streams for out-of-band write synchronization
|
|
315
|
+
- [ ] Cross-node invalidation via Redis Pub/Sub
|
|
316
|
+
- [ ] In-process L1 (LRU) layer for microsecond reads
|
|
317
|
+
- [ ] Refresh-ahead for conservative entries
|
|
318
|
+
- [ ] Atomic Lua version-guarded writes for multi-node precision
|
|
319
|
+
- [ ] Prometheus metrics endpoint
|
|
320
|
+
|
|
321
|
+
## API reference
|
|
322
|
+
|
|
323
|
+
The primary API is `createCache(options): Cache`. The package also exports its internals for advanced use and custom backends:
|
|
324
|
+
|
|
325
|
+
- **Plugin:** `createCache`, `Cache`, `CreateCacheOptions`, `ModelConfig`
|
|
326
|
+
- **Storage:** `CacheStore`, `InMemoryCacheStore`, `RedisCacheStore`, `DependencyIndex`, `InMemoryDependencyIndex`, `RedisDependencyIndex`
|
|
327
|
+
- **Engine:** `CacheManager`, `InvalidationEngine`, `classifyQuery`, `matches`, `isSupportedPredicate`
|
|
328
|
+
- **Keys & serialization:** `buildQueryKey`, `buildDocKey`, `normalizeQuery`, `stableHash`, `serialize`, `deserialize`
|
|
329
|
+
- **Types:** `Tier`, `Filter`, `Doc`, `CachedQuery`, `CacheKeyInput`, `QueryMeta`, `RegisteredQuery`, `WriteReport`
|
|
330
|
+
|
|
331
|
+
All are fully typed; see the bundled `.d.ts` for signatures.
|
|
332
|
+
|
|
333
|
+
## Development
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
npm install
|
|
337
|
+
npm test # vitest: unit + integration (mongodb-memory-server) + fuzzer
|
|
338
|
+
npm run typecheck
|
|
339
|
+
npm run build # tsup โ dist/ (ESM + CJS + types)
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
## License
|
|
343
|
+
|
|
344
|
+
MIT
|