@flow-conductor/core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +213 -0
- package/build/index.d.ts +674 -0
- package/build/index.js +667 -0
- package/build/index.js.map +1 -0
- package/package.json +84 -0
package/build/index.js
ADDED
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
// src/utils/url-validator.ts
|
|
2
|
+
var SSRFError = class extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "SSRFError";
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
var PRIVATE_IP_RANGES = [
|
|
9
|
+
// IPv4 private ranges
|
|
10
|
+
/^10\./,
|
|
11
|
+
/^172\.(1[6-9]|2[0-9]|3[01])\./,
|
|
12
|
+
/^192\.168\./,
|
|
13
|
+
// IPv6 private ranges
|
|
14
|
+
/^fc00:/i,
|
|
15
|
+
/^fe80:/i,
|
|
16
|
+
/^::1$/,
|
|
17
|
+
/^fd/
|
|
18
|
+
];
|
|
19
|
+
function validateUrl(url, options = {}) {
|
|
20
|
+
const {
|
|
21
|
+
allowPrivateIPs = false,
|
|
22
|
+
allowLocalhost = false,
|
|
23
|
+
allowedProtocols = ["http:", "https:"],
|
|
24
|
+
disableValidation = false
|
|
25
|
+
} = options;
|
|
26
|
+
if (disableValidation) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!url || typeof url !== "string") {
|
|
30
|
+
throw new SSRFError("URL must be a non-empty string");
|
|
31
|
+
}
|
|
32
|
+
let parsedUrl;
|
|
33
|
+
try {
|
|
34
|
+
parsedUrl = new URL(url);
|
|
35
|
+
} catch {
|
|
36
|
+
throw new SSRFError(`Invalid URL format: ${url}`);
|
|
37
|
+
}
|
|
38
|
+
const protocol = parsedUrl.protocol.toLowerCase();
|
|
39
|
+
if (!allowedProtocols.includes(protocol)) {
|
|
40
|
+
throw new SSRFError(
|
|
41
|
+
`Protocol "${protocol}" is not allowed. Only ${allowedProtocols.join(", ")} are permitted.`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
const hostname = parsedUrl.hostname.toLowerCase();
|
|
45
|
+
const normalizedHostname = hostname.replace(/^\[|\]$/g, "");
|
|
46
|
+
const isLocalhost = normalizedHostname === "localhost" || normalizedHostname === "127.0.0.1" || normalizedHostname === "::1" || normalizedHostname.startsWith("127.") || normalizedHostname === "0.0.0.0";
|
|
47
|
+
if (isLocalhost && !allowLocalhost) {
|
|
48
|
+
throw new SSRFError(
|
|
49
|
+
"Localhost addresses are not allowed for security reasons. Set allowLocalhost=true to override."
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
if (!allowPrivateIPs) {
|
|
53
|
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(normalizedHostname)) {
|
|
54
|
+
const parts = normalizedHostname.split(".").map(Number);
|
|
55
|
+
const [a, b] = parts;
|
|
56
|
+
if (a === 10) {
|
|
57
|
+
throw new SSRFError(
|
|
58
|
+
"Private IP addresses (10.x.x.x) are not allowed for security reasons."
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (a === 172 && b >= 16 && b <= 31) {
|
|
62
|
+
throw new SSRFError(
|
|
63
|
+
"Private IP addresses (172.16-31.x.x) are not allowed for security reasons."
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (a === 192 && b === 168) {
|
|
67
|
+
throw new SSRFError(
|
|
68
|
+
"Private IP addresses (192.168.x.x) are not allowed for security reasons."
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
if (a === 169 && b === 254) {
|
|
72
|
+
throw new SSRFError(
|
|
73
|
+
"Link-local addresses (169.254.x.x) are not allowed for security reasons."
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const isPrivateIP = PRIVATE_IP_RANGES.some(
|
|
78
|
+
(range) => range.test(normalizedHostname)
|
|
79
|
+
);
|
|
80
|
+
if (isPrivateIP) {
|
|
81
|
+
throw new SSRFError(
|
|
82
|
+
"Private/internal IP addresses are not allowed for security reasons. Set allowPrivateIPs=true to override."
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/request-adapter.ts
|
|
89
|
+
var RequestAdapter = class {
|
|
90
|
+
/**
|
|
91
|
+
* Creates a new RequestAdapter instance.
|
|
92
|
+
*
|
|
93
|
+
* @param urlValidationOptions - Options for URL validation to prevent SSRF attacks
|
|
94
|
+
*/
|
|
95
|
+
constructor(urlValidationOptions = {}) {
|
|
96
|
+
this.urlValidationOptions = urlValidationOptions;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Type-safe getter for the execution result.
|
|
100
|
+
* Allows casting the result to a specific type.
|
|
101
|
+
*
|
|
102
|
+
* @template T - The desired result type
|
|
103
|
+
* @param result - The execution result to cast
|
|
104
|
+
* @returns The result cast to type T
|
|
105
|
+
*/
|
|
106
|
+
getResult(result) {
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Executes a request with URL validation.
|
|
111
|
+
* Validates the URL to prevent SSRF attacks before creating the request.
|
|
112
|
+
*
|
|
113
|
+
* @param requestConfig - The request configuration object
|
|
114
|
+
* @returns A promise that resolves to the execution result
|
|
115
|
+
* @throws {SSRFError} If the URL is invalid or potentially dangerous
|
|
116
|
+
*/
|
|
117
|
+
executeRequest(requestConfig) {
|
|
118
|
+
validateUrl(requestConfig.url, this.urlValidationOptions);
|
|
119
|
+
return this.createRequest(requestConfig);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// src/request-manager.ts
|
|
124
|
+
var RequestFlow = class {
|
|
125
|
+
constructor() {
|
|
126
|
+
/**
|
|
127
|
+
* List of pipeline stages to execute
|
|
128
|
+
*/
|
|
129
|
+
this.requestList = [];
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Sets the request adapter to use for executing HTTP requests.
|
|
133
|
+
*
|
|
134
|
+
* @param adapter - The request adapter instance
|
|
135
|
+
* @returns The current RequestFlow instance for method chaining
|
|
136
|
+
*/
|
|
137
|
+
setRequestAdapter(adapter) {
|
|
138
|
+
this.adapter = adapter;
|
|
139
|
+
return this;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Adds multiple pipeline stages to the request list.
|
|
143
|
+
*
|
|
144
|
+
* @param requestList - Array of pipeline stages to add
|
|
145
|
+
* @returns The current RequestFlow instance for method chaining
|
|
146
|
+
*/
|
|
147
|
+
addAll(requestList = []) {
|
|
148
|
+
this.requestList = this.requestList.concat(requestList);
|
|
149
|
+
return this;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Sets an error handler callback that will be called when an error occurs during execution.
|
|
153
|
+
*
|
|
154
|
+
* @param errorHandler - Function to handle errors
|
|
155
|
+
* @returns The current RequestFlow instance for method chaining
|
|
156
|
+
*/
|
|
157
|
+
withErrorHandler(errorHandler) {
|
|
158
|
+
this.errorHandler = errorHandler;
|
|
159
|
+
return this;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Sets a result handler callback that will be called with the execution result.
|
|
163
|
+
*
|
|
164
|
+
* @param resultHandler - Function to handle results
|
|
165
|
+
* @returns The current RequestFlow instance for method chaining
|
|
166
|
+
*/
|
|
167
|
+
withResultHandler(resultHandler) {
|
|
168
|
+
this.resultHandler = resultHandler;
|
|
169
|
+
return this;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Sets a finish handler callback that will be called after execution completes (success or failure).
|
|
173
|
+
*
|
|
174
|
+
* @param finishHandler - Function to execute on completion
|
|
175
|
+
* @returns The current RequestFlow instance for method chaining
|
|
176
|
+
*/
|
|
177
|
+
withFinishHandler(finishHandler) {
|
|
178
|
+
this.finishHandler = finishHandler;
|
|
179
|
+
return this;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// src/utils/retry-utils.ts
|
|
184
|
+
function getErrorStatus(error) {
|
|
185
|
+
if (typeof error.response !== "undefined" && typeof error.response.status === "number") {
|
|
186
|
+
return error.response.status;
|
|
187
|
+
}
|
|
188
|
+
if (typeof error.status === "number") {
|
|
189
|
+
return error.status;
|
|
190
|
+
}
|
|
191
|
+
if (typeof error.statusCode === "number") {
|
|
192
|
+
return error.statusCode;
|
|
193
|
+
}
|
|
194
|
+
return void 0;
|
|
195
|
+
}
|
|
196
|
+
function isNetworkError(error) {
|
|
197
|
+
const networkErrorNames = [
|
|
198
|
+
"TypeError",
|
|
199
|
+
"NetworkError",
|
|
200
|
+
"TimeoutError",
|
|
201
|
+
"AbortError",
|
|
202
|
+
"ECONNREFUSED",
|
|
203
|
+
"ENOTFOUND",
|
|
204
|
+
"ETIMEDOUT"
|
|
205
|
+
];
|
|
206
|
+
if (networkErrorNames.includes(error.name)) {
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
const networkKeywords = [
|
|
210
|
+
"network",
|
|
211
|
+
"connection",
|
|
212
|
+
"timeout",
|
|
213
|
+
"fetch",
|
|
214
|
+
"failed to fetch",
|
|
215
|
+
"ECONNREFUSED",
|
|
216
|
+
"ENOTFOUND",
|
|
217
|
+
"ETIMEDOUT"
|
|
218
|
+
];
|
|
219
|
+
const errorMessage = error.message?.toLowerCase() || "";
|
|
220
|
+
return networkKeywords.some((keyword) => errorMessage.includes(keyword));
|
|
221
|
+
}
|
|
222
|
+
function defaultRetryCondition(error) {
|
|
223
|
+
return isNetworkError(error);
|
|
224
|
+
}
|
|
225
|
+
function retryOnStatusCodes(...statusCodes) {
|
|
226
|
+
return (error) => {
|
|
227
|
+
const status = getErrorStatus(error);
|
|
228
|
+
return status !== void 0 && statusCodes.includes(status);
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function retryOnNetworkOrStatusCodes(...statusCodes) {
|
|
232
|
+
return (error) => {
|
|
233
|
+
if (isNetworkError(error)) {
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
const status = getErrorStatus(error);
|
|
237
|
+
return status !== void 0 && statusCodes.includes(status);
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/utils/chunk-processor.ts
|
|
242
|
+
async function processStream(stream, config) {
|
|
243
|
+
const { chunkHandler, encoding = "utf-8", accumulate = false } = config;
|
|
244
|
+
const reader = stream.getReader();
|
|
245
|
+
const decoder = new TextDecoder(encoding);
|
|
246
|
+
let accumulatedData;
|
|
247
|
+
let chunkIndex = 0;
|
|
248
|
+
let totalBytesRead = 0;
|
|
249
|
+
try {
|
|
250
|
+
while (true) {
|
|
251
|
+
const { done, value } = await reader.read();
|
|
252
|
+
if (done) {
|
|
253
|
+
if (accumulatedData !== void 0 && accumulatedData.length > 0) {
|
|
254
|
+
await processChunk(
|
|
255
|
+
accumulatedData,
|
|
256
|
+
chunkHandler,
|
|
257
|
+
chunkIndex,
|
|
258
|
+
true,
|
|
259
|
+
totalBytesRead
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
totalBytesRead += value.length;
|
|
265
|
+
if (accumulate) {
|
|
266
|
+
if (accumulatedData === void 0) {
|
|
267
|
+
accumulatedData = value;
|
|
268
|
+
} else if (typeof accumulatedData === "string") {
|
|
269
|
+
accumulatedData += decoder.decode(value, { stream: true });
|
|
270
|
+
} else {
|
|
271
|
+
const combined = new Uint8Array(
|
|
272
|
+
accumulatedData.length + value.length
|
|
273
|
+
);
|
|
274
|
+
combined.set(accumulatedData);
|
|
275
|
+
combined.set(value, accumulatedData.length);
|
|
276
|
+
accumulatedData = combined;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const isLast = false;
|
|
280
|
+
await processChunk(
|
|
281
|
+
value,
|
|
282
|
+
chunkHandler,
|
|
283
|
+
chunkIndex,
|
|
284
|
+
isLast,
|
|
285
|
+
totalBytesRead
|
|
286
|
+
);
|
|
287
|
+
chunkIndex++;
|
|
288
|
+
}
|
|
289
|
+
return accumulatedData;
|
|
290
|
+
} finally {
|
|
291
|
+
reader.releaseLock();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async function processTextStreamLineByLine(stream, config) {
|
|
295
|
+
const { chunkHandler, encoding = "utf-8", accumulate = false } = config;
|
|
296
|
+
const reader = stream.getReader();
|
|
297
|
+
const decoder = new TextDecoder(encoding);
|
|
298
|
+
let buffer = "";
|
|
299
|
+
let lineIndex = 0;
|
|
300
|
+
let totalBytesRead = 0;
|
|
301
|
+
try {
|
|
302
|
+
while (true) {
|
|
303
|
+
const { done, value } = await reader.read();
|
|
304
|
+
if (done) {
|
|
305
|
+
if (buffer.length > 0) {
|
|
306
|
+
await processChunk(
|
|
307
|
+
buffer,
|
|
308
|
+
chunkHandler,
|
|
309
|
+
lineIndex,
|
|
310
|
+
true,
|
|
311
|
+
totalBytesRead
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
totalBytesRead += value.length;
|
|
317
|
+
buffer += decoder.decode(value, { stream: true });
|
|
318
|
+
const lines = buffer.split("\n");
|
|
319
|
+
buffer = lines.pop() || "";
|
|
320
|
+
for (const line of lines) {
|
|
321
|
+
if (line.length > 0) {
|
|
322
|
+
await processChunk(
|
|
323
|
+
line,
|
|
324
|
+
chunkHandler,
|
|
325
|
+
lineIndex,
|
|
326
|
+
false,
|
|
327
|
+
totalBytesRead
|
|
328
|
+
);
|
|
329
|
+
lineIndex++;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return accumulate ? buffer : void 0;
|
|
334
|
+
} finally {
|
|
335
|
+
reader.releaseLock();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
async function processResponseStream(response, config) {
|
|
339
|
+
if (!response.body) {
|
|
340
|
+
throw new Error("Response body is not available for streaming");
|
|
341
|
+
}
|
|
342
|
+
const processed = await processStream(response.body, config);
|
|
343
|
+
if (config.accumulate && processed !== void 0) {
|
|
344
|
+
return processed;
|
|
345
|
+
}
|
|
346
|
+
return response;
|
|
347
|
+
}
|
|
348
|
+
async function processChunk(chunk, handler, index, isLast, totalBytesRead) {
|
|
349
|
+
const result = handler(chunk, {
|
|
350
|
+
index,
|
|
351
|
+
isLast,
|
|
352
|
+
totalBytesRead
|
|
353
|
+
});
|
|
354
|
+
if (result instanceof Promise) {
|
|
355
|
+
await result;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function isReadableStream(value) {
|
|
359
|
+
return value !== null && typeof value === "object" && "getReader" in value && typeof value.getReader === "function";
|
|
360
|
+
}
|
|
361
|
+
function hasReadableStream(response) {
|
|
362
|
+
return response.body !== null && isReadableStream(response.body);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/request-chain.ts
|
|
366
|
+
var _RequestChain = class _RequestChain extends RequestFlow {
|
|
367
|
+
constructor() {
|
|
368
|
+
super(...arguments);
|
|
369
|
+
/**
|
|
370
|
+
* Adds a new stage to the request chain and returns a new chain with updated types.
|
|
371
|
+
* This method enables type-safe chaining of requests.
|
|
372
|
+
*
|
|
373
|
+
* @template NewOut - The output type of the new stage
|
|
374
|
+
* @param stage - The pipeline stage to add (request or manager stage)
|
|
375
|
+
* @returns A new RequestChain instance with the added stage
|
|
376
|
+
*/
|
|
377
|
+
this.next = (stage) => {
|
|
378
|
+
return this.addRequestEntity(stage);
|
|
379
|
+
};
|
|
380
|
+
/**
|
|
381
|
+
* Executes all stages in the chain sequentially and returns the final result.
|
|
382
|
+
* Handles errors and calls registered handlers appropriately.
|
|
383
|
+
*
|
|
384
|
+
* @returns A promise that resolves to the final output result
|
|
385
|
+
* @throws {Error} If an error occurs and no error handler is registered
|
|
386
|
+
*/
|
|
387
|
+
this.execute = async () => {
|
|
388
|
+
try {
|
|
389
|
+
const results = await this.executeAllRequests(this.requestList);
|
|
390
|
+
const result = results[results.length - 1];
|
|
391
|
+
if (this.resultHandler && result) {
|
|
392
|
+
this.resultHandler(result);
|
|
393
|
+
}
|
|
394
|
+
return result;
|
|
395
|
+
} catch (error) {
|
|
396
|
+
if (this.errorHandler) {
|
|
397
|
+
this.errorHandler(error);
|
|
398
|
+
return Promise.reject(error);
|
|
399
|
+
} else {
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
} finally {
|
|
403
|
+
if (this.finishHandler) {
|
|
404
|
+
this.finishHandler();
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
// #endregion
|
|
409
|
+
// #region Private methods
|
|
410
|
+
/**
|
|
411
|
+
* Adds a request entity (stage) to the internal request list.
|
|
412
|
+
*
|
|
413
|
+
* @template NewOut - The output type of the new stage
|
|
414
|
+
* @param stage - The pipeline stage to add
|
|
415
|
+
* @returns A new RequestChain instance with updated types
|
|
416
|
+
*/
|
|
417
|
+
this.addRequestEntity = (stage) => {
|
|
418
|
+
this.requestList.push(stage);
|
|
419
|
+
return this;
|
|
420
|
+
};
|
|
421
|
+
/**
|
|
422
|
+
* Executes all request entities in sequence, handling preconditions and mappers.
|
|
423
|
+
* Stages with failed preconditions are skipped but preserve the previous result.
|
|
424
|
+
*
|
|
425
|
+
* @template Out - The output type
|
|
426
|
+
* @param requestEntityList - List of pipeline stages to execute
|
|
427
|
+
* @returns A promise that resolves to an array of all stage results
|
|
428
|
+
*/
|
|
429
|
+
this.executeAllRequests = async (requestEntityList) => {
|
|
430
|
+
const results = [];
|
|
431
|
+
for (let i = 0; i < requestEntityList.length; i++) {
|
|
432
|
+
const requestEntity = requestEntityList[i];
|
|
433
|
+
if (requestEntity.precondition && !requestEntity.precondition()) {
|
|
434
|
+
const previousEntity2 = requestEntityList[i - 1];
|
|
435
|
+
const previousResult2 = previousEntity2?.result;
|
|
436
|
+
requestEntityList[i].result = previousResult2;
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const previousEntity = requestEntityList[i - 1];
|
|
440
|
+
const previousResult = previousEntity?.result;
|
|
441
|
+
try {
|
|
442
|
+
const requestResult = await this.executeSingle(
|
|
443
|
+
requestEntity,
|
|
444
|
+
previousResult
|
|
445
|
+
);
|
|
446
|
+
let result = requestResult;
|
|
447
|
+
if (requestEntity.mapper) {
|
|
448
|
+
let mappedResult;
|
|
449
|
+
if (isPipelineRequestStage(requestEntity)) {
|
|
450
|
+
mappedResult = requestEntity.mapper(
|
|
451
|
+
requestResult,
|
|
452
|
+
previousResult
|
|
453
|
+
);
|
|
454
|
+
} else if (isPipelineManagerStage(requestEntity)) {
|
|
455
|
+
mappedResult = requestEntity.mapper(
|
|
456
|
+
requestResult,
|
|
457
|
+
previousResult
|
|
458
|
+
);
|
|
459
|
+
} else {
|
|
460
|
+
mappedResult = result;
|
|
461
|
+
}
|
|
462
|
+
result = mappedResult instanceof Promise ? await mappedResult : mappedResult;
|
|
463
|
+
}
|
|
464
|
+
if (requestEntity.resultInterceptor) {
|
|
465
|
+
await requestEntity.resultInterceptor(result);
|
|
466
|
+
}
|
|
467
|
+
requestEntityList[i].result = result;
|
|
468
|
+
results.push(result);
|
|
469
|
+
} catch (error) {
|
|
470
|
+
const requestConfig = isPipelineRequestStage(requestEntity) ? requestEntity.config : void 0;
|
|
471
|
+
error.cause = { ...error.cause, requestConfig };
|
|
472
|
+
if (requestEntity.errorHandler) {
|
|
473
|
+
await requestEntity.errorHandler(error);
|
|
474
|
+
}
|
|
475
|
+
throw error;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return results;
|
|
479
|
+
};
|
|
480
|
+
/**
|
|
481
|
+
* Executes a single request entity (stage).
|
|
482
|
+
* Handles both request stages and nested manager stages.
|
|
483
|
+
* Implements retry logic for request stages when retry configuration is provided.
|
|
484
|
+
* Supports progressive chunk processing for streaming responses.
|
|
485
|
+
*
|
|
486
|
+
* @template Out - The output type
|
|
487
|
+
* @param requestEntity - The pipeline stage to execute
|
|
488
|
+
* @param previousResult - The result from the previous stage (optional)
|
|
489
|
+
* @returns A promise that resolves to the stage result
|
|
490
|
+
* @throws {Error} If the stage type is unknown or all retries are exhausted
|
|
491
|
+
*/
|
|
492
|
+
this.executeSingle = async (requestEntity, previousResult) => {
|
|
493
|
+
if (isPipelineRequestStage(requestEntity)) {
|
|
494
|
+
const { config, retry, chunkProcessing } = requestEntity;
|
|
495
|
+
const requestConfig = typeof config === "function" ? config(previousResult) : config;
|
|
496
|
+
if (retry) {
|
|
497
|
+
return this.executeWithRetry(
|
|
498
|
+
requestConfig,
|
|
499
|
+
retry,
|
|
500
|
+
chunkProcessing
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
const rawResult = await this.adapter.executeRequest(requestConfig);
|
|
504
|
+
return this.processResultWithChunks(rawResult, chunkProcessing);
|
|
505
|
+
} else if (isPipelineManagerStage(requestEntity)) {
|
|
506
|
+
const { request } = requestEntity;
|
|
507
|
+
const rawResult = await request.execute();
|
|
508
|
+
return rawResult;
|
|
509
|
+
} else {
|
|
510
|
+
throw new Error("Unknown type");
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
/**
|
|
514
|
+
* Executes a request with retry logic based on the provided retry configuration.
|
|
515
|
+
* Supports chunk processing for streaming responses.
|
|
516
|
+
*
|
|
517
|
+
* @template Out - The output type
|
|
518
|
+
* @param requestConfig - The request configuration
|
|
519
|
+
* @param retryConfig - The retry configuration
|
|
520
|
+
* @param chunkProcessing - Optional chunk processing configuration
|
|
521
|
+
* @returns A promise that resolves to the request result
|
|
522
|
+
* @throws {Error} If all retry attempts are exhausted
|
|
523
|
+
*/
|
|
524
|
+
this.executeWithRetry = async (requestConfig, retryConfig, chunkProcessing) => {
|
|
525
|
+
const maxRetries = retryConfig.maxRetries ?? 3;
|
|
526
|
+
const retryCondition = retryConfig.retryCondition ?? defaultRetryCondition;
|
|
527
|
+
let lastError;
|
|
528
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
529
|
+
try {
|
|
530
|
+
const rawResult = await this.adapter.executeRequest(requestConfig);
|
|
531
|
+
return this.processResultWithChunks(rawResult, chunkProcessing);
|
|
532
|
+
} catch (error) {
|
|
533
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
534
|
+
const shouldRetry = attempt < maxRetries && retryCondition(lastError, attempt);
|
|
535
|
+
if (!shouldRetry) {
|
|
536
|
+
throw lastError;
|
|
537
|
+
}
|
|
538
|
+
const delay = this.calculateRetryDelay(
|
|
539
|
+
attempt + 1,
|
|
540
|
+
lastError,
|
|
541
|
+
retryConfig
|
|
542
|
+
);
|
|
543
|
+
if (delay > 0) {
|
|
544
|
+
await this.sleep(delay);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
throw lastError || new Error("Retry failed");
|
|
549
|
+
};
|
|
550
|
+
/**
|
|
551
|
+
* Processes a result with chunk processing if enabled.
|
|
552
|
+
* Handles streaming responses by processing chunks progressively.
|
|
553
|
+
*
|
|
554
|
+
* @template Out - The output type
|
|
555
|
+
* @param rawResult - The raw result from the adapter
|
|
556
|
+
* @param chunkProcessing - Optional chunk processing configuration
|
|
557
|
+
* @returns A promise that resolves to the processed result
|
|
558
|
+
*/
|
|
559
|
+
this.processResultWithChunks = async (rawResult, chunkProcessing) => {
|
|
560
|
+
if (chunkProcessing?.enabled && rawResult instanceof Response && hasReadableStream(rawResult)) {
|
|
561
|
+
const clonedResponse = rawResult.clone();
|
|
562
|
+
const processed = await processResponseStream(clonedResponse, {
|
|
563
|
+
...chunkProcessing
|
|
564
|
+
});
|
|
565
|
+
if (chunkProcessing.accumulate && processed !== rawResult) {
|
|
566
|
+
return processed;
|
|
567
|
+
}
|
|
568
|
+
return this.adapter.getResult(rawResult);
|
|
569
|
+
}
|
|
570
|
+
return this.adapter.getResult(rawResult);
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Executes all stages in the chain and returns all results as a tuple.
|
|
575
|
+
* Useful when you need access to intermediate results.
|
|
576
|
+
*
|
|
577
|
+
* @returns A promise that resolves to a tuple of all stage results
|
|
578
|
+
* @throws {Error} If an error occurs and no error handler is registered
|
|
579
|
+
*/
|
|
580
|
+
async executeAll() {
|
|
581
|
+
try {
|
|
582
|
+
const results = await this.executeAllRequests(this.requestList);
|
|
583
|
+
if (this.resultHandler && results.length > 0) {
|
|
584
|
+
this.resultHandler(results);
|
|
585
|
+
}
|
|
586
|
+
return results;
|
|
587
|
+
} catch (error) {
|
|
588
|
+
if (this.errorHandler) {
|
|
589
|
+
this.errorHandler(error);
|
|
590
|
+
return Promise.reject(error);
|
|
591
|
+
} else {
|
|
592
|
+
throw error;
|
|
593
|
+
}
|
|
594
|
+
} finally {
|
|
595
|
+
if (this.finishHandler) {
|
|
596
|
+
this.finishHandler();
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Calculates the delay before the next retry attempt.
|
|
602
|
+
*
|
|
603
|
+
* @param attempt - The current attempt number (1-indexed for retries)
|
|
604
|
+
* @param error - The error that occurred
|
|
605
|
+
* @param retryConfig - The retry configuration
|
|
606
|
+
* @returns The delay in milliseconds
|
|
607
|
+
*/
|
|
608
|
+
calculateRetryDelay(attempt, error, retryConfig) {
|
|
609
|
+
const baseDelay = retryConfig.retryDelay ?? 1e3;
|
|
610
|
+
const maxDelay = retryConfig.maxDelay;
|
|
611
|
+
let delay;
|
|
612
|
+
if (typeof baseDelay === "function") {
|
|
613
|
+
delay = baseDelay(attempt, error);
|
|
614
|
+
} else if (retryConfig.exponentialBackoff) {
|
|
615
|
+
delay = baseDelay * Math.pow(2, attempt - 1);
|
|
616
|
+
if (maxDelay !== void 0 && delay > maxDelay) {
|
|
617
|
+
delay = maxDelay;
|
|
618
|
+
}
|
|
619
|
+
} else {
|
|
620
|
+
delay = baseDelay;
|
|
621
|
+
}
|
|
622
|
+
return Math.max(0, delay);
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Sleeps for the specified number of milliseconds.
|
|
626
|
+
*
|
|
627
|
+
* @param ms - Milliseconds to sleep
|
|
628
|
+
* @returns A promise that resolves after the delay
|
|
629
|
+
*/
|
|
630
|
+
sleep(ms) {
|
|
631
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
632
|
+
}
|
|
633
|
+
// #endregion
|
|
634
|
+
};
|
|
635
|
+
// #region Public methods
|
|
636
|
+
/**
|
|
637
|
+
* Creates a new RequestChain with an initial stage.
|
|
638
|
+
* This is the entry point for building a request chain.
|
|
639
|
+
*
|
|
640
|
+
* @template Out - The output type of the initial stage
|
|
641
|
+
* @template AdapterExecutionResult - The type of result returned by the adapter
|
|
642
|
+
* @template AdapterRequestConfig - The type of request configuration
|
|
643
|
+
* @param stage - The initial pipeline stage (request or manager stage)
|
|
644
|
+
* @param adapter - The request adapter to use for HTTP requests
|
|
645
|
+
* @returns A new RequestChain instance with the initial stage
|
|
646
|
+
*/
|
|
647
|
+
_RequestChain.begin = (stage, adapter) => {
|
|
648
|
+
const requestChain = new _RequestChain();
|
|
649
|
+
requestChain.setRequestAdapter(adapter);
|
|
650
|
+
return requestChain.next(stage);
|
|
651
|
+
};
|
|
652
|
+
var RequestChain = _RequestChain;
|
|
653
|
+
function begin(stage, adapter) {
|
|
654
|
+
const requestChain = new RequestChain();
|
|
655
|
+
requestChain.setRequestAdapter(adapter);
|
|
656
|
+
return requestChain.next(stage);
|
|
657
|
+
}
|
|
658
|
+
function isPipelineRequestStage(stage) {
|
|
659
|
+
return "config" in stage && !("request" in stage);
|
|
660
|
+
}
|
|
661
|
+
function isPipelineManagerStage(stage) {
|
|
662
|
+
return "request" in stage && !("config" in stage);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
export { RequestAdapter, RequestChain, RequestFlow as RequestManager, SSRFError, begin, RequestChain as default, defaultRetryCondition, getErrorStatus, hasReadableStream, isNetworkError, isReadableStream, processResponseStream, processStream, processTextStreamLineByLine, retryOnNetworkOrStatusCodes, retryOnStatusCodes, validateUrl };
|
|
666
|
+
//# sourceMappingURL=index.js.map
|
|
667
|
+
//# sourceMappingURL=index.js.map
|