itd-api 0.9.1 → 0.9.2

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.
Files changed (63) hide show
  1. package/dist/events/index.cjs +3 -4
  2. package/dist/events/index.d.cts +3 -3
  3. package/dist/events/index.d.ts +3 -3
  4. package/dist/events/index.js +4 -4
  5. package/dist/index.cjs +198 -39
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +11 -65
  8. package/dist/index.d.ts +11 -65
  9. package/dist/index.js +199 -39
  10. package/dist/index.js.map +1 -1
  11. package/dist/node/index.cjs +3 -3
  12. package/dist/node/index.js +3 -3
  13. package/dist/rest/index.cjs +4 -5
  14. package/dist/rest/index.d.cts +3 -3
  15. package/dist/rest/index.d.ts +3 -3
  16. package/dist/rest/index.js +5 -5
  17. package/dist/shared/{events-BDdJyH2p.d.ts → events-BCn5LRnT.d.cts} +3 -5
  18. package/dist/shared/{events-D6y0Hxyz.js → events-BIBO2Jif.js} +4 -4
  19. package/dist/shared/{events-D6y0Hxyz.js.map → events-BIBO2Jif.js.map} +1 -1
  20. package/dist/shared/{events-BqFkwwTw.cjs → events-BsRm_Fba.cjs} +3 -9
  21. package/dist/shared/{events-BqFkwwTw.cjs.map → events-BsRm_Fba.cjs.map} +1 -1
  22. package/dist/shared/{events-BQyH5-S0.d.cts → events-t1uGgpkf.d.ts} +3 -5
  23. package/dist/shared/{multi-storage-SoW05uzO.js → multi-storage-C3Ngtop1.js} +13 -8
  24. package/dist/shared/multi-storage-C3Ngtop1.js.map +1 -0
  25. package/dist/shared/{multi-storage-CDKLa956.cjs → multi-storage-s-fPtjoy.cjs} +13 -8
  26. package/dist/shared/multi-storage-s-fPtjoy.cjs.map +1 -0
  27. package/dist/shared/{options-DOJYtoti.cjs → options-DjEtbVaf.cjs} +2 -2
  28. package/dist/shared/{options-DOJYtoti.cjs.map → options-DjEtbVaf.cjs.map} +1 -1
  29. package/dist/shared/{options-DhgGg7Ms.js → options-i4LrKfqx.js} +2 -2
  30. package/dist/shared/{options-DhgGg7Ms.js.map → options-i4LrKfqx.js.map} +1 -1
  31. package/dist/shared/{redact-Ba5iSVYZ.js → redact-DJV0XM3n.js} +40 -15
  32. package/dist/shared/redact-DJV0XM3n.js.map +1 -0
  33. package/dist/shared/{redact-BP00URmQ.cjs → redact-lAmck1Z6.cjs} +45 -14
  34. package/dist/shared/redact-lAmck1Z6.cjs.map +1 -0
  35. package/dist/shared/{render-sb1VJhUA.js → render--ZXmYEf-.js} +127 -96
  36. package/dist/shared/render--ZXmYEf-.js.map +1 -0
  37. package/dist/shared/{render-CZ_-F2Tl.d.ts → render-BdnBV-aT.d.ts} +82 -7
  38. package/dist/shared/{render-BU1_wex8.d.cts → render-DA83gS0f.d.cts} +82 -7
  39. package/dist/shared/{render-DAEeCMk9.cjs → render-DqVzx68r.cjs} +125 -100
  40. package/dist/shared/render-DqVzx68r.cjs.map +1 -0
  41. package/dist/shared/{storage-QNVxzpDB.cjs → storage-CZJZoaz9.cjs} +4 -2
  42. package/dist/shared/storage-CZJZoaz9.cjs.map +1 -0
  43. package/dist/shared/{storage-CfHKzVHf.js → storage-DiDe0PZM.js} +4 -2
  44. package/dist/shared/storage-DiDe0PZM.js.map +1 -0
  45. package/dist/shared/{url-CZZcyPI0.d.ts → url-Dbj6V6fk.d.cts} +4 -2
  46. package/dist/shared/{url-CZZcyPI0.d.cts → url-Dbj6V6fk.d.ts} +4 -2
  47. package/dist/shared/{url-CzHqE-Fg.cjs → url-Doc0XXW_.cjs} +274 -86
  48. package/dist/shared/url-Doc0XXW_.cjs.map +1 -0
  49. package/dist/shared/{url-Dsr4c5r5.js → url-i2f-X1hX.js} +274 -86
  50. package/dist/shared/url-i2f-X1hX.js.map +1 -0
  51. package/dist/web/index.cjs +1 -1
  52. package/dist/web/index.js +1 -1
  53. package/package.json +4 -4
  54. package/dist/shared/multi-storage-CDKLa956.cjs.map +0 -1
  55. package/dist/shared/multi-storage-SoW05uzO.js.map +0 -1
  56. package/dist/shared/redact-BP00URmQ.cjs.map +0 -1
  57. package/dist/shared/redact-Ba5iSVYZ.js.map +0 -1
  58. package/dist/shared/render-DAEeCMk9.cjs.map +0 -1
  59. package/dist/shared/render-sb1VJhUA.js.map +0 -1
  60. package/dist/shared/storage-CfHKzVHf.js.map +0 -1
  61. package/dist/shared/storage-QNVxzpDB.cjs.map +0 -1
  62. package/dist/shared/url-CzHqE-Fg.cjs.map +0 -1
  63. package/dist/shared/url-Dsr4c5r5.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  const require_errors = require("./errors-BmP3TKoW.cjs");
