@nxtedition/cache 2.1.7 → 2.1.9

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
@@ -1,15 +1,17 @@
1
1
  # @nxtedition/cache
2
2
 
3
- An async cache with SQLite persistence, in-memory LRU, stale-while-revalidate, and automatic request deduplication.
3
+ A two-tier async cache with SQLite persistence, in-memory LRU, stale-while-revalidate, cross-process deduplication, and automatic request deduplication.
4
4
 
5
5
  ## Features
6
6
 
7
- - **Two-tier storage**: In-memory LRU cache backed by SQLite on disk
8
- - **Stale-while-revalidate**: Serve stale data while refreshing in the background
9
- - **Request deduplication**: Concurrent fetches for the same key share a single in-flight request
10
- - **Async value resolution**: Transparently fetches missing values via a user-defined `valueSelector`
11
- - **Buffer support**: Store and retrieve binary data (Buffer, Uint8Array) alongside JSON values
12
- - **Size-bounded SQLite**: Configurable max database size with automatic eviction of oldest entries
7
+ - **Two-tier storage** In-memory LRU cache backed by SQLite on disk
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
11
+ - **Async value resolution** Transparently fetches missing values via a user-defined `valueSelector`
12
+ - **Binary support** Store and retrieve `Buffer` / `Uint8Array` alongside JSON values
13
+ - **Size-bounded storage** — Configurable max database size with automatic eviction of oldest entries
14
+ - **Custom serialization** — Pluggable `serialize`/`deserialize` for non-JSON value types
13
15
 
14
16
  ## Usage
15
17
 
@@ -19,14 +21,13 @@ import { Cache } from '@nxtedition/cache'
19
21
  const cache = new Cache(
20
22
  './my-cache.db', // SQLite file path, or ':memory:'
21
23
  async (id: string) => {
22
- // fetch the value for this key
23
24
  const res = await fetch(`https://api.example.com/items/${id}`)
24
25
  return res.json()
25
26
  },
26
27
  (id: string) => id, // keySelector: derive cache key from arguments
27
28
  {
28
- ttl: 60_000, // 60s before value is considered stale
29
- stale: 30_000, // serve stale for 30s while revalidating
29
+ ttl: 60_000, // 60 s before value is considered stale
30
+ stale: 30_000, // serve stale for 30 s while revalidating
30
31
  },
31
32
  )
32
33
 
