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.mjs
CHANGED
|
@@ -6,7 +6,6 @@ import { klona as klona$1 } from "klona/json";
|
|
|
6
6
|
import { EventSourceParserStream } from "eventsource-parser/stream";
|
|
7
7
|
import qs from "qs";
|
|
8
8
|
import picomatch from "picomatch";
|
|
9
|
-
import * as fastq from "fastq";
|
|
10
9
|
//#region src/exception/exception.ts
|
|
11
10
|
var Exception = class extends CustomError {
|
|
12
11
|
constructor(message) {
|
|
@@ -322,10 +321,6 @@ var GatewayTimeoutException = class extends RequestException {
|
|
|
322
321
|
}
|
|
323
322
|
};
|
|
324
323
|
//#endregion
|
|
325
|
-
//#region src/utils/base64.ts
|
|
326
|
-
const base64Encode = globalThis.btoa || ((str) => Buffer.from(str).toString("base64"));
|
|
327
|
-
globalThis.atob;
|
|
328
|
-
//#endregion
|
|
329
324
|
//#region src/validator/validator.ts
|
|
330
325
|
/**
|
|
331
326
|
* Validator 类用于类型检查
|
|
@@ -410,6 +405,13 @@ var Validator = class Validator {
|
|
|
410
405
|
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"]);
|
|
411
406
|
}
|
|
412
407
|
/**
|
|
408
|
+
* 检查是否为 AbortSignal
|
|
409
|
+
*/
|
|
410
|
+
static isAbortSignal(obj) {
|
|
411
|
+
if (typeof AbortSignal !== "undefined" && obj instanceof AbortSignal) return true;
|
|
412
|
+
return Validator.isObject(obj) && typeof obj["aborted"] === "boolean" && Validator.isFunction(obj["addEventListener"]) && Validator.isFunction(obj["removeEventListener"]);
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
413
415
|
* 检查是否为合法的 Header 值
|
|
414
416
|
* 允许范围: HTAB(\t), 0x20-0x7E, 0x80-0xFF
|
|
415
417
|
* 禁止换行等控制字符
|
|
@@ -437,11 +439,6 @@ function shallowClone(obj) {
|
|
|
437
439
|
return obj;
|
|
438
440
|
}
|
|
439
441
|
//#endregion
|
|
440
|
-
//#region src/utils/sleep.ts
|
|
441
|
-
function sleep(ms) {
|
|
442
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
443
|
-
}
|
|
444
|
-
//#endregion
|
|
445
442
|
//#region src/request-init/utils/clone-body.ts
|
|
446
443
|
function cloneBody(obj) {
|
|
447
444
|
if (Validator.isFormData(obj)) {
|
|
@@ -1078,7 +1075,14 @@ function fixContentType(contentType) {
|
|
|
1078
1075
|
function getLocationId(depth = 0) {
|
|
1079
1076
|
const err = /* @__PURE__ */ new Error();
|
|
1080
1077
|
if (!err.stack) return "";
|
|
1081
|
-
|
|
1078
|
+
const lines = err.stack.split("\n");
|
|
1079
|
+
const offset = depth + 2;
|
|
1080
|
+
if (lines[offset]) return lines[offset].trim();
|
|
1081
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1082
|
+
const line = lines[i].trim();
|
|
1083
|
+
if (line) return line;
|
|
1084
|
+
}
|
|
1085
|
+
return "";
|
|
1082
1086
|
}
|
|
1083
1087
|
//#endregion
|
|
1084
1088
|
//#region src/request/utils/resolve-with.ts
|
|
@@ -1200,6 +1204,15 @@ var Core = class extends Promise {
|
|
|
1200
1204
|
_defineProperty(this, "__reject__", void 0);
|
|
1201
1205
|
_defineProperty(this, "__triggered__", false);
|
|
1202
1206
|
_defineProperty(this, "__requestPromise__", void 0);
|
|
1207
|
+
_defineProperty(this, "__aborted__", false);
|
|
1208
|
+
_defineProperty(this, "__abortReason__", void 0);
|
|
1209
|
+
_defineProperty(this, "__activeSharedContext__", void 0);
|
|
1210
|
+
_defineProperty(this, "__resolveAbort__", void 0);
|
|
1211
|
+
_defineProperty(this, "__abortPromise__", new Promise((resolve) => {
|
|
1212
|
+
this.__resolveAbort__ = resolve;
|
|
1213
|
+
}));
|
|
1214
|
+
_defineProperty(this, "__abortWhenCleanups__", []);
|
|
1215
|
+
_defineProperty(this, "__settled__", false);
|
|
1203
1216
|
_defineProperty(this, "requestInit", void 0);
|
|
1204
1217
|
_defineProperty(this, "__listeners__", {});
|
|
1205
1218
|
_defineProperty(this, "__global__", void 0);
|
|
@@ -1242,6 +1255,63 @@ var Core = class extends Promise {
|
|
|
1242
1255
|
for (const [key, value] of Object.entries(opts)) this.__options__[key] = value;
|
|
1243
1256
|
return this;
|
|
1244
1257
|
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Abort the request manually.
|
|
1260
|
+
*
|
|
1261
|
+
* The request rejects with the given reason (an {@link AbortException} by default),
|
|
1262
|
+
* the `abort` event is emitted, and no further retry is performed.
|
|
1263
|
+
* It is safe to call `abort()` before the request is sent: the request will be
|
|
1264
|
+
* aborted as soon as it starts.
|
|
1265
|
+
*
|
|
1266
|
+
* @description 手动中止请求
|
|
1267
|
+
* @param reason Abort reason. Defaults to `new AbortException('Request aborted')`.
|
|
1268
|
+
*/
|
|
1269
|
+
abort(reason = new AbortException("Request aborted")) {
|
|
1270
|
+
if (this.__aborted__) return;
|
|
1271
|
+
this.__aborted__ = true;
|
|
1272
|
+
this.__abortReason__ = reason;
|
|
1273
|
+
this.__resolveAbort__(reason);
|
|
1274
|
+
this.__activeSharedContext__?.request.abort(reason);
|
|
1275
|
+
this.__clearAbortWhenCleanups__();
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Abort the request when the given `AbortSignal` aborts,
|
|
1279
|
+
* or when the given `Promise` settles (resolves or rejects).
|
|
1280
|
+
*
|
|
1281
|
+
* It is designed for the normal `await` flow, so business code
|
|
1282
|
+
* does not need to call `.fire()` first:
|
|
1283
|
+
* `await request.get('/api/cats').abortWhen(signal)`.
|
|
1284
|
+
*
|
|
1285
|
+
* @description 当 signal 中止或 promise 完成时终止请求
|
|
1286
|
+
*/
|
|
1287
|
+
abortWhen(source) {
|
|
1288
|
+
if (this.__aborted__ || this.__settled__) return this;
|
|
1289
|
+
if (Validator.isAbortSignal(source)) {
|
|
1290
|
+
const reason = () => source.reason ?? new AbortException("Request aborted by AbortSignal passed to abortWhen()");
|
|
1291
|
+
if (source.aborted) {
|
|
1292
|
+
this.abort(reason());
|
|
1293
|
+
return this;
|
|
1294
|
+
}
|
|
1295
|
+
const onAbort = () => this.abort(reason());
|
|
1296
|
+
source.addEventListener("abort", onAbort, { once: true });
|
|
1297
|
+
this.__abortWhenCleanups__.push(() => source.removeEventListener("abort", onAbort));
|
|
1298
|
+
return this;
|
|
1299
|
+
}
|
|
1300
|
+
if (Validator.isObject(source) && typeof source.then === "function") {
|
|
1301
|
+
const reason = new AbortException("Request aborted by Promise passed to abortWhen()");
|
|
1302
|
+
let cancelled = false;
|
|
1303
|
+
this.__abortWhenCleanups__.push(() => {
|
|
1304
|
+
cancelled = true;
|
|
1305
|
+
});
|
|
1306
|
+
Promise.resolve(source).then(() => {
|
|
1307
|
+
if (!cancelled) this.abort(reason);
|
|
1308
|
+
}, () => {
|
|
1309
|
+
if (!cancelled) this.abort(reason);
|
|
1310
|
+
});
|
|
1311
|
+
return this;
|
|
1312
|
+
}
|
|
1313
|
+
throw new TypeException("abortWhen() expects an AbortSignal or a Promise");
|
|
1314
|
+
}
|
|
1245
1315
|
derive() {
|
|
1246
1316
|
const derived = new this.constructor(this.requestInit.url, {
|
|
1247
1317
|
method: this.requestInit.method,
|
|
@@ -1278,68 +1348,103 @@ var Core = class extends Promise {
|
|
|
1278
1348
|
const listeners = this.__listeners__[eventName];
|
|
1279
1349
|
for (const listener of listeners) coreContext.emitter.on(eventName, listener);
|
|
1280
1350
|
}
|
|
1351
|
+
this.__activeSharedContext__ = coreContext;
|
|
1352
|
+
if (this.__aborted__) coreContext.request.abort(this.__abortReason__);
|
|
1281
1353
|
return coreContext;
|
|
1282
1354
|
}
|
|
1283
1355
|
async run() {
|
|
1284
1356
|
let attempt;
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
let retryTimes = sharedContext.options.retry?.times || 0;
|
|
1302
|
-
if (retryTimes) {
|
|
1303
|
-
if (retryTimes < 0) retryTimes = 0;
|
|
1304
|
-
else if (!Number.isInteger(retryTimes)) retryTimes = Math.floor(retryTimes);
|
|
1305
|
-
}
|
|
1306
|
-
if (attemptWithDefault >= retryTimes || await retryOn(attemptWithDefault, error, sharedContext) === false) {
|
|
1307
|
-
if (error) {
|
|
1357
|
+
try {
|
|
1358
|
+
while (true) {
|
|
1359
|
+
if (this.__aborted__ && attempt !== void 0) throw this.__abortReason__;
|
|
1360
|
+
const attemptWithDefault = attempt || 0;
|
|
1361
|
+
const sharedContext = this.buildSharedContext();
|
|
1362
|
+
const orchestrator = new KeqMiddlewareOrchestrator(sharedContext, this.__middlewares__);
|
|
1363
|
+
if (attempt !== void 0) sharedContext.data.retry = { attempt };
|
|
1364
|
+
if (attempt && attempt >= 1) sharedContext.emitter.emit("retry", { context: sharedContext });
|
|
1365
|
+
let error = null;
|
|
1366
|
+
try {
|
|
1367
|
+
await orchestrator.execute();
|
|
1368
|
+
} catch (err) {
|
|
1369
|
+
error = err;
|
|
1370
|
+
}
|
|
1371
|
+
if (this.__aborted__) {
|
|
1372
|
+
const reason = error ?? this.__abortReason__;
|
|
1308
1373
|
sharedContext.emitter.emit("error", { context: sharedContext });
|
|
1309
|
-
throw
|
|
1374
|
+
throw reason;
|
|
1375
|
+
}
|
|
1376
|
+
const retryOn = typeof sharedContext.options.retry?.on === "function" ? sharedContext.options.retry.on : (attempt, error) => {
|
|
1377
|
+
if (error instanceof RequestException && error.fatal) return false;
|
|
1378
|
+
return !!error;
|
|
1379
|
+
};
|
|
1380
|
+
let retryTimes = sharedContext.options.retry?.times || 0;
|
|
1381
|
+
if (retryTimes) {
|
|
1382
|
+
if (retryTimes < 0) retryTimes = 0;
|
|
1383
|
+
else if (!Number.isInteger(retryTimes)) retryTimes = Math.floor(retryTimes);
|
|
1384
|
+
}
|
|
1385
|
+
if (attemptWithDefault >= retryTimes || await retryOn(attemptWithDefault, error, sharedContext) === false) {
|
|
1386
|
+
if (error) {
|
|
1387
|
+
sharedContext.emitter.emit("error", { context: sharedContext });
|
|
1388
|
+
throw error;
|
|
1389
|
+
}
|
|
1390
|
+
return sharedContext;
|
|
1310
1391
|
}
|
|
1311
|
-
|
|
1392
|
+
const retryDelay = typeof sharedContext.options.retry?.delay === "function" ? await sharedContext.options.retry.delay(attemptWithDefault, error, sharedContext) : sharedContext.options.retry?.delay || 0;
|
|
1393
|
+
if (retryDelay) await new Promise((resolve) => {
|
|
1394
|
+
const timer = setTimeout(resolve, retryDelay);
|
|
1395
|
+
this.__abortPromise__.then(() => {
|
|
1396
|
+
clearTimeout(timer);
|
|
1397
|
+
resolve();
|
|
1398
|
+
});
|
|
1399
|
+
});
|
|
1400
|
+
attempt = attemptWithDefault + 1;
|
|
1312
1401
|
}
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
attempt = attemptWithDefault + 1;
|
|
1402
|
+
} finally {
|
|
1403
|
+
this.__activeSharedContext__ = void 0;
|
|
1316
1404
|
}
|
|
1317
1405
|
}
|
|
1318
1406
|
__execute__() {
|
|
1319
1407
|
if (!this.__requestPromise__) this.__requestPromise__ = (async () => {
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1408
|
+
try {
|
|
1409
|
+
const coreContext = await this.run();
|
|
1410
|
+
const resolveWithMode = coreContext.options.resolveWith;
|
|
1411
|
+
if (resolveWithMode === "response") return coreContext.response?.clone();
|
|
1412
|
+
const response = coreContext.response;
|
|
1413
|
+
if (!resolveWithMode || resolveWithMode === "intelligent") {
|
|
1414
|
+
const output = coreContext.output;
|
|
1415
|
+
if (output !== void 0) return output;
|
|
1416
|
+
return await intelligentParseResponse(response);
|
|
1417
|
+
}
|
|
1418
|
+
if (!response) throw new Exception([
|
|
1419
|
+
`Unable to process the response with '${resolveWithMode}'. Possible causes:`,
|
|
1420
|
+
"1. The request was never initiated or sent",
|
|
1421
|
+
"2. The request failed before a response was received."
|
|
1422
|
+
].join("\n"));
|
|
1423
|
+
return await resolveWith(response, resolveWithMode);
|
|
1424
|
+
} finally {
|
|
1425
|
+
this.__settled__ = true;
|
|
1426
|
+
this.__clearAbortWhenCleanups__();
|
|
1328
1427
|
}
|
|
1329
|
-
if (!response) throw new Exception([
|
|
1330
|
-
`Unable to process the response with '${resolveWithMode}'. Possible causes:`,
|
|
1331
|
-
"1. The request was never initiated or sent",
|
|
1332
|
-
"2. The request failed before a response was received."
|
|
1333
|
-
].join("\n"));
|
|
1334
|
-
return await resolveWith(response, resolveWithMode);
|
|
1335
1428
|
})();
|
|
1336
1429
|
return this.__requestPromise__;
|
|
1337
1430
|
}
|
|
1431
|
+
__clearAbortWhenCleanups__() {
|
|
1432
|
+
const cleanups = this.__abortWhenCleanups__;
|
|
1433
|
+
this.__abortWhenCleanups__ = [];
|
|
1434
|
+
for (const cleanup of cleanups) cleanup();
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Trigger the request without waiting for the result.
|
|
1438
|
+
*
|
|
1439
|
+
* Returns the request instance itself, so the handler can still be used later,
|
|
1440
|
+
* for example `request.get('/api/cats').fire().abort()`.
|
|
1441
|
+
*/
|
|
1338
1442
|
fire() {
|
|
1339
1443
|
if (!this.__triggered__) {
|
|
1340
1444
|
this.__triggered__ = true;
|
|
1341
1445
|
this.__execute__().then(this.__resolve__, this.__reject__);
|
|
1342
1446
|
}
|
|
1447
|
+
return this;
|
|
1343
1448
|
}
|
|
1344
1449
|
/** @deprecated Await the Keq instance directly or use `.fire()` for fire-and-forget. */
|
|
1345
1450
|
end() {
|
|
@@ -1360,6 +1465,53 @@ var Core = class extends Promise {
|
|
|
1360
1465
|
}
|
|
1361
1466
|
};
|
|
1362
1467
|
//#endregion
|
|
1468
|
+
//#region src/utils/base64.ts
|
|
1469
|
+
const base64Encode = globalThis.btoa || ((str) => Buffer.from(str).toString("base64"));
|
|
1470
|
+
globalThis.atob;
|
|
1471
|
+
//#endregion
|
|
1472
|
+
//#region src/utils/semaphore.ts
|
|
1473
|
+
/**
|
|
1474
|
+
* Concurrency limiter that preserves the caller's async context.
|
|
1475
|
+
*
|
|
1476
|
+
* @description
|
|
1477
|
+
* 并发限流器。等待者 await 属于自己的 Promise,`release()` 仅负责 resolve,
|
|
1478
|
+
* 因此等待者在自己的 async context 中恢复 —— Node 的 AsyncLocalStorage
|
|
1479
|
+
* 上下文得以保留。这是它与 fastq 等「由前一个任务的完成回调驱动下一个任务」
|
|
1480
|
+
* 的队列实现的关键区别。
|
|
1481
|
+
*/
|
|
1482
|
+
var Semaphore = class {
|
|
1483
|
+
constructor(concurrency) {
|
|
1484
|
+
this.concurrency = concurrency;
|
|
1485
|
+
_defineProperty(this, "running", 0);
|
|
1486
|
+
_defineProperty(this, "waiters", []);
|
|
1487
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) throw new TypeError("Semaphore concurrency must be a positive integer");
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Acquire a slot, waiting until one is released if the limit is reached.
|
|
1491
|
+
*
|
|
1492
|
+
* @description 获取一个名额。若已达并发上限,则等待直到有名额释放。
|
|
1493
|
+
*/
|
|
1494
|
+
async acquire() {
|
|
1495
|
+
if (this.running < this.concurrency) {
|
|
1496
|
+
this.running++;
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
await new Promise((resolve) => {
|
|
1500
|
+
this.waiters.push(resolve);
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Release a slot, handing it directly to the next waiter when one exists.
|
|
1505
|
+
*
|
|
1506
|
+
* @description 释放名额。存在等待者时直接移交名额(不减计数),否则减少计数。
|
|
1507
|
+
*/
|
|
1508
|
+
release() {
|
|
1509
|
+
const waiter = this.waiters.shift();
|
|
1510
|
+
if (waiter) waiter();
|
|
1511
|
+
else if (this.running > 0) this.running--;
|
|
1512
|
+
}
|
|
1513
|
+
};
|
|
1514
|
+
//#endregion
|
|
1363
1515
|
//#region src/request/keq.ts
|
|
1364
1516
|
/**
|
|
1365
1517
|
* extends Core to provide a fluent API for building HTTP requests
|
|
@@ -1741,10 +1893,14 @@ function keqSerialFlowControlMiddleware() {
|
|
|
1741
1893
|
const key = typeof signal === "string" ? signal : signal(ctx);
|
|
1742
1894
|
if (!ctx.global.core) ctx.global.core = {};
|
|
1743
1895
|
if (!ctx.global.core.serialFlowControl) ctx.global.core.serialFlowControl = {};
|
|
1744
|
-
if (!ctx.global.core.serialFlowControl[key]) ctx.global.core.serialFlowControl[key] =
|
|
1896
|
+
if (!ctx.global.core.serialFlowControl[key]) ctx.global.core.serialFlowControl[key] = new Semaphore(concurrent);
|
|
1897
|
+
const semaphore = ctx.global.core.serialFlowControl[key];
|
|
1898
|
+
await semaphore.acquire();
|
|
1899
|
+
try {
|
|
1745
1900
|
await next();
|
|
1746
|
-
}
|
|
1747
|
-
|
|
1901
|
+
} finally {
|
|
1902
|
+
semaphore.release();
|
|
1903
|
+
}
|
|
1748
1904
|
};
|
|
1749
1905
|
}
|
|
1750
1906
|
//#endregion
|
|
@@ -1935,6 +2091,6 @@ function createRequest(options) {
|
|
|
1935
2091
|
return new KeqRequest(options);
|
|
1936
2092
|
}
|
|
1937
2093
|
//#endregion
|
|
1938
|
-
export { AbortException, BadGatewayException, BadRequestException, ConflictException, ContentTooLargeException, Exception, ForbiddenException, GatewayTimeoutException, ImATeapotException, InternalServerErrorException, Keq, KeqExecutionContext, KeqMiddlewareApplicator, KeqMiddlewareExecutor, KeqMiddlewareOrchestrator, KeqRequest, KeqSharedContext, MethodNotAllowedException, NotAcceptableException, NotFoundedException, NotImplementedException, PreconditionFailedException, ProxyAuthenticationRequiredException, RequestException, RequestTimeoutException, ServiceUnavailableException, TimeoutException, TooManyRequestsException, TypeException, UnauthorizedException, UnsupportedMediaTypeException, UriTooLongException, composeMiddleware, composeRoute, createProxyResponse, createRequest, request };
|
|
2094
|
+
export { AbortException, BadGatewayException, BadRequestException, ConflictException, ContentTooLargeException, Exception, ForbiddenException, GatewayTimeoutException, ImATeapotException, InternalServerErrorException, Keq, KeqExecutionContext, KeqMiddlewareApplicator, KeqMiddlewareExecutor, KeqMiddlewareOrchestrator, KeqRequest, KeqSharedContext, MethodNotAllowedException, NotAcceptableException, NotFoundedException, NotImplementedException, PreconditionFailedException, ProxyAuthenticationRequiredException, RequestException, RequestTimeoutException, Semaphore, ServiceUnavailableException, TimeoutException, TooManyRequestsException, TypeException, UnauthorizedException, UnsupportedMediaTypeException, UriTooLongException, composeMiddleware, composeRoute, createProxyResponse, createRequest, request };
|
|
1939
2095
|
|
|
1940
2096
|
//# sourceMappingURL=index.mjs.map
|