@nxtedition/cache 2.1.10 → 2.1.12

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 (3) hide show
  1. package/README.md +39 -11
  2. package/lib/index.js +137 -63
  3. package/package.json +6 -2
package/README.md CHANGED
@@ -1,17 +1,19 @@
1
1
  # @nxtedition/cache
2
2
 
3
- A two-tier async cache with SQLite persistence, in-memory LRU, stale-while-revalidate, cross-process deduplication, and automatic request deduplication.
3
+ A two-tier async cache with SQLite persistence, in-memory pseudo-LRU, stale-while-revalidate, cross-process/thread deduplication, and automatic request coalescing.
4
4
 
5
5
  ## Features
6
6
 
7
- - **Two-tier storage** — In-memory LRU cache backed by SQLite on disk
7
+ - **Two-tier storage** — In-memory cache backed by SQLite on disk
8
8
  - **Stale-while-revalidate** — Serve stale data synchronously while refreshing in the background
9
- - **Request deduplication** — Concurrent fetches for the same key share a single in-flight request
10
- - **Cross-process locking** — SQLite-based distributed locks prevent redundant work across processes/threads
9
+ - **Request coalescing** — Concurrent fetches for the same key share a single in-flight request
10
+ - **Cross-process/thread locking** — SQLite-based distributed locks with `Atomics`-based wakeup prevent redundant work across processes and worker threads
11
11
  - **Async value resolution** — Transparently fetches missing values via a user-defined `valueSelector`
12
12
  - **Binary support** — Store and retrieve `Buffer` / `Uint8Array` alongside JSON values
13
13
  - **Size-bounded storage** — Configurable max database size with automatic eviction of oldest entries
14
14
  - **Custom serialization** — Pluggable `serialize`/`deserialize` for non-JSON value types
15
+ - **Batched writes** — SQLite writes are coalesced into transactions via `setImmediate`, reducing I/O
16
+ - **Disposable** — Implements `Symbol.dispose` for use with `using` declarations
15
17
 
16
18
  ## Usage
17
19
 
@@ -42,6 +44,16 @@ if (result.async) {
42
44
  }
