@mark1russell7/collections 0.5.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.
@@ -38,6 +38,11 @@ interface DAGExecutionOptions<TNode extends DAGNode = DAGNode> {
38
38
  concurrency?: number;
39
39
  /** When this value is true, the run stops on the first error. The default is true. */
40
40
  failFast?: boolean;
41
+ /**
42
+ * When this value is true, a node whose dependency failed or was skipped does not start. Its result has `skipped: true`.
43
+ * The default is true. Give false only when the nodes do not use the work of their dependencies.
44
+ */
45
+ skipDependents?: boolean;
41
46
  /** Callback for progress reporting */
42
47
  onNodeStart?: (node: TNode) => void;
43
48
  /** Callback when a node completes */
@@ -59,6 +64,8 @@ interface NodeResult<TNode extends DAGNode = DAGNode> {
59
64
  logs: string[];
60
65
  /** Optional output from the processor */
61
66
  output?: unknown | undefined;
67
+ /** This value is true when the node did not start, because a dependency failed or was skipped. */
68
+ skipped?: boolean | undefined;
62
69
  }
63
70
  /**
64
71
  * The result of a run of an entire DAG
@@ -68,8 +75,10 @@ interface DAGResult<TNode extends DAGNode = DAGNode> {
68
75
  success: boolean;
69
76
  /** Results for each node */
70
77
  results: Map<string, NodeResult<TNode>>;
71
- /** Names of failed nodes */
78
+ /** The ids of the nodes whose processor failed */
72
79
  failedNodes: string[];
80
+ /** The ids of the nodes that did not start, because a dependency failed or was skipped */
81
+ skippedNodes: string[];
73
82
  /** Total duration in milliseconds */
74
83
  totalDuration: number;
75
84
  }
