@vltpkg/graph-run 1.0.0-rc.22 → 1.0.0-rc.24
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/dist/index.d.ts +234 -0
- package/dist/index.js +539 -0
- package/package.json +3 -3
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codes indicating the type of error that was encountered.
|
|
3
|
+
* These are found on the `Error.cause.code` field.
|
|
4
|
+
*
|
|
5
|
+
* They are:
|
|
6
|
+
*
|
|
7
|
+
* - `'GRAPHRUN_TRAVERSAL'` The command run on a given node has failed, either
|
|
8
|
+
* by throwing an error, or by returning a rejected promise.
|
|
9
|
+
* - `'GRAPHRUN_NO_NODES'` An empty list of initial nodes was provided to the
|
|
10
|
+
* graph run operation. At least one starting node must be present in the
|
|
11
|
+
* list.
|
|
12
|
+
* - `'GRAPHRUN_CYCLE_WITHOUT_PATH'` - A cycle in the graph was detected, but
|
|
13
|
+
* the path *to* the node where the cycle was detected could not be
|
|
14
|
+
* determined. This is impossible, and cannot ever happen.
|
|
15
|
+
*/
|
|
16
|
+
export type ErrorCode = 'GRAPHRUN_NO_NODES' | 'GRAPHRUN_CYCLE_WITHOUT_PATH' | 'GRAPHRUN_TRAVERSAL';
|
|
17
|
+
export type ErrorCause<Node> = {
|
|
18
|
+
code: ErrorCode;
|
|
19
|
+
} & ({
|
|
20
|
+
code: 'GRAPHRUN_NO_NODES';
|
|
21
|
+
found: unknown;
|
|
22
|
+
wanted: string;
|
|
23
|
+
} | {
|
|
24
|
+
code: 'GRAPHRUN_CYCLE_WITHOUT_PATH';
|
|
25
|
+
} | {
|
|
26
|
+
code: 'GRAPHRUN_TRAVERSAL';
|
|
27
|
+
cause: Error;
|
|
28
|
+
node: Node;
|
|
29
|
+
path: Node[];
|
|
30
|
+
});
|
|
31
|
+
export declare const isGraphRunError: <Node>(er: unknown) => er is Error & {
|
|
32
|
+
cause: ErrorCause<Node>;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Options that define the graph and how to traverse it
|
|
36
|
+
*/
|
|
37
|
+
export interface RunnerOptions<Node, Result = void> {
|
|
38
|
+
/** Array of one or more entry nodes. */
|
|
39
|
+
graph: [node: Node, ...rest: Node[]];
|
|
40
|
+
/** get the dependencies of a given node */
|
|
41
|
+
getDeps: (node: Node) => Node[] | Promise<Node[]>;
|
|
42
|
+
/** action to take on each node */
|
|
43
|
+
visit: (node: Node, signal: AbortSignal, path: Node[], depResults: DepResults<Node, Result>) => Result | Promise<Result>;
|
|
44
|
+
/**
|
|
45
|
+
* Called when a cycle is encountered.
|
|
46
|
+
* Throw in this method to enforce a DAG graph.
|
|
47
|
+
* If left undefined, then cycles are silently ignored and skipped.
|
|
48
|
+
*
|
|
49
|
+
* `node` parameter is the dependency that is being skipped.
|
|
50
|
+
*
|
|
51
|
+
* `cycle` is the route from the dependent back to itself via
|
|
52
|
+
* the parent.
|
|
53
|
+
*
|
|
54
|
+
* `path` is the path to the dependent who wanted this dep to be
|
|
55
|
+
* loaded.
|
|
56
|
+
*/
|
|
57
|
+
onCycle?: (node: Node, cycle: Node[], path: Node[]) => void | Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Set to `false` to continue operations even if errors occur.
|
|
60
|
+
* If set to false, then an AggregateError will be raised on failure
|
|
61
|
+
* containing all failures (even if only one). If true, then a normal
|
|
62
|
+
* Error will be raised on failure.
|
|
63
|
+
* @default true
|
|
64
|
+
*/
|
|
65
|
+
failFast?: boolean;
|
|
66
|
+
/** a signal that will trigger the graph traversal to end prematurely */
|
|
67
|
+
signal?: AbortSignal;
|
|
68
|
+
}
|
|
69
|
+
export type DepResults<Node, Result> = Map<Node, Result | undefined>;
|
|
70
|
+
/**
|
|
71
|
+
* Options that can define a synchronous graph traversal.
|
|
72
|
+
*
|
|
73
|
+
* Note that if the visit() method is async, then the *promises themselves*
|
|
74
|
+
* will be used as the `Result` type, which is likely not what you want!
|
|
75
|
+
*/
|
|
76
|
+
export interface RunnerOptionsSync<Node, Result = void> extends RunnerOptions<Node, Result> {
|
|
77
|
+
/** Get a set of dependency nodes synchronously */
|
|
78
|
+
getDeps: (node: Node) => Node[];
|
|
79
|
+
/** Visit a node synchronously */
|
|
80
|
+
visit: (node: Node, signal: AbortSignal, path: Node[], depResults: DepResults<Node, Result>) => Result;
|
|
81
|
+
/** Handle cycles synchronously */
|
|
82
|
+
onCycle?: (node: Node, cycle: Node[], path: Node[]) => void;
|
|
83
|
+
}
|
|
84
|
+
/** A map of nodes to their PromiseSettledResult value */
|
|
85
|
+
export type SettledMap<Node, Result = void> = Map<Node, PromiseSettledResult<Result>>;
|
|
86
|
+
/** Any function or class. Used for Error.captureStackTrace */
|
|
87
|
+
export type Callable = Function | ((...a: unknown[]) => unknown) | (new (...a: unknown[]) => unknown);
|
|
88
|
+
/** Base class of Runner and RunnerSync */
|
|
89
|
+
export declare abstract class RunnerBase<
|
|
90
|
+
/** The type of thing to be found in this graph */
|
|
91
|
+
Node,
|
|
92
|
+
/** Type returned by the visit() method */
|
|
93
|
+
Result = void, Sync extends boolean = false, O extends Sync extends true ? RunnerOptionsSync<Node, Result> : RunnerOptions<Node, Result> = Sync extends true ? RunnerOptionsSync<Node, Result> : RunnerOptions<Node, Result>> {
|
|
94
|
+
/** The map of traversal results */
|
|
95
|
+
readonly results: Map<Node, Result>;
|
|
96
|
+
/** The map of PromiseSettledResult objects */
|
|
97
|
+
readonly settled: SettledMap<Node, Result>;
|
|
98
|
+
/** Set of dependents (direct & transitive) on each node */
|
|
99
|
+
readonly dependents: Map<Node, Set<Node>>;
|
|
100
|
+
/** Set of direct dependents on each node */
|
|
101
|
+
readonly directDependents: Map<Node, Set<Node>>;
|
|
102
|
+
/** Options provided to constructor */
|
|
103
|
+
readonly options: O;
|
|
104
|
+
/**
|
|
105
|
+
* AbortController used internally to abort the process.
|
|
106
|
+
*
|
|
107
|
+
* This is internal only, and triggering it at the wrong time may cause
|
|
108
|
+
* undefined and unsupported behavior. Do not use!
|
|
109
|
+
*
|
|
110
|
+
* Instead, if you want to be able to abort the walk at any time, provide
|
|
111
|
+
* your own AbortSignal in the opions.
|
|
112
|
+
* @internal
|
|
113
|
+
*/
|
|
114
|
+
readonly abortController: AbortController;
|
|
115
|
+
/** True if we are in failFast mode */
|
|
116
|
+
readonly failFast: boolean;
|
|
117
|
+
/** Rejections and Errors encountered in the traversal */
|
|
118
|
+
readonly errors: unknown[];
|
|
119
|
+
/**
|
|
120
|
+
* Function defining the callsite where the traversal was initiated,
|
|
121
|
+
* used for Error.captureStackTrace.
|
|
122
|
+
*/
|
|
123
|
+
readonly from: Callable;
|
|
124
|
+
constructor(options: O, from?: Callable);
|
|
125
|
+
/** Initiate the graph traversal, resolving/returning when complete */
|
|
126
|
+
abstract run(): Sync extends true ? void : Promise<void>;
|
|
127
|
+
/** Get the dependencies of a given node */
|
|
128
|
+
abstract getDeps(n: Node): Sync extends true ? Node[] : Promise<Node[]>;
|
|
129
|
+
/** Visit a node. Calls `options.visit()` */
|
|
130
|
+
abstract visit(n: Node, path: Node[], depResults: DepResults<Node, Result>): Sync extends true ? Result : Promise<Result>;
|
|
131
|
+
/**
|
|
132
|
+
* Calls the `options.onCycle()` method when a cycle is detected.
|
|
133
|
+
*/
|
|
134
|
+
abstract onCycle(n: Node, cycle: Node[], path: Node[]): Sync extends true ? void : void | Promise<void>;
|
|
135
|
+
/**
|
|
136
|
+
* For a Node `n` that depends directly or transitively on Node `d`, find the
|
|
137
|
+
* shortest known dependency path from `n` to `d`. This is done by walking
|
|
138
|
+
* backwards breadth-first up the dependency relations from `d` until `n` is
|
|
139
|
+
* found.
|
|
140
|
+
*
|
|
141
|
+
* If no known path can be found, then `undefined` is returned. Otherwise,
|
|
142
|
+
* a path array is returned that starts with `n` and ends with `d`.
|
|
143
|
+
*
|
|
144
|
+
* Note that self-referential links are never considered, since they're
|
|
145
|
+
* by definition cyclical.
|
|
146
|
+
*/
|
|
147
|
+
route(n: Node, d: Node): undefined | [n: Node, ...path: Node[]];
|
|
148
|
+
/**
|
|
149
|
+
* If the dependency from `n -> d` at the specified path would
|
|
150
|
+
* cause a cycle, then call the onCycle method and return true.
|
|
151
|
+
* Oherwise, assign the appropriate entries in the dependency
|
|
152
|
+
* tracking sets, and return false.
|
|
153
|
+
* @internal
|
|
154
|
+
*/
|
|
155
|
+
cycleCheck(n: Node, path: Node[], d: Node): boolean;
|
|
156
|
+
/**
|
|
157
|
+
* Method that handles errors raised by visits.
|
|
158
|
+
* @internal
|
|
159
|
+
*/
|
|
160
|
+
handleError(er: unknown, n: Node, path: Node[]): void;
|
|
161
|
+
/**
|
|
162
|
+
* Method that handles successful visit results
|
|
163
|
+
* @internal
|
|
164
|
+
*/
|
|
165
|
+
handleValue(value: Result, n: Node): void;
|
|
166
|
+
}
|
|
167
|
+
/** Asynchronous graph runner */
|
|
168
|
+
export declare class Runner<Node, Result> extends RunnerBase<Node, Result, false, RunnerOptions<Node, Result>> {
|
|
169
|
+
#private;
|
|
170
|
+
/** Map of nodes currently awaiting completion */
|
|
171
|
+
readonly running: Map<Node, Promise<void>>;
|
|
172
|
+
/** Track which node's promise is waiting for which other nodes */
|
|
173
|
+
readonly promiseWaiting: Map<Node, Set<Node>>;
|
|
174
|
+
getDeps(n: Node): Promise<Node[]>;
|
|
175
|
+
visit(n: Node, path: Node[], depResults: DepResults<Node, Result>): Promise<Result>;
|
|
176
|
+
onCycle(n: Node, cycle: Node[], path: Node[]): Promise<void>;
|
|
177
|
+
run(): Promise<void>;
|
|
178
|
+
}
|
|
179
|
+
/** Synchronous graph runner */
|
|
180
|
+
export declare class RunnerSync<Node, Result> extends RunnerBase<Node, Result, true, RunnerOptionsSync<Node, Result>> {
|
|
181
|
+
#private;
|
|
182
|
+
getDeps(n: Node): Node[];
|
|
183
|
+
visit(n: Node, path: Node[], depResults: DepResults<Node, Result>): Result;
|
|
184
|
+
onCycle(n: Node, cycle: Node[], path: Node[]): void;
|
|
185
|
+
run(): Map<Node, Result>;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Asynchronous graph traversal method
|
|
189
|
+
*
|
|
190
|
+
* If `failFast:false` is set in the options, then an AggregateError
|
|
191
|
+
* will be raised if there were any failures. Otherwise, a normal Error
|
|
192
|
+
* is raised on failure.
|
|
193
|
+
*/
|
|
194
|
+
export declare const graphRun: <Node, Result>(options: RunnerOptions<Node, Result>) => Promise<Map<Node, Result>>;
|
|
195
|
+
/**
|
|
196
|
+
* Synchronous graph traversal method
|
|
197
|
+
*
|
|
198
|
+
* If `failFast:false` is set in the options, then an AggregateError
|
|
199
|
+
* will be thrown if there were any failures. Otherwise, a normal Error
|
|
200
|
+
* is thrown on failure.
|
|
201
|
+
*/
|
|
202
|
+
export declare const graphRunSync: <Node, Result>(options: RunnerOptionsSync<Node, Result>) => Map<Node, Result>;
|
|
203
|
+
/**
|
|
204
|
+
* Asynchronous graph traversal, capturing all error/result
|
|
205
|
+
* statuses.
|
|
206
|
+
*/
|
|
207
|
+
export declare const allSettled: <Node, Result>(options: RunnerOptions<Node, Result>) => Promise<SettledMap<Node, Result>>;
|
|
208
|
+
/**
|
|
209
|
+
* Synchronous graph traversal, capturing all error/result
|
|
210
|
+
* statuses.
|
|
211
|
+
*/
|
|
212
|
+
export declare const allSettledSync: <Node, Result>(options: RunnerOptionsSync<Node, Result>) => SettledMap<Node, Result>;
|
|
213
|
+
/**
|
|
214
|
+
* Asynchronous graph traversal, returning the first successful visit.
|
|
215
|
+
* If all visits fail, then an AggregateError is raised with all errors
|
|
216
|
+
* encountered.
|
|
217
|
+
*/
|
|
218
|
+
export declare const any: <Node, Result>(options: RunnerOptions<Node, Result>) => Promise<Result>;
|
|
219
|
+
/**
|
|
220
|
+
* Synchronous graph traversal, returning the first successful visit.
|
|
221
|
+
* If all visits fail, then an AggregateError is thrown with all errors
|
|
222
|
+
* encountered.
|
|
223
|
+
*/
|
|
224
|
+
export declare const anySync: <Node, Result>(options: RunnerOptionsSync<Node, Result>) => Result;
|
|
225
|
+
/**
|
|
226
|
+
* Asynchronous graph traversal, resolving or rejecting when the first visit
|
|
227
|
+
* resolves or rejects.
|
|
228
|
+
*/
|
|
229
|
+
export declare const race: <Node, Result>(options: RunnerOptions<Node, Result>) => Promise<Result>;
|
|
230
|
+
/**
|
|
231
|
+
* Synchronous graph traversal, returning or throwing when the first visit
|
|
232
|
+
* is completed.
|
|
233
|
+
*/
|
|
234
|
+
export declare const raceSync: <Node, Result>(options: RunnerOptionsSync<Node, Result>) => Result;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
import { setMaxListeners } from 'node:events';
|
|
2
|
+
export const isGraphRunError = (er) => !!er &&
|
|
3
|
+
typeof er === 'object' &&
|
|
4
|
+
'cause' in er &&
|
|
5
|
+
!!er.cause &&
|
|
6
|
+
typeof er.cause === 'object' &&
|
|
7
|
+
'code' in er.cause &&
|
|
8
|
+
((er.cause.code === 'GRAPHRUN_NO_NODES' &&
|
|
9
|
+
'found' in er.cause &&
|
|
10
|
+
'wanted' in er.cause &&
|
|
11
|
+
typeof er.cause.wanted === 'string') ||
|
|
12
|
+
(er.cause.code === 'GRAPHRUN_TRAVERSAL' &&
|
|
13
|
+
'path' in er.cause &&
|
|
14
|
+
Array.isArray(er.cause.path) &&
|
|
15
|
+
'node' in er.cause &&
|
|
16
|
+
'cause' in er.cause) ||
|
|
17
|
+
er.cause.code === 'GRAPHRUN_CYCLE_WITHOUT_PATH');
|
|
18
|
+
/** Base class of Runner and RunnerSync */
|
|
19
|
+
export class RunnerBase {
|
|
20
|
+
/** The map of traversal results */
|
|
21
|
+
results = new Map();
|
|
22
|
+
/** The map of PromiseSettledResult objects */
|
|
23
|
+
settled = new Map();
|
|
24
|
+
/** Set of dependents (direct & transitive) on each node */
|
|
25
|
+
dependents = new Map();
|
|
26
|
+
/** Set of direct dependents on each node */
|
|
27
|
+
directDependents = new Map();
|
|
28
|
+
/** Options provided to constructor */
|
|
29
|
+
options;
|
|
30
|
+
/**
|
|
31
|
+
* AbortController used internally to abort the process.
|
|
32
|
+
*
|
|
33
|
+
* This is internal only, and triggering it at the wrong time may cause
|
|
34
|
+
* undefined and unsupported behavior. Do not use!
|
|
35
|
+
*
|
|
36
|
+
* Instead, if you want to be able to abort the walk at any time, provide
|
|
37
|
+
* your own AbortSignal in the opions.
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
abortController;
|
|
41
|
+
/** True if we are in failFast mode */
|
|
42
|
+
failFast;
|
|
43
|
+
/** Rejections and Errors encountered in the traversal */
|
|
44
|
+
errors = [];
|
|
45
|
+
/**
|
|
46
|
+
* Function defining the callsite where the traversal was initiated,
|
|
47
|
+
* used for Error.captureStackTrace.
|
|
48
|
+
*/
|
|
49
|
+
from;
|
|
50
|
+
constructor(options, from) {
|
|
51
|
+
const ac = new AbortController();
|
|
52
|
+
this.from = from ?? this.constructor;
|
|
53
|
+
this.abortController = ac;
|
|
54
|
+
setMaxListeners(Infinity, ac.signal);
|
|
55
|
+
this.options = options;
|
|
56
|
+
if (!options.graph.length) {
|
|
57
|
+
const er = new Error('no nodes provided to graph traversal', {
|
|
58
|
+
cause: {
|
|
59
|
+
code: 'GRAPHRUN_NO_NODES',
|
|
60
|
+
found: options.graph,
|
|
61
|
+
wanted: '[first: Node, ...rest: Node[]]',
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
Error.captureStackTrace(er, from);
|
|
65
|
+
throw er;
|
|
66
|
+
}
|
|
67
|
+
this.failFast = options.failFast !== false;
|
|
68
|
+
const { signal } = options;
|
|
69
|
+
if (signal !== undefined) {
|
|
70
|
+
signal.addEventListener('abort', reason => ac.abort(reason), {
|
|
71
|
+
once: true,
|
|
72
|
+
signal: ac.signal,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* For a Node `n` that depends directly or transitively on Node `d`, find the
|
|
78
|
+
* shortest known dependency path from `n` to `d`. This is done by walking
|
|
79
|
+
* backwards breadth-first up the dependency relations from `d` until `n` is
|
|
80
|
+
* found.
|
|
81
|
+
*
|
|
82
|
+
* If no known path can be found, then `undefined` is returned. Otherwise,
|
|
83
|
+
* a path array is returned that starts with `n` and ends with `d`.
|
|
84
|
+
*
|
|
85
|
+
* Note that self-referential links are never considered, since they're
|
|
86
|
+
* by definition cyclical.
|
|
87
|
+
*/
|
|
88
|
+
route(n, d) {
|
|
89
|
+
const dependents = this.dependents.get(d);
|
|
90
|
+
if (!dependents?.has(n))
|
|
91
|
+
return undefined;
|
|
92
|
+
const directDependents = this.directDependents.get(d);
|
|
93
|
+
/* c8 ignore next */
|
|
94
|
+
if (!directDependents)
|
|
95
|
+
return undefined;
|
|
96
|
+
if (directDependents.has(n)) {
|
|
97
|
+
return [n, d];
|
|
98
|
+
}
|
|
99
|
+
const queue = [
|
|
100
|
+
...directDependents,
|
|
101
|
+
].map(dd => [dd, d]);
|
|
102
|
+
let step = undefined;
|
|
103
|
+
while (undefined !== (step = queue.shift())) {
|
|
104
|
+
/* c8 ignore next */
|
|
105
|
+
if (!dependents.has(step[0]))
|
|
106
|
+
continue;
|
|
107
|
+
if (step[0] === n) {
|
|
108
|
+
return step;
|
|
109
|
+
}
|
|
110
|
+
const ddd = this.directDependents.get(step[0]);
|
|
111
|
+
if (ddd) {
|
|
112
|
+
for (const d of ddd) {
|
|
113
|
+
queue.push([d, ...step]);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* If the dependency from `n -> d` at the specified path would
|
|
120
|
+
* cause a cycle, then call the onCycle method and return true.
|
|
121
|
+
* Oherwise, assign the appropriate entries in the dependency
|
|
122
|
+
* tracking sets, and return false.
|
|
123
|
+
* @internal
|
|
124
|
+
*/
|
|
125
|
+
cycleCheck(n, path, d) {
|
|
126
|
+
/* c8 ignore next */
|
|
127
|
+
const dependents = this.dependents.get(n) ?? new Set();
|
|
128
|
+
this.dependents.set(n, dependents);
|
|
129
|
+
const isCycle = dependents.has(d);
|
|
130
|
+
if (isCycle) {
|
|
131
|
+
const cycle = this.route(d, n);
|
|
132
|
+
/* c8 ignore start - impossible */
|
|
133
|
+
if (!cycle) {
|
|
134
|
+
throw new Error('cycle detected, but cycle route not found', {
|
|
135
|
+
cause: { code: 'GRAPHRUN_CYCLE_WITHOUT_PATH' },
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/* c8 ignore stop */
|
|
139
|
+
cycle.unshift(n);
|
|
140
|
+
void this.onCycle(d, cycle, path);
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
const depDD = this.directDependents.get(d) ?? new Set();
|
|
144
|
+
this.directDependents.set(d, depDD);
|
|
145
|
+
depDD.add(n);
|
|
146
|
+
const depDependents = this.dependents.get(d) ?? new Set();
|
|
147
|
+
this.dependents.set(d, depDependents);
|
|
148
|
+
for (const n of dependents) {
|
|
149
|
+
depDependents.add(n);
|
|
150
|
+
}
|
|
151
|
+
depDependents.add(n);
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Method that handles errors raised by visits.
|
|
156
|
+
* @internal
|
|
157
|
+
*/
|
|
158
|
+
handleError(er, n, path) {
|
|
159
|
+
this.errors.push(er);
|
|
160
|
+
this.settled.set(n, {
|
|
161
|
+
status: 'rejected',
|
|
162
|
+
reason: er,
|
|
163
|
+
});
|
|
164
|
+
if (this.failFast) {
|
|
165
|
+
this.abortController.abort(er);
|
|
166
|
+
const e = new Error('failed graph traversal', {
|
|
167
|
+
cause: {
|
|
168
|
+
code: 'GRAPHRUN_TRAVERSAL',
|
|
169
|
+
node: n,
|
|
170
|
+
path,
|
|
171
|
+
cause: er,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
Error.captureStackTrace(e, this.from);
|
|
175
|
+
throw e;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Method that handles successful visit results
|
|
180
|
+
* @internal
|
|
181
|
+
*/
|
|
182
|
+
handleValue(value, n) {
|
|
183
|
+
this.results.set(n, value);
|
|
184
|
+
this.settled.set(n, {
|
|
185
|
+
status: 'fulfilled',
|
|
186
|
+
value,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/** Asynchronous graph runner */
|
|
191
|
+
export class Runner extends RunnerBase {
|
|
192
|
+
/** Map of nodes currently awaiting completion */
|
|
193
|
+
running = new Map();
|
|
194
|
+
/** Track which node's promise is waiting for which other nodes */
|
|
195
|
+
promiseWaiting = new Map();
|
|
196
|
+
async getDeps(n) {
|
|
197
|
+
/* c8 ignore next */
|
|
198
|
+
if (this.abortController.signal.aborted)
|
|
199
|
+
return [];
|
|
200
|
+
const deps = await this.options.getDeps(n);
|
|
201
|
+
for (const d of deps) {
|
|
202
|
+
const dependents = this.dependents.get(d) ?? new Set();
|
|
203
|
+
this.dependents.set(d, dependents);
|
|
204
|
+
dependents.add(n);
|
|
205
|
+
const depDD = this.directDependents.get(d) ?? new Set();
|
|
206
|
+
this.directDependents.set(d, depDD);
|
|
207
|
+
depDD.add(n);
|
|
208
|
+
}
|
|
209
|
+
return deps;
|
|
210
|
+
}
|
|
211
|
+
async visit(n, path, depResults) {
|
|
212
|
+
const { signal } = this.abortController;
|
|
213
|
+
return this.options.visit(n, signal, path, depResults);
|
|
214
|
+
}
|
|
215
|
+
async onCycle(n, cycle, path) {
|
|
216
|
+
/* c8 ignore next */
|
|
217
|
+
if (this.abortController.signal.aborted)
|
|
218
|
+
return;
|
|
219
|
+
await this.options.onCycle?.(n, cycle, path);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Check if node `target` is waiting (directly or transitively) for node `n`.
|
|
223
|
+
* If so, making `n` wait for `target` would create a deadlock.
|
|
224
|
+
*/
|
|
225
|
+
#isWaitingFor(target, n, visited = new Set()) {
|
|
226
|
+
if (visited.has(target))
|
|
227
|
+
return false;
|
|
228
|
+
visited.add(target);
|
|
229
|
+
const waiting = this.promiseWaiting.get(target);
|
|
230
|
+
if (!waiting)
|
|
231
|
+
return false;
|
|
232
|
+
if (waiting.has(n))
|
|
233
|
+
return true;
|
|
234
|
+
for (const w of waiting) {
|
|
235
|
+
if (this.#isWaitingFor(w, n, visited))
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
async #walk(n, path) {
|
|
241
|
+
const r = this.running.get(n);
|
|
242
|
+
/* c8 ignore next */
|
|
243
|
+
if (r)
|
|
244
|
+
return r;
|
|
245
|
+
/* c8 ignore start */
|
|
246
|
+
if (this.settled.get(n))
|
|
247
|
+
return;
|
|
248
|
+
/* c8 ignore stop */
|
|
249
|
+
const p = this.#step(n, path).then(() => {
|
|
250
|
+
this.running.delete(n);
|
|
251
|
+
this.promiseWaiting.delete(n);
|
|
252
|
+
},
|
|
253
|
+
/* c8 ignore start - handled deeper in the chain */
|
|
254
|
+
(er) => {
|
|
255
|
+
this.running.delete(n);
|
|
256
|
+
this.promiseWaiting.delete(n);
|
|
257
|
+
throw er;
|
|
258
|
+
});
|
|
259
|
+
this.running.set(n, p);
|
|
260
|
+
return p;
|
|
261
|
+
}
|
|
262
|
+
async #step(n, path) {
|
|
263
|
+
const dependents = this.dependents.get(n) ?? new Set();
|
|
264
|
+
this.dependents.set(n, dependents);
|
|
265
|
+
const deps = await this.getDeps(n);
|
|
266
|
+
const awaiting = [];
|
|
267
|
+
const depPath = [...path, n];
|
|
268
|
+
// Initialize waiting set for this node
|
|
269
|
+
const waitingOn = new Set();
|
|
270
|
+
this.promiseWaiting.set(n, waitingOn);
|
|
271
|
+
for (const d of deps) {
|
|
272
|
+
/* c8 ignore next */
|
|
273
|
+
if (this.abortController.signal.aborted)
|
|
274
|
+
return;
|
|
275
|
+
// self-link, skip
|
|
276
|
+
if (d === n)
|
|
277
|
+
continue;
|
|
278
|
+
if (this.cycleCheck(n, depPath, d))
|
|
279
|
+
continue;
|
|
280
|
+
/* c8 ignore next */
|
|
281
|
+
if (this.settled.get(d))
|
|
282
|
+
continue;
|
|
283
|
+
const runningPromise = this.running.get(d);
|
|
284
|
+
if (runningPromise) {
|
|
285
|
+
// Check if d is already waiting for n (promise-level cycle)
|
|
286
|
+
if (this.#isWaitingFor(d, n)) {
|
|
287
|
+
// Treat as cycle to prevent deadlock
|
|
288
|
+
void this.onCycle(d, [n, d], depPath);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// Record that n is waiting for d
|
|
293
|
+
waitingOn.add(d);
|
|
294
|
+
awaiting.push(runningPromise ?? this.#walk(d, depPath));
|
|
295
|
+
}
|
|
296
|
+
/* c8 ignore next */
|
|
297
|
+
if (this.abortController.signal.aborted)
|
|
298
|
+
return;
|
|
299
|
+
await Promise.all(awaiting);
|
|
300
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
301
|
+
if (this.abortController.signal.aborted)
|
|
302
|
+
return;
|
|
303
|
+
const depRes = new Map(deps.map(d => [d, this.results.get(d)]));
|
|
304
|
+
try {
|
|
305
|
+
this.handleValue(await this.visit(n, path, depRes), n);
|
|
306
|
+
}
|
|
307
|
+
catch (er) {
|
|
308
|
+
this.handleError(er, n, path);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async run() {
|
|
312
|
+
const promises = [];
|
|
313
|
+
for (const n of this.options.graph) {
|
|
314
|
+
promises.push(this.#walk(n, []));
|
|
315
|
+
}
|
|
316
|
+
await Promise.all(promises);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/** Synchronous graph runner */
|
|
320
|
+
export class RunnerSync extends RunnerBase {
|
|
321
|
+
getDeps(n) {
|
|
322
|
+
if (this.abortController.signal.aborted)
|
|
323
|
+
return [];
|
|
324
|
+
return this.options.getDeps(n);
|
|
325
|
+
}
|
|
326
|
+
visit(n, path, depResults) {
|
|
327
|
+
const { signal } = this.abortController;
|
|
328
|
+
return this.options.visit(n, signal, path, depResults);
|
|
329
|
+
}
|
|
330
|
+
onCycle(n, cycle, path) {
|
|
331
|
+
/* c8 ignore next */
|
|
332
|
+
if (this.abortController.signal.aborted)
|
|
333
|
+
return;
|
|
334
|
+
this.options.onCycle?.(n, cycle, path);
|
|
335
|
+
}
|
|
336
|
+
#walk(n, path) {
|
|
337
|
+
/* c8 ignore start */
|
|
338
|
+
if (this.settled.get(n))
|
|
339
|
+
return;
|
|
340
|
+
/* c8 ignore stop */
|
|
341
|
+
this.#step(n, path);
|
|
342
|
+
}
|
|
343
|
+
#step(n, path) {
|
|
344
|
+
const dependents = this.dependents.get(n) ?? new Set();
|
|
345
|
+
this.dependents.set(n, dependents);
|
|
346
|
+
const deps = this.getDeps(n);
|
|
347
|
+
const depPath = [...path, n];
|
|
348
|
+
for (const d of deps) {
|
|
349
|
+
if (this.abortController.signal.aborted)
|
|
350
|
+
return;
|
|
351
|
+
/* c8 ignore next */
|
|
352
|
+
if (d === n)
|
|
353
|
+
continue;
|
|
354
|
+
if (this.cycleCheck(n, depPath, d))
|
|
355
|
+
continue;
|
|
356
|
+
if (!this.settled.get(d))
|
|
357
|
+
this.#walk(d, depPath);
|
|
358
|
+
}
|
|
359
|
+
if (this.abortController.signal.aborted)
|
|
360
|
+
return;
|
|
361
|
+
const depRes = new Map(deps.map(d => [d, this.results.get(d)]));
|
|
362
|
+
try {
|
|
363
|
+
this.handleValue(this.visit(n, path, depRes), n);
|
|
364
|
+
}
|
|
365
|
+
catch (er) {
|
|
366
|
+
this.handleError(er, n, path);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
run() {
|
|
370
|
+
for (const n of this.options.graph) {
|
|
371
|
+
this.#walk(n, []);
|
|
372
|
+
}
|
|
373
|
+
return this.results;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Asynchronous graph traversal method
|
|
378
|
+
*
|
|
379
|
+
* If `failFast:false` is set in the options, then an AggregateError
|
|
380
|
+
* will be raised if there were any failures. Otherwise, a normal Error
|
|
381
|
+
* is raised on failure.
|
|
382
|
+
*/
|
|
383
|
+
export const graphRun = async (options) => {
|
|
384
|
+
const runner = new Runner(options, graphRun);
|
|
385
|
+
await runner.run();
|
|
386
|
+
if (runner.errors.length) {
|
|
387
|
+
const e = new AggregateError(runner.errors, 'failed graph traversal');
|
|
388
|
+
Error.captureStackTrace(e, graphRun);
|
|
389
|
+
throw e;
|
|
390
|
+
}
|
|
391
|
+
return runner.results;
|
|
392
|
+
};
|
|
393
|
+
/**
|
|
394
|
+
* Synchronous graph traversal method
|
|
395
|
+
*
|
|
396
|
+
* If `failFast:false` is set in the options, then an AggregateError
|
|
397
|
+
* will be thrown if there were any failures. Otherwise, a normal Error
|
|
398
|
+
* is thrown on failure.
|
|
399
|
+
*/
|
|
400
|
+
export const graphRunSync = (options) => {
|
|
401
|
+
const runner = new RunnerSync(options, graphRunSync);
|
|
402
|
+
runner.run();
|
|
403
|
+
if (runner.errors.length) {
|
|
404
|
+
const e = new AggregateError(runner.errors, 'failed graph traversal');
|
|
405
|
+
Error.captureStackTrace(e, graphRunSync);
|
|
406
|
+
throw e;
|
|
407
|
+
}
|
|
408
|
+
return runner.results;
|
|
409
|
+
};
|
|
410
|
+
/**
|
|
411
|
+
* Asynchronous graph traversal, capturing all error/result
|
|
412
|
+
* statuses.
|
|
413
|
+
*/
|
|
414
|
+
export const allSettled = async (options) => {
|
|
415
|
+
const runner = new Runner({ ...options, failFast: false }, allSettled);
|
|
416
|
+
await runner.run();
|
|
417
|
+
return runner.settled;
|
|
418
|
+
};
|
|
419
|
+
/**
|
|
420
|
+
* Synchronous graph traversal, capturing all error/result
|
|
421
|
+
* statuses.
|
|
422
|
+
*/
|
|
423
|
+
export const allSettledSync = (options) => {
|
|
424
|
+
const runner = new RunnerSync({ ...options, failFast: false }, allSettledSync);
|
|
425
|
+
runner.run();
|
|
426
|
+
return runner.settled;
|
|
427
|
+
};
|
|
428
|
+
/**
|
|
429
|
+
* Asynchronous graph traversal, returning the first successful visit.
|
|
430
|
+
* If all visits fail, then an AggregateError is raised with all errors
|
|
431
|
+
* encountered.
|
|
432
|
+
*/
|
|
433
|
+
export const any = async (options) => {
|
|
434
|
+
const ac = new AbortController();
|
|
435
|
+
let result;
|
|
436
|
+
let found = false;
|
|
437
|
+
const runner = new Runner({
|
|
438
|
+
...options,
|
|
439
|
+
failFast: false,
|
|
440
|
+
signal: ac.signal,
|
|
441
|
+
visit: async (node, signal, path, depResults) => {
|
|
442
|
+
try {
|
|
443
|
+
result = await options.visit(node, signal, path, depResults);
|
|
444
|
+
found = true;
|
|
445
|
+
ac.abort('found');
|
|
446
|
+
}
|
|
447
|
+
catch { }
|
|
448
|
+
return result;
|
|
449
|
+
},
|
|
450
|
+
}, any);
|
|
451
|
+
await runner.run();
|
|
452
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
453
|
+
if (!found) {
|
|
454
|
+
const e = new AggregateError(runner.errors, 'failed graph traversal');
|
|
455
|
+
Error.captureStackTrace(e, any);
|
|
456
|
+
throw e;
|
|
457
|
+
}
|
|
458
|
+
return result;
|
|
459
|
+
};
|
|
460
|
+
/**
|
|
461
|
+
* Synchronous graph traversal, returning the first successful visit.
|
|
462
|
+
* If all visits fail, then an AggregateError is thrown with all errors
|
|
463
|
+
* encountered.
|
|
464
|
+
*/
|
|
465
|
+
export const anySync = (options) => {
|
|
466
|
+
const ac = new AbortController();
|
|
467
|
+
let result;
|
|
468
|
+
let found = false;
|
|
469
|
+
const runner = new RunnerSync({
|
|
470
|
+
...options,
|
|
471
|
+
failFast: false,
|
|
472
|
+
signal: ac.signal,
|
|
473
|
+
visit: (node, signal, path, depResults) => {
|
|
474
|
+
try {
|
|
475
|
+
result = options.visit(node, signal, path, depResults);
|
|
476
|
+
found = true;
|
|
477
|
+
ac.abort('found');
|
|
478
|
+
}
|
|
479
|
+
catch { }
|
|
480
|
+
return result;
|
|
481
|
+
},
|
|
482
|
+
}, anySync);
|
|
483
|
+
runner.run();
|
|
484
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
485
|
+
if (!found) {
|
|
486
|
+
const e = new AggregateError(runner.errors, 'failed graph traversal');
|
|
487
|
+
Error.captureStackTrace(e, anySync);
|
|
488
|
+
throw e;
|
|
489
|
+
}
|
|
490
|
+
return result;
|
|
491
|
+
};
|
|
492
|
+
/**
|
|
493
|
+
* Asynchronous graph traversal, resolving or rejecting when the first visit
|
|
494
|
+
* resolves or rejects.
|
|
495
|
+
*/
|
|
496
|
+
export const race = async (options) => {
|
|
497
|
+
const ac = new AbortController();
|
|
498
|
+
let result;
|
|
499
|
+
const runner = new Runner({
|
|
500
|
+
...options,
|
|
501
|
+
failFast: true,
|
|
502
|
+
signal: ac.signal,
|
|
503
|
+
visit: async (node, signal, path, depResults) => {
|
|
504
|
+
try {
|
|
505
|
+
result = await options.visit(node, signal, path, depResults);
|
|
506
|
+
ac.abort('found');
|
|
507
|
+
/* c8 ignore next */
|
|
508
|
+
}
|
|
509
|
+
catch { }
|
|
510
|
+
return result;
|
|
511
|
+
},
|
|
512
|
+
}, race);
|
|
513
|
+
await runner.run();
|
|
514
|
+
return result;
|
|
515
|
+
};
|
|
516
|
+
/**
|
|
517
|
+
* Synchronous graph traversal, returning or throwing when the first visit
|
|
518
|
+
* is completed.
|
|
519
|
+
*/
|
|
520
|
+
export const raceSync = (options) => {
|
|
521
|
+
const ac = new AbortController();
|
|
522
|
+
let result;
|
|
523
|
+
const runner = new RunnerSync({
|
|
524
|
+
...options,
|
|
525
|
+
failFast: true,
|
|
526
|
+
signal: ac.signal,
|
|
527
|
+
visit: (node, signal, path, depResults) => {
|
|
528
|
+
try {
|
|
529
|
+
result = options.visit(node, signal, path, depResults);
|
|
530
|
+
ac.abort('found');
|
|
531
|
+
/* c8 ignore next */
|
|
532
|
+
}
|
|
533
|
+
catch { }
|
|
534
|
+
return result;
|
|
535
|
+
},
|
|
536
|
+
}, race);
|
|
537
|
+
runner.run();
|
|
538
|
+
return result;
|
|
539
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vltpkg/graph-run",
|
|
3
3
|
"description": "Run operations on a graph, maximizing parallelism",
|
|
4
|
-
"version": "1.0.0-rc.
|
|
4
|
+
"version": "1.0.0-rc.24",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/vltpkg/vltpkg.git",
|
|
@@ -41,13 +41,13 @@
|
|
|
41
41
|
"extends": "../../tap-config.yaml"
|
|
42
42
|
},
|
|
43
43
|
"prettier": "../../.prettierrc.js",
|
|
44
|
-
"module": "./
|
|
44
|
+
"module": "./dist/index.js",
|
|
45
45
|
"type": "module",
|
|
46
46
|
"exports": {
|
|
47
47
|
"./package.json": "./package.json",
|
|
48
48
|
".": {
|
|
49
49
|
"import": {
|
|
50
|
-
"default": "./
|
|
50
|
+
"default": "./dist/index.js"
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
},
|