@mark1russell7/collections 0.4.0 → 0.6.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/build/dag/index.d.ts +13 -1
- package/build/dag/index.d.ts.map +1 -1
- package/build/dag/index.js +29 -3
- package/build/dag/index.js.map +1 -1
- package/build/index.d.ts +22 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +94 -7
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/behaviors/bounded.ts +9 -4
- package/src/behaviors/lru.ts +42 -4
- package/src/core/capacity.ts +21 -0
- package/src/dag/executor.ts +24 -2
- package/src/dag/types.ts +10 -1
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.6.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
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* }
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { checkCapacity } from "../core/capacity.js";
|
|
18
19
|
import type { Middleware } from "../core/middleware.js";
|
|
19
20
|
import type { List } from "../interfaces/list.js";
|
|
20
21
|
import type { Queue, Deque } from "../interfaces/queue.js";
|
|
@@ -66,7 +67,8 @@ export interface BoundedCollection {
|
|
|
66
67
|
export function boundedList<T>(
|
|
67
68
|
options: BoundedOptions<T>
|
|
68
69
|
): Middleware<List<T>, List<T> & BoundedCollection> {
|
|
69
|
-
const {
|
|
70
|
+
const { policy = "throw", onOverflow } = options;
|
|
71
|
+
const capacity = checkCapacity(options.capacity, "boundedList");
|
|
70
72
|
|
|
71
73
|
return (next: List<T>): List<T> & BoundedCollection => {
|
|
72
74
|
// An unshift puts the element at the front, so drop-oldest drops the last element for it
|
|
@@ -235,7 +237,8 @@ export function boundedList<T>(
|
|
|
235
237
|
export function boundedQueue<T>(
|
|
236
238
|
options: BoundedOptions<T>
|
|
237
239
|
): Middleware<Queue<T>, Queue<T> & BoundedCollection> {
|
|
238
|
-
const {
|
|
240
|
+
const { policy = "throw", onOverflow } = options;
|
|
241
|
+
const capacity = checkCapacity(options.capacity, "boundedQueue");
|
|
239
242
|
|
|
240
243
|
return (next: Queue<T>): Queue<T> & BoundedCollection => {
|
|
241
244
|
const handleOverflow = (element: T): void => {
|
|
@@ -306,7 +309,8 @@ export function boundedQueue<T>(
|
|
|
306
309
|
export function boundedDeque<T>(
|
|
307
310
|
options: BoundedOptions<T>
|
|
308
311
|
): Middleware<Deque<T>, Deque<T> & BoundedCollection> {
|
|
309
|
-
const {
|
|
312
|
+
const { policy = "throw", onOverflow } = options;
|
|
313
|
+
const capacity = checkCapacity(options.capacity, "boundedDeque");
|
|
310
314
|
|
|
311
315
|
return (next: Deque<T>): Deque<T> & BoundedCollection => {
|
|
312
316
|
const handleOverflow = (element: T): void => {
|
|
@@ -383,7 +387,8 @@ export function boundedMap<K, V>(
|
|
|
383
387
|
onOverflow?: (context: Omit<OverflowContext<V>, "newElement"> & { key: K; value: V }) => void;
|
|
384
388
|
}
|
|
385
389
|
): Middleware<MapLike<K, V>, MapLike<K, V> & BoundedCollection> {
|
|
386
|
-
const {
|
|
390
|
+
const { policy = "throw", onOverflow } = options;
|
|
391
|
+
const capacity = checkCapacity(options.capacity, "boundedMap");
|
|
387
392
|
|
|
388
393
|
return (next: MapLike<K, V>): MapLike<K, V> & BoundedCollection => {
|
|
389
394
|
let insertionOrder: K[] = []; // Track insertion order for drop-oldest
|
package/src/behaviors/lru.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* // When full, least recently accessed keys are evicted
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { checkCapacity } from "../core/capacity.js";
|
|
17
18
|
import type { Middleware } from "../core/middleware.js";
|
|
18
19
|
import type { MapLike } from "../interfaces/map.js";
|
|
19
20
|
import { compositeMapMethod } from "./composite.js";
|
|
@@ -23,7 +24,8 @@ import { compositeMapMethod } from "./composite.js";
|
|
|
23
24
|
*/
|
|
24
25
|
export interface LRUOptions {
|
|
25
26
|
/**
|
|
26
|
-
*
|
|
27
|
+
* The maximum number of entries, a positive whole number. When the map is full, a new key
|
|
28
|
+
* evicts the least recently used entry.
|
|
27
29
|
*/
|
|
28
30
|
capacity: number;
|
|
29
31
|
|
|
@@ -50,11 +52,14 @@ interface LRUNode<K, V> {
|
|
|
50
52
|
* This function maintains a doubly-linked list to track access order.
|
|
51
53
|
* Most recently accessed items are at the head.
|
|
52
54
|
* When capacity is exceeded, items at the tail are evicted.
|
|
55
|
+
*
|
|
56
|
+
* @throws RangeError when `options.capacity` is not a positive whole number.
|
|
53
57
|
*/
|
|
54
58
|
export function lruMap<K, V>(
|
|
55
59
|
options: LRUOptions
|
|
56
60
|
): Middleware<MapLike<K, V>, MapLike<K, V> & { readonly isFull: boolean }> {
|
|
57
|
-
const {
|
|
61
|
+
const { onEvict } = options;
|
|
62
|
+
const capacity = checkCapacity(options.capacity, "lruMap");
|
|
58
63
|
|
|
59
64
|
return (next: MapLike<K, V>): MapLike<K, V> & { readonly isFull: boolean } => {
|
|
60
65
|
// Doubly-linked list for LRU tracking
|
|
@@ -290,8 +295,13 @@ export class LRUCache<K, V> {
|
|
|
290
295
|
private head: LRUNode<K, V> | null = null;
|
|
291
296
|
private tail: LRUNode<K, V> | null = null;
|
|
292
297
|
|
|
298
|
+
/**
|
|
299
|
+
* @param capacity - The maximum number of entries, a positive whole number.
|
|
300
|
+
* @param onEvict - The function that this cache starts for each entry that it evicts.
|
|
301
|
+
* @throws RangeError when `capacity` is not a positive whole number.
|
|
302
|
+
*/
|
|
293
303
|
constructor(capacity: number, onEvict?: ((key: K, value: V) => void) | undefined) {
|
|
294
|
-
this.capacity = capacity;
|
|
304
|
+
this.capacity = checkCapacity(capacity, "LRUCache");
|
|
295
305
|
this.onEvict = onEvict;
|
|
296
306
|
}
|
|
297
307
|
|
|
@@ -359,6 +369,32 @@ export class LRUCache<K, V> {
|
|
|
359
369
|
return true;
|
|
360
370
|
}
|
|
361
371
|
|
|
372
|
+
/**
|
|
373
|
+
* This method deletes each entry for which `matches` gives true, and it gives the number of
|
|
374
|
+
* deleted entries. It is not a use: the order of the other entries does not change, and it
|
|
375
|
+
* does not start `onEvict`. It starts `matches` once for each entry, the most recent first. If
|
|
376
|
+
* `matches` throws, the entries that it matched before stay deleted.
|
|
377
|
+
*
|
|
378
|
+
* @param matches - The test of an entry.
|
|
379
|
+
* @returns The number of deleted entries.
|
|
380
|
+
*/
|
|
381
|
+
deleteWhere(matches: (value: V, key: K) => boolean): number {
|
|
382
|
+
const nodes: LRUNode<K, V>[] = [];
|
|
383
|
+
for (let node = this.head; node; node = node.next) nodes.push(node);
|
|
384
|
+
let deleted = 0;
|
|
385
|
+
for (const node of nodes) {
|
|
386
|
+
// The function `matches` can change the cache. Skip each node that is not in it now.
|
|
387
|
+
if (this.map.get(node.key)?.node !== node) continue;
|
|
388
|
+
if (!matches(node.value, node.key)) continue;
|
|
389
|
+
// The function `matches` can delete this node itself, so check again.
|
|
390
|
+
if (this.map.get(node.key)?.node !== node) continue;
|
|
391
|
+
this.removeNode(node);
|
|
392
|
+
this.map.delete(node.key);
|
|
393
|
+
deleted++;
|
|
394
|
+
}
|
|
395
|
+
return deleted;
|
|
396
|
+
}
|
|
397
|
+
|
|
362
398
|
clear(): void {
|
|
363
399
|
this.map.clear();
|
|
364
400
|
this.head = null;
|
|
@@ -439,7 +475,9 @@ export class LRUCache<K, V> {
|
|
|
439
475
|
}
|
|
440
476
|
|
|
441
477
|
/**
|
|
442
|
-
*
|
|
478
|
+
* This function makes an LRU cache.
|
|
479
|
+
*
|
|
480
|
+
* @throws RangeError when `capacity` is not a positive whole number.
|
|
443
481
|
*/
|
|
444
482
|
export function lruCache<K, V>(
|
|
445
483
|
capacity: number,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The check of a capacity, for each layer and class that has a maximum size.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* This function gives the capacity when it is a positive whole number. Otherwise it throws a
|
|
7
|
+
* `RangeError`. A capacity of `NaN` makes a comparison `size >= capacity` always false, so a
|
|
8
|
+
* structure with that capacity grows without a limit. A capacity of 0 or less, or a fraction,
|
|
9
|
+
* has no clear meaning.
|
|
10
|
+
*
|
|
11
|
+
* @param capacity - The capacity to check.
|
|
12
|
+
* @param owner - The name of the layer or class, for the message of the error.
|
|
13
|
+
* @returns The same capacity.
|
|
14
|
+
* @throws RangeError when the capacity is not a positive whole number.
|
|
15
|
+
*/
|
|
16
|
+
export function checkCapacity(capacity: number, owner: string): number {
|
|
17
|
+
if (!Number.isSafeInteger(capacity) || capacity < 1) {
|
|
18
|
+
throw new RangeError(`${owner}: the capacity must be a positive whole number, not ${String(capacity)}`);
|
|
19
|
+
}
|
|
20
|
+
return capacity;
|
|
21
|
+
}
|
package/src/dag/executor.ts
CHANGED
|
@@ -68,6 +68,9 @@ function notify<T>(name: string, listener: ((arg: T) => void) | undefined, arg:
|
|
|
68
68
|
* A processor that throws does NOT reject the whole run. The run records the throw
|
|
69
69
|
* as a failed NodeResult, so partial results are always given.
|
|
70
70
|
*
|
|
71
|
+
* With `skipDependents` (the default), a node whose dependency failed or was skipped does not start. Its result has
|
|
72
|
+
* `skipped: true` and an error that names those dependencies. A dependency outside the DAG does not count.
|
|
73
|
+
*
|
|
71
74
|
* `failFast` operates at level granularity. When a node in a level fails, each
|
|
72
75
|
* node already in flight in that level continues to completion. No later level
|
|
73
76
|
* starts. (There is no mid-level cancellation.)
|
|
@@ -80,20 +83,37 @@ export async function executeDAG<TNode extends DAGNode>(
|
|
|
80
83
|
const {
|
|
81
84
|
concurrency = 4,
|
|
82
85
|
failFast = true,
|
|
86
|
+
skipDependents = true,
|
|
83
87
|
onNodeStart,
|
|
84
88
|
onNodeComplete,
|
|
85
89
|
} = options;
|
|
86
90
|
|
|
87
91
|
const results = new Map<string, NodeResult<TNode>>();
|
|
88
92
|
const failedNodes: string[] = [];
|
|
93
|
+
const skippedNodes: string[] = [];
|
|
94
|
+
// The ids of the nodes that failed or were skipped
|
|
95
|
+
const blocked = new Set<string>();
|
|
89
96
|
const startTime = Date.now();
|
|
90
97
|
let shouldStop = false;
|
|
91
98
|
|
|
92
99
|
for (const level of dag.levels) {
|
|
93
100
|
if (shouldStop) break;
|
|
94
101
|
|
|
102
|
+
const runnable: TNode[] = [];
|
|
103
|
+
for (const node of level) {
|
|
104
|
+
const blockers = skipDependents ? node.dependencies.filter((dep) => blocked.has(dep)) : [];
|
|
105
|
+
if (blockers.length === 0) {
|
|
106
|
+
runnable.push(node);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const message = "Skipped " + node.id + ": the dependencies " + blockers.join(", ") + " failed or were skipped";
|
|
110
|
+
results.set(node.id, { node, success: false, skipped: true, error: new Error(message), duration: 0, logs: [message] });
|
|
111
|
+
skippedNodes.push(node.id);
|
|
112
|
+
blocked.add(node.id);
|
|
113
|
+
}
|
|
114
|
+
|
|
95
115
|
const levelResults = await executeWithConcurrency(
|
|
96
|
-
|
|
116
|
+
runnable,
|
|
97
117
|
async (node) => {
|
|
98
118
|
const startError = notify("onNodeStart", onNodeStart, node);
|
|
99
119
|
const nodeStart = Date.now();
|
|
@@ -127,6 +147,7 @@ export async function executeDAG<TNode extends DAGNode>(
|
|
|
127
147
|
results.set(result.node.id, result);
|
|
128
148
|
if (!result.success) {
|
|
129
149
|
failedNodes.push(result.node.id);
|
|
150
|
+
blocked.add(result.node.id);
|
|
130
151
|
if (failFast) {
|
|
131
152
|
shouldStop = true;
|
|
132
153
|
}
|
|
@@ -135,9 +156,10 @@ export async function executeDAG<TNode extends DAGNode>(
|
|
|
135
156
|
}
|
|
136
157
|
|
|
137
158
|
return {
|
|
138
|
-
success: failedNodes.length === 0,
|
|
159
|
+
success: failedNodes.length === 0 && skippedNodes.length === 0,
|
|
139
160
|
results,
|
|
140
161
|
failedNodes,
|
|
162
|
+
skippedNodes,
|
|
141
163
|
totalDuration: Date.now() - startTime,
|
|
142
164
|
};
|
|
143
165
|
}
|
package/src/dag/types.ts
CHANGED
|
@@ -41,6 +41,11 @@ export interface DAGExecutionOptions<TNode extends DAGNode = DAGNode> {
|
|
|
41
41
|
concurrency?: number;
|
|
42
42
|
/** When this value is true, the run stops on the first error. The default is true. */
|
|
43
43
|
failFast?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* When this value is true, a node whose dependency failed or was skipped does not start. Its result has `skipped: true`.
|
|
46
|
+
* The default is true. Give false only when the nodes do not use the work of their dependencies.
|
|
47
|
+
*/
|
|
48
|
+
skipDependents?: boolean;
|
|
44
49
|
/** Callback for progress reporting */
|
|
45
50
|
onNodeStart?: (node: TNode) => void;
|
|
46
51
|
/** Callback when a node completes */
|
|
@@ -63,6 +68,8 @@ export interface NodeResult<TNode extends DAGNode = DAGNode> {
|
|
|
63
68
|
logs: string[];
|
|
64
69
|
/** Optional output from the processor */
|
|
65
70
|
output?: unknown | undefined;
|
|
71
|
+
/** This value is true when the node did not start, because a dependency failed or was skipped. */
|
|
72
|
+
skipped?: boolean | undefined;
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
/**
|
|
@@ -73,8 +80,10 @@ export interface DAGResult<TNode extends DAGNode = DAGNode> {
|
|
|
73
80
|
success: boolean;
|
|
74
81
|
/** Results for each node */
|
|
75
82
|
results: Map<string, NodeResult<TNode>>;
|
|
76
|
-
/**
|
|
83
|
+
/** The ids of the nodes whose processor failed */
|
|
77
84
|
failedNodes: string[];
|
|
85
|
+
/** The ids of the nodes that did not start, because a dependency failed or was skipped */
|
|
86
|
+
skippedNodes: string[];
|
|
78
87
|
/** Total duration in milliseconds */
|
|
79
88
|
totalDuration: number;
|
|
80
89
|
}
|