@nxtedition/cache 1.0.4 → 1.0.6

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
@@ -43,14 +43,14 @@ if (result.async) {
43
43
 
44
44
  ## API
45
45
 
46
- ### `new AsyncCache(location, valueSelector?, keySelector?, opts?)`
46
+ ### `new AsyncCache(location, valueSelector, keySelector, opts?)`
47
47
 
48
- | Parameter | Type | Description |
49
- | --------------- | ------------------------------ | ------------------------------------------------------------------------------------------ |
50
- | `location` | `string` | SQLite database path, or `':memory:'` |
51
- | `valueSelector` | `(...args) => V \| Promise<V>` | Optional function to fetch a value on cache miss |
52
- | `keySelector` | `(...args) => string` | Optional function to derive a cache key from arguments. Defaults to `JSON.stringify(args)` |
53
- | `opts` | `AsyncCacheOptions<V>` | Optional configuration |
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` | `AsyncCacheOptions<V>` | Optional configuration |
54
54
 
55
55
  #### Options
56
56
 
@@ -61,31 +61,40 @@ if (result.async) {
61
61
  | `lru` | `LRUCache.Options \| false \| null` | `{ max: 4096 }` | LRU cache options, or `false`/`null` to disable in-memory caching |
62
62
  | `db` | `{ timeout?, maxSize? } \| false \| null` | `{ timeout: 20, maxSize: 256MB }` | SQLite options, or `false`/`null` to disable persistence |
63
63
 
64
- ### Methods
64
+ ### `CacheResult<V>`
65
65
 
66
- #### `cache.get(...args): CacheResult<V>`
66
+ Both `get()` and `peek()` return a `CacheResult<V>`, a discriminated union on the `async` property:
67
67
 
68
- Returns the cached value or triggers a fetch via `valueSelector`.
68
+ | `async` | `value` | Meaning |
69
+ | ------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
70
+ | `false` | `V` | Cache hit — the value is available synchronously. Also returned for stale entries (a background refresh is triggered automatically). |
71
+ | `true` | `Promise<V> \| undefined` | Cache miss — `value` is a `Promise` that resolves when the `valueSelector` completes, or `undefined` when called via `peek()` (which never triggers a fetch). |
69
72
 
70
73
  ```ts
71
- type CacheResult<V> =
72
- | { value: V; async: false } // cache hit
73
- | { value: Promise<V> | null; async: true } // cache miss
74
+ const result = cache.get('key')
75
+
76
+ if (result.async) {
77
+ // miss — await the fetch
78
+ const value = await result.value
79
+ } else {
80
+ // hit (fresh or stale) — use directly
81
+ const value = result.value
82
+ }
74
83
  ```
75
84
 
76
- When `async: true`, `value` is a Promise that resolves once the `valueSelector` completes. If no `valueSelector` was provided, `value` is `undefined`.
85
+ ### Methods
77
86
 
78
- #### `cache.peek(...args): CacheResult<V>`
87
+ #### `cache.get(...args): CacheResult<V>`
79
88
 
80
- Same as `get()` but does **not** trigger a refresh on cache miss. Returns `{ value: null, async: true }` for missing entries.
89
+ Returns a cached value or triggers a fetch on cache miss.
81
90
 
82
- #### `cache.refresh(...args): Promise<V> | undefined`
91
+ #### `cache.peek(...args): CacheResult<V>`
83
92
 
84
- Forces a fetch via `valueSelector` regardless of cache state. Returns `undefined` if no `valueSelector` is configured. Concurrent calls for the same key are deduplicated.
93
+ Same as `get()` but does **not** trigger a refresh on cache miss. Returns `{ value: undefined, async: true }` for missing entries.
85
94
 
86
- #### `cache.set(key, value): void`
95
+ #### `cache.refresh(...args): Promise<V>`
87
96
 
88
- Manually set a value in the cache.
97
+ 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.
89
98
 
90
99
  #### `cache.delete(key): void`
91
100
 
package/lib/index.d.ts CHANGED
@@ -18,18 +18,17 @@ export type CacheResult<V> = {
18
18
  value: V;
19
19
  async: false;
20
20
  } | {
21
- value: Promise<V> | null | undefined;
21
+ value: Promise<V> | undefined;
22
22
  async: true;
23
23
  };
24
24
  export declare class AsyncCache<V = unknown, A extends unknown[] = unknown[]> {
25
25
  #private;
26
- constructor(location: string, valueSelector?: (...args: A) => V | Promise<V>, keySelector?: (...args: A) => string, opts?: AsyncCacheOptions<V>);
26
+ constructor(location: string, valueSelector: (...args: A) => V | Promise<V>, keySelector: (...args: A) => string, opts?: AsyncCacheOptions<V>);
27
27
  close(): void;
28
28
  get(...args: A): CacheResult<V>;
29
29
  peek(...args: A): CacheResult<V>;
30
- refresh(...args: A): Promise<V> | undefined;
30
+ refresh(...args: A): Promise<V>;
31
31
  delete(key: string): void;
32
- set(key: string, value: V): void;
33
32
  purgeStale(): void;
34
33
  }
35
34
  export {};
package/lib/index.js CHANGED
@@ -57,14 +57,14 @@ const dbs = new Set ()
57
57
 
58
58
 
59
59
 
60
-
60
+
61
61
 
62
62
  const VERSION = 2
63
63
  const MAX_DURATION = 365000000e3
64
64
 
65
65
  export class AsyncCache {
66
66
  #lru
67
- #valueSelector
67
+ #valueSelector
68
68
  #keySelector
69
69
  #dedupe = new Map ()
70
70
 
@@ -80,27 +80,23 @@ export class AsyncCache {
80
80
 
81
81
  constructor(
82
82
  location ,
83
- valueSelector ,
84
- keySelector ,
83
+ valueSelector ,
84
+ keySelector ,
85
85
  opts ,
86
86
  ) {
87
- if (typeof location === 'string') {
88
- // Do nothing...
89
- } else {
90
- throw new TypeError('location must be undefined or a string')
87
+ if (typeof location !== 'string') {
88
+ throw new TypeError('location must be a string')
91
89
  }
92
90
 
93
- if (typeof valueSelector === 'function' || valueSelector === undefined) {
94
- this.#valueSelector = valueSelector
95
- } else {
91
+ if (typeof valueSelector !== 'function') {
96
92
  throw new TypeError('valueSelector must be a function')
97
93
  }
94
+ this.#valueSelector = valueSelector
98
95
 
99
- if (typeof keySelector === 'function' || keySelector === undefined) {
100
- this.#keySelector = keySelector ?? ((...args ) => JSON.stringify(args))
101
- } else {
96
+ if (typeof keySelector !== 'function') {
102
97
  throw new TypeError('keySelector must be a function')
103
98
  }
99
+ this.#keySelector = keySelector
104
100
 
105
101
  if (typeof opts?.ttl === 'number' || opts?.ttl === undefined) {
106
102
  const ttl = opts?.ttl ?? Number.MAX_SAFE_INTEGER
@@ -196,7 +192,7 @@ export class AsyncCache {
196
192
  return this.#load(args, false)
197
193
  }
198
194
 
199
- refresh(...args ) {
195
+ refresh(...args ) {
200
196
  return this.#refresh(args)
201
197
  }
202
198
 
@@ -204,10 +200,6 @@ export class AsyncCache {
204
200
  this.#delete(key)
205
201
  }
206
202
 
207
- set(key , value ) {
208
- this.#set(key, value)
209
- }
210
-
211
203
  purgeStale() {
212
204
  try {
213
205
  this.#lru?.purgeStale()
@@ -269,39 +261,41 @@ export class AsyncCache {
269
261
  }
270
262
  }
271
263
 
272
- const promise = refresh ? this.#refresh(args, key) : null
264
+ const promise = refresh ? this.#refresh(args, key) : undefined
273
265
 
274
266
  return cached !== undefined
275
267
  ? { value: cached.value, async: false }
276
268
  : { value: promise, async: true }
277
269
  }
278
270
 
279
- #refresh(args , key = this.#keySelector(...args)) {
271
+ #refresh(args , key = this.#keySelector(...args)) {
280
272
  if (typeof key !== 'string' || key.length === 0) {
281
273
  throw new TypeError('keySelector must return a non-empty string')
282
274
  }
283
275
 
284
276
  // TODO (fix): cross process/thread dedupe...
285
- let promise = this.#dedupe.get(key)
286
- if (promise === undefined && this.#valueSelector) {
287
- // eslint-disable-next-line: no-unsafe-argument
288
- promise = Promise.resolve(this.#valueSelector(...args)).then(
289
- (value) => {
290
- if (this.#dedupe.get(key) === promise) {
291
- this.#dedupe.delete(key)
292
- this.#set(key, value)
293
- }
294
- return value
295
- },
296
- (err) => {
297
- this.#dedupe.delete(key)
298
- throw err
299
- },
300
- )
301
- promise.catch(noop)
302
- this.#dedupe.set(key, promise)
277
+ const existing = this.#dedupe.get(key)
278
+ if (existing !== undefined) {
279
+ return existing
303
280
  }
304
281
 
282
+ // eslint-disable-next-line: no-unsafe-argument
283
+ const promise = Promise.resolve(this.#valueSelector(...args)).then(
284
+ (value) => {
285
+ if (this.#dedupe.get(key) === promise) {
286
+ this.#dedupe.delete(key)
287
+ this.#set(key, value)
288
+ }
289
+ return value
290
+ },
291
+ (err) => {
292
+ this.#dedupe.delete(key)
293
+ throw err
294
+ },
295
+ )
296
+ promise.catch(noop)
297
+ this.#dedupe.set(key, promise)
298
+
305
299
  return promise
306
300
  }
307
301
 
@@ -310,6 +304,8 @@ export class AsyncCache {
310
304
  throw new TypeError('key must be a non-empty string')
311
305
  }
312
306
 
307
+ this.#dedupe.delete(key)
308
+
313
309
  const ttlValue = Math.min(MAX_DURATION, this.#ttl(value, key) ?? Infinity)
314
310
  if (!Number.isFinite(ttlValue) || ttlValue < 0) {
315
311
  throw new TypeError('ttl must be nully or a positive integer')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/cache",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "type": "module",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -31,5 +31,5 @@
31
31
  "dependencies": {
32
32
  "lru-cache": "^11.2.6"
33
33
  },
34
- "gitHead": "f3a0afdf05d4d395ba8bdb45c48f619da7491d28"
34
+ "gitHead": "5c8b45723e68c527888e75a1536569479da7e4c5"
35
35
  }