@@ -43,23 +44,25 @@ if (result.async) {
43
44
 
44
45
  ## API
45
46
 
46
- ### `new Cache(location, valueSelector, keySelector, opts?)`
47
+ ### `new Cache(location, valueSelector?, keySelector?, opts?)`
47
48
 
48
- | Parameter | Type | Description |
49
- | --------------- | ------------------------------ | --------------------------------------------- |
50
- | `location` | `string` | SQLite database path, or `':memory:'` |
51
- | `valueSelector` | `(...args) => V \| Promise<V>` | Function to fetch a value on cache miss |
52
- | `keySelector` | `(...args) => string` | Function to derive a cache key from arguments |
53
- | `opts` | `CacheOptions<V>` | Optional configuration |
49
+ | Parameter | Type | Description |
50
+ | --------------- | ---------------------------------- | --------------------------------------------- |
51
+ | `location` | `string` | SQLite database path, or `':memory:'` |
52
+ | `valueSelector` | `(...args) => V \| PromiseLike<V>` | Function to fetch a value on cache miss |
53
+ | `keySelector` | `(...args) => string` | Function to derive a cache key from arguments |
54
+ | `opts` | `CacheOptions<V>` | Optional configuration |
54
55
 
55
- #### Options
56
+ #### `CacheOptions`
56
57
 
57
- | Option | Type | Default | Description |
58
- | ---------- | ---------------------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------- |
59
- | `ttl` | `number \| (value, key) => number` | `MAX_SAFE_INTEGER` | Time-to-live in milliseconds. After this, the entry is stale. |
60
- | `stale` | `number \| (value, key) => number` | `MAX_SAFE_INTEGER` | Stale-while-revalidate window in milliseconds. After `ttl + stale`, the entry is purged. |
61
- | `memory` | `MemoryOptions \| false \| null` | `{ maxSize: 16MB, maxCount: 16384 }` | In-memory cache options, or `false`/`null` to disable in-memory caching. |
62
- | `database` | `DatabaseOptions \| false \| null` | `{ timeout: 20, maxSize: 256MB }` | SQLite options, or `false`/`null` to disable persistence. |
58
+ | Option | Type | Default | Description |
59
+ | ------------ | ---------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------ |
60
+ | `ttl` | `number \| (value, key) => number` | `MAX_SAFE_INTEGER` | Time-to-live in milliseconds. After this, the entry is stale. |
61
+ | `stale` | `number \| (value, key) => number` | `MAX_SAFE_INTEGER` | Stale-while-revalidate window in ms. After `ttl + stale`, the entry is purged. |
62
+ | `memory` | `MemoryOptions \| false \| null` | `{ maxSize: 16MB, maxCount: 16384 }` | In-memory cache config, or `false`/`null` to disable. |
63
+ | `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. |
65
+ | `serializer` | `Serializer<V>` | JSON + ArrayBufferView passthrough | Custom `{ serialize, deserialize }` for value encoding. |
63
66
 
64
67
  #### `MemoryOptions`
65
68
 
@@ -73,26 +76,42 @@ if (result.async) {
73
76
  | Option | Type | Default | Description |
74
77
  | --------- | -------- | ---------------------------- | ----------------------------------------------------------------- |
75
78
  | `timeout` | `number` | `20` | SQLite busy timeout in milliseconds. |
76
- | `maxSize` | `number` | `256 * 1024 * 1024` (256 MB) | Maximum database file size. Oldest entries are evicted when full. |
79
+ | `maxSize` | `number` | `128 * 1024 * 1024` (128 MB) | Maximum database file size. Oldest entries are evicted when full. |
80
+
81
+ #### `LockOptions`
82
+
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.
84
+
85
+ | Option | Type | Default | Description |
86
+ | ------------ | -------- | ------- | -------------------------------------------------------------------------- |
87
+ | `minTimeout` | `number` | `1` | Minimum lock timeout in ms. Also the starting timeout before EMA warms up. |
88
+ | `maxTimeout` | `number` | `1000` | Maximum lock timeout in ms. Caps the EMA-derived timeout. |
89
+
90
+ #### `Serializer<V>`
91
+
92
+ | Method | Signature | Description |
93
+ | ------------- | ---------------------------------------------- | --------------------------- |
94
+ | `serialize` | `(value: V) => Buffer \| Uint8Array \| string` | Encode a value for storage. |
95
+ | `deserialize` | `(data: Buffer \| string) => V` | Decode a stored value. |
96
+
97
+ The default serializer passes `ArrayBufferView` values through as-is and uses `JSON.stringify`/`JSON.parse` for everything else.
77
98
 
78
99
  ### `CacheResult<V>`
79
100
 
80
101
  Both `get()` and `peek()` return a `CacheResult<V>`, a discriminated union on the `async` property:
81
102
 
82
- | `async` | `value` | Meaning |
83
- | ------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
84
- | `false` | `V \| undefined` | Cache hit — the value is available synchronously. Also returned for stale entries (a background refresh is triggered automatically). `undefined` when `peek()` has no cached entry. |
85
- | `true` | `Promise<V>` | Cache miss — `value` is a `Promise` that resolves when the `valueSelector` completes. |
103
+ | `async` | `value` | Meaning |
104
+ | ------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
105
+ | `false` | `V \| undefined` | Cache hit — the value is available synchronously. Also returned for stale entries (a background refresh is triggered). `undefined` when `peek()` has no cached entry. |
106
+ | `true` | `Promise<V>` | Cache miss — `value` is a `Promise` that resolves when the `valueSelector` completes. |
86
107
 
87
108
  ```ts
88
109
  const result = cache.get('key')
89
110
 
90
111
  if (result.async) {
91
- // miss — await the fetch
92
- const value = await result.value
112
+ const value = await result.value // miss — await the fetch
93
113
  } else {
94
- // hit (fresh or stale) — use directly
95
- const value = result.value
114
+ const value = result.value // hit (fresh or stale) — use directly
96
115
  }
97
116
  ```
98
117
 
@@ -100,61 +119,85 @@ if (result.async) {
100
119
 
101
120
  #### `cache.get(...args): CacheResult<V>`
102
121
 
103
- Returns a cached value or triggers a fetch on cache miss.
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.
104
123
 
105
124
  #### `cache.peek(...args): CacheResult<V>`
106
125
 
107
- Same as `get()` but does **not** trigger a refresh on cache miss. Returns `{ value: undefined, async: false }` for missing entries.
126
+ Same as `get()` but does **not** trigger a refresh on cache miss or stale entry. Returns `{ value: undefined, async: false }` for missing/expired entries, or the stale value if within the stale window.
108
127
 
109
- #### `cache.refresh(...args): Promise<V>`
128
+ #### `cache.refresh(...args): CacheResult<V>`
110
129
 
111
- Triggers a fetch via `valueSelector` regardless of cache state. If a fetch for the same key is already in-flight (from a prior `get()` or `refresh()`), the existing promise is returned instead of starting a new one.
130
+ Forces a new fetch via `valueSelector` regardless of cache state. Unlike `get()`, concurrent `refresh()` calls for the same key do **not** deduplicate each call invokes the `valueSelector`. However, `get()` calls during a pending `refresh()` will return the in-flight promise.
112
131
 
113
132
  #### `cache.delete(...args): void`
114
133
 
115
- Remove an entry from the cache. The cache key is derived from `args` via the `keySelector`, just like `get()` and `refresh()`. Also cancels any in-flight deduplication for that key, meaning a pending fetch will not write its result to the cache.
134
+ Remove an entry from both memory and SQLite. Also cancels any in-flight deduplication for that key a pending fetch will still resolve for its callers, but the result is **not** written to the cache.
116
135
 
117
136
  #### `cache.purgeStale(): void`
118
137
 
119
- Remove all expired entries from both the in-memory cache and SQLite.
138
+ Remove all expired entries (past `ttl + stale`) from both the in-memory cache and SQLite. Also cleans up stale lock rows older than 1 hour and runs `PRAGMA wal_checkpoint(TRUNCATE)` + `PRAGMA optimize`.
120
139
 
121
140
  #### `cache.close(): void`
122
141
 
123
- Close the SQLite database and release resources.
142
+ Close the SQLite database and release resources. Clears all in-flight deduplication. Operations after `close()` throw.
143
+
144
+ #### `cache.stats`
145
+
146
+ Returns runtime statistics:
147
+
148
+ ```ts
149
+ {
150
+ lock: { timeout, mean, stddev } | undefined,
151
+ dedupe: { size },
152
+ memory: { size, maxSize, count, maxCount } | undefined,
153
+ database: { location, size } | undefined,
154
+ }
155
+ ```
124
156
 
125
157
  ## Deduplication
126
158
 
127
- Concurrent calls to `get()` or `refresh()` for the same key share a single in-flight Promise. The `valueSelector` is called only once, and all callers receive the same resolved value.
159
+ Concurrent calls to `get()` for the same key share a single in-flight `Promise`. The `valueSelector` is called only once:
128
160
 
129
161
  ```ts
130
162
  // valueSelector is called once, both promises resolve to the same value
131
163
  const [a, b] = await Promise.all([cache.get('key').value, cache.get('key').value])
132
164
  ```
133
165
 
134
- If a fetch fails, the deduplication entry is cleaned up and subsequent calls will retry.
166
+ If a fetch fails, the deduplication entry is cleaned up and subsequent calls retry.
135
167
 
136
168
  Calling `cache.delete(key)` while a fetch is in-flight invalidates the deduplication entry. The pending promise still resolves for its callers, but the result is **not** written to the cache.
137
169
 
170
+ `refresh()` does **not** deduplicate with itself — each call starts a new fetch. However, `get()` calls see the most recent pending promise.
171
+
138
172
  ## Stale-While-Revalidate
139
173
 
140
- When an entry's TTL has expired but is still within the stale window (`ttl + stale`), `get()` returns the stale value synchronously (`async: false`) while triggering a background refresh.
174
+ When an entry's TTL has expired but is still within the stale window, `get()` returns the stale value synchronously (`async: false`) and triggers a background refresh (when the `valueSelector` is async). If the refresh fails, the stale value is preserved.
141
175
 
142
176
  Once the stale window expires, the entry is purged entirely and the next `get()` returns `async: true`.
143
177
 
144
178
  ```
145
179
  |--- ttl ---|--- stale ---|
146
180
  fresh stale expired
181
+ ↓ ↓ ↓
182
+ sync hit sync hit async miss
183
+ + bg refresh
147
184
  ```
148
185
 
186
+ ## Cross-Process Locking
187
+
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.
189
+
190
+ If the lock holder crashes, the lock becomes stale after `3 × lockTimeout` and another process steals it.
191
+
149
192
  ## Off-Peak Purge
150
193
 
151
- All cache instances listen for messages on the `nxt:offPeak` BroadcastChannel. When a message is received, `purgeStale()` is called on every active instance, allowing coordinated cleanup during low-traffic periods.
194
+ All cache instances listen on the `nxt:offPeak` `BroadcastChannel`. When a message is received, `purgeStale()` is called on every active instance, enabling coordinated cleanup during low-traffic periods.
152
195
 
153
196
  ## Scripts
154
197
 
155
198
  ```sh
156
- npm test # run tests
157
- npm run test:coverage # run tests with branch coverage report (90%+ enforced)
158
- npm run typecheck # type-check without emitting
159
- npm run build # build for publishing
199
+ yarn test # run tests
200
+ yarn test:coverage # run tests with coverage report (90%+ enforced)
201
+ yarn typecheck # type-check without emitting
202
+ yarn build # build for publishing
160
203
  ```
package/lib/index.d.ts CHANGED
@@ -29,9 +29,9 @@ export type CacheResult<V> = {
29
29
  async: true;
30
30
  };
31
31
  declare global {
32
- var __nxt_cache: {
32
+ var __nxt_cache: WeakRef<{
33
33
  stats: Cache['stats'];
34
- }[];
34
+ }>[];
35
35
  }
36
36
  export declare class Cache<V = unknown, A extends unknown[] = [string]> extends EventEmitter {
37
37
  #private;
@@ -56,6 +56,7 @@ export declare class Cache<V = unknown, A extends unknown[] = [string]> extends
56
56
  size: number | undefined;
57
57
  } | undefined;
58
58
  };
59
+ [Symbol.dispose](): void;
59
60
  close(): void;
60
61
  get(...args: A): CacheResult<V>;
61
62
  peek(...args: A): CacheResult<V>;
package/lib/index.js CHANGED
@@ -74,7 +74,7 @@ const defaultSerializer = {
74
74
 
75
75
 
76
76
 
77
-
77
+
78
78
 
79
79
 
80
80
  const VERSION = 5
@@ -98,8 +98,9 @@ export class Cache extends EventEmi
98
98
  #lockTimeout = 10
99
99
  #lockMinTimeout = 1
100
100
  #lockMaxTimeout = 1_000
101
-
101
+ #flushHandle = null
102
102
  #location
103
+ #databaseTimeout = 20
103
104
  #database = null
104
105
  #getQuery = null
105
106
  #delQuery = null
@@ -205,9 +206,9 @@ export class Cache extends EventEmi
205
206
  for (let n = 0; opts?.database !== null && opts?.database !== false; n++) {
206
207
  try {
207
208
  const maxSize = opts?.database?.maxSize ?? 128 * 1024 * 1024
208
- const timeout = opts?.database?.timeout ?? 20
209
+ this.#databaseTimeout = opts?.database?.timeout ?? 20
209
210
 
210
- this.#database ??= new DatabaseSync(location, { timeout })
211
+ this.#database ??= new DatabaseSync(location, { timeout: this.#databaseTimeout })
211
212
 
212
213
  this.#database.exec(`
213
214
  PRAGMA journal_mode = WAL;
@@ -267,6 +268,8 @@ export class Cache extends EventEmi
267
268
  this.#pageSizeQuery = this.#database.prepare('PRAGMA page_size')
268
269
  break
269
270
  } catch (err) {
271
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100)
272
+
270
273
  if (n >= 16) {
271
274
  this.#database?.close()
272
275
  this.#database = null
@@ -292,7 +295,7 @@ export class Cache extends EventEmi
292
295
  dbs.add(this)
293
296
 
294
297
  globalThis.__nxt_cache ??= []
295
- globalThis.__nxt_cache.push(this)
298
+ globalThis.__nxt_cache.push(new WeakRef(this))
296
299
  }
