@mark1russell7/collections 0.9.6 → 0.9.8
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/build/index.d.ts +9 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +229 -70
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/behaviors/evented.ts +198 -1
- package/src/behaviors/synchronized.ts +20 -3
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.9.
|
|
4
|
+
"version": "0.9.8",
|
|
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/evented.ts
CHANGED
|
@@ -19,10 +19,11 @@
|
|
|
19
19
|
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
|
-
import type { MapLike } from "../interfaces/map.js";
|
|
22
|
+
import type { Entry, 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
25
|
import { removalsOf, type ElementRemoval, type MapRemoval, type RemovalReason } from "../core/removals.js";
|
|
26
|
+
import { compositeMapMethod } from "./composite.js";
|
|
26
27
|
|
|
27
28
|
/** The number of removals that a layer heard from the layers below it, and the number from the first end. */
|
|
28
29
|
interface Heard {
|
|
@@ -48,6 +49,62 @@ function hear<R extends ElementRemoval<unknown> | MapRemoval<unknown, unknown>>(
|
|
|
48
49
|
const added = (size: number, sizeBefore: number, heard: Heard, heardBefore: number): boolean =>
|
|
49
50
|
size - sizeBefore + heard.all - heardBefore > 0;
|
|
50
51
|
|
|
52
|
+
/**
|
|
53
|
+
* This function gives the removals that change `before` to `after`, in the order of a replay.
|
|
54
|
+
* Each index is the place of the value at the time of its removal. It gives undefined when `after` is not in the order of `before`.
|
|
55
|
+
*/
|
|
56
|
+
function removalsInOrder<T>(before: readonly T[], after: readonly T[]): { value: T; index: number }[] | undefined {
|
|
57
|
+
const removed: { value: T; index: number }[] = [];
|
|
58
|
+
let kept = 0;
|
|
59
|
+
for (const value of before) {
|
|
60
|
+
if (kept < after.length && Object.is(value, after[kept])) kept++;
|
|
61
|
+
else removed.push({ value, index: kept });
|
|
62
|
+
}
|
|
63
|
+
return kept === after.length ? removed : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** This function gives the values of `before` that are not in `after`, with each copy counted. The order does not matter. */
|
|
67
|
+
function removedValues<T>(before: readonly T[], after: readonly T[]): T[] {
|
|
68
|
+
const left = new Map<T, number>();
|
|
69
|
+
for (const value of after) left.set(value, (left.get(value) ?? 0) + 1);
|
|
70
|
+
const removed: T[] = [];
|
|
71
|
+
for (const value of before) {
|
|
72
|
+
const count = left.get(value) ?? 0;
|
|
73
|
+
if (count > 0) left.set(value, count - 1);
|
|
74
|
+
else removed.push(value);
|
|
75
|
+
}
|
|
76
|
+
return removed;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* This function wraps a bulk removal (removeAll, retainAll, removeRange) of the collection below.
|
|
81
|
+
* The base does the removal with its own equality, and `report` gets the contents before and after it.
|
|
82
|
+
*/
|
|
83
|
+
function bulkRemoval<T>(
|
|
84
|
+
target: { readonly size: number; toArray(): T[] },
|
|
85
|
+
method: Function,
|
|
86
|
+
report: (before: T[], after: T[]) => void,
|
|
87
|
+
): (...args: unknown[]) => unknown {
|
|
88
|
+
return function (...args: unknown[]): unknown {
|
|
89
|
+
const before = target.toArray();
|
|
90
|
+
const result = method.apply(target, args);
|
|
91
|
+
if (target.size !== before.length) report(before, target.toArray());
|
|
92
|
+
return result;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* This function adds each element with `add` of the layer, one after the other.
|
|
98
|
+
* Thus each element gives its own event, with the rules of a single add. It gives true when one element goes in.
|
|
99
|
+
*/
|
|
100
|
+
function addEach<T>(elements: Iterable<T>, add: (element: T) => boolean): boolean {
|
|
101
|
+
let changed = false;
|
|
102
|
+
for (const element of Array.from(elements)) {
|
|
103
|
+
if (add(element)) changed = true;
|
|
104
|
+
}
|
|
105
|
+
return changed;
|
|
106
|
+
}
|
|
107
|
+
|
|
51
108
|
// ============================================================================
|
|
52
109
|
// Event type definitions
|
|
53
110
|
// ============================================================================
|
|
@@ -78,6 +135,8 @@ export interface QueueEvents<T> {
|
|
|
78
135
|
enqueue: { value: T };
|
|
79
136
|
/** Element was removed from queue. A removal by a layer below (a drop) has a `reason`. */
|
|
80
137
|
dequeue: { value: T; reason?: RemovalReason };
|
|
138
|
+
/** An element left from a place that is not the head: `remove`, `removeAll` or `retainAll`. */
|
|
139
|
+
remove: { value: T };
|
|
81
140
|
/** Collection was cleared */
|
|
82
141
|
clear: {};
|
|
83
142
|
}
|
|
@@ -94,6 +153,8 @@ export interface DequeEvents<T> {
|
|
|
94
153
|
removeFirst: { value: T; reason?: RemovalReason };
|
|
95
154
|
/** Element was removed from back. A removal by a layer below (a drop) has a `reason`. */
|
|
96
155
|
removeLast: { value: T; reason?: RemovalReason };
|
|
156
|
+
/** An element left from `index`, not with a method of an end: `remove`, `removeAll` or `retainAll`. */
|
|
157
|
+
remove: { value: T; index: number };
|
|
97
158
|
/** Collection was cleared */
|
|
98
159
|
clear: {};
|
|
99
160
|
}
|
|
@@ -232,6 +293,46 @@ export function eventedList<T>(): Middleware<
|
|
|
232
293
|
emitter.emit("clear", {});
|
|
233
294
|
};
|
|
234
295
|
|
|
296
|
+
// The base writes a bulk change into its storage, and does not call its own add or remove (C24)
|
|
297
|
+
case "addAll":
|
|
298
|
+
return function (elements: Iterable<T>): boolean {
|
|
299
|
+
return addEach(elements, (element) => {
|
|
300
|
+
const size = target.size;
|
|
301
|
+
const all = heard.all;
|
|
302
|
+
(receiver as List<T>).add(element);
|
|
303
|
+
return added(target.size, size, heard, all);
|
|
304
|
+
});
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
case "insertAll":
|
|
308
|
+
return function (index: number, elements: Iterable<T>): boolean {
|
|
309
|
+
let at = index;
|
|
310
|
+
return addEach(elements, (element) => {
|
|
311
|
+
const size = target.size;
|
|
312
|
+
const all = heard.all;
|
|
313
|
+
const first = heard.first;
|
|
314
|
+
(receiver as List<T>).insert(at, element);
|
|
315
|
+
const wentIn = added(target.size, size, heard, all);
|
|
316
|
+
// A drop of the first element below moved the place one lower. The next element goes after a new one.
|
|
317
|
+
at = Math.min(target.size, Math.max(0, at - (heard.first - first)) + (wentIn ? 1 : 0));
|
|
318
|
+
return wentIn;
|
|
319
|
+
});
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
case "removeAll":
|
|
323
|
+
case "retainAll":
|
|
324
|
+
case "removeRange":
|
|
325
|
+
return bulkRemoval<T>(target, value as Function, (before, after) => {
|
|
326
|
+
const removed = removalsInOrder(before, after);
|
|
327
|
+
if (removed) {
|
|
328
|
+
for (const removal of removed) emitter.emit("remove", removal);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
// A base that changed the order of the rest: a clear and the adds give the same contents
|
|
332
|
+
emitter.emit("clear", {});
|
|
333
|
+
after.forEach((element, at) => emitter.emit("add", { value: element, index: at }));
|
|
334
|
+
});
|
|
335
|
+
|
|
235
336
|
case "sort":
|
|
236
337
|
return function (...args: any[]): void {
|
|
237
338
|
(value as Function).apply(target, args);
|
|
@@ -297,11 +398,32 @@ export function eventedQueue<T>(): Middleware<
|
|
|
297
398
|
return removed;
|
|
298
399
|
};
|
|
299
400
|
|
|
401
|
+
case "pollOrUndefined":
|
|
402
|
+
return function (): T | undefined {
|
|
403
|
+
const size = target.size;
|
|
404
|
+
const removed = (value as Function).call(target);
|
|
405
|
+
if (target.size < size) emitter.emit("dequeue", { value: removed });
|
|
406
|
+
return removed;
|
|
407
|
+
};
|
|
408
|
+
|
|
300
409
|
case "clear":
|
|
301
410
|
return function (): void {
|
|
302
411
|
(value as Function).call(target);
|
|
303
412
|
emitter.emit("clear", {});
|
|
304
413
|
};
|
|
414
|
+
|
|
415
|
+
// The base writes a bulk change into its storage, and does not call its own add or remove (C24)
|
|
416
|
+
case "addAll":
|
|
417
|
+
return function (elements: Iterable<T>): boolean {
|
|
418
|
+
return addEach(elements, (element) => (receiver as Queue<T>).add(element) !== false);
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
case "remove":
|
|
422
|
+
case "removeAll":
|
|
423
|
+
case "retainAll":
|
|
424
|
+
return bulkRemoval<T>(target, value as Function, (before, after) => {
|
|
425
|
+
for (const removed of removedValues(before, after)) emitter.emit("remove", { value: removed });
|
|
426
|
+
});
|
|
305
427
|
}
|
|
306
428
|
}
|
|
307
429
|
|
|
@@ -380,11 +502,44 @@ export function eventedDeque<T>(): Middleware<
|
|
|
380
502
|
return removed;
|
|
381
503
|
};
|
|
382
504
|
|
|
505
|
+
case "pollOrUndefined":
|
|
506
|
+
return function (): T | undefined {
|
|
507
|
+
const size = target.size;
|
|
508
|
+
const removed = (value as Function).call(target);
|
|
509
|
+
if (target.size < size) emitter.emit("removeFirst", { value: removed });
|
|
510
|
+
return removed;
|
|
511
|
+
};
|
|
512
|
+
|
|
383
513
|
case "clear":
|
|
384
514
|
return function (): void {
|
|
385
515
|
(value as Function).call(target);
|
|
386
516
|
emitter.emit("clear", {});
|
|
387
517
|
};
|
|
518
|
+
|
|
519
|
+
// The base writes a bulk change into its storage, and does not call its own add or remove (C24)
|
|
520
|
+
case "addAll":
|
|
521
|
+
return function (elements: Iterable<T>): boolean {
|
|
522
|
+
return addEach(elements, (element) => {
|
|
523
|
+
const size = target.size;
|
|
524
|
+
const all = heard.all;
|
|
525
|
+
(receiver as Deque<T>).addLast(element);
|
|
526
|
+
return added(target.size, size, heard, all);
|
|
527
|
+
});
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
case "remove":
|
|
531
|
+
case "removeAll":
|
|
532
|
+
case "retainAll":
|
|
533
|
+
return bulkRemoval<T>(target, value as Function, (before, after) => {
|
|
534
|
+
const removed = removalsInOrder(before, after);
|
|
535
|
+
if (removed) {
|
|
536
|
+
for (const removal of removed) emitter.emit("remove", removal);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
// A base that changed the order of the rest: a clear and the adds give the same contents
|
|
540
|
+
emitter.emit("clear", {});
|
|
541
|
+
for (const element of after) emitter.emit("addLast", { value: element });
|
|
542
|
+
});
|
|
388
543
|
}
|
|
389
544
|
}
|
|
390
545
|
|
|
@@ -416,6 +571,13 @@ export function eventedMap<K, V>(): Middleware<
|
|
|
416
571
|
return emitter.on.bind(emitter);
|
|
417
572
|
}
|
|
418
573
|
|
|
574
|
+
// Composite methods (computeIfAbsent, compute, merge, putAll and others) go through this
|
|
575
|
+
// proxy's own set and delete, so each change gives its event (C24). The base calls its own set.
|
|
576
|
+
const composite = compositeMapMethod(receiver as MapLike<K, V>, prop);
|
|
577
|
+
if (composite) {
|
|
578
|
+
return composite;
|
|
579
|
+
}
|
|
580
|
+
|
|
419
581
|
const value = Reflect.get(target, prop, receiver);
|
|
420
582
|
|
|
421
583
|
if (typeof value === "function") {
|
|
@@ -441,6 +603,15 @@ export function eventedMap<K, V>(): Middleware<
|
|
|
441
603
|
return deletedValue;
|
|
442
604
|
};
|
|
443
605
|
|
|
606
|
+
// The first or last entry of a navigable map (treeMap)
|
|
607
|
+
case "pollFirstEntry":
|
|
608
|
+
case "pollLastEntry":
|
|
609
|
+
return function (): Entry<K, V> | undefined {
|
|
610
|
+
const entry: Entry<K, V> | undefined = (value as Function).call(target);
|
|
611
|
+
if (entry !== undefined) emitter.emit("delete", { key: entry.key, value: entry.value });
|
|
612
|
+
return entry;
|
|
613
|
+
};
|
|
614
|
+
|
|
444
615
|
case "clear":
|
|
445
616
|
return function (): void {
|
|
446
617
|
(value as Function).call(target);
|
|
@@ -504,6 +675,32 @@ export function evented<T>(): Middleware<
|
|
|
504
675
|
(value as Function).call(target);
|
|
505
676
|
emitter.emit("clear", {});
|
|
506
677
|
};
|
|
678
|
+
|
|
679
|
+
// The base writes a bulk change into its storage, and does not call its own add or remove (C24)
|
|
680
|
+
case "addAll":
|
|
681
|
+
return function (elements: Iterable<T>): boolean {
|
|
682
|
+
let changed = false;
|
|
683
|
+
for (const element of Array.from(elements)) {
|
|
684
|
+
if ((receiver as Collection<T>).add(element)) changed = true;
|
|
685
|
+
}
|
|
686
|
+
return changed;
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
case "removeAll":
|
|
690
|
+
case "retainAll":
|
|
691
|
+
return bulkRemoval<T>(target, value as Function, (before, after) => {
|
|
692
|
+
for (const removed of removedValues(before, after)) emitter.emit("remove", { value: removed });
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
// The first or last element of a sorted set (treeSet)
|
|
696
|
+
case "pollFirst":
|
|
697
|
+
case "pollLast":
|
|
698
|
+
return function (): T | undefined {
|
|
699
|
+
const size = target.size;
|
|
700
|
+
const removed = (value as Function).call(target);
|
|
701
|
+
if (target.size < size) emitter.emit("remove", { value: removed });
|
|
702
|
+
return removed;
|
|
703
|
+
};
|
|
507
704
|
}
|
|
508
705
|
}
|
|
509
706
|
|
|
@@ -277,8 +277,12 @@ export function readWriteLock<C extends object>(): Middleware<C> {
|
|
|
277
277
|
"forEach",
|
|
278
278
|
"getOrDefault",
|
|
279
279
|
"getOrUndefined",
|
|
280
|
+
"descendingIterator",
|
|
280
281
|
]);
|
|
281
282
|
|
|
283
|
+
// The reads that give an iterator. The lock copies the elements before it releases the read (C32).
|
|
284
|
+
const ITERATOR_READS = new Set(["keys", "values", "entries", "descendingIterator"]);
|
|
285
|
+
|
|
282
286
|
return new Proxy(next, {
|
|
283
287
|
get(target, prop, receiver) {
|
|
284
288
|
const value = Reflect.get(target, prop, receiver);
|
|
@@ -287,15 +291,28 @@ export function readWriteLock<C extends object>(): Middleware<C> {
|
|
|
287
291
|
return value;
|
|
288
292
|
}
|
|
289
293
|
|
|
290
|
-
|
|
294
|
+
// A method with a symbol key, for example Symbol.iterator, stays synchronous, so `for of` works (C32).
|
|
295
|
+
// The loop reads at once with no lock, as a plain method of synchronized().
|
|
296
|
+
if (typeof prop === "symbol") {
|
|
297
|
+
return function (this: unknown, ...args: unknown[]) {
|
|
298
|
+
return (value as Function).apply(target, args);
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const isReadMethod = READ_METHODS.has(prop);
|
|
303
|
+
const isIteratorRead = ITERATOR_READS.has(prop);
|
|
291
304
|
|
|
292
305
|
return async function (this: any, ...args: any[]) {
|
|
293
306
|
if (isReadMethod) {
|
|
294
307
|
// Acquire read lock
|
|
295
308
|
await acquireRead();
|
|
296
309
|
try {
|
|
297
|
-
const
|
|
298
|
-
|
|
310
|
+
const raw = (value as Function).apply(target, args);
|
|
311
|
+
const result = raw instanceof Promise ? await raw : raw;
|
|
312
|
+
// Before C32, the iteration started after the release of the read, so a write could change the elements.
|
|
313
|
+
return isIteratorRead && result != null && typeof result[Symbol.iterator] === "function"
|
|
314
|
+
? Array.from(result as Iterable<unknown>)[Symbol.iterator]()
|
|
315
|
+
: result;
|
|
299
316
|
} finally {
|
|
300
317
|
releaseRead();
|
|
301
318
|
}
|