keq 5.2.2 → 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/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)) {
@@ -1207,6 +1204,15 @@ var Core = class extends Promise {
1207
1204
  _defineProperty(this, "__reject__", void 0);
1208
1205
  _defineProperty(this, "__triggered__", false);
1209
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);
1210
1216
  _defineProperty(this, "requestInit", void 0);
1211
1217
  _defineProperty(this, "__listeners__", {});
1212
1218
  _defineProperty(this, "__global__", void 0);
@@ -1249,6 +1255,63 @@ var Core = class extends Promise {
1249
1255
  for (const [key, value] of Object.entries(opts)) this.__options__[key] = value;
1250
1256
  return this;
1251
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
+ }
1252
1315
  derive() {
1253
1316
  const derived = new this.constructor(this.requestInit.url, {
1254
1317
  method: this.requestInit.method,
@@ -1285,68 +1348,103 @@ var Core = class extends Promise {
1285
1348
  const listeners = this.__listeners__[eventName];
1286
1349
  for (const listener of listeners) coreContext.emitter.on(eventName, listener);
1287
1350
  }
1351
+ this.__activeSharedContext__ = coreContext;
1352
+ if (this.__aborted__) coreContext.request.abort(this.__abortReason__);
1288
1353
  return coreContext;
1289
1354
  }
1290
1355
  async run() {
1291
1356
  let attempt;
1292
- while (true) {
1293
- const attemptWithDefault = attempt || 0;
1294
- const sharedContext = this.buildSharedContext();
1295
- const orchestrator = new KeqMiddlewareOrchestrator(sharedContext, this.__middlewares__);
1296
- if (attempt !== void 0) sharedContext.data.retry = { attempt };
1297
- if (attempt && attempt >= 1) sharedContext.emitter.emit("retry", { context: sharedContext });
1298
- let error = null;
1299
- try {
1300
- await orchestrator.execute();
1301
- } catch (err) {
1302
- error = err;
1303
- }
1304
- const retryOn = typeof sharedContext.options.retry?.on === "function" ? sharedContext.options.retry.on : (attempt, error) => {
1305
- if (error instanceof RequestException && error.fatal) return false;
1306
- return !!error;
1307
- };
1308
- let retryTimes = sharedContext.options.retry?.times || 0;
1309
- if (retryTimes) {
1310
- if (retryTimes < 0) retryTimes = 0;
1311
- else if (!Number.isInteger(retryTimes)) retryTimes = Math.floor(retryTimes);
1312
- }
1313
- if (attemptWithDefault >= retryTimes || await retryOn(attemptWithDefault, error, sharedContext) === false) {
1314
- 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__;
1315
1373
  sharedContext.emitter.emit("error", { context: sharedContext });
1316
- throw error;
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;
1317
1391
  }
1318
- return sharedContext;
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;
1319
1401
  }
1320
- const retryDelay = typeof sharedContext.options.retry?.delay === "function" ? await sharedContext.options.retry.delay(attemptWithDefault, error, sharedContext) : sharedContext.options.retry?.delay || 0;
1321
- if (retryDelay) await sleep(retryDelay);
1322
- attempt = attemptWithDefault + 1;
1402
+ } finally {
1403
+ this.__activeSharedContext__ = void 0;
1323
1404
  }
1324
1405
  }
1325
1406
  __execute__() {
1326
1407
  if (!this.__requestPromise__) this.__requestPromise__ = (async () => {
1327
- const coreContext = await this.run();
1328
- const resolveWithMode = coreContext.options.resolveWith;
1329
- if (resolveWithMode === "response") return coreContext.response?.clone();
1330
- const response = coreContext.response;
1331
- if (!resolveWithMode || resolveWithMode === "intelligent") {
1332
- const output = coreContext.output;
1333
- if (output !== void 0) return output;
1334
- return await intelligentParseResponse(response);
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__();
1335
1427
  }
1336
- if (!response) throw new Exception([
1337
- `Unable to process the response with '${resolveWithMode}'. Possible causes:`,
1338
- "1. The request was never initiated or sent",
1339
- "2. The request failed before a response was received."
1340
- ].join("\n"));
1341
- return await resolveWith(response, resolveWithMode);
1342
1428
  })();
1343
1429
  return this.__requestPromise__;
1344
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
+ */
1345
1442
  fire() {
1346
1443
  if (!this.__triggered__) {
1347
1444
  this.__triggered__ = true;
1348
1445
  this.__execute__().then(this.__resolve__, this.__reject__);
1349
1446
  }
1447
+ return this;
1350
1448
  }
1351
1449
  /** @deprecated Await the Keq instance directly or use `.fire()` for fire-and-forget. */
1352
1450
  end() {
@@ -1367,6 +1465,53 @@ var Core = class extends Promise {
1367
1465
  }
1368
1466
  };
1369
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
1370
1515
  //#region src/request/keq.ts
1371
1516
  /**
1372
1517
  * extends Core to provide a fluent API for building HTTP requests
@@ -1748,10 +1893,14 @@ function keqSerialFlowControlMiddleware() {
1748
1893
  const key = typeof signal === "string" ? signal : signal(ctx);
1749
1894
  if (!ctx.global.core) ctx.global.core = {};
1750
1895
  if (!ctx.global.core.serialFlowControl) ctx.global.core.serialFlowControl = {};
1751
- if (!ctx.global.core.serialFlowControl[key]) ctx.global.core.serialFlowControl[key] = fastq.promise(async ({ next }) => {
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 {
1752
1900
  await next();
1753
- }, concurrent);
1754
- await ctx.global.core.serialFlowControl[key].push({ next });
1901
+ } finally {
1902
+ semaphore.release();
1903
+ }
1755
1904
  };
1756
1905
  }
1757
1906
  //#endregion
@@ -1942,6 +2091,6 @@ function createRequest(options) {
1942
2091
  return new KeqRequest(options);
1943
2092
  }
1944
2093
  //#endregion
1945
- 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 };
1946
2095
 
1947
2096
  //# sourceMappingURL=index.mjs.map