@@ -107,6 +116,9 @@ export declare function visualizeDAG<TNode extends DAGNode>(dag: DependencyDAG<T
107
116
  * A processor that throws does NOT reject the whole run. The run records the throw
108
117
  * as a failed NodeResult, so partial results are always given.
109
118
  *
119
+ * With `skipDependents` (the default), a node whose dependency failed or was skipped does not start. Its result has
120
+ * `skipped: true` and an error that names those dependencies. A dependency outside the DAG does not count.
121
+ *
110
122
  * `failFast` operates at level granularity. When a node in a level fails, each
111
123
  * node already in flight in that level continues to completion. No later level
112
124
  * starts. (There is no mid-level cancellation.)
@@ -1 +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"}
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;;;;;EAKA;;EAEA,eAAe,MAAM;;EAErB,kBAAkB,QAAQ,WAAW;;;;;UAMtB,WAAW,cAAc,UAAU;;EAElD,MAAM;;EAEN;;EAEA,QAAQ;;EAER;;EAEA;;EAEA;;EAEA;;;;;UAMe,UAAU,cAAc,UAAU;;EAEjD;;EAEA,SAAS,YAAY,WAAW;;EAEhC;;EAEA;;EAEA;;;;;KAMU,cAAc,cAAc,UAAU,YAChD,MAAM,UACH,QAAQ,WAAW;;;;;;;;;;;;wBC3ER,gBAAgB,cAAc,SAC5C,OAAO,YAAY,SAClB,cAAc;;;;wBA+ED,oBAAoB,cAAc,SAChD,KAAK,cAAc,SAClB;;;;wBAOa,aAAa,cAAc,SACzC,KAAK,cAAc;;;;;;;;;;;;;;;;;;wBClCC,WAAW,cAAc,SAC7C,KAAK,cAAc,QACnB,YAAY,MAAM,UAAU,QAAQ,WAAW,SAC/C,UAAS,oBAAoB,SAC5B,QAAQ,UAAU;;;;wBAwFC,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;;;;wBC7KvB,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"}
@@ -122,19 +122,43 @@ function notify(name, listener, arg) {
122
122
  * A processor that throws does NOT reject the whole run. The run records the throw
123
123
  * as a failed NodeResult, so partial results are always given.
124
124
  *
125
+ * With `skipDependents` (the default), a node whose dependency failed or was skipped does not start. Its result has
126
+ * `skipped: true` and an error that names those dependencies. A dependency outside the DAG does not count.
127
+ *
125
128
  * `failFast` operates at level granularity. When a node in a level fails, each
126
129
  * node already in flight in that level continues to completion. No later level
127
130
  * starts. (There is no mid-level cancellation.)
128
131
  */
129
132
  async function executeDAG(dag, processor, options = {}) {
130
- const { concurrency = 4, failFast = true, onNodeStart, onNodeComplete } = options;
133
+ const { concurrency = 4, failFast = true, skipDependents = true, onNodeStart, onNodeComplete } = options;
131
134
  const results = /* @__PURE__ */ new Map();
132
135
  const failedNodes = [];
136
+ const skippedNodes = [];
137
+ const blocked = /* @__PURE__ */ new Set();
133
138
  const startTime = Date.now();
134
139
  let shouldStop = false;
135
140
  for (const level of dag.levels) {
136
141
  if (shouldStop) break;
137
- const levelResults = await executeWithConcurrency(level, async (node) => {
142
+ const runnable = [];
143
+ for (const node of level) {
144
+ const blockers = skipDependents ? node.dependencies.filter((dep) => blocked.has(dep)) : [];
145
+ if (blockers.length === 0) {
146
+ runnable.push(node);
147
+ continue;
148
+ }
149
+ const message = "Skipped " + node.id + ": the dependencies " + blockers.join(", ") + " failed or were skipped";
150
+ results.set(node.id, {
151
+ node,
152
+ success: false,
153
+ skipped: true,
154
+ error: new Error(message),
155
+ duration: 0,
156
+ logs: [message]
157
+ });
158
+ skippedNodes.push(node.id);
159
+ blocked.add(node.id);
160
+ }
161
+ const levelResults = await executeWithConcurrency(runnable, async (node) => {
138
162
  const startError = notify("onNodeStart", onNodeStart, node);
139
163
  const nodeStart = Date.now();
140
164
  let result;
@@ -161,14 +185,16 @@ async function executeDAG(dag, processor, options = {}) {
161
185
  results.set(result.node.id, result);
162
186
  if (!result.success) {
163
187
  failedNodes.push(result.node.id);
188
+ blocked.add(result.node.id);
164
189
  if (failFast) shouldStop = true;
165
190
  }
166
191
  }
167
192
  }
168
193
  return {
169
- success: failedNodes.length === 0,
194
+ success: failedNodes.length === 0 && skippedNodes.length === 0,
170
195
  results,
171
196
  failedNodes,
197
+ skippedNodes,
172
198
  totalDuration: Date.now() - startTime
173
199
  };
174
200
  }
@@ -1 +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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"}
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 * With `skipDependents` (the default), a node whose dependency failed or was skipped does not start. Its result has\n * `skipped: true` and an error that names those dependencies. A dependency outside the DAG does not count.\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 skipDependents = true,\n onNodeStart,\n onNodeComplete,\n } = options;\n\n const results = new Map<string, NodeResult<TNode>>();\n const failedNodes: string[] = [];\n const skippedNodes: string[] = [];\n // The ids of the nodes that failed or were skipped\n const blocked = new Set<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 runnable: TNode[] = [];\n for (const node of level) {\n const blockers = skipDependents ? node.dependencies.filter((dep) => blocked.has(dep)) : [];\n if (blockers.length === 0) {\n runnable.push(node);\n continue;\n }\n const message = \"Skipped \" + node.id + \": the dependencies \" + blockers.join(\", \") + \" failed or were skipped\";\n results.set(node.id, { node, success: false, skipped: true, error: new Error(message), duration: 0, logs: [message] });\n skippedNodes.push(node.id);\n blocked.add(node.id);\n }\n\n const levelResults = await executeWithConcurrency(\n runnable,\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 blocked.add(result.node.id);\n if (failFast) {\n shouldStop = true;\n }\n }\n }\n }\n\n return {\n success: failedNodes.length === 0 && skippedNodes.length === 0,\n results,\n failedNodes,\n skippedNodes,\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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"}
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.5.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",
@@ -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
- level,
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
- /** Names of failed nodes */
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
  }