@mark1russell7/collections 0.8.0 → 0.9.1
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 +10 -1
- package/build/index.d.ts +77 -6
- package/build/index.d.ts.map +1 -1
- package/build/index.js +184 -31
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/async/channels.ts +26 -0
- package/src/behaviors/bounded.ts +24 -6
- package/src/behaviors/evented.ts +75 -25
- package/src/behaviors/lru.ts +18 -22
- package/src/behaviors/ttl.ts +12 -0
- package/src/core/removals.ts +85 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@mark1russell7/collections",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.9.1",
|
|
5
5
|
"description": "Collections with no dependencies: lists, maps, queues, a summary tree, channels, lazy operations and dependency graphs.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"collections",
|
package/src/async/channels.ts
CHANGED
|
@@ -27,6 +27,8 @@ import { asyncQueue, type AsyncQueue } from "./async-queue.js";
|
|
|
27
27
|
*
|
|
28
28
|
* The channels of Go are the model. This class gives send/receive operations
|
|
29
29
|
* that can block until the other side is ready.
|
|
30
|
+
*
|
|
31
|
+
* @deprecated Import `Channel` from `@mark1russell7/service/channel`. The next MINOR version of collections removes it.
|
|
30
32
|
*/
|
|
31
33
|
export class Channel<T> {
|
|
32
34
|
private queue: AsyncQueue<T>;
|
|
@@ -140,6 +142,8 @@ export class Channel<T> {
|
|
|
140
142
|
|
|
141
143
|
/**
|
|
142
144
|
* SelectCase represents a case in a select statement.
|
|
145
|
+
*
|
|
146
|
+
* @deprecated Import `SelectCase` from `@mark1russell7/service/channel`. The next MINOR version of collections removes it.
|
|
143
147
|
*/
|
|
144
148
|
export interface SelectCase<R> {
|
|
145
149
|
channel: Channel<any>;
|
|
@@ -212,6 +216,8 @@ function firstOf(waits: readonly Wait[]): Promise<{ index: number; value: unknow
|
|
|
212
216
|
* ch2.case((v) => `ch2: ${v}`),
|
|
213
217
|
* timeout(1000).case(() => 'timeout')
|
|
214
218
|
* )
|
|
219
|
+
*
|
|
220
|
+
* @deprecated Import `select` from `@mark1russell7/service/channel`. The next MINOR version of collections removes it.
|
|
215
221
|
*/
|
|
216
222
|
export async function select<R>(...cases: SelectCase<R>[]): Promise<R> {
|
|
217
223
|
const { index, value } = await firstOf(
|
|
@@ -229,6 +235,8 @@ export async function select<R>(...cases: SelectCase<R>[]): Promise<R> {
|
|
|
229
235
|
* dataChannel.case((v) => v),
|
|
230
236
|
* timeout(5000).case(() => 'timeout!')
|
|
231
237
|
* )
|
|
238
|
+
*
|
|
239
|
+
* @deprecated Import `timeout` from `@mark1russell7/service/channel`. The next MINOR version of collections removes it.
|
|
232
240
|
*/
|
|
233
241
|
export function timeout(ms: number): Channel<void> {
|
|
234
242
|
const ch = new Channel<void>();
|
|
@@ -247,6 +255,8 @@ export function timeout(ms: number): Channel<void> {
|
|
|
247
255
|
* for await (const t of tick) {
|
|
248
256
|
* console.log('Tick:', t)
|
|
249
257
|
* }
|
|
258
|
+
*
|
|
259
|
+
* @deprecated Import `ticker` from `@mark1russell7/service/channel`. The next MINOR version of collections removes it.
|
|
250
260
|
*/
|
|
251
261
|
export function ticker(ms: number): Channel<number> & { stop: () => void } {
|
|
252
262
|
const ch = new Channel<number>(1);
|
|
@@ -273,6 +283,8 @@ export function ticker(ms: number): Channel<number> & { stop: () => void } {
|
|
|
273
283
|
*
|
|
274
284
|
* await input.send(5)
|
|
275
285
|
* const result = await output.receive() // 10
|
|
286
|
+
*
|
|
287
|
+
* @deprecated Import `pipeline` from `@mark1russell7/service/channel`. The next MINOR version of collections removes it.
|
|
276
288
|
*/
|
|
277
289
|
export function pipeline<T, U>(
|
|
278
290
|
input: Channel<T>,
|
|
@@ -305,6 +317,8 @@ export function pipeline<T, U>(
|
|
|
305
317
|
* const [out1, out2, out3] = fanOut(input, 3)
|
|
306
318
|
*
|
|
307
319
|
* // Each output gets a copy of every input value
|
|
320
|
+
*
|
|
321
|
+
* @deprecated Import `fanOut` from `@mark1russell7/service/channel`. The next MINOR version of collections removes it.
|
|
308
322
|
*/
|
|
309
323
|
export function fanOut<T>(input: Channel<T>, count: number, bufferSize: number = 0): Channel<T>[] {
|
|
310
324
|
const outputs = Array.from({ length: count }, () => new Channel<T>(bufferSize));
|
|
@@ -331,6 +345,8 @@ export function fanOut<T>(input: Channel<T>, count: number, bufferSize: number =
|
|
|
331
345
|
* const merged = fanIn(ch1, ch2)
|
|
332
346
|
*
|
|
333
347
|
* // merged receives values from both ch1 and ch2
|
|
348
|
+
*
|
|
349
|
+
* @deprecated Import `fanIn` from `@mark1russell7/service/channel`. The next MINOR version of collections removes it.
|
|
334
350
|
*/
|
|
335
351
|
export function fanIn<T>(...inputs: Channel<T>[]): Channel<T> {
|
|
336
352
|
const output = new Channel<T>();
|
|
@@ -356,6 +372,8 @@ export function fanIn<T>(...inputs: Channel<T>[]): Channel<T> {
|
|
|
356
372
|
* It takes one value at a time, from the inputs in turn. It keeps no receive on an input while
|
|
357
373
|
* it waits for the reader of the output. Thus each value of each input reaches the output one
|
|
358
374
|
* time, in the order of its input, or stays in its input.
|
|
375
|
+
*
|
|
376
|
+
* @deprecated Import `merge` from `@mark1russell7/service/channel`. The next MINOR version of collections removes it.
|
|
359
377
|
*/
|
|
360
378
|
export function merge<T>(...inputs: Channel<T>[]): Channel<T> {
|
|
361
379
|
const output = new Channel<T>();
|
|
@@ -389,6 +407,8 @@ export function merge<T>(...inputs: Channel<T>[]): Channel<T> {
|
|
|
389
407
|
|
|
390
408
|
/**
|
|
391
409
|
* Buffered channel with specified capacity.
|
|
410
|
+
*
|
|
411
|
+
* @deprecated Import `buffered` from `@mark1russell7/service/channel`. The next MINOR version of collections removes it.
|
|
392
412
|
*/
|
|
393
413
|
export function buffered<T>(capacity: number): Channel<T> {
|
|
394
414
|
return new Channel<T>(capacity);
|
|
@@ -397,6 +417,8 @@ export function buffered<T>(capacity: number): Channel<T> {
|
|
|
397
417
|
/**
|
|
398
418
|
* Unbuffered channel (rendezvous point).
|
|
399
419
|
* Sends block until receiver is ready.
|
|
420
|
+
*
|
|
421
|
+
* @deprecated Import `unbuffered` from `@mark1russell7/service/channel`. The next MINOR version of collections removes it.
|
|
400
422
|
*/
|
|
401
423
|
export function unbuffered<T>(): Channel<T> {
|
|
402
424
|
return new Channel<T>(0);
|
|
@@ -408,6 +430,8 @@ export function unbuffered<T>(): Channel<T> {
|
|
|
408
430
|
* @example
|
|
409
431
|
* const ch = channel<number>()
|
|
410
432
|
* const bufferedCh = channel<string>(10)
|
|
433
|
+
*
|
|
434
|
+
* @deprecated Import `channel` from `@mark1russell7/service/channel`. The next MINOR version of collections removes it.
|
|
411
435
|
*/
|
|
412
436
|
export function channel<T>(bufferSize: number = 0): Channel<T> {
|
|
413
437
|
return new Channel<T>(bufferSize);
|
|
@@ -424,6 +448,8 @@ export function channel<T>(bufferSize: number = 0): Channel<T> {
|
|
|
424
448
|
* )
|
|
425
449
|
*
|
|
426
450
|
* // 4 workers concurrently process jobs from jobChannel
|
|
451
|
+
*
|
|
452
|
+
* @deprecated Import `workerPool` from `@mark1russell7/service/channel`. The next MINOR version of collections removes it.
|
|
427
453
|
*/
|
|
428
454
|
export function workerPool<T, R>(
|
|
429
455
|
jobs: Channel<T>,
|
package/src/behaviors/bounded.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type { Middleware } from "../core/middleware.js";
|
|
|
20
20
|
import type { List } from "../interfaces/list.js";
|
|
21
21
|
import type { Queue, Deque } from "../interfaces/queue.js";
|
|
22
22
|
import type { MapLike } from "../interfaces/map.js";
|
|
23
|
+
import { removals, removalSource, type ElementRemoval, type MapRemoval } from "../core/removals.js";
|
|
23
24
|
import type { Collection } from "../interfaces/collection.js";
|
|
24
25
|
import type {
|
|
25
26
|
OverflowPolicy,
|
|
@@ -71,6 +72,7 @@ export function boundedList<T>(
|
|
|
71
72
|
const capacity = checkCapacity(options.capacity, "boundedList");
|
|
72
73
|
|
|
73
74
|
return (next: List<T>): List<T> & BoundedCollection => {
|
|
75
|
+
const source = removalSource<ElementRemoval<T>>(next);
|
|
74
76
|
// An unshift puts the element at the front, so drop-oldest drops the last element for it
|
|
75
77
|
const handleOverflow = (element: T, dropFrom: "first" | "last" = "first"): void => {
|
|
76
78
|
// Call overflow handler if provided
|
|
@@ -92,8 +94,8 @@ export function boundedList<T>(
|
|
|
92
94
|
case "drop-oldest":
|
|
93
95
|
// Remove one element: the first, or the last for an unshift
|
|
94
96
|
if (!next.isEmpty) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
+
const value = dropFrom === "last" ? next.pop() : next.shift();
|
|
98
|
+
source.emit({ value, end: dropFrom, reason: "drop" });
|
|
97
99
|
}
|
|
98
100
|
break;
|
|
99
101
|
|
|
@@ -123,6 +125,7 @@ export function boundedList<T>(
|
|
|
123
125
|
return new Proxy(next, {
|
|
124
126
|
get(target, prop, receiver) {
|
|
125
127
|
// Capacity property
|
|
128
|
+
if (prop === removals) return source.channel;
|
|
126
129
|
if (prop === "capacity") {
|
|
127
130
|
return capacity;
|
|
128
131
|
}
|
|
@@ -254,6 +257,7 @@ export function boundedQueue<T>(
|
|
|
254
257
|
const capacity = checkCapacity(options.capacity, "boundedQueue");
|
|
255
258
|
|
|
256
259
|
return (next: Queue<T>): Queue<T> & BoundedCollection => {
|
|
260
|
+
const source = removalSource<ElementRemoval<T>>(next);
|
|
257
261
|
const handleOverflow = (element: T): void => {
|
|
258
262
|
if (onOverflow) {
|
|
259
263
|
const context: OverflowContext<T> = {
|
|
@@ -270,7 +274,7 @@ export function boundedQueue<T>(
|
|
|
270
274
|
throw new Error(`Queue is full (capacity: ${capacity})`);
|
|
271
275
|
case "drop-oldest":
|
|
272
276
|
if (!next.isEmpty) {
|
|
273
|
-
next.poll();
|
|
277
|
+
source.emit({ value: next.poll() as T, end: "first", reason: "drop" });
|
|
274
278
|
}
|
|
275
279
|
break;
|
|
276
280
|
case "drop-newest":
|
|
@@ -287,6 +291,7 @@ export function boundedQueue<T>(
|
|
|
287
291
|
|
|
288
292
|
return new Proxy(next, {
|
|
289
293
|
get(target, prop, receiver) {
|
|
294
|
+
if (prop === removals) return source.channel;
|
|
290
295
|
if (prop === "capacity") return capacity;
|
|
291
296
|
if (prop === "isFull") return target.size >= capacity;
|
|
292
297
|
if (prop === "remainingCapacity") return Math.max(0, capacity - target.size);
|
|
@@ -332,6 +337,7 @@ export function boundedDeque<T>(
|
|
|
332
337
|
const capacity = checkCapacity(options.capacity, "boundedDeque");
|
|
333
338
|
|
|
334
339
|
return (next: Deque<T>): Deque<T> & BoundedCollection => {
|
|
340
|
+
const source = removalSource<ElementRemoval<T>>(next);
|
|
335
341
|
// An insert at the front drops the last element, as unshift of boundedList does (C10)
|
|
336
342
|
const handleOverflow = (element: T, dropFrom: "first" | "last"): void => {
|
|
337
343
|
if (onOverflow) {
|
|
@@ -349,8 +355,8 @@ export function boundedDeque<T>(
|
|
|
349
355
|
throw new Error(`Deque is full (capacity: ${capacity})`);
|
|
350
356
|
case "drop-oldest":
|
|
351
357
|
if (!next.isEmpty) {
|
|
352
|
-
|
|
353
|
-
|
|
358
|
+
const value = dropFrom === "last" ? next.removeLast() : next.removeFirst();
|
|
359
|
+
source.emit({ value, end: dropFrom, reason: "drop" });
|
|
354
360
|
}
|
|
355
361
|
break;
|
|
356
362
|
case "drop-newest":
|
|
@@ -367,6 +373,7 @@ export function boundedDeque<T>(
|
|
|
367
373
|
|
|
368
374
|
return new Proxy(next, {
|
|
369
375
|
get(target, prop, receiver) {
|
|
376
|
+
if (prop === removals) return source.channel;
|
|
370
377
|
if (prop === "capacity") return capacity;
|
|
371
378
|
if (prop === "isFull") return target.size >= capacity;
|
|
372
379
|
if (prop === "remainingCapacity") return Math.max(0, capacity - target.size);
|
|
@@ -419,6 +426,10 @@ export function boundedMap<K, V>(
|
|
|
419
426
|
|
|
420
427
|
return (next: MapLike<K, V>): MapLike<K, V> & BoundedCollection => {
|
|
421
428
|
let insertionOrder: K[] = []; // Track insertion order for drop-oldest
|
|
429
|
+
// A removal below (for example an eviction of lruMap) takes its key out of the order
|
|
430
|
+
const source = removalSource<MapRemoval<K, V>>(next, ({ key }) => {
|
|
431
|
+
insertionOrder = insertionOrder.filter((k) => !next.keyEq(k, key));
|
|
432
|
+
});
|
|
422
433
|
|
|
423
434
|
const handleOverflow = (key: K, value: V): void => {
|
|
424
435
|
if (onOverflow) {
|
|
@@ -436,9 +447,15 @@ export function boundedMap<K, V>(
|
|
|
436
447
|
throw new Error(`Map is full (capacity: ${capacity})`);
|
|
437
448
|
case "drop-oldest":
|
|
438
449
|
// Remove oldest key
|
|
439
|
-
|
|
450
|
+
// The removal channel below keeps the order exact. A layer below with no channel can remove a key
|
|
451
|
+
// without this layer seeing it, so a drop skips each key that is gone.
|
|
452
|
+
while (insertionOrder.length > 0) {
|
|
440
453
|
const oldestKey = insertionOrder.shift()!;
|
|
454
|
+
if (!next.has(oldestKey)) continue;
|
|
455
|
+
const value = next.get(oldestKey);
|
|
441
456
|
next.delete(oldestKey);
|
|
457
|
+
source.emit({ key: oldestKey, value, reason: "drop" });
|
|
458
|
+
break;
|
|
442
459
|
}
|
|
443
460
|
break;
|
|
444
461
|
case "drop-newest":
|
|
@@ -455,6 +472,7 @@ export function boundedMap<K, V>(
|
|
|
455
472
|
|
|
456
473
|
return new Proxy(next, {
|
|
457
474
|
get(target, prop, receiver) {
|
|
475
|
+
if (prop === removals) return source.channel;
|
|
458
476
|
if (prop === "capacity") return capacity;
|
|
459
477
|
if (prop === "isFull") return target.size >= capacity;
|
|
460
478
|
if (prop === "remainingCapacity") return Math.max(0, capacity - target.size);
|
package/src/behaviors/evented.ts
CHANGED
|
@@ -22,6 +22,31 @@ import type { Queue, Deque } from "../interfaces/queue.js";
|
|
|
22
22
|
import type { MapLike } from "../interfaces/map.js";
|
|
23
23
|
import type { Collection } from "../interfaces/collection.js";
|
|
24
24
|
import { Emitter, type Unsubscribe } from "../core/events.js";
|
|
25
|
+
import { removalsOf, type ElementRemoval, type MapRemoval, type RemovalReason } from "../core/removals.js";
|
|
26
|
+
|
|
27
|
+
/** The number of removals that a layer heard from the layers below it, and the number from the first end. */
|
|
28
|
+
interface Heard {
|
|
29
|
+
all: number;
|
|
30
|
+
first: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* This function gives each removal below `next` to `listener` and counts the removals.
|
|
35
|
+
* With the counts, a layer tells a refused add (the size did not grow) from an add that caused a drop.
|
|
36
|
+
*/
|
|
37
|
+
function hear<R extends ElementRemoval<unknown> | MapRemoval<unknown, unknown>>(next: object, listener: (removal: R) => void): Heard {
|
|
38
|
+
const heard: Heard = { all: 0, first: 0 };
|
|
39
|
+
removalsOf<R>(next)?.on((removal) => {
|
|
40
|
+
heard.all++;
|
|
41
|
+
if ("end" in removal && removal.end === "first") heard.first++;
|
|
42
|
+
listener(removal);
|
|
43
|
+
});
|
|
44
|
+
return heard;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** This function tells if a call added an element: the size grew, after the removals of the layers below. */
|
|
48
|
+
const added = (size: number, sizeBefore: number, heard: Heard, heardBefore: number): boolean =>
|
|
49
|
+
size - sizeBefore + heard.all - heardBefore > 0;
|
|
25
50
|
|
|
26
51
|
// ============================================================================
|
|
27
52
|
// Event type definitions
|
|
@@ -33,8 +58,8 @@ import { Emitter, type Unsubscribe } from "../core/events.js";
|
|
|
33
58
|
export interface ListEvents<T> {
|
|
34
59
|
/** Element was added */
|
|
35
60
|
add: { value: T; index?: number };
|
|
36
|
-
/** Element was removed */
|
|
37
|
-
remove: { value: T; index?: number };
|
|
61
|
+
/** Element was removed. A removal by a layer below (a drop) has a `reason`. */
|
|
62
|
+
remove: { value: T; index?: number; reason?: RemovalReason };
|
|
38
63
|
/** Element was replaced */
|
|
39
64
|
set: { oldValue: T; newValue: T; index: number };
|
|
40
65
|
/** Collection was cleared */
|
|
@@ -51,8 +76,8 @@ export interface ListEvents<T> {
|
|
|
51
76
|
export interface QueueEvents<T> {
|
|
52
77
|
/** Element was added to queue */
|
|
53
78
|
enqueue: { value: T };
|
|
54
|
-
/** Element was removed from queue */
|
|
55
|
-
dequeue: { value: T };
|
|
79
|
+
/** Element was removed from queue. A removal by a layer below (a drop) has a `reason`. */
|
|
80
|
+
dequeue: { value: T; reason?: RemovalReason };
|
|
56
81
|
/** Collection was cleared */
|
|
57
82
|
clear: {};
|
|
58
83
|
}
|
|
@@ -65,10 +90,10 @@ export interface DequeEvents<T> {
|
|
|
65
90
|
addFirst: { value: T };
|
|
66
91
|
/** Element was added to back */
|
|
67
92
|
addLast: { value: T };
|
|
68
|
-
/** Element was removed from front */
|
|
69
|
-
removeFirst: { value: T };
|
|
70
|
-
/** Element was removed from back */
|
|
71
|
-
removeLast: { value: T };
|
|
93
|
+
/** Element was removed from front. A removal by a layer below (a drop) has a `reason`. */
|
|
94
|
+
removeFirst: { value: T; reason?: RemovalReason };
|
|
95
|
+
/** Element was removed from back. A removal by a layer below (a drop) has a `reason`. */
|
|
96
|
+
removeLast: { value: T; reason?: RemovalReason };
|
|
72
97
|
/** Collection was cleared */
|
|
73
98
|
clear: {};
|
|
74
99
|
}
|
|
@@ -79,8 +104,8 @@ export interface DequeEvents<T> {
|
|
|
79
104
|
export interface MapEvents<K, V> {
|
|
80
105
|
/** Key-value pair was set */
|
|
81
106
|
set: { key: K; value: V; oldValue?: V };
|
|
82
|
-
/** Key was deleted */
|
|
83
|
-
delete: { key: K; value: V };
|
|
107
|
+
/** Key was deleted. A removal by a layer below (a drop, an eviction or an expiry) has a `reason`. */
|
|
108
|
+
delete: { key: K; value: V; reason?: RemovalReason };
|
|
84
109
|
/** Collection was cleared */
|
|
85
110
|
clear: {};
|
|
86
111
|
}
|
|
@@ -91,8 +116,8 @@ export interface MapEvents<K, V> {
|
|
|
91
116
|
export interface CollectionEvents<T> {
|
|
92
117
|
/** Element was added */
|
|
93
118
|
add: { value: T };
|
|
94
|
-
/** Element was removed */
|
|
95
|
-
remove: { value: T };
|
|
119
|
+
/** Element was removed. A removal by a layer below has a `reason`. */
|
|
120
|
+
remove: { value: T; reason?: RemovalReason };
|
|
96
121
|
/** Collection was cleared */
|
|
97
122
|
clear: {};
|
|
98
123
|
}
|
|
@@ -117,6 +142,10 @@ export function eventedList<T>(): Middleware<
|
|
|
117
142
|
> {
|
|
118
143
|
return (next: List<T>) => {
|
|
119
144
|
const emitter = new Emitter<ListEvents<T>>();
|
|
145
|
+
// A removal from the last end left the index that is now the size
|
|
146
|
+
const heard = hear<ElementRemoval<T>>(next, ({ value, end, reason }) =>
|
|
147
|
+
emitter.emit("remove", end === undefined ? { value, reason } : { value, index: end === "first" ? 0 : next.size, reason }),
|
|
148
|
+
);
|
|
120
149
|
|
|
121
150
|
return new Proxy(next, {
|
|
122
151
|
get(target, prop, receiver) {
|
|
@@ -129,24 +158,34 @@ export function eventedList<T>(): Middleware<
|
|
|
129
158
|
|
|
130
159
|
if (typeof value === "function") {
|
|
131
160
|
switch (prop) {
|
|
161
|
+
// A layer below can refuse the element (reject, drop-newest): then no event
|
|
132
162
|
case "add":
|
|
133
163
|
case "push":
|
|
134
164
|
return function (element: T): any {
|
|
165
|
+
const size = target.size;
|
|
166
|
+
const all = heard.all;
|
|
135
167
|
const result = (value as Function).call(target, element);
|
|
136
|
-
emitter.emit("add", { value: element });
|
|
168
|
+
if (added(target.size, size, heard, all)) emitter.emit("add", { value: element });
|
|
137
169
|
return result;
|
|
138
170
|
};
|
|
139
171
|
|
|
140
172
|
case "unshift":
|
|
141
173
|
return function (element: T): void {
|
|
174
|
+
const size = target.size;
|
|
175
|
+
const all = heard.all;
|
|
142
176
|
(value as Function).call(target, element);
|
|
143
|
-
emitter.emit("add", { value: element, index: 0 });
|
|
177
|
+
if (added(target.size, size, heard, all)) emitter.emit("add", { value: element, index: 0 });
|
|
144
178
|
};
|
|
145
179
|
|
|
146
180
|
case "insert":
|
|
147
181
|
return function (index: number, element: T): void {
|
|
182
|
+
const size = target.size;
|
|
183
|
+
const all = heard.all;
|
|
184
|
+
const first = heard.first;
|
|
148
185
|
(value as Function).call(target, index, element);
|
|
149
|
-
|
|
186
|
+
if (!added(target.size, size, heard, all)) return;
|
|
187
|
+
// A drop of the first element below moved each later index one place lower
|
|
188
|
+
emitter.emit("add", { value: element, index: heard.first > first ? Math.max(0, index - 1) : index });
|
|
150
189
|
};
|
|
151
190
|
|
|
152
191
|
case "set":
|
|
@@ -175,7 +214,8 @@ export function eventedList<T>(): Middleware<
|
|
|
175
214
|
case "pop":
|
|
176
215
|
return function (): T {
|
|
177
216
|
const removed = (value as Function).call(target);
|
|
178
|
-
|
|
217
|
+
// The last element had the index that is now the size
|
|
218
|
+
emitter.emit("remove", { value: removed, index: target.size });
|
|
179
219
|
return removed;
|
|
180
220
|
};
|
|
181
221
|
|
|
@@ -226,6 +266,7 @@ export function eventedQueue<T>(): Middleware<
|
|
|
226
266
|
> {
|
|
227
267
|
return (next: Queue<T>) => {
|
|
228
268
|
const emitter = new Emitter<QueueEvents<T>>();
|
|
269
|
+
hear<ElementRemoval<T>>(next, ({ value, reason }) => emitter.emit("dequeue", { value, reason }));
|
|
229
270
|
|
|
230
271
|
return new Proxy(next, {
|
|
231
272
|
get(target, prop, receiver) {
|
|
@@ -284,6 +325,9 @@ export function eventedDeque<T>(): Middleware<
|
|
|
284
325
|
> {
|
|
285
326
|
return (next: Deque<T>) => {
|
|
286
327
|
const emitter = new Emitter<DequeEvents<T>>();
|
|
328
|
+
const heard = hear<ElementRemoval<T>>(next, ({ value, end, reason }) =>
|
|
329
|
+
emitter.emit(end === "last" ? "removeLast" : "removeFirst", { value, reason }),
|
|
330
|
+
);
|
|
287
331
|
|
|
288
332
|
return new Proxy(next, {
|
|
289
333
|
get(target, prop, receiver) {
|
|
@@ -295,22 +339,21 @@ export function eventedDeque<T>(): Middleware<
|
|
|
295
339
|
|
|
296
340
|
if (typeof value === "function") {
|
|
297
341
|
switch (prop) {
|
|
342
|
+
// push of a deque adds at the end (see Deque.push). A layer below can refuse the element.
|
|
298
343
|
case "addFirst":
|
|
299
344
|
case "offerFirst":
|
|
300
|
-
case "push":
|
|
301
|
-
return function (element: T): any {
|
|
302
|
-
const result = (value as Function).call(target, element);
|
|
303
|
-
emitter.emit("addFirst", { value: element });
|
|
304
|
-
return result;
|
|
305
|
-
};
|
|
306
|
-
|
|
307
345
|
case "addLast":
|
|
308
346
|
case "offerLast":
|
|
309
347
|
case "offer":
|
|
310
348
|
case "add":
|
|
349
|
+
case "push":
|
|
311
350
|
return function (element: T): any {
|
|
351
|
+
const size = target.size;
|
|
352
|
+
const all = heard.all;
|
|
312
353
|
const result = (value as Function).call(target, element);
|
|
313
|
-
|
|
354
|
+
if (added(target.size, size, heard, all)) {
|
|
355
|
+
emitter.emit(prop === "addFirst" || prop === "offerFirst" ? "addFirst" : "addLast", { value: element });
|
|
356
|
+
}
|
|
314
357
|
return result;
|
|
315
358
|
};
|
|
316
359
|
|
|
@@ -365,6 +408,7 @@ export function eventedMap<K, V>(): Middleware<
|
|
|
365
408
|
> {
|
|
366
409
|
return (next: MapLike<K, V>) => {
|
|
367
410
|
const emitter = new Emitter<MapEvents<K, V>>();
|
|
411
|
+
const heard = hear<MapRemoval<K, V>>(next, ({ key, value, reason }) => emitter.emit("delete", { key, value, reason }));
|
|
368
412
|
|
|
369
413
|
return new Proxy(next, {
|
|
370
414
|
get(target, prop, receiver) {
|
|
@@ -378,8 +422,13 @@ export function eventedMap<K, V>(): Middleware<
|
|
|
378
422
|
switch (prop) {
|
|
379
423
|
case "set":
|
|
380
424
|
return function (key: K, val: V): V | undefined {
|
|
425
|
+
const size = target.size;
|
|
426
|
+
const all = heard.all;
|
|
381
427
|
const oldValue = (value as Function).call(target, key, val);
|
|
382
|
-
|
|
428
|
+
// A new key that a layer below refused (reject, drop-newest) gives no event
|
|
429
|
+
if (oldValue !== undefined || added(target.size, size, heard, all)) {
|
|
430
|
+
emitter.emit("set", { key, value: val, oldValue });
|
|
431
|
+
}
|
|
383
432
|
return oldValue;
|
|
384
433
|
};
|
|
385
434
|
|
|
@@ -420,6 +469,7 @@ export function evented<T>(): Middleware<
|
|
|
420
469
|
> {
|
|
421
470
|
return (next: Collection<T>) => {
|
|
422
471
|
const emitter = new Emitter<CollectionEvents<T>>();
|
|
472
|
+
hear<ElementRemoval<T>>(next, ({ value, reason }) => emitter.emit("remove", { value, reason }));
|
|
423
473
|
|
|
424
474
|
return new Proxy(next, {
|
|
425
475
|
get(target, prop, receiver) {
|
package/src/behaviors/lru.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { checkCapacity } from "../core/capacity.js";
|
|
|
18
18
|
import type { Middleware } from "../core/middleware.js";
|
|
19
19
|
import type { MapLike } from "../interfaces/map.js";
|
|
20
20
|
import { compositeMapMethod } from "./composite.js";
|
|
21
|
+
import { removals, removalSource, type MapRemoval } from "../core/removals.js";
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Options for LRU behavior.
|
|
@@ -133,9 +134,15 @@ export function lruMap<K, V>(
|
|
|
133
134
|
nodeMap.delete(node.key);
|
|
134
135
|
};
|
|
135
136
|
|
|
136
|
-
// A
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
// A removal below (for example an expiry of ttlMap, in compose(lruMap, ttlMap)) removes the node of its key
|
|
138
|
+
const source = removalSource<MapRemoval<K, V>>(next, ({ key }) => {
|
|
139
|
+
const node = nodeMap.get(key);
|
|
140
|
+
if (node) removeNode(node);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// A layer below with no removal channel can remove an entry without this layer seeing it. Its node is
|
|
144
|
+
// then stale: the list must not keep it (it holds the value, and it would grow without bound) and evicting
|
|
145
|
+
// it is not an eviction. With the channel, no node is stale and these checks find nothing.
|
|
139
146
|
const dropStaleNodes = (): void => {
|
|
140
147
|
for (const node of [...nodeMap.values()]) {
|
|
141
148
|
if (!next.has(node.key)) {
|
|
@@ -161,12 +168,16 @@ export function lruMap<K, V>(
|
|
|
161
168
|
timestamp: candidate.lastAccessed,
|
|
162
169
|
});
|
|
163
170
|
}
|
|
171
|
+
source.emit({ key: candidate.key, value: candidate.value, reason: "evict" });
|
|
164
172
|
return;
|
|
165
173
|
}
|
|
166
174
|
};
|
|
167
175
|
|
|
168
176
|
return new Proxy(next, {
|
|
169
177
|
get(target, prop, receiver) {
|
|
178
|
+
if (prop === removals) {
|
|
179
|
+
return source.channel;
|
|
180
|
+
}
|
|
170
181
|
// Add isFull property
|
|
171
182
|
if (prop === "isFull") {
|
|
172
183
|
return target.size >= capacity;
|
|
@@ -185,17 +196,7 @@ export function lruMap<K, V>(
|
|
|
185
196
|
switch (prop) {
|
|
186
197
|
case "get":
|
|
187
198
|
return function (key: K): V {
|
|
188
|
-
|
|
189
|
-
try {
|
|
190
|
-
result = (value as Function).call(target, key);
|
|
191
|
-
} catch (error) {
|
|
192
|
-
// The entry is gone below (for example expired): forget its node
|
|
193
|
-
const stale = nodeMap.get(key);
|
|
194
|
-
if (stale) {
|
|
195
|
-
removeNode(stale);
|
|
196
|
-
}
|
|
197
|
-
throw error;
|
|
198
|
-
}
|
|
199
|
+
const result: V = (value as Function).call(target, key);
|
|
199
200
|
// Update access time
|
|
200
201
|
const node = nodeMap.get(key);
|
|
201
202
|
if (node) {
|
|
@@ -208,14 +209,9 @@ export function lruMap<K, V>(
|
|
|
208
209
|
return function (key: K): boolean {
|
|
209
210
|
const result = (value as Function).call(target, key);
|
|
210
211
|
const node = nodeMap.get(key);
|
|
211
|
-
if (node) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
moveToHead(node);
|
|
215
|
-
} else {
|
|
216
|
-
// The entry is gone below (for example expired): forget its node
|
|
217
|
-
removeNode(node);
|
|
218
|
-
}
|
|
212
|
+
if (node && result) {
|
|
213
|
+
// Update access time
|
|
214
|
+
moveToHead(node);
|
|
219
215
|
}
|
|
220
216
|
return result;
|
|
221
217
|
};
|
package/src/behaviors/ttl.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import type { Middleware } from "../core/middleware.js";
|
|
17
17
|
import type { Entry, MapLike } from "../interfaces/map.js";
|
|
18
18
|
import { compositeMapMethod } from "./composite.js";
|
|
19
|
+
import { removals, removalSource, type ElementRemoval, type MapRemoval } from "../core/removals.js";
|
|
19
20
|
import type { Collection } from "../interfaces/collection.js";
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -81,6 +82,8 @@ export function ttlMap<K, V>(
|
|
|
81
82
|
// The times of the live keys, in the order of expiry: a set deletes its key before it adds it again
|
|
82
83
|
const metadata = new Map<K, TTLEntry<V>>();
|
|
83
84
|
let intervalId: ReturnType<typeof setInterval> | null = null;
|
|
85
|
+
// A removal below (for example an eviction of lruMap) ends the time of its key, so it never expires
|
|
86
|
+
const source = removalSource<MapRemoval<K, V>>(next, ({ key }) => void metadata.delete(key));
|
|
84
87
|
|
|
85
88
|
// This function removes the expired entries from the front of the order, and stops at the first live entry
|
|
86
89
|
const sweep = (): void => {
|
|
@@ -90,6 +93,7 @@ export function ttlMap<K, V>(
|
|
|
90
93
|
metadata.delete(key);
|
|
91
94
|
next.delete(key);
|
|
92
95
|
onExpire?.({ key, value: entry.value, createdAt: entry.createdAt, expiredAt: now });
|
|
96
|
+
source.emit({ key, value: entry.value, reason: "expire" });
|
|
93
97
|
}
|
|
94
98
|
};
|
|
95
99
|
|
|
@@ -107,6 +111,9 @@ export function ttlMap<K, V>(
|
|
|
107
111
|
if (prop === "dispose") {
|
|
108
112
|
return dispose;
|
|
109
113
|
}
|
|
114
|
+
if (prop === removals) {
|
|
115
|
+
return source.channel;
|
|
116
|
+
}
|
|
110
117
|
|
|
111
118
|
// Composite methods (setIfAbsent, compute, merge, putAll and others) go through this
|
|
112
119
|
// proxy's own primitives, so they keep the expiry rules (BUGS-2026-07 L13)
|
|
@@ -205,6 +212,7 @@ export function ttlCollection<T>(
|
|
|
205
212
|
): Collection<T> & { readonly dispose: () => void } => {
|
|
206
213
|
const metadata = new Map<T, { expiresAt: number; createdAt: number }>();
|
|
207
214
|
let intervalId: ReturnType<typeof setInterval> | null = null;
|
|
215
|
+
const source = removalSource<ElementRemoval<T>>(next, ({ value }) => void metadata.delete(value));
|
|
208
216
|
|
|
209
217
|
const checkExpired = (): void => {
|
|
210
218
|
const now = Date.now();
|
|
@@ -229,6 +237,7 @@ export function ttlCollection<T>(
|
|
|
229
237
|
expiredAt: now,
|
|
230
238
|
});
|
|
231
239
|
}
|
|
240
|
+
source.emit({ value: element, reason: "expire" });
|
|
232
241
|
}
|
|
233
242
|
}
|
|
234
243
|
};
|
|
@@ -247,6 +256,9 @@ export function ttlCollection<T>(
|
|
|
247
256
|
if (prop === "dispose") {
|
|
248
257
|
return dispose;
|
|
249
258
|
}
|
|
259
|
+
if (prop === removals) {
|
|
260
|
+
return source.channel;
|
|
261
|
+
}
|
|
250
262
|
|
|
251
263
|
const value = Reflect.get(target, prop, receiver);
|
|
252
264
|
|