@nxtedition/cache 1.0.4 → 1.0.5

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
 
@@ -73,19 +73,15 @@ type CacheResult<V> =
73
73
  | { value: Promise<V> | null; async: true } // cache miss
74
74
  ```
75
75
 
76
- When `async: true`, `value` is a Promise that resolves once the `valueSelector` completes. If no `valueSelector` was provided, `value` is `undefined`.
76
+ When `async: true`, `value` is a Promise that resolves once the `valueSelector` completes.
77
77
 
78
78
  #### `cache.peek(...args): CacheResult<V>`
79
79
 
80
80
  Same as `get()` but does **not** trigger a refresh on cache miss. Returns `{ value: null, async: true }` for missing entries.
81
81
 
82
- #### `cache.refresh(...args): Promise<V> | undefined`
82
+ #### `cache.refresh(...args): Promise<V>`
83
83
 
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.
85
-
86
- #### `cache.set(key, value): void`
87
-
88
- Manually set a value in the cache.
84
+ 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
85
 
90
86
  #### `cache.delete(key): void`
91
87
 
package/lib/index.d.ts CHANGED
@@ -23,13 +23,12 @@ export type CacheResult<V> = {
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
@@ -64,7 +64,7 @@ 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()
@@ -276,32 +268,34 @@ export class AsyncCache {
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.5",
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": "284d1bf697548d0bda69df9a240268da019b626f"
35
35
  }