recker 1.0.73 → 1.0.75-next.2e5a94f
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 +5 -18
- package/dist/browser/core/client.d.ts +14 -8
- package/dist/browser/core/client.js +199 -17
- package/dist/browser/core/errors.d.ts +15 -1
- package/dist/browser/core/errors.js +140 -9
- package/dist/browser/core/request.d.ts +5 -0
- package/dist/browser/core/request.js +33 -2
- package/dist/browser/core-runtime/plugin-manifest.d.ts +24 -0
- package/dist/browser/core-runtime/plugin-manifest.js +159 -0
- package/dist/browser/core-runtime/request-context.d.ts +13 -0
- package/dist/browser/core-runtime/request-context.js +24 -0
- package/dist/browser/core-runtime/typed-events.d.ts +89 -0
- package/dist/browser/core-runtime/typed-events.js +34 -0
- package/dist/browser/index.iife.min.js +79 -79
- package/dist/browser/index.min.js +79 -79
- package/dist/browser/index.mini.iife.js +913 -97
- package/dist/browser/index.mini.iife.min.js +46 -46
- package/dist/browser/index.mini.min.js +46 -46
- package/dist/browser/index.mini.umd.js +913 -97
- package/dist/browser/index.mini.umd.min.js +46 -46
- package/dist/browser/index.umd.min.js +79 -79
- package/dist/browser/plugins/auth/aws-sigv4.d.ts +1 -0
- package/dist/browser/plugins/auth/aws-sigv4.js +19 -2
- package/dist/browser/plugins/retry.js +29 -1
- package/dist/browser/presets/aws.d.ts +1 -0
- package/dist/browser/presets/aws.js +62 -1
- package/dist/browser/runner/request-runner.d.ts +15 -5
- package/dist/browser/runner/request-runner.js +164 -30
- package/dist/browser/scrape/parser/nodes/html.d.ts +6 -0
- package/dist/browser/scrape/parser/nodes/html.js +70 -18
- package/dist/browser/scrape/parser/nodes/node.d.ts +1 -0
- package/dist/browser/scrape/parser/nodes/node.js +5 -0
- package/dist/browser/scrape/spider.d.ts +1 -0
- package/dist/browser/scrape/spider.js +39 -26
- package/dist/browser/seo/analyzer.d.ts +1 -1
- package/dist/browser/seo/analyzer.js +73 -42
- package/dist/browser/seo/index.d.ts +1 -1
- package/dist/browser/seo/rules/types.d.ts +2 -0
- package/dist/browser/seo/seo-spider.d.ts +2 -3
- package/dist/browser/seo/seo-spider.js +26 -202
- package/dist/browser/seo/types.d.ts +4 -0
- package/dist/browser/seo/validators/sitemap.js +9 -2
- package/dist/browser/transport/fetch.js +38 -5
- package/dist/browser/transport/undici.js +73 -11
- package/dist/browser/transport/worker.d.ts +0 -1
- package/dist/browser/transport/worker.js +1 -3
- package/dist/browser/types/index.d.ts +24 -0
- package/dist/cli/commands/mcp.js +5 -3
- package/dist/core/client.d.ts +14 -8
- package/dist/core/client.js +199 -17
- package/dist/core/errors.d.ts +15 -1
- package/dist/core/errors.js +140 -9
- package/dist/core/request.d.ts +5 -0
- package/dist/core/request.js +33 -2
- package/dist/core-runtime/plugin-manifest.d.ts +24 -0
- package/dist/core-runtime/plugin-manifest.js +159 -0
- package/dist/core-runtime/request-context.d.ts +13 -0
- package/dist/core-runtime/request-context.js +24 -0
- package/dist/core-runtime/typed-events.d.ts +89 -0
- package/dist/core-runtime/typed-events.js +34 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/mcp/cli.js +10 -8
- package/dist/mcp/profiles.d.ts +1 -1
- package/dist/mcp/profiles.js +31 -6
- package/dist/mcp/tools/categories.js +0 -1
- package/dist/plugins/auth/aws-sigv4.d.ts +1 -0
- package/dist/plugins/auth/aws-sigv4.js +19 -2
- package/dist/plugins/retry.js +29 -1
- package/dist/presets/aws.d.ts +1 -0
- package/dist/presets/aws.js +62 -1
- package/dist/recker.d.ts +3 -0
- package/dist/recker.js +5 -0
- package/dist/runner/request-runner.d.ts +15 -5
- package/dist/runner/request-runner.js +164 -30
- package/dist/scrape/parser/nodes/html.d.ts +6 -0
- package/dist/scrape/parser/nodes/html.js +70 -18
- package/dist/scrape/parser/nodes/node.d.ts +1 -0
- package/dist/scrape/parser/nodes/node.js +5 -0
- package/dist/scrape/spider.d.ts +1 -0
- package/dist/scrape/spider.js +39 -26
- package/dist/search/google.d.ts +67 -0
- package/dist/search/google.js +480 -0
- package/dist/search/index.d.ts +3 -0
- package/dist/search/index.js +1 -0
- package/dist/seo/analyzer.d.ts +1 -1
- package/dist/seo/analyzer.js +73 -42
- package/dist/seo/index.d.ts +1 -1
- package/dist/seo/rules/types.d.ts +2 -0
- package/dist/seo/seo-spider.d.ts +2 -3
- package/dist/seo/seo-spider.js +26 -202
- package/dist/seo/types.d.ts +4 -0
- package/dist/seo/validators/sitemap.js +9 -2
- package/dist/transport/fetch.js +38 -5
- package/dist/transport/undici.js +73 -11
- package/dist/transport/worker.d.ts +0 -1
- package/dist/transport/worker.js +1 -3
- package/dist/types/index.d.ts +24 -0
- package/dist/version.js +1 -1
- package/package.json +9 -1
|
@@ -3,20 +3,30 @@ export class ReckerError extends Error {
|
|
|
3
3
|
response;
|
|
4
4
|
suggestions;
|
|
5
5
|
retriable;
|
|
6
|
-
|
|
6
|
+
classification;
|
|
7
|
+
constructor(message, request, response, suggestions = [], retriable = false, classification) {
|
|
7
8
|
super(message);
|
|
8
9
|
this.name = 'ReckerError';
|
|
9
10
|
this.request = request;
|
|
10
11
|
this.response = response;
|
|
11
12
|
this.suggestions = suggestions;
|
|
12
13
|
this.retriable = retriable;
|
|
14
|
+
this.classification = classification;
|
|
13
15
|
}
|
|
14
16
|
}
|
|
15
17
|
export class HttpError extends ReckerError {
|
|
16
18
|
status;
|
|
17
19
|
statusText;
|
|
18
20
|
constructor(response, request) {
|
|
19
|
-
|
|
21
|
+
const retriable = isRetryableStatus(response.status);
|
|
22
|
+
super(`Request failed with status code ${response.status} ${response.statusText}`, request, response, ['Check the upstream service response body for error details.', 'Inspect request headers/body to ensure they match the API contract.', 'Retry if this is a transient 5xx/429 error.'], retriable, {
|
|
23
|
+
category: 'http',
|
|
24
|
+
source: 'server',
|
|
25
|
+
severity: response.status >= 500 ? 'high' : 'medium',
|
|
26
|
+
canRetry: retriable,
|
|
27
|
+
reason: `HTTP ${response.status}`,
|
|
28
|
+
statusCode: response.status
|
|
29
|
+
});
|
|
20
30
|
this.name = 'HttpError';
|
|
21
31
|
this.status = response.status;
|
|
22
32
|
this.statusText = response.statusText;
|
|
@@ -53,7 +63,13 @@ export class TimeoutError extends ReckerError {
|
|
|
53
63
|
'Increase the specific timeout phase or optimize the upstream response time.',
|
|
54
64
|
'Reduce concurrent requests if the connection pool is exhausted.'
|
|
55
65
|
];
|
|
56
|
-
super(message, request, undefined, suggestions, true
|
|
66
|
+
super(message, request, undefined, suggestions, true, {
|
|
67
|
+
category: 'timeout',
|
|
68
|
+
source: 'transport',
|
|
69
|
+
severity: 'medium',
|
|
70
|
+
canRetry: true,
|
|
71
|
+
reason: `Timeout while ${phase}`
|
|
72
|
+
});
|
|
57
73
|
this.name = 'TimeoutError';
|
|
58
74
|
this.phase = phase;
|
|
59
75
|
this.timeout = timeout ?? 0;
|
|
@@ -69,7 +85,13 @@ export class NetworkError extends ReckerError {
|
|
|
69
85
|
'Check proxy/VPN/firewall settings that might block the request.',
|
|
70
86
|
'Retry the request or switch transport if this is transient.'
|
|
71
87
|
];
|
|
72
|
-
super(message, request, undefined, suggestions, true
|
|
88
|
+
super(message, request, undefined, suggestions, true, {
|
|
89
|
+
category: 'network',
|
|
90
|
+
source: 'transport',
|
|
91
|
+
severity: code ? 'medium' : 'low',
|
|
92
|
+
canRetry: true,
|
|
93
|
+
reason: code ? `Network error (${code})` : message
|
|
94
|
+
});
|
|
73
95
|
this.name = 'NetworkError';
|
|
74
96
|
this.code = code;
|
|
75
97
|
}
|
|
@@ -83,7 +105,14 @@ export class Http2Error extends ReckerError {
|
|
|
83
105
|
const retriable = isRetriableHttp2Error(errorCode);
|
|
84
106
|
const suggestFallback = shouldFallbackToHttp1(errorCode);
|
|
85
107
|
const suggestions = getHttp2ErrorSuggestions(errorCode, suggestFallback);
|
|
86
|
-
super(message, options.request, undefined, suggestions, retriable
|
|
108
|
+
super(message, options.request, undefined, suggestions, retriable, {
|
|
109
|
+
category: 'protocol',
|
|
110
|
+
source: 'transport',
|
|
111
|
+
severity: retriable ? 'medium' : 'high',
|
|
112
|
+
canRetry: retriable,
|
|
113
|
+
reason: `HTTP/2 ${errorCode}`,
|
|
114
|
+
statusCode: errorCode ? Number.parseInt(String(errorCode), 10) || undefined : undefined
|
|
115
|
+
});
|
|
87
116
|
this.name = 'Http2Error';
|
|
88
117
|
this.errorCode = errorCode;
|
|
89
118
|
this.level = options.level ?? 'stream';
|
|
@@ -171,6 +200,84 @@ export function parseHttp2Error(error) {
|
|
|
171
200
|
}
|
|
172
201
|
return null;
|
|
173
202
|
}
|
|
203
|
+
export function classifyTransportError(error) {
|
|
204
|
+
if (error instanceof ReckerError && error.classification) {
|
|
205
|
+
return error.classification;
|
|
206
|
+
}
|
|
207
|
+
if (!error || typeof error !== 'object') {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
const anyError = error;
|
|
211
|
+
const name = anyError.name || '';
|
|
212
|
+
const message = anyError.message || '';
|
|
213
|
+
const code = anyError.code?.toUpperCase();
|
|
214
|
+
if (name === 'AbortError' || code === 'ABORT_ERR' || code === 'UND_ERR_ABORTED' || message.includes('aborted') || message.includes('AbortError')) {
|
|
215
|
+
return {
|
|
216
|
+
category: 'queue',
|
|
217
|
+
source: 'client',
|
|
218
|
+
severity: 'low',
|
|
219
|
+
canRetry: true,
|
|
220
|
+
reason: 'Request was aborted'
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
if (name === 'TimeoutError' || code === 'UND_ERR_CONNECT_TIMEOUT' || code === 'UND_ERR_HEADERS_TIMEOUT' || code === 'UND_ERR_BODY_TIMEOUT' || message.includes('timeout')) {
|
|
224
|
+
return {
|
|
225
|
+
category: 'timeout',
|
|
226
|
+
source: 'transport',
|
|
227
|
+
severity: 'medium',
|
|
228
|
+
canRetry: true,
|
|
229
|
+
reason: message || 'Request timed out'
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
const networkCodes = new Set([
|
|
233
|
+
'ECONNRESET',
|
|
234
|
+
'ECONNREFUSED',
|
|
235
|
+
'ENOTFOUND',
|
|
236
|
+
'EPIPE',
|
|
237
|
+
'ETIMEDOUT',
|
|
238
|
+
'EHOSTUNREACH',
|
|
239
|
+
'ENETUNREACH',
|
|
240
|
+
'ENETDOWN',
|
|
241
|
+
'EAI_AGAIN'
|
|
242
|
+
]);
|
|
243
|
+
if (code && networkCodes.has(code)) {
|
|
244
|
+
return {
|
|
245
|
+
category: 'network',
|
|
246
|
+
source: 'transport',
|
|
247
|
+
severity: 'medium',
|
|
248
|
+
canRetry: true,
|
|
249
|
+
reason: message || `Network error (${code})`
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
if (name === 'Http2Error' || message.includes('HTTP/2') || message.includes('RST_STREAM') || message.includes('GOAWAY')) {
|
|
253
|
+
return {
|
|
254
|
+
category: 'protocol',
|
|
255
|
+
source: 'transport',
|
|
256
|
+
severity: 'medium',
|
|
257
|
+
canRetry: true,
|
|
258
|
+
reason: message || 'HTTP/2 protocol error'
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
if (name === 'MaxSizeExceededError' || name === 'ParseError') {
|
|
262
|
+
return {
|
|
263
|
+
category: 'resource',
|
|
264
|
+
source: 'server',
|
|
265
|
+
severity: 'low',
|
|
266
|
+
canRetry: false,
|
|
267
|
+
reason: message || 'Resource limitation'
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
if (name === 'ConfigurationError' || name === 'ValidationError' || name === 'StateError') {
|
|
271
|
+
return {
|
|
272
|
+
category: 'state',
|
|
273
|
+
source: 'client',
|
|
274
|
+
severity: 'high',
|
|
275
|
+
canRetry: false,
|
|
276
|
+
reason: message || 'Request cannot be retried'
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
174
281
|
export class MaxSizeExceededError extends ReckerError {
|
|
175
282
|
maxSize;
|
|
176
283
|
actualSize;
|
|
@@ -182,7 +289,13 @@ export class MaxSizeExceededError extends ReckerError {
|
|
|
182
289
|
'Increase maxResponseSize if the larger payload is expected.',
|
|
183
290
|
'Add pagination/streaming to reduce payload size.',
|
|
184
291
|
'Ensure the upstream is not returning unexpected large responses.'
|
|
185
|
-
], false
|
|
292
|
+
], false, {
|
|
293
|
+
category: 'resource',
|
|
294
|
+
source: 'server',
|
|
295
|
+
severity: 'medium',
|
|
296
|
+
canRetry: false,
|
|
297
|
+
reason: 'Response size exceeded configured max'
|
|
298
|
+
});
|
|
186
299
|
this.name = 'MaxSizeExceededError';
|
|
187
300
|
this.maxSize = maxSize;
|
|
188
301
|
this.actualSize = actualSize;
|
|
@@ -198,7 +311,13 @@ export class AbortError extends ReckerError {
|
|
|
198
311
|
'Check if the abort was intentional (user-triggered or timeout).',
|
|
199
312
|
'Increase timeout if the request needs more time to complete.',
|
|
200
313
|
'Ensure AbortController is not being triggered prematurely.'
|
|
201
|
-
], true
|
|
314
|
+
], true, {
|
|
315
|
+
category: 'timeout',
|
|
316
|
+
source: 'client',
|
|
317
|
+
severity: 'low',
|
|
318
|
+
canRetry: true,
|
|
319
|
+
reason: reason ? `Request was aborted: ${reason}` : 'Request was aborted'
|
|
320
|
+
});
|
|
202
321
|
this.name = 'AbortError';
|
|
203
322
|
this.reason = reason;
|
|
204
323
|
}
|
|
@@ -212,7 +331,13 @@ export class ConnectionError extends ReckerError {
|
|
|
212
331
|
'Verify the host and port are correct and the service is running.',
|
|
213
332
|
'Check network connectivity and firewall rules.',
|
|
214
333
|
'Ensure the service is accepting connections on the specified port.'
|
|
215
|
-
], options?.retriable ?? true
|
|
334
|
+
], options?.retriable ?? true, {
|
|
335
|
+
category: 'network',
|
|
336
|
+
source: 'transport',
|
|
337
|
+
severity: options?.retriable === false ? 'high' : 'medium',
|
|
338
|
+
canRetry: options?.retriable ?? true,
|
|
339
|
+
reason: options?.code ? `Connection error (${options.code})` : 'Connection error'
|
|
340
|
+
});
|
|
216
341
|
this.name = 'ConnectionError';
|
|
217
342
|
this.host = options?.host;
|
|
218
343
|
this.port = options?.port;
|
|
@@ -370,7 +495,13 @@ export class QueueCancelledError extends ReckerError {
|
|
|
370
495
|
'This is typically expected during shutdown.',
|
|
371
496
|
'Check if the queue was manually cleared.',
|
|
372
497
|
'Retry the operation if the queue is still active.'
|
|
373
|
-
], true
|
|
498
|
+
], true, {
|
|
499
|
+
category: 'queue',
|
|
500
|
+
source: 'client',
|
|
501
|
+
severity: 'low',
|
|
502
|
+
canRetry: true,
|
|
503
|
+
reason: message || 'Queue was cancelled'
|
|
504
|
+
});
|
|
374
505
|
this.name = 'QueueCancelledError';
|
|
375
506
|
this.queueName = options?.queueName;
|
|
376
507
|
}
|
|
@@ -15,6 +15,11 @@ export declare class HttpRequest implements ReckerRequest {
|
|
|
15
15
|
readonly followRedirects?: boolean;
|
|
16
16
|
readonly http2?: boolean;
|
|
17
17
|
readonly useCurl?: boolean;
|
|
18
|
+
readonly correlationId?: string;
|
|
19
|
+
readonly tenant?: string;
|
|
20
|
+
readonly policyTags: string[];
|
|
21
|
+
readonly policySource?: string;
|
|
22
|
+
readonly traceId?: string;
|
|
18
23
|
constructor(url: string, options?: RequestOptions);
|
|
19
24
|
withHeader(name: string, value: string): ReckerRequest;
|
|
20
25
|
withBody(body: BodyInit): ReckerRequest;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { attachRequestContext, getRequestContext } from '../core-runtime/request-context.js';
|
|
1
2
|
function normalizeTimeout(timeout) {
|
|
2
3
|
if (timeout === undefined)
|
|
3
4
|
return undefined;
|
|
@@ -22,6 +23,11 @@ export class HttpRequest {
|
|
|
22
23
|
followRedirects;
|
|
23
24
|
http2;
|
|
24
25
|
useCurl;
|
|
26
|
+
correlationId;
|
|
27
|
+
tenant;
|
|
28
|
+
policyTags;
|
|
29
|
+
policySource;
|
|
30
|
+
traceId;
|
|
25
31
|
constructor(url, options = {}) {
|
|
26
32
|
this.url = url;
|
|
27
33
|
this.method = options.method || 'GET';
|
|
@@ -40,11 +46,17 @@ export class HttpRequest {
|
|
|
40
46
|
this.followRedirects = options.followRedirects;
|
|
41
47
|
this.http2 = options.http2;
|
|
42
48
|
this.useCurl = options.useCurl;
|
|
49
|
+
this.correlationId = options.correlationId;
|
|
50
|
+
this.tenant = options.tenant;
|
|
51
|
+
this.policyTags = options.policyTags ?? [];
|
|
52
|
+
this.policySource = options.policySource;
|
|
53
|
+
this.traceId = options.traceId;
|
|
43
54
|
}
|
|
44
55
|
withHeader(name, value) {
|
|
56
|
+
const context = getRequestContext(this);
|
|
45
57
|
const newHeaders = new Headers(this.headers);
|
|
46
58
|
newHeaders.set(name, value);
|
|
47
|
-
|
|
59
|
+
const request = new HttpRequest(this.url, {
|
|
48
60
|
method: this.method,
|
|
49
61
|
headers: newHeaders,
|
|
50
62
|
body: this.body,
|
|
@@ -59,10 +71,20 @@ export class HttpRequest {
|
|
|
59
71
|
followRedirects: this.followRedirects,
|
|
60
72
|
http2: this.http2,
|
|
61
73
|
useCurl: this.useCurl,
|
|
74
|
+
correlationId: this.correlationId,
|
|
75
|
+
tenant: this.tenant,
|
|
76
|
+
policyTags: this.policyTags,
|
|
77
|
+
policySource: this.policySource,
|
|
78
|
+
traceId: this.traceId
|
|
62
79
|
});
|
|
80
|
+
if (context) {
|
|
81
|
+
return attachRequestContext(request, context);
|
|
82
|
+
}
|
|
83
|
+
return request;
|
|
63
84
|
}
|
|
64
85
|
withBody(body) {
|
|
65
|
-
|
|
86
|
+
const context = getRequestContext(this);
|
|
87
|
+
const request = new HttpRequest(this.url, {
|
|
66
88
|
method: this.method,
|
|
67
89
|
headers: this.headers,
|
|
68
90
|
body: body,
|
|
@@ -77,6 +99,15 @@ export class HttpRequest {
|
|
|
77
99
|
followRedirects: this.followRedirects,
|
|
78
100
|
http2: this.http2,
|
|
79
101
|
useCurl: this.useCurl,
|
|
102
|
+
correlationId: this.correlationId,
|
|
103
|
+
tenant: this.tenant,
|
|
104
|
+
policyTags: this.policyTags,
|
|
105
|
+
policySource: this.policySource,
|
|
106
|
+
traceId: this.traceId
|
|
80
107
|
});
|
|
108
|
+
if (context) {
|
|
109
|
+
return attachRequestContext(request, context);
|
|
110
|
+
}
|
|
111
|
+
return request;
|
|
81
112
|
}
|
|
82
113
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Plugin } from '../types/index.js';
|
|
2
|
+
export type PluginScope = 'request' | 'protocol' | 'transport' | 'runtime';
|
|
3
|
+
export interface PluginManifest {
|
|
4
|
+
name: string;
|
|
5
|
+
version: string;
|
|
6
|
+
scope: PluginScope;
|
|
7
|
+
priority: number;
|
|
8
|
+
dependsOn: string[];
|
|
9
|
+
setup?: string;
|
|
10
|
+
teardown?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface PluginRegistration {
|
|
13
|
+
plugin: Plugin;
|
|
14
|
+
manifest: PluginManifest;
|
|
15
|
+
registrationIndex: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function attachPluginManifest(plugin: Plugin, manifest: Partial<PluginManifest>): Plugin;
|
|
18
|
+
export declare function normalizePluginManifest(plugin: Plugin, options?: Partial<PluginManifest>): PluginManifest;
|
|
19
|
+
export declare function getPluginManifest(plugin: Plugin): PluginManifest | undefined;
|
|
20
|
+
export declare function toSortedPlugins(plugins: Plugin[]): PluginRegistration[];
|
|
21
|
+
export declare function normalizePlugins(registrations: PluginRegistration[]): {
|
|
22
|
+
ordered: PluginRegistration[];
|
|
23
|
+
debugOrder: string[];
|
|
24
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
const MANIFEST_KEY = '__reckerManifest';
|
|
2
|
+
const DEFAULT_SCOPE = 'request';
|
|
3
|
+
const DEFAULT_PRIORITY = 0;
|
|
4
|
+
export function attachPluginManifest(plugin, manifest) {
|
|
5
|
+
plugin.__reckerManifest = normalizeManifest(plugin, manifest);
|
|
6
|
+
return plugin;
|
|
7
|
+
}
|
|
8
|
+
export function normalizePluginManifest(plugin, options = {}) {
|
|
9
|
+
return normalizeManifest(plugin, options);
|
|
10
|
+
}
|
|
11
|
+
export function getPluginManifest(plugin) {
|
|
12
|
+
return plugin.__reckerManifest;
|
|
13
|
+
}
|
|
14
|
+
function normalizeManifest(plugin, manifest) {
|
|
15
|
+
const pluginId = manifest?.name || inferPluginName(plugin);
|
|
16
|
+
return {
|
|
17
|
+
name: pluginId,
|
|
18
|
+
version: manifest?.version || '1',
|
|
19
|
+
scope: manifest?.scope || DEFAULT_SCOPE,
|
|
20
|
+
priority: manifest?.priority ?? DEFAULT_PRIORITY,
|
|
21
|
+
dependsOn: manifest?.dependsOn || [],
|
|
22
|
+
setup: manifest?.setup,
|
|
23
|
+
teardown: manifest?.teardown,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function inferPluginName(plugin) {
|
|
27
|
+
return plugin.name && plugin.name !== 'anonymous'
|
|
28
|
+
? plugin.name
|
|
29
|
+
: `plugin-${Math.random().toString(16).slice(2, 10)}`;
|
|
30
|
+
}
|
|
31
|
+
function assertUniqueName(name, existing, source) {
|
|
32
|
+
if (existing.has(name)) {
|
|
33
|
+
throw new Error(`Invalid plugin manifest: duplicate plugin name '${name}'. ${source}`);
|
|
34
|
+
}
|
|
35
|
+
existing.add(name);
|
|
36
|
+
}
|
|
37
|
+
function buildDependencyError(path, target) {
|
|
38
|
+
return `${target} -> ${path.join(' -> ')} -> ${target}`;
|
|
39
|
+
}
|
|
40
|
+
function formatSortState(names) {
|
|
41
|
+
return names.join(' -> ');
|
|
42
|
+
}
|
|
43
|
+
export function toSortedPlugins(plugins) {
|
|
44
|
+
return plugins.map((plugin, registrationIndex) => ({
|
|
45
|
+
plugin,
|
|
46
|
+
manifest: normalizePluginManifest(plugin, getPluginManifest(plugin)),
|
|
47
|
+
registrationIndex
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
export function normalizePlugins(registrations) {
|
|
51
|
+
const byName = new Map();
|
|
52
|
+
const seenNames = new Set();
|
|
53
|
+
registrations.forEach((entry) => {
|
|
54
|
+
const manifest = entry.manifest;
|
|
55
|
+
const name = manifest.name;
|
|
56
|
+
assertUniqueName(name, seenNames, 'Use explicit `manifest.name` to avoid collisions for anonymous plugin factories.');
|
|
57
|
+
byName.set(name, entry);
|
|
58
|
+
});
|
|
59
|
+
const edges = new Map();
|
|
60
|
+
const indegree = new Map();
|
|
61
|
+
registrations.forEach((entry) => {
|
|
62
|
+
const { name, dependsOn } = entry.manifest;
|
|
63
|
+
indegree.set(name, 0);
|
|
64
|
+
edges.set(name, new Set());
|
|
65
|
+
for (const dependency of dependsOn) {
|
|
66
|
+
if (!byName.has(dependency)) {
|
|
67
|
+
throw new Error(`Invalid plugin dependency for '${name}': missing plugin '${dependency}'. ` +
|
|
68
|
+
'Available plugins: ' +
|
|
69
|
+
Array.from(byName.keys()).sort().join(', '));
|
|
70
|
+
}
|
|
71
|
+
edges.get(dependency).add(name);
|
|
72
|
+
indegree.set(name, (indegree.get(name) || 0) + 1);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
const queue = [];
|
|
76
|
+
byName.forEach((_entry, name) => {
|
|
77
|
+
if ((indegree.get(name) || 0) === 0) {
|
|
78
|
+
queue.push(name);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
const ordered = [];
|
|
82
|
+
const nextReady = (names) => names.sort((a, b) => {
|
|
83
|
+
const aMeta = byName.get(a);
|
|
84
|
+
const bMeta = byName.get(b);
|
|
85
|
+
const priorityDiff = bMeta.manifest.priority - aMeta.manifest.priority;
|
|
86
|
+
if (priorityDiff !== 0) {
|
|
87
|
+
return priorityDiff;
|
|
88
|
+
}
|
|
89
|
+
return aMeta.registrationIndex - bMeta.registrationIndex;
|
|
90
|
+
});
|
|
91
|
+
while (queue.length > 0) {
|
|
92
|
+
const current = nextReady(queue).splice(0, 1)[0];
|
|
93
|
+
const index = queue.indexOf(current);
|
|
94
|
+
if (index >= 0) {
|
|
95
|
+
queue.splice(index, 1);
|
|
96
|
+
}
|
|
97
|
+
const currentEntry = byName.get(current);
|
|
98
|
+
if (!currentEntry) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
ordered.push(currentEntry);
|
|
102
|
+
const dependents = edges.get(current);
|
|
103
|
+
if (!dependents) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
for (const dependentName of dependents) {
|
|
107
|
+
const currentDegree = (indegree.get(dependentName) || 0) - 1;
|
|
108
|
+
indegree.set(dependentName, currentDegree);
|
|
109
|
+
if (currentDegree === 0) {
|
|
110
|
+
queue.push(dependentName);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (ordered.length !== registrations.length) {
|
|
115
|
+
const unresolved = Array.from(indegree.entries())
|
|
116
|
+
.filter(([, degree]) => degree > 0)
|
|
117
|
+
.map(([name]) => name);
|
|
118
|
+
const cycle = detectCycle(unresolved, edges);
|
|
119
|
+
throw new Error('Invalid plugin manifest: circular dependency detected. ' +
|
|
120
|
+
`Unresolved nodes: ${formatSortState(unresolved)}. ` +
|
|
121
|
+
(cycle ? `Detected cycle: ${cycle}.` : 'Refactor dependency graph to a DAG.'));
|
|
122
|
+
}
|
|
123
|
+
const debugOrder = ordered.map((entry) => `${entry.manifest.name}:${entry.manifest.scope}:${entry.manifest.priority}`);
|
|
124
|
+
return { ordered, debugOrder };
|
|
125
|
+
}
|
|
126
|
+
function detectCycle(nodes, edges) {
|
|
127
|
+
const visited = new Set();
|
|
128
|
+
const inStack = new Set();
|
|
129
|
+
const dfs = (node, stack) => {
|
|
130
|
+
if (inStack.has(node)) {
|
|
131
|
+
const cycleStart = stack.indexOf(node);
|
|
132
|
+
if (cycleStart >= 0) {
|
|
133
|
+
return buildDependencyError(stack.slice(cycleStart), node);
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
if (visited.has(node)) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
visited.add(node);
|
|
141
|
+
inStack.add(node);
|
|
142
|
+
const nextNodes = edges.get(node) || new Set();
|
|
143
|
+
for (const next of nextNodes) {
|
|
144
|
+
const hit = dfs(next, [...stack, next]);
|
|
145
|
+
if (hit) {
|
|
146
|
+
return hit;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
inStack.delete(node);
|
|
150
|
+
return null;
|
|
151
|
+
};
|
|
152
|
+
for (const node of nodes) {
|
|
153
|
+
const cycle = dfs(node, [node]);
|
|
154
|
+
if (cycle) {
|
|
155
|
+
return cycle;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReckerRequest } from '../types/index.js';
|
|
2
|
+
export interface RuntimeContext {
|
|
3
|
+
requestId: string;
|
|
4
|
+
correlationId: string;
|
|
5
|
+
tenant?: string;
|
|
6
|
+
policyTags: string[];
|
|
7
|
+
policySource?: string;
|
|
8
|
+
createdAt: number;
|
|
9
|
+
traceId?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function createRequestContext(seed?: Partial<RuntimeContext>): RuntimeContext;
|
|
12
|
+
export declare function attachRequestContext<T extends ReckerRequest>(request: T, context: RuntimeContext): T;
|
|
13
|
+
export declare function getRequestContext(request: ReckerRequest): RuntimeContext | undefined;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
let requestSeq = 0;
|
|
2
|
+
export function createRequestContext(seed = {}) {
|
|
3
|
+
requestSeq += 1;
|
|
4
|
+
const requestId = `${Date.now().toString(36)}-${requestSeq.toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
|
|
5
|
+
return {
|
|
6
|
+
requestId,
|
|
7
|
+
correlationId: seed.correlationId || requestId,
|
|
8
|
+
tenant: seed.tenant,
|
|
9
|
+
policyTags: seed.policyTags ?? [],
|
|
10
|
+
policySource: seed.policySource,
|
|
11
|
+
createdAt: Date.now(),
|
|
12
|
+
traceId: seed.traceId
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function attachRequestContext(request, context) {
|
|
16
|
+
if (!request) {
|
|
17
|
+
return request;
|
|
18
|
+
}
|
|
19
|
+
request._runtime = context;
|
|
20
|
+
return request;
|
|
21
|
+
}
|
|
22
|
+
export function getRequestContext(request) {
|
|
23
|
+
return request._runtime;
|
|
24
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { ReckerRequest, ReckerResponse } from '../types/index.js';
|
|
2
|
+
import type { RuntimeContext } from './request-context.js';
|
|
3
|
+
export type RuntimeEventName = 'request:start' | 'request:success' | 'request:failed' | 'request:retry' | 'request:policy' | 'policy:block' | 'circuit:trip' | 'cache:hit' | 'cache:miss' | 'cache:store' | 'security:block' | 'transport:start' | 'transport:finish' | 'transport:error';
|
|
4
|
+
export interface RuntimeEventPayloads {
|
|
5
|
+
'request:start': {
|
|
6
|
+
context: RuntimeContext;
|
|
7
|
+
req: ReckerRequest;
|
|
8
|
+
};
|
|
9
|
+
'request:success': {
|
|
10
|
+
context: RuntimeContext;
|
|
11
|
+
req: ReckerRequest;
|
|
12
|
+
res: ReckerResponse;
|
|
13
|
+
durationMs: number;
|
|
14
|
+
};
|
|
15
|
+
'request:failed': {
|
|
16
|
+
context: RuntimeContext;
|
|
17
|
+
req: ReckerRequest;
|
|
18
|
+
error: Error;
|
|
19
|
+
durationMs: number;
|
|
20
|
+
};
|
|
21
|
+
'request:retry': {
|
|
22
|
+
context: RuntimeContext;
|
|
23
|
+
req: ReckerRequest;
|
|
24
|
+
attempt: number;
|
|
25
|
+
delayMs: number;
|
|
26
|
+
reason: string;
|
|
27
|
+
};
|
|
28
|
+
'request:policy': {
|
|
29
|
+
context: RuntimeContext;
|
|
30
|
+
req: ReckerRequest;
|
|
31
|
+
policy: string;
|
|
32
|
+
policySource?: string;
|
|
33
|
+
reason?: string;
|
|
34
|
+
};
|
|
35
|
+
'policy:block': {
|
|
36
|
+
context: RuntimeContext;
|
|
37
|
+
req: ReckerRequest;
|
|
38
|
+
policy: string;
|
|
39
|
+
reason: string;
|
|
40
|
+
};
|
|
41
|
+
'circuit:trip': {
|
|
42
|
+
context: RuntimeContext;
|
|
43
|
+
req: ReckerRequest;
|
|
44
|
+
key: string;
|
|
45
|
+
reason: string;
|
|
46
|
+
};
|
|
47
|
+
'cache:hit': {
|
|
48
|
+
context: RuntimeContext;
|
|
49
|
+
req: ReckerRequest;
|
|
50
|
+
cacheStatus: 'hit' | 'stale' | 'revalidated' | 'stale-error';
|
|
51
|
+
};
|
|
52
|
+
'cache:miss': {
|
|
53
|
+
context: RuntimeContext;
|
|
54
|
+
req: ReckerRequest;
|
|
55
|
+
};
|
|
56
|
+
'cache:store': {
|
|
57
|
+
context: RuntimeContext;
|
|
58
|
+
req: ReckerRequest;
|
|
59
|
+
status: number;
|
|
60
|
+
};
|
|
61
|
+
'security:block': {
|
|
62
|
+
context: RuntimeContext;
|
|
63
|
+
req: ReckerRequest;
|
|
64
|
+
rule: string;
|
|
65
|
+
reason: string;
|
|
66
|
+
};
|
|
67
|
+
'transport:start': {
|
|
68
|
+
context: RuntimeContext;
|
|
69
|
+
req: ReckerRequest;
|
|
70
|
+
};
|
|
71
|
+
'transport:finish': {
|
|
72
|
+
context: RuntimeContext;
|
|
73
|
+
req: ReckerRequest;
|
|
74
|
+
durationMs: number;
|
|
75
|
+
};
|
|
76
|
+
'transport:error': {
|
|
77
|
+
context: RuntimeContext;
|
|
78
|
+
req: ReckerRequest;
|
|
79
|
+
error: Error;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export interface TypedEventBus {
|
|
83
|
+
on<K extends RuntimeEventName>(name: K, handler: (event: RuntimeEventPayloads[K]) => void): () => void;
|
|
84
|
+
emit<K extends RuntimeEventName>(name: K, event: RuntimeEventPayloads[K]): void;
|
|
85
|
+
}
|
|
86
|
+
export declare function createNoopEventBus(): TypedEventBus;
|
|
87
|
+
export declare function createRuntimeEventBus(options?: {
|
|
88
|
+
debugLogger?: (...args: any[]) => void;
|
|
89
|
+
}): TypedEventBus;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function createNoopEventBus() {
|
|
2
|
+
return {
|
|
3
|
+
on: () => () => undefined,
|
|
4
|
+
emit: () => undefined
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
export function createRuntimeEventBus(options = {}) {
|
|
8
|
+
const handlers = {};
|
|
9
|
+
const logger = options.debugLogger;
|
|
10
|
+
return {
|
|
11
|
+
on: (name, handler) => {
|
|
12
|
+
const bucket = (handlers[name] ||= new Set());
|
|
13
|
+
bucket.add(handler);
|
|
14
|
+
return () => {
|
|
15
|
+
bucket.delete(handler);
|
|
16
|
+
if (bucket.size === 0) {
|
|
17
|
+
delete handlers[name];
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
},
|
|
21
|
+
emit: (name, event) => {
|
|
22
|
+
if (logger) {
|
|
23
|
+
logger(name, event);
|
|
24
|
+
}
|
|
25
|
+
const bucket = handlers[name];
|
|
26
|
+
if (!bucket || bucket.size === 0) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
for (const handler of bucket) {
|
|
30
|
+
handler(event);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|