offheap 0.1.0 → 0.2.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.
Files changed (30) hide show
  1. package/.github/workflows/CI.yml +22 -1
  2. package/Cargo.toml +2 -2
  3. package/artifacts/bindings-aarch64-apple-darwin/index.d.ts +37 -25
  4. package/artifacts/bindings-aarch64-apple-darwin/index.js +165 -295
  5. package/artifacts/bindings-aarch64-unknown-linux-musl/index.d.ts +42 -0
  6. package/artifacts/bindings-aarch64-unknown-linux-musl/index.js +186 -0
  7. package/artifacts/bindings-x86_64-apple-darwin/index.d.ts +37 -25
  8. package/artifacts/bindings-x86_64-apple-darwin/index.js +165 -295
  9. package/artifacts/bindings-x86_64-pc-windows-msvc/index.d.ts +42 -30
  10. package/artifacts/bindings-x86_64-pc-windows-msvc/index.js +186 -316
  11. package/artifacts/bindings-x86_64-unknown-linux-gnu/index.d.ts +37 -25
  12. package/artifacts/bindings-x86_64-unknown-linux-gnu/index.js +165 -295
  13. package/artifacts/bindings-x86_64-unknown-linux-musl/index.d.ts +42 -0
  14. package/artifacts/bindings-x86_64-unknown-linux-musl/index.js +186 -0
  15. package/benchmarks/crossover_bench.js +139 -0
  16. package/docs/.vitepress/config.js +35 -0
  17. package/docs/guide/api.md +216 -0
  18. package/docs/guide/architecture.md +58 -0
  19. package/docs/guide/benchmarks.md +48 -0
  20. package/docs/guide/getting-started.md +74 -0
  21. package/docs/index.md +23 -0
  22. package/index.d.ts +37 -25
  23. package/index.js +165 -295
  24. package/package.json +21 -12
  25. package/src/algorithms/arc.rs +123 -25
  26. package/src/algorithms/lru.rs +94 -12
  27. package/src/algorithms/mod.rs +4 -0
  28. package/src/algorithms/tinylfu.rs +98 -35
  29. package/src/cache.rs +289 -94
  30. package/tests/index.test.js +134 -7
