rust-node-cache 0.2.0 → 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,
@@ -44,6 +49,25 @@ Works with both **ESM** and **CommonJS**, and requires **Node.js 18+**.
44
49
 
45
50
  ---
46
51
 
52
+ ## Supported platforms
53
+
54
+ Prebuilt binaries are published for:
55
+
56
+ | OS | Architecture | Triple |
57
+ | ------- | --------------------- | ---------------------------- |
58
+ | macOS | arm64 | `aarch64-apple-darwin` |
59
+ | macOS | x64 | `x86_64-apple-darwin` |
60
+ | Linux | x64 (glibc) | `x86_64-unknown-linux-gnu` |
61
+ | Linux | arm64 (glibc) | `aarch64-unknown-linux-gnu` |
62
+ | Linux | x64 (musl, Alpine) | `x86_64-unknown-linux-musl` |
63
+ | Linux | arm64 (musl, Alpine) | `aarch64-unknown-linux-musl` |
64
+ | Windows | x64 | `x86_64-pc-windows-msvc` |
65
+
66
+ Every release is installed from the packed tarball and exercised on each of these platforms
67
+ in CI before it is published.
68
+
69
+ ---
70
+
47
71
  ## Quick Start
48
72
 
49
73
  ```ts
@@ -73,7 +97,8 @@ console.log(cache.get("greeting")); // "hello"
73
97
  ### `set(key, value, options?)`
74
98
 
75
99
  Stores (or overwrites) a value. Returns `true` on success, or `false` when a
76
- `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)).
77
102
 
78
103
  ```ts
79
104
  cache.set("user:1", { id: 1, name: "Roberto" });
@@ -82,12 +107,25 @@ cache.set("session:123", session, { ttlSeconds: 60 });
82
107
 
83
108
  ### `get(key)`
84
109
 
85
- 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.
86
112
 
87
113
  ```ts
88
114
  const user = cache.get<User>("user:1"); // User | null
89
115
  ```
90
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
+
91
129
  ### `delete(key)`
92
130
 
93
131
  Removes a key. Returns `true` if it existed, `false` otherwise.
@@ -98,7 +136,8 @@ cache.delete("user:1"); // true
98
136
 
99
137
  ### `exists(key)`
100
138
 
101
- 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.
102
141
 
103
142
  ```ts
104
143
  cache.exists("user:1"); // boolean
@@ -122,14 +161,39 @@ cache.size(); // 42
122
161
 
123
162
  ---
124
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
+
125
185
  ## TTL
126
186
 
127
- 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):
128
189
 
129
190
  ```ts
130
191
  cache.set("session:123", session, { ttlSeconds: 60 });
131
192
  ```
132
193
 
194
+ `ttlSeconds` must be a positive number: `0`, negative values, `NaN` and
195
+ `Infinity` throw instead of expiring at once or never.
196
+
133
197
  After the TTL elapses, reads return `null`:
134
198
 
135
199
  ```ts
@@ -170,8 +234,11 @@ const b = new Cache({ maxSize: 1000, evictionPolicy: "lru" });
170
234
  b.evictionPolicy; // "lru"
171
235
  ```
172
236
 
173
- LRU recency is tracked per entry (updated on every `get` hit) and the evicted
174
- 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.
175
242
 
176
243
  ---
177
244
 
@@ -200,20 +267,46 @@ the storage but does **not** reset the historical counters.
200
267
 
201
268
  ---
202
269
 
203
- ## 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.
204
276
 
205
- The cache core is implemented entirely in Rust and tuned for throughput:
277
+ **Good fit:**
206
278
 
207
- - **Rust core** — no GC pauses on the cache itself; deterministic memory handling
208
- via RAII (entries are freed the instant they leave the map).
209
- - **DashMap** — the map is split into multiple internal shards, each guarded by
210
- its own lock, so operations on different keys proceed in parallel.
211
- - **Lock-free reads** for statistics — counters use atomic integers
212
- (`AtomicU64`) and are incremented with relaxed atomics, avoiding a global lock.
213
- - **Low allocation** — values are stored as a single contiguous `Vec<u8>`; there
214
- is no per-field boxing.
215
- - **Fast TTL checks** — expiration is a single integer comparison against a
216
- cached millisecond timestamp.
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):
294
+
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 | — |
302
+
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 |
217
310
 
218
311
  ---
219
312
 
