@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 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 lazy expiry, a tag index, and an optional LRU cap
40
- (`new MemoryCache({ maxEntries: 10_000 })`). For a cache shared across instances, implement `CacheStore`
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
@@ -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` (default) = unbounded. */
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,kGAAkG;IAClG,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,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;IAsBnE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIzB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAMhC,KAAK,IAAI,IAAI;IAKb,kDAAkD;IAClD,IAAI,IAAI,MAAM;IAId,oEAAoE;IACpE,OAAO,CAAC,KAAK;CAYd"}
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"}
@@ -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 ?? 0;
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
@@ -1 +1 @@
1
- {"version":3,"file":"memory-cache.js","sourceRoot":"","sources":["../src/memory-cache.ts"],"names":[],"mappings":"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"}
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.1.0",
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",
@@ -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` (default) = unbounded. */
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 ?? 0
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
  }