@apiquest/fracture 1.0.2 → 1.0.4
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/README.md +119 -0
- package/bin/cli.js +2 -2
- package/dist/CollectionRunner.js +3 -3
- package/dist/ScriptEngine.js +4 -4
- package/dist/cli/plugin-commands.d.ts.map +1 -1
- package/dist/cli/plugin-commands.js +2 -1
- package/dist/cli/plugin-commands.js.map +1 -1
- package/package.json +55 -50
- package/src/CollectionAnalyzer.ts +102 -102
- package/src/CollectionRunner.ts +1423 -1423
- package/src/CollectionRunner.types.ts +9 -9
- package/src/CollectionValidator.ts +289 -289
- package/src/ConsoleReporter.ts +143 -143
- package/src/CookieJar.ts +258 -258
- package/src/DagScheduler.ts +439 -439
- package/src/Logger.ts +85 -85
- package/src/PluginLoader.ts +126 -126
- package/src/PluginManager.ts +208 -208
- package/src/PluginResolver.ts +154 -154
- package/src/QuestAPI.ts +764 -764
- package/src/QuestAPI.types.ts +33 -33
- package/src/QuestTestAPI.ts +164 -164
- package/src/RequestFilter.ts +224 -224
- package/src/ScriptEngine.ts +219 -219
- package/src/ScriptValidator.ts +428 -428
- package/src/TaskGraph.ts +598 -598
- package/src/TestCounter.ts +109 -109
- package/src/VariableResolver.ts +114 -114
- package/src/cli/index.ts +480 -480
- package/src/cli/plugin-commands.ts +342 -341
- package/src/cli/plugin-discovery.ts +44 -44
- package/src/index.ts +24 -24
- package/src/utils.ts +52 -52
- package/tsconfig.json +20 -20
- package/tsconfig.test.json +5 -5
- package/vitest.config.ts +22 -22
- package/dist/ExecutionTree.d.ts +0 -77
- package/dist/ExecutionTree.d.ts.map +0 -1
- package/dist/ExecutionTree.js +0 -265
- package/dist/ExecutionTree.js.map +0 -1
- package/dist/fracture/src/CollectionAnalyzer.d.ts +0 -17
- package/dist/fracture/src/CollectionAnalyzer.d.ts.map +0 -1
- package/dist/fracture/src/CollectionAnalyzer.js +0 -70
- package/dist/fracture/src/CollectionAnalyzer.js.map +0 -1
- package/dist/fracture/src/CollectionRunner.d.ts +0 -39
- package/dist/fracture/src/CollectionRunner.d.ts.map +0 -1
- package/dist/fracture/src/CollectionRunner.js +0 -802
- package/dist/fracture/src/CollectionRunner.js.map +0 -1
- package/dist/fracture/src/CollectionRunner.types.d.ts +0 -8
- package/dist/fracture/src/CollectionRunner.types.d.ts.map +0 -1
- package/dist/fracture/src/CollectionRunner.types.js +0 -2
- package/dist/fracture/src/CollectionRunner.types.js.map +0 -1
- package/dist/fracture/src/CollectionValidator.d.ts +0 -14
- package/dist/fracture/src/CollectionValidator.d.ts.map +0 -1
- package/dist/fracture/src/CollectionValidator.js +0 -145
- package/dist/fracture/src/CollectionValidator.js.map +0 -1
- package/dist/fracture/src/ConsoleReporter.d.ts +0 -24
- package/dist/fracture/src/ConsoleReporter.d.ts.map +0 -1
- package/dist/fracture/src/ConsoleReporter.js +0 -123
- package/dist/fracture/src/ConsoleReporter.js.map +0 -1
- package/dist/fracture/src/CookieJar.d.ts +0 -70
- package/dist/fracture/src/CookieJar.d.ts.map +0 -1
- package/dist/fracture/src/CookieJar.js +0 -233
- package/dist/fracture/src/CookieJar.js.map +0 -1
- package/dist/fracture/src/ExecutionTree.d.ts +0 -77
- package/dist/fracture/src/ExecutionTree.d.ts.map +0 -1
- package/dist/fracture/src/ExecutionTree.js +0 -258
- package/dist/fracture/src/ExecutionTree.js.map +0 -1
- package/dist/fracture/src/Logger.d.ts +0 -25
- package/dist/fracture/src/Logger.d.ts.map +0 -1
- package/dist/fracture/src/Logger.js +0 -78
- package/dist/fracture/src/Logger.js.map +0 -1
- package/dist/fracture/src/PluginLoader.d.ts +0 -23
- package/dist/fracture/src/PluginLoader.d.ts.map +0 -1
- package/dist/fracture/src/PluginLoader.js +0 -102
- package/dist/fracture/src/PluginLoader.js.map +0 -1
- package/dist/fracture/src/PluginManager.d.ts +0 -64
- package/dist/fracture/src/PluginManager.d.ts.map +0 -1
- package/dist/fracture/src/PluginManager.js +0 -162
- package/dist/fracture/src/PluginManager.js.map +0 -1
- package/dist/fracture/src/PluginResolver.d.ts +0 -35
- package/dist/fracture/src/PluginResolver.d.ts.map +0 -1
- package/dist/fracture/src/PluginResolver.js +0 -128
- package/dist/fracture/src/PluginResolver.js.map +0 -1
- package/dist/fracture/src/QuestAPI.d.ts +0 -9
- package/dist/fracture/src/QuestAPI.d.ts.map +0 -1
- package/dist/fracture/src/QuestAPI.js +0 -679
- package/dist/fracture/src/QuestAPI.js.map +0 -1
- package/dist/fracture/src/QuestAPI.types.d.ts +0 -35
- package/dist/fracture/src/QuestAPI.types.d.ts.map +0 -1
- package/dist/fracture/src/QuestAPI.types.js +0 -3
- package/dist/fracture/src/QuestAPI.types.js.map +0 -1
- package/dist/fracture/src/QuestTestAPI.d.ts +0 -12
- package/dist/fracture/src/QuestTestAPI.d.ts.map +0 -1
- package/dist/fracture/src/QuestTestAPI.js +0 -133
- package/dist/fracture/src/QuestTestAPI.js.map +0 -1
- package/dist/fracture/src/ScriptEngine.d.ts +0 -21
- package/dist/fracture/src/ScriptEngine.d.ts.map +0 -1
- package/dist/fracture/src/ScriptEngine.js +0 -183
- package/dist/fracture/src/ScriptEngine.js.map +0 -1
- package/dist/fracture/src/ScriptValidator.d.ts +0 -68
- package/dist/fracture/src/ScriptValidator.d.ts.map +0 -1
- package/dist/fracture/src/ScriptValidator.js +0 -351
- package/dist/fracture/src/ScriptValidator.js.map +0 -1
- package/dist/fracture/src/TestCounter.d.ts +0 -18
- package/dist/fracture/src/TestCounter.d.ts.map +0 -1
- package/dist/fracture/src/TestCounter.js +0 -82
- package/dist/fracture/src/TestCounter.js.map +0 -1
- package/dist/fracture/src/VariableResolver.d.ts +0 -20
- package/dist/fracture/src/VariableResolver.d.ts.map +0 -1
- package/dist/fracture/src/VariableResolver.js +0 -100
- package/dist/fracture/src/VariableResolver.js.map +0 -1
- package/dist/fracture/src/cli/index.d.ts +0 -3
- package/dist/fracture/src/cli/index.d.ts.map +0 -1
- package/dist/fracture/src/cli/index.js +0 -347
- package/dist/fracture/src/cli/index.js.map +0 -1
- package/dist/fracture/src/cli/plugin-commands.d.ts +0 -6
- package/dist/fracture/src/cli/plugin-commands.d.ts.map +0 -1
- package/dist/fracture/src/cli/plugin-commands.js +0 -263
- package/dist/fracture/src/cli/plugin-commands.js.map +0 -1
- package/dist/fracture/src/cli/plugin-discovery.d.ts +0 -11
- package/dist/fracture/src/cli/plugin-discovery.d.ts.map +0 -1
- package/dist/fracture/src/cli/plugin-discovery.js +0 -64
- package/dist/fracture/src/cli/plugin-discovery.js.map +0 -1
- package/dist/fracture/src/index.d.ts +0 -13
- package/dist/fracture/src/index.d.ts.map +0 -1
- package/dist/fracture/src/index.js +0 -17
- package/dist/fracture/src/index.js.map +0 -1
- package/dist/fracture/src/utils.d.ts +0 -28
- package/dist/fracture/src/utils.d.ts.map +0 -1
- package/dist/fracture/src/utils.js +0 -48
- package/dist/fracture/src/utils.js.map +0 -1
- package/dist/plugin-auth/src/apikey-auth.d.ts +0 -3
- package/dist/plugin-auth/src/apikey-auth.d.ts.map +0 -1
- package/dist/plugin-auth/src/apikey-auth.js +0 -73
- package/dist/plugin-auth/src/apikey-auth.js.map +0 -1
- package/dist/plugin-auth/src/basic-auth.d.ts +0 -3
- package/dist/plugin-auth/src/basic-auth.d.ts.map +0 -1
- package/dist/plugin-auth/src/basic-auth.js +0 -61
- package/dist/plugin-auth/src/basic-auth.js.map +0 -1
- package/dist/plugin-auth/src/bearer-auth.d.ts +0 -3
- package/dist/plugin-auth/src/bearer-auth.d.ts.map +0 -1
- package/dist/plugin-auth/src/bearer-auth.js +0 -49
- package/dist/plugin-auth/src/bearer-auth.js.map +0 -1
- package/dist/plugin-auth/src/helpers.d.ts +0 -3
- package/dist/plugin-auth/src/helpers.d.ts.map +0 -1
- package/dist/plugin-auth/src/helpers.js +0 -8
- package/dist/plugin-auth/src/helpers.js.map +0 -1
- package/dist/plugin-auth/src/index.d.ts +0 -10
- package/dist/plugin-auth/src/index.d.ts.map +0 -1
- package/dist/plugin-auth/src/index.js +0 -25
- package/dist/plugin-auth/src/index.js.map +0 -1
- package/dist/plugin-auth/src/oauth2-auth.d.ts +0 -35
- package/dist/plugin-auth/src/oauth2-auth.d.ts.map +0 -1
- package/dist/plugin-auth/src/oauth2-auth.js +0 -266
- package/dist/plugin-auth/src/oauth2-auth.js.map +0 -1
- package/dist/plugin-http/src/index.d.ts +0 -4
- package/dist/plugin-http/src/index.d.ts.map +0 -1
- package/dist/plugin-http/src/index.js +0 -266
- package/dist/plugin-http/src/index.js.map +0 -1
- package/dist/plugin-vault-file/src/index.d.ts +0 -67
- package/dist/plugin-vault-file/src/index.d.ts.map +0 -1
- package/dist/plugin-vault-file/src/index.js +0 -171
- package/dist/plugin-vault-file/src/index.js.map +0 -1
- package/dist/types.d.ts +0 -374
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -13
- package/dist/types.js.map +0 -1
package/src/DagScheduler.ts
CHANGED
|
@@ -1,439 +1,439 @@
|
|
|
1
|
-
import { EventEmitter } from 'events';
|
|
2
|
-
import type { TaskNode } from './TaskGraph.js';
|
|
3
|
-
import type { ExecutionContext, RequestResult, Request, Folder, ScriptResult } from '@apiquest/types';
|
|
4
|
-
import { ScriptType } from '@apiquest/types';
|
|
5
|
-
import { isNullOrWhitespace } from './utils.js';
|
|
6
|
-
import { Logger } from './Logger.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Callback interface for DagScheduler to call back into CollectionRunner
|
|
10
|
-
*/
|
|
11
|
-
export interface DagExecutionCallbacks {
|
|
12
|
-
// Flags are passed for every node execution.
|
|
13
|
-
// When skip is true: no script execution, emit skipped assertions only
|
|
14
|
-
// When bail is true: suppress assertions and scripts
|
|
15
|
-
// Script execution (always through queue)
|
|
16
|
-
executeScript(
|
|
17
|
-
script: string,
|
|
18
|
-
scriptType: ScriptType,
|
|
19
|
-
context: ExecutionContext,
|
|
20
|
-
node: TaskNode,
|
|
21
|
-
flags: { skip: boolean; bail: boolean }
|
|
22
|
-
): Promise<ScriptResult>;
|
|
23
|
-
|
|
24
|
-
// Folder lifecycle (always through queue)
|
|
25
|
-
executeFolderEnter(
|
|
26
|
-
node: TaskNode,
|
|
27
|
-
context: ExecutionContext,
|
|
28
|
-
flags: { skip: boolean; bail: boolean }
|
|
29
|
-
): Promise<void>;
|
|
30
|
-
|
|
31
|
-
executeFolderExit(
|
|
32
|
-
node: TaskNode,
|
|
33
|
-
context: ExecutionContext,
|
|
34
|
-
flags: { skip: boolean; bail: boolean }
|
|
35
|
-
): Promise<void>;
|
|
36
|
-
|
|
37
|
-
// Request execution (I/O only, scripts handled separately)
|
|
38
|
-
executeRequestIO(
|
|
39
|
-
node: TaskNode,
|
|
40
|
-
context: ExecutionContext,
|
|
41
|
-
flags: { skip: boolean; bail: boolean }
|
|
42
|
-
): Promise<RequestResult>;
|
|
43
|
-
|
|
44
|
-
// Condition evaluation
|
|
45
|
-
evaluateCondition(
|
|
46
|
-
condition: string,
|
|
47
|
-
context: ExecutionContext
|
|
48
|
-
): Promise<boolean>;
|
|
49
|
-
|
|
50
|
-
// Abort check
|
|
51
|
-
isAborted(): boolean;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Async queue for workers
|
|
56
|
-
*/
|
|
57
|
-
class AsyncQueue<T> extends EventEmitter {
|
|
58
|
-
private items: T[] = [];
|
|
59
|
-
private waiting: Array<(value: T | null) => void> = [];
|
|
60
|
-
private closed = false;
|
|
61
|
-
|
|
62
|
-
enqueue(item: T): void {
|
|
63
|
-
if (this.closed) {
|
|
64
|
-
throw new Error('Queue is closed');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// If someone is waiting, give it to them immediately
|
|
68
|
-
const resolver = this.waiting.shift();
|
|
69
|
-
if (resolver !== undefined) {
|
|
70
|
-
resolver(item);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Otherwise, queue it
|
|
75
|
-
this.items.push(item);
|
|
76
|
-
this.emit('item');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async dequeue(): Promise<T | null> {
|
|
80
|
-
// If items available, return immediately
|
|
81
|
-
if (this.items.length > 0) {
|
|
82
|
-
return this.items.shift()!;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// If closed and empty, return null
|
|
86
|
-
if (this.closed) {
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Wait for item or close
|
|
91
|
-
return new Promise<T | null>((resolve) => {
|
|
92
|
-
this.waiting.push(resolve);
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
close(): void {
|
|
97
|
-
this.closed = true;
|
|
98
|
-
// Resolve all waiting promises with null
|
|
99
|
-
for (const resolver of this.waiting) {
|
|
100
|
-
resolver(null);
|
|
101
|
-
}
|
|
102
|
-
this.waiting = [];
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
get length(): number {
|
|
106
|
-
return this.items.length;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* DagScheduler coordinates parallel execution via DAG
|
|
112
|
-
*/
|
|
113
|
-
export class DagScheduler {
|
|
114
|
-
private callbacks: DagExecutionCallbacks;
|
|
115
|
-
private maxConcurrency: number;
|
|
116
|
-
private logger: Logger;
|
|
117
|
-
|
|
118
|
-
// Tracking
|
|
119
|
-
private completedNodes = new Set<string>();
|
|
120
|
-
private totalNodes = 0;
|
|
121
|
-
private aborted = false;
|
|
122
|
-
private skippedNodes = new Set<string>();
|
|
123
|
-
private graph?: import('./TaskGraph.js').TaskGraph;
|
|
124
|
-
|
|
125
|
-
// Queues
|
|
126
|
-
private scriptQueue = new AsyncQueue<TaskNode>();
|
|
127
|
-
private requestQueue = new AsyncQueue<TaskNode>();
|
|
128
|
-
|
|
129
|
-
constructor(callbacks: DagExecutionCallbacks, maxConcurrency: number, baseLogger?: Logger) {
|
|
130
|
-
this.callbacks = callbacks;
|
|
131
|
-
this.maxConcurrency = maxConcurrency;
|
|
132
|
-
this.logger = baseLogger?.createLogger('DagScheduler') ?? new Logger('DagScheduler');
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Execute the DAG
|
|
137
|
-
* Returns array of RequestResult for all requests that were executed
|
|
138
|
-
*/
|
|
139
|
-
public async execute(
|
|
140
|
-
graph: import('./TaskGraph.js').TaskGraph,
|
|
141
|
-
context: ExecutionContext
|
|
142
|
-
): Promise<RequestResult[]> {
|
|
143
|
-
const results: RequestResult[] = [];
|
|
144
|
-
|
|
145
|
-
// Initialize tracking
|
|
146
|
-
this.totalNodes = graph.getNodes().size;
|
|
147
|
-
this.completedNodes.clear();
|
|
148
|
-
this.skippedNodes.clear();
|
|
149
|
-
this.aborted = false;
|
|
150
|
-
this.graph = graph;
|
|
151
|
-
|
|
152
|
-
this.logger.debug(`Starting DAG execution: ${this.totalNodes} nodes, maxConcurrency=${this.maxConcurrency}`);
|
|
153
|
-
|
|
154
|
-
// Get initial ready nodes
|
|
155
|
-
const readyNodes = graph.getReadyNodes();
|
|
156
|
-
this.logger.debug(`Initial ready nodes: ${readyNodes.length}`);
|
|
157
|
-
|
|
158
|
-
if (readyNodes.length === 0) {
|
|
159
|
-
// Empty DAG
|
|
160
|
-
return results;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Enqueue initial ready nodes
|
|
164
|
-
for (const node of readyNodes) {
|
|
165
|
-
this.enqueueNode(node);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Start workers
|
|
169
|
-
const workers: Promise<void>[] = [];
|
|
170
|
-
|
|
171
|
-
// Script worker (single threaded)
|
|
172
|
-
workers.push(this.runScriptWorker(graph, context, results));
|
|
173
|
-
|
|
174
|
-
// Request workers (parallel pool)
|
|
175
|
-
for (let i = 0; i < this.maxConcurrency; i++) {
|
|
176
|
-
workers.push(this.runRequestWorker(graph, context, results));
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Wait for all workers to complete
|
|
180
|
-
await Promise.all(workers);
|
|
181
|
-
|
|
182
|
-
return results;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
private async runScriptWorker(
|
|
186
|
-
graph: import('./TaskGraph.js').TaskGraph,
|
|
187
|
-
context: ExecutionContext,
|
|
188
|
-
results: RequestResult[]
|
|
189
|
-
): Promise<void> {
|
|
190
|
-
while (true) {
|
|
191
|
-
const node = await this.scriptQueue.dequeue();
|
|
192
|
-
if (node === null) {
|
|
193
|
-
// Queue closed, we're done
|
|
194
|
-
break;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Check abort
|
|
198
|
-
if (this.aborted || this.callbacks.isAborted()) {
|
|
199
|
-
this.markComplete(node.id, graph);
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Check if node was skipped (e.g., by folder condition)
|
|
204
|
-
if (this.skippedNodes.has(node.id)) {
|
|
205
|
-
// Node already skipped by skipSubtree(), just mark complete
|
|
206
|
-
this.markComplete(node.id, graph);
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Execute script node
|
|
211
|
-
await this.executeScriptNode(node, context, results);
|
|
212
|
-
|
|
213
|
-
// Mark complete and enqueue newly-ready nodes
|
|
214
|
-
this.markComplete(node.id, graph);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
private async runRequestWorker(
|
|
219
|
-
graph: import('./TaskGraph.js').TaskGraph,
|
|
220
|
-
context: ExecutionContext,
|
|
221
|
-
results: RequestResult[]
|
|
222
|
-
): Promise<void> {
|
|
223
|
-
while (true) {
|
|
224
|
-
const node = await this.requestQueue.dequeue();
|
|
225
|
-
if (node === null) {
|
|
226
|
-
// Queue closed, we're done
|
|
227
|
-
break;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Check abort
|
|
231
|
-
if (this.aborted || this.callbacks.isAborted()) {
|
|
232
|
-
this.markComplete(node.id, graph);
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Check if node was skipped (e.g., by folder condition)
|
|
237
|
-
if (this.skippedNodes.has(node.id)) {
|
|
238
|
-
// Node already skipped by skipSubtree(), just mark complete
|
|
239
|
-
this.markComplete(node.id, graph);
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Execute request node
|
|
244
|
-
await this.executeRequestNode(node, context, results);
|
|
245
|
-
|
|
246
|
-
// Mark complete and enqueue newly-ready nodes
|
|
247
|
-
this.markComplete(node.id, graph);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
private async executeScriptNode(
|
|
252
|
-
node: TaskNode,
|
|
253
|
-
context: ExecutionContext,
|
|
254
|
-
results: RequestResult[]
|
|
255
|
-
): Promise<void> {
|
|
256
|
-
const flags = {
|
|
257
|
-
skip: false,
|
|
258
|
-
bail: this.aborted || this.callbacks.isAborted()
|
|
259
|
-
};
|
|
260
|
-
// Handle folder lifecycle nodes
|
|
261
|
-
if (node.type === 'folder-enter') {
|
|
262
|
-
if (node.condition !== undefined) {
|
|
263
|
-
this.logger.debug(`Evaluating folder condition for ${node.name}: ${node.condition}`);
|
|
264
|
-
const conditionPassed = await this.callbacks.evaluateCondition(node.condition, context);
|
|
265
|
-
this.logger.debug(`Folder condition result for ${node.name}: ${conditionPassed}`);
|
|
266
|
-
if (!conditionPassed) {
|
|
267
|
-
this.logger.debug(`Skipping folder subtree for ${node.name} (condition=false)`);
|
|
268
|
-
await this.skipSubtree(node.id, context, results, 'condition-false');
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Execute folder enter lifecycle (PUSH scope + emit beforeFolder)
|
|
274
|
-
await this.callbacks.executeFolderEnter(node, context, flags);
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (node.type === 'folder-exit') {
|
|
279
|
-
// Execute folder exit lifecycle (POP scope + emit afterFolder)
|
|
280
|
-
await this.callbacks.executeFolderExit(node, context, flags);
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Handle regular script nodes
|
|
285
|
-
// Evaluate condition if present
|
|
286
|
-
if (node.condition !== undefined) {
|
|
287
|
-
const conditionPassed = await this.callbacks.evaluateCondition(node.condition, context);
|
|
288
|
-
if (!conditionPassed) {
|
|
289
|
-
// Skip this node
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Execute script if present
|
|
295
|
-
if (!isNullOrWhitespace(node.script) && node.scriptType !== undefined) {
|
|
296
|
-
const scriptResult = await this.callbacks.executeScript(
|
|
297
|
-
node.script!,
|
|
298
|
-
node.scriptType,
|
|
299
|
-
context,
|
|
300
|
-
node,
|
|
301
|
-
flags
|
|
302
|
-
);
|
|
303
|
-
|
|
304
|
-
// Handle test failures (trigger bail if enabled)
|
|
305
|
-
// Note: callbacks.executeScript handles bail internally
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
private async executeRequestNode(
|
|
310
|
-
node: TaskNode,
|
|
311
|
-
context: ExecutionContext,
|
|
312
|
-
results: RequestResult[]
|
|
313
|
-
): Promise<void> {
|
|
314
|
-
const flags = {
|
|
315
|
-
skip: false,
|
|
316
|
-
bail: this.aborted || this.callbacks.isAborted()
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
if (flags.bail) {
|
|
320
|
-
this.logger.debug(`Bail active, skipping request ${node.id}`);
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
// Evaluate condition if present
|
|
324
|
-
if (node.condition !== undefined) {
|
|
325
|
-
const conditionPassed = await this.callbacks.evaluateCondition(node.condition, context);
|
|
326
|
-
if (!conditionPassed) {
|
|
327
|
-
flags.skip = true;
|
|
328
|
-
this.logger.debug(`Request ${node.id} skipped by condition`);
|
|
329
|
-
// Add skipped result
|
|
330
|
-
const request = node.item as Request;
|
|
331
|
-
const skippedResult: RequestResult = {
|
|
332
|
-
requestId: request.id,
|
|
333
|
-
requestName: request.name,
|
|
334
|
-
path: node.path,
|
|
335
|
-
success: true,
|
|
336
|
-
tests: [],
|
|
337
|
-
duration: 0,
|
|
338
|
-
iteration: context.iterationCurrent,
|
|
339
|
-
scriptError: 'Skipped by condition'
|
|
340
|
-
};
|
|
341
|
-
results.push(skippedResult);
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Execute request I/O
|
|
347
|
-
const result = await this.callbacks.executeRequestIO(node, context, flags);
|
|
348
|
-
results.push(result);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
private async skipSubtree(
|
|
352
|
-
rootNodeId: string,
|
|
353
|
-
context: ExecutionContext,
|
|
354
|
-
results: RequestResult[],
|
|
355
|
-
reason: string
|
|
356
|
-
): Promise<void> {
|
|
357
|
-
const graph = this.graph;
|
|
358
|
-
if (graph === undefined) {
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
const nodes = graph.getNodes();
|
|
363
|
-
const childrenByFolderId = graph.getChildrenByFolderId();
|
|
364
|
-
const stack: string[] = [rootNodeId];
|
|
365
|
-
|
|
366
|
-
this.logger.debug(`Skipping subtree from node ${rootNodeId} (${reason})`);
|
|
367
|
-
|
|
368
|
-
while (stack.length > 0) {
|
|
369
|
-
const currentId = stack.pop() as string;
|
|
370
|
-
if (this.skippedNodes.has(currentId)) {
|
|
371
|
-
continue;
|
|
372
|
-
}
|
|
373
|
-
this.skippedNodes.add(currentId);
|
|
374
|
-
|
|
375
|
-
const currentNode = nodes.get(currentId);
|
|
376
|
-
if (currentNode?.type === 'request') {
|
|
377
|
-
const request = currentNode.item as Request;
|
|
378
|
-
const result = await this.callbacks.executeRequestIO(currentNode, context, {
|
|
379
|
-
skip: true,
|
|
380
|
-
bail: false
|
|
381
|
-
});
|
|
382
|
-
results.push(result);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const childIds = currentNode?.path !== undefined
|
|
386
|
-
? (childrenByFolderId.get(currentNode.path) ?? [])
|
|
387
|
-
: [];
|
|
388
|
-
|
|
389
|
-
for (const childId of childIds) {
|
|
390
|
-
stack.push(childId);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
this.markComplete(currentId, graph);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
private markComplete(
|
|
398
|
-
nodeId: string,
|
|
399
|
-
graph: import('./TaskGraph.js').TaskGraph
|
|
400
|
-
): void {
|
|
401
|
-
this.completedNodes.add(nodeId);
|
|
402
|
-
|
|
403
|
-
// Get newly-ready nodes
|
|
404
|
-
const nowReady = graph.completeNode(nodeId);
|
|
405
|
-
|
|
406
|
-
// Only enqueue new nodes if not aborted (bail stops scheduling new work)
|
|
407
|
-
if (!this.aborted && !this.callbacks.isAborted()) {
|
|
408
|
-
for (const readyNode of nowReady) {
|
|
409
|
-
this.enqueueNode(readyNode);
|
|
410
|
-
}
|
|
411
|
-
} else {
|
|
412
|
-
// Mark skipped nodes as complete so we can finish
|
|
413
|
-
for (const readyNode of nowReady) {
|
|
414
|
-
this.completedNodes.add(readyNode.id);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Check if all nodes complete OR if aborted and queues empty
|
|
419
|
-
const allComplete = this.completedNodes.size === this.totalNodes;
|
|
420
|
-
const abortedAndIdle = (this.aborted || this.callbacks.isAborted()) &&
|
|
421
|
-
this.scriptQueue.length === 0 &&
|
|
422
|
-
this.requestQueue.length === 0;
|
|
423
|
-
|
|
424
|
-
if (allComplete || abortedAndIdle) {
|
|
425
|
-
// Close queues to signal workers to exit
|
|
426
|
-
this.scriptQueue.close();
|
|
427
|
-
this.requestQueue.close();
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
private enqueueNode(node: TaskNode): void {
|
|
432
|
-
// folder-enter and folder-exit are lifecycle nodes, must be serialized through script queue
|
|
433
|
-
if (node.type === 'folder-enter' || node.type === 'folder-exit' || node.type === 'script') {
|
|
434
|
-
this.scriptQueue.enqueue(node);
|
|
435
|
-
} else {
|
|
436
|
-
this.requestQueue.enqueue(node);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import type { TaskNode } from './TaskGraph.js';
|
|
3
|
+
import type { ExecutionContext, RequestResult, Request, Folder, ScriptResult } from '@apiquest/types';
|
|
4
|
+
import { ScriptType } from '@apiquest/types';
|
|
5
|
+
import { isNullOrWhitespace } from './utils.js';
|
|
6
|
+
import { Logger } from './Logger.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Callback interface for DagScheduler to call back into CollectionRunner
|
|
10
|
+
*/
|
|
11
|
+
export interface DagExecutionCallbacks {
|
|
12
|
+
// Flags are passed for every node execution.
|
|
13
|
+
// When skip is true: no script execution, emit skipped assertions only
|
|
14
|
+
// When bail is true: suppress assertions and scripts
|
|
15
|
+
// Script execution (always through queue)
|
|
16
|
+
executeScript(
|
|
17
|
+
script: string,
|
|
18
|
+
scriptType: ScriptType,
|
|
19
|
+
context: ExecutionContext,
|
|
20
|
+
node: TaskNode,
|
|
21
|
+
flags: { skip: boolean; bail: boolean }
|
|
22
|
+
): Promise<ScriptResult>;
|
|
23
|
+
|
|
24
|
+
// Folder lifecycle (always through queue)
|
|
25
|
+
executeFolderEnter(
|
|
26
|
+
node: TaskNode,
|
|
27
|
+
context: ExecutionContext,
|
|
28
|
+
flags: { skip: boolean; bail: boolean }
|
|
29
|
+
): Promise<void>;
|
|
30
|
+
|
|
31
|
+
executeFolderExit(
|
|
32
|
+
node: TaskNode,
|
|
33
|
+
context: ExecutionContext,
|
|
34
|
+
flags: { skip: boolean; bail: boolean }
|
|
35
|
+
): Promise<void>;
|
|
36
|
+
|
|
37
|
+
// Request execution (I/O only, scripts handled separately)
|
|
38
|
+
executeRequestIO(
|
|
39
|
+
node: TaskNode,
|
|
40
|
+
context: ExecutionContext,
|
|
41
|
+
flags: { skip: boolean; bail: boolean }
|
|
42
|
+
): Promise<RequestResult>;
|
|
43
|
+
|
|
44
|
+
// Condition evaluation
|
|
45
|
+
evaluateCondition(
|
|
46
|
+
condition: string,
|
|
47
|
+
context: ExecutionContext
|
|
48
|
+
): Promise<boolean>;
|
|
49
|
+
|
|
50
|
+
// Abort check
|
|
51
|
+
isAborted(): boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Async queue for workers
|
|
56
|
+
*/
|
|
57
|
+
class AsyncQueue<T> extends EventEmitter {
|
|
58
|
+
private items: T[] = [];
|
|
59
|
+
private waiting: Array<(value: T | null) => void> = [];
|
|
60
|
+
private closed = false;
|
|
61
|
+
|
|
62
|
+
enqueue(item: T): void {
|
|
63
|
+
if (this.closed) {
|
|
64
|
+
throw new Error('Queue is closed');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// If someone is waiting, give it to them immediately
|
|
68
|
+
const resolver = this.waiting.shift();
|
|
69
|
+
if (resolver !== undefined) {
|
|
70
|
+
resolver(item);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Otherwise, queue it
|
|
75
|
+
this.items.push(item);
|
|
76
|
+
this.emit('item');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async dequeue(): Promise<T | null> {
|
|
80
|
+
// If items available, return immediately
|
|
81
|
+
if (this.items.length > 0) {
|
|
82
|
+
return this.items.shift()!;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// If closed and empty, return null
|
|
86
|
+
if (this.closed) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Wait for item or close
|
|
91
|
+
return new Promise<T | null>((resolve) => {
|
|
92
|
+
this.waiting.push(resolve);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
close(): void {
|
|
97
|
+
this.closed = true;
|
|
98
|
+
// Resolve all waiting promises with null
|
|
99
|
+
for (const resolver of this.waiting) {
|
|
100
|
+
resolver(null);
|
|
101
|
+
}
|
|
102
|
+
this.waiting = [];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
get length(): number {
|
|
106
|
+
return this.items.length;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* DagScheduler coordinates parallel execution via DAG
|
|
112
|
+
*/
|
|
113
|
+
export class DagScheduler {
|
|
114
|
+
private callbacks: DagExecutionCallbacks;
|
|
115
|
+
private maxConcurrency: number;
|
|
116
|
+
private logger: Logger;
|
|
117
|
+
|
|
118
|
+
// Tracking
|
|
119
|
+
private completedNodes = new Set<string>();
|
|
120
|
+
private totalNodes = 0;
|
|
121
|
+
private aborted = false;
|
|
122
|
+
private skippedNodes = new Set<string>();
|
|
123
|
+
private graph?: import('./TaskGraph.js').TaskGraph;
|
|
124
|
+
|
|
125
|
+
// Queues
|
|
126
|
+
private scriptQueue = new AsyncQueue<TaskNode>();
|
|
127
|
+
private requestQueue = new AsyncQueue<TaskNode>();
|
|
128
|
+
|
|
129
|
+
constructor(callbacks: DagExecutionCallbacks, maxConcurrency: number, baseLogger?: Logger) {
|
|
130
|
+
this.callbacks = callbacks;
|
|
131
|
+
this.maxConcurrency = maxConcurrency;
|
|
132
|
+
this.logger = baseLogger?.createLogger('DagScheduler') ?? new Logger('DagScheduler');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Execute the DAG
|
|
137
|
+
* Returns array of RequestResult for all requests that were executed
|
|
138
|
+
*/
|
|
139
|
+
public async execute(
|
|
140
|
+
graph: import('./TaskGraph.js').TaskGraph,
|
|
141
|
+
context: ExecutionContext
|
|
142
|
+
): Promise<RequestResult[]> {
|
|
143
|
+
const results: RequestResult[] = [];
|
|
144
|
+
|
|
145
|
+
// Initialize tracking
|
|
146
|
+
this.totalNodes = graph.getNodes().size;
|
|
147
|
+
this.completedNodes.clear();
|
|
148
|
+
this.skippedNodes.clear();
|
|
149
|
+
this.aborted = false;
|
|
150
|
+
this.graph = graph;
|
|
151
|
+
|
|
152
|
+
this.logger.debug(`Starting DAG execution: ${this.totalNodes} nodes, maxConcurrency=${this.maxConcurrency}`);
|
|
153
|
+
|
|
154
|
+
// Get initial ready nodes
|
|
155
|
+
const readyNodes = graph.getReadyNodes();
|
|
156
|
+
this.logger.debug(`Initial ready nodes: ${readyNodes.length}`);
|
|
157
|
+
|
|
158
|
+
if (readyNodes.length === 0) {
|
|
159
|
+
// Empty DAG
|
|
160
|
+
return results;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Enqueue initial ready nodes
|
|
164
|
+
for (const node of readyNodes) {
|
|
165
|
+
this.enqueueNode(node);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Start workers
|
|
169
|
+
const workers: Promise<void>[] = [];
|
|
170
|
+
|
|
171
|
+
// Script worker (single threaded)
|
|
172
|
+
workers.push(this.runScriptWorker(graph, context, results));
|
|
173
|
+
|
|
174
|
+
// Request workers (parallel pool)
|
|
175
|
+
for (let i = 0; i < this.maxConcurrency; i++) {
|
|
176
|
+
workers.push(this.runRequestWorker(graph, context, results));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Wait for all workers to complete
|
|
180
|
+
await Promise.all(workers);
|
|
181
|
+
|
|
182
|
+
return results;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private async runScriptWorker(
|
|
186
|
+
graph: import('./TaskGraph.js').TaskGraph,
|
|
187
|
+
context: ExecutionContext,
|
|
188
|
+
results: RequestResult[]
|
|
189
|
+
): Promise<void> {
|
|
190
|
+
while (true) {
|
|
191
|
+
const node = await this.scriptQueue.dequeue();
|
|
192
|
+
if (node === null) {
|
|
193
|
+
// Queue closed, we're done
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check abort
|
|
198
|
+
if (this.aborted || this.callbacks.isAborted()) {
|
|
199
|
+
this.markComplete(node.id, graph);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Check if node was skipped (e.g., by folder condition)
|
|
204
|
+
if (this.skippedNodes.has(node.id)) {
|
|
205
|
+
// Node already skipped by skipSubtree(), just mark complete
|
|
206
|
+
this.markComplete(node.id, graph);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Execute script node
|
|
211
|
+
await this.executeScriptNode(node, context, results);
|
|
212
|
+
|
|
213
|
+
// Mark complete and enqueue newly-ready nodes
|
|
214
|
+
this.markComplete(node.id, graph);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private async runRequestWorker(
|
|
219
|
+
graph: import('./TaskGraph.js').TaskGraph,
|
|
220
|
+
context: ExecutionContext,
|
|
221
|
+
results: RequestResult[]
|
|
222
|
+
): Promise<void> {
|
|
223
|
+
while (true) {
|
|
224
|
+
const node = await this.requestQueue.dequeue();
|
|
225
|
+
if (node === null) {
|
|
226
|
+
// Queue closed, we're done
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Check abort
|
|
231
|
+
if (this.aborted || this.callbacks.isAborted()) {
|
|
232
|
+
this.markComplete(node.id, graph);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check if node was skipped (e.g., by folder condition)
|
|
237
|
+
if (this.skippedNodes.has(node.id)) {
|
|
238
|
+
// Node already skipped by skipSubtree(), just mark complete
|
|
239
|
+
this.markComplete(node.id, graph);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Execute request node
|
|
244
|
+
await this.executeRequestNode(node, context, results);
|
|
245
|
+
|
|
246
|
+
// Mark complete and enqueue newly-ready nodes
|
|
247
|
+
this.markComplete(node.id, graph);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private async executeScriptNode(
|
|
252
|
+
node: TaskNode,
|
|
253
|
+
context: ExecutionContext,
|
|
254
|
+
results: RequestResult[]
|
|
255
|
+
): Promise<void> {
|
|
256
|
+
const flags = {
|
|
257
|
+
skip: false,
|
|
258
|
+
bail: this.aborted || this.callbacks.isAborted()
|
|
259
|
+
};
|
|
260
|
+
// Handle folder lifecycle nodes
|
|
261
|
+
if (node.type === 'folder-enter') {
|
|
262
|
+
if (node.condition !== undefined) {
|
|
263
|
+
this.logger.debug(`Evaluating folder condition for ${node.name}: ${node.condition}`);
|
|
264
|
+
const conditionPassed = await this.callbacks.evaluateCondition(node.condition, context);
|
|
265
|
+
this.logger.debug(`Folder condition result for ${node.name}: ${conditionPassed}`);
|
|
266
|
+
if (!conditionPassed) {
|
|
267
|
+
this.logger.debug(`Skipping folder subtree for ${node.name} (condition=false)`);
|
|
268
|
+
await this.skipSubtree(node.id, context, results, 'condition-false');
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Execute folder enter lifecycle (PUSH scope + emit beforeFolder)
|
|
274
|
+
await this.callbacks.executeFolderEnter(node, context, flags);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (node.type === 'folder-exit') {
|
|
279
|
+
// Execute folder exit lifecycle (POP scope + emit afterFolder)
|
|
280
|
+
await this.callbacks.executeFolderExit(node, context, flags);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Handle regular script nodes
|
|
285
|
+
// Evaluate condition if present
|
|
286
|
+
if (node.condition !== undefined) {
|
|
287
|
+
const conditionPassed = await this.callbacks.evaluateCondition(node.condition, context);
|
|
288
|
+
if (!conditionPassed) {
|
|
289
|
+
// Skip this node
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Execute script if present
|
|
295
|
+
if (!isNullOrWhitespace(node.script) && node.scriptType !== undefined) {
|
|
296
|
+
const scriptResult = await this.callbacks.executeScript(
|
|
297
|
+
node.script!,
|
|
298
|
+
node.scriptType,
|
|
299
|
+
context,
|
|
300
|
+
node,
|
|
301
|
+
flags
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
// Handle test failures (trigger bail if enabled)
|
|
305
|
+
// Note: callbacks.executeScript handles bail internally
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private async executeRequestNode(
|
|
310
|
+
node: TaskNode,
|
|
311
|
+
context: ExecutionContext,
|
|
312
|
+
results: RequestResult[]
|
|
313
|
+
): Promise<void> {
|
|
314
|
+
const flags = {
|
|
315
|
+
skip: false,
|
|
316
|
+
bail: this.aborted || this.callbacks.isAborted()
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
if (flags.bail) {
|
|
320
|
+
this.logger.debug(`Bail active, skipping request ${node.id}`);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
// Evaluate condition if present
|
|
324
|
+
if (node.condition !== undefined) {
|
|
325
|
+
const conditionPassed = await this.callbacks.evaluateCondition(node.condition, context);
|
|
326
|
+
if (!conditionPassed) {
|
|
327
|
+
flags.skip = true;
|
|
328
|
+
this.logger.debug(`Request ${node.id} skipped by condition`);
|
|
329
|
+
// Add skipped result
|
|
330
|
+
const request = node.item as Request;
|
|
331
|
+
const skippedResult: RequestResult = {
|
|
332
|
+
requestId: request.id,
|
|
333
|
+
requestName: request.name,
|
|
334
|
+
path: node.path,
|
|
335
|
+
success: true,
|
|
336
|
+
tests: [],
|
|
337
|
+
duration: 0,
|
|
338
|
+
iteration: context.iterationCurrent,
|
|
339
|
+
scriptError: 'Skipped by condition'
|
|
340
|
+
};
|
|
341
|
+
results.push(skippedResult);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Execute request I/O
|
|
347
|
+
const result = await this.callbacks.executeRequestIO(node, context, flags);
|
|
348
|
+
results.push(result);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private async skipSubtree(
|
|
352
|
+
rootNodeId: string,
|
|
353
|
+
context: ExecutionContext,
|
|
354
|
+
results: RequestResult[],
|
|
355
|
+
reason: string
|
|
356
|
+
): Promise<void> {
|
|
357
|
+
const graph = this.graph;
|
|
358
|
+
if (graph === undefined) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const nodes = graph.getNodes();
|
|
363
|
+
const childrenByFolderId = graph.getChildrenByFolderId();
|
|
364
|
+
const stack: string[] = [rootNodeId];
|
|
365
|
+
|
|
366
|
+
this.logger.debug(`Skipping subtree from node ${rootNodeId} (${reason})`);
|
|
367
|
+
|
|
368
|
+
while (stack.length > 0) {
|
|
369
|
+
const currentId = stack.pop() as string;
|
|
370
|
+
if (this.skippedNodes.has(currentId)) {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
this.skippedNodes.add(currentId);
|
|
374
|
+
|
|
375
|
+
const currentNode = nodes.get(currentId);
|
|
376
|
+
if (currentNode?.type === 'request') {
|
|
377
|
+
const request = currentNode.item as Request;
|
|
378
|
+
const result = await this.callbacks.executeRequestIO(currentNode, context, {
|
|
379
|
+
skip: true,
|
|
380
|
+
bail: false
|
|
381
|
+
});
|
|
382
|
+
results.push(result);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const childIds = currentNode?.path !== undefined
|
|
386
|
+
? (childrenByFolderId.get(currentNode.path) ?? [])
|
|
387
|
+
: [];
|
|
388
|
+
|
|
389
|
+
for (const childId of childIds) {
|
|
390
|
+
stack.push(childId);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
this.markComplete(currentId, graph);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private markComplete(
|
|
398
|
+
nodeId: string,
|
|
399
|
+
graph: import('./TaskGraph.js').TaskGraph
|
|
400
|
+
): void {
|
|
401
|
+
this.completedNodes.add(nodeId);
|
|
402
|
+
|
|
403
|
+
// Get newly-ready nodes
|
|
404
|
+
const nowReady = graph.completeNode(nodeId);
|
|
405
|
+
|
|
406
|
+
// Only enqueue new nodes if not aborted (bail stops scheduling new work)
|
|
407
|
+
if (!this.aborted && !this.callbacks.isAborted()) {
|
|
408
|
+
for (const readyNode of nowReady) {
|
|
409
|
+
this.enqueueNode(readyNode);
|
|
410
|
+
}
|
|
411
|
+
} else {
|
|
412
|
+
// Mark skipped nodes as complete so we can finish
|
|
413
|
+
for (const readyNode of nowReady) {
|
|
414
|
+
this.completedNodes.add(readyNode.id);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Check if all nodes complete OR if aborted and queues empty
|
|
419
|
+
const allComplete = this.completedNodes.size === this.totalNodes;
|
|
420
|
+
const abortedAndIdle = (this.aborted || this.callbacks.isAborted()) &&
|
|
421
|
+
this.scriptQueue.length === 0 &&
|
|
422
|
+
this.requestQueue.length === 0;
|
|
423
|
+
|
|
424
|
+
if (allComplete || abortedAndIdle) {
|
|
425
|
+
// Close queues to signal workers to exit
|
|
426
|
+
this.scriptQueue.close();
|
|
427
|
+
this.requestQueue.close();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
private enqueueNode(node: TaskNode): void {
|
|
432
|
+
// folder-enter and folder-exit are lifecycle nodes, must be serialized through script queue
|
|
433
|
+
if (node.type === 'folder-enter' || node.type === 'folder-exit' || node.type === 'script') {
|
|
434
|
+
this.scriptQueue.enqueue(node);
|
|
435
|
+
} else {
|
|
436
|
+
this.requestQueue.enqueue(node);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|