@nxtedition/cache 2.1.6 → 2.1.8

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
@@ -7,6 +7,7 @@ export interface DatabaseOptions {
7
7
  }
8
8
  export interface LockOptions {
9
9
  minTimeout?: number;
10
+ maxTimeout?: number;
10
11
  }
11
12
  export interface Serializer<V> {
12
13
  serialize: (value: V) => Buffer | Uint8Array | string;
@@ -28,9 +29,9 @@ export type CacheResult<V> = {
28
29
  async: true;
29
30
  };
30
31
  declare global {
31
- var __nxt_cache: {
32
+ var __nxt_cache: WeakRef<{
32
33
  stats: Cache['stats'];
33
- }[];
34
+ }>[];
34
35
  }
35
36
  export declare class Cache<V = unknown, A extends unknown[] = [string]> extends EventEmitter {
36
37
  #private;
@@ -55,6 +56,7 @@ export declare class Cache<V = unknown, A extends unknown[] = [string]> extends
55
56
  size: number | undefined;
56
57
  } | undefined;
57
58
  };
59
+ [Symbol.dispose](): void;
58
60
  close(): void;
59
61
  get(...args: A): CacheResult<V>;
60
62
  peek(...args: A): CacheResult<V>;
package/lib/index.js CHANGED
@@ -39,6 +39,7 @@ const dbs = new Set ()
39
39
 
40
40
 
41
41
 
42
+
42
43
 
43
44
 
44
45
 
@@ -73,7 +74,7 @@ const defaultSerializer = {
73
74
 
74
75
 
75
76
 
76
-
77
+
77
78
 
78
79
 
79
80
  const VERSION = 5
@@ -97,8 +98,9 @@ export class Cache extends EventEmi
97
98
  #lockTimeout = 10
98
99
  #lockMinTimeout = 1
99
100
  #lockMaxTimeout = 1_000
100
-
101
+ #flushHandle = null
101
102
  #location
103
+ #databaseTimeout = 20
102
104
  #database = null
103
105
  #getQuery = null
104
106
  #delQuery = null
@@ -177,6 +179,12 @@ export class Cache extends EventEmi
177
179
  this.#lockTimeout = this.#lockMinTimeout
178
180
  this.#lockMean = this.#lockTimeout / 1.2
179
181
  }
182
+ if (opts.lock.maxTimeout !== undefined) {
183
+ if (typeof opts.lock.maxTimeout !== 'number') {
184
+ throw new TypeError('lock.maxTimeout must be a number')
185
+ }
186
+ this.#lockMaxTimeout = Math.max(this.#lockMinTimeout, opts.lock.maxTimeout)
187
+ }
180
188
  }
181
189
 
