@adviser/cement 0.4.63 → 0.4.64

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.
Files changed (98) hide show
  1. package/cjs/index.cjs +1 -0
  2. package/cjs/index.cjs.map +1 -1
  3. package/cjs/index.d.ts +1 -0
  4. package/cjs/index.d.ts.map +1 -1
  5. package/cjs/keyed-ng.cjs +93 -0
  6. package/cjs/keyed-ng.cjs.map +1 -0
  7. package/cjs/keyed-ng.d.ts +51 -0
  8. package/cjs/keyed-ng.d.ts.map +1 -0
  9. package/cjs/keyed-ng.test.cjs +151 -0
  10. package/cjs/keyed-ng.test.cjs.map +1 -0
  11. package/cjs/keyed-ng.test.d.ts +2 -0
  12. package/cjs/keyed-ng.test.d.ts.map +1 -0
  13. package/cjs/lru-map-set.cjs +2 -1
  14. package/cjs/lru-map-set.cjs.map +1 -1
  15. package/cjs/lru-map-set.d.ts +4 -1
  16. package/cjs/lru-map-set.d.ts.map +1 -1
  17. package/cjs/resolve-once.cjs +67 -104
  18. package/cjs/resolve-once.cjs.map +1 -1
  19. package/cjs/resolve-once.d.ts +26 -62
  20. package/cjs/resolve-once.d.ts.map +1 -1
  21. package/cjs/resolve-once.test.cjs +119 -31
  22. package/cjs/resolve-once.test.cjs.map +1 -1
  23. package/cjs/version.cjs +1 -1
  24. package/deno.json +1 -1
  25. package/esm/index.d.ts +1 -0
  26. package/esm/index.d.ts.map +1 -1
  27. package/esm/index.js +1 -0
  28. package/esm/index.js.map +1 -1
  29. package/esm/keyed-ng.d.ts +51 -0
  30. package/esm/keyed-ng.d.ts.map +1 -0
  31. package/esm/keyed-ng.js +89 -0
  32. package/esm/keyed-ng.js.map +1 -0
  33. package/esm/keyed-ng.test.d.ts +2 -0
  34. package/esm/keyed-ng.test.d.ts.map +1 -0
  35. package/esm/keyed-ng.test.js +149 -0
  36. package/esm/keyed-ng.test.js.map +1 -0
  37. package/esm/lru-map-set.d.ts +4 -1
  38. package/esm/lru-map-set.d.ts.map +1 -1
  39. package/esm/lru-map-set.js +2 -1
  40. package/esm/lru-map-set.js.map +1 -1
  41. package/esm/resolve-once.d.ts +26 -62
  42. package/esm/resolve-once.d.ts.map +1 -1
  43. package/esm/resolve-once.js +66 -102
  44. package/esm/resolve-once.js.map +1 -1
  45. package/esm/resolve-once.test.js +119 -31
  46. package/esm/resolve-once.test.js.map +1 -1
  47. package/esm/version.js +1 -1
  48. package/package.json +3 -3
  49. package/src/index.ts +1 -0
  50. package/src/keyed-ng.ts +628 -0
  51. package/src/lru-map-set.ts +6 -2
  52. package/src/resolve-once.ts +281 -324
  53. package/ts/cjs/index.d.ts +1 -0
  54. package/ts/cjs/index.d.ts.map +1 -1
  55. package/ts/cjs/index.js +1 -0
  56. package/ts/cjs/index.js.map +1 -1
  57. package/ts/cjs/keyed-ng.d.ts +51 -0
  58. package/ts/cjs/keyed-ng.d.ts.map +1 -0
  59. package/ts/cjs/keyed-ng.js +93 -0
  60. package/ts/cjs/keyed-ng.js.map +1 -0
  61. package/ts/cjs/keyed-ng.test.d.ts +2 -0
  62. package/ts/cjs/keyed-ng.test.d.ts.map +1 -0
  63. package/ts/cjs/keyed-ng.test.js +151 -0
  64. package/ts/cjs/keyed-ng.test.js.map +1 -0
  65. package/ts/cjs/lru-map-set.d.ts +4 -1
  66. package/ts/cjs/lru-map-set.d.ts.map +1 -1
  67. package/ts/cjs/lru-map-set.js +2 -1
  68. package/ts/cjs/lru-map-set.js.map +1 -1
  69. package/ts/cjs/resolve-once.d.ts +26 -62
  70. package/ts/cjs/resolve-once.d.ts.map +1 -1
  71. package/ts/cjs/resolve-once.js +67 -104
  72. package/ts/cjs/resolve-once.js.map +1 -1
  73. package/ts/cjs/resolve-once.test.js +119 -31
  74. package/ts/cjs/resolve-once.test.js.map +1 -1
  75. package/ts/cjs/version.js +1 -1
  76. package/ts/esm/index.d.ts +1 -0
  77. package/ts/esm/index.d.ts.map +1 -1
  78. package/ts/esm/index.js +1 -0
  79. package/ts/esm/index.js.map +1 -1
  80. package/ts/esm/keyed-ng.d.ts +51 -0
  81. package/ts/esm/keyed-ng.d.ts.map +1 -0
  82. package/ts/esm/keyed-ng.js +89 -0
  83. package/ts/esm/keyed-ng.js.map +1 -0
  84. package/ts/esm/keyed-ng.test.d.ts +2 -0
  85. package/ts/esm/keyed-ng.test.d.ts.map +1 -0
  86. package/ts/esm/keyed-ng.test.js +149 -0
  87. package/ts/esm/keyed-ng.test.js.map +1 -0
  88. package/ts/esm/lru-map-set.d.ts +4 -1
  89. package/ts/esm/lru-map-set.d.ts.map +1 -1
  90. package/ts/esm/lru-map-set.js +2 -1
  91. package/ts/esm/lru-map-set.js.map +1 -1
  92. package/ts/esm/resolve-once.d.ts +26 -62
  93. package/ts/esm/resolve-once.d.ts.map +1 -1
  94. package/ts/esm/resolve-once.js +66 -102
  95. package/ts/esm/resolve-once.js.map +1 -1
  96. package/ts/esm/resolve-once.test.js +119 -31
  97. package/ts/esm/resolve-once.test.js.map +1 -1
  98. package/ts/esm/version.js +1 -1
