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 +126 -48
- package/binding.d.ts +17 -9
- package/dist/express.d.mts +4 -0
- package/dist/express.d.ts +4 -0
- package/dist/express.js +13 -3
- package/dist/express.js.map +1 -1
- package/dist/express.mjs +13 -3
- package/dist/express.mjs.map +1 -1
- package/dist/fastify.js +5 -11
- package/dist/fastify.js.map +1 -1
- package/dist/fastify.mjs +5 -11
- package/dist/fastify.mjs.map +1 -1
- package/dist/index.d.mts +46 -17
- package/dist/index.d.ts +46 -17
- package/dist/index.js +119 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +119 -6
- package/dist/index.mjs.map +1 -1
- package/dist/nestjs.js +11 -3
- package/dist/nestjs.js.map +1 -1
- package/dist/nestjs.mjs +11 -3
- package/dist/nestjs.mjs.map +1 -1
- package/package.json +4 -3
- package/rust-node-cache.darwin-arm64.node +0 -0
- package/rust-node-cache.darwin-x64.node +0 -0
- package/rust-node-cache.linux-arm64-gnu.node +0 -0
- package/rust-node-cache.linux-arm64-musl.node +0 -0
- package/rust-node-cache.linux-x64-gnu.node +0 -0
- package/rust-node-cache.linux-x64-musl.node +0 -0
- package/rust-node-cache.win32-x64-msvc.node +0 -0
package/README.md
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
# rust-node-cache
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**In-memory cache for Node.js that keeps your data off the V8 heap.**
|
|
4
4
|
|
|
5
|
-
`rust-node-cache`
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
- ๐ฆ **
|
|
25
|
-
-
|
|
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
|
-
-
|
|
29
|
-
|
|
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
|
-
|
|
193
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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
|
|
322
|
+
Rust cache engine (set / get / ttl / stats / cleanup)
|
|
247
323
|
โ
|
|
248
|
-
|
|
324
|
+
LruCache behind a Mutex (hash map + recency list: O(1) lookups and evictions)
|
|
249
325
|
```
|
|
250
326
|
|
|
251
|
-
Values cross the boundary as
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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 |
|
|
386
|
-
| v0.4 |
|
|
387
|
-
| v0.5 |
|
|
388
|
-
| v0.6 |
|
|
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
|
-
/**
|
|
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
|
|
46
|
-
* atomic counters). Every method takes `&self`: the state is
|
|
47
|
-
* inside, so it is safe to
|
|
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
|
-
|
|
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). */
|
package/dist/express.d.mts
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.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.
|
|
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
|
-
|
|
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
|
package/dist/express.js.map
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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
|
};
|
package/dist/express.mjs.map
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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
|
package/dist/fastify.js.map
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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
|
};
|
package/dist/fastify.mjs.map
CHANGED
|
@@ -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.
|
|
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":[]}
|