@procedures/dag 0.0.0-stage → 0.1.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/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/README.md +498 -2
- package/README_NEW.md +736 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/dag/builder.d.ts +25 -0
- package/dist/dag/builder.d.ts.map +1 -0
- package/dist/dag/builder.js +95 -0
- package/dist/dag/builder.js.map +1 -0
- package/dist/dag/executor.d.ts +28 -0
- package/dist/dag/executor.d.ts.map +1 -0
- package/dist/dag/executor.js +147 -0
- package/dist/dag/executor.js.map +1 -0
- package/dist/dag/index.d.ts +7 -0
- package/dist/dag/index.d.ts.map +1 -0
- package/dist/dag/index.js +7 -0
- package/dist/dag/index.js.map +1 -0
- package/dist/dag/traversal.d.ts +28 -0
- package/dist/dag/traversal.d.ts.map +1 -0
- package/dist/dag/traversal.js +103 -0
- package/dist/dag/traversal.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +80 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/package.json +62 -3
- package/src/dag/builder.ts +124 -0
- package/src/dag/executor.ts +190 -0
- package/src/dag/index.ts +23 -0
- package/src/dag/traversal.ts +129 -0
- package/src/index.ts +64 -0
- package/src/types.ts +87 -0
package/README_NEW.md
ADDED
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
# @procedures/dag
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@procedures/dag)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
Generic DAG (Directed Acyclic Graph) algorithms for dependency management, topological sorting, and parallel execution.
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
`client-dag` provides a robust, generic DAG implementation using Kahn's algorithm for topological sorting. It enables dependency-aware parallel execution with concurrency control, making it ideal for package build systems, task scheduling, and any scenario requiring ordered execution of dependent operations.
|
|
11
|
+
|
|
12
|
+
### Key Features
|
|
13
|
+
|
|
14
|
+
- **Kahn's Algorithm**: Topological sort with cycle detection
|
|
15
|
+
- **Level Assignment**: Automatic grouping for parallel execution
|
|
16
|
+
- **Parallel Execution**: Configurable concurrency per level
|
|
17
|
+
- **Generic Types**: Type-safe with custom node data
|
|
18
|
+
- **Graph Utilities**: Filter, traverse, and analyze dependencies
|
|
19
|
+
- **Fail-Fast or Continue**: Flexible error handling
|
|
20
|
+
- **Progress Tracking**: Callbacks for monitoring
|
|
21
|
+
- **Visualization**: Helper functions for debugging
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @procedures/dag
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Dependencies
|
|
30
|
+
|
|
31
|
+
- No runtime dependencies (pure TypeScript/JavaScript)
|
|
32
|
+
- Dev: `@mark1russell7/cue`, `vitest`, `@mark1russell7/test`
|
|
33
|
+
|
|
34
|
+
## Architecture
|
|
35
|
+
|
|
36
|
+
```mermaid
|
|
37
|
+
graph TB
|
|
38
|
+
subgraph "Input Layer"
|
|
39
|
+
Nodes[Node Definitions<br/>id + dependencies]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
subgraph "Builder Module"
|
|
43
|
+
CreateNode[createNode<br/>Factory]
|
|
44
|
+
BuildMap[buildNodeMap<br/>Array to Map]
|
|
45
|
+
Filter[filterDAGFromRoot<br/>Subset extraction]
|
|
46
|
+
Ancestors[getAncestors<br/>Transitive deps]
|
|
47
|
+
Descendants[getDescendants<br/>Reverse deps]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
subgraph "Traversal Module"
|
|
51
|
+
Kahn[Kahn's Algorithm<br/>Topological Sort]
|
|
52
|
+
BuildDAG[buildLeveledDAG<br/>Level assignment]
|
|
53
|
+
TopoOrder[getTopologicalOrder<br/>Flat list]
|
|
54
|
+
Visualize[visualizeDAG<br/>Debug output]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
subgraph "Executor Module"
|
|
58
|
+
Processor[createProcessor<br/>Wrapper factory]
|
|
59
|
+
Execute[executeDAG<br/>Parallel execution]
|
|
60
|
+
Sequential[executeDAGSequential<br/>Sequential mode]
|
|
61
|
+
Concurrency[executeWithConcurrency<br/>Batch control]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
subgraph "Output Layer"
|
|
65
|
+
DAG[DependencyDAG<br/>nodes + levels]
|
|
66
|
+
Results[DAGResult<br/>success + results]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
Nodes --> CreateNode
|
|
70
|
+
CreateNode --> BuildMap
|
|
71
|
+
BuildMap --> BuildDAG
|
|
72
|
+
BuildDAG --> Kahn
|
|
73
|
+
Kahn --> DAG
|
|
74
|
+
DAG --> Filter
|
|
75
|
+
DAG --> Ancestors
|
|
76
|
+
DAG --> Descendants
|
|
77
|
+
DAG --> TopoOrder
|
|
78
|
+
DAG --> Visualize
|
|
79
|
+
DAG --> Execute
|
|
80
|
+
Execute --> Concurrency
|
|
81
|
+
Execute --> Results
|
|
82
|
+
|
|
83
|
+
Processor --> Execute
|
|
84
|
+
Sequential --> Execute
|
|
85
|
+
|
|
86
|
+
style BuildDAG fill:#e1f5ff
|
|
87
|
+
style Execute fill:#e8f5e9
|
|
88
|
+
style Kahn fill:#fff4e1
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Kahn's Algorithm Flow
|
|
92
|
+
|
|
93
|
+
```mermaid
|
|
94
|
+
flowchart TD
|
|
95
|
+
Start[Start: Nodes with dependencies]
|
|
96
|
+
Init[Initialize in-degree counters]
|
|
97
|
+
Queue[Queue = nodes with in-degree 0]
|
|
98
|
+
Process{Queue empty?}
|
|
99
|
+
Take[Take all nodes from queue]
|
|
100
|
+
Level[Create level from queue nodes]
|
|
101
|
+
Reduce[Reduce in-degree of dependents]
|
|
102
|
+
Zero{Any in-degree = 0?}
|
|
103
|
+
AddQueue[Add to queue]
|
|
104
|
+
NextLevel[Move to next level]
|
|
105
|
+
Check{Processed all nodes?}
|
|
106
|
+
Success[Success: Return leveled DAG]
|
|
107
|
+
Cycle[Error: Cycle detected]
|
|
108
|
+
|
|
109
|
+
Start --> Init
|
|
110
|
+
Init --> Queue
|
|
111
|
+
Queue --> Process
|
|
112
|
+
Process -->|No| Take
|
|
113
|
+
Take --> Level
|
|
114
|
+
Level --> Reduce
|
|
115
|
+
Reduce --> Zero
|
|
116
|
+
Zero -->|Yes| AddQueue
|
|
117
|
+
AddQueue --> NextLevel
|
|
118
|
+
NextLevel --> Process
|
|
119
|
+
Zero -->|No| Process
|
|
120
|
+
Process -->|Yes| Check
|
|
121
|
+
Check -->|Yes| Success
|
|
122
|
+
Check -->|No| Cycle
|
|
123
|
+
|
|
124
|
+
style Kahn fill:#fff4e1
|
|
125
|
+
style Success fill:#e8f5e9
|
|
126
|
+
style Cycle fill:#ffe1e1
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Execution Flow
|
|
130
|
+
|
|
131
|
+
```mermaid
|
|
132
|
+
sequenceDiagram
|
|
133
|
+
participant App as Application
|
|
134
|
+
participant Exec as executeDAG
|
|
135
|
+
participant Level as Level Processor
|
|
136
|
+
participant Conc as Concurrency Control
|
|
137
|
+
participant Proc as Node Processor
|
|
138
|
+
|
|
139
|
+
App->>Exec: executeDAG(dag, processor, {concurrency: 4})
|
|
140
|
+
Exec->>Exec: Initialize results map
|
|
141
|
+
|
|
142
|
+
loop For each level in dag.levels
|
|
143
|
+
Exec->>Level: Process level
|
|
144
|
+
Level->>Conc: executeWithConcurrency(nodes, fn, limit)
|
|
145
|
+
|
|
146
|
+
loop For each node in level
|
|
147
|
+
Conc->>Proc: processor(node)
|
|
148
|
+
Proc->>Proc: Execute node logic
|
|
149
|
+
Proc-->>Conc: NodeResult
|
|
150
|
+
Conc->>Conc: Track executing promises
|
|
151
|
+
Conc->>Conc: Enforce concurrency limit
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
Conc-->>Level: All node results
|
|
155
|
+
Level->>Level: Check for failures
|
|
156
|
+
Level->>Level: Call onNodeComplete callbacks
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
Exec->>Exec: Aggregate results
|
|
160
|
+
Exec-->>App: DAGResult
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Usage
|
|
164
|
+
|
|
165
|
+
### Basic DAG Creation
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import {
|
|
169
|
+
createNode,
|
|
170
|
+
buildNodeMap,
|
|
171
|
+
buildLeveledDAG,
|
|
172
|
+
type DAGNode
|
|
173
|
+
} from "@procedures/dag";
|
|
174
|
+
|
|
175
|
+
// Method 1: Manual node creation
|
|
176
|
+
const nodes: DAGNode[] = [
|
|
177
|
+
{ id: "app", dependencies: ["lib-a", "lib-b"] },
|
|
178
|
+
{ id: "lib-a", dependencies: ["core"] },
|
|
179
|
+
{ id: "lib-b", dependencies: ["core"] },
|
|
180
|
+
{ id: "core", dependencies: [] }
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
// Method 2: Using factories
|
|
184
|
+
const nodesAlt = [
|
|
185
|
+
createNode("app", ["lib-a", "lib-b"]),
|
|
186
|
+
createNode("lib-a", ["core"]),
|
|
187
|
+
createNode("lib-b", ["core"]),
|
|
188
|
+
createNode("core")
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
// Convert to Map
|
|
192
|
+
const nodeMap = buildNodeMap(nodes);
|
|
193
|
+
|
|
194
|
+
// Build leveled DAG
|
|
195
|
+
const dag = buildLeveledDAG(nodeMap);
|
|
196
|
+
|
|
197
|
+
console.log(dag.levels);
|
|
198
|
+
// [
|
|
199
|
+
// [{ id: "core", level: 0, ... }], // Level 0: no dependencies
|
|
200
|
+
// [{ id: "lib-a", level: 1 }, { id: "lib-b", level: 1 }], // Level 1
|
|
201
|
+
// [{ id: "app", level: 2, ... }] // Level 2: depends on all
|
|
202
|
+
// ]
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Parallel Execution
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
import { executeDAG, createProcessor } from "@procedures/dag";
|
|
209
|
+
|
|
210
|
+
// Simple processor
|
|
211
|
+
const processor = createProcessor(async (node) => {
|
|
212
|
+
console.log(`Building ${node.id}...`);
|
|
213
|
+
await build(node.id);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Execute with parallelism
|
|
217
|
+
const result = await executeDAG(dag, processor, {
|
|
218
|
+
concurrency: 4, // Up to 4 nodes in parallel per level
|
|
219
|
+
failFast: true, // Stop on first error
|
|
220
|
+
onNodeStart: (node) => console.log(`Started: ${node.id}`),
|
|
221
|
+
onNodeComplete: (result) => {
|
|
222
|
+
if (result.success) {
|
|
223
|
+
console.log(`Completed: ${result.node.id} (${result.duration}ms)`);
|
|
224
|
+
} else {
|
|
225
|
+
console.error(`Failed: ${result.node.id}`, result.error);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
if (!result.success) {
|
|
231
|
+
console.error("Failed nodes:", result.failedNodes);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Custom Node Data
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
interface PackageNode extends DAGNode {
|
|
240
|
+
data: {
|
|
241
|
+
path: string;
|
|
242
|
+
version: string;
|
|
243
|
+
scripts: Record<string, string>;
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const packageNodes: PackageNode[] = [
|
|
248
|
+
{
|
|
249
|
+
id: "@org/app",
|
|
250
|
+
dependencies: ["@org/ui"],
|
|
251
|
+
data: {
|
|
252
|
+
path: "/packages/app",
|
|
253
|
+
version: "1.0.0",
|
|
254
|
+
scripts: { build: "tsc" }
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
id: "@org/ui",
|
|
259
|
+
dependencies: [],
|
|
260
|
+
data: {
|
|
261
|
+
path: "/packages/ui",
|
|
262
|
+
version: "2.0.0",
|
|
263
|
+
scripts: { build: "vite build" }
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
const dag = buildLeveledDAG(buildNodeMap(packageNodes));
|
|
269
|
+
|
|
270
|
+
const processor = createProcessor(async (node: PackageNode) => {
|
|
271
|
+
console.log(`Building ${node.id} at ${node.data.path}`);
|
|
272
|
+
await execSync(node.data.scripts.build, { cwd: node.data.path });
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
await executeDAG(dag, processor);
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Graph Filtering
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
import { filterDAGFromRoot, getAncestors, getDescendants } from "@procedures/dag";
|
|
282
|
+
|
|
283
|
+
// Filter to only nodes needed for a specific target
|
|
284
|
+
const appOnlyNodes = filterDAGFromRoot(nodeMap, "app");
|
|
285
|
+
const appDAG = buildLeveledDAG(appOnlyNodes);
|
|
286
|
+
|
|
287
|
+
// Get all dependencies of a node (transitive)
|
|
288
|
+
const appDeps = getAncestors(nodeMap, "app");
|
|
289
|
+
console.log(appDeps); // Set { "lib-a", "lib-b", "core" }
|
|
290
|
+
|
|
291
|
+
// Get all dependents of a node (reverse dependencies)
|
|
292
|
+
const coreDependents = getDescendants(nodeMap, "core");
|
|
293
|
+
console.log(coreDependents); // Set { "lib-a", "lib-b", "app" }
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Visualization
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import { visualizeDAG } from "@procedures/dag";
|
|
300
|
+
|
|
301
|
+
const output = visualizeDAG(dag);
|
|
302
|
+
console.log(output);
|
|
303
|
+
// DAG Structure:
|
|
304
|
+
// Level 0:
|
|
305
|
+
// - core (leaf)
|
|
306
|
+
// Level 1:
|
|
307
|
+
// - lib-a -> [core]
|
|
308
|
+
// - lib-b -> [core]
|
|
309
|
+
// Level 2:
|
|
310
|
+
// - app -> [lib-a, lib-b]
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Sequential Execution
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
import { executeDAGSequential } from "@procedures/dag";
|
|
317
|
+
|
|
318
|
+
// Execute one node at a time (concurrency: 1)
|
|
319
|
+
const result = await executeDAGSequential(dag, processor, {
|
|
320
|
+
failFast: false // Continue even if some nodes fail
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## API Reference
|
|
325
|
+
|
|
326
|
+
### Types
|
|
327
|
+
|
|
328
|
+
#### `DAGNode<TData = unknown>`
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
interface DAGNode<TData = unknown> {
|
|
332
|
+
id: string; // Unique identifier
|
|
333
|
+
dependencies: string[]; // IDs this node depends on
|
|
334
|
+
level?: number; // Topological level (computed by buildLeveledDAG)
|
|
335
|
+
data?: TData | undefined; // Optional user data
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
#### `DependencyDAG<TNode extends DAGNode = DAGNode>`
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
interface DependencyDAG<TNode extends DAGNode = DAGNode> {
|
|
343
|
+
nodes: Map<string, TNode>; // All nodes by ID
|
|
344
|
+
levels: TNode[][]; // Nodes grouped by level (0 = leaves)
|
|
345
|
+
roots: string[]; // Node IDs with no dependents
|
|
346
|
+
leaves: string[]; // Node IDs with no dependencies
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
#### `DAGExecutionOptions<TNode extends DAGNode = DAGNode>`
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
interface DAGExecutionOptions<TNode extends DAGNode = DAGNode> {
|
|
354
|
+
concurrency?: number; // Max parallel per level (default: 4)
|
|
355
|
+
failFast?: boolean; // Stop on first error (default: true)
|
|
356
|
+
onNodeStart?: (node: TNode) => void; // Called when node starts
|
|
357
|
+
onNodeComplete?: (result: NodeResult<TNode>) => void; // Called when node completes
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
#### `NodeResult<TNode extends DAGNode = DAGNode>`
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
interface NodeResult<TNode extends DAGNode = DAGNode> {
|
|
365
|
+
node: TNode; // The processed node
|
|
366
|
+
success: boolean; // Whether processing succeeded
|
|
367
|
+
error?: Error | undefined; // Error if failed
|
|
368
|
+
duration: number; // Processing time in milliseconds
|
|
369
|
+
logs: string[]; // Log messages
|
|
370
|
+
output?: unknown | undefined; // Optional output data
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
#### `DAGResult<TNode extends DAGNode = DAGNode>`
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
interface DAGResult<TNode extends DAGNode = DAGNode> {
|
|
378
|
+
success: boolean; // Overall success
|
|
379
|
+
results: Map<string, NodeResult<TNode>>; // Results by node ID
|
|
380
|
+
failedNodes: string[]; // IDs of failed nodes
|
|
381
|
+
totalDuration: number; // Total execution time in ms
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
#### `NodeProcessor<TNode extends DAGNode = DAGNode>`
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
type NodeProcessor<TNode extends DAGNode = DAGNode> = (
|
|
389
|
+
node: TNode
|
|
390
|
+
) => Promise<NodeResult<TNode>>;
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Functions
|
|
394
|
+
|
|
395
|
+
#### Building
|
|
396
|
+
|
|
397
|
+
##### `createNode<TData>(id: string, dependencies?: string[], data?: TData): DAGNode<TData>`
|
|
398
|
+
|
|
399
|
+
Create a DAG node from minimal input.
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
const node = createNode("my-package", ["dep-a", "dep-b"], {
|
|
403
|
+
path: "/packages/my-package"
|
|
404
|
+
});
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
##### `buildNodeMap<TNode extends DAGNode>(nodes: TNode[]): Map<string, TNode>`
|
|
408
|
+
|
|
409
|
+
Convert an array of nodes to a Map keyed by ID.
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
const nodeMap = buildNodeMap([
|
|
413
|
+
createNode("a", ["b"]),
|
|
414
|
+
createNode("b", [])
|
|
415
|
+
]);
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
#### Traversal
|
|
419
|
+
|
|
420
|
+
##### `buildLeveledDAG<TNode extends DAGNode>(nodes: Map<string, TNode>): DependencyDAG<TNode>`
|
|
421
|
+
|
|
422
|
+
Build a leveled DAG using Kahn's algorithm. Assigns level to each node and groups by level.
|
|
423
|
+
|
|
424
|
+
**Throws:** Error if a cycle is detected.
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
427
|
+
const dag = buildLeveledDAG(nodeMap);
|
|
428
|
+
console.log(dag.levels.length); // Number of levels
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
##### `getTopologicalOrder<TNode extends DAGNode>(dag: DependencyDAG<TNode>): TNode[]`
|
|
432
|
+
|
|
433
|
+
Get a flat array of nodes in topological order (leaves first).
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
const order = getTopologicalOrder(dag);
|
|
437
|
+
// [core, lib-a, lib-b, app]
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
##### `visualizeDAG<TNode extends DAGNode>(dag: DependencyDAG<TNode>): string`
|
|
441
|
+
|
|
442
|
+
Generate a string visualization of the DAG structure.
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
console.log(visualizeDAG(dag));
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
#### Filtering
|
|
449
|
+
|
|
450
|
+
##### `filterDAGFromRoot<TNode extends DAGNode>(nodes: Map<string, TNode>, rootId: string): Map<string, TNode>`
|
|
451
|
+
|
|
452
|
+
Filter DAG to only include nodes reachable from a root (the root and all its dependencies).
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
const appOnly = filterDAGFromRoot(allNodes, "app");
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
##### `getAncestors<TNode extends DAGNode>(nodes: Map<string, TNode>, id: string): Set<string>`
|
|
459
|
+
|
|
460
|
+
Get all ancestors of a node (transitive dependencies).
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
const deps = getAncestors(nodes, "app");
|
|
464
|
+
// Set of all nodes that "app" depends on
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
##### `getDescendants<TNode extends DAGNode>(nodes: Map<string, TNode>, id: string): Set<string>`
|
|
468
|
+
|
|
469
|
+
Get all descendants of a node (reverse transitive dependencies).
|
|
470
|
+
|
|
471
|
+
```typescript
|
|
472
|
+
const dependents = getDescendants(nodes, "core");
|
|
473
|
+
// Set of all nodes that depend on "core"
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
#### Execution
|
|
477
|
+
|
|
478
|
+
##### `executeDAG<TNode extends DAGNode>(dag: DependencyDAG<TNode>, processor: NodeProcessor<TNode>, options?: DAGExecutionOptions<TNode>): Promise<DAGResult<TNode>>`
|
|
479
|
+
|
|
480
|
+
Execute DAG with level-based parallelization.
|
|
481
|
+
|
|
482
|
+
- Levels are processed sequentially (level 0, then 1, etc.)
|
|
483
|
+
- Within each level, nodes are processed in parallel up to concurrency limit
|
|
484
|
+
- Returns aggregated results
|
|
485
|
+
|
|
486
|
+
```typescript
|
|
487
|
+
const result = await executeDAG(dag, processor, {
|
|
488
|
+
concurrency: 4,
|
|
489
|
+
failFast: true,
|
|
490
|
+
onNodeStart: (node) => console.log(`Starting ${node.id}`),
|
|
491
|
+
onNodeComplete: (result) => console.log(`Done ${result.node.id}`)
|
|
492
|
+
});
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
##### `executeDAGSequential<TNode extends DAGNode>(dag: DependencyDAG<TNode>, processor: NodeProcessor<TNode>, options?: Omit<DAGExecutionOptions<TNode>, 'concurrency'>): Promise<DAGResult<TNode>>`
|
|
496
|
+
|
|
497
|
+
Execute DAG sequentially (concurrency: 1).
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
const result = await executeDAGSequential(dag, processor);
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
##### `createProcessor<TNode extends DAGNode>(fn: (node: TNode) => Promise<void>): NodeProcessor<TNode>`
|
|
504
|
+
|
|
505
|
+
Create a processor that wraps an async function, handling timing and error catching.
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
const processor = createProcessor(async (node) => {
|
|
509
|
+
await buildPackage(node.id);
|
|
510
|
+
});
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
## Examples
|
|
514
|
+
|
|
515
|
+
### Example 1: Monorepo Package Build
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
import { buildLeveledDAG, executeDAG, createProcessor } from "@procedures/dag";
|
|
519
|
+
|
|
520
|
+
const packages = [
|
|
521
|
+
{ id: "app", dependencies: ["ui", "api-client"] },
|
|
522
|
+
{ id: "ui", dependencies: ["components", "utils"] },
|
|
523
|
+
{ id: "api-client", dependencies: ["utils"] },
|
|
524
|
+
{ id: "components", dependencies: ["utils"] },
|
|
525
|
+
{ id: "utils", dependencies: [] }
|
|
526
|
+
];
|
|
527
|
+
|
|
528
|
+
const dag = buildLeveledDAG(buildNodeMap(packages));
|
|
529
|
+
|
|
530
|
+
const processor = createProcessor(async (pkg) => {
|
|
531
|
+
console.log(`Building ${pkg.id}...`);
|
|
532
|
+
await exec(`npm run build`, { cwd: `/packages/${pkg.id}` });
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
const result = await executeDAG(dag, processor, {
|
|
536
|
+
concurrency: 3,
|
|
537
|
+
onNodeComplete: (r) => {
|
|
538
|
+
if (r.success) {
|
|
539
|
+
console.log(`✓ ${r.node.id} built in ${r.duration}ms`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
### Example 2: Task Pipeline
|
|
546
|
+
|
|
547
|
+
```typescript
|
|
548
|
+
const tasks = [
|
|
549
|
+
{ id: "deploy", dependencies: ["test", "build-docker"] },
|
|
550
|
+
{ id: "test", dependencies: ["build"] },
|
|
551
|
+
{ id: "build-docker", dependencies: ["build"] },
|
|
552
|
+
{ id: "build", dependencies: ["lint", "typecheck"] },
|
|
553
|
+
{ id: "lint", dependencies: [] },
|
|
554
|
+
{ id: "typecheck", dependencies: [] }
|
|
555
|
+
];
|
|
556
|
+
|
|
557
|
+
const dag = buildLeveledDAG(buildNodeMap(tasks));
|
|
558
|
+
|
|
559
|
+
const processor = createProcessor(async (task) => {
|
|
560
|
+
await runTask(task.id);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
await executeDAG(dag, processor, { failFast: true });
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
### Example 3: Selective Execution
|
|
567
|
+
|
|
568
|
+
```typescript
|
|
569
|
+
// Only build what's needed for "app"
|
|
570
|
+
const allPackages = buildNodeMap([...]);
|
|
571
|
+
const appDeps = filterDAGFromRoot(allPackages, "app");
|
|
572
|
+
const appDAG = buildLeveledDAG(appDeps);
|
|
573
|
+
|
|
574
|
+
await executeDAG(appDAG, buildProcessor);
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### Example 4: Progress Tracking
|
|
578
|
+
|
|
579
|
+
```typescript
|
|
580
|
+
let completed = 0;
|
|
581
|
+
const total = dag.nodes.size;
|
|
582
|
+
|
|
583
|
+
const result = await executeDAG(dag, processor, {
|
|
584
|
+
onNodeComplete: (result) => {
|
|
585
|
+
completed++;
|
|
586
|
+
const percent = ((completed / total) * 100).toFixed(1);
|
|
587
|
+
console.log(`[${percent}%] ${result.node.id}: ${result.success ? 'OK' : 'FAIL'}`);
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
### Example 5: Continue on Error
|
|
593
|
+
|
|
594
|
+
```typescript
|
|
595
|
+
const result = await executeDAG(dag, processor, {
|
|
596
|
+
failFast: false // Process all nodes even if some fail
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
if (!result.success) {
|
|
600
|
+
console.error("Failed nodes:", result.failedNodes.join(", "));
|
|
601
|
+
for (const id of result.failedNodes) {
|
|
602
|
+
const nodeResult = result.results.get(id);
|
|
603
|
+
console.error(`${id}: ${nodeResult?.error?.message}`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
## Cycle Detection
|
|
609
|
+
|
|
610
|
+
If a cycle is detected, `buildLeveledDAG` throws an error:
|
|
611
|
+
|
|
612
|
+
```typescript
|
|
613
|
+
const nodes = [
|
|
614
|
+
{ id: "a", dependencies: ["b"] },
|
|
615
|
+
{ id: "b", dependencies: ["c"] },
|
|
616
|
+
{ id: "c", dependencies: ["a"] } // Cycle!
|
|
617
|
+
];
|
|
618
|
+
|
|
619
|
+
try {
|
|
620
|
+
const dag = buildLeveledDAG(buildNodeMap(nodes));
|
|
621
|
+
} catch (error) {
|
|
622
|
+
console.error(error.message);
|
|
623
|
+
// "Circular dependency detected involving: a, b, c"
|
|
624
|
+
}
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
## Performance
|
|
628
|
+
|
|
629
|
+
### Complexity
|
|
630
|
+
|
|
631
|
+
- **buildLeveledDAG**: O(V + E) where V = vertices, E = edges
|
|
632
|
+
- **executeDAG**: O(V * T) where T = average task time
|
|
633
|
+
- **getAncestors/getDescendants**: O(V + E) per call
|
|
634
|
+
|
|
635
|
+
### Optimization Tips
|
|
636
|
+
|
|
637
|
+
1. **Concurrency**: Tune based on CPU cores and I/O characteristics
|
|
638
|
+
2. **Fail-Fast**: Enable for faster feedback in CI/CD
|
|
639
|
+
3. **Level Inspection**: Use `dag.levels` to understand parallelism opportunities
|
|
640
|
+
4. **Node Filtering**: Use `filterDAGFromRoot` to reduce work
|
|
641
|
+
|
|
642
|
+
## Testing
|
|
643
|
+
|
|
644
|
+
The package includes comprehensive tests:
|
|
645
|
+
|
|
646
|
+
```bash
|
|
647
|
+
npm test
|
|
648
|
+
npm run test:watch
|
|
649
|
+
npm run test:coverage
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
Test files:
|
|
653
|
+
- `src/dag/__tests__/builder.test.ts` - Graph building and filtering
|
|
654
|
+
- `src/dag/__tests__/traversal.test.ts` - Kahn's algorithm and topological sort
|
|
655
|
+
- `src/dag/__tests__/executor.test.ts` - Parallel execution and concurrency
|
|
656
|
+
|
|
657
|
+
## Package Structure
|
|
658
|
+
|
|
659
|
+
```
|
|
660
|
+
client-dag/
|
|
661
|
+
├── src/
|
|
662
|
+
│ ├── index.ts # Main exports
|
|
663
|
+
│ ├── types.ts # TypeScript type definitions
|
|
664
|
+
│ └── dag/
|
|
665
|
+
│ ├── index.ts # DAG module exports
|
|
666
|
+
│ ├── builder.ts # Graph building utilities
|
|
667
|
+
│ ├── traversal.ts # Kahn's algorithm + topological sort
|
|
668
|
+
│ ├── executor.ts # Parallel execution engine
|
|
669
|
+
│ └── __tests__/ # Test suite
|
|
670
|
+
├── dist/ # Compiled output
|
|
671
|
+
├── package.json
|
|
672
|
+
├── tsconfig.json
|
|
673
|
+
└── README.md
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
## Development
|
|
677
|
+
|
|
678
|
+
### Build
|
|
679
|
+
|
|
680
|
+
```bash
|
|
681
|
+
npm run build
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
### Type Check
|
|
685
|
+
|
|
686
|
+
```bash
|
|
687
|
+
npm run typecheck
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
### Clean
|
|
691
|
+
|
|
692
|
+
```bash
|
|
693
|
+
npm run clean
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
### Run Tests
|
|
697
|
+
|
|
698
|
+
```bash
|
|
699
|
+
npm test
|
|
700
|
+
npm run test:watch
|
|
701
|
+
npm run test:coverage
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
## Integration with Ecosystem
|
|
705
|
+
|
|
706
|
+
### Used By
|
|
707
|
+
|
|
708
|
+
- **client-lib**: Package dependency resolution
|
|
709
|
+
- **CLI**: Build orchestration commands
|
|
710
|
+
- **CI/CD**: Multi-package pipelines
|
|
711
|
+
|
|
712
|
+
### Dependency Graph
|
|
713
|
+
|
|
714
|
+
```mermaid
|
|
715
|
+
graph LR
|
|
716
|
+
clientDag[client-dag]
|
|
717
|
+
cue[cue]
|
|
718
|
+
test[test]
|
|
719
|
+
vitest[vitest]
|
|
720
|
+
|
|
721
|
+
clientDag -.dev.-> cue
|
|
722
|
+
clientDag -.dev.-> test
|
|
723
|
+
clientDag -.dev.-> vitest
|
|
724
|
+
|
|
725
|
+
style clientDag fill:#e1f5ff
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
## License
|
|
729
|
+
|
|
730
|
+
MIT - Mark Russell
|
|
731
|
+
|
|
732
|
+
## Related Packages
|
|
733
|
+
|
|
734
|
+
- `@procedures/lib` - Uses DAG for package builds
|
|
735
|
+
- `@mark1russell7/test` - Testing utilities
|
|
736
|
+
- `@mark1russell7/cue` - Build configuration
|