@@ -222,17 +315,19 @@ The cache core is implemented entirely in Rust and tuned for throughput:
222
315
  ```txt
223
316
  Node.js
224
317
  ↓
225
- 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)
226
321
  ↓
227
- Rust Cache Engine (set / get / ttl / stats / cleanup)
322
+ Rust cache engine (set / get / ttl / stats / cleanup)
228
323
  ↓
229
- DashMap (concurrent, lock-sharded HashMap<String, CacheEntry>)
324
+ LruCache behind a Mutex (hash map + recency list: O(1) lookups and evictions)
230
325
  ```
231
326
 
232
- Values cross the boundary as `serde_json::Value` and are serialized to JSON
233
- bytes (`Vec<u8>`) inside the engine. This keeps the storage layer agnostic to the
234
- shape of your data and leaves room to adopt binary formats (MessagePack, CBOR,
235
- 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.
236
331
 
237
332
  ---
238
333
 
@@ -244,6 +339,8 @@ Bincode) later without touching the cache logic.
244
339
  | `set(key, value, options?)` | `boolean` | Store/overwrite a value. `options.ttlSeconds` sets expiration. |
245
340
  | `evictionPolicy` (getter) | `string` | The active policy: `"reject"` or `"lru"`. |
246
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. |
247
344
  | `delete(key)` | `boolean` | Remove a key. `true` if it existed. |
248
345
  | `exists(key)` | `boolean` | Whether the key exists and is valid. |
249
346
  | `clear()` | `void` | Remove all entries. |
@@ -255,13 +352,13 @@ Bincode) later without touching the cache logic.
255
352
 
256
353
  ```ts
257
354
  interface CacheOptions {
258
- maxSize?: number;
355
+ maxSize?: number; // positive integer
259
356
  evictionPolicy?: "reject" | "lru"; // default "reject"
260
- cleanupIntervalSeconds?: number; // enables the background sweeper
357
+ cleanupIntervalSeconds?: number; // enables the background sweeper; fractions allowed
261
358
  }
262
359
 
263
360
  interface SetOptions {
264
- ttlSeconds?: number;
361
+ ttlSeconds?: number; // > 0, fractions allowed (min 0.001)
265
362
  }
266
363
 
267
364
  interface CacheStats {
@@ -305,6 +402,10 @@ app.get("/manual/:id", async (req, res) => {
305
402
  });
306
403
  ```
307
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
+
308
409
  ---
309
410
 
310
411
  ## Fastify Example
@@ -342,18 +443,13 @@ app.useGlobalInterceptors(new CacheInterceptor({ cache, ttlSeconds: 60 }));
342
443
 
343
444
  ---
344
445
 
345
- ## Benchmarks
346
-
347
- Coming soon.
348
-
349
- ---
350
-
351
446
  ## Limitations
352
447
 
353
448
  - Local process only
354
449
  - Not distributed
355
450
  - Data is lost on restart
356
451
  - Multiple workers (cluster / PM2) have separate, independent caches
