@thi.ng/cache 2.1.106 → 2.2.0

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/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Change Log
2
2
 
3
- - **Last updated**: 2024-03-11T08:41:23Z
3
+ - **Last updated**: 2024-03-11T10:09:24Z
4
4
  - **Generator**: [thi.ng/monopub](https://thi.ng/monopub)
5
5
 
6
6
  All notable changes to this project will be documented in this file.
@@ -9,6 +9,19 @@ See [Conventional Commits](https://conventionalcommits.org/) for commit guidelin
9
9
  **Note:** Unlisted _patch_ versions only involve non-code or otherwise excluded changes
10
10
  and/or version bumps of transitive dependencies.
11
11
 
12
+ ## [2.2.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/cache@2.2.0) (2024-03-11)
13
+
14
+ #### 🚀 Features
15
+
16
+ - add value update callback, update TLRUCache ([d2fed06](https://github.com/thi-ng/umbrella/commit/d2fed06))
17
+ - add CacheOpts.update
18
+ - update doSetEntry() in all impls
19
+ - refactor TLRUCache.getSet() as async fn
20
+ - update TLRUCache.prune() to return eviction count
21
+ - various other refactoring
22
+ - add tests
23
+ - add docs
24
+
12
25
  ### [2.1.106](https://github.com/thi-ng/umbrella/tree/@thi.ng/cache@2.1.106) (2024-03-11)
13
26
 
14
27
  #### 🩹 Bug fixes
package/README.md CHANGED
@@ -47,7 +47,8 @@ strategies](https://en.wikipedia.org/wiki/Cache_replacement_policies).
47
47
  - Supports any types for both keys & values
48
48
  - Customizable cache limits (no. of items / actual size)
49
49
  - Customizable key equality checks (@thi.ng/equiv by default)
50
- - Optional item release callbacks (to clean up resources when value is expunged)
50
+ - Optional item update & release callbacks (e.g. to clean up resources when
51
+ value is being updated or evicted)
51
52
 
52
53
  ## Status
53
54
 
@@ -79,7 +80,7 @@ For Node.js REPL:
79
80
  const cache = await import("@thi.ng/cache");
80
81
  ```
81
82
 
82
- Package sizes (brotli'd, pre-treeshake): ESM: 1.08 KB
83
+ Package sizes (brotli'd, pre-treeshake): ESM: 1.06 KB
83
84
 
84
85
  ## Dependencies
85
86
 
@@ -119,6 +120,10 @@ interface CacheOpts<K, V> {
119
120
  * Eviction callback to clean up resources
120
121
  */
121
122
  release: (k: K, v: V) => void;
123
+ /**
124
+ * Update callback to clean up resources
125
+ */
126
+ update: (k: K, vold: V, vnew: V) => void;
122
127
  /**
123
128
  * Factory for ES6 Map compatible instance
124
129
  * to index cache entries
@@ -137,13 +142,14 @@ interface CacheOpts<K, V> {
137
142
 
138
143
  ### LRU
139
144
 
140
- Removes least recently used items if a new item is added, but would not satisfy cache limit. Every time a cached item is accessed, it's recency is updated.
145
+ Removes least recently used items if a new item is added, but would not satisfy
146
+ cache limit. Every time a cached item is accessed, it's recency is updated.
141
147
 
142
148
  ```ts
143
- import * as cache from "@thi.ng/cache";
149
+ import { LRUCache } from "@thi.ng/cache";
144
150
 
145
- // caches can be configured with maxLen, maxSize and sizing functions (see below)
146
- const lru = new cache.LRUCache<string, number>(null, { maxlen: 3 });
151
+ // caches can be configured with maxlen, maxsize and sizing functions (see below)
152
+ const lru = new LRUCache<string, number>(null, { maxlen: 3 });
147
153
  lru.set("foo", 23);
148
154
  lru.set("bar", 42);
149
155
  lru.set("baz", 66);
@@ -155,7 +161,7 @@ lru.get("foo");
155
161
  // 23
156
162
 
157
163
  // caches are fully iterable
158
- // largely intended for inspection, does not update recency
164
+ // largely intended for inspection only, does not update recency
159
165
  // btw. "foo" appears last since most recently accessed
160
166
  [...lru]
161
167
  // [ { k: 'bar', v: 42, s: 0 },
@@ -172,12 +178,12 @@ lru.delete("foo");
172
178
 
173
179
  // caches have a getSet() method to obtain & store a new value
174
180
  // if its key is not known. this process is asynchronous
175
- lru.getSet("boo", () => Promise.resolve(999)).then(console.log);
181
+ lru.getSet("boo", async () => 999).then(console.log);
176
182
  // 999
177
183
 
178
184
  // the given retrieval fn is only called if there's a cache miss
179
185
  // (not the case here). `getSet()` always returns a promise
180
- lru.getSet("boo", () => Promise.resolve(123)).then(console.log);
186
+ lru.getSet("boo", async () => 123).then(console.log);
181
187
  // 999
182
188
 
183
189
  // caches can be limited by size instead of (or in addition to)
@@ -187,13 +193,14 @@ lru.getSet("boo", () => Promise.resolve(123)).then(console.log);
187
193
  // we also provide a release hook for demo purposes
188
194
 
189
195
  // the first arg is an iterable of KV pairs to store (just as for Map)
190
- lru = new cache.LRUCache<string, number[]>(
196
+ lru = new LRUCache<string, number[]>(
191
197
  [ ["a", [1.0, 2.0]], ["b", [3.0, 4.0, 5.0]] ],
192
198
  {
193
199
  maxsize: 32,
194
200
  ksize: (k) => k.length,
195
201
  vsize: (v) => v.length * 8,
196
- release: (k, v) => console.log("release", k, v)
202
+ release: (k, v) => console.log("release", k, v),
203
+ update: (k, vold, vnew) => console.log("update", k, vold, "->", vnew)
197
204
  }
198
205
  );
199
206
  // release a [1, 2] ("a" is evicted due to maxsize constraint)
@@ -206,19 +213,18 @@ lru.size
206
213
 
207
214
  ### TLRU
208
215
 
209
- Time-aware LRU cache. Extends LRU strategy with TTL (time-to-live)
210
- values associated with each entry. `has()` will only return `true` and
211
- `get()` only returns a cached value if its TTL hasn't yet expired. When
212
- adding a new value to the cache, first removes expired entries and if
213
- there's still not sufficient space removes entries in LRU order. `set()`
214
- takes an optional entry specific `ttl` arg. If not given, uses the cache
215
- instance's default (provided via ctor option arg). If no instance TTL is
216
- given, TTL defaults to 1 hour.
216
+ Time-aware [LRU cache](#lru). Extends LRU strategy with TTL (time-to-live)
217
+ values associated with each entry. `has()` will only return `true` and `get()`
218
+ only returns a cached value if its TTL hasn't yet expired. When adding a new
219
+ value to the cache, first removes expired entries and if there's still not
220
+ sufficient space removes entries in LRU order. `set()` takes an optional entry
221
+ specific `ttl` arg. If not given, uses the cache instance's default (provided
222
+ via ctor option arg). If no instance TTL is given, TTL defaults to 1 hour.
217
223
 
218
224
  ```ts
219
225
  import { TLRUCache } from "@thi.ng/cache";
220
226
 
221
- // same opts as LRUCache, but here with custom TTL period (in ms)
227
+ // same opts as LRUCache, but here with custom default TTL period (in ms)
222
228
  tlru = new TLRUCache(null, { ttl: 10000 });
223
229
 
224
230
  // with item specific TTL (500ms)
@@ -227,7 +233,8 @@ tlru.set("foo", 42, 500)
227
233
 
228
234
  ### MRU
229
235
 
230
- Similar to LRU, but removes most recently accessed items first. [Wikipedia](https://en.wikipedia.org/wiki/Cache_replacement_policies#Most_recently_used_(MRU))
236
+ Similar to LRU, but removes most recently accessed items first.
237
+ [Wikipedia](https://en.wikipedia.org/wiki/Cache_replacement_policies#Most_recently_used_(MRU))
231
238
 
232
239
  ```ts
233
240
  import { MRUCache } from "@thi.ng/cache";
package/api.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Fn, Fn0, Fn2, ICopy, IEmpty, ILength, IRelease } from "@thi.ng/api";
1
+ import type { Fn, Fn0, Fn2, Fn3, ICopy, IEmpty, ILength, IRelease } from "@thi.ng/api";
2
2
  export interface ICache<K, V> extends Iterable<Readonly<[K, CacheEntry<K, V>]>>, ICopy<ICache<K, V>>, IEmpty<ICache<K, V>>, ILength, IRelease {
3
3
  readonly size: number;
4
4
  /**
@@ -57,20 +57,25 @@ export interface ICache<K, V> extends Iterable<Readonly<[K, CacheEntry<K, V>]>>,
57
57
  }
58
58
  export interface CacheOpts<K, V> {
59
59
  /**
60
- * Key size in arbitrary user defined units (must be same unit as given to
61
- * {@link CacheOpts.maxSize}).
60
+ * Function to compute the size of a given key in arbitrary user defined
61
+ * units (must be same unit as given to {@link CacheOpts.maxsize}).
62
62
  */
63
63
  ksize: Fn<K, number>;
64
64
  /**
65
- * Value size in arbitrary user defined units (must be same unit as given to
66
- * {@link CacheOpts.maxSize}).
65
+ * Function to compute the size of a given value in arbitrary user defined
66
+ * units (must be same unit as given to {@link CacheOpts.maxsize}).
67
67
  */
68
68
  vsize: Fn<V, number>;
69
69
  /**
70
- * Function to perform custom cleanup tasks when an item gets evicted from
70
+ * Callback function to perform custom tasks when an item gets evicted from
71
71
  * the cache.
72
72
  */
73
73
  release: Fn2<K, V, void>;
74
+ /**
75
+ * Callback function to perform custom tasks when an item gets updated in the cache.
76
+ * The function will be called with a key and its associated old and news values.
77
+ */
78
+ update: Fn3<K, V, V, void>;
74
79
  /**
75
80
  * Custom ES6 Map compatible implementation to use as the cache's backing
76
81
  * store.
package/lru.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Fn0 } from "@thi.ng/api";
1
+ import type { Fn0, Nullable } from "@thi.ng/api";
2
2
  import type { ConsCell } from "@thi.ng/dcons";
3
3
  import { DCons } from "@thi.ng/dcons/dcons";
4
4
  import type { CacheEntry, CacheOpts, ICache } from "./api.js";
@@ -7,7 +7,7 @@ export declare class LRUCache<K, V> implements ICache<K, V> {
7
7
  protected items: DCons<CacheEntry<K, V>>;
8
8
  protected opts: CacheOpts<K, V>;
9
9
  protected _size: number;
10
- constructor(pairs?: Iterable<[K, V]> | null, opts?: Partial<CacheOpts<K, V>>);
10
+ constructor(pairs: Nullable<Iterable<[K, V]>>, opts?: Partial<CacheOpts<K, V>>);
11
11
  get length(): number;
12
12
  get size(): number;
13
13
  [Symbol.iterator](): IterableIterator<readonly [K, CacheEntry<K, V>]>;
package/lru.js CHANGED
@@ -6,16 +6,14 @@ class LRUCache {
6
6
  opts;
7
7
  _size;
8
8
  constructor(pairs, opts) {
9
- const _opts = Object.assign(
10
- {
11
- maxlen: Infinity,
12
- maxsize: Infinity,
13
- map: () => /* @__PURE__ */ new Map(),
14
- ksize: () => 0,
15
- vsize: () => 0
16
- },
17
- opts
18
- );
9
+ const _opts = {
10
+ maxlen: Infinity,
11
+ maxsize: Infinity,
12
+ map: () => /* @__PURE__ */ new Map(),
13
+ ksize: () => 0,
14
+ vsize: () => 0,
15
+ ...opts
16
+ };
19
17
  this.map = _opts.map();
20
18
  this.items = new DCons();
21
19
  this._size = 0;
@@ -73,10 +71,7 @@ class LRUCache {
73
71
  }
74
72
  get(key, notFound) {
75
73
  const e = this.map.get(key);
76
- if (e) {
77
- return this.resetEntry(e);
78
- }
79
- return notFound;
74
+ return e ? this.resetEntry(e) : notFound;
80
75
  }
81
76
  set(key, value) {
82
77
  const size = this.opts.ksize(key) + this.opts.vsize(value);
@@ -113,16 +108,13 @@ class LRUCache {
113
108
  return e.value.v;
114
109
  }
115
110
  ensureSize() {
116
- const release = this.opts.release;
117
- const maxs = this.opts.maxsize;
118
- const maxl = this.opts.maxlen;
119
- while (this._size > maxs || this.length >= maxl) {
111
+ const { release, maxsize, maxlen } = this.opts;
112
+ while (this._size > maxsize || this.length >= maxlen) {
120
113
  const e = this.items.drop();
121
- if (!e) {
114
+ if (!e)
122
115
  return false;
123
- }
124
116
  this.map.delete(e.k);
125
- release && release(e.k, e.v);
117
+ release?.(e.k, e.v);
126
118
  this._size -= e.s;
127
119
  }
128
120
  return true;
@@ -131,11 +123,12 @@ class LRUCache {
131
123
  const ee = e.value;
132
124
  this.map.delete(ee.k);
133
125
  this.items.remove(e);
134
- this.opts.release && this.opts.release(ee.k, ee.v);
126
+ this.opts.release?.(ee.k, ee.v);
135
127
  this._size -= ee.s;
136
128
  }
137
129
  doSetEntry(e, k, v, s) {
138
130
  if (e) {
131
+ this.opts.update?.(k, e.value.v, v);
139
132
  e.value.v = v;
140
133
  e.value.s = s;
141
134
  this.items.asTail(e);
package/mru.d.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  import type { ConsCell } from "@thi.ng/dcons";
2
- import type { CacheEntry, CacheOpts } from "./api.js";
2
+ import type { CacheEntry } from "./api.js";
3
3
  import { LRUCache } from "./lru.js";
4
4
  export declare class MRUCache<K, V> extends LRUCache<K, V> {
5
- constructor(pairs?: Iterable<[K, V]> | null, opts?: Partial<CacheOpts<K, V>>);
6
5
  empty(): MRUCache<K, V>;
7
6
  protected resetEntry(e: ConsCell<CacheEntry<K, V>>): V;
8
7
  protected doSetEntry(e: ConsCell<CacheEntry<K, V>> | undefined, k: K, v: V, s: number): void;
package/mru.js CHANGED
@@ -1,8 +1,5 @@
1
1
  import { LRUCache } from "./lru.js";
2
2
  class MRUCache extends LRUCache {
3
- constructor(pairs, opts) {
4
- super(pairs, opts);
5
- }
6
3
  empty() {
7
4
  return new MRUCache(null, this.opts);
8
5
  }
@@ -12,6 +9,7 @@ class MRUCache extends LRUCache {
12
9
  }
13
10
  doSetEntry(e, k, v, s) {
14
11
  if (e) {
12
+ this.opts.update?.(k, e.value.v, v);
15
13
  e.value.v = v;
16
14
  e.value.s = s;
17
15
  this.items.asHead(e);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thi.ng/cache",
3
- "version": "2.1.106",
3
+ "version": "2.2.0",
4
4
  "description": "In-memory cache implementations with ES6 Map-like API and different eviction strategies",
5
5
  "type": "module",
6
6
  "module": "./index.js",
@@ -91,5 +91,5 @@
91
91
  ],
92
92
  "year": 2018
93
93
  },
94
- "gitHead": "aadddd399541d69266672a5815a3f28359a71425\n"
94
+ "gitHead": "8c206a7a1aa00822e5adedb67d922591648f7ced\n"
95
95
  }
package/tlru.d.ts CHANGED
@@ -1,8 +1,14 @@
1
- import type { Fn0 } from "@thi.ng/api";
1
+ import type { Fn0, Nullable } from "@thi.ng/api";
2
2
  import type { ConsCell, DCons } from "@thi.ng/dcons";
3
3
  import type { CacheEntry, CacheOpts } from "./api.js";
4
4
  import { LRUCache } from "./lru.js";
5
5
  export interface TLRUCacheOpts<K, V> extends CacheOpts<K, V> {
6
+ /**
7
+ * Default time-to-live (cache period in milliseconds) before an entry is
8
+ * considered expired.
9
+ *
10
+ * @defaultValue 3600000 (1 hour)
11
+ */
6
12
  ttl: number;
7
13
  }
8
14
  export interface TLRUCacheEntry<K, V> extends CacheEntry<K, V> {
@@ -26,14 +32,23 @@ export declare class TLRUCache<K, V> extends LRUCache<K, V> {
26
32
  protected opts: TLRUCacheOpts<K, V>;
27
33
  protected map: Map<K, ConsCell<TLRUCacheEntry<K, V>>>;
28
34
  protected items: DCons<TLRUCacheEntry<K, V>>;
29
- constructor(pairs?: Iterable<[K, V]> | null, opts?: Partial<TLRUCacheOpts<K, V>>);
35
+ constructor(pairs: Nullable<Iterable<[K, V]>>, opts?: Partial<TLRUCacheOpts<K, V>>);
30
36
  empty(): TLRUCache<K, V>;
31
37
  has(key: K): boolean;
32
38
  get(key: K, notFound?: V): V | undefined;
33
39
  set(key: K, value: V, ttl?: number): V;
34
40
  getSet(key: K, retrieve: Fn0<Promise<V>>, ttl?: number): Promise<V>;
35
- prune(): void;
41
+ /**
42
+ * Scans all cached entries and evicts any which are expired by now (based
43
+ * on their TTL). Does **not** modify last-accessed time of remaining
44
+ * entries. Returns number of entries evicted.
45
+ *
46
+ * @remarks
47
+ * For very large caches, it's recommended to call this function in a
48
+ * cron-like manner...
49
+ */
50
+ prune(): number;
36
51
  protected ensureSize(): boolean;
37
- protected doSetTlruEntry(e: ConsCell<TLRUCacheEntry<K, V>> | undefined, k: K, v: V, s: number, t: number): void;
52
+ protected doSetEntry(e: ConsCell<TLRUCacheEntry<K, V>> | undefined, k: K, v: V, s: number, ttl?: number): void;
38
53
  }
39
54
  //# sourceMappingURL=tlru.d.ts.map
package/tlru.js CHANGED
@@ -1,8 +1,7 @@
1
1
  import { LRUCache } from "./lru.js";
2
2
  class TLRUCache extends LRUCache {
3
3
  constructor(pairs, opts) {
4
- opts = Object.assign({ ttl: 60 * 60 * 1e3 }, opts);
5
- super(pairs, opts);
4
+ super(pairs, { ttl: 60 * 60 * 1e3, ...opts });
6
5
  }
7
6
  empty() {
8
7
  return new TLRUCache(null, this.opts);
@@ -26,36 +25,43 @@ class TLRUCache extends LRUCache {
26
25
  const additionalSize = Math.max(0, size - (e ? e.value.s : 0));
27
26
  this._size += additionalSize;
28
27
  if (this.ensureSize()) {
29
- const t = Date.now() + ttl;
30
- this.doSetTlruEntry(e, key, value, size, t);
28
+ this.doSetEntry(e, key, value, size, ttl);
31
29
  } else {
32
30
  this._size -= additionalSize;
33
31
  }
34
32
  return value;
35
33
  }
36
- getSet(key, retrieve, ttl = this.opts.ttl) {
34
+ async getSet(key, retrieve, ttl = this.opts.ttl) {
37
35
  const e = this.get(key);
38
- if (e) {
39
- return Promise.resolve(e);
40
- }
41
- return retrieve().then((v) => this.set(key, v, ttl));
36
+ return e !== void 0 ? e : this.set(key, await retrieve(), ttl);
42
37
  }
38
+ /**
39
+ * Scans all cached entries and evicts any which are expired by now (based
40
+ * on their TTL). Does **not** modify last-accessed time of remaining
41
+ * entries. Returns number of entries evicted.
42
+ *
43
+ * @remarks
44
+ * For very large caches, it's recommended to call this function in a
45
+ * cron-like manner...
46
+ */
43
47
  prune() {
44
48
  const now = Date.now();
45
49
  let cell = this.items.head;
50
+ let count = 0;
46
51
  while (cell) {
47
52
  if (cell.value.t < now) {
48
53
  this.removeEntry(cell);
54
+ count++;
49
55
  }
50
56
  cell = cell.next;
51
57
  }
58
+ return count;
52
59
  }
53
60
  ensureSize() {
54
- const maxs = this.opts.maxsize;
55
- const maxl = this.opts.maxlen;
61
+ const { maxlen, maxsize } = this.opts;
56
62
  const now = Date.now();
57
63
  let cell = this.items.head;
58
- while (cell && (this._size > maxs || this.length >= maxl)) {
64
+ while (cell && (this._size > maxsize || this.length >= maxlen)) {
59
65
  if (cell.value.t < now) {
60
66
  this.removeEntry(cell);
61
67
  }
@@ -63,8 +69,10 @@ class TLRUCache extends LRUCache {
63
69
  }
64
70
  return super.ensureSize();
65
71
  }
66
- doSetTlruEntry(e, k, v, s, t) {
72
+ doSetEntry(e, k, v, s, ttl = this.opts.ttl) {
73
+ const t = Date.now() + ttl;
67
74
  if (e) {
75
+ this.opts.update?.(k, e.value.v, v);
68
76
  e.value.v = v;
69
77
  e.value.s = s;
70
78
  e.value.t = t;