rust-node-cache 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Roberto Lima
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,390 @@
1
+ # rust-node-cache
2
+
3
+ **Ultra-fast in-memory cache powered by Rust.**
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.
9
+
10
+ It ships as a prebuilt native addon (via [napi-rs](https://napi.rs/)), so there
11
+ is **no compiler required at install time** on supported platforms.
12
+
13
+ [![CI](https://github.com/robertolima-dev/rust-node-cache/actions/workflows/CI.yml/badge.svg)](https://github.com/robertolima-dev/rust-node-cache/actions/workflows/CI.yml)
14
+ [![npm](https://img.shields.io/npm/v/rust-node-cache.svg)](https://www.npmjs.com/package/rust-node-cache)
15
+ ![node](https://img.shields.io/badge/node-%3E%3D18-43853d)
16
+ ![license](https://img.shields.io/badge/license-MIT-blue)
17
+
18
+ ---
19
+
20
+ ## Features
21
+
22
+ - 🩀 **Rust-powered** core for predictable, low-overhead performance
23
+ - 🔒 **Thread-safe** via `DashMap` (lock-sharded concurrent map)
24
+ - ⏱ **TTL support** per entry (lazy expiration + active cleanup)
25
+ - ⚡ **Fast reads** and **fast writes**
26
+ - 📊 Built-in **statistics** (hits, misses, sets, deletes, expired, size)
27
+ - đŸ§© **TypeScript** support with generics out of the box
28
+ - 🌐 **Framework agnostic** — works with Express, Fastify, NestJS, Hono,
29
+ Next.js, or plain Node.js
30
+
31
+ ---
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ npm install rust-node-cache
37
+ ```
38
+
39
+ Works with both **ESM** and **CommonJS**, and requires **Node.js 18+**.
40
+
41
+ ---
42
+
43
+ ## Quick Start
44
+
45
+ ```ts
46
+ import { Cache } from "rust-node-cache";
47
+
48
+ const cache = new Cache();
49
+
50
+ cache.set("user:1", { id: 1, name: "Roberto" });
51
+
52
+ const user = cache.get<{ id: number; name: string }>("user:1");
53
+ console.log(user); // { id: 1, name: "Roberto" }
54
+ ```
55
+
56
+ ```js
57
+ // CommonJS
58
+ const { Cache } = require("rust-node-cache");
59
+
60
+ const cache = new Cache();
61
+ cache.set("greeting", "hello");
62
+ console.log(cache.get("greeting")); // "hello"
63
+ ```
64
+
65
+ ---
66
+
67
+ ## Basic Usage
68
+
69
+ ### `set(key, value, options?)`
70
+
71
+ Stores (or overwrites) a value. Returns `true` on success, or `false` when a
72
+ `maxSize` limit is set, the cache is full, and the key is new.
73
+
74
+ ```ts
75
+ cache.set("user:1", { id: 1, name: "Roberto" });
76
+ cache.set("session:123", session, { ttlSeconds: 60 });
77
+ ```
78
+
79
+ ### `get(key)`
80
+
81
+ Returns the stored value, or `null` if the key is missing or expired.
82
+
83
+ ```ts
84
+ const user = cache.get<User>("user:1"); // User | null
85
+ ```
86
+
87
+ ### `delete(key)`
88
+
89
+ Removes a key. Returns `true` if it existed, `false` otherwise.
90
+
91
+ ```ts
92
+ cache.delete("user:1"); // true
93
+ ```
94
+
95
+ ### `exists(key)`
96
+
97
+ Returns `true` if the key exists and is still valid (not expired).
98
+
99
+ ```ts
100
+ cache.exists("user:1"); // boolean
101
+ ```
102
+
103
+ ### `clear()`
104
+
105
+ Removes every entry from the cache.
106
+
107
+ ```ts
108
+ cache.clear();
109
+ ```
110
+
111
+ ### `size()`
112
+
113
+ Returns the number of keys currently stored.
114
+
115
+ ```ts
116
+ cache.size(); // 42
117
+ ```
118
+
119
+ ---
120
+
121
+ ## TTL
122
+
123
+ Set an entry with an expiration in seconds:
124
+
125
+ ```ts
126
+ cache.set("session:123", session, { ttlSeconds: 60 });
127
+ ```
128
+
129
+ After the TTL elapses, reads return `null`:
130
+
131
+ ```ts
132
+ cache.get("session:123"); // null (once expired)
133
+ ```
134
+
135
+ Expiration is **lazy** by default: an expired entry is evicted the moment it is
136
+ accessed. To proactively reclaim memory from keys that are never read again, run
137
+ an active sweep:
138
+
139
+ ```ts
140
+ const removed = cache.cleanupExpired(); // number of entries removed
141
+ ```
142
+
143
+ ---
144
+
145
+ ## Statistics
146
+
147
+ ```ts
148
+ cache.stats();
149
+ ```
150
+
151
+ Returns:
152
+
153
+ ```ts
154
+ {
155
+ hits: 1500,
156
+ misses: 200,
157
+ sets: 800,
158
+ deletes: 20,
159
+ expired: 15,
160
+ size: 780
161
+ }
162
+ ```
163
+
164
+ Counters are cumulative for the lifetime of the cache instance. `clear()` empties
165
+ the storage but does **not** reset the historical counters.
166
+
167
+ ---
168
+
169
+ ## Performance
170
+
171
+ The cache core is implemented entirely in Rust and tuned for throughput:
172
+
173
+ - **Rust core** — no GC pauses on the cache itself; deterministic memory handling
174
+ via RAII (entries are freed the instant they leave the map).
175
+ - **DashMap** — the map is split into multiple internal shards, each guarded by
176
+ its own lock, so operations on different keys proceed in parallel.
177
+ - **Lock-free reads** for statistics — counters use atomic integers
178
+ (`AtomicU64`) and are incremented with relaxed atomics, avoiding a global lock.
179
+ - **Low allocation** — values are stored as a single contiguous `Vec<u8>`; there
180
+ is no per-field boxing.
181
+ - **Fast TTL checks** — expiration is a single integer comparison against a
182
+ cached millisecond timestamp.
183
+
184
+ ---
185
+
186
+ ## Architecture
187
+
188
+ ```txt
189
+ Node.js
190
+ ↓
191
+ napi-rs (the Rust ⇆ V8 bridge: #[napi] generates the glue code)
192
+ ↓
193
+ Rust Cache Engine (set / get / ttl / stats / cleanup)
194
+ ↓
195
+ DashMap (concurrent, lock-sharded HashMap<String, CacheEntry>)
196
+ ```
197
+
198
+ Values cross the boundary as `serde_json::Value` and are serialized to JSON
199
+ bytes (`Vec<u8>`) inside the engine. This keeps the storage layer agnostic to the
200
+ shape of your data and leaves room to adopt binary formats (MessagePack, CBOR,
201
+ Bincode) later without touching the cache logic.
202
+
203
+ ---
204
+
205
+ ## API Reference
206
+
207
+ | Method | Returns | Description |
208
+ | ------------------------------ | -------------- | ----------------------------------------------------------------- |
209
+ | `new Cache(options?)` | `Cache` | Create a cache. `options.maxSize` caps the number of keys. |
210
+ | `set(key, value, options?)` | `boolean` | Store/overwrite a value. `options.ttlSeconds` sets expiration. |
211
+ | `get<T>(key)` | `T \| null` | Read a value (lazy-expires stale entries). |
212
+ | `delete(key)` | `boolean` | Remove a key. `true` if it existed. |
213
+ | `exists(key)` | `boolean` | Whether the key exists and is valid. |
214
+ | `clear()` | `void` | Remove all entries. |
215
+ | `size()` | `number` | Number of keys currently stored. |
216
+ | `cleanupExpired()` | `number` | Sweep expired entries; returns how many were removed. |
217
+ | `stats()` | `CacheStats` | Cumulative counters + current size. |
218
+
219
+ ### Types
220
+
221
+ ```ts
222
+ interface CacheOptions {
223
+ maxSize?: number;
224
+ }
225
+
226
+ interface SetOptions {
227
+ ttlSeconds?: number;
228
+ }
229
+
230
+ interface CacheStats {
231
+ hits: number;
232
+ misses: number;
233
+ sets: number;
234
+ deletes: number;
235
+ expired: number;
236
+ size: number;
237
+ }
238
+ ```
239
+
240
+ ---
241
+
242
+ ## Express Example
243
+
244
+ ```ts
245
+ import express from "express";
246
+ import { Cache } from "rust-node-cache";
247
+ import { cacheMiddleware } from "rust-node-cache/express";
248
+
249
+ const cache = new Cache();
250
+ const app = express();
251
+
252
+ // Automatic response caching (adds an `X-Cache: HIT|MISS` header).
253
+ app.get("/users/:id", cacheMiddleware({ cache, ttlSeconds: 60 }), async (req, res) => {
254
+ const user = await database.findUser(req.params.id);
255
+ res.json(user);
256
+ });
257
+
258
+ // Or use the cache manually.
259
+ app.get("/manual/:id", async (req, res) => {
260
+ const key = `user:${req.params.id}`;
261
+ const cached = cache.get(key);
262
+ if (cached) return res.json(cached);
263
+
264
+ const user = await database.findUser(req.params.id);
265
+ cache.set(key, user, { ttlSeconds: 60 });
266
+ res.json(user);
267
+ });
268
+ ```
269
+
270
+ ---
271
+
272
+ ## Fastify Example
273
+
274
+ ```ts
275
+ import Fastify from "fastify";
276
+ import { Cache } from "rust-node-cache";
277
+ import { cachePlugin } from "rust-node-cache/fastify";
278
+
279
+ const cache = new Cache();
280
+ const fastify = Fastify();
281
+
282
+ fastify.register(cachePlugin, { cache, ttlSeconds: 60 });
283
+
284
+ fastify.get("/users/:id", async (req) => {
285
+ return database.findUser(req.params.id);
286
+ });
287
+ ```
288
+
289
+ ---
290
+
291
+ ## NestJS Example
292
+
293
+ ```ts
294
+ import { Cache } from "rust-node-cache";
295
+ import { CacheInterceptor } from "rust-node-cache/nestjs";
296
+
297
+ const cache = new Cache();
298
+
299
+ // Apply globally...
300
+ app.useGlobalInterceptors(new CacheInterceptor({ cache, ttlSeconds: 60 }));
301
+
302
+ // ...or per controller/route with @UseInterceptors(new CacheInterceptor({ cache })).
303
+ ```
304
+
305
+ ---
306
+
307
+ ## Benchmarks
308
+
309
+ Coming soon.
310
+
311
+ ---
312
+
313
+ ## Limitations
314
+
315
+ - Local process only
316
+ - Not distributed
317
+ - Data is lost on restart
318
+ - Multiple workers (cluster / PM2) have separate, independent caches
319
+
320
+ ---
321
+
322
+ ## Roadmap
323
+
324
+ | Version | Feature |
325
+ | ------- | ------------------------ |
326
+ | v0.1 | Basic cache |
327
+ | v0.2 | TTL cleanup thread |
328
+ | v0.3 | LRU cache |
329
+ | v0.4 | LFU cache |
330
+ | v0.5 | Redis synchronization |
331
+ | v0.6 | Prometheus metrics |
332
+ | v0.7 | ImmutableLog integration |
333
+
334
+ ### Future: ImmutableLog Integration
335
+
336
+ ```ts
337
+ cache.onEvicted((event) => {
338
+ immutablelog.send(event);
339
+ });
340
+ ```
341
+
342
+ ```json
343
+ {
344
+ "event_type": "cache_evicted",
345
+ "key": "user:123",
346
+ "reason": "expired"
347
+ }
348
+ ```
349
+
350
+ ### Future: Decorator API
351
+
352
+ ```ts
353
+ @Cacheable({ ttlSeconds: 60 })
354
+ async function getUser(id: number) {}
355
+ ```
356
+
357
+ ---
358
+
359
+ ## Development
360
+
361
+ ```bash
362
+ npm install
363
+ npm run build # builds the native addon + the TypeScript layer
364
+ npm test
365
+ ```
366
+
367
+ The build runs in two stages:
368
+
369
+ - `npm run build:native` — compiles the Rust crate and emits the platform addon
370
+ plus `binding.js` / `binding.d.ts` (the native loader and its types).
371
+ - `npm run build:js` — bundles the TypeScript layer in `js/` into `dist/` (CJS +
372
+ ESM + type declarations) with [tsup](https://tsup.egoist.dev/).
373
+
374
+ ---
375
+
376
+ ## Publishing
377
+
378
+ ```bash
379
+ npm version patch
380
+ git push --tags
381
+ ```
382
+
383
+ Pushing a `vX.Y.Z` tag triggers CI to build every platform binary, bundle them
384
+ into a single self-contained package, and publish it to npm.
385
+
386
+ ---
387
+
388
+ ## License
389
+
390
+ MIT © [Roberto Lima](https://github.com/robertolima-dev)
package/binding.d.ts ADDED
@@ -0,0 +1,54 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+
4
+ /* auto-generated by NAPI-RS */
5
+
6
+ /** OpçÔes do construtor: `new Cache({ maxSize })`. */
7
+ export interface CacheOptions {
8
+ /** Limite opcional de chaves. Ao atingir, `set` de chave nova retorna `false`. */
9
+ maxSize?: number
10
+ }
11
+ /** OpçÔes por escrita: `cache.set(key, value, { ttlSeconds })`. */
12
+ export interface SetOptions {
13
+ /** Tempo de vida em segundos. Ausente => a entrada nĂŁo expira por tempo. */
14
+ ttlSeconds?: number
15
+ }
16
+ /** Objeto devolvido por `cache.stats()`. Campos `i64` viram `number` no JS. */
17
+ export interface CacheStatsObject {
18
+ hits: number
19
+ misses: number
20
+ sets: number
21
+ deletes: number
22
+ expired: number
23
+ size: number
24
+ }
25
+ /**
26
+ * Cache em memĂłria exposto ao Node como a classe `Cache`.
27
+ *
28
+ * Internamente delega tudo para o `RustCache` (que carrega o `DashMap` e os
29
+ * contadores atÎmicos). Todos os métodos recebem `&self`: como o estado é
30
+ * concorrente por dentro, vĂĄrias chamadas podem rodar em paralelo.
31
+ */
32
+ export declare class Cache {
33
+ /** `new Cache()` ou `new Cache({ maxSize })`. */
34
+ constructor(options?: CacheOptions | undefined | null)
35
+ /**
36
+ * Insere/atualiza uma chave. Retorna `true` em sucesso, `false` se o cache
37
+ * estiver cheio (`maxSize`) e a chave for nova.
38
+ */
39
+ set(key: string, value: any, options?: SetOptions | undefined | null): boolean
40
+ /** LĂȘ uma chave. Retorna o valor ou `null` se ausente/expirada. */
41
+ get(key: string): any | null
42
+ /** Remove uma chave. Retorna `true` se existia, `false` caso contrĂĄrio. */
43
+ delete(key: string): boolean
44
+ /** Indica se a chave existe e estĂĄ vĂĄlida (nĂŁo expirada). */
45
+ exists(key: string): boolean
46
+ /** Esvazia todas as entradas do cache. */
47
+ clear(): void
48
+ /** Quantidade de chaves armazenadas no momento. */
49
+ size(): number
50
+ /** Varre e remove todas as entradas expiradas. Retorna quantas removeu. */
51
+ cleanupExpired(): number
52
+ /** EstatĂ­sticas acumuladas + tamanho atual. */
53
+ stats(): CacheStatsObject
54
+ }