@@ -0,0 +1,628 @@
1
+ /**
2
+ * Generic keyed factory and collection management with LRU caching support.
3
+ *
4
+ * This module provides a flexible system for managing keyed collections where values
5
+ * are created on-demand through factory functions. The KeyedNg class serves as a base
6
+ * for creating type-safe keyed collections with built-in LRU eviction support.
7
+ *
8
+ * ## Core Concepts
9
+ *
10
+ * - **KeyedNgItem**: A structured container holding the key, value, and context
11
+ * - **KeyedNg**: Base class for managing keyed collections with factory-based value creation
12
+ * - **KeyedIf**: Interface defining the contract for keyed collection implementations
13
+ *
14
+ * ## Features
15
+ *
16
+ * - Factory-based value creation on first access
17
+ * - Optional LRU caching with automatic eviction
18
+ * - Type-safe key-to-string conversion
19
+ * - Context passing for value creation
20
+ * - Event callbacks for set/delete operations
21
+ *
22
+ * @module keyed-ng
23
+ */
24
+
25
+ import { LRUParam, LRUMap, UnregFn } from "./lru-map-set.js";
26
+ import { toSortedObject } from "./utils/sorted-object.js";
27
+
28
+ /**
29
+ * Interface defining the contract for keyed collection implementations.
30
+ *
31
+ * Provides a common interface for managing keyed collections with lifecycle callbacks,
32
+ * LRU support, and flexible key handling. Implementations should provide type-safe
33
+ * access to values indexed by keys.
34
+ *
35
+ * @template ITEM - The item type stored in the collection (typically KeyedNgItem)
36
+ * @template V - The value type extracted from items
37
+ * @template K - The key type (defaults to string)
38
+ * @template CTX - The context type (defaults to unknown)
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * class MyKeyed implements KeyedIf<MyItem, MyValue, string, MyContext> {
43
+ * // Implementation...
44
+ * }
45
+ * ```
46
+ */
47
+ export interface KeyedIf<ITEM, V, K = string, CTX = unknown> {
48
+ /**
49
+ * Registers a callback that fires when a new entry is added to the map.
50
+ *
51
+ * @param fn - Callback function receiving key and value
52
+ * @returns Unregister function to remove the callback
53
+ */
54
+ onSet(fn: (value: ITEM) => void): UnregFn;
55
+
56
+ /**
57
+ * Registers a callback that fires when an entry is deleted from the map.
58
+ *
59
+ * @param fn - Callback function receiving key and value
60
+ * @returns Unregister function to remove the callback
61
+ */
62
+ onDelete(fn: (value: ITEM) => void): UnregFn;
63
+
64
+ /**
65
+ * Updates the LRU parameters of the underlying map.
66
+ *
67
+ * @param params - New parameters to apply
68
+ */
69
+ setParam(params: Partial<KeyedNgOptions<K, ITEM, CTX>>): void;
70
+
71
+ /**
72
+ * Async variant of get() that accepts a function returning a promise for the key.
73
+ *
74
+ * @param key - Function that returns a promise resolving to the key
75
+ * @returns Promise resolving to the value
76
+ */
77
+ asyncGet(key: () => Promise<K>): Promise<V>;
78
+
79
+ /**
80
+ * Gets or creates a value for the given key.
81
+ *
82
+ * If the key doesn't exist, creates a new instance using the factory function.
83
+ *
84
+ * @param key - The key or function returning the key
85
+ * @returns The value associated with the key
86
+ */
87
+ get(key: K | (() => K)): V;
88
+
89
+ getItem(key: K, ctx?: unknown): ITEM;
90
+
91
+ /**
92
+ * Checks if a key exists in the map.
93
+ *
94
+ * @param key - The key or function returning the key
95
+ * @returns True if the key exists
96
+ */
97
+ has(key: K | (() => K)): boolean;
98
+
99
+ /**
100
+ * Deletes an entry from the map.
101
+ *
102
+ * @param key - The key to delete
103
+ */
104
+ delete(key: K): void;
105
+
106
+ /**
107
+ * Returns all values in the map.
108
+ *
109
+ * @returns Array of all values
110
+ */
111
+ values(): ITEM[];
112
+
113
+ /**
114
+ * Returns all keys in the map.
115
+ *
116
+ * @returns Array of all keys
117
+ */
118
+ keys(): K[];
119
+
120
+ /**
121
+ * Iterates over all entries in the map.
122
+ *
123
+ * @yields Key-value pairs
124
+ */
125
+ forEach(fn: (v: ITEM, idx: number) => void): void;
126
+
127
+ entries(): Iterable<[ITEM, number]>;
128
+ }
129
+
130
+ /**
131
+ * Utility type that makes the 'value' property of a type writable.
132
+ *
133
+ * This is useful for internal implementations that need to construct items
134
+ * with initially undefined values that are later populated.
135
+ *
136
+ * @template T - The type to make writable
137
+ *
138
+ * @example
139
+ * ```typescript
140
+ * type ReadOnly = { readonly value: number };
141
+ * type Writable = WritableValue<ReadOnly>;
142
+ * // Result: { value: number }
143
+ * ```
144
+ */
145
+ export type WritableValue<T> = Omit<T, "value"> & { value: T extends { value: infer V } ? V : unknown };
146
+
147
+ /**
148
+ * Configuration options for creating a KeyedNg instance.
149
+ *
150
+ * @template K - The key type
151
+ * @template V - The value type
152
+ * @template CTX - The context type
153
+ *
154
+ * @example
155
+ * ```typescript
156
+ * const options: KeyedNgOptions<string, MyValue, MyContext> = {
157
+ * createValue: (item) => new MyValue(item.givenKey),
158
+ * key2string: (key) => key.toLowerCase(),
159
+ * ctx: { config: 'default' },
160
+ * lru: { max: 100 }
161
+ * };
162
+ * ```
163
+ */
164
+ export interface KeyedNgOptions<K, V, CTX> {
165
+ /**
166
+ * Factory function that creates a value for a given key.
167
+ *
168
+ * Called once per unique key when the value is first accessed.
169
+ * The function receives a KeyedNgItem with value initially undefined.
170
+ *
171
+ * @param keyItem - The item containing key, context, and placeholder for value
172
+ * @returns The created value instance
173
+ */
174
+ readonly createValue: (keyItem: KeyedNgItem<K, V, CTX>) => V;
175
+
176
+ /**
177
+ * Optional function to convert keys to strings for internal storage.
178
+ *
179
+ * If not provided, uses default conversion:
180
+ * - strings: as-is
181
+ * - numbers: toString()
182
+ * - booleans: "true" or "false"
183
+ * - objects: JSON.stringify with sorted keys
184
+ *
185
+ * @param key - The key to convert
186
+ * @returns A string representation of the key
187
+ */
188
+ readonly key2string?: (key: K) => string;
189
+
190
+ /**
191
+ * Optional default context passed to value creation.
192
+ *
193
+ * Can be overridden on a per-get basis. Defaults to empty object.
194
+ */
195
+ readonly ctx?: CTX;
196
+
197
+ /**
198
+ * Optional LRU cache configuration.
199
+ *
200
+ * When provided, the collection will automatically evict least-recently-used
201
+ * items when size limits are exceeded.
202
+ */
203
+ readonly lru?: Partial<LRUParam<unknown, string>>;
204
+ }
205
+
206
+ /**
207
+ * A structured item containing a key, value, and context.
208
+ *
209
+ * This is the fundamental container type used throughout the keyed-ng system.
210
+ * Items are created by KeyedNg and passed to factory functions and callbacks.
211
+ *
212
+ * @template K - The key type
213
+ * @template V - The value type
214
+ * @template CTX - The context type
215
+ *
216
+ * @example
217
+ * ```typescript
218
+ * const item: KeyedNgItem<string, number, MyContext> = {
219
+ * refKey: "user-123",
220
+ * givenKey: "user-123",
221
+ * value: 42,
222
+ * ctx: { config: 'production' }
223
+ * };
224
+ * ```
225
+ */
226
+ export interface KeyedNgItem<K, V, CTX> {
227
+ /**
228
+ * The normalized string key used for internal storage.
229
+ *
230
+ * This is the result of applying key2string to the givenKey.
231
+ */
232
+ readonly refKey: string;
233
+
234
+ /**
235
+ * The original key as provided by the caller.
236
+ *
237
+ * This preserves the original key type and value, even if it's
238
+ * been converted to a string for storage.
239
+ */
240
+ readonly givenKey: K;
241
+
242
+ /**
243
+ * The value associated with this key.
244
+ *
245
+ * Created by the factory function on first access.
246
+ */
247
+ readonly value: V;
248
+
249
+ /**
250
+ * The context provided when this item was created.
251
+ *
252
+ * Can be used to pass configuration or state to value factories.
253
+ */
254
+ readonly ctx: CTX;
255
+ }
256
+
257
+ /**
258
+ * Type helper that represents a KeyedNgItem without its value property.
259
+ *
260
+ * Useful for contexts where the value is not yet available or not needed,
261
+ * such as in callback signatures.
262
+ *
263
+ * @template K - The key type
264
+ * @template CTX - The context type
265
+ *
266
+ * @example
267
+ * ```typescript
268
+ * function process(item: KeyedNgItemWithoutValue<string, MyContext>) {
269
+ * console.log(item.refKey, item.givenKey, item.ctx);
270
+ * // item.value is not available
271
+ * }
272
+ * ```
273
+ */
274
+ export type KeyedNgItemWithoutValue<K, CTX> = Omit<KeyedNgItem<K, never, CTX>, "value">;
275
+
276
+ /**
277
+ * Generic keyed factory and collection with LRU caching support.
278
+ *
279
+ * KeyedNg manages a map of values indexed by keys, where values are created
280
+ * on-demand using a factory function. It provides type-safe key handling,
281
+ * optional LRU eviction, and lifecycle callbacks for monitoring changes.
282
+ *
283
+ * This class serves as the foundation for more specialized keyed collections
284
+ * like KeyedResolvOnce and KeyedResolvSeq.
285
+ *
286
+ * @template K - The key type (can be any type with string conversion)
287
+ * @template V - The value type created by the factory
288
+ * @template CTX - The context type passed to factory and callbacks
289
+ *
290
+ * @example
291
+ * ```typescript
292
+ * // Simple factory for objects
293
+ * const cache = new KeyedNg({
294
+ * createValue: (item) => ({
295
+ * id: item.refKey,
296
+ * created: Date.now()
297
+ * }),
298
+ * lru: { max: 100 }
299
+ * });
300
+ *
301
+ * const obj1 = cache.get('key1');
302
+ * const obj2 = cache.get('key2');
303
+ * ```
304
+ *
305
+ * @example
306
+ * ```typescript
307
+ * // With complex keys and context
308
+ * interface UserKey { org: string; userId: string; }
309
+ * interface UserContext { apiKey: string; }
310
+ *
311
+ * const users = new KeyedNg<UserKey, User, UserContext>({
312
+ * createValue: (item) => new User(item.givenKey, item.ctx),
313
+ * key2string: (key) => `${key.org}:${key.userId}`,
314
+ * ctx: { apiKey: 'default' }
315
+ * });
316
+ *
317
+ * const user = users.get(
318
+ * { org: 'acme', userId: '123' },
319
+ * { apiKey: 'custom' }
320
+ * );
321
+ * ```
322
+ */
323
+ export class KeyedNg<K, V, CTX> implements KeyedIf<KeyedNgItem<K, V, CTX>, V, K, CTX> {
324
+ /**
325
+ * The resolved options with defaults applied.
326
+ * @internal
327
+ */
328
+ readonly opts: Required<Omit<KeyedNgOptions<K, V, CTX>, "lru">>;
329
+
330
+ /**
331
+ * Internal LRU map for storing items.
332
+ * @internal
333
+ */
334
+ readonly #map: LRUMap<string, KeyedNgItem<K, V, CTX>>;
335
+
336
+ /**
337
+ * Creates a new KeyedNg instance.
338
+ *
339
+ * @param opts - Configuration options
340
+ */
341
+ constructor(opts: KeyedNgOptions<K, V, CTX>) {
342
+ this.opts = {
343
+ ...opts,
344
+ key2string:
345
+ opts.key2string ??
346
+ ((key: K): string => {
347
+ if (typeof key === "string") {
348
+ return key;
349
+ }
350
+ if (typeof key === "number") {
351
+ return key.toString();
352
+ }
353
+ if (typeof key === "boolean") {
354
+ return key ? "true" : "false";
355
+ }
356
+ return JSON.stringify(toSortedObject(key as unknown as Record<string, unknown>));
357
+ }),
358
+ ctx: opts.ctx ?? ({} as CTX),
359
+ };
360
+ this.#map = new LRUMap(opts.lru as LRUParam<KeyedNgItem<K, V, CTX>, string>);
361
+ }
362
+
363
+ /**
364
+ * Registers a callback that fires when a new item is added to the collection.
365
+ *
366
+ * The callback is invoked after the item is created and stored. Multiple
367
+ * callbacks can be registered.
368
+ *
369
+ * @param fn - Callback function receiving the new item
370
+ * @returns Unregister function to remove the callback
371
+ *
372
+ * @example
373
+ * ```typescript
374
+ * const unregister = keyed.onSet((item) => {
375
+ * console.log('Added:', item.givenKey, item.value);
376
+ * });
377
+ *
378
+ * // Later: remove the callback
379
+ * unregister();
380
+ * ```
381
+ */
382
+ onSet(fn: (value: KeyedNgItem<K, V, CTX>) => void): UnregFn {
383
+ return this.#map.onSet((_keyStr: string, item: KeyedNgItem<K, V, CTX>) => {
384
+ fn(item);
385
+ });
386
+ }
387
+
388
+ /**
389
+ * Registers a callback that fires when an item is deleted from the collection.
390
+ *
391
+ * The callback is invoked before the item is removed. This includes both
392
+ * explicit deletions and LRU evictions.
393
+ *
394
+ * @param fn - Callback function receiving the deleted item
395
+ * @returns Unregister function to remove the callback
396
+ *
397
+ * @example
398
+ * ```typescript
399
+ * const unregister = keyed.onDelete((item) => {
400
+ * console.log('Removed:', item.givenKey);
401
+ * item.value.cleanup?.(); // Perform cleanup if needed
402
+ * });
403
+ * ```
404
+ */
405
+ onDelete(fn: (value: KeyedNgItem<K, V, CTX>) => void): UnregFn {
406
+ return this.#map.onDelete((_keyStr: string, item: KeyedNgItem<K, V, CTX>) => {
407
+ fn(item);
408
+ });
409
+ }
410
+
411
+ /**
412
+ * Updates the LRU parameters of the underlying collection.
413
+ *
414
+ * Allows dynamic adjustment of caching behavior without recreating
415
+ * the entire collection.
416
+ *
417
+ * @param params - New parameters to apply (partial update)
418
+ *
419
+ * @example
420
+ * ```typescript
421
+ * keyed.setParam({ lru: { max: 200 } });
422
+ * ```
423
+ */
424
+ setParam(params: Partial<KeyedNgOptions<K, KeyedNgItem<K, V, CTX>, CTX>>): void {
425
+ this.#map.setParam(params.lru as LRUParam<KeyedNgItem<K, V, CTX>, string>);
426
+ }
427
+
428
+ /**
429
+ * Asynchronously gets or creates a value for a key resolved from a promise.
430
+ *
431
+ * Useful when the key itself needs to be computed asynchronously, such as
432
+ * from a database lookup or API call.
433
+ *
434
+ * @param key - Function returning a promise that resolves to the key
435
+ * @returns Promise resolving to the value
436
+ *
437
+ * @example
438
+ * ```typescript
439
+ * const value = await keyed.asyncGet(async () => {
440
+ * const id = await fetchUserId();
441
+ * return id;
442
+ * });
443
+ * ```
444
+ */
445
+ asyncGet(key: () => Promise<K>): Promise<V> {
446
+ return key().then((k) => this.get(k));
447
+ }
448
+
449
+ /**
450
+ * Checks if a key exists in the collection.
451
+ *
452
+ * Does not create a new value if the key doesn't exist.
453
+ *
454
+ * @param keyOfFnKey - The key or function returning the key
455
+ * @returns True if the key exists in the collection
456
+ *
457
+ * @example
458
+ * ```typescript
459
+ * if (keyed.has('myKey')) {
460
+ * console.log('Key exists');
461
+ * }
462
+ *
463
+ * // With function
464
+ * if (keyed.has(() => computeKey())) {
465
+ * console.log('Computed key exists');
466
+ * }
467
+ * ```
468
+ */
469
+ has(keyOfFnKey: K | (() => K)): boolean {
470
+ if (typeof keyOfFnKey === "function") {
471
+ keyOfFnKey = (keyOfFnKey as () => K)();
472
+ }
473
+ return this.#map.has(this.opts.key2string(keyOfFnKey));
474
+ }
475
+
476
+ /**
477
+ * Deletes an item from the collection.
478
+ *
479
+ * Triggers onDelete callbacks before removal. The item will need to be
480
+ * recreated if accessed again.
481
+ *
482
+ * @param key - The key to delete
483
+ *
484
+ * @example
485
+ * ```typescript
486
+ * keyed.delete('myKey');
487
+ * ```
488
+ */
489
+ delete(key: K): void {
490
+ this.#map.delete(this.opts.key2string(key));
491
+ }
492
+
493
+ /**
494
+ * Returns all keys currently in the collection.
495
+ *
496
+ * Returns the original keys (givenKey), not the normalized string versions.
497
+ *
498
+ * @returns Array of all keys
499
+ *
500
+ * @example
501
+ * ```typescript
502
+ * const keys = keyed.keys();
503
+ * console.log(keys); // ['key1', 'key2', 'key3']
504
+ * ```
505
+ */
506
+ keys(): K[] {
507
+ return Array.from(this.#map.entries()).map(([_, item]) => item.givenKey);
508
+ }
509
+
510
+ /**
511
+ * Iterates over all items in the collection.
512
+ *
513
+ * The callback receives each item and its index.
514
+ *
515
+ * @param fn - Callback function receiving item and index
516
+ *
517
+ * @example
518
+ * ```typescript
519
+ * keyed.forEach((item, idx) => {
520
+ * console.log(idx, item.givenKey, item.value);
521
+ * });
522
+ * ```
523
+ */
524
+ forEach(fn: (v: KeyedNgItem<K, V, CTX>, idx: number) => void): void {
525
+ return this.#map.forEach((item, _, ctx) => {
526
+ fn(item, ctx.idx);
527
+ });
528
+ }
529
+
530
+ /**
531
+ * Returns an iterable of all items with their indices.
532
+ *
533
+ * @returns Iterable of [item, index] pairs
534
+ *
535
+ * @example
536
+ * ```typescript
537
+ * for (const [item, idx] of keyed.entries()) {
538
+ * console.log(idx, item.givenKey, item.value);
539
+ * }
540
+ * ```
541
+ */
542
+ entries(): Iterable<[KeyedNgItem<K, V, CTX>, number]> {
543
+ let idx = 0;
544
+ return Array.from(this.#map.entries()).map(([_, item]) => [item, idx++]);
545
+ }
546
+
547
+ /**
548
+ * Gets or creates the full KeyedNgItem for a given key.
549
+ *
550
+ * Returns the complete item structure including key, value, and context.
551
+ * If the key doesn't exist, creates it using the factory function.
552
+ *
553
+ * @param key - The key to get
554
+ * @param ctx - Optional context override for this get operation
555
+ * @returns The complete KeyedNgItem
556
+ *
557
+ * @example
558
+ * ```typescript
559
+ * const item = keyed.getItem('myKey', { custom: 'context' });
560
+ * console.log(item.refKey, item.givenKey, item.value, item.ctx);
561
+ * ```
562
+ */
563
+ getItem(key: K, ctx?: CTX): KeyedNgItem<K, V, CTX> {
564
+ const keyStr = this.opts.key2string(key);
565
+ let item = this.#map.get(keyStr);
566
+ if (!item) {
567
+ item = {
568
+ refKey: keyStr,
569
+ givenKey: key,
570
+ ctx: ctx ?? this.opts.ctx,
571
+ value: undefined as unknown as V,
572
+ };
573
+ (item as { value: V }).value = this.opts.createValue(item);
574
+ this.#map.set(keyStr, item);
575
+ }
576
+ return item;
577
+ }
578
+
579
+ /**
580
+ * Gets or creates a value for the given key.
581
+ *
582
+ * This is the primary method for accessing values. If the key doesn't exist,
583
+ * the factory function is called to create the value. Subsequent calls with
584
+ * the same key return the cached value.
585
+ *
586
+ * @param key - The key or function returning the key
587
+ * @param ctx - Optional context override for this get operation
588
+ * @returns The value associated with the key
589
+ *
590
+ * @example
591
+ * ```typescript
592
+ * // Simple key
593
+ * const value1 = keyed.get('myKey');
594
+ *
595
+ * // With function
596
+ * const value2 = keyed.get(() => computeKey());
597
+ *
598
+ * // With custom context
599
+ * const value3 = keyed.get('myKey', { custom: 'context' });
600
+ * ```
601
+ */
602
+ get(key: K | (() => K), ctx?: CTX): V {
603
+ if (typeof key === "function") {
604
+ key = (key as () => K)();
605
+ }
606
+ const item = this.getItem(key, ctx);
607
+ return item.value;
608
+ }
609
+
610
+ /**
611
+ * Returns all items currently in the collection.
612
+ *
613
+ * Returns complete KeyedNgItem objects, not just the values.
614
+ *
615
+ * @returns Array of all items
616
+ *
617
+ * @example
618
+ * ```typescript
619
+ * const items = keyed.values();
620
+ * items.forEach(item => {
621
+ * console.log(item.givenKey, item.value);
622
+ * });
623
+ * ```
624
+ */
625
+ values(): KeyedNgItem<K, V, CTX>[] {
626
+ return Array.from(this.#map.entries()).map(([_, item]) => item);
627
+ }
628
+ }
@@ -70,6 +70,8 @@ export class LRUSet<T> {
70
70
  }
71
71
  }
72
72
 
73
+ export type LRUWithIdx<T> = T & { readonly idx: number };
74
+
73
75
  export interface LRUCtx<T, K> {
74
76
  readonly update: boolean;
75
77
  readonly ref: LRUMap<K, T>;
@@ -287,9 +289,11 @@ export class LRUMap<K, V> {
287
289
  this._map.clear();
288
290
  }
289
291
 
290
- forEach(fn: (value: V, key: K, ctx: LRUCtx<V, K>) => void): void {
292
+ forEach(fn: (value: V, key: K, ctx: LRUWithIdx<LRUCtx<V, K>>) => void): void {
293
+ let idx = 0;
291
294
  this._map.forEach((v, k) => {
292
- fn(v.value, k, this.buildItemCtx(v, false));
295
+ // not really efficient but ok for now
296
+ fn(v.value, k, { ...this.buildItemCtx(v, false), idx: idx++ });
293
297
  });
294
298
  }
295
299