@apiquest/fracture 1.0.4 → 1.0.5
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 +90 -2
- package/dist/CollectionRunner.d.ts +2 -0
- package/dist/CollectionRunner.d.ts.map +1 -1
- package/dist/CollectionRunner.js +20 -2
- package/dist/CollectionRunner.js.map +1 -1
- package/dist/LibraryLoader.d.ts +49 -0
- package/dist/LibraryLoader.d.ts.map +1 -0
- package/dist/LibraryLoader.js +198 -0
- package/dist/LibraryLoader.js.map +1 -0
- package/dist/PluginLoader.d.ts.map +1 -1
- package/dist/PluginLoader.js +9 -6
- package/dist/PluginLoader.js.map +1 -1
- package/dist/PluginResolver.d.ts +1 -1
- package/dist/PluginResolver.d.ts.map +1 -1
- package/dist/PluginResolver.js +1 -1
- package/dist/PluginResolver.js.map +1 -1
- package/dist/ScriptEngine.d.ts +2 -1
- package/dist/ScriptEngine.d.ts.map +1 -1
- package/dist/ScriptEngine.js +15 -8
- package/dist/ScriptEngine.js.map +1 -1
- package/dist/cli/index.js +35 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/plugin-commands.d.ts.map +1 -1
- package/dist/cli/plugin-commands.js +47 -81
- package/dist/cli/plugin-commands.js.map +1 -1
- package/dist/cli/plugin-installer.d.ts +48 -0
- package/dist/cli/plugin-installer.d.ts.map +1 -0
- package/dist/cli/plugin-installer.js +136 -0
- package/dist/cli/plugin-installer.js.map +1 -0
- package/dist/cli/plugin-registry.d.ts +17 -0
- package/dist/cli/plugin-registry.d.ts.map +1 -0
- package/dist/cli/plugin-registry.js +77 -0
- package/dist/cli/plugin-registry.js.map +1 -0
- package/package.json +1 -1
- package/src/CollectionAnalyzer.ts +0 -102
- package/src/CollectionRunner.ts +0 -1423
- package/src/CollectionRunner.types.ts +0 -9
- package/src/CollectionValidator.ts +0 -289
- package/src/ConsoleReporter.ts +0 -143
- package/src/CookieJar.ts +0 -258
- package/src/DagScheduler.ts +0 -439
- package/src/Logger.ts +0 -85
- package/src/PluginLoader.ts +0 -126
- package/src/PluginManager.ts +0 -208
- package/src/PluginResolver.ts +0 -154
- package/src/QuestAPI.ts +0 -764
- package/src/QuestAPI.types.ts +0 -33
- package/src/QuestTestAPI.ts +0 -164
- package/src/RequestFilter.ts +0 -224
- package/src/ScriptEngine.ts +0 -219
- package/src/ScriptValidator.ts +0 -428
- package/src/TaskGraph.ts +0 -598
- package/src/TestCounter.ts +0 -109
- package/src/VariableResolver.ts +0 -114
- package/src/cli/index.ts +0 -480
- package/src/cli/plugin-commands.ts +0 -342
- package/src/cli/plugin-discovery.ts +0 -44
- package/src/index.ts +0 -24
- package/src/utils.ts +0 -52
package/src/DagScheduler.ts
DELETED
|
@@ -1,439 +0,0 @@
|
|
|
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
|
-
}
|
package/src/Logger.ts
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from 'events';
|
|
2
|
-
import { randomUUID } from 'crypto';
|
|
3
|
-
import { ILogger, LogLevel } from '@apiquest/types';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Logger for fracture runtime and host integrations.
|
|
7
|
-
*/
|
|
8
|
-
export class Logger implements ILogger {
|
|
9
|
-
private level: LogLevel;
|
|
10
|
-
private component: string;
|
|
11
|
-
private emitter?: EventEmitter;
|
|
12
|
-
|
|
13
|
-
constructor(component: string, level: LogLevel = LogLevel.INFO, emitter?: EventEmitter) {
|
|
14
|
-
this.component = component;
|
|
15
|
-
this.level = level;
|
|
16
|
-
this.emitter = emitter;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
createLogger(component: string): Logger {
|
|
20
|
-
return new Logger(component, this.level, this.emitter);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
setLevel(level: LogLevel): void {
|
|
24
|
-
this.level = level;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
error(message: string, ...args: unknown[]): void {
|
|
28
|
-
this.log(LogLevel.ERROR, message, ...args);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
warn(message: string, ...args: unknown[]): void {
|
|
32
|
-
this.log(LogLevel.WARN, message, ...args);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
info(message: string, ...args: unknown[]): void {
|
|
36
|
-
this.log(LogLevel.INFO, message, ...args);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
debug(message: string, ...args: unknown[]): void {
|
|
40
|
-
this.log(LogLevel.DEBUG, message, ...args);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
trace(message: string, ...args: unknown[]): void {
|
|
44
|
-
this.log(LogLevel.TRACE, message, ...args);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
private log(level: LogLevel, message: string, ...args: unknown[]): void {
|
|
48
|
-
if (level > this.level) return;
|
|
49
|
-
|
|
50
|
-
const levelNames = ['error', 'warn', 'info', 'debug', 'trace'];
|
|
51
|
-
const levelName = levelNames[level];
|
|
52
|
-
const prefix = `[${this.component}]`;
|
|
53
|
-
const fullMessage = `${prefix} ${message}`;
|
|
54
|
-
|
|
55
|
-
const formattedArgs = args.length > 0
|
|
56
|
-
? ' ' + args.map(a => {
|
|
57
|
-
if (a instanceof Error) {
|
|
58
|
-
return a.message;
|
|
59
|
-
}
|
|
60
|
-
if (typeof a === 'object' && a !== null) {
|
|
61
|
-
try {
|
|
62
|
-
return JSON.stringify(a);
|
|
63
|
-
} catch {
|
|
64
|
-
return String(a);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return String(a);
|
|
68
|
-
}).join(' ')
|
|
69
|
-
: '';
|
|
70
|
-
|
|
71
|
-
const finalMessage = fullMessage + formattedArgs;
|
|
72
|
-
|
|
73
|
-
if (this.emitter !== null && this.emitter !== undefined) {
|
|
74
|
-
this.emitter.emit('console', {
|
|
75
|
-
id: randomUUID(),
|
|
76
|
-
level,
|
|
77
|
-
levelName,
|
|
78
|
-
component: this.component,
|
|
79
|
-
message: finalMessage,
|
|
80
|
-
args,
|
|
81
|
-
timestamp: new Date().toISOString()
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
package/src/PluginLoader.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import type { IProtocolPlugin, IAuthPlugin, IValueProviderPlugin } from '@apiquest/types';
|
|
2
|
-
import { Logger } from './Logger.js';
|
|
3
|
-
import { PluginManager } from './PluginManager.js';
|
|
4
|
-
import type { ResolvedPlugin } from './PluginResolver.js';
|
|
5
|
-
import type { PluginRequirements } from './CollectionAnalyzer.js';
|
|
6
|
-
import { isNullOrEmpty } from './utils.js';
|
|
7
|
-
|
|
8
|
-
export class PluginLoader {
|
|
9
|
-
private logger: Logger;
|
|
10
|
-
private pluginManager: PluginManager;
|
|
11
|
-
private loadedPlugins: Set<string> = new Set();
|
|
12
|
-
|
|
13
|
-
constructor(pluginManager: PluginManager, baseLogger?: Logger) {
|
|
14
|
-
this.pluginManager = pluginManager;
|
|
15
|
-
this.logger = baseLogger?.createLogger('PluginLoader') ?? new Logger('PluginLoader');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Load only plugins needed by the collection
|
|
20
|
-
*/
|
|
21
|
-
async loadRequiredPlugins(
|
|
22
|
-
resolved: ResolvedPlugin[],
|
|
23
|
-
requirements: PluginRequirements
|
|
24
|
-
): Promise<void> {
|
|
25
|
-
const needed = this.filterNeededPlugins(resolved, requirements);
|
|
26
|
-
|
|
27
|
-
this.logger.info(`Loading ${needed.length} required plugins (${resolved.length} available)`);
|
|
28
|
-
|
|
29
|
-
const loadPromises = needed.map(plugin =>
|
|
30
|
-
this.loadPlugin(plugin).catch(err => {
|
|
31
|
-
this.logger.error(`Failed to load ${plugin.name}:`, err);
|
|
32
|
-
throw err;
|
|
33
|
-
})
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
await Promise.all(loadPromises);
|
|
37
|
-
this.logger.debug('Required plugins loaded');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Filter resolved plugins to only those needed by collection
|
|
42
|
-
*/
|
|
43
|
-
private filterNeededPlugins(
|
|
44
|
-
resolved: ResolvedPlugin[],
|
|
45
|
-
requirements: PluginRequirements
|
|
46
|
-
): ResolvedPlugin[] {
|
|
47
|
-
const needed: ResolvedPlugin[] = [];
|
|
48
|
-
|
|
49
|
-
for (const plugin of resolved) {
|
|
50
|
-
let isNeeded = false;
|
|
51
|
-
|
|
52
|
-
if (plugin.type === 'protocol') {
|
|
53
|
-
// Check if collection uses this protocol
|
|
54
|
-
if (plugin.protocols?.some(p => requirements.protocols.has(p)) === true) {
|
|
55
|
-
isNeeded = true;
|
|
56
|
-
}
|
|
57
|
-
} else if (plugin.type === 'auth') {
|
|
58
|
-
// Check if collection uses any of these auth types
|
|
59
|
-
if (plugin.authTypes?.some(a => requirements.authTypes.has(a)) === true) {
|
|
60
|
-
isNeeded = true;
|
|
61
|
-
}
|
|
62
|
-
} else if (plugin.type === 'value') {
|
|
63
|
-
// Check if collection uses this value provider
|
|
64
|
-
const provider = plugin.provider;
|
|
65
|
-
if (provider !== null && provider !== undefined && provider !== '' && requirements.valueProviders.has(provider)) {
|
|
66
|
-
isNeeded = true;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (isNeeded) {
|
|
71
|
-
this.logger.debug(`Plugin needed: ${plugin.name} v${plugin.version} (${plugin.type})`);
|
|
72
|
-
needed.push(plugin);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return needed;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Dynamically import and register a single plugin
|
|
81
|
-
*/
|
|
82
|
-
private async loadPlugin(plugin: ResolvedPlugin): Promise<void> {
|
|
83
|
-
const { pathToFileURL } = await import('url');
|
|
84
|
-
|
|
85
|
-
// Skip if already loaded
|
|
86
|
-
if (this.loadedPlugins.has(plugin.name)) {
|
|
87
|
-
this.logger.debug(`Already loaded: ${plugin.name}`);
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
this.logger.debug(`Loading ${plugin.name} v${plugin.version} from ${plugin.path}`);
|
|
92
|
-
|
|
93
|
-
// Mark as loaded
|
|
94
|
-
this.loadedPlugins.add(plugin.name);
|
|
95
|
-
|
|
96
|
-
// Convert to file:// URL for Windows compatibility
|
|
97
|
-
const moduleUrl = pathToFileURL(plugin.entryPoint).href;
|
|
98
|
-
const pluginModule = await import(moduleUrl) as Record<string, unknown>;
|
|
99
|
-
|
|
100
|
-
// Handle different export patterns
|
|
101
|
-
const defaultExport = pluginModule.default;
|
|
102
|
-
const namedExport = pluginModule[Object.keys(pluginModule)[0]];
|
|
103
|
-
const exported = defaultExport ?? namedExport;
|
|
104
|
-
|
|
105
|
-
if (exported === null || exported === undefined) {
|
|
106
|
-
throw new Error(`Plugin ${plugin.name} has no exports`);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Register based on plugin type
|
|
110
|
-
if (plugin.type === 'protocol') {
|
|
111
|
-
this.pluginManager.registerPlugin(exported as IProtocolPlugin);
|
|
112
|
-
this.logger.debug(`Registered protocol plugin: ${plugin.protocols?.join(', ') ?? ''}`);
|
|
113
|
-
} else if (plugin.type === 'auth') {
|
|
114
|
-
// Auth plugins might export array or single
|
|
115
|
-
const authArray = Array.isArray(exported) ? (exported as IAuthPlugin[]) : [exported as IAuthPlugin];
|
|
116
|
-
|
|
117
|
-
for (const authPlugin of authArray) {
|
|
118
|
-
this.pluginManager.registerAuthPlugin(authPlugin);
|
|
119
|
-
this.logger.debug(`Registered auth plugin: ${authPlugin.authTypes.join(', ')}`);
|
|
120
|
-
}
|
|
121
|
-
} else if (plugin.type === 'value') {
|
|
122
|
-
this.pluginManager.registerVariableProvider(exported as IValueProviderPlugin);
|
|
123
|
-
this.logger.debug(`Registered value provider: ${plugin.provider ?? ''}`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|