297
300
 
298
301
  get stats() {
@@ -324,7 +327,16 @@ export class Cache extends EventEmi
324
327
  }
325
328
  }
326
329
 
330
+ [Symbol.dispose]() {
331
+ this.close()
332
+ }
333
+
327
334
  close() {
335
+ while (this.#flushHandle) {
336
+ clearImmediate(this.#flushHandle)
337
+ this.#flush()
338
+ }
339
+
328
340
  this.#closed = true
329
341
  this.#dedupe.clear()
330
342
  dbs.delete(this)
@@ -343,7 +355,8 @@ export class Cache extends EventEmi
343
355
  this.#database?.close()
344
356
  this.#database = null
345
357
 
346
- const idx = globalThis.__nxt_cache?.indexOf(this) ?? -1
358
+ globalThis.__nxt_cache = globalThis.__nxt_cache?.filter((ref) => ref.deref() != null) ?? []
359
+ const idx = globalThis.__nxt_cache?.findIndex((ref) => ref.deref() === this) ?? -1
347
360
  if (idx !== -1) {
348
361
  globalThis.__nxt_cache.splice(idx, 1)
349
362
  }
@@ -381,14 +394,35 @@ export class Cache extends EventEmi
381
394
  if (this.#closed) {
382
395
  throw new Error('cache is closed')
383
396
  }
397
+ this.#memory?.purgeStale(Date.now())
398
+ this.#database?.exec('PRAGMA busy_timeout = 5000')
384
399
  try {
385
- this.#memory?.purgeStale(Date.now())
386
- this.#purgeStaleQuery?.run(Date.now())
387
- this.#lockPurgeQuery?.run(Date.now() - 3_600_000)
388
- this.#database?.exec('PRAGMA wal_checkpoint(TRUNCATE)')
389
- this.#database?.exec('PRAGMA optimize')
390
- } catch (err) {
391
- this.#emitError(err )
400
+ try {
401
+ this.#purgeStaleQuery?.run(Date.now())
402
+ } catch (err) {
403
+ this.#emitError(err )
404
+ }
405
+ try {
406
+ this.#lockPurgeQuery?.run(Date.now() - 3_600_000)
407
+ } catch (err) {
408
+ this.#emitError(err )
409
+ }
410
+ try {
411
+ this.#database?.exec('PRAGMA wal_checkpoint(TRUNCATE)')
412
+ } catch (err) {
413
+ this.#emitError(err )
414
+ }
415
+ try {
416
+ this.#database?.exec('PRAGMA optimize')
417
+ } catch (err) {
418
+ this.#emitError(err )
419
+ }
420
+ } finally {
421
+ try {
422
+ this.#database?.exec(`PRAGMA busy_timeout = ${this.#databaseTimeout}`)
423
+ } catch (err) {
424
+ this.#emitError(err )
425
+ }
392
426
  }
393
427
  }
394
428
 
@@ -541,12 +575,12 @@ export class Cache extends EventEmi
541
575
 
542
576
  const ttlValue = Math.min(MAX_DURATION, this.#ttl(value, key) ?? Infinity)
543
577
  if (!Number.isFinite(ttlValue) || ttlValue < 0) {
544
- throw new TypeError('ttl must be nully or a positive integer')
578
+ throw new TypeError('ttl must be undefined, null, or a non-negative finite number')
545
579
  }
546
580
 
547
581
  const staleValue = Math.min(MAX_DURATION, this.#stale(value, key) ?? Infinity)
548
582
  if (!Number.isFinite(staleValue) || staleValue < 0) {
549
- throw new TypeError('stale must be nully or a positive integer')
583
+ throw new TypeError('stale must be undefined, null, or a non-negative finite number')
550
584
  }
551
585
 
552
586
  const now = Date.now()
@@ -567,15 +601,20 @@ export class Cache extends EventEmi
567
601
  )
568
602
  this.#memory?.set(key, entry)
569
603
 
570
- if (this.#setBatch.length === 0) {
604
+ if (!this.#flushHandle) {
571
605
  this.#memory?.cork()
572
- setImmediate(this.#flush)
606
+ this.#flushHandle = setImmediate(this.#flush)
607
+ } else if (this.#setBatch.length > 512) {
608
+ clearImmediate(this.#flushHandle)
609
+ this.#flush()
573
610
  }
574
611
 
575
612
  this.#setBatch.push({ key, data, ttl, stale })
576
613
  }
577
614
 
578
615
  #flush = () => {
616
+ this.#flushHandle = null
617
+
579
618
  if (this.#setBatch.length === 0 || this.#closed || this.#database == null) {
580
619
  this.#setBatch.length = 0
581
620
  this.#memory?.uncork()
@@ -632,7 +671,7 @@ export class Cache extends EventEmi
632
671
 
633
672
  if (this.#setBatch.length > 0) {
634
673
  // If we weren't able to flush the entire batch within the time limit, schedule another flush.
635
- setImmediate(this.#flush)
674
+ this.#flushHandle = setImmediate(this.#flush)
636
675
  } else {
637
676
  this.#memory?.uncork()
638
677
  }
@@ -715,24 +754,19 @@ export class Cache extends EventEmi
715
754
 
716
755
  #updateLockTimeout(duration ) {
717
756
  // EMA of mean and variance (Welford-style), clamped to [10ms, 1s].
718
- // Timeout = mean * 1.2 + 3 * stddev: 20% base margin plus 3 standard
719
- // deviations to accommodate timing variability.
757
+ // Timeout = mean + 3 * stddev to accommodate timing variability.
720
758
  // Winsorize input at mean + 5σ so a single extreme outlier can't blow up the stats.
759
+ // Floor the stddev at 20% of the mean so the window never collapses to zero
760
+ // when variance decays (all observations near the mean).
721
761
  const alpha = 0.2
722
- const clamped = Math.min(
723
- duration,
724
- this.#lockMean + 5 * Math.sqrt(this.#lockVar),
725
- this.#lockMaxTimeout,
726
- )
762
+ const stddev = Math.max(this.#lockMean * 0.2, Math.sqrt(this.#lockVar))
763
+ const clamped = Math.min(duration, this.#lockMean + 5 * stddev, this.#lockMaxTimeout)
727
764
  const diff = clamped - this.#lockMean
728
765
  this.#lockMean += alpha * diff
729
766
  this.#lockVar = (1 - alpha) * (this.#lockVar + alpha * diff * diff)
730
767
  this.#lockTimeout = Math.max(
731
768
  this.#lockMinTimeout,
732
- Math.min(
733
- this.#lockMaxTimeout,
734
- Math.ceil(this.#lockMean * 1.2 + Math.sqrt(this.#lockVar) * 3),
735
- ),
769
+ Math.min(this.#lockMaxTimeout, Math.ceil(this.#lockMean + Math.sqrt(this.#lockVar) * 3)),
736
770
  )
737
771
  }
738
772
 
package/lib/memory.js CHANGED
@@ -13,11 +13,6 @@
13
13
 
14
14
 
15
15
 
16
- let fastNow = Date.now()
17
- setInterval(() => {
18
- fastNow = Date.now()
19
- }, 1e3).unref()
20
-
21
16
  export class MemoryCache {
22
17
  #map = new Map()
23
18
  #arr = []
@@ -52,7 +47,7 @@ export class MemoryCache {
52
47
  this.#map.set(key, entry)
53
48
  entry.key = key
54
49
  entry.index = this.#arr.push(entry) - 1
55
- entry.counter = fastNow
50
+ entry.counter = performance.now()
56
51
 
57
52
  this.#size += entry.size ?? 0
58
53
  this.#count += 1
@@ -76,7 +71,7 @@ export class MemoryCache {
76
71
  get(key ) {
77
72
  const entry = this.#map.get(key)
78
73
  if (entry != null) {
79
- entry.counter = fastNow
74
+ entry.counter = performance.now()
80
75
  }
81
76
  return entry
82
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/cache",
3
- "version": "2.1.7",
3
+ "version": "2.1.9",
4
4
  "type": "module",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -30,5 +30,5 @@
30
30
  "tsd": "^0.33.0",
31
31
  "typescript": "^5.9.3"
32
32
  },
33
- "gitHead": "160284d139399d39195532624e26d564aa8c8003"
33
+ "gitHead": "0bc928c692badb55bdf3c6928eea7742b8e17732"
34
34
  }