182
190
  if (opts?.serializer !== undefined) {
@@ -198,9 +206,9 @@ export class Cache extends EventEmi
198
206
  for (let n = 0; opts?.database !== null && opts?.database !== false; n++) {
199
207
  try {
200
208
  const maxSize = opts?.database?.maxSize ?? 128 * 1024 * 1024
201
- const timeout = opts?.database?.timeout ?? 20
209
+ this.#databaseTimeout = opts?.database?.timeout ?? 20
202
210
 
203
- this.#database ??= new DatabaseSync(location, { timeout })
211
+ this.#database ??= new DatabaseSync(location, { timeout: this.#databaseTimeout })
204
212
 
205
213
  this.#database.exec(`
206
214
  PRAGMA journal_mode = WAL;
@@ -260,6 +268,8 @@ export class Cache extends EventEmi
260
268
  this.#pageSizeQuery = this.#database.prepare('PRAGMA page_size')
261
269
  break
262
270
  } catch (err) {
271
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100)
272
+
263
273
  if (n >= 16) {
264
274
  this.#database?.close()
265
275
  this.#database = null
@@ -285,7 +295,7 @@ export class Cache extends EventEmi
285
295
  dbs.add(this)
286
296
 
287
297
  globalThis.__nxt_cache ??= []
288
- globalThis.__nxt_cache.push(this)
298
+ globalThis.__nxt_cache.push(new WeakRef(this))
289
299
  }
290
300
 
291
301
  get stats() {
@@ -317,7 +327,16 @@ export class Cache extends EventEmi
317
327
  }
318
328
  }
319
329
 
330
+ [Symbol.dispose]() {
331
+ this.close()
332
+ }
333
+
320
334
  close() {
335
+ while (this.#flushHandle) {
336
+ clearImmediate(this.#flushHandle)
337
+ this.#flush()
338
+ }
339
+
321
340
  this.#closed = true
322
341
  this.#dedupe.clear()
323
342
  dbs.delete(this)
@@ -336,7 +355,8 @@ export class Cache extends EventEmi
336
355
  this.#database?.close()
337
356
  this.#database = null
338
357
 
339
- 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
340
360
  if (idx !== -1) {
341
361
  globalThis.__nxt_cache.splice(idx, 1)
342
362
  }
@@ -374,14 +394,35 @@ export class Cache extends EventEmi
374
394
  if (this.#closed) {
375
395
  throw new Error('cache is closed')
376
396
  }
397
+ this.#memory?.purgeStale(Date.now())
398
+ this.#database?.exec('PRAGMA busy_timeout = 5000')
377
399
  try {
378
- this.#memory?.purgeStale(Date.now())
379
- this.#purgeStaleQuery?.run(Date.now())
380
- this.#lockPurgeQuery?.run(Date.now() - 3_600_000)
381
- this.#database?.exec('PRAGMA wal_checkpoint(TRUNCATE)')
382
- this.#database?.exec('PRAGMA optimize')
383
- } catch (err) {
384
- 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
+ }
385
426
  }
386
427
  }
387
428
 
@@ -534,12 +575,12 @@ export class Cache extends EventEmi
534
575
 
535
576
  const ttlValue = Math.min(MAX_DURATION, this.#ttl(value, key) ?? Infinity)
536
577
  if (!Number.isFinite(ttlValue) || ttlValue < 0) {
537
- 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')
538
579
  }
539
580
 
540
581
  const staleValue = Math.min(MAX_DURATION, this.#stale(value, key) ?? Infinity)
541
582
  if (!Number.isFinite(staleValue) || staleValue < 0) {
542
- 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')
543
584
  }
544
585
 
545
586
  const now = Date.now()
@@ -560,15 +601,20 @@ export class Cache extends EventEmi
560
601
  )
561
602
  this.#memory?.set(key, entry)
562
603
 
563
- if (this.#setBatch.length === 0) {
604
+ if (!this.#flushHandle) {
564
605
  this.#memory?.cork()
565
- setImmediate(this.#flush)
606
+ this.#flushHandle = setImmediate(this.#flush)
607
+ } else if (this.#setBatch.length > 512) {
608
+ clearImmediate(this.#flushHandle)
609
+ this.#flush()
566
610
  }
567
611
 
568
612
  this.#setBatch.push({ key, data, ttl, stale })
569
613
  }
570
614
 
571
615
  #flush = () => {
616
+ this.#flushHandle = null
617
+
572
618
  if (this.#setBatch.length === 0 || this.#closed || this.#database == null) {
573
619
  this.#setBatch.length = 0
574
620
  this.#memory?.uncork()
@@ -625,7 +671,7 @@ export class Cache extends EventEmi
625
671
 
626
672
  if (this.#setBatch.length > 0) {
627
673
  // If we weren't able to flush the entire batch within the time limit, schedule another flush.
628
- setImmediate(this.#flush)
674
+ this.#flushHandle = setImmediate(this.#flush)
629
675
  } else {
630
676
  this.#memory?.uncork()
631
677
  }
@@ -711,12 +757,11 @@ export class Cache extends EventEmi
711
757
  // Timeout = mean * 1.2 + 3 * stddev: 20% base margin plus 3 standard
712
758
  // deviations to accommodate timing variability.
713
759
  // Winsorize input at mean + 5σ so a single extreme outlier can't blow up the stats.
760
+ // Floor the stddev at half the mean so the window never collapses to zero
761
+ // when variance decays (all observations near the mean).
714
762
  const alpha = 0.2
715
- const clamped = Math.min(
716
- duration,
717
- this.#lockMean + 5 * Math.sqrt(this.#lockVar),
718
- this.#lockMaxTimeout,
719
- )
763
+ const stddev = Math.max(this.#lockMean * 0.5, Math.sqrt(this.#lockVar))
764
+ const clamped = Math.min(duration, this.#lockMean + 5 * stddev, this.#lockMaxTimeout)
720
765
  const diff = clamped - this.#lockMean
721
766
  this.#lockMean += alpha * diff
722
767
  this.#lockVar = (1 - alpha) * (this.#lockVar + alpha * diff * diff)
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.6",
3
+ "version": "2.1.8",
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": "a7202d8b4d3decaff213576df560e8cd5775db37"
33
+ "gitHead": "20b962a02ddc295425538b02f1fef1b7b418af70"
34
34
  }