keq 5.2.1 → 5.3.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/CHANGELOG.md +13 -0
- package/dist/index.d.mts +75 -6
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +75 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +213 -57
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +213 -57
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -3
package/dist/index.js
CHANGED
|
@@ -32,8 +32,6 @@ let qs = require("qs");
|
|
|
32
32
|
qs = __toESM(qs);
|
|
33
33
|
let picomatch = require("picomatch");
|
|
34
34
|
picomatch = __toESM(picomatch);
|
|
35
|
-
let fastq = require("fastq");
|
|
36
|
-
fastq = __toESM(fastq);
|
|
37
35
|
//#region src/exception/exception.ts
|
|
38
36
|
var Exception = class extends ts_custom_error.CustomError {
|
|
39
37
|
constructor(message) {
|
|
@@ -349,10 +347,6 @@ var GatewayTimeoutException = class extends RequestException {
|
|
|
349
347
|
}
|
|
350
348
|
};
|
|
351
349
|
//#endregion
|
|
352
|
-
//#region src/utils/base64.ts
|
|
353
|
-
const base64Encode = globalThis.btoa || ((str) => Buffer.from(str).toString("base64"));
|
|
354
|
-
globalThis.atob;
|
|
355
|
-
//#endregion
|
|
356
350
|
//#region src/validator/validator.ts
|
|
357
351
|
/**
|
|
358
352
|
* Validator 类用于类型检查
|
|
@@ -437,6 +431,13 @@ var Validator = class Validator {
|
|
|
437
431
|
return Validator.isObject(obj) && Validator.isFunction(obj["append"]) && Validator.isFunction(obj["delete"]) && Validator.isFunction(obj["entries"]) && Validator.isFunction(obj["forEach"]) && Validator.isFunction(obj["get"]) && Validator.isFunction(obj["getAll"]) && Validator.isFunction(obj["has"]) && Validator.isFunction(obj["keys"]) && Validator.isFunction(obj["set"]) && Validator.isFunction(obj["values"]) && Validator.isFunction(obj["sort"]) && Validator.isFunction(obj["toString"]);
|
|
438
432
|
}
|
|
439
433
|
/**
|
|
434
|
+
* 检查是否为 AbortSignal
|
|
435
|
+
*/
|
|
436
|
+
static isAbortSignal(obj) {
|
|
437
|
+
if (typeof AbortSignal !== "undefined" && obj instanceof AbortSignal) return true;
|
|
438
|
+
return Validator.isObject(obj) && typeof obj["aborted"] === "boolean" && Validator.isFunction(obj["addEventListener"]) && Validator.isFunction(obj["removeEventListener"]);
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
440
441
|
* 检查是否为合法的 Header 值
|
|
441
442
|
* 允许范围: HTAB(\t), 0x20-0x7E, 0x80-0xFF
|
|
442
443
|
* 禁止换行等控制字符
|
|
@@ -464,11 +465,6 @@ function shallowClone(obj) {
|
|
|
464
465
|
return obj;
|
|
465
466
|
}
|
|
466
467
|
//#endregion
|
|
467
|
-
//#region src/utils/sleep.ts
|
|
468
|
-
function sleep(ms) {
|
|
469
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
470
|
-
}
|
|
471
|
-
//#endregion
|
|
472
468
|
//#region src/request-init/utils/clone-body.ts
|
|
473
469
|
function cloneBody(obj) {
|
|
474
470
|
if (Validator.isFormData(obj)) {
|
|
@@ -1105,7 +1101,14 @@ function fixContentType(contentType) {
|
|
|
1105
1101
|
function getLocationId(depth = 0) {
|
|
1106
1102
|
const err = /* @__PURE__ */ new Error();
|
|
1107
1103
|
if (!err.stack) return "";
|
|
1108
|
-
|
|
1104
|
+
const lines = err.stack.split("\n");
|
|
1105
|
+
const offset = depth + 2;
|
|
1106
|
+
if (lines[offset]) return lines[offset].trim();
|
|
1107
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1108
|
+
const line = lines[i].trim();
|
|
1109
|
+
if (line) return line;
|
|
1110
|
+
}
|
|
1111
|
+
return "";
|
|
1109
1112
|
}
|
|
1110
1113
|
//#endregion
|
|
1111
1114
|
//#region src/request/utils/resolve-with.ts
|
|
@@ -1227,6 +1230,15 @@ var Core = class extends Promise {
|
|
|
1227
1230
|
_defineProperty(this, "__reject__", void 0);
|
|
1228
1231
|
_defineProperty(this, "__triggered__", false);
|
|
1229
1232
|
_defineProperty(this, "__requestPromise__", void 0);
|
|
1233
|
+
_defineProperty(this, "__aborted__", false);
|
|
1234
|
+
_defineProperty(this, "__abortReason__", void 0);
|
|
1235
|
+
_defineProperty(this, "__activeSharedContext__", void 0);
|
|
1236
|
+
_defineProperty(this, "__resolveAbort__", void 0);
|
|
1237
|
+
_defineProperty(this, "__abortPromise__", new Promise((resolve) => {
|
|
1238
|
+
this.__resolveAbort__ = resolve;
|
|
1239
|
+
}));
|
|
1240
|
+
_defineProperty(this, "__abortWhenCleanups__", []);
|
|
1241
|
+
_defineProperty(this, "__settled__", false);
|
|
1230
1242
|
_defineProperty(this, "requestInit", void 0);
|
|
1231
1243
|
_defineProperty(this, "__listeners__", {});
|
|
1232
1244
|
_defineProperty(this, "__global__", void 0);
|
|
@@ -1269,6 +1281,63 @@ var Core = class extends Promise {
|
|
|
1269
1281
|
for (const [key, value] of Object.entries(opts)) this.__options__[key] = value;
|
|
1270
1282
|
return this;
|
|
1271
1283
|
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Abort the request manually.
|
|
1286
|
+
*
|
|
1287
|
+
* The request rejects with the given reason (an {@link AbortException} by default),
|
|
1288
|
+
* the `abort` event is emitted, and no further retry is performed.
|
|
1289
|
+
* It is safe to call `abort()` before the request is sent: the request will be
|
|
1290
|
+
* aborted as soon as it starts.
|
|
1291
|
+
*
|
|
1292
|
+
* @description 手动中止请求
|
|
1293
|
+
* @param reason Abort reason. Defaults to `new AbortException('Request aborted')`.
|
|
1294
|
+
*/
|
|
1295
|
+
abort(reason = new AbortException("Request aborted")) {
|
|
1296
|
+
if (this.__aborted__) return;
|
|
1297
|
+
this.__aborted__ = true;
|
|
1298
|
+
this.__abortReason__ = reason;
|
|
1299
|
+
this.__resolveAbort__(reason);
|
|
1300
|
+
this.__activeSharedContext__?.request.abort(reason);
|
|
1301
|
+
this.__clearAbortWhenCleanups__();
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Abort the request when the given `AbortSignal` aborts,
|
|
1305
|
+
* or when the given `Promise` settles (resolves or rejects).
|
|
1306
|
+
*
|
|
1307
|
+
* It is designed for the normal `await` flow, so business code
|
|
1308
|
+
* does not need to call `.fire()` first:
|
|
1309
|
+
* `await request.get('/api/cats').abortWhen(signal)`.
|
|
1310
|
+
*
|
|
1311
|
+
* @description 当 signal 中止或 promise 完成时终止请求
|
|
1312
|
+
*/
|
|
1313
|
+
abortWhen(source) {
|
|
1314
|
+
if (this.__aborted__ || this.__settled__) return this;
|
|
1315
|
+
if (Validator.isAbortSignal(source)) {
|
|
1316
|
+
const reason = () => source.reason ?? new AbortException("Request aborted by AbortSignal passed to abortWhen()");
|
|
1317
|
+
if (source.aborted) {
|
|
1318
|
+
this.abort(reason());
|
|
1319
|
+
return this;
|
|
1320
|
+
}
|
|
1321
|
+
const onAbort = () => this.abort(reason());
|
|
1322
|
+
source.addEventListener("abort", onAbort, { once: true });
|
|
1323
|
+
this.__abortWhenCleanups__.push(() => source.removeEventListener("abort", onAbort));
|
|
1324
|
+
return this;
|
|
1325
|
+
}
|
|
1326
|
+
if (Validator.isObject(source) && typeof source.then === "function") {
|
|
1327
|
+
const reason = new AbortException("Request aborted by Promise passed to abortWhen()");
|
|
1328
|
+
let cancelled = false;
|
|
1329
|
+
this.__abortWhenCleanups__.push(() => {
|
|
1330
|
+
cancelled = true;
|
|
1331
|
+
});
|
|
1332
|
+
Promise.resolve(source).then(() => {
|
|
1333
|
+
if (!cancelled) this.abort(reason);
|
|
1334
|
+
}, () => {
|
|
1335
|
+
if (!cancelled) this.abort(reason);
|
|
1336
|
+
});
|
|
1337
|
+
return this;
|
|
1338
|
+
}
|
|
1339
|
+
throw new TypeException("abortWhen() expects an AbortSignal or a Promise");
|
|
1340
|
+
}
|
|
1272
1341
|
derive() {
|
|
1273
1342
|
const derived = new this.constructor(this.requestInit.url, {
|
|
1274
1343
|
method: this.requestInit.method,
|
|
@@ -1305,68 +1374,103 @@ var Core = class extends Promise {
|
|
|
1305
1374
|
const listeners = this.__listeners__[eventName];
|
|
1306
1375
|
for (const listener of listeners) coreContext.emitter.on(eventName, listener);
|
|
1307
1376
|
}
|
|
1377
|
+
this.__activeSharedContext__ = coreContext;
|
|
1378
|
+
if (this.__aborted__) coreContext.request.abort(this.__abortReason__);
|
|
1308
1379
|
return coreContext;
|
|
1309
1380
|
}
|
|
1310
1381
|
async run() {
|
|
1311
1382
|
let attempt;
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
let retryTimes = sharedContext.options.retry?.times || 0;
|
|
1329
|
-
if (retryTimes) {
|
|
1330
|
-
if (retryTimes < 0) retryTimes = 0;
|
|
1331
|
-
else if (!Number.isInteger(retryTimes)) retryTimes = Math.floor(retryTimes);
|
|
1332
|
-
}
|
|
1333
|
-
if (attemptWithDefault >= retryTimes || await retryOn(attemptWithDefault, error, sharedContext) === false) {
|
|
1334
|
-
if (error) {
|
|
1383
|
+
try {
|
|
1384
|
+
while (true) {
|
|
1385
|
+
if (this.__aborted__ && attempt !== void 0) throw this.__abortReason__;
|
|
1386
|
+
const attemptWithDefault = attempt || 0;
|
|
1387
|
+
const sharedContext = this.buildSharedContext();
|
|
1388
|
+
const orchestrator = new KeqMiddlewareOrchestrator(sharedContext, this.__middlewares__);
|
|
1389
|
+
if (attempt !== void 0) sharedContext.data.retry = { attempt };
|
|
1390
|
+
if (attempt && attempt >= 1) sharedContext.emitter.emit("retry", { context: sharedContext });
|
|
1391
|
+
let error = null;
|
|
1392
|
+
try {
|
|
1393
|
+
await orchestrator.execute();
|
|
1394
|
+
} catch (err) {
|
|
1395
|
+
error = err;
|
|
1396
|
+
}
|
|
1397
|
+
if (this.__aborted__) {
|
|
1398
|
+
const reason = error ?? this.__abortReason__;
|
|
1335
1399
|
sharedContext.emitter.emit("error", { context: sharedContext });
|
|
1336
|
-
throw
|
|
1400
|
+
throw reason;
|
|
1401
|
+
}
|
|
1402
|
+
const retryOn = typeof sharedContext.options.retry?.on === "function" ? sharedContext.options.retry.on : (attempt, error) => {
|
|
1403
|
+
if (error instanceof RequestException && error.fatal) return false;
|
|
1404
|
+
return !!error;
|
|
1405
|
+
};
|
|
1406
|
+
let retryTimes = sharedContext.options.retry?.times || 0;
|
|
1407
|
+
if (retryTimes) {
|
|
1408
|
+
if (retryTimes < 0) retryTimes = 0;
|
|
1409
|
+
else if (!Number.isInteger(retryTimes)) retryTimes = Math.floor(retryTimes);
|
|
1410
|
+
}
|
|
1411
|
+
if (attemptWithDefault >= retryTimes || await retryOn(attemptWithDefault, error, sharedContext) === false) {
|
|
1412
|
+
if (error) {
|
|
1413
|
+
sharedContext.emitter.emit("error", { context: sharedContext });
|
|
1414
|
+
throw error;
|
|
1415
|
+
}
|
|
1416
|
+
return sharedContext;
|
|
1337
1417
|
}
|
|
1338
|
-
|
|
1418
|
+
const retryDelay = typeof sharedContext.options.retry?.delay === "function" ? await sharedContext.options.retry.delay(attemptWithDefault, error, sharedContext) : sharedContext.options.retry?.delay || 0;
|
|
1419
|
+
if (retryDelay) await new Promise((resolve) => {
|
|
1420
|
+
const timer = setTimeout(resolve, retryDelay);
|
|
1421
|
+
this.__abortPromise__.then(() => {
|
|
1422
|
+
clearTimeout(timer);
|
|
1423
|
+
resolve();
|
|
1424
|
+
});
|
|
1425
|
+
});
|
|
1426
|
+
attempt = attemptWithDefault + 1;
|
|
1339
1427
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
attempt = attemptWithDefault + 1;
|
|
1428
|
+
} finally {
|
|
1429
|
+
this.__activeSharedContext__ = void 0;
|
|
1343
1430
|
}
|
|
1344
1431
|
}
|
|
1345
1432
|
__execute__() {
|
|
1346
1433
|
if (!this.__requestPromise__) this.__requestPromise__ = (async () => {
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1434
|
+
try {
|
|
1435
|
+
const coreContext = await this.run();
|
|
1436
|
+
const resolveWithMode = coreContext.options.resolveWith;
|
|
1437
|
+
if (resolveWithMode === "response") return coreContext.response?.clone();
|
|
1438
|
+
const response = coreContext.response;
|
|
1439
|
+
if (!resolveWithMode || resolveWithMode === "intelligent") {
|
|
1440
|
+
const output = coreContext.output;
|
|
1441
|
+
if (output !== void 0) return output;
|
|
1442
|
+
return await intelligentParseResponse(response);
|
|
1443
|
+
}
|
|
1444
|
+
if (!response) throw new Exception([
|
|
1445
|
+
`Unable to process the response with '${resolveWithMode}'. Possible causes:`,
|
|
1446
|
+
"1. The request was never initiated or sent",
|
|
1447
|
+
"2. The request failed before a response was received."
|
|
1448
|
+
].join("\n"));
|
|
1449
|
+
return await resolveWith(response, resolveWithMode);
|
|
1450
|
+
} finally {
|
|
1451
|
+
this.__settled__ = true;
|
|
1452
|
+
this.__clearAbortWhenCleanups__();
|
|
1355
1453
|
}
|
|
1356
|
-
if (!response) throw new Exception([
|
|
1357
|
-
`Unable to process the response with '${resolveWithMode}'. Possible causes:`,
|
|
1358
|
-
"1. The request was never initiated or sent",
|
|
1359
|
-
"2. The request failed before a response was received."
|
|
1360
|
-
].join("\n"));
|
|
1361
|
-
return await resolveWith(response, resolveWithMode);
|
|
1362
1454
|
})();
|
|
1363
1455
|
return this.__requestPromise__;
|
|
1364
1456
|
}
|
|
1457
|
+
__clearAbortWhenCleanups__() {
|
|
1458
|
+
const cleanups = this.__abortWhenCleanups__;
|
|
1459
|
+
this.__abortWhenCleanups__ = [];
|
|
1460
|
+
for (const cleanup of cleanups) cleanup();
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Trigger the request without waiting for the result.
|
|
1464
|
+
*
|
|
1465
|
+
* Returns the request instance itself, so the handler can still be used later,
|
|
1466
|
+
* for example `request.get('/api/cats').fire().abort()`.
|
|
1467
|
+
*/
|
|
1365
1468
|
fire() {
|
|
1366
1469
|
if (!this.__triggered__) {
|
|
1367
1470
|
this.__triggered__ = true;
|
|
1368
1471
|
this.__execute__().then(this.__resolve__, this.__reject__);
|
|
1369
1472
|
}
|
|
1473
|
+
return this;
|
|
1370
1474
|
}
|
|
1371
1475
|
/** @deprecated Await the Keq instance directly or use `.fire()` for fire-and-forget. */
|
|
1372
1476
|
end() {
|
|
@@ -1387,6 +1491,53 @@ var Core = class extends Promise {
|
|
|
1387
1491
|
}
|
|
1388
1492
|
};
|
|
1389
1493
|
//#endregion
|
|
1494
|
+
//#region src/utils/base64.ts
|
|
1495
|
+
const base64Encode = globalThis.btoa || ((str) => Buffer.from(str).toString("base64"));
|
|
1496
|
+
globalThis.atob;
|
|
1497
|
+
//#endregion
|
|
1498
|
+
//#region src/utils/semaphore.ts
|
|
1499
|
+
/**
|
|
1500
|
+
* Concurrency limiter that preserves the caller's async context.
|
|
1501
|
+
*
|
|
1502
|
+
* @description
|
|
1503
|
+
* 并发限流器。等待者 await 属于自己的 Promise,`release()` 仅负责 resolve,
|
|
1504
|
+
* 因此等待者在自己的 async context 中恢复 —— Node 的 AsyncLocalStorage
|
|
1505
|
+
* 上下文得以保留。这是它与 fastq 等「由前一个任务的完成回调驱动下一个任务」
|
|
1506
|
+
* 的队列实现的关键区别。
|
|
1507
|
+
*/
|
|
1508
|
+
var Semaphore = class {
|
|
1509
|
+
constructor(concurrency) {
|
|
1510
|
+
this.concurrency = concurrency;
|
|
1511
|
+
_defineProperty(this, "running", 0);
|
|
1512
|
+
_defineProperty(this, "waiters", []);
|
|
1513
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) throw new TypeError("Semaphore concurrency must be a positive integer");
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* Acquire a slot, waiting until one is released if the limit is reached.
|
|
1517
|
+
*
|
|
1518
|
+
* @description 获取一个名额。若已达并发上限,则等待直到有名额释放。
|
|
1519
|
+
*/
|
|
1520
|
+
async acquire() {
|
|
1521
|
+
if (this.running < this.concurrency) {
|
|
1522
|
+
this.running++;
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
await new Promise((resolve) => {
|
|
1526
|
+
this.waiters.push(resolve);
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* Release a slot, handing it directly to the next waiter when one exists.
|
|
1531
|
+
*
|
|
1532
|
+
* @description 释放名额。存在等待者时直接移交名额(不减计数),否则减少计数。
|
|
1533
|
+
*/
|
|
1534
|
+
release() {
|
|
1535
|
+
const waiter = this.waiters.shift();
|
|
1536
|
+
if (waiter) waiter();
|
|
1537
|
+
else if (this.running > 0) this.running--;
|
|
1538
|
+
}
|
|
1539
|
+
};
|
|
1540
|
+
//#endregion
|
|
1390
1541
|
//#region src/request/keq.ts
|
|
1391
1542
|
/**
|
|
1392
1543
|
* extends Core to provide a fluent API for building HTTP requests
|
|
@@ -1768,10 +1919,14 @@ function keqSerialFlowControlMiddleware() {
|
|
|
1768
1919
|
const key = typeof signal === "string" ? signal : signal(ctx);
|
|
1769
1920
|
if (!ctx.global.core) ctx.global.core = {};
|
|
1770
1921
|
if (!ctx.global.core.serialFlowControl) ctx.global.core.serialFlowControl = {};
|
|
1771
|
-
if (!ctx.global.core.serialFlowControl[key]) ctx.global.core.serialFlowControl[key] =
|
|
1922
|
+
if (!ctx.global.core.serialFlowControl[key]) ctx.global.core.serialFlowControl[key] = new Semaphore(concurrent);
|
|
1923
|
+
const semaphore = ctx.global.core.serialFlowControl[key];
|
|
1924
|
+
await semaphore.acquire();
|
|
1925
|
+
try {
|
|
1772
1926
|
await next();
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1927
|
+
} finally {
|
|
1928
|
+
semaphore.release();
|
|
1929
|
+
}
|
|
1775
1930
|
};
|
|
1776
1931
|
}
|
|
1777
1932
|
//#endregion
|
|
@@ -1987,6 +2142,7 @@ exports.PreconditionFailedException = PreconditionFailedException;
|
|
|
1987
2142
|
exports.ProxyAuthenticationRequiredException = ProxyAuthenticationRequiredException;
|
|
1988
2143
|
exports.RequestException = RequestException;
|
|
1989
2144
|
exports.RequestTimeoutException = RequestTimeoutException;
|
|
2145
|
+
exports.Semaphore = Semaphore;
|
|
1990
2146
|
exports.ServiceUnavailableException = ServiceUnavailableException;
|
|
1991
2147
|
exports.TimeoutException = TimeoutException;
|
|
1992
2148
|
exports.TooManyRequestsException = TooManyRequestsException;
|