452
+ - Values must be JSON-representable (see [Values](#values))
357
453
 
358
454
  ---
359
455
 
@@ -363,10 +459,11 @@ Coming soon.
363
459
  | ------- | ------------------------ | ------ |
364
460
  | v0.1 | Basic cache | ✅ |
365
461
  | v0.2 | TTL cleanup thread + LRU eviction | ✅ |
366
- | v0.3 | LFU cache | 🔜 |
367
- | v0.4 | Redis synchronization | 🔜 |
368
- | v0.5 | Prometheus metrics | 🔜 |
369
- | 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 | 🔜 |
370
467
 
371
468
  ### Future: ImmutableLog Integration
372
469
 
package/binding.d.ts CHANGED
@@ -4,32 +4,36 @@
4
4
  /* auto-generated by NAPI-RS */
5
5
 
6
6
  /**
7
- * Opções do construtor:
7
+ * Constructor options:
8
8
  * `new Cache({ maxSize, evictionPolicy, cleanupIntervalSeconds })`.
9
9
  */
10
10
  export interface CacheOptions {
11
11
  /**
12
- * Limite opcional de chaves. Comportamento ao atingir depende de
13
- * `evictionPolicy`.
12
+ * Optional limit on the number of keys. What happens when it is reached
13
+ * depends on `evictionPolicy`.
14
14
  */
15
15
  maxSize?: number
16
16
  /**
17
- * `"reject"` (padrão) — `set` de chave nova retorna `false` quando cheio;
18
- * `"lru"` — remove a entrada menos recentemente usada e prossegue.
17
+ * `"reject"` (default) — `set` of a new key returns `false` when full;
18
+ * `"lru"` — removes the least recently used entry and goes on.
19
19
  */
20
20
  evictionPolicy?: string
21
21
  /**
22
- * Se definido (> 0), liga uma thread que varre entradas expiradas a cada
23
- * N segundos (expiração em background, além da lazy).
22
+ * When set (> 0), starts a thread that sweeps expired entries every N
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
- /** Opções por escrita: `cache.set(key, value, { ttlSeconds })`. */
28
+ /** Per-write options: `cache.set(key, value, { ttlSeconds })`. */
28
29
  export interface SetOptions {
29
- /** Tempo de vida em segundos. Ausente => a entrada não expira por tempo. */
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
- /** Objeto devolvido por `cache.stats()`. Campos `i64` viram `number` no JS. */
36
+ /** Object returned by `cache.stats()`. `i64` fields become `number` in JS. */
33
37
  export interface CacheStatsObject {
34
38
  hits: number
35
39
  misses: number
@@ -40,34 +44,38 @@ export interface CacheStatsObject {
40
44
  size: number
41
45
  }
42
46
  /**
43
- * Cache em memória exposto ao Node como a classe `Cache`.
47
+ * In-memory cache exposed to Node as the `Cache` class.
44
48
  *
45
- * Internamente delega tudo para o `RustCache` (que carrega o `DashMap` e os
46
- * contadores atômicos). Todos os métodos recebem `&self`: como o estado é
47
- * concorrente por dentro, várias chamadas podem rodar em paralelo.
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
- /** `new Cache()` ou `new Cache({ maxSize, evictionPolicy, cleanupIntervalSeconds })`. */
54
+ /** `new Cache()` or `new Cache({ maxSize, evictionPolicy, cleanupIntervalSeconds })`. */
51
55
  constructor(options?: CacheOptions | undefined | null)
52
- /** A política de evicção ativa: `"reject"` ou `"lru"`. */
56
+ /** The active eviction policy: `"reject"` or `"lru"`. */
53
57
  get evictionPolicy(): string
54
58
  /**
55
- * Insere/atualiza uma chave. Retorna `true` em sucesso, `false` se o cache
56
- * estiver cheio (`maxSize`) e a chave for nova.
59
+ * Inserts or updates a key. Returns `true` on success, `false` if the cache
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
- /** Lê uma chave. Retorna o valor ou `null` se ausente/expirada. */
60
- get(key: string): any | null
61
- /** Remove uma chave. Retorna `true` se existia, `false` caso contrário. */
68
+ get(key: string): string | null
69
+ /** Removes a key. Returns `true` if it existed, `false` otherwise. */
62
70
  delete(key: string): boolean
63
- /** Indica se a chave existe e está válida (não expirada). */
71
+ /** Whether the key exists and is still valid (not expired). */
64
72
  exists(key: string): boolean
65
- /** Esvazia todas as entradas do cache. */
73
+ /** Removes every entry from the cache. */
66
74
  clear(): void
67
- /** Quantidade de chaves armazenadas no momento. */
75
+ /** Number of keys currently stored. */
68
76
  size(): number
69
- /** Varre e remove todas as entradas expiradas. Retorna quantas removeu. */
77
+ /** Sweeps and removes every expired entry. Returns how many it removed. */
70
78
  cleanupExpired(): number
71
- /** Estatísticas acumuladas + tamanho atual. */
79
+ /** Accumulated statistics + the current size. */
72
80
  stats(): CacheStatsObject
73
81
  }
@@ -2,32 +2,36 @@ import { Request, Response, NextFunction } from 'express';
2
2
  import { Cache } from './index.mjs';
3
3
 
4
4
  /**
5
- * Integração com Express: cache de respostas JSON.
5
+ * Express integration: caching of JSON responses.
6
6
  *
7
- * Uso:
7
+ * Usage:
8
8
  * import { Cache } from "rust-node-cache";
9
9
  * import { cacheMiddleware } from "rust-node-cache/express";
10
10
  *
11
11
  * const cache = new Cache();
12
12
  * app.get("/users/:id", cacheMiddleware({ cache, ttlSeconds: 60 }), handler);
13
13
  *
14
- * O middleware intercepta `res.json`: num HIT responde direto do cache; num
15
- * MISS deixa o handler rodar e guarda o corpo da resposta (apenas em 2xx).
14
+ * The middleware intercepts `res.json`: on a HIT it responds straight from the
15
+ * cache; on a MISS it lets the handler run and stores the response body (2xx only).
16
16
  */
17
17
 
18
18
  interface CacheMiddlewareOptions {
19
- /** Instância de cache a usar. */
19
+ /** Cache instance to use. */
20
20
  cache: Cache;
21
- /** TTL aplicado às respostas guardadas (em segundos). */
21
+ /** TTL applied to stored responses (in seconds). */
22
22
  ttlSeconds?: number;
23
- /** Métodos HTTP elegíveis para cache. Padrão: `["GET"]`. */
23
+ /** HTTP methods eligible for caching. Default: `["GET"]`. */
24
24
  methods?: string[];
25
- /** Gera a chave de cache a partir da requisição. Padrão: `METHOD:url`. */
25
+ /** Builds the cache key from the request. Default: `METHOD:url`. */
26
26
  keyGenerator?: (req: Request) => string;
27
27
  }
28
28
  /**
29
- * Cria um middleware Express que cacheia respostas JSON. Adiciona o cabeçalho
30
- * `X-Cache: HIT|MISS` para facilitar a observabilidade.
29
+ * Creates an Express middleware that caches JSON responses. Adds an
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
@@ -2,32 +2,36 @@ import { Request, Response, NextFunction } from 'express';
2
2
  import { Cache } from './index.js';
3
3
 
4
4
  /**
5
- * Integração com Express: cache de respostas JSON.
5
+ * Express integration: caching of JSON responses.
6
6
  *
7
- * Uso:
7
+ * Usage:
8
8
  * import { Cache } from "rust-node-cache";
9
9
  * import { cacheMiddleware } from "rust-node-cache/express";
10
10
  *
11
11
  * const cache = new Cache();
12
12
  * app.get("/users/:id", cacheMiddleware({ cache, ttlSeconds: 60 }), handler);
13
13
  *
14
- * O middleware intercepta `res.json`: num HIT responde direto do cache; num
15
- * MISS deixa o handler rodar e guarda o corpo da resposta (apenas em 2xx).
14
+ * The middleware intercepts `res.json`: on a HIT it responds straight from the
15
+ * cache; on a MISS it lets the handler run and stores the response body (2xx only).
16
16
  */
17
17
 
18
18
  interface CacheMiddlewareOptions {
19
- /** Instância de cache a usar. */
19
+ /** Cache instance to use. */
20
20
  cache: Cache;
21
- /** TTL aplicado às respostas guardadas (em segundos). */
21
+ /** TTL applied to stored responses (in seconds). */
22
22
  ttlSeconds?: number;
23
- /** Métodos HTTP elegíveis para cache. Padrão: `["GET"]`. */
23
+ /** HTTP methods eligible for caching. Default: `["GET"]`. */
24
24
  methods?: string[];
25
- /** Gera a chave de cache a partir da requisição. Padrão: `METHOD:url`. */
25
+ /** Builds the cache key from the request. Default: `METHOD:url`. */
26
26
  keyGenerator?: (req: Request) => string;
27
27
  }
28
28
  /**
29
- * Cria um middleware Express que cacheia respostas JSON. Adiciona o cabeçalho
30
- * `X-Cache: HIT|MISS` para facilitar a observabilidade.
29
+ * Creates an Express middleware that caches JSON responses. Adds an
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 * Integração com Express: cache de respostas JSON.\n *\n * Uso:\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 * O middleware intercepta `res.json`: num HIT responde direto do cache; num\n * MISS deixa o handler rodar e guarda o corpo da resposta (apenas em 2xx).\n */\n\nimport type { Request, Response, NextFunction } from \"express\";\nimport type { Cache } from \"./index\";\n\nexport interface CacheMiddlewareOptions {\n /** Instância de cache a usar. */\n cache: Cache;\n /** TTL aplicado às respostas guardadas (em segundos). */\n ttlSeconds?: number;\n /** Métodos HTTP elegíveis para cache. Padrão: `[\"GET\"]`. */\n methods?: string[];\n /** Gera a chave de cache a partir da requisição. Padrão: `METHOD:url`. */\n keyGenerator?: (req: Request) => string;\n}\n\n/**\n * Cria um middleware Express que cacheia respostas JSON. Adiciona o cabeçalho\n * `X-Cache: HIT|MISS` para facilitar a observabilidade.\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 // Só guardamos respostas bem-sucedidas (2xx).\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
  };