@mark1russell7/collections 0.7.2 → 0.9.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/README.md +12 -0
- package/build/index.d.ts +58 -8
- package/build/index.d.ts.map +1 -1
- package/build/index.js +232 -75
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/behaviors/bounded.ts +52 -7
- package/src/behaviors/evented.ts +75 -25
- package/src/behaviors/lru.ts +22 -24
- package/src/behaviors/ttl.ts +92 -84
- 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.0",
|
|
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/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
|
}
|
|
@@ -231,6 +234,19 @@ export function boundedList<T>(
|
|
|
231
234
|
};
|
|
232
235
|
}
|
|
233
236
|
|
|
237
|
+
/**
|
|
238
|
+
* This function adds each element with `add`, one after the other, until `add` refuses one.
|
|
239
|
+
* It gives true when one element goes in. A throw of `add` stops it, and the earlier elements stay.
|
|
240
|
+
*/
|
|
241
|
+
function addEach<T>(elements: Iterable<T>, add: (element: T) => boolean): boolean {
|
|
242
|
+
let added = false;
|
|
243
|
+
for (const element of Array.from(elements)) {
|
|
244
|
+
if (!add(element)) break;
|
|
245
|
+
added = true;
|
|
246
|
+
}
|
|
247
|
+
return added;
|
|
248
|
+
}
|
|
249
|
+
|
|
234
250
|
/**
|
|
235
251
|
* This function makes a bounded Queue middleware.
|
|
236
252
|
*/
|
|
@@ -241,6 +257,7 @@ export function boundedQueue<T>(
|
|
|
241
257
|
const capacity = checkCapacity(options.capacity, "boundedQueue");
|
|
242
258
|
|
|
243
259
|
return (next: Queue<T>): Queue<T> & BoundedCollection => {
|
|
260
|
+
const source = removalSource<ElementRemoval<T>>(next);
|
|
244
261
|
const handleOverflow = (element: T): void => {
|
|
245
262
|
if (onOverflow) {
|
|
246
263
|
const context: OverflowContext<T> = {
|
|
@@ -257,7 +274,7 @@ export function boundedQueue<T>(
|
|
|
257
274
|
throw new Error(`Queue is full (capacity: ${capacity})`);
|
|
258
275
|
case "drop-oldest":
|
|
259
276
|
if (!next.isEmpty) {
|
|
260
|
-
next.poll();
|
|
277
|
+
source.emit({ value: next.poll() as T, end: "first", reason: "drop" });
|
|
261
278
|
}
|
|
262
279
|
break;
|
|
263
280
|
case "drop-newest":
|
|
@@ -274,6 +291,7 @@ export function boundedQueue<T>(
|
|
|
274
291
|
|
|
275
292
|
return new Proxy(next, {
|
|
276
293
|
get(target, prop, receiver) {
|
|
294
|
+
if (prop === removals) return source.channel;
|
|
277
295
|
if (prop === "capacity") return capacity;
|
|
278
296
|
if (prop === "isFull") return target.size >= capacity;
|
|
279
297
|
if (prop === "remainingCapacity") return Math.max(0, capacity - target.size);
|
|
@@ -294,6 +312,12 @@ export function boundedQueue<T>(
|
|
|
294
312
|
}
|
|
295
313
|
return (value as Function).call(target, element);
|
|
296
314
|
};
|
|
315
|
+
|
|
316
|
+
// The base addAll goes past the bound, so each element goes through add of this proxy (C10)
|
|
317
|
+
case "addAll":
|
|
318
|
+
return function (elements: Iterable<T>): boolean {
|
|
319
|
+
return addEach(elements, (element) => (receiver as Queue<T>).add(element));
|
|
320
|
+
};
|
|
297
321
|
}
|
|
298
322
|
}
|
|
299
323
|
|
|
@@ -313,7 +337,9 @@ export function boundedDeque<T>(
|
|
|
313
337
|
const capacity = checkCapacity(options.capacity, "boundedDeque");
|
|
314
338
|
|
|
315
339
|
return (next: Deque<T>): Deque<T> & BoundedCollection => {
|
|
316
|
-
const
|
|
340
|
+
const source = removalSource<ElementRemoval<T>>(next);
|
|
341
|
+
// An insert at the front drops the last element, as unshift of boundedList does (C10)
|
|
342
|
+
const handleOverflow = (element: T, dropFrom: "first" | "last"): void => {
|
|
317
343
|
if (onOverflow) {
|
|
318
344
|
const context: OverflowContext<T> = {
|
|
319
345
|
newElement: element,
|
|
@@ -329,7 +355,8 @@ export function boundedDeque<T>(
|
|
|
329
355
|
throw new Error(`Deque is full (capacity: ${capacity})`);
|
|
330
356
|
case "drop-oldest":
|
|
331
357
|
if (!next.isEmpty) {
|
|
332
|
-
next.removeFirst();
|
|
358
|
+
const value = dropFrom === "last" ? next.removeLast() : next.removeFirst();
|
|
359
|
+
source.emit({ value, end: dropFrom, reason: "drop" });
|
|
333
360
|
}
|
|
334
361
|
break;
|
|
335
362
|
case "drop-newest":
|
|
@@ -346,6 +373,7 @@ export function boundedDeque<T>(
|
|
|
346
373
|
|
|
347
374
|
return new Proxy(next, {
|
|
348
375
|
get(target, prop, receiver) {
|
|
376
|
+
if (prop === removals) return source.channel;
|
|
349
377
|
if (prop === "capacity") return capacity;
|
|
350
378
|
if (prop === "isFull") return target.size >= capacity;
|
|
351
379
|
if (prop === "remainingCapacity") return Math.max(0, capacity - target.size);
|
|
@@ -363,13 +391,19 @@ export function boundedDeque<T>(
|
|
|
363
391
|
case "add":
|
|
364
392
|
return function (element: T): any {
|
|
365
393
|
if (target.size >= capacity && policy !== "grow") {
|
|
366
|
-
handleOverflow(element);
|
|
394
|
+
handleOverflow(element, prop === "addFirst" || prop === "offerFirst" ? "last" : "first");
|
|
367
395
|
if (policy === "reject" || policy === "drop-newest") {
|
|
368
396
|
return prop.startsWith("offer") || prop === "add" ? false : undefined;
|
|
369
397
|
}
|
|
370
398
|
}
|
|
371
399
|
return (value as Function).call(target, element);
|
|
372
400
|
};
|
|
401
|
+
|
|
402
|
+
// The base addAll goes past the bound, so each element goes through add of this proxy (C10)
|
|
403
|
+
case "addAll":
|
|
404
|
+
return function (elements: Iterable<T>): boolean {
|
|
405
|
+
return addEach(elements, (element) => (receiver as Deque<T>).add(element));
|
|
406
|
+
};
|
|
373
407
|
}
|
|
374
408
|
}
|
|
375
409
|
|
|
@@ -392,6 +426,10 @@ export function boundedMap<K, V>(
|
|
|
392
426
|
|
|
393
427
|
return (next: MapLike<K, V>): MapLike<K, V> & BoundedCollection => {
|
|
394
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
|
+
});
|
|
395
433
|
|
|
396
434
|
const handleOverflow = (key: K, value: V): void => {
|
|
397
435
|
if (onOverflow) {
|
|
@@ -409,9 +447,15 @@ export function boundedMap<K, V>(
|
|
|
409
447
|
throw new Error(`Map is full (capacity: ${capacity})`);
|
|
410
448
|
case "drop-oldest":
|
|
411
449
|
// Remove oldest key
|
|
412
|
-
|
|
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) {
|
|
413
453
|
const oldestKey = insertionOrder.shift()!;
|
|
454
|
+
if (!next.has(oldestKey)) continue;
|
|
455
|
+
const value = next.get(oldestKey);
|
|
414
456
|
next.delete(oldestKey);
|
|
457
|
+
source.emit({ key: oldestKey, value, reason: "drop" });
|
|
458
|
+
break;
|
|
415
459
|
}
|
|
416
460
|
break;
|
|
417
461
|
case "drop-newest":
|
|
@@ -428,6 +472,7 @@ export function boundedMap<K, V>(
|
|
|
428
472
|
|
|
429
473
|
return new Proxy(next, {
|
|
430
474
|
get(target, prop, receiver) {
|
|
475
|
+
if (prop === removals) return source.channel;
|
|
431
476
|
if (prop === "capacity") return capacity;
|
|
432
477
|
if (prop === "isFull") return target.size >= capacity;
|
|
433
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
|
};
|
|
@@ -240,8 +236,10 @@ export function lruMap<K, V>(
|
|
|
240
236
|
if (stale) {
|
|
241
237
|
removeNode(stale);
|
|
242
238
|
}
|
|
243
|
-
// Keep the node list bounded
|
|
244
|
-
|
|
239
|
+
// Keep the node list bounded. At most `capacity` nodes are live, so at twice the capacity a scan
|
|
240
|
+
// removes at least `capacity` stale nodes: the cost of a set is amortized O(1). A scan at the
|
|
241
|
+
// capacity read each node at each set into a full map (C10). evictLRU skips the other stale nodes.
|
|
242
|
+
if (nodeMap.size >= 2 * capacity) {
|
|
245
243
|
dropStaleNodes();
|
|
246
244
|
}
|
|
247
245
|
if (target.size >= capacity) {
|