43
45
  ```
44
46
 
47
+ ### Using `Symbol.dispose`
48
+
49
+ ```ts
50
+ {
51
+ using cache = new Cache('./tmp.db', fetchItem, (id) => id, { ttl: 5_000 })
52
+ const result = cache.get('key')
53
+ // ...
54
+ } // cache.close() called automatically
55
+ ```
56
+
45
57
  ## API
46
58
 
47
59
  ### `new Cache(location, valueSelector?, keySelector?, opts?)`
@@ -61,7 +73,7 @@ if (result.async) {
61
73
  | `stale` | `number \| (value, key) => number` | `MAX_SAFE_INTEGER` | Stale-while-revalidate window in ms. After `ttl + stale`, the entry is purged. |
62
74
  | `memory` | `MemoryOptions \| false \| null` | `{ maxSize: 16MB, maxCount: 16384 }` | In-memory cache config, or `false`/`null` to disable. |
63
75
  | `database` | `DatabaseOptions \| false \| null` | `{ timeout: 20, maxSize: 128MB }` | SQLite config, or `false`/`null` to disable persistence. |
64
- | `lock` | `LockOptions \| false \| null` | `{ minTimeout: 1, maxTimeout: 1000 }` | Cross-process lock config, or `false`/`null` to disable. |
76
+ | `lock` | `LockOptions \| false \| null` | `{ minTimeout: 1, maxTimeout: 1000 }` | Cross-process/thread lock config, or `false`/`null` to disable. |
65
77
  | `serializer` | `Serializer<V>` | JSON + ArrayBufferView passthrough | Custom `{ serialize, deserialize }` for value encoding. |
66
78
 
67
79
  #### `MemoryOptions`
@@ -71,6 +83,8 @@ if (result.async) {
71
83
  | `maxSize` | `number` | `16 * 1024 * 1024` (16 MB) | Maximum total size in bytes of cached entries. |
72
84
  | `maxCount` | `number` | `16 * 1024` (16384) | Maximum number of entries in memory. |
73
85
 
86
+ The in-memory tier uses a random-two-choice eviction strategy: when the cache is full, two entries are sampled at random and the least recently accessed one is evicted. This provides near-LRU behavior with O(1) eviction cost.
87
+
74
88
  #### `DatabaseOptions`
75
89
 
76
90
  | Option | Type | Default | Description |
@@ -80,7 +94,7 @@ if (result.async) {
80
94
 
81
95
  #### `LockOptions`
82
96
 
83
- Cross-process locking prevents multiple processes from computing the same value simultaneously. The lock timeout is adaptive — it uses an exponential moving average (EMA) of `valueSelector` durations to estimate how long to wait before taking over a lock.
97
+ Cross-process/thread locking prevents multiple processes or worker threads from computing the same value simultaneously. The lock timeout is adaptive — it uses an exponential moving average (EMA) of `valueSelector` durations to estimate how long to wait before taking over a lock.
84
98
 
85
99
  | Option | Type | Default | Description |
86
100
  | ------------ | -------- | ------- | -------------------------------------------------------------------------- |
@@ -119,7 +133,7 @@ if (result.async) {
119
133
 
120
134
  #### `cache.get(...args): CacheResult<V>`
121
135
 
122
- Returns a cached value or triggers a fetch on cache miss. If the entry is stale and the `valueSelector` is async, returns the stale value synchronously while a background refresh runs.
136
+ Returns a cached value or triggers a fetch on cache miss. If the entry is stale and the `valueSelector` is async, returns the stale value synchronously (`async: false`) while a background refresh runs. If the `valueSelector` throws during a stale revalidation, the error is emitted and the stale value is preserved.
123
137
 
124
138
  #### `cache.peek(...args): CacheResult<V>`
125
139
 
@@ -139,7 +153,9 @@ Remove all expired entries (past `ttl + stale`) from both the in-memory cache an
139
153
 
140
154
  #### `cache.close(): void`
141
155
 
142
- Close the SQLite database and release resources. Clears all in-flight deduplication. Operations after `close()` throw.
156
+ Flush any pending writes, close the SQLite database, and release resources. Clears all in-flight deduplication. Operations after `close()` throw.
157
+
158
+ Also available as `[Symbol.dispose]()` for use with `using` declarations.
143
159
 
144
160
  #### `cache.stats`
145
161
 
@@ -183,11 +199,23 @@ Once the stale window expires, the entry is purged entirely and the next `get()`
183
199
  + bg refresh
184
200
  ```
185
201
 
186
- ## Cross-Process Locking
202
+ ## Cross-Process/Thread Locking
203
+
204
+ When multiple processes or worker threads share the same SQLite database, the lock mechanism prevents redundant `valueSelector` calls. Process A acquires a lock, computes the value, and writes it. Process B sees the lock, waits for the estimated completion time (EMA-based), then reads the value from SQLite.
205
+
206
+ For worker threads sharing the same process, a `SharedArrayBuffer` + `Atomics.waitAsync` mechanism provides efficient cross-thread wakeup — the waiting thread is notified immediately when the lock holder writes the value, rather than polling.
207
+
208
+ If the lock holder crashes, the lock becomes stale after `3 * lockTimeout` and another process steals it.
209
+
210
+ ## Batched Writes
211
+
212
+ SQLite writes are batched using `setImmediate` — multiple `set()` calls within the same microtask turn are coalesced into a single `BEGIN`/`COMMIT` transaction. While a write batch is pending, the in-memory cache is corked (eviction deferred) to avoid dropping entries before they reach disk. If a batch exceeds 512 items, it is flushed immediately.
213
+
214
+ If the database is full (`SQLITE_FULL`), the cache evicts the 256 oldest entries and retries up to 3 times. On other errors, the rolled-back items are dropped from the batch and the error is emitted.
187
215
 
188
- When multiple processes or threads share the same SQLite database, the lock mechanism prevents redundant `valueSelector` calls. Process A acquires a lock, computes the value, and writes it. Process B sees the lock, waits for the estimated completion time (EMA-based), then reads the value from SQLite.
216
+ ## Error Handling
189
217
 
190
- If the lock holder crashes, the lock becomes stale after `3 × lockTimeout` and another process steals it.
218
+ `Cache` extends `EventEmitter`. Non-fatal errors (SQLite failures, lock contention errors, stale revalidation failures) are emitted as `'error'` events when a listener is attached. If no `'error'` listener is registered, errors are surfaced via `process.emitWarning()` instead, avoiding unhandled crashes.
191
219
 
192
220
  ## Off-Peak Purge