2
- const require_redact = require("./redact-BP00URmQ.cjs");
2
+ const require_redact = require("./redact-lAmck1Z6.cjs");
3
3
  //#region src/core/async-dispose.ts
4
4
  /**
5
5
  * Подставляет метод `await using` там, где `Symbol.asyncDispose` отсутствует.
@@ -85,7 +85,7 @@ function requireOptionalBoolean(value, name) {
85
85
  //#endregion
86
86
  //#region src/core/version.ts
87
87
  /** Версия библиотеки. Попадает в `User-Agent`. */
88
- const LIBRARY_VERSION = "0.9.1";
88
+ const LIBRARY_VERSION = "0.9.2";
89
89
  //#endregion
90
90
  //#region src/core/config.ts
91
91
  /** Базовый URL API итд.com. Домен записан в punycode: `итд.com`. */
@@ -386,6 +386,12 @@ const RELATION_FIELDS = [
386
386
  "before",
387
387
  "after"
388
388
  ];
389
+ const PLUGIN_NAME_PART = "[a-z][a-z0-9]*(?:[-_.][a-z0-9]+)*";
390
+ const PLUGIN_NAME_PATTERN = new RegExp(`^(?:${PLUGIN_NAME_PART}|@${PLUGIN_NAME_PART}(?:/${PLUGIN_NAME_PART})?(?::${PLUGIN_NAME_PART})?)$`);
391
+ const PLUGIN_NAME_FORMAT = "name, @package[:addon] или @scope/package[:addon] с разделителями -, _ и .";
392
+ function isPluginName(value) {
393
+ return PLUGIN_NAME_PATTERN.test(value);
394
+ }
389
395
  function validateNameList(plugin, field) {
390
396
  const values = plugin[field];
391
397
  if (values === void 0) return;
@@ -393,6 +399,7 @@ function validateNameList(plugin, field) {
393
399
  const seen = /* @__PURE__ */ new Set();
394
400
  for (const value of values) {
395
401
  if (typeof value !== "string" || value.trim() === "") throw new require_errors.ItdConfigError(`плагин «${plugin.name}»: ${field} содержит пустое имя плагина`);
402
+ if (!isPluginName(value)) throw new require_errors.ItdConfigError(`плагин «${plugin.name}»: имя «${value}» в ${field} должно соответствовать ${PLUGIN_NAME_FORMAT}`);
396
403
  if (value === plugin.name) throw new require_errors.ItdConfigError(`плагин «${plugin.name}» не может указать себя в ${field}`);
397
404
  if (seen.has(value)) throw new require_errors.ItdConfigError(`плагин «${plugin.name}»: ${field} повторяет имя «${value}»`);
398
405
  seen.add(value);
@@ -411,6 +418,7 @@ function validatePluginDefinition(plugin) {
411
418
  if (typeof plugin?.install !== "function") throw new require_errors.ItdConfigError("Плагин должен быть объектом с методом install()");
412
419
  const name = plugin.name;
413
420
  if (typeof name !== "string" || name.trim() === "") throw new require_errors.ItdConfigError("У плагина должно быть непустое имя");
421
+ if (!isPluginName(name)) throw new require_errors.ItdConfigError(`Имя плагина «${name}» должно соответствовать ${PLUGIN_NAME_FORMAT}`);
414
422
  for (const field of RELATION_FIELDS) validateNameList(plugin, field);
415
423
  }
416
424
  function addEdge(from, to, edges, indegree) {
@@ -649,7 +657,7 @@ var PluginRegistry = class {
649
657
  *
650
658
  * @param execute выполнение логической операции, вызываемое самым внутренним transformer
651
659
  */
652
- async run(request, execute) {
660
+ async run(request, execute, signal) {
653
661
  const ordered = this.#ordered;
654
662
  if (ordered.length === 0) return execute(request);
655
663
  const entries = [...ordered];
@@ -661,6 +669,7 @@ var PluginRegistry = class {
661
669
  const operationId = request.operationId;
662
670
  const method = request.method.toUpperCase();
663
671
  const retrySafety = request.retrySafety;
672
+ const context = Object.freeze({ signal });
664
673
  const scoped = (current) => {
665
674
  const prepared = current.retrySafety === void 0 ? {
666
675
  ...current,
@@ -683,7 +692,7 @@ var PluginRegistry = class {
683
692
  called = true;
684
693
  validateContract(prepared);
685
694
  return next(scoped(prepared));
686
- });
695
+ }, context);
687
696
  }, (current) => {
688
697
  validateContract(current);
689
698
  return execute(scoped(current));
@@ -1067,8 +1076,8 @@ function seedLimit(bucket, options) {
1067
1076
  * Очереди по парам «направление — серверный счётчик частоты».
1068
1077
  *
1069
1078
  * Направление — origin уже разрешённого URL: разные локальные имена одного хоста делят
1070
- * лимит, а запрос с разовым внешним `baseUrl` не попадает в основную очередь. Мощность
1071
- * карты ограничена каталогом операций, поэтому ни TTL, ни вытеснение не нужны.
1079
+ * лимит, а запрос с разовым внешним `baseUrl` не попадает в основную очередь. Созданные
1080
+ * очереди сохраняются до `clear()`/`dispose()`, чтобы не терять известное состояние лимитов.
1072
1081
  *
1073
1082
  * @internal
1074
1083
  */
@@ -1278,10 +1287,114 @@ var ServiceRegistry = class {
1278
1287
  }
1279
1288
  };
1280
1289
  //#endregion
1290
+ //#region src/core/plugins/hooks.ts
1291
+ /** Проверяет наличие публичного hook, не создавая дорогой контекст. @internal */
1292
+ function hasRequestHook(hooks, field) {
1293
+ return hooks[field] !== void 0;
1294
+ }
1295
+ /** Последовательно вызывает публичный hook конструктора. @internal */
1296
+ async function dispatchRequestHook(hooks, field, context) {
1297
+ const hook = hooks[field];
1298
+ await hook?.(context);
1299
+ }
1300
+ //#endregion
1301
+ //#region src/core/execution/lifecycle.ts
1302
+ /** Объединяет отмену вызова, завершение клиента и общий таймаут логической операции. @internal */
1303
+ function createRequestAbortScope(signal, lifetimeSignal, timeout, clock) {
1304
+ const controller = new AbortController();
1305
+ let timeoutReached = false;
1306
+ const link = (source) => {
1307
+ if (!source) return void 0;
1308
+ if (source.aborted) {
1309
+ controller.abort(source.reason);
1310
+ return;
1311
+ }
1312
+ const onAbort = () => controller.abort(source.reason);
1313
+ source.addEventListener("abort", onAbort, { once: true });
1314
+ return () => source.removeEventListener("abort", onAbort);
1315
+ };
1316
+ const detach = [link(signal), link(lifetimeSignal)];
1317
+ const cancelTimer = timeout > 0 ? clock.schedule(() => {
1318
+ timeoutReached = true;
1319
+ controller.abort();
1320
+ }, timeout) : void 0;
1321
+ return {
1322
+ signal: controller.signal,
1323
+ timedOut: () => timeoutReached,
1324
+ cleanup: () => {
1325
+ cancelTimer?.();
1326
+ for (const remove of detach) remove?.();
1327
+ }
1328
+ };
1329
+ }
1330
+ /** Прерывает только ожидание; исходный промис может корректно завершить общую фоновую работу. @internal */
1331
+ function waitForRequest(promise, signal) {
1332
+ if (signal.aborted) {
1333
+ promise.catch(() => {});
1334
+ return Promise.reject(new require_errors.ItdAbortError(void 0, { cause: signal.reason }));
1335
+ }
1336
+ let onAbort;
1337
+ const aborted = new Promise((_resolve, reject) => {
1338
+ onAbort = () => reject(new require_errors.ItdAbortError(void 0, { cause: signal.reason }));
1339
+ signal.addEventListener("abort", onAbort, { once: true });
1340
+ });
1341
+ return Promise.race([promise, aborted]).finally(() => {
1342
+ if (onAbort) signal.removeEventListener("abort", onAbort);
1343
+ });
1344
+ }
1345
+ /** Возвращает публичную ошибку, соответствующую причине остановки операции. @internal */
1346
+ function requestAbortError(scope, request, fallback) {
1347
+ if (scope.timedOut()) return new require_errors.ItdTimeoutError({
1348
+ timeout: request.timeout,
1349
+ method: request.method.toUpperCase(),
1350
+ path: request.path
1351
+ });
1352
+ if (scope.signal.aborted && !(fallback instanceof require_errors.ItdAbortError)) return new require_errors.ItdAbortError(`Запрос ${request.method.toUpperCase()} ${request.path} отменён`, { cause: scope.signal.reason });
1353
+ return fallback;
1354
+ }
1355
+ //#endregion
1281
1356
  //#region src/core/execution/pipeline.ts
1282
1357
  const REQUEST_ATTEMPT_STATE = Symbol("itd-api.request-attempt-state");
1283
1358
  const REQUEST_QUEUE_KEY = Symbol("itd-api.request-queue-key");
1359
+ const REQUEST_AUTH_RECOVERY_STATE = Symbol("itd-api.request-auth-recovery-state");
1360
+ const REQUEST_ERROR_OBSERVATION_STATE = Symbol("itd-api.request-error-observation-state");
1284
1361
  const DISPOSE_CLEANUP_REQUEST = Symbol("itd-api.dispose-cleanup-request");
1362
+ /** Возвращает общее для всех retry состояние восстановления авторизации. @internal */
1363
+ function requestAuthRecoveryState(request) {
1364
+ const internal = request;
1365
+ const current = internal[REQUEST_AUTH_RECOVERY_STATE];
1366
+ if (current) return current;
1367
+ const state = { recovered: false };
1368
+ internal[REQUEST_AUTH_RECOVERY_STATE] = state;
1369
+ return state;
1370
+ }
1371
+ /** Создаёт общую для копий логического запроса отметку вызова `onError`. @internal */
1372
+ function trackRequestErrorObservation(request) {
1373
+ const internal = request;
1374
+ internal[REQUEST_ERROR_OBSERVATION_STATE] ??= {
1375
+ errors: /* @__PURE__ */ new Set(),
1376
+ lifecycleAbortedDuringNotification: false
1377
+ };
1378
+ }
1379
+ /** Отмечает, что ошибка логического запроса уже была передана в `onError`. @internal */
1380
+ function markRequestErrorObserved(request, error) {
1381
+ trackRequestErrorObservation(request);
1382
+ request[REQUEST_ERROR_OBSERVATION_STATE]?.errors.add(error);
1383
+ }
1384
+ /** Была ли конкретная ошибка этой логической операции уже передана в `onError`. @internal */
1385
+ function wasRequestErrorObserved(request, error) {
1386
+ return request[REQUEST_ERROR_OBSERVATION_STATE]?.errors.has(error) ?? false;
1387
+ }
1388
+ /** Отмечает отмену lifecycle во время уже начатого локального `onError`. @internal */
1389
+ function markRequestErrorNotificationAborted(request) {
1390
+ trackRequestErrorObservation(request);
1391
+ const state = request[REQUEST_ERROR_OBSERVATION_STATE];
1392
+ if (state) state.lifecycleAbortedDuringNotification = true;
1393
+ }
1394
+ /** Нужно ли верхней границе не запускать тот же `onError` повторно после отмены. @internal */
1395
+ function wasRequestErrorNotificationAborted(request) {
1396
+ return request[REQUEST_ERROR_OBSERVATION_STATE]?.lifecycleAbortedDuringNotification ?? false;
1397
+ }
1285
1398
  /** Один раз присваивает низкоуровневому запросу семантический ID до входа в middleware. */
1286
1399
  function identifyRequest(request) {
1287
1400
  return request.operationId === void 0 ? {
@@ -1378,10 +1491,20 @@ var HttpClient = class {
1378
1491
  #handler;
1379
1492
  #plugins;
1380
1493
  #baseUrl;
1494
+ #timeout;
1495
+ #clock;
1496
+ #lifetimeSignal;
1497
+ #hooks;
1498
+ #assertActive;
1381
1499
  constructor(deps) {
1382
1500
  this.#handler = deps.handler;
1383
1501
  this.#plugins = deps.plugins;
1384
1502
  this.#baseUrl = deps.baseUrl;
1503
+ this.#timeout = deps.timeout;
1504
+ this.#clock = deps.clock;
1505
+ this.#lifetimeSignal = deps.lifetimeSignal;
1506
+ this.#hooks = deps.hooks;
1507
+ this.#assertActive = deps.assertActive;
1385
1508
  }
1386
1509
  /** Базовый URL, к которому обращается клиент. */
1387
1510
  get baseUrl() {
@@ -1398,7 +1521,7 @@ var HttpClient = class {
1398
1521
  */
1399
1522
  request(options) {
1400
1523
  const request = identifyRequest(options);
1401
- return this.#plugins.run(request, (prepared) => this.#handler(prepared));
1524
+ return this.#runWithLifecycle(request, (prepared) => this.#handler(prepared));
1402
1525
  }
1403
1526
  /** Выполняет контракт операции; `next()` плагина возвращает результат после `read`. */
1404
1527
  execute(operation, options) {
@@ -1411,7 +1534,7 @@ var HttpClient = class {
1411
1534
  method: operation.method,
1412
1535
  retrySafety: options.retrySafety ?? operation.retrySafety
1413
1536
  });
1414
- return this.#plugins.run(request, async (prepared) => operation.read(await this.#handler(prepared), prepared));
1537
+ return this.#runWithLifecycle(request, async (prepared) => operation.read(await this.#handler(prepared), prepared));
1415
1538
  }
1416
1539
  /** Выполняет внутреннюю операцию финализации после начала `ItdClient.dispose()`. @internal */
1417
1540
  cleanupOperation(operation, options) {
@@ -1421,24 +1544,65 @@ var HttpClient = class {
1421
1544
  method: operation.method,
1422
1545
  retrySafety: options.retrySafety ?? operation.retrySafety
1423
1546
  });
1424
- return this.#plugins.run(request, async (prepared) => {
1547
+ return this.#runWithLifecycle(request, async (prepared) => {
1425
1548
  const cleanupRequest = markDisposeCleanupRequest(prepared);
1426
1549
  return operation.read(await this.#handler(cleanupRequest), prepared);
1427
- });
1550
+ }, true);
1551
+ }
1552
+ async #runWithLifecycle(request, execute, allowDisposed = false) {
1553
+ if (!allowDisposed) this.#assertActive?.();
1554
+ const timeout = request.timeout ?? this.#timeout;
1555
+ const scope = createRequestAbortScope(request.signal, this.#lifetimeSignal, timeout, this.#clock);
1556
+ const startedAt = this.#clock.now();
1557
+ trackRequestErrorObservation(request);
1558
+ try {
1559
+ const pending = this.#plugins.run(request, (prepared) => execute({
1560
+ ...prepared,
1561
+ signal: scope.signal,
1562
+ timeout: 0
1563
+ }), scope.signal);
1564
+ return await waitForRequest(Promise.resolve(pending), scope.signal);
1565
+ } catch (error) {
1566
+ const failure = requestAbortError(scope, {
1567
+ timeout,
1568
+ method: request.method,
1569
+ path: request.path
1570
+ }, error);
1571
+ if (!(wasRequestErrorObserved(request, error) || scope.signal.aborted && wasRequestErrorNotificationAborted(request))) {
1572
+ markRequestErrorObserved(request, error);
1573
+ let headers;
1574
+ try {
1575
+ headers = new Headers({
1576
+ ...request.layerHeaders,
1577
+ ...request.headers
1578
+ });
1579
+ } catch {
1580
+ headers = new Headers();
1581
+ }
1582
+ const notification = dispatchRequestHook(this.#hooks, "onError", {
1583
+ operationId: request.operationId,
1584
+ signal: scope.signal,
1585
+ method: request.method.toUpperCase(),
1586
+ path: request.path,
1587
+ url: require_redact.joinUrl(request.baseUrl ?? this.#baseUrl, request.path) + require_redact.buildQuery(request.query),
1588
+ headers,
1589
+ attempt: request.attempt ?? 1,
1590
+ duration: this.#clock.now() - startedAt,
1591
+ error: failure
1592
+ });
1593
+ try {
1594
+ await waitForRequest(notification, scope.signal);
1595
+ } catch (hookError) {
1596
+ if (!scope.signal.aborted) throw hookError;
1597
+ }
1598
+ }
1599
+ throw failure;
1600
+ } finally {
1601
+ scope.cleanup();
1602
+ }
1428
1603
  }
1429
1604
  };
1430
1605
  //#endregion
1431
- //#region src/core/plugins/hooks.ts
1432
- /** Проверяет наличие публичного hook, не создавая дорогой контекст. @internal */
1433
- function hasRequestHook(hooks, field) {
1434
- return hooks[field] !== void 0;
1435
- }
1436
- /** Последовательно вызывает публичный hook конструктора. @internal */
1437
- async function dispatchRequestHook(hooks, field, context) {
1438
- const hook = hooks[field];
1439
- await hook?.(context);
1440
- }
1441
- //#endregion
1442
1606
  //#region src/core/operation.ts
1443
1607
  /** Семантическая безопасность автоматического повтора операции. */
1444
1608
  const RetrySafety = Object.freeze({
@@ -1646,7 +1810,10 @@ function createServicesMiddleware(registry) {
1646
1810
  */
1647
1811
  function createAuthPreparationMiddleware(auth) {
1648
1812
  return async (request, next) => {
1649
- if (!request.skipAuth) await auth.prepare();
1813
+ if (!request.skipAuth) {
1814
+ const pending = auth.prepare();
1815
+ await (request.signal ? waitForRequest(pending, request.signal) : pending);
1816
+ }
1650
1817
  return next(request);
1651
1818
  };
1652
1819
  }
@@ -1681,11 +1848,14 @@ function createAttemptMiddleware() {
1681
1848
  */
1682
1849
  function createAuthRecoveryMiddleware(auth) {
1683
1850
  return async (request, next) => {
1851
+ const recovery = requestAuthRecoveryState(request);
1684
1852
  try {
1685
1853
  return await next(request);
1686
1854
  } catch (error) {
1687
- if (request.skipAuthRefresh || !require_errors.isItdApiError(error) || error.status !== 401) throw error;
1688
- if (!await auth.recover()) throw error;
1855
+ if (request.skipAuthRefresh || recovery.recovered || !require_errors.isItdApiError(error) || error.status !== 401) throw error;
1856
+ const pending = auth.recover();
1857
+ if (!await (request.signal ? waitForRequest(pending, request.signal) : pending)) throw error;
1858
+ recovery.recovered = true;
1689
1859
  return next({
1690
1860
  ...request,
1691
1861
  skipAuthRefresh: true
@@ -1740,8 +1910,9 @@ function createRetryMiddleware(deps) {
1740
1910
  else retryAttempt += 1;
1741
1911
  const delay = nextDelay(error, retryAttempt, rateLimitAttempt, trackedRequest, policy, backoff);
1742
1912
  if (delay === void 0) throw error;
1743
- await dispatchRequestHook(deps.hooks, "onRetry", {
1913
+ const notification = dispatchRequestHook(deps.hooks, "onRetry", {
1744
1914
  operationId: request.operationId,
1915
+ signal: request.signal,
1745
1916
  method,
1746
1917
  path: request.path,
1747
1918
  url: deps.buildUrl(request),
@@ -1753,6 +1924,7 @@ function createRetryMiddleware(deps) {
1753
1924
  error,
1754
1925
  delay
1755
1926
  });
1927
+ await (request.signal ? waitForRequest(notification, request.signal) : notification);
1756
1928
  deps.logger?.debug(`повтор ${method} ${request.path}, попытка ${transportAttempt + 1} через ${delay} мс`);
1757
1929
  await sleep(deps.clock ?? systemClock, delay, request.signal);
1758
1930
  }
@@ -2099,39 +2271,6 @@ function abortable(promise, signal) {
2099
2271
  });
2100
2272
  }
2101
2273
  /**
2102
- * Объединяет пользовательский `AbortSignal`, сигнал жизни клиента и таймаут.
2103
- *
2104
- * Реализовано вручную, а не через `AbortSignal.any`: последний появился только в Node 20,
2105
- * а библиотека поддерживает Node 18. Причина отмены переносится от источника как есть.
2106
- */
2107
- function createAbortBundle(userSignal, lifetimeSignal, timeout, clock) {
2108
- const controller = new AbortController();
2109
- let timedOut = false;
2110
- const link = (source) => {
2111
- if (!source) return void 0;
2112
- if (source.aborted) {
2113
- controller.abort(source.reason);
2114
- return;
2115
- }
2116
- const onAbort = () => controller.abort(source.reason);
2117
- source.addEventListener("abort", onAbort, { once: true });
2118
- return () => source.removeEventListener("abort", onAbort);
2119
- };
2120
- const unlink = [link(userSignal), link(lifetimeSignal)];
2121
- const cancelTimer = timeout > 0 ? clock.schedule(() => {
2122
- timedOut = true;
2123
- controller.abort();
2124
- }, timeout) : void 0;
2125
- return {
2126
- signal: controller.signal,
2127
- timedOut: () => timedOut,
2128
- cleanup: () => {
2129
- cancelTimer?.();
2130
- for (const detach of unlink) detach?.();
2131
- }
2132
- };
2133
- }
2134
- /**
2135
2274
  * Единственное место, откуда библиотека ходит в сеть.
2136
2275
  *
2137
2276
  * Отвечает за сборку URL, заголовки, cookie, таймауты, разбор ответа и превращение любой
@@ -2161,44 +2300,58 @@ var Transport = class {
2161
2300
  const request = identifyRequest(input);
2162
2301
  const method = request.method.toUpperCase();
2163
2302
  const url = this.buildUrl(request);
2164
- const headers = await this.#buildHeaders(request, url);
2165
2303
  const attempt = request.attempt ?? 1;
2166
2304
  const timeout = request.timeout ?? this.#config.timeout;
2167
- const abort = createAbortBundle(request.signal, this.#deps.lifetimeSignal, timeout, this.#config.clock);
2305
+ const abort = createRequestAbortScope(request.signal, this.#deps.lifetimeSignal, timeout, this.#config.clock);
2168
2306
  const startedAt = this.#config.clock.now();
2169
2307
  let cleanupBody;
2170
2308
  try {
2309
+ const headers = await abortable(this.#buildHeaders(request, url), abort.signal).catch((error) => {
2310
+ throw this.#toTransportError(error, abort, request, method, timeout);
2311
+ });
2171
2312
  let body;
2172
2313
  try {
2173
2314
  const prepared = await this.#prepareBody(request, headers, abort.signal, attempt);
2174
2315
  body = prepared.body;
2175
2316
  cleanupBody = prepared.cleanup;
2176
2317
  } catch (error) {
2177
- const failure = this.#toTransportError(error, abort, request, method, timeout);
2318
+ const failure = abort.signal.aborted || error instanceof require_errors.ItdError ? this.#toTransportError(error, abort, request, method, timeout) : new require_errors.ItdConfigError(`Не удалось подготовить тело ${method} ${request.path}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
2178
2319
  const context = {
2179
2320
  operationId: request.operationId,
2321
+ signal: abort.signal,
2180
2322
  method,
2181
2323
  path: request.path,
2182
2324
  url,
2183
2325
  headers,
2184
2326
  attempt
2185
2327
  };
2186
- await dispatchRequestHook(this.#config.hooks, "onError", {
2328
+ await this.#dispatchErrorHook(request, {
2187
2329
  ...context,
2188
2330
  duration: this.#config.clock.now() - startedAt,
2189
2331
  error: failure
2190
- });
2332
+ }, abort, timeout);
2191
2333
  throw failure;
2192
2334
  }
2193
2335
  const context = {
2194
2336
  operationId: request.operationId,
2337
+ signal: abort.signal,
2195
2338
  method,
2196
2339
  path: request.path,
2197
2340
  url,
2198
2341
  headers,
2199
2342
  attempt
2200
2343
  };
2201
- await dispatchRequestHook(this.#config.hooks, "onRequest", context);
2344
+ try {
2345
+ await abortable(dispatchRequestHook(this.#config.hooks, "onRequest", context), abort.signal);
2346
+ } catch (error) {
2347
+ const failure = abort.signal.aborted ? this.#toTransportError(error, abort, request, method, timeout) : error;
2348
+ await this.#dispatchErrorHook(request, {
2349
+ ...context,
2350
+ duration: this.#config.clock.now() - startedAt,
2351
+ error: failure
2352
+ }, abort, timeout);
2353
+ throw failure;
2354
+ }
2202
2355
  this.#config.logger?.debug(`→ ${method} ${request.path}`, {
2203
2356
  headers: require_redact.redactHeaders(headers),
2204
2357
  body: request.bodyFactory ? "[повторяемое тело]" : require_redact.redactBody(request.body)
@@ -2226,11 +2379,11 @@ var Transport = class {
2226
2379
  });
2227
2380
  } catch (error) {
2228
2381
  const duration = this.#config.clock.now() - startedAt;
2229
- await dispatchRequestHook(this.#config.hooks, "onError", {
2382
+ await this.#dispatchErrorHook(request, {
2230
2383
  ...context,
2231
2384
  duration,
2232
2385
  error
2233
- });
2386
+ }, abort, timeout);
2234
2387
  this.#config.logger?.warn(`× ${method} ${request.path} (${duration} мс): ${error instanceof Error ? error.message : String(error)}`);
2235
2388
  throw error;
2236
2389
  }
@@ -2238,14 +2391,30 @@ var Transport = class {
2238
2391
  const { limit, remaining } = readRateLimit(response.headers);
2239
2392
  this.#deps.onRateLimit(limit, remaining, request);
2240
2393
  }
2241
- if (this.#config.useCookieJar) this.#deps.cookies?.setFromResponse(url, response);
2242
- if (response.ok && hasRequestHook(this.#config.hooks, "onResponse")) await dispatchRequestHook(this.#config.hooks, "onResponse", {
2243
- ...context,
2244
- status: response.status,
2245
- duration: this.#config.clock.now() - startedAt,
2246
- response: response.clone()
2247
- });
2248
- const payload = await this.#readBodyOrFail(response, context, request, method, abort, timeout);
2394
+ if (this.#config.useCookieJar) (request.cookieJar ?? this.#deps.cookies)?.setFromResponse(response.url || url, response);
2395
+ if (response.ok && hasRequestHook(this.#config.hooks, "onResponse")) {
2396
+ const hookResponse = response.clone();
2397
+ try {
2398
+ await abortable(dispatchRequestHook(this.#config.hooks, "onResponse", {
2399
+ ...context,
2400
+ status: response.status,
2401
+ duration: this.#config.clock.now() - startedAt,
2402
+ response: hookResponse
2403
+ }), abort.signal);
2404
+ } catch (error) {
2405
+ response.body?.cancel().catch(() => {});
2406
+ const failure = abort.signal.aborted ? this.#toTransportError(error, abort, request, method, timeout) : error;
2407
+ await this.#dispatchErrorHook(request, {
2408
+ ...context,
2409
+ duration: this.#config.clock.now() - startedAt,
2410
+ error: failure
2411
+ }, abort, timeout);
2412
+ throw failure;
2413
+ } finally {
2414
+ if (!hookResponse.bodyUsed) hookResponse.body?.cancel().catch(() => {});
2415
+ }
2416
+ }
2417
+ const payload = await this.#readBodyOrFail(response, context, request, method, abort, timeout, startedAt);
2249
2418
  const duration = this.#config.clock.now() - startedAt;
2250
2419
  if (!response.ok) {
2251
2420
  const error = createApiError({
@@ -2258,11 +2427,11 @@ var Transport = class {
2258
2427
  response,
2259
2428
  body: payload
2260
2429
  });
2261
- await dispatchRequestHook(this.#config.hooks, "onError", {
2430
+ await this.#dispatchErrorHook(request, {
2262
2431
  ...context,
2263
2432
  duration,
2264
2433
  error
2265
- });
2434
+ }, abort, timeout);
2266
2435
  this.#config.logger?.warn(`← ${response.status} ${method} ${request.path} (${duration} мс): ${error.message}`);
2267
2436
  throw error;
2268
2437
  }
@@ -2278,6 +2447,21 @@ var Transport = class {
2278
2447
  }
2279
2448
  }
2280
2449
  };
2450
+ async #dispatchErrorHook(request, context, abort, timeout) {
2451
+ markRequestErrorObserved(request, context.error);
2452
+ const onAbort = () => markRequestErrorNotificationAborted(request);
2453
+ if (abort.signal.aborted) onAbort();
2454
+ else abort.signal.addEventListener("abort", onAbort, { once: true });
2455
+ try {
2456
+ await abortable(dispatchRequestHook(this.#config.hooks, "onError", context), abort.signal);
2457
+ } catch (error) {
2458
+ const failure = abort.signal.aborted ? this.#toTransportError(error, abort, request, context.method, timeout) : error;
2459
+ markRequestErrorObserved(request, failure);
2460
+ throw failure;
2461
+ } finally {
2462
+ abort.signal.removeEventListener("abort", onAbort);
2463
+ }
2464
+ }
2281
2465
  /** Подготавливает тело внутри попытки, чтобы поток можно было открыть заново при retry. */
2282
2466
  async #prepareBody(request, headers, signal, attempt) {
2283
2467
  if (request.bodyFactory) {
@@ -2320,18 +2504,17 @@ var Transport = class {
2320
2504
  };
2321
2505
  }
2322
2506
  /** Читает тело и преобразует ошибку чтения в транспортную ошибку библиотеки. */
2323
- async #readBodyOrFail(response, context, request, method, abort, timeout) {
2324
- const startedAt = this.#config.clock.now();
2507
+ async #readBodyOrFail(response, context, request, method, abort, timeout, startedAt) {
2325
2508
  try {
2326
2509
  return await abortable(readBody(response), abort.signal);
2327
2510
  } catch (error) {
2328
2511
  await response.body?.cancel().catch(() => {});
2329
2512
  const failure = this.#toTransportError(error, abort, request, method, timeout);
2330
- await dispatchRequestHook(this.#config.hooks, "onError", {
2513
+ await this.#dispatchErrorHook(request, {
2331
2514
  ...context,
2332
2515
  duration: this.#config.clock.now() - startedAt,
2333
2516
  error: failure
2334
- });
2517
+ }, abort, timeout);
2335
2518
  this.#config.logger?.warn(`× ${method} ${request.path}: не удалось прочитать тело ответа — ${failure.message}`);
2336
2519
  throw failure;
2337
2520
  }
@@ -2349,14 +2532,14 @@ var Transport = class {
2349
2532
  * Собирает общие заголовки клиента: `User-Agent`, идентификатор устройства,
2350
2533
  * заголовки конфигурации и cookie для указанного адреса.
2351
2534
  */
2352
- async platformHeaders(url) {
2535
+ async platformHeaders(url, cookieJar = this.#deps.cookies) {
2353
2536
  const headers = new Headers();
2354
2537
  headers.set("X-Requested-With", "XMLHttpRequest");
2355
2538
  if (this.#config.userAgent) setHeader(headers, "User-Agent", this.#config.userAgent);
2356
2539
  if (this.#deps.getDeviceId) setHeader(headers, "X-Device-Id", await this.#deps.getDeviceId());
2357
2540
  for (const [name, value] of Object.entries(this.#config.headers)) setHeader(headers, name, value);
2358
- if (this.#config.useCookieJar && this.#deps.cookies) {
2359
- const cookie = this.#deps.cookies.getHeader(url);
2541
+ if (this.#config.useCookieJar && cookieJar) {
2542
+ const cookie = cookieJar.getHeader(url);
2360
2543
  if (cookie) setHeader(headers, "Cookie", cookie);
2361
2544
  }
2362
2545
  return headers;
@@ -2366,7 +2549,7 @@ var Transport = class {
2366
2549
  * Заголовки вызова применяются последними.
2367
2550
  */
2368
2551
  async #buildHeaders(request, url) {
2369
- const headers = await this.platformHeaders(url);
2552
+ const headers = await this.platformHeaders(url, request.cookieJar);
2370
2553
  if (!headers.has("Accept")) headers.set("Accept", "application/json");
2371
2554
  for (const [name, value] of Object.entries(request.layerHeaders ?? {})) setHeader(headers, name, value);
2372
2555
  for (const [name, value] of Object.entries(request.headers ?? {})) setHeader(headers, name, value);
@@ -2523,7 +2706,12 @@ function createClientRuntime(options, internals) {
2523
2706
  const http = new HttpClient({
2524
2707
  handler: clientHandler,
2525
2708
  plugins,
2526
- baseUrl: config.baseUrl
2709
+ baseUrl: config.baseUrl,
2710
+ timeout: config.timeout,
2711
+ clock: config.clock,
2712
+ lifetimeSignal: lifetime.signal,
2713
+ hooks: config.hooks,
2714
+ assertActive: () => internals.assertActive?.("выполнить новый запрос")
2527
2715
  });
2528
2716
  auth = internals.auth({
2529
2717
  config,
@@ -4192,4 +4380,4 @@ Object.defineProperty(exports, "voidResult", {
4192
4380
  }
4193
4381
  });
4194
4382
 
4195
- //# sourceMappingURL=url-CzHqE-Fg.cjs.map
4383
+ //# sourceMappingURL=url-Doc0XXW_.cjs.map