rust-node-cache 0.2.1 โ†’ 0.3.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/README.md CHANGED
@@ -1,11 +1,15 @@
1
1
  # rust-node-cache
2
2
 
3
- **Ultra-fast in-memory cache powered by Rust.**
3
+ **In-memory cache for Node.js that keeps your data off the V8 heap.**
4
4
 
5
- `rust-node-cache` is a high-performance in-memory cache for Node.js applications
6
- with a core written in Rust. It is built on top of a concurrent, lock-sharded
7
- hash map ([DashMap](https://docs.rs/dashmap)), giving you fast reads, fast
8
- writes, and thread-safe access with very low overhead.
5
+ `rust-node-cache` stores entries in Rust memory instead of on the JavaScript
6
+ heap. A large cache adds nothing for the garbage collector to trace, so it does
7
+ not lengthen GC pauses or count against `--max-old-space-size`. Eviction is
8
+ exact LRU in O(1), with an optional TTL per entry and a background sweeper.
9
+
10
+ Values are kept as JSON. If you want the fastest possible cache of live object
11
+ references, use [`lru-cache`](https://www.npmjs.com/package/lru-cache) โ€” see
12
+ [When to use it](#when-to-use-it).
9
13
 
10
14
  It ships as a prebuilt native addon (via [napi-rs](https://napi.rs/)), so there
11
15
  is **no compiler required at install time** on supported platforms.
@@ -21,12 +25,13 @@ is **no compiler required at install time** on supported platforms.
21
25
 
22
26
  ## Features
23
27
 
24
- - ๐Ÿฆ€ **Rust-powered** core for predictable, low-overhead performance
25
- - ๐Ÿ”’ **Thread-safe** via `DashMap` (lock-sharded concurrent map)
26
- - โฑ๏ธ **TTL support** per entry (lazy expiration + active cleanup)
28
+ - ๐Ÿฆ€ **Off-heap storage** โ€” entries live in Rust memory; the V8 heap and GC do not see them
29
+ - ๐Ÿง  **Exact LRU eviction in O(1)** โ€” bound the cache by `maxSize` with `evictionPolicy: "lru"`
30
+ - โฑ๏ธ **TTL support** per entry, with millisecond precision (lazy expiration + active cleanup)
27
31
  - ๐Ÿงน **Background expiration** โ€” optional sweeper thread (`cleanupIntervalSeconds`)
28
- - ๐Ÿง  **LRU eviction** โ€” bound the cache by `maxSize` with `evictionPolicy: "lru"`
29
- - โšก **Fast reads** and **fast writes**
32
+ - ๐Ÿ›ก๏ธ **What you store is what you get** โ€” values that JSON cannot represent are rejected with
33
+ a clear error instead of coming back changed
34
+ - ๐Ÿ“ฆ **Raw JSON access** โ€” `setJson`/`getJson` store and serve pre-serialized responses
30
35
  - ๐Ÿ“Š Built-in **statistics** (hits, misses, sets, deletes, expired, evicted, size)
31
36
  - ๐Ÿงฉ **TypeScript** support with generics out of the box
32
37
  - ๐ŸŒ **Framework agnostic** โ€” works with Express, Fastify, NestJS, Hono,
@@ -92,7 +97,8 @@ console.log(cache.get("greeting")); // "hello"
92
97
  ### `set(key, value, options?)`
93
98
 
94
99
  Stores (or overwrites) a value. Returns `true` on success, or `false` when a
95
- `maxSize` limit is set, the cache is full, and the key is new.
100
+ `maxSize` limit is set, the cache is full, and the key is new. Throws a
101
+ `TypeError` if the value cannot be stored as JSON (see [Values](#values)).
96
102
 
97
103
  ```ts
98
104
  cache.set("user:1", { id: 1, name: "Roberto" });
@@ -101,12 +107,25 @@ cache.set("session:123", session, { ttlSeconds: 60 });
101
107
 
102
108
  ### `get(key)`
103
109
 
104
- Returns the stored value, or `null` if the key is missing or expired.
110
+ Returns the stored value, or `null` if the key is missing or expired. Every call
111
+ returns a new copy, so changing it does not change the cache.
105
112
 
106
113
  ```ts
107
114
  const user = cache.get<User>("user:1"); // User | null
108
115
  ```
109
116
 
117
+ ### `setJson(key, json, options?)` / `getJson(key)`
118
+
119
+ Store and read text that is **already JSON**, without parsing or re-encoding it โ€”
120
+ for example an HTTP response body. `setJson` does not validate the text; storing
121
+ something that is not JSON makes a later `get()` throw. The framework adapters
122
+ below use this pair, so a cache HIT sends the stored text as is.
123
+
124
+ ```ts
125
+ cache.setJson("GET:/users/1", '{"id":1,"name":"Roberto"}', { ttlSeconds: 60 });
126
+ res.type("application/json").send(cache.getJson("GET:/users/1"));
127
+ ```
128
+
110
129
  ### `delete(key)`
111
130
 
112
131
  Removes a key. Returns `true` if it existed, `false` otherwise.
@@ -117,7 +136,8 @@ cache.delete("user:1"); // true
117
136
 
118
137
  ### `exists(key)`
119
138
 
120
- Returns `true` if the key exists and is still valid (not expired).
139
+ Returns `true` if the key exists and is still valid (not expired). It does not
140
+ count as a use for LRU eviction.
121
141
 
122
142
  ```ts
123
143
  cache.exists("user:1"); // boolean
@@ -141,14 +161,39 @@ cache.size(); // 42
141
161
 
142
162
  ---
143
163
 
164
+ ## Values
165
+
166
+ Values are stored as JSON, so the cache accepts what JSON represents exactly:
167
+ plain objects, arrays, strings, finite numbers, booleans and `null`. Anything
168
+ else would come back different from what you stored โ€” `JSON.stringify` turns a
169
+ `Date` into a string, a `Map` into `{}` and `NaN` into `null` โ€” so `set` throws a
170
+ `TypeError` that names where the problem is:
171
+
172
+ ```ts
173
+ cache.set("user:1", { profile: { createdAt: new Date() } });
174
+ // TypeError: rust-node-cache: cannot store value.profile.createdAt (a Date). Values are
175
+ // kept as JSON, so only plain objects, arrays, strings, finite numbers, booleans and null
176
+ // are accepted. Store date.toISOString() or date.getTime() instead.
177
+ ```
178
+
179
+ Rejected: `Date`, `Map`, `Set`, `BigInt`, class instances, functions, symbols,
180
+ `NaN`, `Infinity`, `undefined` as the value or as an array item, and circular
181
+ references. Object properties set to `undefined` are left out, as JSON does.
182
+
183
+ ---
184
+
144
185
  ## TTL
145
186
 
146
- Set an entry with an expiration in seconds:
187
+ Set an entry with an expiration in seconds. Fractions are allowed down to one
188
+ millisecond (`ttlSeconds: 0.5` is 500 ms):
147
189
 
148
190
  ```ts
149
191
  cache.set("session:123", session, { ttlSeconds: 60 });
150
192
  ```
151
193
 
194
+ `ttlSeconds` must be a positive number: `0`, negative values, `NaN` and
195
+ `Infinity` throw instead of expiring at once or never.
196
+
152
197
  After the TTL elapses, reads return `null`:
153
198
 
154
199
  ```ts
@@ -189,8 +234,11 @@ const b = new Cache({ maxSize: 1000, evictionPolicy: "lru" });
189
234
  b.evictionPolicy; // "lru"
190
235
  ```
191
236
 
192
- LRU recency is tracked per entry (updated on every `get` hit) and the evicted
193
- count is surfaced in `stats().evicted`. Overwriting an existing key never evicts.
237
+ Writes and `get` hits make an entry the most recently used; `exists` does not.
238
+ Eviction is O(1) however large the cache is, and the evicted count is surfaced in
239
+ `stats().evicted` (a victim that had already expired counts in `expired`
240
+ instead). Overwriting an existing key never evicts. `maxSize` must be a positive
241
+ integer.
194
242
 
195
243
  ---
196
244
 
@@ -219,20 +267,46 @@ the storage but does **not** reset the historical counters.
219
267
 
220
268
  ---
221
269
 
222
- ## Performance
270
+ ## When to use it
271
+
272
+ Each call crosses from JavaScript into Rust and values go through JSON, so this
273
+ cache is **slower than a pure-JavaScript cache** that just keeps references to
274
+ your objects. What it buys you is a cache the garbage collector does not have to
275
+ trace.
276
+
277
+ **Good fit:**
278
+
279
+ - large caches (hundreds of thousands of entries or more) in latency-sensitive
280
+ services, where heap growth means longer GC pauses;
281
+ - processes close to their heap limit (`--max-old-space-size`);
282
+ - caching HTTP responses that are JSON anyway (`setJson`/`getJson`, or the
283
+ Express/Fastify/NestJS adapters).
284
+
285
+ **Use `lru-cache` instead** when the cache is small, when you need to store live
286
+ objects (class instances, `Date`, `Map`) or when raw operations per second is
287
+ what matters most.
288
+
289
+ ---
290
+
291
+ ## Benchmarks
292
+
293
+ 200,000 entries, Apple M2, Node.js 24 (operations per second, higher is better):
223
294
 
224
- The cache core is implemented entirely in Rust and tuned for throughput:
295
+ | Operation | rust-node-cache 0.3 | rust-node-cache 0.2 | lru-cache 11 |
296
+ | --------------------------------- | ------------------- | ------------------- | ------------ |
297
+ | `set` (5-field object) | 1.6 M | 0.4 M | 12 M |
298
+ | `get` (5-field object) | 1.9 M | 0.5 M | 13 M |
299
+ | `set` (string) | 3.4 M | 2.2 M | 11 M |
300
+ | `get` (string) | 3.9 M | 3.0 M | 13 M |
301
+ | `set` that evicts, `maxSize` 100k | 2.0 M | 0.002 M | โ€” |
225
302
 
226
- - **Rust core** โ€” no GC pauses on the cache itself; deterministic memory handling
227
- via RAII (entries are freed the instant they leave the map).
228
- - **DashMap** โ€” the map is split into multiple internal shards, each guarded by
229
- its own lock, so operations on different keys proceed in parallel.
230
- - **Lock-free reads** for statistics โ€” counters use atomic integers
231
- (`AtomicU64`) and are incremented with relaxed atomics, avoiding a global lock.
232
- - **Low allocation** โ€” values are stored as a single contiguous `Vec<u8>`; there
233
- is no per-field boxing.
234
- - **Fast TTL checks** โ€” expiration is a single integer comparison against a
235
- cached millisecond timestamp.
303
+ Memory with 500,000 of those objects stored:
304
+
305
+ | | rust-node-cache | lru-cache |
306
+ | ------------------------ | --------------- | --------- |
307
+ | V8 heap growth | 0 MB | 102 MB |
308
+ | Full GC (`global.gc()`) | 1 ms | 26 ms |
309
+ | Process RSS growth | 150 MB | 174 MB |
236
310
 
237
311
  ---
238
312
 
@@ -241,17 +315,19 @@ The cache core is implemented entirely in Rust and tuned for throughput:
241
315
  ```txt
242
316
  Node.js
243
317
  โ†“
244
- napi-rs (the Rust โ‡† V8 bridge: #[napi] generates the glue code)
318
+ TypeScript layer (validates values, JSON.stringify / JSON.parse in V8)
319
+ โ†“
320
+ napi-rs (the Rust โ‡† V8 bridge: #[napi] generates the glue code)
245
321
  โ†“
246
- Rust Cache Engine (set / get / ttl / stats / cleanup)
322
+ Rust cache engine (set / get / ttl / stats / cleanup)
247
323
  โ†“
248
- DashMap (concurrent, lock-sharded HashMap<String, CacheEntry>)
324
+ LruCache behind a Mutex (hash map + recency list: O(1) lookups and evictions)
249
325
  ```
250
326
 
251
- Values cross the boundary as `serde_json::Value` and are serialized to JSON
252
- bytes (`Vec<u8>`) inside the engine. This keeps the storage layer agnostic to the
253
- shape of your data and leaves room to adopt binary formats (MessagePack, CBOR,
254
- Bincode) later without touching the cache logic.
327
+ Values cross the boundary as JSON **text**: V8 encodes and decodes JSON far
328
+ faster than an object graph can be converted field by field through N-API. The
329
+ lock is uncontended in practice โ€” JavaScript calls come from one thread, and the
330
+ only other thread is the optional sweeper. Statistics counters are atomics.
255
331
 
256
332
  ---
257
333
 
@@ -263,6 +339,8 @@ Bincode) later without touching the cache logic.
263
339
  | `set(key, value, options?)` | `boolean` | Store/overwrite a value. `options.ttlSeconds` sets expiration. |
264
340
  | `evictionPolicy` (getter) | `string` | The active policy: `"reject"` or `"lru"`. |
265
341
  | `get<T>(key)` | `T \| null` | Read a value (lazy-expires stale entries). |
342
+ | `setJson(key, json, options?)` | `boolean` | Store text that is already JSON, as is. |
343
+ | `getJson(key)` | `string \| null` | Read the stored JSON text without parsing it. |
266
344
  | `delete(key)` | `boolean` | Remove a key. `true` if it existed. |
267
345
  | `exists(key)` | `boolean` | Whether the key exists and is valid. |
268
346
  | `clear()` | `void` | Remove all entries. |
@@ -274,13 +352,13 @@ Bincode) later without touching the cache logic.
274
352
 
275
353
  ```ts
276
354
  interface CacheOptions {
277
- maxSize?: number;
355
+ maxSize?: number; // positive integer
278
356
  evictionPolicy?: "reject" | "lru"; // default "reject"
279
- cleanupIntervalSeconds?: number; // enables the background sweeper
357
+ cleanupIntervalSeconds?: number; // enables the background sweeper; fractions allowed
280
358
  }
281
359
 
282
360
  interface SetOptions {
283
- ttlSeconds?: number;
361
+ ttlSeconds?: number; // > 0, fractions allowed (min 0.001)
284
362
  }
285
363
 
286
364
  interface CacheStats {
@@ -324,6 +402,10 @@ app.get("/manual/:id", async (req, res) => {
324
402
  });
325
403
  ```
326
404
 
405
+ The middleware stores the JSON the client received: a `Date` in the body is
406
+ cached as its ISO string, exactly as `res.json` sent it, so a HIT and a MISS
407
+ return the same response.
408
+
327
409
  ---
328
410
 
329
411
  ## Fastify Example
@@ -361,18 +443,13 @@ app.useGlobalInterceptors(new CacheInterceptor({ cache, ttlSeconds: 60 }));
361
443
 
362
444
  ---
363
445
 
364
- ## Benchmarks
365
-
366
- Coming soon.
367
-
368
- ---
369
-
370
446
  ## Limitations
371
447
 
372
448
  - Local process only
373
449
  - Not distributed
374
450
  - Data is lost on restart
375
451
  - Multiple workers (cluster / PM2) have separate, independent caches
452
+ - Values must be JSON-representable (see [Values](#values))
376
453
 
377
454
  ---
378
455
 
@@ -382,10 +459,11 @@ Coming soon.
382
459
  | ------- | ------------------------ | ------ |
383
460
  | v0.1 | Basic cache | โœ… |
384
461
  | v0.2 | TTL cleanup thread + LRU eviction | โœ… |
385
- | v0.3 | LFU cache | ๐Ÿ”œ |
386
- | v0.4 | Redis synchronization | ๐Ÿ”œ |
387
- | v0.5 | Prometheus metrics | ๐Ÿ”œ |
388
- | v0.6 | ImmutableLog integration | ๐Ÿ”œ |
462
+ | v0.3 | O(1) LRU, JSON fast path, strict values, `setJson`/`getJson` | โœ… |
463
+ | v0.4 | One cache shared by `worker_threads` + `maxBytes` memory limit | ๐Ÿ”œ |
464
+ | v0.5 | LFU cache | ๐Ÿ”œ |
465
+ | v0.6 | Prometheus metrics | ๐Ÿ”œ |
466
+ | later | Redis synchronization, ImmutableLog integration | ๐Ÿ”œ |
389
467
 
390
468
  ### Future: ImmutableLog Integration
391
469
 
package/binding.d.ts CHANGED
@@ -20,13 +20,17 @@ export interface CacheOptions {
20
20
  evictionPolicy?: string
21
21
  /**
22
22
  * When set (> 0), starts a thread that sweeps expired entries every N
23
- * seconds (background expiration, on top of the lazy one).
23
+ * seconds (background expiration, on top of the lazy one). Fractions are
24
+ * allowed; `0` leaves the sweeper off.
24
25
  */
25
26
  cleanupIntervalSeconds?: number
26
27
  }
27
28
  /** Per-write options: `cache.set(key, value, { ttlSeconds })`. */
28
29
  export interface SetOptions {
29
- /** Time to live in seconds. Absent => the entry does not expire by time. */
30
+ /**
31
+ * Time to live in seconds (fractions allowed, at least 0.001). Absent =>
32
+ * the entry does not expire by time.
33
+ */
30
34
  ttlSeconds?: number
31
35
  }
32
36
  /** Object returned by `cache.stats()`. `i64` fields become `number` in JS. */
@@ -42,9 +46,9 @@ export interface CacheStatsObject {
42
46
  /**
43
47
  * In-memory cache exposed to Node as the `Cache` class.
44
48
  *
45
- * It delegates everything to `RustCache` (which holds the `DashMap` and the
46
- * atomic counters). Every method takes `&self`: the state is concurrent
47
- * inside, so it is safe to use from several threads (such as the sweeper).
49
+ * It delegates everything to `RustCache` (which holds the entries and the
50
+ * atomic counters). Every method takes `&self`: the state is synchronized
51
+ * inside, so it is safe to share with the background sweeper thread.
48
52
  */
49
53
  export declare class Cache {
50
54
  /** `new Cache()` or `new Cache({ maxSize, evictionPolicy, cleanupIntervalSeconds })`. */
@@ -53,11 +57,15 @@ export declare class Cache {
53
57
  get evictionPolicy(): string
54
58
  /**
55
59
  * Inserts or updates a key. Returns `true` on success, `false` if the cache
56
- * is full (`maxSize`) and the key is new.
60
+ * is full (`maxSize`) and the key is new. `value` is the JSON text of the
61
+ * value (encoded by the TypeScript layer).
62
+ */
63
+ set(key: string, value: string, options?: SetOptions | undefined | null): boolean
64
+ /**
65
+ * Reads a key. Returns the value's JSON text, or `null` if missing or
66
+ * expired.
57
67
  */
58
- set(key: string, value: any, options?: SetOptions | undefined | null): boolean
59
- /** Reads a key. Returns the value, or `null` if missing or expired. */
60
- get(key: string): any | null
68
+ get(key: string): string | null
61
69
  /** Removes a key. Returns `true` if it existed, `false` otherwise. */
62
70
  delete(key: string): boolean
63
71
  /** Whether the key exists and is still valid (not expired). */
@@ -28,6 +28,10 @@ interface CacheMiddlewareOptions {
28
28
  /**
29
29
  * Creates an Express middleware that caches JSON responses. Adds an
30
30
  * `X-Cache: HIT|MISS` header for observability.
31
+ *
32
+ * What is stored is the JSON the client received (a `Date` in the body is
33
+ * stored as its ISO string, as `res.json` sends it), and a HIT sends that text
34
+ * as is, without parsing or re-serializing it.
31
35
  */
32
36
  declare function cacheMiddleware(options: CacheMiddlewareOptions): (req: Request, res: Response, next: NextFunction) => void;
33
37
 
package/dist/express.d.ts CHANGED
@@ -28,6 +28,10 @@ interface CacheMiddlewareOptions {
28
28
  /**
29
29
  * Creates an Express middleware that caches JSON responses. Adds an
30
30
  * `X-Cache: HIT|MISS` header for observability.
31
+ *
32
+ * What is stored is the JSON the client received (a `Date` in the body is
33
+ * stored as its ISO string, as `res.json` sends it), and a HIT sends that text
34
+ * as is, without parsing or re-serializing it.
31
35
  */
32
36
  declare function cacheMiddleware(options: CacheMiddlewareOptions): (req: Request, res: Response, next: NextFunction) => void;
33
37
 
package/dist/express.js CHANGED
@@ -34,23 +34,33 @@ function cacheMiddleware(options) {
34
34
  return;
35
35
  }
36
36
  const key = keyGenerator(req);
37
- const cached = cache.get(key);
37
+ const cached = cache.getJson(key);
38
38
  if (cached !== null) {
39
39
  res.setHeader("X-Cache", "HIT");
40
- res.json(cached);
40
+ res.type("application/json").send(cached);
41
41
  return;
42
42
  }
43
43
  res.setHeader("X-Cache", "MISS");
44
44
  const originalJson = res.json.bind(res);
45
45
  res.json = (body) => {
46
46
  if (res.statusCode >= 200 && res.statusCode < 300) {
47
- cache.set(key, body, ttlSeconds ? { ttlSeconds } : void 0);
47
+ const json = toJson(body);
48
+ if (json !== void 0) {
49
+ cache.setJson(key, json, ttlSeconds ? { ttlSeconds } : void 0);
50
+ }
48
51
  }
49
52
  return originalJson(body);
50
53
  };
51
54
  next();
52
55
  };
53
56
  }
57
+ function toJson(body) {
58
+ try {
59
+ return JSON.stringify(body);
60
+ } catch {
61
+ return void 0;
62
+ }
63
+ }
54
64
  // Annotate the CommonJS export names for ESM import in node:
55
65
  0 && (module.exports = {
56
66
  cacheMiddleware
@@ -1 +1 @@
1
- {"version":3,"sources":["../js/express.ts"],"sourcesContent":["/**\n * Express integration: caching of JSON responses.\n *\n * Usage:\n * import { Cache } from \"rust-node-cache\";\n * import { cacheMiddleware } from \"rust-node-cache/express\";\n *\n * const cache = new Cache();\n * app.get(\"/users/:id\", cacheMiddleware({ cache, ttlSeconds: 60 }), handler);\n *\n * The middleware intercepts `res.json`: on a HIT it responds straight from the\n * cache; on a MISS it lets the handler run and stores the response body (2xx only).\n */\n\nimport type { Request, Response, NextFunction } from \"express\";\nimport type { Cache } from \"./index\";\n\nexport interface CacheMiddlewareOptions {\n /** Cache instance to use. */\n cache: Cache;\n /** TTL applied to stored responses (in seconds). */\n ttlSeconds?: number;\n /** HTTP methods eligible for caching. Default: `[\"GET\"]`. */\n methods?: string[];\n /** Builds the cache key from the request. Default: `METHOD:url`. */\n keyGenerator?: (req: Request) => string;\n}\n\n/**\n * Creates an Express middleware that caches JSON responses. Adds an\n * `X-Cache: HIT|MISS` header for observability.\n */\nexport function cacheMiddleware(options: CacheMiddlewareOptions) {\n const { cache } = options;\n const ttlSeconds = options.ttlSeconds;\n const methods = options.methods ?? [\"GET\"];\n const keyGenerator =\n options.keyGenerator ?? ((req: Request) => `${req.method}:${req.originalUrl}`);\n\n return function rustNodeCache(\n req: Request,\n res: Response,\n next: NextFunction,\n ): void {\n if (!methods.includes(req.method)) {\n next();\n return;\n }\n\n const key = keyGenerator(req);\n const cached = cache.get(key);\n if (cached !== null) {\n res.setHeader(\"X-Cache\", \"HIT\");\n res.json(cached);\n return;\n }\n\n res.setHeader(\"X-Cache\", \"MISS\");\n const originalJson = res.json.bind(res);\n res.json = (body: unknown): Response => {\n // Only successful responses (2xx) are stored.\n if (res.statusCode >= 200 && res.statusCode < 300) {\n cache.set(key, body, ttlSeconds ? { ttlSeconds } : undefined);\n }\n return originalJson(body);\n };\n\n next();\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAgCO,SAAS,gBAAgB,SAAiC;AAC/D,QAAM,EAAE,MAAM,IAAI;AAClB,QAAM,aAAa,QAAQ;AAC3B,QAAM,UAAU,QAAQ,WAAW,CAAC,KAAK;AACzC,QAAM,eACJ,QAAQ,iBAAiB,CAAC,QAAiB,GAAG,IAAI,MAAM,IAAI,IAAI,WAAW;AAE7E,SAAO,SAAS,cACd,KACA,KACA,MACM;AACN,QAAI,CAAC,QAAQ,SAAS,IAAI,MAAM,GAAG;AACjC,WAAK;AACL;AAAA,IACF;AAEA,UAAM,MAAM,aAAa,GAAG;AAC5B,UAAM,SAAS,MAAM,IAAI,GAAG;AAC5B,QAAI,WAAW,MAAM;AACnB,UAAI,UAAU,WAAW,KAAK;AAC9B,UAAI,KAAK,MAAM;AACf;AAAA,IACF;AAEA,QAAI,UAAU,WAAW,MAAM;AAC/B,UAAM,eAAe,IAAI,KAAK,KAAK,GAAG;AACtC,QAAI,OAAO,CAAC,SAA4B;AAEtC,UAAI,IAAI,cAAc,OAAO,IAAI,aAAa,KAAK;AACjD,cAAM,IAAI,KAAK,MAAM,aAAa,EAAE,WAAW,IAAI,MAAS;AAAA,MAC9D;AACA,aAAO,aAAa,IAAI;AAAA,IAC1B;AAEA,SAAK;AAAA,EACP;AACF;","names":[]}
1
+ {"version":3,"sources":["../js/express.ts"],"sourcesContent":["/**\n * Express integration: caching of JSON responses.\n *\n * Usage:\n * import { Cache } from \"rust-node-cache\";\n * import { cacheMiddleware } from \"rust-node-cache/express\";\n *\n * const cache = new Cache();\n * app.get(\"/users/:id\", cacheMiddleware({ cache, ttlSeconds: 60 }), handler);\n *\n * The middleware intercepts `res.json`: on a HIT it responds straight from the\n * cache; on a MISS it lets the handler run and stores the response body (2xx only).\n */\n\nimport type { Request, Response, NextFunction } from \"express\";\nimport type { Cache } from \"./index\";\n\nexport interface CacheMiddlewareOptions {\n /** Cache instance to use. */\n cache: Cache;\n /** TTL applied to stored responses (in seconds). */\n ttlSeconds?: number;\n /** HTTP methods eligible for caching. Default: `[\"GET\"]`. */\n methods?: string[];\n /** Builds the cache key from the request. Default: `METHOD:url`. */\n keyGenerator?: (req: Request) => string;\n}\n\n/**\n * Creates an Express middleware that caches JSON responses. Adds an\n * `X-Cache: HIT|MISS` header for observability.\n *\n * What is stored is the JSON the client received (a `Date` in the body is\n * stored as its ISO string, as `res.json` sends it), and a HIT sends that text\n * as is, without parsing or re-serializing it.\n */\nexport function cacheMiddleware(options: CacheMiddlewareOptions) {\n const { cache } = options;\n const ttlSeconds = options.ttlSeconds;\n const methods = options.methods ?? [\"GET\"];\n const keyGenerator =\n options.keyGenerator ?? ((req: Request) => `${req.method}:${req.originalUrl}`);\n\n return function rustNodeCache(\n req: Request,\n res: Response,\n next: NextFunction,\n ): void {\n if (!methods.includes(req.method)) {\n next();\n return;\n }\n\n const key = keyGenerator(req);\n const cached = cache.getJson(key);\n if (cached !== null) {\n res.setHeader(\"X-Cache\", \"HIT\");\n res.type(\"application/json\").send(cached);\n return;\n }\n\n res.setHeader(\"X-Cache\", \"MISS\");\n const originalJson = res.json.bind(res);\n res.json = (body: unknown): Response => {\n // Only successful responses (2xx) are stored.\n if (res.statusCode >= 200 && res.statusCode < 300) {\n const json = toJson(body);\n if (json !== undefined) {\n cache.setJson(key, json, ttlSeconds ? { ttlSeconds } : undefined);\n }\n }\n return originalJson(body);\n };\n\n next();\n };\n}\n\n/** The body as `res.json` would send it, or `undefined` when it cannot be cached. */\nfunction toJson(body: unknown): string | undefined {\n try {\n return JSON.stringify(body);\n } catch {\n return undefined; // circular or BigInt: `res.json` reports it, not the cache.\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAoCO,SAAS,gBAAgB,SAAiC;AAC/D,QAAM,EAAE,MAAM,IAAI;AAClB,QAAM,aAAa,QAAQ;AAC3B,QAAM,UAAU,QAAQ,WAAW,CAAC,KAAK;AACzC,QAAM,eACJ,QAAQ,iBAAiB,CAAC,QAAiB,GAAG,IAAI,MAAM,IAAI,IAAI,WAAW;AAE7E,SAAO,SAAS,cACd,KACA,KACA,MACM;AACN,QAAI,CAAC,QAAQ,SAAS,IAAI,MAAM,GAAG;AACjC,WAAK;AACL;AAAA,IACF;AAEA,UAAM,MAAM,aAAa,GAAG;AAC5B,UAAM,SAAS,MAAM,QAAQ,GAAG;AAChC,QAAI,WAAW,MAAM;AACnB,UAAI,UAAU,WAAW,KAAK;AAC9B,UAAI,KAAK,kBAAkB,EAAE,KAAK,MAAM;AACxC;AAAA,IACF;AAEA,QAAI,UAAU,WAAW,MAAM;AAC/B,UAAM,eAAe,IAAI,KAAK,KAAK,GAAG;AACtC,QAAI,OAAO,CAAC,SAA4B;AAEtC,UAAI,IAAI,cAAc,OAAO,IAAI,aAAa,KAAK;AACjD,cAAM,OAAO,OAAO,IAAI;AACxB,YAAI,SAAS,QAAW;AACtB,gBAAM,QAAQ,KAAK,MAAM,aAAa,EAAE,WAAW,IAAI,MAAS;AAAA,QAClE;AAAA,MACF;AACA,aAAO,aAAa,IAAI;AAAA,IAC1B;AAEA,SAAK;AAAA,EACP;AACF;AAGA,SAAS,OAAO,MAAmC;AACjD,MAAI;AACF,WAAO,KAAK,UAAU,IAAI;AAAA,EAC5B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
package/dist/express.mjs CHANGED
@@ -10,23 +10,33 @@ function cacheMiddleware(options) {
10
10
  return;
11
11
  }
12
12
  const key = keyGenerator(req);
13
- const cached = cache.get(key);
13
+ const cached = cache.getJson(key);
14
14
  if (cached !== null) {
15
15
  res.setHeader("X-Cache", "HIT");
16
- res.json(cached);
16
+ res.type("application/json").send(cached);
17
17
  return;
18
18
  }
19
19
  res.setHeader("X-Cache", "MISS");
20
20
  const originalJson = res.json.bind(res);
21
21
  res.json = (body) => {
22
22
  if (res.statusCode >= 200 && res.statusCode < 300) {
23
- cache.set(key, body, ttlSeconds ? { ttlSeconds } : void 0);
23
+ const json = toJson(body);
24
+ if (json !== void 0) {
25
+ cache.setJson(key, json, ttlSeconds ? { ttlSeconds } : void 0);
26
+ }
24
27
  }
25
28
  return originalJson(body);
26
29
  };
27
30
  next();
28
31
  };
29
32
  }
33
+ function toJson(body) {
34
+ try {
35
+ return JSON.stringify(body);
36
+ } catch {
37
+ return void 0;
38
+ }
39
+ }
30
40
  export {
31
41
  cacheMiddleware
32
42
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../js/express.ts"],"sourcesContent":["/**\n * Express integration: caching of JSON responses.\n *\n * Usage:\n * import { Cache } from \"rust-node-cache\";\n * import { cacheMiddleware } from \"rust-node-cache/express\";\n *\n * const cache = new Cache();\n * app.get(\"/users/:id\", cacheMiddleware({ cache, ttlSeconds: 60 }), handler);\n *\n * The middleware intercepts `res.json`: on a HIT it responds straight from the\n * cache; on a MISS it lets the handler run and stores the response body (2xx only).\n */\n\nimport type { Request, Response, NextFunction } from \"express\";\nimport type { Cache } from \"./index\";\n\nexport interface CacheMiddlewareOptions {\n /** Cache instance to use. */\n cache: Cache;\n /** TTL applied to stored responses (in seconds). */\n ttlSeconds?: number;\n /** HTTP methods eligible for caching. Default: `[\"GET\"]`. */\n methods?: string[];\n /** Builds the cache key from the request. Default: `METHOD:url`. */\n keyGenerator?: (req: Request) => string;\n}\n\n/**\n * Creates an Express middleware that caches JSON responses. Adds an\n * `X-Cache: HIT|MISS` header for observability.\n */\nexport function cacheMiddleware(options: CacheMiddlewareOptions) {\n const { cache } = options;\n const ttlSeconds = options.ttlSeconds;\n const methods = options.methods ?? [\"GET\"];\n const keyGenerator =\n options.keyGenerator ?? ((req: Request) => `${req.method}:${req.originalUrl}`);\n\n return function rustNodeCache(\n req: Request,\n res: Response,\n next: NextFunction,\n ): void {\n if (!methods.includes(req.method)) {\n next();\n return;\n }\n\n const key = keyGenerator(req);\n const cached = cache.get(key);\n if (cached !== null) {\n res.setHeader(\"X-Cache\", \"HIT\");\n res.json(cached);\n return;\n }\n\n res.setHeader(\"X-Cache\", \"MISS\");\n const originalJson = res.json.bind(res);\n res.json = (body: unknown): Response => {\n // Only successful responses (2xx) are stored.\n if (res.statusCode >= 200 && res.statusCode < 300) {\n cache.set(key, body, ttlSeconds ? { ttlSeconds } : undefined);\n }\n return originalJson(body);\n };\n\n next();\n };\n}\n"],"mappings":";AAgCO,SAAS,gBAAgB,SAAiC;AAC/D,QAAM,EAAE,MAAM,IAAI;AAClB,QAAM,aAAa,QAAQ;AAC3B,QAAM,UAAU,QAAQ,WAAW,CAAC,KAAK;AACzC,QAAM,eACJ,QAAQ,iBAAiB,CAAC,QAAiB,GAAG,IAAI,MAAM,IAAI,IAAI,WAAW;AAE7E,SAAO,SAAS,cACd,KACA,KACA,MACM;AACN,QAAI,CAAC,QAAQ,SAAS,IAAI,MAAM,GAAG;AACjC,WAAK;AACL;AAAA,IACF;AAEA,UAAM,MAAM,aAAa,GAAG;AAC5B,UAAM,SAAS,MAAM,IAAI,GAAG;AAC5B,QAAI,WAAW,MAAM;AACnB,UAAI,UAAU,WAAW,KAAK;AAC9B,UAAI,KAAK,MAAM;AACf;AAAA,IACF;AAEA,QAAI,UAAU,WAAW,MAAM;AAC/B,UAAM,eAAe,IAAI,KAAK,KAAK,GAAG;AACtC,QAAI,OAAO,CAAC,SAA4B;AAEtC,UAAI,IAAI,cAAc,OAAO,IAAI,aAAa,KAAK;AACjD,cAAM,IAAI,KAAK,MAAM,aAAa,EAAE,WAAW,IAAI,MAAS;AAAA,MAC9D;AACA,aAAO,aAAa,IAAI;AAAA,IAC1B;AAEA,SAAK;AAAA,EACP;AACF;","names":[]}
1
+ {"version":3,"sources":["../js/express.ts"],"sourcesContent":["/**\n * Express integration: caching of JSON responses.\n *\n * Usage:\n * import { Cache } from \"rust-node-cache\";\n * import { cacheMiddleware } from \"rust-node-cache/express\";\n *\n * const cache = new Cache();\n * app.get(\"/users/:id\", cacheMiddleware({ cache, ttlSeconds: 60 }), handler);\n *\n * The middleware intercepts `res.json`: on a HIT it responds straight from the\n * cache; on a MISS it lets the handler run and stores the response body (2xx only).\n */\n\nimport type { Request, Response, NextFunction } from \"express\";\nimport type { Cache } from \"./index\";\n\nexport interface CacheMiddlewareOptions {\n /** Cache instance to use. */\n cache: Cache;\n /** TTL applied to stored responses (in seconds). */\n ttlSeconds?: number;\n /** HTTP methods eligible for caching. Default: `[\"GET\"]`. */\n methods?: string[];\n /** Builds the cache key from the request. Default: `METHOD:url`. */\n keyGenerator?: (req: Request) => string;\n}\n\n/**\n * Creates an Express middleware that caches JSON responses. Adds an\n * `X-Cache: HIT|MISS` header for observability.\n *\n * What is stored is the JSON the client received (a `Date` in the body is\n * stored as its ISO string, as `res.json` sends it), and a HIT sends that text\n * as is, without parsing or re-serializing it.\n */\nexport function cacheMiddleware(options: CacheMiddlewareOptions) {\n const { cache } = options;\n const ttlSeconds = options.ttlSeconds;\n const methods = options.methods ?? [\"GET\"];\n const keyGenerator =\n options.keyGenerator ?? ((req: Request) => `${req.method}:${req.originalUrl}`);\n\n return function rustNodeCache(\n req: Request,\n res: Response,\n next: NextFunction,\n ): void {\n if (!methods.includes(req.method)) {\n next();\n return;\n }\n\n const key = keyGenerator(req);\n const cached = cache.getJson(key);\n if (cached !== null) {\n res.setHeader(\"X-Cache\", \"HIT\");\n res.type(\"application/json\").send(cached);\n return;\n }\n\n res.setHeader(\"X-Cache\", \"MISS\");\n const originalJson = res.json.bind(res);\n res.json = (body: unknown): Response => {\n // Only successful responses (2xx) are stored.\n if (res.statusCode >= 200 && res.statusCode < 300) {\n const json = toJson(body);\n if (json !== undefined) {\n cache.setJson(key, json, ttlSeconds ? { ttlSeconds } : undefined);\n }\n }\n return originalJson(body);\n };\n\n next();\n };\n}\n\n/** The body as `res.json` would send it, or `undefined` when it cannot be cached. */\nfunction toJson(body: unknown): string | undefined {\n try {\n return JSON.stringify(body);\n } catch {\n return undefined; // circular or BigInt: `res.json` reports it, not the cache.\n }\n}\n"],"mappings":";AAoCO,SAAS,gBAAgB,SAAiC;AAC/D,QAAM,EAAE,MAAM,IAAI;AAClB,QAAM,aAAa,QAAQ;AAC3B,QAAM,UAAU,QAAQ,WAAW,CAAC,KAAK;AACzC,QAAM,eACJ,QAAQ,iBAAiB,CAAC,QAAiB,GAAG,IAAI,MAAM,IAAI,IAAI,WAAW;AAE7E,SAAO,SAAS,cACd,KACA,KACA,MACM;AACN,QAAI,CAAC,QAAQ,SAAS,IAAI,MAAM,GAAG;AACjC,WAAK;AACL;AAAA,IACF;AAEA,UAAM,MAAM,aAAa,GAAG;AAC5B,UAAM,SAAS,MAAM,QAAQ,GAAG;AAChC,QAAI,WAAW,MAAM;AACnB,UAAI,UAAU,WAAW,KAAK;AAC9B,UAAI,KAAK,kBAAkB,EAAE,KAAK,MAAM;AACxC;AAAA,IACF;AAEA,QAAI,UAAU,WAAW,MAAM;AAC/B,UAAM,eAAe,IAAI,KAAK,KAAK,GAAG;AACtC,QAAI,OAAO,CAAC,SAA4B;AAEtC,UAAI,IAAI,cAAc,OAAO,IAAI,aAAa,KAAK;AACjD,cAAM,OAAO,OAAO,IAAI;AACxB,YAAI,SAAS,QAAW;AACtB,gBAAM,QAAQ,KAAK,MAAM,aAAa,EAAE,WAAW,IAAI,MAAS;AAAA,QAClE;AAAA,MACF;AACA,aAAO,aAAa,IAAI;AAAA,IAC1B;AAEA,SAAK;AAAA,EACP;AACF;AAGA,SAAS,OAAO,MAAmC;AACjD,MAAI;AACF,WAAO,KAAK,UAAU,IAAI;AAAA,EAC5B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
package/dist/fastify.js CHANGED
@@ -37,10 +37,10 @@ async function cachePlugin(fastify, options) {
37
37
  return;
38
38
  }
39
39
  const key = keyGenerator(request);
40
- const cached = cache.get(key);
40
+ const cached = cache.getJson(key);
41
41
  if (cached !== null) {
42
42
  reply.header("X-Cache", "HIT");
43
- reply.send(cached);
43
+ reply.type("application/json; charset=utf-8").send(cached);
44
44
  return;
45
45
  }
46
46
  reply.header("X-Cache", "MISS");
@@ -52,20 +52,14 @@ async function cachePlugin(fastify, options) {
52
52
  "onSend",
53
53
  (request, reply, payload, done) => {
54
54
  const key = request[CACHE_KEY];
55
- if (key && typeof payload === "string" && reply.statusCode >= 200 && reply.statusCode < 300) {
56
- try {
57
- cache.set(
58
- key,
59
- JSON.parse(payload),
60
- ttlSeconds ? { ttlSeconds } : void 0
61
- );
62
- } catch {
63
- }
55
+ if (key && typeof payload === "string" && reply.statusCode >= 200 && reply.statusCode < 300 && String(reply.getHeader("content-type") ?? "").startsWith("application/json")) {
56
+ cache.setJson(key, payload, ttlSeconds ? { ttlSeconds } : void 0);
64
57
  }
65
58
  done(null, payload);
66
59
  }
67
60
  );
68
61
  }
62
+ cachePlugin[/* @__PURE__ */ Symbol.for("skip-override")] = true;
69
63
  // Annotate the CommonJS export names for ESM import in node:
70
64
  0 && (module.exports = {
71
65
  cachePlugin
@@ -1 +1 @@
1
- {"version":3,"sources":["../js/fastify.ts"],"sourcesContent":["/**\n * Fastify integration: caching of JSON responses through a plugin.\n *\n * Usage:\n * import { Cache } from \"rust-node-cache\";\n * import { cachePlugin } from \"rust-node-cache/fastify\";\n *\n * const cache = new Cache();\n * fastify.register(cachePlugin, { cache, ttlSeconds: 60 });\n *\n * Flow: the `onRequest` hook checks the cache and, on a HIT, responds right away\n * (short-circuit). On a MISS, the `onSend` hook stores the serialized payload.\n */\n\nimport type {\n FastifyInstance,\n FastifyReply,\n FastifyRequest,\n} from \"fastify\";\nimport type { Cache } from \"./index\";\n\nexport interface CachePluginOptions {\n /** Cache instance to use. */\n cache: Cache;\n /** TTL applied to stored responses (in seconds). */\n ttlSeconds?: number;\n /** HTTP methods eligible for caching. Default: `[\"GET\"]`. */\n methods?: string[];\n /** Builds the cache key from the request. Default: `METHOD:url`. */\n keyGenerator?: (req: FastifyRequest) => string;\n}\n\n// The computed key is kept on the request itself between hooks.\nconst CACHE_KEY = Symbol(\"rustNodeCacheKey\");\n\n/**\n * Fastify plugin that caches JSON responses. Use it with `fastify.register`.\n */\nexport async function cachePlugin(\n fastify: FastifyInstance,\n options: CachePluginOptions,\n): Promise<void> {\n const { cache } = options;\n const ttlSeconds = options.ttlSeconds;\n const methods = options.methods ?? [\"GET\"];\n const keyGenerator =\n options.keyGenerator ?? ((req: FastifyRequest) => `${req.method}:${req.url}`);\n\n fastify.addHook(\n \"onRequest\",\n (request: FastifyRequest, reply: FastifyReply, done: () => void) => {\n if (!methods.includes(request.method)) {\n done();\n return;\n }\n\n const key = keyGenerator(request);\n const cached = cache.get(key);\n if (cached !== null) {\n reply.header(\"X-Cache\", \"HIT\");\n reply.send(cached);\n return; // done() is not called: the response was already sent.\n }\n\n reply.header(\"X-Cache\", \"MISS\");\n (request as unknown as Record<symbol, string>)[CACHE_KEY] = key;\n done();\n },\n );\n\n fastify.addHook(\n \"onSend\",\n (\n request: FastifyRequest,\n reply: FastifyReply,\n payload: unknown,\n done: (err: Error | null, value?: unknown) => void,\n ) => {\n const key = (request as unknown as Record<symbol, string | undefined>)[\n CACHE_KEY\n ];\n if (\n key &&\n typeof payload === \"string\" &&\n reply.statusCode >= 200 &&\n reply.statusCode < 300\n ) {\n try {\n cache.set(\n key,\n JSON.parse(payload),\n ttlSeconds ? { ttlSeconds } : undefined,\n );\n } catch {\n // non-JSON payload: simply not cached.\n }\n }\n done(null, payload);\n },\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAiCA,IAAM,YAAY,uBAAO,kBAAkB;AAK3C,eAAsB,YACpB,SACA,SACe;AACf,QAAM,EAAE,MAAM,IAAI;AAClB,QAAM,aAAa,QAAQ;AAC3B,QAAM,UAAU,QAAQ,WAAW,CAAC,KAAK;AACzC,QAAM,eACJ,QAAQ,iBAAiB,CAAC,QAAwB,GAAG,IAAI,MAAM,IAAI,IAAI,GAAG;AAE5E,UAAQ;AAAA,IACN;AAAA,IACA,CAAC,SAAyB,OAAqB,SAAqB;AAClE,UAAI,CAAC,QAAQ,SAAS,QAAQ,MAAM,GAAG;AACrC,aAAK;AACL;AAAA,MACF;AAEA,YAAM,MAAM,aAAa,OAAO;AAChC,YAAM,SAAS,MAAM,IAAI,GAAG;AAC5B,UAAI,WAAW,MAAM;AACnB,cAAM,OAAO,WAAW,KAAK;AAC7B,cAAM,KAAK,MAAM;AACjB;AAAA,MACF;AAEA,YAAM,OAAO,WAAW,MAAM;AAC9B,MAAC,QAA8C,SAAS,IAAI;AAC5D,WAAK;AAAA,IACP;AAAA,EACF;AAEA,UAAQ;AAAA,IACN;AAAA,IACA,CACE,SACA,OACA,SACA,SACG;AACH,YAAM,MAAO,QACX,SACF;AACA,UACE,OACA,OAAO,YAAY,YACnB,MAAM,cAAc,OACpB,MAAM,aAAa,KACnB;AACA,YAAI;AACF,gBAAM;AAAA,YACJ;AAAA,YACA,KAAK,MAAM,OAAO;AAAA,YAClB,aAAa,EAAE,WAAW,IAAI;AAAA,UAChC;AAAA,QACF,QAAQ;AAAA,QAER;AAAA,MACF;AACA,WAAK,MAAM,OAAO;AAAA,IACpB;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../js/fastify.ts"],"sourcesContent":["/**\n * Fastify integration: caching of JSON responses through a plugin.\n *\n * Usage:\n * import { Cache } from \"rust-node-cache\";\n * import { cachePlugin } from \"rust-node-cache/fastify\";\n *\n * const cache = new Cache();\n * fastify.register(cachePlugin, { cache, ttlSeconds: 60 });\n *\n * Flow: the `onRequest` hook checks the cache and, on a HIT, responds right away\n * (short-circuit). On a MISS, the `onSend` hook stores the serialized payload.\n */\n\nimport type {\n FastifyInstance,\n FastifyReply,\n FastifyRequest,\n} from \"fastify\";\nimport type { Cache } from \"./index\";\n\nexport interface CachePluginOptions {\n /** Cache instance to use. */\n cache: Cache;\n /** TTL applied to stored responses (in seconds). */\n ttlSeconds?: number;\n /** HTTP methods eligible for caching. Default: `[\"GET\"]`. */\n methods?: string[];\n /** Builds the cache key from the request. Default: `METHOD:url`. */\n keyGenerator?: (req: FastifyRequest) => string;\n}\n\n// The computed key is kept on the request itself between hooks.\nconst CACHE_KEY = Symbol(\"rustNodeCacheKey\");\n\n/**\n * Fastify plugin that caches JSON responses. Use it with `fastify.register`.\n */\nexport async function cachePlugin(\n fastify: FastifyInstance,\n options: CachePluginOptions,\n): Promise<void> {\n const { cache } = options;\n const ttlSeconds = options.ttlSeconds;\n const methods = options.methods ?? [\"GET\"];\n const keyGenerator =\n options.keyGenerator ?? ((req: FastifyRequest) => `${req.method}:${req.url}`);\n\n fastify.addHook(\n \"onRequest\",\n (request: FastifyRequest, reply: FastifyReply, done: () => void) => {\n if (!methods.includes(request.method)) {\n done();\n return;\n }\n\n const key = keyGenerator(request);\n const cached = cache.getJson(key);\n if (cached !== null) {\n reply.header(\"X-Cache\", \"HIT\");\n reply.type(\"application/json; charset=utf-8\").send(cached);\n return; // done() is not called: the response was already sent.\n }\n\n reply.header(\"X-Cache\", \"MISS\");\n (request as unknown as Record<symbol, string>)[CACHE_KEY] = key;\n done();\n },\n );\n\n fastify.addHook(\n \"onSend\",\n (\n request: FastifyRequest,\n reply: FastifyReply,\n payload: unknown,\n done: (err: Error | null, value?: unknown) => void,\n ) => {\n const key = (request as unknown as Record<symbol, string | undefined>)[\n CACHE_KEY\n ];\n // The payload is already the serialized body: store that text as is.\n // Only JSON responses are cached.\n if (\n key &&\n typeof payload === \"string\" &&\n reply.statusCode >= 200 &&\n reply.statusCode < 300 &&\n String(reply.getHeader(\"content-type\") ?? \"\").startsWith(\"application/json\")\n ) {\n cache.setJson(key, payload, ttlSeconds ? { ttlSeconds } : undefined);\n }\n done(null, payload);\n },\n );\n}\n\n// Disables Fastify encapsulation so the hooks apply to the scope where the\n// plugin is registered (the same as wrapping it with `fastify-plugin`). Without\n// it, routes registered as \"siblings\" of the plugin are never cached.\n(cachePlugin as unknown as Record<symbol, boolean>)[\n Symbol.for(\"skip-override\")\n] = true;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAiCA,IAAM,YAAY,uBAAO,kBAAkB;AAK3C,eAAsB,YACpB,SACA,SACe;AACf,QAAM,EAAE,MAAM,IAAI;AAClB,QAAM,aAAa,QAAQ;AAC3B,QAAM,UAAU,QAAQ,WAAW,CAAC,KAAK;AACzC,QAAM,eACJ,QAAQ,iBAAiB,CAAC,QAAwB,GAAG,IAAI,MAAM,IAAI,IAAI,GAAG;AAE5E,UAAQ;AAAA,IACN;AAAA,IACA,CAAC,SAAyB,OAAqB,SAAqB;AAClE,UAAI,CAAC,QAAQ,SAAS,QAAQ,MAAM,GAAG;AACrC,aAAK;AACL;AAAA,MACF;AAEA,YAAM,MAAM,aAAa,OAAO;AAChC,YAAM,SAAS,MAAM,QAAQ,GAAG;AAChC,UAAI,WAAW,MAAM;AACnB,cAAM,OAAO,WAAW,KAAK;AAC7B,cAAM,KAAK,iCAAiC,EAAE,KAAK,MAAM;AACzD;AAAA,MACF;AAEA,YAAM,OAAO,WAAW,MAAM;AAC9B,MAAC,QAA8C,SAAS,IAAI;AAC5D,WAAK;AAAA,IACP;AAAA,EACF;AAEA,UAAQ;AAAA,IACN;AAAA,IACA,CACE,SACA,OACA,SACA,SACG;AACH,YAAM,MAAO,QACX,SACF;AAGA,UACE,OACA,OAAO,YAAY,YACnB,MAAM,cAAc,OACpB,MAAM,aAAa,OACnB,OAAO,MAAM,UAAU,cAAc,KAAK,EAAE,EAAE,WAAW,kBAAkB,GAC3E;AACA,cAAM,QAAQ,KAAK,SAAS,aAAa,EAAE,WAAW,IAAI,MAAS;AAAA,MACrE;AACA,WAAK,MAAM,OAAO;AAAA,IACpB;AAAA,EACF;AACF;AAKC,YACC,uBAAO,IAAI,eAAe,CAC5B,IAAI;","names":[]}
package/dist/fastify.mjs CHANGED
@@ -13,10 +13,10 @@ async function cachePlugin(fastify, options) {
13
13
  return;
14
14
  }
15
15
  const key = keyGenerator(request);
16
- const cached = cache.get(key);
16
+ const cached = cache.getJson(key);
17
17
  if (cached !== null) {
18
18
  reply.header("X-Cache", "HIT");
19
- reply.send(cached);
19
+ reply.type("application/json; charset=utf-8").send(cached);
20
20
  return;
21
21
  }
22
22
  reply.header("X-Cache", "MISS");
@@ -28,20 +28,14 @@ async function cachePlugin(fastify, options) {
28
28
  "onSend",
29
29
  (request, reply, payload, done) => {
30
30
  const key = request[CACHE_KEY];
31
- if (key && typeof payload === "string" && reply.statusCode >= 200 && reply.statusCode < 300) {
32
- try {
33
- cache.set(
34
- key,
35
- JSON.parse(payload),
36
- ttlSeconds ? { ttlSeconds } : void 0
37
- );
38
- } catch {
39
- }
31
+ if (key && typeof payload === "string" && reply.statusCode >= 200 && reply.statusCode < 300 && String(reply.getHeader("content-type") ?? "").startsWith("application/json")) {
32
+ cache.setJson(key, payload, ttlSeconds ? { ttlSeconds } : void 0);
40
33
  }
41
34
  done(null, payload);
42
35
  }
43
36
  );
44
37
  }
38
+ cachePlugin[/* @__PURE__ */ Symbol.for("skip-override")] = true;
45
39
  export {
46
40
  cachePlugin
47
41
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../js/fastify.ts"],"sourcesContent":["/**\n * Fastify integration: caching of JSON responses through a plugin.\n *\n * Usage:\n * import { Cache } from \"rust-node-cache\";\n * import { cachePlugin } from \"rust-node-cache/fastify\";\n *\n * const cache = new Cache();\n * fastify.register(cachePlugin, { cache, ttlSeconds: 60 });\n *\n * Flow: the `onRequest` hook checks the cache and, on a HIT, responds right away\n * (short-circuit). On a MISS, the `onSend` hook stores the serialized payload.\n */\n\nimport type {\n FastifyInstance,\n FastifyReply,\n FastifyRequest,\n} from \"fastify\";\nimport type { Cache } from \"./index\";\n\nexport interface CachePluginOptions {\n /** Cache instance to use. */\n cache: Cache;\n /** TTL applied to stored responses (in seconds). */\n ttlSeconds?: number;\n /** HTTP methods eligible for caching. Default: `[\"GET\"]`. */\n methods?: string[];\n /** Builds the cache key from the request. Default: `METHOD:url`. */\n keyGenerator?: (req: FastifyRequest) => string;\n}\n\n// The computed key is kept on the request itself between hooks.\nconst CACHE_KEY = Symbol(\"rustNodeCacheKey\");\n\n/**\n * Fastify plugin that caches JSON responses. Use it with `fastify.register`.\n */\nexport async function cachePlugin(\n fastify: FastifyInstance,\n options: CachePluginOptions,\n): Promise<void> {\n const { cache } = options;\n const ttlSeconds = options.ttlSeconds;\n const methods = options.methods ?? [\"GET\"];\n const keyGenerator =\n options.keyGenerator ?? ((req: FastifyRequest) => `${req.method}:${req.url}`);\n\n fastify.addHook(\n \"onRequest\",\n (request: FastifyRequest, reply: FastifyReply, done: () => void) => {\n if (!methods.includes(request.method)) {\n done();\n return;\n }\n\n const key = keyGenerator(request);\n const cached = cache.get(key);\n if (cached !== null) {\n reply.header(\"X-Cache\", \"HIT\");\n reply.send(cached);\n return; // done() is not called: the response was already sent.\n }\n\n reply.header(\"X-Cache\", \"MISS\");\n (request as unknown as Record<symbol, string>)[CACHE_KEY] = key;\n done();\n },\n );\n\n fastify.addHook(\n \"onSend\",\n (\n request: FastifyRequest,\n reply: FastifyReply,\n payload: unknown,\n done: (err: Error | null, value?: unknown) => void,\n ) => {\n const key = (request as unknown as Record<symbol, string | undefined>)[\n CACHE_KEY\n ];\n if (\n key &&\n typeof payload === \"string\" &&\n reply.statusCode >= 200 &&\n reply.statusCode < 300\n ) {\n try {\n cache.set(\n key,\n JSON.parse(payload),\n ttlSeconds ? { ttlSeconds } : undefined,\n );\n } catch {\n // non-JSON payload: simply not cached.\n }\n }\n done(null, payload);\n },\n );\n}\n"],"mappings":";AAiCA,IAAM,YAAY,uBAAO,kBAAkB;AAK3C,eAAsB,YACpB,SACA,SACe;AACf,QAAM,EAAE,MAAM,IAAI;AAClB,QAAM,aAAa,QAAQ;AAC3B,QAAM,UAAU,QAAQ,WAAW,CAAC,KAAK;AACzC,QAAM,eACJ,QAAQ,iBAAiB,CAAC,QAAwB,GAAG,IAAI,MAAM,IAAI,IAAI,GAAG;AAE5E,UAAQ;AAAA,IACN;AAAA,IACA,CAAC,SAAyB,OAAqB,SAAqB;AAClE,UAAI,CAAC,QAAQ,SAAS,QAAQ,MAAM,GAAG;AACrC,aAAK;AACL;AAAA,MACF;AAEA,YAAM,MAAM,aAAa,OAAO;AAChC,YAAM,SAAS,MAAM,IAAI,GAAG;AAC5B,UAAI,WAAW,MAAM;AACnB,cAAM,OAAO,WAAW,KAAK;AAC7B,cAAM,KAAK,MAAM;AACjB;AAAA,MACF;AAEA,YAAM,OAAO,WAAW,MAAM;AAC9B,MAAC,QAA8C,SAAS,IAAI;AAC5D,WAAK;AAAA,IACP;AAAA,EACF;AAEA,UAAQ;AAAA,IACN;AAAA,IACA,CACE,SACA,OACA,SACA,SACG;AACH,YAAM,MAAO,QACX,SACF;AACA,UACE,OACA,OAAO,YAAY,YACnB,MAAM,cAAc,OACpB,MAAM,aAAa,KACnB;AACA,YAAI;AACF,gBAAM;AAAA,YACJ;AAAA,YACA,KAAK,MAAM,OAAO;AAAA,YAClB,aAAa,EAAE,WAAW,IAAI;AAAA,UAChC;AAAA,QACF,QAAQ;AAAA,QAER;AAAA,MACF;AACA,WAAK,MAAM,OAAO;AAAA,IACpB;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../js/fastify.ts"],"sourcesContent":["/**\n * Fastify integration: caching of JSON responses through a plugin.\n *\n * Usage:\n * import { Cache } from \"rust-node-cache\";\n * import { cachePlugin } from \"rust-node-cache/fastify\";\n *\n * const cache = new Cache();\n * fastify.register(cachePlugin, { cache, ttlSeconds: 60 });\n *\n * Flow: the `onRequest` hook checks the cache and, on a HIT, responds right away\n * (short-circuit). On a MISS, the `onSend` hook stores the serialized payload.\n */\n\nimport type {\n FastifyInstance,\n FastifyReply,\n FastifyRequest,\n} from \"fastify\";\nimport type { Cache } from \"./index\";\n\nexport interface CachePluginOptions {\n /** Cache instance to use. */\n cache: Cache;\n /** TTL applied to stored responses (in seconds). */\n ttlSeconds?: number;\n /** HTTP methods eligible for caching. Default: `[\"GET\"]`. */\n methods?: string[];\n /** Builds the cache key from the request. Default: `METHOD:url`. */\n keyGenerator?: (req: FastifyRequest) => string;\n}\n\n// The computed key is kept on the request itself between hooks.\nconst CACHE_KEY = Symbol(\"rustNodeCacheKey\");\n\n/**\n * Fastify plugin that caches JSON responses. Use it with `fastify.register`.\n */\nexport async function cachePlugin(\n fastify: FastifyInstance,\n options: CachePluginOptions,\n): Promise<void> {\n const { cache } = options;\n const ttlSeconds = options.ttlSeconds;\n const methods = options.methods ?? [\"GET\"];\n const keyGenerator =\n options.keyGenerator ?? ((req: FastifyRequest) => `${req.method}:${req.url}`);\n\n fastify.addHook(\n \"onRequest\",\n (request: FastifyRequest, reply: FastifyReply, done: () => void) => {\n if (!methods.includes(request.method)) {\n done();\n return;\n }\n\n const key = keyGenerator(request);\n const cached = cache.getJson(key);\n if (cached !== null) {\n reply.header(\"X-Cache\", \"HIT\");\n reply.type(\"application/json; charset=utf-8\").send(cached);\n return; // done() is not called: the response was already sent.\n }\n\n reply.header(\"X-Cache\", \"MISS\");\n (request as unknown as Record<symbol, string>)[CACHE_KEY] = key;\n done();\n },\n );\n\n fastify.addHook(\n \"onSend\",\n (\n request: FastifyRequest,\n reply: FastifyReply,\n payload: unknown,\n done: (err: Error | null, value?: unknown) => void,\n ) => {\n const key = (request as unknown as Record<symbol, string | undefined>)[\n CACHE_KEY\n ];\n // The payload is already the serialized body: store that text as is.\n // Only JSON responses are cached.\n if (\n key &&\n typeof payload === \"string\" &&\n reply.statusCode >= 200 &&\n reply.statusCode < 300 &&\n String(reply.getHeader(\"content-type\") ?? \"\").startsWith(\"application/json\")\n ) {\n cache.setJson(key, payload, ttlSeconds ? { ttlSeconds } : undefined);\n }\n done(null, payload);\n },\n );\n}\n\n// Disables Fastify encapsulation so the hooks apply to the scope where the\n// plugin is registered (the same as wrapping it with `fastify-plugin`). Without\n// it, routes registered as \"siblings\" of the plugin are never cached.\n(cachePlugin as unknown as Record<symbol, boolean>)[\n Symbol.for(\"skip-override\")\n] = true;\n"],"mappings":";AAiCA,IAAM,YAAY,uBAAO,kBAAkB;AAK3C,eAAsB,YACpB,SACA,SACe;AACf,QAAM,EAAE,MAAM,IAAI;AAClB,QAAM,aAAa,QAAQ;AAC3B,QAAM,UAAU,QAAQ,WAAW,CAAC,KAAK;AACzC,QAAM,eACJ,QAAQ,iBAAiB,CAAC,QAAwB,GAAG,IAAI,MAAM,IAAI,IAAI,GAAG;AAE5E,UAAQ;AAAA,IACN;AAAA,IACA,CAAC,SAAyB,OAAqB,SAAqB;AAClE,UAAI,CAAC,QAAQ,SAAS,QAAQ,MAAM,GAAG;AACrC,aAAK;AACL;AAAA,MACF;AAEA,YAAM,MAAM,aAAa,OAAO;AAChC,YAAM,SAAS,MAAM,QAAQ,GAAG;AAChC,UAAI,WAAW,MAAM;AACnB,cAAM,OAAO,WAAW,KAAK;AAC7B,cAAM,KAAK,iCAAiC,EAAE,KAAK,MAAM;AACzD;AAAA,MACF;AAEA,YAAM,OAAO,WAAW,MAAM;AAC9B,MAAC,QAA8C,SAAS,IAAI;AAC5D,WAAK;AAAA,IACP;AAAA,EACF;AAEA,UAAQ;AAAA,IACN;AAAA,IACA,CACE,SACA,OACA,SACA,SACG;AACH,YAAM,MAAO,QACX,SACF;AAGA,UACE,OACA,OAAO,YAAY,YACnB,MAAM,cAAc,OACpB,MAAM,aAAa,OACnB,OAAO,MAAM,UAAU,cAAc,KAAK,EAAE,EAAE,WAAW,kBAAkB,GAC3E;AACA,cAAM,QAAQ,KAAK,SAAS,aAAa,EAAE,WAAW,IAAI,MAAS;AAAA,MACrE;AACA,WAAK,MAAM,OAAO;AAAA,IACpB;AAAA,EACF;AACF;AAKC,YACC,uBAAO,IAAI,eAAe,CAC5B,IAAI;","names":[]}