@nifrajs/cache 3.1.0 → 3.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 +3 -2
- package/dist/memory-cache.d.ts +4 -1
- package/dist/memory-cache.d.ts.map +1 -1
- package/dist/memory-cache.js +26 -1
- package/dist/memory-cache.js.map +1 -1
- package/package.json +1 -1
- package/src/memory-cache.ts +27 -2
package/README.md
CHANGED
|
@@ -36,8 +36,9 @@ await cache.invalidateTag(`user:${id}`)
|
|
|
36
36
|
|
|
37
37
|
## Stores
|
|
38
38
|
|
|
39
|
-
The default `MemoryCache` is in-process with
|
|
40
|
-
|
|
39
|
+
The default `MemoryCache` is in-process with incremental expiry, a tag index, and a 10,000-entry LRU
|
|
40
|
+
cap. Tune it with `new MemoryCache({ maxEntries: 2_000 })`; `maxEntries: 0` is the explicit unbounded
|
|
41
|
+
opt-in. For a cache shared across instances, implement `CacheStore`
|
|
41
42
|
(`get` / `set` / `delete` / `invalidateTag` / `clear`) over CF KV, Redis, etc.:
|
|
42
43
|
|
|
43
44
|
```ts
|
package/dist/memory-cache.d.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { CacheStore, StoredEntry } from "./types.js";
|
|
7
7
|
export interface MemoryCacheOptions {
|
|
8
|
-
/** Evict the least-recently-used entry once the count exceeds this. `0`
|
|
8
|
+
/** Evict the least-recently-used entry once the count exceeds this. Default 10,000; `0` explicitly
|
|
9
|
+
* opts into an unbounded cache. */
|
|
9
10
|
readonly maxEntries?: number;
|
|
10
11
|
/** Injectable clock (tests). Default `() => Date.now()`. */
|
|
11
12
|
readonly now?: () => number;
|
|
@@ -13,6 +14,7 @@ export interface MemoryCacheOptions {
|
|
|
13
14
|
export declare class MemoryCache implements CacheStore {
|
|
14
15
|
private readonly rows;
|
|
15
16
|
private readonly tagIndex;
|
|
17
|
+
private expirySweep;
|
|
16
18
|
private readonly maxEntries;
|
|
17
19
|
private readonly now;
|
|
18
20
|
constructor(options?: MemoryCacheOptions);
|
|
@@ -25,5 +27,6 @@ export declare class MemoryCache implements CacheStore {
|
|
|
25
27
|
size(): number;
|
|
26
28
|
/** Remove a key and unlink it from every tag set it belonged to. */
|
|
27
29
|
private evict;
|
|
30
|
+
private sweepExpired;
|
|
28
31
|
}
|
|
29
32
|
//# sourceMappingURL=memory-cache.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-cache.d.ts","sourceRoot":"","sources":["../src/memory-cache.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AASzD,MAAM,WAAW,kBAAkB;IACjC
|
|
1
|
+
{"version":3,"file":"memory-cache.d.ts","sourceRoot":"","sources":["../src/memory-cache.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AASzD,MAAM,WAAW,kBAAkB;IACjC;uCACmC;IACnC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,4DAA4D;IAC5D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CAC5B;AAED,qBAAa,WAAY,YAAW,UAAU;IAE5C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyB;IAC9C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiC;IAC1D,OAAO,CAAC,WAAW,CAAsB;IACzC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IACnC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;gBAEtB,OAAO,GAAE,kBAAuB;IAQ5C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IAazC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI;IAyBnE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIzB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAMhC,KAAK,IAAI,IAAI;IAMb,kDAAkD;IAClD,IAAI,IAAI,MAAM;IAId,oEAAoE;IACpE,OAAO,CAAC,KAAK;IAab,OAAO,CAAC,YAAY;CAkBrB"}
|
package/dist/memory-cache.js
CHANGED
|
@@ -2,10 +2,11 @@ export class MemoryCache {
|
|
|
2
2
|
// Map iteration is insertion-ordered, so the first key is the LRU victim; `get`/`set` re-insert to touch.
|
|
3
3
|
rows = new Map();
|
|
4
4
|
tagIndex = new Map();
|
|
5
|
+
expirySweep = this.rows.entries();
|
|
5
6
|
maxEntries;
|
|
6
7
|
now;
|
|
7
8
|
constructor(options = {}) {
|
|
8
|
-
this.maxEntries = options.maxEntries ??
|
|
9
|
+
this.maxEntries = options.maxEntries ?? 10_000;
|
|
9
10
|
if (!Number.isSafeInteger(this.maxEntries) || this.maxEntries < 0) {
|
|
10
11
|
throw new RangeError("MemoryCache: maxEntries must be a non-negative safe integer");
|
|
11
12
|
}
|
|
@@ -25,6 +26,9 @@ export class MemoryCache {
|
|
|
25
26
|
return { value: row.value, expiresAt: row.expiresAt, staleAt: row.staleAt };
|
|
26
27
|
}
|
|
27
28
|
set(key, entry, tags) {
|
|
29
|
+
// Incremental expiry keeps cold, high-cardinality keys from occupying the entire bound until
|
|
30
|
+
// their exact key is read. A fixed batch avoids an O(n) latency spike on a write.
|
|
31
|
+
this.sweepExpired(8);
|
|
28
32
|
this.evict(key); // clear any prior tag links before overwriting
|
|
29
33
|
this.rows.set(key, {
|
|
30
34
|
value: entry.value,
|
|
@@ -59,6 +63,7 @@ export class MemoryCache {
|
|
|
59
63
|
clear() {
|
|
60
64
|
this.rows.clear();
|
|
61
65
|
this.tagIndex.clear();
|
|
66
|
+
this.expirySweep = this.rows.entries();
|
|
62
67
|
}
|
|
63
68
|
/** Live entry count (for observability/tests). */
|
|
64
69
|
size() {
|
|
@@ -79,5 +84,25 @@ export class MemoryCache {
|
|
|
79
84
|
}
|
|
80
85
|
}
|
|
81
86
|
}
|
|
87
|
+
sweepExpired(limit) {
|
|
88
|
+
let restarted = false;
|
|
89
|
+
for (let scanned = 0; scanned < limit;) {
|
|
90
|
+
const next = this.expirySweep.next();
|
|
91
|
+
if (next.done) {
|
|
92
|
+
if (restarted)
|
|
93
|
+
return; // the map is empty, or the fresh iterator also reached its end
|
|
94
|
+
this.expirySweep = this.rows.entries();
|
|
95
|
+
restarted = true;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
restarted = false;
|
|
99
|
+
scanned++;
|
|
100
|
+
const [key, row] = next.value;
|
|
101
|
+
// The key may have been overwritten since the iterator produced its row; only evict that exact
|
|
102
|
+
// entry so a fresh value is never removed by a stale sweep observation.
|
|
103
|
+
if (this.rows.get(key) === row && this.now() >= row.expiresAt)
|
|
104
|
+
this.evict(key);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
82
107
|
}
|
|
83
108
|
//# sourceMappingURL=memory-cache.js.map
|
package/dist/memory-cache.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-cache.js","sourceRoot":"","sources":["../src/memory-cache.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"memory-cache.js","sourceRoot":"","sources":["../src/memory-cache.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/cache",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "Typed KV cache for nifra - TTL, stale-while-revalidate, tag invalidation, and single-flight stampede protection on a pluggable store (in-memory default; bring CF KV / Redis). Dependency-free, runs on Bun/Node/Deno/Workers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
package/src/memory-cache.ts
CHANGED
|
@@ -13,7 +13,8 @@ interface Row {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export interface MemoryCacheOptions {
|
|
16
|
-
/** Evict the least-recently-used entry once the count exceeds this. `0`
|
|
16
|
+
/** Evict the least-recently-used entry once the count exceeds this. Default 10,000; `0` explicitly
|
|
17
|
+
* opts into an unbounded cache. */
|
|
17
18
|
readonly maxEntries?: number
|
|
18
19
|
/** Injectable clock (tests). Default `() => Date.now()`. */
|
|
19
20
|
readonly now?: () => number
|
|
@@ -23,11 +24,12 @@ export class MemoryCache implements CacheStore {
|
|
|
23
24
|
// Map iteration is insertion-ordered, so the first key is the LRU victim; `get`/`set` re-insert to touch.
|
|
24
25
|
private readonly rows = new Map<string, Row>()
|
|
25
26
|
private readonly tagIndex = new Map<string, Set<string>>()
|
|
27
|
+
private expirySweep = this.rows.entries()
|
|
26
28
|
private readonly maxEntries: number
|
|
27
29
|
private readonly now: () => number
|
|
28
30
|
|
|
29
31
|
constructor(options: MemoryCacheOptions = {}) {
|
|
30
|
-
this.maxEntries = options.maxEntries ??
|
|
32
|
+
this.maxEntries = options.maxEntries ?? 10_000
|
|
31
33
|
if (!Number.isSafeInteger(this.maxEntries) || this.maxEntries < 0) {
|
|
32
34
|
throw new RangeError("MemoryCache: maxEntries must be a non-negative safe integer")
|
|
33
35
|
}
|
|
@@ -48,6 +50,9 @@ export class MemoryCache implements CacheStore {
|
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
set(key: string, entry: StoredEntry, tags: readonly string[]): void {
|
|
53
|
+
// Incremental expiry keeps cold, high-cardinality keys from occupying the entire bound until
|
|
54
|
+
// their exact key is read. A fixed batch avoids an O(n) latency spike on a write.
|
|
55
|
+
this.sweepExpired(8)
|
|
51
56
|
this.evict(key) // clear any prior tag links before overwriting
|
|
52
57
|
this.rows.set(key, {
|
|
53
58
|
value: entry.value,
|
|
@@ -82,6 +87,7 @@ export class MemoryCache implements CacheStore {
|
|
|
82
87
|
clear(): void {
|
|
83
88
|
this.rows.clear()
|
|
84
89
|
this.tagIndex.clear()
|
|
90
|
+
this.expirySweep = this.rows.entries()
|
|
85
91
|
}
|
|
86
92
|
|
|
87
93
|
/** Live entry count (for observability/tests). */
|
|
@@ -102,4 +108,23 @@ export class MemoryCache implements CacheStore {
|
|
|
102
108
|
}
|
|
103
109
|
}
|
|
104
110
|
}
|
|
111
|
+
|
|
112
|
+
private sweepExpired(limit: number): void {
|
|
113
|
+
let restarted = false
|
|
114
|
+
for (let scanned = 0; scanned < limit; ) {
|
|
115
|
+
const next = this.expirySweep.next()
|
|
116
|
+
if (next.done) {
|
|
117
|
+
if (restarted) return // the map is empty, or the fresh iterator also reached its end
|
|
118
|
+
this.expirySweep = this.rows.entries()
|
|
119
|
+
restarted = true
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
restarted = false
|
|
123
|
+
scanned++
|
|
124
|
+
const [key, row] = next.value
|
|
125
|
+
// The key may have been overwritten since the iterator produced its row; only evict that exact
|
|
126
|
+
// entry so a fresh value is never removed by a stale sweep observation.
|
|
127
|
+
if (this.rows.get(key) === row && this.now() >= row.expiresAt) this.evict(key)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
105
130
|
}
|