@@ -0,0 +1,139 @@
1
+ const { LRUCache } = require('lru-cache');
2
+ const { CacheManager } = require('../index.js');
3
+
4
+ function formatMemory(bytes) {
5
+ return (bytes / 1024 / 1024).toFixed(2) + ' MB';
6
+ }
7
+
8
+ function runMeasurement(payloadSize, entryCount) {
9
+ console.log(`\n------------------------------------------------------------`);
10
+ console.log(`Payload Size: ${payloadSize} bytes | Total Entries: ${entryCount}`);
11
+ console.log(`------------------------------------------------------------`);
12
+
13
+ const keys = Array.from({ length: entryCount }, (_, i) => `key-${i}`);
14
+ const val = 'a'.repeat(payloadSize);
15
+
16
+ // 1. Measure Pure Map
17
+ global.gc && global.gc();
18
+ const memBeforeMap = process.memoryUsage().heapUsed;
19
+ const map = new Map();
20
+ const startMapSet = Date.now();
21
+ for (let i = 0; i < entryCount; i++) {
22
+ map.set(keys[i], val);
23
+ }
24
+ const mapSetTime = Date.now() - startMapSet;
25
+
26
+ const startMapGet = Date.now();
27
+ for (let i = 0; i < entryCount; i++) {
28
+ const res = map.get(keys[i]);
29
+ }
30
+ const mapGetTime = Date.now() - startMapGet;
31
+ const memAfterMap = process.memoryUsage().heapUsed;
32
+ const mapMem = Math.max(0, memAfterMap - memBeforeMap);
33
+
34
+ map.clear();
35
+ global.gc && global.gc();
36
+
37
+ // 2. Measure lru-cache (JS)
38
+ global.gc && global.gc();
39
+ const memBeforeLru = process.memoryUsage().heapUsed;
40
+ const jsLru = new LRUCache({ max: entryCount });
41
+ const startLruSet = Date.now();
42
+ for (let i = 0; i < entryCount; i++) {
43
+ jsLru.set(keys[i], val);
44
+ }
45
+ const lruSetTime = Date.now() - startLruSet;
46
+
47
+ const startLruGet = Date.now();
48
+ for (let i = 0; i < entryCount; i++) {
49
+ const res = jsLru.get(keys[i]);
50
+ }
51
+ const lruGetTime = Date.now() - startLruGet;
52
+ const memAfterLru = process.memoryUsage().heapUsed;
53
+ const lruMem = Math.max(0, memAfterLru - memBeforeLru);
54
+
55
+ jsLru.clear();
56
+ global.gc && global.gc();
57
+
58
+ // 3. Measure OffHeap
59
+ global.gc && global.gc();
60
+ const memBeforeOffheap = process.memoryUsage().heapUsed;
61
+ const manager = new CacheManager();
62
+ const offheap = manager.createCache('crossover', { policy: 'lru', capacity: entryCount, shards: 8 });
63
+ const startOffheapSet = Date.now();
64
+ for (let i = 0; i < entryCount; i++) {
65
+ offheap.set(keys[i], val);
66
+ }
67
+ const offheapSetTime = Date.now() - startOffheapSet;
68
+
69
+ const startOffheapGet = Date.now();
70
+ for (let i = 0; i < entryCount; i++) {
71
+ const res = offheap.get(keys[i]);
72
+ }
73
+ const offheapGetTime = Date.now() - startOffheapGet;
74
+ const memAfterOffheap = process.memoryUsage().heapUsed;
75
+ const offheapMem = Math.max(0, memAfterOffheap - memBeforeOffheap);
76
+
77
+ offheap.dispose();
78
+ manager.dispose();
79
+ global.gc && global.gc();
80
+
81
+ console.log(`Pure Map | Set: ${mapSetTime}ms | Get: ${mapGetTime}ms | Heap: ${formatMemory(mapMem)}`);
82
+ console.log(`lru-cache | Set: ${lruSetTime}ms | Get: ${lruGetTime}ms | Heap: ${formatMemory(lruMem)}`);
83
+ console.log(`OffHeap | Set: ${offheapSetTime}ms | Get: ${offheapGetTime}ms | Heap: ${formatMemory(offheapMem)}`);
84
+
85
+ return {
86
+ payloadSize,
87
+ entryCount,
88
+ map: { set: mapSetTime, get: mapGetTime, mem: mapMem },
89
+ lru: { set: lruSetTime, get: lruGetTime, mem: lruMem },
90
+ offheap: { set: offheapSetTime, get: offheapGetTime, mem: offheapMem },
91
+ };
92
+ }
93
+
94
+ async function run() {
95
+ console.log('Running OffHeap vs Map vs lru-cache Crossover Benchmark...');
96
+ console.log('(Note: Run with node --expose-gc to get accurate heap measurements)');
97
+
98
+ const results = [];
99
+
100
+ // Test 1: Small payload, medium count (low V8 GC pressure)
101
+ results.push(runMeasurement(100, 30000));
102
+
103
+ // Test 2: Medium payload, medium count
104
+ results.push(runMeasurement(2048, 30000));
105
+
106
+ // Test 3: Large payload, large count (high V8 GC pressure)
107
+ results.push(runMeasurement(10240, 50000));
108
+
109
+ console.log('\n\n========================= CROSSOVER SUMMARY =========================');
110
+ console.log('| Payload | Entries | Engine | Set Time | Get Time | V8 Heap Used |');
111
+ console.log('|---------|---------|----------|----------|----------|--------------|');
112
+ for (const r of results) {
113
+ const fmtSize = r.payloadSize >= 1024 ? `${r.payloadSize / 1024} KB` : `${r.payloadSize} B`;
114
+ console.log(`| ${fmtSize.padEnd(7)} | ${String(r.entryCount).padEnd(7)} | Pure Map | ${String(r.map.set).padEnd(6)}ms | ${String(r.map.get).padEnd(6)}ms | ${formatMemory(r.map.mem).padEnd(12)} |`);
115
+ console.log(`| | | lru-ca.. | ${String(r.lru.set).padEnd(6)}ms | ${String(r.lru.get).padEnd(6)}ms | ${formatMemory(r.lru.mem).padEnd(12)} |`);
116
+ console.log(`| | | OffHeap | ${String(r.offheap.set).padEnd(6)}ms | ${String(r.offheap.get).padEnd(6)}ms | ${formatMemory(r.offheap.mem).padEnd(12)} |`);
117
+ console.log('|---------|---------|----------|----------|----------|--------------|');
118
+ }
119
+
120
+ console.log('\n========================= V8 HEAP COMPARISON GRAPH =========================');
121
+ console.log('Memory usage at 50k entries of 10 KB:');
122
+ const r3 = results[2];
123
+ const maxMem = Math.max(r3.map.mem, r3.lru.mem, r3.offheap.mem);
124
+ const getBar = (bytes) => {
125
+ if (maxMem === 0) return ' (0.00 MB)';
126
+ const pct = bytes / maxMem;
127
+ const len = Math.round(pct * 40);
128
+ return '█'.repeat(len) + ` (${formatMemory(bytes)})`;
129
+ };
130
+ console.log(`Pure Map : ${getBar(r3.map.mem)}`);
131
+ console.log(`lru-cache : ${getBar(r3.lru.mem)}`);
132
+ console.log(`OffHeap : ${getBar(r3.offheap.mem)}`);
133
+ console.log('\nCONCLUSION:');
134
+ console.log('OffHeap keeps the V8 heap usage at virtually 0 MB regardless of cache size and payload size!');
135
+ console.log('For simple small-map reads, Map/lru-cache are faster in microbenchmarks due to zero-copy direct V8 references.');
136
+ console.log('However, once cache size exceeds ~50MB or entries exceed ~50,000, OffHeap prevents high GC pauses and memory bloating.');
137
+ }
138
+
139
+ run().catch(console.error);
@@ -0,0 +1,35 @@
1
+ export default {
2
+ title: 'OffHeap',
3
+ description: 'High-performance off-heap caching framework for Node.js',
4
+ themeConfig: {
5
+ logo: '/logo.png',
6
+ nav: [
7
+ { text: 'Guide', link: '/guide/getting-started' },
8
+ { text: 'API Reference', link: '/guide/api' },
9
+ { text: 'Benchmarks', link: '/guide/benchmarks' }
10
+ ],
11
+ sidebar: [
12
+ {
13
+ text: 'Getting Started',
14
+ items: [
15
+ { text: 'Introduction', link: '/guide/getting-started' },
16
+ { text: 'Architecture & Design', link: '/guide/architecture' }
17
+ ]
18
+ },
19
+ {
20
+ text: 'Reference',
21
+ items: [
22
+ { text: 'API Reference', link: '/guide/api' },
23
+ { text: 'Benchmarks & Telemetry', link: '/guide/benchmarks' }
24
+ ]
25
+ }
26
+ ],
27
+ socialLinks: [
28
+ { icon: 'github', link: 'https://github.com/ryangustav/OffHeap' }
29
+ ],
30
+ footer: {
31
+ message: 'Released under the MIT and Apache 2.0 Licenses.',
32
+ copyright: 'Copyright © 2026 Ryan Gustavo & OffHeap Contributors'
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,216 @@
1
+ # API Reference
2
+
3
+ All methods exported by the native addon execute **synchronously** to avoid microtask queue scheduling overhead in Node.js, ensuring maximum throughput. Custom high-level utilities like `getOrSet` seamlessly support both synchronous and asynchronous operations.
4
+
5
+ ---
6
+
7
+ ## `CacheManager`
8
+
9
+ The central manager responsible for provisioning and isolating individual caches.
10
+
11
+ ```javascript
12
+ const { CacheManager } = require('offheap');
13
+ const manager = new CacheManager();
14
+ ```
15
+
16
+ ### `new CacheManager()`
17
+ Instantiates a new central cache manager.
18
+ * **Returns**: `CacheManager` instance.
19
+
20
+ ### `createCache(name, config)`
21
+ Creates and returns an isolated cache instance.
22
+ ```javascript
23
+ const cache = manager.createCache('products', {
24
+ policy: 'tinylfu',
25
+ capacity: 10000,
26
+ shards: 16,
27
+ maxBytes: 100 * 1024 * 1024 // 100 MB byte-capacity limit
28
+ });
29
+ ```
30
+ * **Parameters**:
31
+ * `name` (`string`): Unique name/namespace for the cache.
32
+ * `config` (`CacheConfig`):
33
+ * `policy` (`"lru" | "arc" | "tinylfu"`): The eviction policy.
34
+ * `capacity` (`number`): The maximum number of entries allowed in the cache.
35
+ * `shards` (`number`, *optional*): Number of internal locks shards. High concurrency workloads benefit from larger shard numbers (e.g. 16 or 32). Default: `8`.
36
+ * `maxBytes` (`number`, *optional*): The maximum memory size of keys and values combined in bytes. When this threshold is crossed, entries are evicted according to the active policy.
37
+ * **Returns**: `Cache` instance.
38
+ * **Throws**: Error if a cache with the specified `name` already exists.
39
+
40
+ ### `getCache(name)`
41
+ Retrieves an existing cache by name.
42
+ * **Parameters**:
43
+ * `name` (`string`): The cache namespace.
44
+ * **Returns**: `Cache | null` (returns `null` if the cache does not exist).
45
+
46
+ ### `deleteCache(name)`
47
+ Deletes a cache instance from the manager.
48
+ * **Parameters**:
49
+ * `name` (`string`): The cache namespace to delete.
50
+ * **Returns**: `boolean` (true if the cache was successfully deleted).
51
+
52
+ ### `clear()`
53
+ Deletes all cache instances managed by this instance.
54
+ * **Returns**: `void`
55
+
56
+ ### `dispose()`
57
+ Releases all cache instances immediately, releasing all underlying native memory.
58
+ * **Returns**: `void`
59
+
60
+ ---
61
+
62
+ ## `Cache`
63
+
64
+ An isolated, thread-safe cache instance.
65
+
66
+ ### `get(key)`
67
+ Retrieves a value from the cache.
68
+ ```javascript
69
+ const value = cache.get('prod_101');
70
+ ```
71
+ * **Parameters**:
72
+ * `key` (`string`): The lookup key.
73
+ * **Returns**: `Buffer | string | object | number | boolean | undefined`
74
+ * Returns `Buffer` if the value was stored as a `Buffer` or `Uint8Array`.
75
+ * Returns `string` if the value was stored as a string.
76
+ * Returns `object | array | number | boolean` if the value was stored as a JSON-serializable type.
77
+ * Returns `undefined` if the key is missing or expired.
78
+
79
+ ### `set(key, value, ttl_ms?)`
80
+ Stores an entry in the cache. If the key already exists, its value is overwritten.
81
+ ```javascript
82
+ cache.set('key', { data: 'test' }, 60000); // Stores object with 60s TTL
83
+ ```
84
+ * **Parameters**:
85
+ * `key` (`string`): The entry key.
86
+ * `value` (`Buffer | Uint8Array | string | any`): The payload to store.
87
+ * `ttl_ms` (`number`, *optional*): The time-to-live in milliseconds. If omitted, the entry has no expiry.
88
+ * **Returns**: `Buffer | string | object | undefined` (returns the old value if it was overwritten, or `undefined`).
89
+
90
+ ### `has(key)`
91
+ Checks if a key exists in the cache and is not expired, without deserializing the value.
92
+ ```javascript
93
+ if (cache.has('auth_session')) { ... }
94
+ ```
95
+ * **Parameters**:
96
+ * `key` (`string`): Key to check.
97
+ * **Returns**: `boolean` (true if key exists and is valid).
98
+
99
+ ### `peek(key)`
100
+ Retrieves a value without updating the eviction metadata (e.g., LRU order or frequency sketch count). Useful for logging, debugging, or health checks.
101
+ ```javascript
102
+ const debugVal = cache.peek('hot_key');
103
+ ```
104
+ * **Parameters**:
105
+ * `key` (`string`): Key to retrieve.
106
+ * **Returns**: `Buffer | string | object | undefined`
107
+
108
+ ### `touch(key, ttl_ms)`
109
+ Renews or changes the Time-To-Live (TTL) of a key without re-writing the cached value.
110
+ ```javascript
111
+ cache.touch('session_12', 30 * 60 * 1000); // Extend session by 30 min
112
+ ```
113
+ * **Parameters**:
114
+ * `key` (`string`): Key to renew.
115
+ * `ttl_ms` (`number`, *optional*): New time-to-live in milliseconds. Use `undefined` to clear expiry.
116
+ * **Returns**: `boolean` (true if the key existed and TTL was updated).
117
+
118
+ ### `increment(key, delta?, ttl_ms?)`
119
+ Atomically increments a numeric counter key in memory (Tag 4). Ideal for rate limiters.
120
+ ```javascript
121
+ const requestCount = cache.increment('rate_limit:ip_127.0.0.1', 1, 60000);
122
+ ```
123
+ * **Parameters**:
124
+ * `key` (`string`): Key of the counter.
125
+ * `delta` (`number`, *optional*): Value to increment by. Default: `1`.
126
+ * `ttl_ms` (`number`, *optional*): Time-to-live for the counter if it's created.
127
+ * **Returns**: `number` (the newly incremented counter value).
128
+
129
+ ### `decrement(key, delta?, ttl_ms?)`
130
+ Atomically decrements a numeric counter key in memory (Tag 4).
131
+ ```javascript
132
+ const remainingTokens = cache.decrement('api_tokens:user_88', 1);
133
+ ```
134
+ * **Parameters**:
135
+ * `key` (`string`): Key of the counter.
136
+ * `delta` (`number`, *optional*): Value to decrement by. Default: `1`.
137
+ * `ttl_ms` (`number`, *optional*): Time-to-live.
138
+ * **Returns**: `number` (the newly decremented counter value).
139
+
140
+ ### `mget(keys)`
141
+ Performs a batch lookup for multiple keys in a single FFI boundary crossing, significantly improving throughput for multi-key lookups.
142
+ ```javascript
143
+ const items = cache.mget(['k1', 'k2', 'k3']); // Returns { k1: val1, k2: val2 }
144
+ ```
145
+ * **Parameters**:
146
+ * `keys` (`string[]`): Array of keys to retrieve.
147
+ * **Returns**: `Record<string, any>` (Object mapping found keys to their deserialized values).
148
+
149
+ ### `mset(entries, ttl_ms?)`
150
+ Performs a batch write of multiple key-value entries in a single FFI crossing.
151
+ ```javascript
152
+ cache.mset({ a: 1, b: 'hello', c: Buffer.from([1, 2]) }, 60000);
153
+ ```
154
+ * **Parameters**:
155
+ * `entries` (`Record<string, any>`): Object representing key-value entries to store.
156
+ * `ttl_ms` (`number`, *optional*): Time-to-live in milliseconds for all written entries.
157
+
158
+ ### `mdelete(keys)`
159
+ Performs a batch delete of multiple keys in a single FFI crossing.
160
+ ```javascript
161
+ const deletedCount = cache.mdelete(['a', 'b', 'c']);
162
+ ```
163
+ * **Parameters**:
164
+ * `keys` (`string[]`): Array of keys to delete.
165
+ * **Returns**: `number` (number of deleted keys).
166
+
167
+ ### `getOrSet(key, factory, ttl_ms?)`
168
+ Implements a coalesced compute-on-miss cache access pattern. If two concurrent requests lookup the same missing key, they will await the **same** factory promise, preventing cache stampedes.
169
+ ```javascript
170
+ const product = await cache.getOrSet('prod_101', async () => {
171
+ return await db.fetchProduct(101);
172
+ }, 60000);
173
+ ```
174
+ * **Parameters**:
175
+ * `key` (`string`): Key.
176
+ * `factory` (`() => any | Promise<any>`): A callback that computes the value if missing. Can return a Promise or a synchronous value.
177
+ * `ttl_ms` (`number`, *optional*): TTL in milliseconds for the computed value.
178
+ * **Returns**: `any` (the cached value, or the resolved promise result).
179
+
180
+ ### `delete(key)`
181
+ Deletes a specific key from the cache.
182
+ * **Parameters**:
183
+ * `key` (`string`): The key to remove.
184
+ * **Returns**: `boolean` (true if the key existed and was deleted).
185
+
186
+ ### `clear()`
187
+ Removes all keys and resets stats for this cache instance.
188
+ * **Returns**: `void`
189
+
190
+ ### `keys()`
191
+ Returns an array of all active (non-expired) keys in the cache.
192
+ * **Returns**: `string[]`
193
+
194
+ ### `stats()`
195
+ Returns telemetry statistics for the cache.
196
+ ```javascript
197
+ const telemetry = cache.stats();
198
+ // Telemetry format:
199
+ // {
200
+ // hits: 1452,
201
+ // misses: 92,
202
+ // capacity: 10000,
203
+ // size: 4210,
204
+ // bytesUsed: 541029 // total size of keys + values in bytes
205
+ // }
206
+ ```
207
+ * **Returns**: `CacheStats` object:
208
+ * `hits` (`number`): Number of successful read queries.
209
+ * `misses` (`number`): Number of queries for missing or expired keys.
210
+ * `capacity` (`number`): Sized capacity.
211
+ * `size` (`number`): Current count of active entries.
212
+ * `bytesUsed` (`number`): Current byte-capacity usage of stored keys and values.
213
+
214
+ ### `dispose()`
215
+ Explicitly disposes of the native sub-caches immediately, freeing all of its memory back to the OS.
216
+ * **Returns**: `void`
@@ -0,0 +1,58 @@
1
+ # Architecture & Design
2
+
3
+ ```mermaid
4
+ graph TD
5
+ JS[Node.js Application] <-->|NAPI-RS FFI| Bridge[Native Addon Bridge]
6
+ Bridge <--> Manager[CacheManager]
7
+ Manager -->|Instantiates| Cache1[Cache: 'Sessions']
8
+ Manager -->|Instantiates| Cache2[Cache: 'Products']
9
+
10
+ subgraph Rust Memory Space (Off-Heap)
11
+ Cache2 --> Lock[Mutex Lock]
12
+ Lock --> Core[Cache Core Engine]
13
+ Core --> LRU[LRU Eviction]
14
+ Core --> ARC[ARC Eviction]
15
+ Core --> TinyLFU[W-TinyLFU Eviction]
16
+
17
+ TinyLFU -.-> Sketch[4-bit Count-Min Sketch]
18
+ end
19
+ ```
20
+
21
+ ## V8 Heap vs. Off-Heap Caching
22
+
23
+ In high-throughput Node.js applications, caching millions of elements in standard JavaScript memory (on-heap) introduces major performance bottlenecks.
24
+
25
+ ### The Problem: V8 Garbage Collection Pauses
26
+ The V8 engine tracks every JavaScript object, array, and string to manage memory. As the cache grows to hundreds of thousands or millions of active entries, the Garbage Collector must traverse an increasingly complex object graph during its mark-and-sweep phases.
27
+
28
+ This causes two major issues:
29
+ 1. **Stop-the-World Pauses**: Under heavy memory pressure, V8 triggers major GC collections that pause the main JavaScript execution thread, raising P99 latencies from milliseconds to several seconds.
30
+ 2. **Heap Memory Limits**: Node.js defaults to a maximum heap size (often 1.4 GB or 4 GB depending on system configuration). Exceeding this limit causes `FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory`.
31
+
32
+ ### The Solution: Off-Heap Memory
33
+ OffHeap completely bypasses the JavaScript heap by allocating memory directly in the **Rust memory space** (off-heap).
34
+
35
+ - When you write an entry using `set()`, OffHeap serializes the JavaScript value and transfers the raw bytes across the Foreign Function Interface (FFI) boundary.
36
+ - The raw bytes are stored in standard Rust heap allocations (`Vec<u8>`).
37
+ - To the V8 Garbage Collector, the entire cache exists as a single native pointer. V8 does not traverse, inspect, or manage this memory, reducing major GC sweep times to virtually zero.
38
+
39
+ ---
40
+
41
+ ## Caching Policies
42
+
43
+ ### Least Recently Used (LRU)
44
+ The baseline policy. It evicts the least recently accessed item when the cache capacity is reached. LRU is implemented using a hash map for $O(1)$ lookups and an index-based doubly linked list to track access order.
45
+
46
+ ### Adaptive Replacement Cache (ARC)
47
+ ARC dynamically self-tunes between recency and frequency based on the active workload:
48
+ * It maintains two double-linked lists: $L_1$ for recency (items seen once) and $L_2$ for frequency (items seen multiple times).
49
+ * These lists are split into top sections (actual data stored in the cache) and bottom sections (ghost lists that only store keys of evicted items).
50
+ * If a cache hit occurs in the recency ghost list ($B_1$), the cache adjusts its target allocation to favor recency. If a hit occurs in the frequency ghost list ($B_2$), it tunes the allocation to favor frequency.
51
+
52
+ ### Window TinyLFU (W-TinyLFU)
53
+ While LRU is the industry default, it is highly susceptible to **sparse access bursts**—occasional database sweeps or batch operations can completely flush out highly valuable, frequently accessed data. W-TinyLFU resolves this:
54
+
55
+ 1. **Window LRU**: A small percentage of the total capacity (1%) is allocated to a Window LRU. All new writes enter this window first to capture short-term recency bursts.
56
+ 2. **Segmented LRU (SLRU) Main Cache**: The remaining 99% of the capacity is split into a **Probationary Segment** (20%) and a **Protected Segment** (80%). When an item in Probation is hit, it is promoted to the Protected segment.
57
+ 3. **TinyLFU Admission Filter**: When items overflow from the Window LRU, they compete to enter the Probationary segment. If the Probationary segment is full, the incoming item (candidate) is compared against the least recently used item in Probation (victim).
58
+ 4. **4-bit Count-Min Sketch**: OffHeap uses a memory-efficient Count-Min Sketch (with an aging decay mechanism) to track key frequencies. An item is only admitted to the main cache if its access frequency is strictly higher than the victim's. If not, the candidate is evicted immediately.
@@ -0,0 +1,48 @@
1
+ # Benchmarks & Telemetry
2
+
3
+ Below are the performance metrics comparing the throughput of the LRU, ARC, and W-TinyLFU cache engines.
4
+
5
+ ---
6
+
7
+ ## Performance Results
8
+
9
+ ### 1. Write Throughput (`SET` Operations)
10
+ *10,000 capacity, 20,000 unique keys (forcing evictions)*
11
+
12
+ | Engine | Operations/sec | Avg Latency | Margin | Samples |
13
+ | :--- | :--- | :--- | :--- | :--- |
14
+ | **OffHeap LRU** | **486,701 ops/s** | 2.05 μs | ±0.26% | 486,702 |
15
+ | **OffHeap ARC** | **485,246 ops/s** | 2.06 μs | ±0.24% | 485,247 |
16
+ | **OffHeap W-TinyLFU** | **375,509 ops/s** | 2.66 μs | ±0.30% | 375,510 |
17
+
18
+ ### 2. Read Throughput (`GET` Operations)
19
+ *100% Cache Hits*
20
+
21
+ | Engine | Operations/sec | Avg Latency | Margin | Samples |
22
+ | :--- | :--- | :--- | :--- | :--- |
23
+ | **OffHeap LRU** | **774,848 ops/s** | 1.29 μs | ±0.84% | 774,849 |
24
+ | **OffHeap ARC** | **784,789 ops/s** | 1.27 μs | ±0.13% | 784,790 |
25
+ | **OffHeap W-TinyLFU** | **727,002 ops/s** | 1.37 μs | ±0.14% | 727,003 |
26
+
27
+ ---
28
+
29
+ ## Testing Methodology
30
+
31
+ All benchmarks were run locally and are fully reproducible using the scripts in our repository.
32
+
33
+ * **Machine Specifications**: Intel(R) Core(TM) i9 CPU (2.4 GHz, 8 Cores), 16 GB RAM.
34
+ * **OS Environment**: Windows 11 (build-optimized native binary).
35
+ * **Node.js Version**: v24.16.0
36
+ * **Payload Size**: Storing flat JSON objects containing a 100-character payload string (`{ data: "..." }`).
37
+ * **Test Runner**: Benchmarks compiled with release optimizations (`cargo build --release` / `napi build --release`) and executed using [tinybench](https://www.npmjs.com/package/tinybench) over a 1000ms duration per task.
38
+
39
+ ---
40
+
41
+ ## ⚠️ Important Architectural Warning
42
+
43
+ > [!WARNING]
44
+ > **FFI Boundary Overhead**
45
+ > The communication between Node.js (V8) and Rust occurs via a Native Foreign Function Interface (FFI) boundary. Crossing this boundary, casting types, and copying bytes incurs a micro overhead of approximately **1.2 to 2.5 microseconds** per operation.
46
+ >
47
+ > * **When NOT to use OffHeap**: If you are caching small amounts of data (under a few megabytes) consisting of tiny objects, a pure JavaScript Map or simple JS LRU cache will be faster, as it runs entirely in the V8 heap without FFI crossings.
48
+ > * **When to use OffHeap**: If your cache holds **gigabytes of data** or **millions of active keys** where Garbage Collection pauses dominate your application latency, OffHeap is highly superior. The micro FFI overhead is a tiny fraction of the latency saved by preventing major Stop-the-World GC sweeps.
@@ -0,0 +1,74 @@
1
+ # Getting Started
2
+
3
+ ```javascript
4
+ const { CacheManager } = require('offheap');
5
+
6
+ // 1. Initialize the Central Manager
7
+ const manager = new CacheManager();
8
+
9
+ // 2. Instantiate isolated caches with specific eviction policies
10
+ const productCache = manager.createCache('products', {
11
+ policy: 'tinylfu', // Options: 'lru', 'arc', 'tinylfu'
12
+ capacity: 100000 // Sized for up to 100,000 items
13
+ });
14
+
15
+ const sessionCache = manager.createCache('sessions', {
16
+ policy: 'lru',
17
+ capacity: 20000
18
+ });
19
+
20
+ // 3. Store and retrieve values (Preserves Buffers, Strings, and JSON Objects)
21
+ productCache.set('prod_1', { id: 1, name: 'Premium Widget', tags: ['fast'] });
22
+ const product = productCache.get('prod_1'); // Returns the parsed JS object
23
+
24
+ // 4. Set values with TTL (lazily expired)
25
+ sessionCache.set('session_token', 'user_123', 60000); // 60 seconds (60000 ms) TTL
26
+ ```
27
+
28
+ ## Automatic Serialization & Type Preservation
29
+
30
+ OffHeap dynamically adapts its serialization path at the FFI boundary to maximize performance and save memory. You do not need to manually stringify objects or convert buffers to strings:
31
+
32
+ ```javascript
33
+ // 1. Binary Data (zero-copy buffer allocation)
34
+ cache.set('binary_data', fs.readFileSync('image.png'));
35
+ const buffer = cache.get('binary_data'); // Returns a Node.js Buffer
36
+
37
+ // 2. Plain Text / Strings
38
+ cache.set('text_data', 'Hello World!');
39
+ const message = cache.get('text_data'); // Returns a standard JS string
40
+
41
+ // 3. Structured JSON (objects, arrays, numbers, booleans)
42
+ cache.set('json_data', { username: 'ryangustavo', role: 'admin' });
43
+ const profile = cache.get('json_data'); // Returns the deserialized JS Object
44
+
45
+ // 4. Atomic Counters (high-efficiency integer values)
46
+ cache.set('page_views', 100);
47
+ cache.increment('page_views', 1); // Returns 101 without JSON overhead
48
+ ```
49
+
50
+ ## Introduction
51
+
52
+ OffHeap is a high-performance, in-process, off-heap caching framework for Node.js written from scratch in **Rust** using **NAPI-RS**. By moving cached values out of the V8 JavaScript engine heap and into Rust memory space, OffHeap prevents V8 Garbage Collection (GC) sweeps from scanning the cache graphs. This layout eliminates Stop-the-World GC latency spikes under high retention workloads.
53
+
54
+ Unlike traditional Node.js cache engines that store values as JavaScript objects, OffHeap serializes values and stores them as raw bytes in Rust memory. When data is requested, it is transferred across the Foreign Function Interface (FFI) boundary and deserialized back into its original type. The framework supports raw binary Buffers, text Strings, and complex JSON Objects, choosing the most efficient serialization path based on the type.
55
+
56
+ OffHeap is built using an architecture where you instantiate isolated, named `Cache` instances from a single central `CacheManager`. This design lets you run multiple caches under different eviction policies—including classic Least Recently Used (LRU), Adaptive Replacement Cache (ARC), and Window TinyLFU (W-TinyLFU)—providing optimal hit ratios tailored to each microservice's access pattern.
57
+
58
+ ## Installation
59
+
60
+ Install the package using your preferred Node.js package manager:
61
+
62
+ ```bash
63
+ # npm
64
+ npm install offheap
65
+
66
+ # yarn
67
+ yarn add offheap
68
+
69
+ # pnpm
70
+ pnpm add offheap
71
+ ```
72
+
73
+ > [!NOTE]
74
+ > The package automatically downloads pre-built native binaries compiled for your target operating system and architecture (Windows, macOS, and Linux) during installation. No local C++ compiler or Rust toolchain is required for end-users.
package/docs/index.md ADDED
@@ -0,0 +1,23 @@
1
+ ---
2
+ layout: home
3
+
4
+ hero:
5
+ name: OffHeap
6
+ text: High-Performance Off-Heap Caching
7
+ tagline: Bypassing the V8 Heap using native Rust and NAPI-RS. Zero Garbage Collection overhead at scale.
8
+ actions:
9
+ - theme: brand
10
+ text: Get Started
11
+ link: /guide/getting-started
12
+ - theme: alt
13
+ text: View API Reference
14
+ link: /guide/api
15
+
16
+ features:
17
+ - title: Zero GC Pause
18
+ details: Storing serialized data in Rust memory keeps the V8 heap clean and prevents Stop-the-World garbage collection sweeps.
19
+ - title: W-TinyLFU & ARC
20
+ details: Implementations of Adaptive Replacement Cache and Window TinyLFU from scratch to maximize cache hit rates under skewed workloads.
21
+ - title: Multiple Isolated Caches
22
+ details: Provision isolated cache instances from a central manager, each with its own capacity and eviction strategies.
23
+ ---
package/index.d.ts CHANGED
@@ -1,30 +1,42 @@
1
- /* tslint:disable */
2
- /* eslint-disable */
3
-
4
- /* auto-generated by NAPI-RS */
5
-
6
1
  export interface CacheConfig {
7
- policy: string
8
- capacity: number
2
+ policy: 'lru' | 'arc' | 'tinylfu';
3
+ capacity: number;
4
+ shards?: number;
5
+ maxBytes?: number;
9
6
  }
10
- export interface CacheStatsJs {
11
- hits: number
12
- misses: number
13
- capacity: number
14
- size: number
7
+
8
+ export interface CacheStats {
9
+ hits: number;
10
+ misses: number;
11
+ capacity: number;
12
+ size: number;
13
+ bytesUsed: number;
15
14
  }
16
- export declare class Cache {
17
- get(key: string): unknown
18
- set(key: string, value: unknown, ttlMs?: number | undefined | null): unknown
19
- delete(key: string): boolean
20
- clear(): void
21
- stats(): CacheStatsJs
22
- keys(): Array<string>
15
+
16
+ export class Cache {
17
+ get<T = any>(key: string): T | undefined;
18
+ peek<T = any>(key: string): T | undefined;
19
+ has(key: string): boolean;
20
+ set<T = any>(key: string, value: T, ttlMs?: number): T | undefined;
21
+ touch(key: string, ttlMs?: number): boolean;
22
+ delete(key: string): boolean;
23
+ clear(): void;
24
+ stats(): CacheStats;
25
+ keys(): string[];
26
+ increment(key: string, delta?: number, ttlMs?: number): number;
27
+ decrement(key: string, delta?: number, ttlMs?: number): number;
28
+ mget(keys: string[]): Record<string, any>;
29
+ mset(entries: Record<string, any>, ttlMs?: number): void;
30
+ mdelete(keys: string[]): number;
31
+ dispose(): void;
32
+ getOrSet<T = any>(key: string, factory: () => T | Promise<T>, ttlMs?: number): T | Promise<T>;
23
33
  }
24
- export declare class CacheManager {
25
- constructor()
26
- createCache(name: string, config: CacheConfig): Cache
27
- getCache(name: string): Cache | null
28
- deleteCache(name: string): boolean
29
- clear(): void
34
+
35
+ export class CacheManager {
36
+ constructor();
37
+ createCache(name: string, config: CacheConfig): Cache;
38
+ getCache(name: string): Cache | null;
39
+ deleteCache(name: string): boolean;
40
+ clear(): void;
41
+ dispose(): void;
30
42
  }