@mark1russell7/collections 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +78 -2
- package/build/dag/index.d.ts +144 -0
- package/build/dag/index.d.ts.map +1 -0
- package/build/dag/index.js +294 -0
- package/build/dag/index.js.map +1 -0
- package/build/graph/index.d.ts +20 -0
- package/build/graph/index.d.ts.map +1 -0
- package/build/graph/index.js +48 -0
- package/build/graph/index.js.map +1 -0
- package/build/index.d.ts +4967 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +7897 -0
- package/build/index.js.map +1 -0
- package/build/testing/index.d.ts +25 -0
- package/build/testing/index.d.ts.map +1 -0
- package/build/testing/index.js +72 -0
- package/build/testing/index.js.map +1 -0
- package/package.json +77 -3
- package/src/async/async-queue.ts +481 -0
- package/src/async/channels.ts +450 -0
- package/src/async/settled.ts +41 -0
- package/src/behaviors/bounded.ts +485 -0
- package/src/behaviors/composite.ts +99 -0
- package/src/behaviors/evented.ts +464 -0
- package/src/behaviors/lru.ts +449 -0
- package/src/behaviors/readonly.ts +236 -0
- package/src/behaviors/safe.ts +358 -0
- package/src/behaviors/synchronized.ts +316 -0
- package/src/behaviors/ttl.ts +464 -0
- package/src/core/effects.ts +327 -0
- package/src/core/events.ts +363 -0
- package/src/core/middleware.ts +413 -0
- package/src/core/policies.ts +316 -0
- package/src/core/traits.ts +164 -0
- package/src/dag/builder.ts +124 -0
- package/src/dag/executor.ts +190 -0
- package/src/dag/index.ts +14 -0
- package/src/dag/traversal.ts +129 -0
- package/src/dag/types.ts +87 -0
- package/src/fx/collectors.ts +500 -0
- package/src/fx/iter.ts +536 -0
- package/src/graph/hub-order.ts +53 -0
- package/src/graph/index.ts +5 -0
- package/src/impl/array-deque.ts +463 -0
- package/src/impl/array-list.ts +490 -0
- package/src/impl/hash-map.ts +623 -0
- package/src/impl/hash-set.ts +342 -0
- package/src/impl/linked-hash-map.ts +572 -0
- package/src/impl/linked-list.ts +610 -0
- package/src/impl/priority-queue.ts +448 -0
- package/src/impl/summary-tree.ts +594 -0
- package/src/impl/tree-map.ts +913 -0
- package/src/impl/tree-set.ts +469 -0
- package/src/index.ts +237 -0
- package/src/interfaces/collection.ts +141 -0
- package/src/interfaces/list.ts +281 -0
- package/src/interfaces/map.ts +405 -0
- package/src/interfaces/queue.ts +353 -0
- package/src/interfaces/set.ts +158 -0
- package/src/order/index.ts +6 -0
- package/src/order/rank.ts +271 -0
- package/src/storage/listeners.ts +72 -0
- package/src/testing/check.ts +81 -0
- package/src/testing/index.ts +7 -0
- package/src/utils/defaults.ts +443 -0
- package/src/utils/factories.ts +357 -0
- package/src/utils/helpers.ts +248 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mark Russell
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,79 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @mark1russell7/collections
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@mark1russell7/collections)
|
|
4
|
+
|
|
5
|
+
Collections for TypeScript with no dependencies: lists, maps, queues, a summary tree, channels, lazy operations and dependency graphs.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm i @mark1russell7/collections
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Lists, maps and behaviors
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { arrayList, compose, hashMap, lruMap } from "@mark1russell7/collections";
|
|
15
|
+
|
|
16
|
+
const list = arrayList<number>();
|
|
17
|
+
list.add(1);
|
|
18
|
+
list.add(2);
|
|
19
|
+
|
|
20
|
+
const cache = compose(lruMap<string, number>({ capacity: 2 }))(hashMap<string, number>());
|
|
21
|
+
cache.set("a", 1);
|
|
22
|
+
cache.set("b", 2);
|
|
23
|
+
cache.set("c", 3);
|
|
24
|
+
cache.has("a"); // false: the map removed the oldest key
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## The summary tree
|
|
28
|
+
|
|
29
|
+
A summary tree is a persistent sequence. Each node keeps the sum of its leaves in a monoid. Each change gives a new tree in logarithmic time, and the old tree stays the same.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { summaryTree, type SummarySpec } from "@mark1russell7/collections";
|
|
33
|
+
|
|
34
|
+
const text: SummarySpec<string, string> = {
|
|
35
|
+
monoid: { empty: "", combine: (a, b) => a + b },
|
|
36
|
+
measure: (leaf) => leaf,
|
|
37
|
+
};
|
|
38
|
+
const tree = summaryTree(text, ["a", "b", "c"]);
|
|
39
|
+
tree.summary; // "abc"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Dependency graphs
|
|
43
|
+
|
|
44
|
+
The entry `./dag` gives the levels of a graph, and a run of each level with a limit of concurrency.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { buildLeveledDAG, getTopologicalOrder } from "@mark1russell7/collections/dag";
|
|
48
|
+
|
|
49
|
+
const nodes = new Map([
|
|
50
|
+
["app", { id: "app", dependencies: ["lib"] }],
|
|
51
|
+
["lib", { id: "lib", dependencies: [] }],
|
|
52
|
+
]);
|
|
53
|
+
getTopologicalOrder(buildLeveledDAG(nodes)).map((node) => node.id); // ["lib", "app"]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The entry `./graph` gives `hubBreadthFirstOrder`: the order of a weighted graph, from its largest hub, with no other imports.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { hubBreadthFirstOrder } from "@mark1russell7/collections/graph";
|
|
60
|
+
|
|
61
|
+
hubBreadthFirstOrder(3, [{ a: 0, b: 1 }, { a: 1, b: 2 }]); // [1, 0, 2]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Contract checks
|
|
65
|
+
|
|
66
|
+
The entry `./testing` gives the small checks of a contract suite: `holds`, `same`, `rejects`, `deepEqual`, `show` and `ContractViolation`. These checks import no test framework.
|
|
67
|
+
|
|
68
|
+
## Entries
|
|
69
|
+
|
|
70
|
+
| Entry | Contents |
|
|
71
|
+
| --- | --- |
|
|
72
|
+
| `@mark1russell7/collections` | Lists, sets, maps, queues, the summary tree, behaviors, channels, lazy operations, `keyBetween` and the other functions of ranks |
|
|
73
|
+
| `@mark1russell7/collections/dag` | Levels, topological order, a run with a limit of concurrency, filters |
|
|
74
|
+
| `@mark1russell7/collections/graph` | `hubBreadthFirstOrder` |
|
|
75
|
+
| `@mark1russell7/collections/testing` | The checks of a contract suite |
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic DAG Types
|
|
3
|
+
*
|
|
4
|
+
* These types are designed to be reusable across different use cases.
|
|
5
|
+
* For ecosystem-specific DAG building, see client-lib.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* A node in a dependency DAG
|
|
9
|
+
*/
|
|
10
|
+
interface DAGNode<TData = unknown> {
|
|
11
|
+
/** Unique identifier for this node */
|
|
12
|
+
id: string;
|
|
13
|
+
/** Names/IDs of nodes this node depends on */
|
|
14
|
+
dependencies: string[];
|
|
15
|
+
/** Topological level (0 = leaves, computed by Kahn's algorithm) */
|
|
16
|
+
level?: number;
|
|
17
|
+
/** Optional user data associated with this node */
|
|
18
|
+
data?: TData | undefined;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A dependency DAG with level information for parallel execution
|
|
22
|
+
*/
|
|
23
|
+
interface DependencyDAG<TNode extends DAGNode = DAGNode> {
|
|
24
|
+
/** All nodes in the DAG */
|
|
25
|
+
nodes: Map<string, TNode>;
|
|
26
|
+
/** Nodes grouped by level for parallel execution */
|
|
27
|
+
levels: TNode[][];
|
|
28
|
+
/** Root nodes (no dependents in the DAG) */
|
|
29
|
+
roots: string[];
|
|
30
|
+
/** Leaf nodes (no dependencies in the DAG) */
|
|
31
|
+
leaves: string[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Options for DAG execution
|
|
35
|
+
*/
|
|
36
|
+
interface DAGExecutionOptions<TNode extends DAGNode = DAGNode> {
|
|
37
|
+
/** Max parallel operations per level (default: 4) */
|
|
38
|
+
concurrency?: number;
|
|
39
|
+
/** When this value is true, the run stops on the first error. The default is true. */
|
|
40
|
+
failFast?: boolean;
|
|
41
|
+
/** Callback for progress reporting */
|
|
42
|
+
onNodeStart?: (node: TNode) => void;
|
|
43
|
+
/** Callback when a node completes */
|
|
44
|
+
onNodeComplete?: (result: NodeResult<TNode>) => void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Result of processing a single node
|
|
48
|
+
*/
|
|
49
|
+
interface NodeResult<TNode extends DAGNode = DAGNode> {
|
|
50
|
+
/** The node that was processed */
|
|
51
|
+
node: TNode;
|
|
52
|
+
/** If processing succeeded */
|
|
53
|
+
success: boolean;
|
|
54
|
+
/** Error if failed */
|
|
55
|
+
error?: Error | undefined;
|
|
56
|
+
/** Duration in milliseconds */
|
|
57
|
+
duration: number;
|
|
58
|
+
/** This value logs from processing */
|
|
59
|
+
logs: string[];
|
|
60
|
+
/** Optional output from the processor */
|
|
61
|
+
output?: unknown | undefined;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The result of a run of an entire DAG
|
|
65
|
+
*/
|
|
66
|
+
interface DAGResult<TNode extends DAGNode = DAGNode> {
|
|
67
|
+
/** Overall success */
|
|
68
|
+
success: boolean;
|
|
69
|
+
/** Results for each node */
|
|
70
|
+
results: Map<string, NodeResult<TNode>>;
|
|
71
|
+
/** Names of failed nodes */
|
|
72
|
+
failedNodes: string[];
|
|
73
|
+
/** Total duration in milliseconds */
|
|
74
|
+
totalDuration: number;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Processor function type
|
|
78
|
+
*/
|
|
79
|
+
type NodeProcessor<TNode extends DAGNode = DAGNode> = (node: TNode) => Promise<NodeResult<TNode>>;
|
|
80
|
+
/**
|
|
81
|
+
* Build a leveled DAG from nodes using Kahn's algorithm
|
|
82
|
+
*
|
|
83
|
+
* This assigns levels to each node where:
|
|
84
|
+
* - Level 0: Nodes with no dependencies (leaves)
|
|
85
|
+
* - Level N: Nodes whose dependencies are all at level < N
|
|
86
|
+
*
|
|
87
|
+
* Nodes at the same level can be processed in parallel.
|
|
88
|
+
*
|
|
89
|
+
* @throws Error if a cycle is detected
|
|
90
|
+
*/
|
|
91
|
+
export declare function buildLeveledDAG<TNode extends DAGNode>(nodes: Map<string, TNode>): DependencyDAG<TNode>;
|
|
92
|
+
/**
|
|
93
|
+
* Get the topological order (flat list, leaves first)
|
|
94
|
+
*/
|
|
95
|
+
export declare function getTopologicalOrder<TNode extends DAGNode>(dag: DependencyDAG<TNode>): TNode[];
|
|
96
|
+
/**
|
|
97
|
+
* Visualize the DAG structure (for debugging)
|
|
98
|
+
*/
|
|
99
|
+
export declare function visualizeDAG<TNode extends DAGNode>(dag: DependencyDAG<TNode>): string;
|
|
100
|
+
/**
|
|
101
|
+
* Start DAG with level-based parallelization
|
|
102
|
+
*
|
|
103
|
+
* - Levels are processed sequentially (level 0 first, then level 1, and so on)
|
|
104
|
+
* - Within each level, nodes are processed in parallel up to concurrency limit
|
|
105
|
+
* - Supports fail-fast or continue-on-error modes
|
|
106
|
+
*
|
|
107
|
+
* A processor that throws does NOT reject the whole run. The run records the throw
|
|
108
|
+
* as a failed NodeResult, so partial results are always given.
|
|
109
|
+
*
|
|
110
|
+
* `failFast` operates at level granularity. When a node in a level fails, each
|
|
111
|
+
* node already in flight in that level continues to completion. No later level
|
|
112
|
+
* starts. (There is no mid-level cancellation.)
|
|
113
|
+
*/
|
|
114
|
+
export declare function executeDAG<TNode extends DAGNode>(dag: DependencyDAG<TNode>, processor: (node: TNode) => Promise<NodeResult<TNode>>, options?: DAGExecutionOptions<TNode>): Promise<DAGResult<TNode>>;
|
|
115
|
+
/**
|
|
116
|
+
* Start DAG sequentially (no parallelization)
|
|
117
|
+
*/
|
|
118
|
+
export declare function executeDAGSequential<TNode extends DAGNode>(dag: DependencyDAG<TNode>, processor: (node: TNode) => Promise<NodeResult<TNode>>, options?: Omit<DAGExecutionOptions<TNode>, "concurrency">): Promise<DAGResult<TNode>>;
|
|
119
|
+
/**
|
|
120
|
+
* Make a simple processor that wraps an async function
|
|
121
|
+
*/
|
|
122
|
+
export declare function createProcessor<TNode extends DAGNode>(fn: (node: TNode) => Promise<void>): (node: TNode) => Promise<NodeResult<TNode>>;
|
|
123
|
+
/**
|
|
124
|
+
* Filter DAG to only include nodes reachable from a root
|
|
125
|
+
*/
|
|
126
|
+
export declare function filterDAGFromRoot<TNode extends DAGNode>(nodes: Map<string, TNode>, rootId: string): Map<string, TNode>;
|
|
127
|
+
/**
|
|
128
|
+
* Get all ancestors of a node (nodes that this node depends on, transitively)
|
|
129
|
+
*/
|
|
130
|
+
export declare function getAncestors<TNode extends DAGNode>(nodes: Map<string, TNode>, id: string): Set<string>;
|
|
131
|
+
/**
|
|
132
|
+
* Get all descendants of a node (nodes that depend on this node, transitively)
|
|
133
|
+
*/
|
|
134
|
+
export declare function getDescendants<TNode extends DAGNode>(nodes: Map<string, TNode>, id: string): Set<string>;
|
|
135
|
+
/**
|
|
136
|
+
* Make a DAG node from minimal input
|
|
137
|
+
*/
|
|
138
|
+
export declare function createNode<TData = unknown>(id: string, dependencies?: string[], data?: TData | undefined): DAGNode<TData>;
|
|
139
|
+
/**
|
|
140
|
+
* Build a Map of nodes from an array
|
|
141
|
+
*/
|
|
142
|
+
export declare function buildNodeMap<TNode extends DAGNode>(nodes: TNode[]): Map<string, TNode>;
|
|
143
|
+
export type { DAGExecutionOptions, DAGNode, DAGResult, DependencyDAG, NodeProcessor, NodeResult };
|
|
144
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/dag/types.ts","../../src/dag/traversal.ts","../../src/dag/executor.ts","../../src/dag/builder.ts"],"mappings":";;;;;;;;;UAUiB,QAAQ;;EAEvB;;EAEA;;EAEA;;EAEA,OAAO;;;;;UAMQ,cAAc,cAAc,UAAU;;EAErD,OAAO,YAAY;;EAEnB,QAAQ;;EAER;;EAEA;;;;;UAMe,oBAAoB,cAAc,UAAU;;EAE3D;;EAEA;;EAEA,eAAe,MAAM;;EAErB,kBAAkB,QAAQ,WAAW;;;;;UAMtB,WAAW,cAAc,UAAU;;EAElD,MAAM;;EAEN;;EAEA,QAAQ;;EAER;;EAEA;;EAEA;;;;;UAMe,UAAU,cAAc,UAAU;;EAEjD;;EAEA,SAAS,YAAY,WAAW;;EAEhC;;EAEA;;;;;KAMU,cAAc,cAAc,UAAU,YAChD,MAAM,UACH,QAAQ,WAAW;;;;;;;;;;;;wBClER,gBAAgB,cAAc,SAC5C,OAAO,YAAY,SAClB,cAAc;;;;wBA+ED,oBAAoB,cAAc,SAChD,KAAK,cAAc,SAClB;;;;wBAOa,aAAa,cAAc,SACzC,KAAK,cAAc;;;;;;;;;;;;;;;wBCrCC,WAAW,cAAc,SAC7C,KAAK,cAAc,QACnB,YAAY,MAAM,UAAU,QAAQ,WAAW,SAC/C,UAAS,oBAAoB,SAC5B,QAAQ,UAAU;;;;wBAqEC,qBAAqB,cAAc,SACvD,KAAK,cAAc,QACnB,YAAY,MAAM,UAAU,QAAQ,WAAW,SAC/C,UAAS,KAAK,oBAAoB,yBACjC,QAAQ,UAAU;;;;wBAOL,gBAAgB,cAAc,SAC5C,KAAK,MAAM,UAAU,iBACnB,MAAM,UAAU,QAAQ,WAAW;;;;wBCvJvB,kBAAkB,cAAc,SAC9C,OAAO,YAAY,QACnB,iBACC,YAAY;;;;wBAyBC,aAAa,cAAc,SACzC,OAAO,YAAY,QACnB,aACC;;;;wBA0Ba,eAAe,cAAc,SAC3C,OAAO,YAAY,QACnB,aACC;;;;wBAmCa,WAAW,iBACzB,YACA,yBACA,OAAO,oBACN,QAAQ;;;;wBAWK,aAAa,cAAc,SACzC,OAAO,UACN,YAAY"}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// src/dag/traversal.ts
|
|
2
|
+
/**
|
|
3
|
+
* Build a leveled DAG from nodes using Kahn's algorithm
|
|
4
|
+
*
|
|
5
|
+
* This assigns levels to each node where:
|
|
6
|
+
* - Level 0: Nodes with no dependencies (leaves)
|
|
7
|
+
* - Level N: Nodes whose dependencies are all at level < N
|
|
8
|
+
*
|
|
9
|
+
* Nodes at the same level can be processed in parallel.
|
|
10
|
+
*
|
|
11
|
+
* @throws Error if a cycle is detected
|
|
12
|
+
*/
|
|
13
|
+
function buildLeveledDAG(nodes) {
|
|
14
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
15
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
16
|
+
for (const [name, node] of nodes) {
|
|
17
|
+
const depsInDag = new Set(node.dependencies.filter((dep) => nodes.has(dep)));
|
|
18
|
+
inDegree.set(name, depsInDag.size);
|
|
19
|
+
dependents.set(name, /* @__PURE__ */ new Set());
|
|
20
|
+
}
|
|
21
|
+
for (const [name, node] of nodes) for (const dep of node.dependencies) if (dependents.has(dep)) dependents.get(dep).add(name);
|
|
22
|
+
let queue = [];
|
|
23
|
+
for (const [name, degree] of inDegree) if (degree === 0) queue.push(name);
|
|
24
|
+
const levels = [];
|
|
25
|
+
const nodeLevel = /* @__PURE__ */ new Map();
|
|
26
|
+
let processedCount = 0;
|
|
27
|
+
while (queue.length > 0) {
|
|
28
|
+
const currentLevel = levels.length;
|
|
29
|
+
const levelNodes = [];
|
|
30
|
+
const nextQueue = [];
|
|
31
|
+
for (const name of queue) {
|
|
32
|
+
const node = nodes.get(name);
|
|
33
|
+
node.level = currentLevel;
|
|
34
|
+
nodeLevel.set(name, currentLevel);
|
|
35
|
+
levelNodes.push(node);
|
|
36
|
+
processedCount++;
|
|
37
|
+
for (const dependent of dependents.get(name) ?? []) {
|
|
38
|
+
const newDegree = inDegree.get(dependent) - 1;
|
|
39
|
+
inDegree.set(dependent, newDegree);
|
|
40
|
+
if (newDegree === 0) nextQueue.push(dependent);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
levels.push(levelNodes);
|
|
44
|
+
queue = nextQueue;
|
|
45
|
+
}
|
|
46
|
+
if (processedCount !== nodes.size) {
|
|
47
|
+
const cycleNodes = [...nodes.keys()].filter((n) => !nodeLevel.has(n));
|
|
48
|
+
throw new Error("Circular dependency detected involving: " + cycleNodes.join(", "));
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
nodes,
|
|
52
|
+
levels,
|
|
53
|
+
roots: [...nodes.keys()].filter((name) => {
|
|
54
|
+
const deps = dependents.get(name);
|
|
55
|
+
return !deps || deps.size === 0;
|
|
56
|
+
}),
|
|
57
|
+
leaves: [...nodes.keys()].filter((name) => {
|
|
58
|
+
return nodes.get(name).dependencies.filter((dep) => nodes.has(dep)).length === 0;
|
|
59
|
+
})
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get the topological order (flat list, leaves first)
|
|
64
|
+
*/
|
|
65
|
+
function getTopologicalOrder(dag) {
|
|
66
|
+
return dag.levels.flat();
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Visualize the DAG structure (for debugging)
|
|
70
|
+
*/
|
|
71
|
+
function visualizeDAG(dag) {
|
|
72
|
+
const lines = ["DAG Structure:"];
|
|
73
|
+
for (let i = 0; i < dag.levels.length; i++) {
|
|
74
|
+
const level = dag.levels[i];
|
|
75
|
+
lines.push(" Level " + i + ":");
|
|
76
|
+
for (const node of level) {
|
|
77
|
+
const deps = node.dependencies.length > 0 ? " -> [" + node.dependencies.join(", ") + "]" : " (leaf)";
|
|
78
|
+
lines.push(" - " + node.id + deps);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return lines.join("\n");
|
|
82
|
+
}
|
|
83
|
+
// src/dag/executor.ts
|
|
84
|
+
/**
|
|
85
|
+
* Start a batch of items with concurrency limit
|
|
86
|
+
*/
|
|
87
|
+
async function executeWithConcurrency(items, fn, limit) {
|
|
88
|
+
const results = [];
|
|
89
|
+
const executing = /* @__PURE__ */ new Set();
|
|
90
|
+
for (const item of items) {
|
|
91
|
+
const promise = fn(item).then((result) => {
|
|
92
|
+
results.push(result);
|
|
93
|
+
}).finally(() => {
|
|
94
|
+
executing.delete(promise);
|
|
95
|
+
});
|
|
96
|
+
executing.add(promise);
|
|
97
|
+
if (executing.size >= limit) await Promise.race(executing);
|
|
98
|
+
}
|
|
99
|
+
await Promise.all(executing);
|
|
100
|
+
return results;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Call a listener. A listener that throws does not stop the run: the function gives a log
|
|
104
|
+
* line for its error (deep dive DATA-21).
|
|
105
|
+
*/
|
|
106
|
+
function notify(name, listener, arg) {
|
|
107
|
+
if (!listener) return void 0;
|
|
108
|
+
try {
|
|
109
|
+
listener(arg);
|
|
110
|
+
return;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return name + " threw: " + (error instanceof Error ? error.message : String(error));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Start DAG with level-based parallelization
|
|
117
|
+
*
|
|
118
|
+
* - Levels are processed sequentially (level 0 first, then level 1, and so on)
|
|
119
|
+
* - Within each level, nodes are processed in parallel up to concurrency limit
|
|
120
|
+
* - Supports fail-fast or continue-on-error modes
|
|
121
|
+
*
|
|
122
|
+
* A processor that throws does NOT reject the whole run. The run records the throw
|
|
123
|
+
* as a failed NodeResult, so partial results are always given.
|
|
124
|
+
*
|
|
125
|
+
* `failFast` operates at level granularity. When a node in a level fails, each
|
|
126
|
+
* node already in flight in that level continues to completion. No later level
|
|
127
|
+
* starts. (There is no mid-level cancellation.)
|
|
128
|
+
*/
|
|
129
|
+
async function executeDAG(dag, processor, options = {}) {
|
|
130
|
+
const { concurrency = 4, failFast = true, onNodeStart, onNodeComplete } = options;
|
|
131
|
+
const results = /* @__PURE__ */ new Map();
|
|
132
|
+
const failedNodes = [];
|
|
133
|
+
const startTime = Date.now();
|
|
134
|
+
let shouldStop = false;
|
|
135
|
+
for (const level of dag.levels) {
|
|
136
|
+
if (shouldStop) break;
|
|
137
|
+
const levelResults = await executeWithConcurrency(level, async (node) => {
|
|
138
|
+
const startError = notify("onNodeStart", onNodeStart, node);
|
|
139
|
+
const nodeStart = Date.now();
|
|
140
|
+
let result;
|
|
141
|
+
try {
|
|
142
|
+
result = await processor(node);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
145
|
+
result = {
|
|
146
|
+
node,
|
|
147
|
+
success: false,
|
|
148
|
+
error: err,
|
|
149
|
+
duration: Date.now() - nodeStart,
|
|
150
|
+
logs: ["Processor threw: " + err.message]
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const listenerErrors = [startError, notify("onNodeComplete", onNodeComplete, result)].filter((line) => line !== void 0);
|
|
154
|
+
if (listenerErrors.length > 0) result = {
|
|
155
|
+
...result,
|
|
156
|
+
logs: [...result.logs ?? [], ...listenerErrors]
|
|
157
|
+
};
|
|
158
|
+
return result;
|
|
159
|
+
}, concurrency);
|
|
160
|
+
for (const result of levelResults) {
|
|
161
|
+
results.set(result.node.id, result);
|
|
162
|
+
if (!result.success) {
|
|
163
|
+
failedNodes.push(result.node.id);
|
|
164
|
+
if (failFast) shouldStop = true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
success: failedNodes.length === 0,
|
|
170
|
+
results,
|
|
171
|
+
failedNodes,
|
|
172
|
+
totalDuration: Date.now() - startTime
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Start DAG sequentially (no parallelization)
|
|
177
|
+
*/
|
|
178
|
+
async function executeDAGSequential(dag, processor, options = {}) {
|
|
179
|
+
return executeDAG(dag, processor, {
|
|
180
|
+
...options,
|
|
181
|
+
concurrency: 1
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Make a simple processor that wraps an async function
|
|
186
|
+
*/
|
|
187
|
+
function createProcessor(fn) {
|
|
188
|
+
return async (node) => {
|
|
189
|
+
const startTime = Date.now();
|
|
190
|
+
const logs = [];
|
|
191
|
+
try {
|
|
192
|
+
logs.push("Starting " + node.id);
|
|
193
|
+
await fn(node);
|
|
194
|
+
logs.push("Completed " + node.id);
|
|
195
|
+
return {
|
|
196
|
+
node,
|
|
197
|
+
success: true,
|
|
198
|
+
duration: Date.now() - startTime,
|
|
199
|
+
logs
|
|
200
|
+
};
|
|
201
|
+
} catch (error) {
|
|
202
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
203
|
+
logs.push("Failed " + node.id + ": " + err.message);
|
|
204
|
+
return {
|
|
205
|
+
node,
|
|
206
|
+
success: false,
|
|
207
|
+
error: err,
|
|
208
|
+
duration: Date.now() - startTime,
|
|
209
|
+
logs
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
// src/dag/builder.ts
|
|
215
|
+
/**
|
|
216
|
+
* Filter DAG to only include nodes reachable from a root
|
|
217
|
+
*/
|
|
218
|
+
function filterDAGFromRoot(nodes, rootId) {
|
|
219
|
+
const filtered = /* @__PURE__ */ new Map();
|
|
220
|
+
const visited = /* @__PURE__ */ new Set();
|
|
221
|
+
function visit(id) {
|
|
222
|
+
if (visited.has(id)) return;
|
|
223
|
+
visited.add(id);
|
|
224
|
+
const node = nodes.get(id);
|
|
225
|
+
if (!node) return;
|
|
226
|
+
filtered.set(id, node);
|
|
227
|
+
for (const dep of node.dependencies) visit(dep);
|
|
228
|
+
}
|
|
229
|
+
visit(rootId);
|
|
230
|
+
return filtered;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Get all ancestors of a node (nodes that this node depends on, transitively)
|
|
234
|
+
*/
|
|
235
|
+
function getAncestors(nodes, id) {
|
|
236
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
237
|
+
const visited = /* @__PURE__ */ new Set();
|
|
238
|
+
function visit(n) {
|
|
239
|
+
if (visited.has(n)) return;
|
|
240
|
+
visited.add(n);
|
|
241
|
+
const node = nodes.get(n);
|
|
242
|
+
if (!node) return;
|
|
243
|
+
for (const dep of node.dependencies) if (nodes.has(dep)) {
|
|
244
|
+
ancestors.add(dep);
|
|
245
|
+
visit(dep);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
visit(id);
|
|
249
|
+
return ancestors;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Get all descendants of a node (nodes that depend on this node, transitively)
|
|
253
|
+
*/
|
|
254
|
+
function getDescendants(nodes, id) {
|
|
255
|
+
const descendants = /* @__PURE__ */ new Set();
|
|
256
|
+
const visited = /* @__PURE__ */ new Set();
|
|
257
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
258
|
+
for (const [nodeId, node] of nodes) for (const dep of node.dependencies) {
|
|
259
|
+
if (!dependents.has(dep)) dependents.set(dep, /* @__PURE__ */ new Set());
|
|
260
|
+
dependents.get(dep).add(nodeId);
|
|
261
|
+
}
|
|
262
|
+
function visit(n) {
|
|
263
|
+
if (visited.has(n)) return;
|
|
264
|
+
visited.add(n);
|
|
265
|
+
const deps = dependents.get(n);
|
|
266
|
+
if (!deps) return;
|
|
267
|
+
for (const dep of deps) {
|
|
268
|
+
descendants.add(dep);
|
|
269
|
+
visit(dep);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
visit(id);
|
|
273
|
+
return descendants;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Make a DAG node from minimal input
|
|
277
|
+
*/
|
|
278
|
+
function createNode(id, dependencies = [], data) {
|
|
279
|
+
const node = {
|
|
280
|
+
id,
|
|
281
|
+
dependencies
|
|
282
|
+
};
|
|
283
|
+
if (data !== void 0) node.data = data;
|
|
284
|
+
return node;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Build a Map of nodes from an array
|
|
288
|
+
*/
|
|
289
|
+
function buildNodeMap(nodes) {
|
|
290
|
+
return new Map(nodes.map((n) => [n.id, n]));
|
|
291
|
+
}
|
|
292
|
+
export { buildLeveledDAG, buildNodeMap, createNode, createProcessor, executeDAG, executeDAGSequential, filterDAGFromRoot, getAncestors, getDescendants, getTopologicalOrder, visualizeDAG };
|
|
293
|
+
|
|
294
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/dag/traversal.ts","../../src/dag/executor.ts","../../src/dag/builder.ts"],"sourcesContent":["/**\n * DAG Traversal using Kahn's Algorithm\n *\n * This module implements topological sort with level assignment for parallel execution.\n * Level 0 = leaves (no dependencies), higher levels depend on lower levels.\n */\n\nimport type { DAGNode, DependencyDAG } from \"./types.js\";\n\n/**\n * Build a leveled DAG from nodes using Kahn's algorithm\n *\n * This assigns levels to each node where:\n * - Level 0: Nodes with no dependencies (leaves)\n * - Level N: Nodes whose dependencies are all at level < N\n *\n * Nodes at the same level can be processed in parallel.\n *\n * @throws Error if a cycle is detected\n */\nexport function buildLeveledDAG<TNode extends DAGNode>(\n nodes: Map<string, TNode>\n): DependencyDAG<TNode> {\n const inDegree = new Map<string, number>();\n const dependents = new Map<string, Set<string>>();\n\n for (const [name, node] of nodes) {\n // A set: a dependency that a node lists twice counts once. (`dependents` is a set, so it\n // takes the in-degree down once: a repeated dependency was \"Circular\": deep dive DATA-21.)\n const depsInDag = new Set(node.dependencies.filter((dep) => nodes.has(dep)));\n inDegree.set(name, depsInDag.size);\n dependents.set(name, new Set());\n }\n\n for (const [name, node] of nodes) {\n for (const dep of node.dependencies) {\n if (dependents.has(dep)) {\n dependents.get(dep)!.add(name);\n }\n }\n }\n\n let queue: string[] = [];\n for (const [name, degree] of inDegree) {\n if (degree === 0) {\n queue.push(name);\n }\n }\n\n const levels: TNode[][] = [];\n const nodeLevel = new Map<string, number>();\n let processedCount = 0;\n\n while (queue.length > 0) {\n const currentLevel = levels.length;\n const levelNodes: TNode[] = [];\n const nextQueue: string[] = [];\n\n for (const name of queue) {\n const node = nodes.get(name)!;\n node.level = currentLevel;\n nodeLevel.set(name, currentLevel);\n levelNodes.push(node);\n processedCount++;\n\n for (const dependent of dependents.get(name) ?? []) {\n const newDegree = inDegree.get(dependent)! - 1;\n inDegree.set(dependent, newDegree);\n\n if (newDegree === 0) {\n nextQueue.push(dependent);\n }\n }\n }\n\n levels.push(levelNodes);\n queue = nextQueue;\n }\n\n if (processedCount !== nodes.size) {\n const cycleNodes = [...nodes.keys()].filter((n) => !nodeLevel.has(n));\n throw new Error(\"Circular dependency detected involving: \" + cycleNodes.join(\", \"));\n }\n\n const roots = [...nodes.keys()].filter((name) => {\n const deps = dependents.get(name);\n return !deps || deps.size === 0;\n });\n\n const leaves = [...nodes.keys()].filter((name) => {\n const node = nodes.get(name)!;\n const depsInDag = node.dependencies.filter((dep) => nodes.has(dep));\n return depsInDag.length === 0;\n });\n\n return { nodes, levels, roots, leaves };\n}\n\n/**\n * Get the topological order (flat list, leaves first)\n */\nexport function getTopologicalOrder<TNode extends DAGNode>(\n dag: DependencyDAG<TNode>\n): TNode[] {\n return dag.levels.flat();\n}\n\n/**\n * Visualize the DAG structure (for debugging)\n */\nexport function visualizeDAG<TNode extends DAGNode>(\n dag: DependencyDAG<TNode>\n): string {\n const lines = [\"DAG Structure:\"];\n\n for (let i = 0; i < dag.levels.length; i++) {\n const level = dag.levels[i]!;\n lines.push(\" Level \" + i + \":\");\n for (const node of level) {\n const deps =\n node.dependencies.length > 0\n ? \" -> [\" + node.dependencies.join(\", \") + \"]\"\n : \" (leaf)\";\n lines.push(\" - \" + node.id + deps);\n }\n }\n\n return lines.join(\"\\n\");\n}\n","/**\n * DAG Executor with parallel level-based execution\n */\n\nimport type {\n DependencyDAG,\n DAGNode,\n DAGExecutionOptions,\n NodeResult,\n DAGResult,\n} from \"./types.js\";\n\n/**\n * Start a batch of items with concurrency limit\n */\nasync function executeWithConcurrency<T, R>(\n items: T[],\n fn: (item: T) => Promise<R>,\n limit: number\n): Promise<R[]> {\n const results: R[] = [];\n const executing = new Set<Promise<void>>();\n\n for (const item of items) {\n const promise = fn(item)\n .then((result) => {\n results.push(result);\n })\n .finally(() => {\n // Rejection-safe cleanup: always remove the settled promise from the\n // executing set, even if `fn` rejected, so the set never leaks a\n // settled (possibly rejected) promise into later `race`/`all` waits.\n executing.delete(promise);\n });\n\n executing.add(promise);\n\n if (executing.size >= limit) {\n await Promise.race(executing);\n }\n }\n\n await Promise.all(executing);\n return results;\n}\n\n/**\n * Call a listener. A listener that throws does not stop the run: the function gives a log\n * line for its error (deep dive DATA-21).\n */\nfunction notify<T>(name: string, listener: ((arg: T) => void) | undefined, arg: T): string | undefined {\n if (!listener) return undefined;\n try {\n listener(arg);\n return undefined;\n } catch (error) {\n return name + \" threw: \" + (error instanceof Error ? error.message : String(error));\n }\n}\n\n/**\n * Start DAG with level-based parallelization\n *\n * - Levels are processed sequentially (level 0 first, then level 1, and so on)\n * - Within each level, nodes are processed in parallel up to concurrency limit\n * - Supports fail-fast or continue-on-error modes\n *\n * A processor that throws does NOT reject the whole run. The run records the throw\n * as a failed NodeResult, so partial results are always given.\n *\n * `failFast` operates at level granularity. When a node in a level fails, each\n * node already in flight in that level continues to completion. No later level\n * starts. (There is no mid-level cancellation.)\n */\nexport async function executeDAG<TNode extends DAGNode>(\n dag: DependencyDAG<TNode>,\n processor: (node: TNode) => Promise<NodeResult<TNode>>,\n options: DAGExecutionOptions<TNode> = {}\n): Promise<DAGResult<TNode>> {\n const {\n concurrency = 4,\n failFast = true,\n onNodeStart,\n onNodeComplete,\n } = options;\n\n const results = new Map<string, NodeResult<TNode>>();\n const failedNodes: string[] = [];\n const startTime = Date.now();\n let shouldStop = false;\n\n for (const level of dag.levels) {\n if (shouldStop) break;\n\n const levelResults = await executeWithConcurrency(\n level,\n async (node) => {\n const startError = notify(\"onNodeStart\", onNodeStart, node);\n const nodeStart = Date.now();\n let result: NodeResult<TNode>;\n try {\n result = await processor(node);\n } catch (error) {\n // Convert a thrown processor error into a failed NodeResult instead\n // of rejecting the entire run and discarding partial results.\n const err =\n error instanceof Error ? error : new Error(String(error));\n result = {\n node,\n success: false,\n error: err,\n duration: Date.now() - nodeStart,\n logs: [\"Processor threw: \" + err.message],\n };\n }\n const completeError = notify(\"onNodeComplete\", onNodeComplete, result);\n const listenerErrors = [startError, completeError].filter((line): line is string => line !== undefined);\n if (listenerErrors.length > 0) {\n result = { ...result, logs: [...(result.logs ?? []), ...listenerErrors] };\n }\n return result;\n },\n concurrency\n );\n\n for (const result of levelResults) {\n results.set(result.node.id, result);\n if (!result.success) {\n failedNodes.push(result.node.id);\n if (failFast) {\n shouldStop = true;\n }\n }\n }\n }\n\n return {\n success: failedNodes.length === 0,\n results,\n failedNodes,\n totalDuration: Date.now() - startTime,\n };\n}\n\n/**\n * Start DAG sequentially (no parallelization)\n */\nexport async function executeDAGSequential<TNode extends DAGNode>(\n dag: DependencyDAG<TNode>,\n processor: (node: TNode) => Promise<NodeResult<TNode>>,\n options: Omit<DAGExecutionOptions<TNode>, \"concurrency\"> = {}\n): Promise<DAGResult<TNode>> {\n return executeDAG(dag, processor, { ...options, concurrency: 1 });\n}\n\n/**\n * Make a simple processor that wraps an async function\n */\nexport function createProcessor<TNode extends DAGNode>(\n fn: (node: TNode) => Promise<void>\n): (node: TNode) => Promise<NodeResult<TNode>> {\n return async (node: TNode): Promise<NodeResult<TNode>> => {\n const startTime = Date.now();\n const logs: string[] = [];\n\n try {\n logs.push(\"Starting \" + node.id);\n await fn(node);\n logs.push(\"Completed \" + node.id);\n\n return {\n node,\n success: true,\n duration: Date.now() - startTime,\n logs,\n };\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n logs.push(\"Failed \" + node.id + \": \" + err.message);\n\n return {\n node,\n success: false,\n error: err,\n duration: Date.now() - startTime,\n logs,\n };\n }\n };\n}\n","/**\n * DAG Builder utilities\n */\n\nimport type { DAGNode } from \"./types.js\";\n\n/**\n * Filter DAG to only include nodes reachable from a root\n */\nexport function filterDAGFromRoot<TNode extends DAGNode>(\n nodes: Map<string, TNode>,\n rootId: string\n): Map<string, TNode> {\n const filtered = new Map<string, TNode>();\n const visited = new Set<string>();\n\n function visit(id: string): void {\n if (visited.has(id)) return;\n visited.add(id);\n\n const node = nodes.get(id);\n if (!node) return;\n\n filtered.set(id, node);\n\n for (const dep of node.dependencies) {\n visit(dep);\n }\n }\n\n visit(rootId);\n return filtered;\n}\n\n/**\n * Get all ancestors of a node (nodes that this node depends on, transitively)\n */\nexport function getAncestors<TNode extends DAGNode>(\n nodes: Map<string, TNode>,\n id: string\n): Set<string> {\n const ancestors = new Set<string>();\n const visited = new Set<string>();\n\n function visit(n: string): void {\n if (visited.has(n)) return;\n visited.add(n);\n\n const node = nodes.get(n);\n if (!node) return;\n\n for (const dep of node.dependencies) {\n if (nodes.has(dep)) {\n ancestors.add(dep);\n visit(dep);\n }\n }\n }\n\n visit(id);\n return ancestors;\n}\n\n/**\n * Get all descendants of a node (nodes that depend on this node, transitively)\n */\nexport function getDescendants<TNode extends DAGNode>(\n nodes: Map<string, TNode>,\n id: string\n): Set<string> {\n const descendants = new Set<string>();\n const visited = new Set<string>();\n\n // Build reverse dependency map\n const dependents = new Map<string, Set<string>>();\n for (const [nodeId, node] of nodes) {\n for (const dep of node.dependencies) {\n if (!dependents.has(dep)) {\n dependents.set(dep, new Set());\n }\n dependents.get(dep)!.add(nodeId);\n }\n }\n\n function visit(n: string): void {\n if (visited.has(n)) return;\n visited.add(n);\n\n const deps = dependents.get(n);\n if (!deps) return;\n\n for (const dep of deps) {\n descendants.add(dep);\n visit(dep);\n }\n }\n\n visit(id);\n return descendants;\n}\n\n/**\n * Make a DAG node from minimal input\n */\nexport function createNode<TData = unknown>(\n id: string,\n dependencies: string[] = [],\n data?: TData | undefined\n): DAGNode<TData> {\n const node: DAGNode<TData> = { id, dependencies };\n if (data !== undefined) {\n node.data = data;\n }\n return node;\n}\n\n/**\n * Build a Map of nodes from an array\n */\nexport function buildNodeMap<TNode extends DAGNode>(\n nodes: TNode[]\n): Map<string, TNode> {\n return new Map(nodes.map((n) => [n.id, n]));\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** An undirected edge between the nodes `a` and `b`. The default weight is 1. */
|
|
2
|
+
interface WeightedEdge {
|
|
3
|
+
readonly a: number;
|
|
4
|
+
readonly b: number;
|
|
5
|
+
readonly weight?: number | undefined;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* This function gives a breadth-first order of the nodes `0` to `n - 1` of an undirected weighted graph.
|
|
9
|
+
*
|
|
10
|
+
* - The hub of a component is the node with the largest sum of edge weights. `compare`, then the lower index, breaks a tie.
|
|
11
|
+
* - The order starts at the hub of each component, and goes breadth-first. `compare`, then the lower index, orders the neighbours of a node.
|
|
12
|
+
* - Without edges, each node is a component, so the order is the order of `compare`.
|
|
13
|
+
* - An edge from a node to itself adds no weight.
|
|
14
|
+
* - A bad `n`, an index outside the nodes, or a weight that is not a finite number above 0, throws a `RangeError`.
|
|
15
|
+
*
|
|
16
|
+
* The function is pure. It keeps no state, and it does not change its arguments.
|
|
17
|
+
*/
|
|
18
|
+
export declare function hubBreadthFirstOrder(n: number, edges?: readonly WeightedEdge[], compare?: (i: number, j: number) => number): number[];
|
|
19
|
+
export type { WeightedEdge };
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/graph/hub-order.ts"],"mappings":";UACiB;WACN;WACA;WACA;;;;;;;;;;;;;wBAcK,qBAAqB,WAAW,iBAAgB,gBAAqB,WAAU,WAAW"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// src/graph/hub-order.ts
|
|
2
|
+
/**
|
|
3
|
+
* This function gives a breadth-first order of the nodes `0` to `n - 1` of an undirected weighted graph.
|
|
4
|
+
*
|
|
5
|
+
* - The hub of a component is the node with the largest sum of edge weights. `compare`, then the lower index, breaks a tie.
|
|
6
|
+
* - The order starts at the hub of each component, and goes breadth-first. `compare`, then the lower index, orders the neighbours of a node.
|
|
7
|
+
* - Without edges, each node is a component, so the order is the order of `compare`.
|
|
8
|
+
* - An edge from a node to itself adds no weight.
|
|
9
|
+
* - A bad `n`, an index outside the nodes, or a weight that is not a finite number above 0, throws a `RangeError`.
|
|
10
|
+
*
|
|
11
|
+
* The function is pure. It keeps no state, and it does not change its arguments.
|
|
12
|
+
*/
|
|
13
|
+
function hubBreadthFirstOrder(n, edges = [], compare = () => 0) {
|
|
14
|
+
if (!Number.isInteger(n) || n < 0) throw new RangeError(`The count of the nodes is a whole number from 0, not ${n}.`);
|
|
15
|
+
const degree = new Float64Array(n);
|
|
16
|
+
const next = Array.from({ length: n }, () => []);
|
|
17
|
+
for (const { a, b, weight = 1 } of edges) {
|
|
18
|
+
if (!Number.isInteger(a) || !Number.isInteger(b) || a < 0 || b < 0 || a >= n || b >= n) throw new RangeError(`An edge joins the nodes ${a} and ${b}, but the indexes go from 0 to ${n - 1}.`);
|
|
19
|
+
if (!Number.isFinite(weight) || weight <= 0) throw new RangeError(`The weight of an edge is a finite number above 0, not ${weight}.`);
|
|
20
|
+
if (a === b) continue;
|
|
21
|
+
degree[a] = degree[a] + weight;
|
|
22
|
+
degree[b] = degree[b] + weight;
|
|
23
|
+
next[a].push(b);
|
|
24
|
+
next[b].push(a);
|
|
25
|
+
}
|
|
26
|
+
const order = (i, j) => compare(i, j) || i - j;
|
|
27
|
+
const hubs = Array.from({ length: n }, (_, i) => i).sort((i, j) => degree[j] - degree[i] || order(i, j));
|
|
28
|
+
const seen = new Uint8Array(n);
|
|
29
|
+
const out = [];
|
|
30
|
+
for (const hub of hubs) {
|
|
31
|
+
if (seen[hub] === 1) continue;
|
|
32
|
+
seen[hub] = 1;
|
|
33
|
+
const queue = [hub];
|
|
34
|
+
for (let k = 0; k < queue.length; k++) {
|
|
35
|
+
const i = queue[k];
|
|
36
|
+
out.push(i);
|
|
37
|
+
for (const j of next[i].toSorted(order)) {
|
|
38
|
+
if (seen[j] === 1) continue;
|
|
39
|
+
seen[j] = 1;
|
|
40
|
+
queue.push(j);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
export { hubBreadthFirstOrder };
|
|
47
|
+
|
|
48
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/graph/hub-order.ts"],"sourcesContent":["/** An undirected edge between the nodes `a` and `b`. The default weight is 1. */\nexport interface WeightedEdge {\n readonly a: number;\n readonly b: number;\n readonly weight?: number | undefined;\n}\n\n/**\n * This function gives a breadth-first order of the nodes `0` to `n - 1` of an undirected weighted graph.\n *\n * - The hub of a component is the node with the largest sum of edge weights. `compare`, then the lower index, breaks a tie.\n * - The order starts at the hub of each component, and goes breadth-first. `compare`, then the lower index, orders the neighbours of a node.\n * - Without edges, each node is a component, so the order is the order of `compare`.\n * - An edge from a node to itself adds no weight.\n * - A bad `n`, an index outside the nodes, or a weight that is not a finite number above 0, throws a `RangeError`.\n *\n * The function is pure. It keeps no state, and it does not change its arguments.\n */\nexport function hubBreadthFirstOrder(n: number, edges: readonly WeightedEdge[] = [], compare: (i: number, j: number) => number = () => 0): number[] {\n if (!Number.isInteger(n) || n < 0) throw new RangeError(`The count of the nodes is a whole number from 0, not ${n}.`);\n const degree = new Float64Array(n);\n const next: number[][] = Array.from({ length: n }, () => []);\n for (const { a, b, weight = 1 } of edges) {\n if (!Number.isInteger(a) || !Number.isInteger(b) || a < 0 || b < 0 || a >= n || b >= n) {\n throw new RangeError(`An edge joins the nodes ${a} and ${b}, but the indexes go from 0 to ${n - 1}.`);\n }\n if (!Number.isFinite(weight) || weight <= 0) throw new RangeError(`The weight of an edge is a finite number above 0, not ${weight}.`);\n if (a === b) continue;\n degree[a] = degree[a]! + weight;\n degree[b] = degree[b]! + weight;\n next[a]!.push(b);\n next[b]!.push(a);\n }\n const order = (i: number, j: number): number => compare(i, j) || i - j;\n const hubs = Array.from({ length: n }, (_, i) => i).sort((i, j) => degree[j]! - degree[i]! || order(i, j));\n const seen = new Uint8Array(n);\n const out: number[] = [];\n for (const hub of hubs) {\n if (seen[hub] === 1) continue;\n seen[hub] = 1;\n const queue = [hub];\n for (let k = 0; k < queue.length; k++) {\n const i = queue[k]!;\n out.push(i);\n for (const j of next[i]!.toSorted(order)) {\n if (seen[j] === 1) continue;\n seen[j] = 1;\n queue.push(j);\n }\n }\n }\n return out;\n}\n"],"mappings":";;;;;;;;;;;;AAkBA,SAAgB,qBAAqB,GAAW,QAAiC,CAAC,GAAG,gBAAkD,GAAa;CAClJ,IAAI,CAAC,OAAO,UAAU,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI,WAAW,wDAAwD,EAAE,EAAE;CACpH,MAAM,SAAS,IAAI,aAAa,CAAC;CACjC,MAAM,OAAmB,MAAM,KAAK,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;CAC3D,KAAK,MAAM,EAAE,GAAG,GAAG,SAAS,OAAO,OAAO;EACxC,IAAI,CAAC,OAAO,UAAU,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,KAAK,IAAI,KAAK,IAAI,KAAK,KAAK,KAAK,KAAK,GACnF,MAAM,IAAI,WAAW,2BAA2B,EAAE,OAAO,EAAE,iCAAiC,IAAI,EAAE,EAAE;EAEtG,IAAI,CAAC,OAAO,SAAS,MAAM,KAAK,UAAU,GAAG,MAAM,IAAI,WAAW,yDAAyD,OAAO,EAAE;EACpI,IAAI,MAAM,GAAG;EACb,OAAO,KAAK,OAAO,KAAM;EACzB,OAAO,KAAK,OAAO,KAAM;EACzB,KAAK,EAAE,CAAE,KAAK,CAAC;EACf,KAAK,EAAE,CAAE,KAAK,CAAC;CACjB;CACA,MAAM,SAAS,GAAW,MAAsB,QAAQ,GAAG,CAAC,KAAK,IAAI;CACrE,MAAM,OAAO,MAAM,KAAK,EAAE,QAAQ,EAAE,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,GAAG,MAAM,OAAO,KAAM,OAAO,MAAO,MAAM,GAAG,CAAC,CAAC;CACzG,MAAM,OAAO,IAAI,WAAW,CAAC;CAC7B,MAAM,MAAgB,CAAC;CACvB,KAAK,MAAM,OAAO,MAAM;EACtB,IAAI,KAAK,SAAS,GAAG;EACrB,KAAK,OAAO;EACZ,MAAM,QAAQ,CAAC,GAAG;EAClB,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;GACrC,MAAM,IAAI,MAAM;GAChB,IAAI,KAAK,CAAC;GACV,KAAK,MAAM,KAAK,KAAK,EAAE,CAAE,SAAS,KAAK,GAAG;IACxC,IAAI,KAAK,OAAO,GAAG;IACnB,KAAK,KAAK;IACV,MAAM,KAAK,CAAC;GACd;EACF;CACF;CACA,OAAO;AACT"}
|