193
221
 
package/lib/index.js CHANGED
@@ -2,6 +2,11 @@ import { randomUUID } from 'node:crypto'
2
2
  import { EventEmitter } from 'node:events'
3
3
  import { DatabaseSync, } from 'node:sqlite'
4
4
  import { setTimeout as delay } from 'node:timers/promises'
5
+
6
+ import xxhash from 'xxhash-wasm'
7
+
8
+ import { getOrCreate } from '@nxtedition/shared'
9
+
5
10
  import { MemoryCache, } from "./memory.js"
6
11
 
7
12
 
@@ -79,6 +84,7 @@ const defaultSerializer = {
79
84
 
80
85
  const VERSION = 5
81
86
  const MAX_DURATION = 365000000e3
87
+ const HASHER = await xxhash()
82
88
 
83
89
  export class Cache extends EventEmitter {
84
90
  #memory
@@ -92,12 +98,15 @@ export class Cache extends EventEmi
92
98
  #stale
93
99
  #serializer
94
100
 
95
- #lockId = randomUUID()
101
+ #lockId = null
102
+ #lockArray = null
103
+ #lockSet
96
104
  #lockVar = 0
97
105
  #lockMean = 5
98
106
  #lockTimeout = 10
99
107
  #lockMinTimeout = 1
100
108
  #lockMaxTimeout = 1_000
109
+
101
110
  #flushHandle = null
102
111
  #location
103
112
  #databaseTimeout = 20
@@ -113,9 +122,10 @@ export class Cache extends EventEmi
113
122
  #pageCountQuery = null
114
123
  #pageSizeQuery = null
115
124
  #setQuery = null
116
- #setBatch = []
125
+ #setBatch =
126
+ []
117
127
 
118
- #emitError(err ) {
128
+ #emitError = (err ) => {
119
129
  if (this.listenerCount('error') > 0) {
120
130
  this.emit('error', err)
121
131
  } else {
@@ -164,27 +174,36 @@ export class Cache extends EventEmi
164
174
  }
165
175
 
166
176
  if (opts?.lock === false || opts?.lock === null) {
177
+ this.#lockId = null
178
+ this.#lockArray = null
179
+ this.#lockSet = null
167
180
  this.#lockMinTimeout = -1
168
181
  this.#lockTimeout = -1
169
182
  this.#lockMean = -1
170
- } else if (opts?.lock !== undefined) {
171
- if (typeof opts.lock !== 'object' || opts.lock === null) {
172
- throw new TypeError('lock must be an object')
173
- }
174
- if (opts.lock.minTimeout !== undefined) {
175
- if (typeof opts.lock.minTimeout !== 'number') {
176
- throw new TypeError('lock.minTimeout must be a number')
183
+ } else {
184
+ if (opts?.lock !== undefined) {
185
+ if (typeof opts.lock !== 'object' || opts.lock === null) {
186
+ throw new TypeError('lock must be an object')
177
187
  }
178
- this.#lockMinTimeout = Math.max(0, opts.lock.minTimeout)
179
- this.#lockTimeout = this.#lockMinTimeout
180
- this.#lockMean = this.#lockTimeout / 1.2
181
- }
182
- if (opts.lock.maxTimeout !== undefined) {
183
- if (typeof opts.lock.maxTimeout !== 'number') {
184
- throw new TypeError('lock.maxTimeout must be a number')
188
+ if (opts.lock.minTimeout !== undefined) {
189
+ if (typeof opts.lock.minTimeout !== 'number') {
190
+ throw new TypeError('lock.minTimeout must be a number')
191
+ }
192
+ this.#lockMinTimeout = Math.max(0, opts.lock.minTimeout)
193
+ this.#lockTimeout = this.#lockMinTimeout
194
+ this.#lockMean = this.#lockTimeout / 1.2
195
+ }
196
+ if (opts.lock.maxTimeout !== undefined) {
197
+ if (typeof opts.lock.maxTimeout !== 'number') {
198
+ throw new TypeError('lock.maxTimeout must be a number')
199
+ }
200
+ this.#lockMaxTimeout = Math.max(this.#lockMinTimeout, opts.lock.maxTimeout)
185
201
  }
186
- this.#lockMaxTimeout = Math.max(this.#lockMinTimeout, opts.lock.maxTimeout)
187
202
  }
203
+
204
+ this.#lockId = randomUUID()
205
+ this.#lockArray = new Int32Array(getOrCreate(`__@nxtedition/cache/${location}`, 8 + 1024))
206
+ this.#lockSet = new Set()
188
207
  }
189
208
 
190
209
  if (opts?.serializer !== undefined) {
@@ -227,12 +246,6 @@ export class Cache extends EventEmi
227
246
  ) WITHOUT ROWID;
228
247
 
229
248
  CREATE INDEX IF NOT EXISTS cache_v${VERSION}_stale_idx ON cache_v${VERSION}(stale);
230
-
231
- CREATE TABLE IF NOT EXISTS cache_lock_v${VERSION} (
232
- key TEXT PRIMARY KEY NOT NULL,
233
- lock_acquired INTEGER NOT NULL,
234
- lock_owner TEXT NOT NULL
235
- );
236
249
  `)
237
250
 
238
251
  this.#getQuery = this.#database.prepare(
@@ -249,21 +262,32 @@ export class Cache extends EventEmi
249
262
  `DELETE FROM cache_v${VERSION} WHERE key IN (SELECT key FROM cache_v${VERSION} ORDER BY stale ASC LIMIT ?)`,
250
263
  )
251
264
 
252
- // ON CONFLICT refreshes lock_acquired when we are the owner,
253
- // preventing other processes from stealing our still-active lock.
254
- this.#lockAcquireQuery = this.#database.prepare(
255
- `INSERT INTO cache_lock_v${VERSION} (key, lock_acquired, lock_owner) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET lock_acquired = CASE WHEN lock_owner = excluded.lock_owner THEN excluded.lock_acquired ELSE lock_acquired END RETURNING lock_acquired, lock_owner`,
256
- )
257
- this.#lockStealQuery = this.#database.prepare(
258
- `UPDATE cache_lock_v${VERSION} SET lock_acquired = ?, lock_owner = ? WHERE key = ? AND lock_acquired = ?`,
259
- )
260
- // Read the current lock state after a failed steal to get the winner's timestamp.
261
- this.#lockGetQuery = this.#database.prepare(
262
- `SELECT lock_acquired FROM cache_lock_v${VERSION} WHERE key = ?`,
263
- )
264
- this.#lockPurgeQuery = this.#database.prepare(
265
- `DELETE FROM cache_lock_v${VERSION} WHERE lock_acquired <= ?`,
266
- )
265
+ if (this.#lockId != null) {
266
+ this.#database.exec(`
267
+ CREATE TABLE IF NOT EXISTS cache_lock_v${VERSION} (
268
+ key TEXT PRIMARY KEY NOT NULL,
269
+ lock_acquired INTEGER NOT NULL,
270
+ lock_owner TEXT NOT NULL
271
+ );
272
+ `)
273
+
274
+ // ON CONFLICT refreshes lock_acquired when we are the owner,
275
+ // preventing other processes from stealing our still-active lock.
276
+ this.#lockAcquireQuery = this.#database.prepare(
277
+ `INSERT INTO cache_lock_v${VERSION} (key, lock_acquired, lock_owner) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET lock_acquired = CASE WHEN lock_owner = excluded.lock_owner THEN excluded.lock_acquired ELSE lock_acquired END RETURNING lock_acquired, lock_owner`,
278
+ )
279
+ this.#lockStealQuery = this.#database.prepare(
280
+ `UPDATE cache_lock_v${VERSION} SET lock_acquired = ?, lock_owner = ? WHERE key = ? AND lock_acquired = ?`,
281
+ )
282
+ // Read the current lock state after a failed steal to get the winner's timestamp.
283
+ this.#lockGetQuery = this.#database.prepare(
284
+ `SELECT lock_acquired FROM cache_lock_v${VERSION} WHERE key = ?`,
285
+ )
286
+ this.#lockPurgeQuery = this.#database.prepare(
287
+ `DELETE FROM cache_lock_v${VERSION} WHERE lock_acquired <= ?`,
288
+ )
289
+ }
290
+
267
291
  this.#pageCountQuery = this.#database.prepare('PRAGMA page_count')
268
292
  this.#pageSizeQuery = this.#database.prepare('PRAGMA page_size')
269
293
  break
@@ -497,15 +521,23 @@ export class Cache extends EventEmi
497
521
  throw new TypeError('keySelector must return a non-empty string')
498
522
  }
499
523
 
500
- if (this.#lockTimeout >= 0) {
524
+ if (this.#lockId != null) {
501
525
  // Another process holds the lock — wait for the value instead of calling valueSelector.
502
526
  // Use deferred pattern so #waitForValue can check promise identity to detect delete+get races.
503
- const lockResult = this.#tryAcquireLock(key)
504
- if (typeof lockResult === 'number') {
527
+ let lockIdx = -1
528
+ let lockVal = 0
529
+ if (this.#lockArray) {
530
+ lockIdx = HASHER.h32(key) % this.#lockArray.length
531
+ lockVal = Atomics.load(this.#lockArray, lockIdx)
532
+ }
533
+
534
+ const lockRes = this.#tryAcquireLock(key, lockIdx)
535
+
536
+ if (typeof lockRes === 'number') {
505
537
  const { promise, resolve, reject } = Promise.withResolvers ()
506
538
  promise.catch(noop)
507
539
  this.#dedupe.set(key, promise)
508
- this.#waitForValue(args, key, lockResult, promise).then(resolve, reject)
540
+ this.#waitForValue(args, key, lockIdx, lockVal, lockRes, promise).then(resolve, reject)
509
541
  return { async: true, value: promise }
510
542
  }
511
543
  }
@@ -541,16 +573,26 @@ export class Cache extends EventEmi
541
573
  const value = this.#valueSelector(...args)
542
574
 
543
575
  if (isThenable(value)) {
576
+ const promise = Promise.resolve(value)
577
+
578
+ if (this.#lockId != null) {
579
+ promise
580
+ .then(() => {
581
+ this.#updateLockTimeout(performance.now() - startTime)
582
+ })
583
+ .catch(this.#emitError)
584
+ }
585
+
544
586
  return {
545
587
  async: true,
546
- value: Promise.resolve(value).then((value) => {
547
- this.#updateLockTimeout(performance.now() - startTime)
548
- return value
549
- }),
588
+ value: promise,
550
589
  }
551
590
  }
552
591
 
553
- this.#updateLockTimeout(performance.now() - startTime)
592
+ if (this.#lockId != null) {
593
+ this.#updateLockTimeout(performance.now() - startTime)
594
+ }
595
+
554
596
  return { async: false, value }
555
597
  }
556
598
 
@@ -609,7 +651,7 @@ export class Cache extends EventEmi
609
651
  this.#flush()
610
652
  }
611
653
 
612
- this.#setBatch.push({ key, data, ttl, stale })
654
+ this.#setBatch.push({ key, data, ttl, stale, hash: this.#lockArray ? HASHER.h32(key) : -1 })
613
655
  }
614
656
 
615
657
  #flush = () => {
@@ -628,18 +670,31 @@ export class Cache extends EventEmi
628
670
  try {
629
671
  this.#database.exec('BEGIN')
630
672
  while (n < this.#setBatch.length) {
631
- const { key, data, ttl, stale } = this.#setBatch[n++]
673
+ const { key, data, ttl, stale, hash } = this.#setBatch[n++]
632
674
  if (data != null) {
633
675
  this.#setQuery?.run(key, data, ttl, stale)
634
676
  } else {
635
677
  this.#delQuery?.run(key)
636
678
  }
679
+
680
+ if (this.#lockSet && this.#lockArray && hash >= 0) {
681
+ this.#lockSet.add(hash % this.#lockArray.length)
682
+ }
683
+
637
684
  if ((n & 0xf) === 0 && performance.now() - startTime > 10) {
638
685
  break
639
686
  }
640
687
  }
641
688
  this.#database.exec('COMMIT')
642
689
  this.#setBatch.splice(0, n)
690
+
691
+ if (this.#lockSet && this.#lockArray) {
692
+ for (const idx of this.#lockSet) {
693
+ Atomics.add(this.#lockArray, idx, 1)
694
+ Atomics.notify(this.#lockArray, idx)
695
+ }
696
+ this.#lockSet.clear()
697
+ }
643
698
  break
644
699
  } catch (err) {
645
700
  // ROLLBACK is required: a failed statement leaves the connection with
@@ -712,8 +767,8 @@ export class Cache extends EventEmi
712
767
  }
713
768
 
714
769
  // Returns lock_acquired timestamp (number) when contended, or a string status.
715
- #tryAcquireLock(key ) {
716
- if (this.#lockAcquireQuery === null) {
770
+ #tryAcquireLock(key , lockIdx ) {
771
+ if (this.#lockAcquireQuery === null || this.#lockId == null) {
717
772
  return 'unavailable'
718
773
  }
719
774
 
@@ -729,6 +784,9 @@ export class Cache extends EventEmi
729
784
  }
730
785
 
731
786
  if (row.lock_owner === this.#lockId) {
787
+ if (this.#lockArray) {
788
+ Atomics.add(this.#lockArray, lockIdx, 1)
789
+ }
732
790
  return 'acquired'
733
791
  }
734
792
 
@@ -739,6 +797,9 @@ export class Cache extends EventEmi
739
797
  // process wins if multiple try to steal concurrently.
740
798
  const stealResult = this.#lockStealQuery .run(now, this.#lockId, key, lockedAt)
741
799
  if (stealResult.changes === 1) {
800
+ if (this.#lockArray) {
801
+ Atomics.add(this.#lockArray, lockIdx, 1)
802
+ }
742
803
  return 'acquired'
743
804
  }
744
805
 
@@ -779,16 +840,29 @@ export class Cache extends EventEmi
779
840
  // If no result found, take over by calling valueSelector ourselves.
780
841
  // selfPromise is the exact promise stored in #dedupe, used for identity checks
781
842
  // to avoid double valueSelector calls after delete()+get() races.
782
- async #waitForValue(args , key , lockedAt , selfPromise ) {
843
+ async #waitForValue(
844
+ args ,
845
+ key ,
846
+ lockIdx ,
847
+ lockVal ,
848
+ lockedAt ,
849
+ selfPromise ,
850
+ ) {
851
+ // Wait for estimated completion: locked_at + our EMA-based timeout.
783
852
  // Loop: wait for lock holder to write a value, retry if lock was stolen by another process.
784
853
  for (let retries = 0; retries < 2 && this.#dedupe.get(key) === selfPromise; retries++) {
785
- // Wait for estimated completion: locked_at + our EMA-based timeout.
786
- const waitTime = Math.max(0, lockedAt + this.#lockTimeout - Date.now())
787
- if (waitTime > 0) {
788
- // Add 20% jitter to reduce thundering herd on contention
789
- await delay(waitTime + Math.floor(Math.random() * waitTime * 0.2), undefined, {
790
- ref: false,
791
- })
854
+ // Add 5% jitter to reduce thundering herd on contention
855
+ const waitTime = Math.ceil(
856
+ Math.max(0, lockedAt + this.#lockTimeout - Date.now()) * (1 + Math.random() * 0.05),
857
+ )
858
+
859
+ if (this.#lockArray != null && lockIdx >= 0) {
860
+ const { async, value } = Atomics.waitAsync(this.#lockArray, lockIdx, lockVal, waitTime)
861
+ if (async) {
862
+ await value
863
+ }
864
+ } else {
865
+ await delay(waitTime, undefined, { ref: false })
792
866
  }
793
867
 
794
868
  // Check if the lock holder wrote a value
@@ -807,10 +881,10 @@ export class Cache extends EventEmi
807
881
  }
808
882
 
809
883
  // Try to acquire lock for takeover
810
- const lockResult = this.#tryAcquireLock(key)
811
- if (typeof lockResult === 'number') {
884
+ const lockRes = this.#tryAcquireLock(key, lockIdx)
885
+ if (typeof lockRes === 'number') {
812
886
  // Another process holds the lock — wait for them
813
- lockedAt = lockResult
887
+ lockedAt = lockRes
814
888
  } else {
815
889
  // We acquired the lock or lock is unavailable — do the work
816
890
  break
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/cache",
3
- "version": "2.1.10",
3
+ "version": "2.1.12",
4
4
  "type": "module",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -30,5 +30,9 @@
30
30
  "tsd": "^0.33.0",
31
31
  "typescript": "^5.9.3"
32
32
  },
33
- "gitHead": "7e5874931730a283de31418b891b69627ff5acae"
33
+ "gitHead": "89b5cbdea4feddf44ef7955ba2b60105dea996f0",
34
+ "dependencies": {
35
+ "@nxtedition/shared": "^5.1.10",
36
+ "xxhash-wasm": "^1.1.0"
37
+ }
34
38
  }