itd-api 0.9.1 → 0.9.3

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 +283 -90
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +16 -72
  8. package/dist/index.d.ts +16 -72
  9. package/dist/index.js +284 -90
  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-BPxBF0gQ.d.cts} +3 -5
  18. package/dist/shared/{events-D6y0Hxyz.js → events-BzjI6pn5.js} +4 -4
  19. package/dist/shared/{events-D6y0Hxyz.js.map → events-BzjI6pn5.js.map} +1 -1
  20. package/dist/shared/{events-BqFkwwTw.cjs → events-C1FudmSe.cjs} +3 -9
  21. package/dist/shared/{events-BqFkwwTw.cjs.map → events-C1FudmSe.cjs.map} +1 -1
  22. package/dist/shared/{events-BQyH5-S0.d.cts → events-DnwfcLxS.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-BU1_wex8.d.cts → render-C1F_QWYD.d.cts} +82 -19
  36. package/dist/shared/{render-sb1VJhUA.js → render-CtOpp9Pm.js} +127 -124
  37. package/dist/shared/render-CtOpp9Pm.js.map +1 -0
  38. package/dist/shared/{render-DAEeCMk9.cjs → render-Omy1drH1.cjs} +125 -128
  39. package/dist/shared/render-Omy1drH1.cjs.map +1 -0
  40. package/dist/shared/{render-CZ_-F2Tl.d.ts → render-uFW2kDxp.d.ts} +82 -19
  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-Dsr4c5r5.js → url-3Ts6EEHQ.js} +301 -96
  46. package/dist/shared/url-3Ts6EEHQ.js.map +1 -0
  47. package/dist/shared/{url-CzHqE-Fg.cjs → url-D6wsXm1T.cjs} +301 -96
  48. package/dist/shared/url-D6wsXm1T.cjs.map +1 -0
  49. package/dist/shared/{url-CZZcyPI0.d.cts → url-DubjkRR9.d.cts} +10 -16
  50. package/dist/shared/{url-CZZcyPI0.d.ts → url-DubjkRR9.d.ts} +10 -16
  51. package/dist/web/index.cjs +1 -1
  52. package/dist/web/index.js +1 -1
  53. package/package.json +5 -5
  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.3";
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,117 @@ 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 = {
1368
+ recovered: false,
1369
+ preparationErrors: /* @__PURE__ */ new Set()
1370
+ };
1371
+ internal[REQUEST_AUTH_RECOVERY_STATE] = state;
1372
+ return state;
1373
+ }
1374
+ /** Создаёт общую для копий логического запроса отметку вызова `onError`. @internal */
1375
+ function trackRequestErrorObservation(request) {
1376
+ const internal = request;
1377
+ internal[REQUEST_ERROR_OBSERVATION_STATE] ??= {
1378
+ errors: /* @__PURE__ */ new Set(),
1379
+ lifecycleAbortedDuringNotification: false
1380
+ };
1381
+ }
1382
+ /** Отмечает, что ошибка логического запроса уже была передана в `onError`. @internal */
1383
+ function markRequestErrorObserved(request, error) {
1384
+ trackRequestErrorObservation(request);
1385
+ request[REQUEST_ERROR_OBSERVATION_STATE]?.errors.add(error);
1386
+ }
1387
+ /** Была ли конкретная ошибка этой логической операции уже передана в `onError`. @internal */
1388
+ function wasRequestErrorObserved(request, error) {
1389
+ return request[REQUEST_ERROR_OBSERVATION_STATE]?.errors.has(error) ?? false;
1390
+ }
1391
+ /** Отмечает отмену lifecycle во время уже начатого локального `onError`. @internal */
1392
+ function markRequestErrorNotificationAborted(request) {
1393
+ trackRequestErrorObservation(request);
1394
+ const state = request[REQUEST_ERROR_OBSERVATION_STATE];
1395
+ if (state) state.lifecycleAbortedDuringNotification = true;
1396
+ }
1397
+ /** Нужно ли верхней границе не запускать тот же `onError` повторно после отмены. @internal */
1398
+ function wasRequestErrorNotificationAborted(request) {
1399
+ return request[REQUEST_ERROR_OBSERVATION_STATE]?.lifecycleAbortedDuringNotification ?? false;
1400
+ }
1285
1401
  /** Один раз присваивает низкоуровневому запросу семантический ID до входа в middleware. */
1286
1402
  function identifyRequest(request) {
1287
1403
  return request.operationId === void 0 ? {
@@ -1378,10 +1494,20 @@ var HttpClient = class {
1378
1494
  #handler;
1379
1495
  #plugins;
1380
1496
  #baseUrl;
1497
+ #timeout;
1498
+ #clock;
1499
+ #lifetimeSignal;
1500
+ #hooks;
1501
+ #assertActive;
1381
1502
  constructor(deps) {
1382
1503
  this.#handler = deps.handler;
1383
1504
  this.#plugins = deps.plugins;
1384
1505
  this.#baseUrl = deps.baseUrl;
1506
+ this.#timeout = deps.timeout;
1507
+ this.#clock = deps.clock;
1508
+ this.#lifetimeSignal = deps.lifetimeSignal;
1509
+ this.#hooks = deps.hooks;
1510
+ this.#assertActive = deps.assertActive;
1385
1511
  }
1386
1512
  /** Базовый URL, к которому обращается клиент. */
1387
1513
  get baseUrl() {
@@ -1398,7 +1524,7 @@ var HttpClient = class {
1398
1524
  */
1399
1525
  request(options) {
1400
1526
  const request = identifyRequest(options);
1401
- return this.#plugins.run(request, (prepared) => this.#handler(prepared));
1527
+ return this.#runWithLifecycle(request, (prepared) => this.#handler(prepared));
1402
1528
  }
1403
1529
  /** Выполняет контракт операции; `next()` плагина возвращает результат после `read`. */
1404
1530
  execute(operation, options) {
@@ -1411,7 +1537,7 @@ var HttpClient = class {
1411
1537
  method: operation.method,
1412
1538
  retrySafety: options.retrySafety ?? operation.retrySafety
1413
1539
  });
1414
- return this.#plugins.run(request, async (prepared) => operation.read(await this.#handler(prepared), prepared));
1540
+ return this.#runWithLifecycle(request, async (prepared) => operation.read(await this.#handler(prepared), prepared));
1415
1541
  }
1416
1542
  /** Выполняет внутреннюю операцию финализации после начала `ItdClient.dispose()`. @internal */
1417
1543
  cleanupOperation(operation, options) {
@@ -1421,24 +1547,65 @@ var HttpClient = class {
1421
1547
  method: operation.method,
1422
1548
  retrySafety: options.retrySafety ?? operation.retrySafety
1423
1549
  });
1424
- return this.#plugins.run(request, async (prepared) => {
1550
+ return this.#runWithLifecycle(request, async (prepared) => {
1425
1551
  const cleanupRequest = markDisposeCleanupRequest(prepared);
1426
1552
  return operation.read(await this.#handler(cleanupRequest), prepared);
1427
- });
1553
+ }, true);
1554
+ }
1555
+ async #runWithLifecycle(request, execute, allowDisposed = false) {
1556
+ if (!allowDisposed) this.#assertActive?.();
1557
+ const timeout = request.timeout ?? this.#timeout;
1558
+ const scope = createRequestAbortScope(request.signal, this.#lifetimeSignal, timeout, this.#clock);
1559
+ const startedAt = this.#clock.now();
1560
+ trackRequestErrorObservation(request);
1561
+ try {
1562
+ const pending = this.#plugins.run(request, (prepared) => execute({
1563
+ ...prepared,
1564
+ signal: scope.signal,
1565
+ timeout: 0
1566
+ }), scope.signal);
1567
+ return await waitForRequest(Promise.resolve(pending), scope.signal);
1568
+ } catch (error) {
1569
+ const failure = requestAbortError(scope, {
1570
+ timeout,
1571
+ method: request.method,
1572
+ path: request.path
1573
+ }, error);
1574
+ if (!(wasRequestErrorObserved(request, error) || scope.signal.aborted && wasRequestErrorNotificationAborted(request))) {
1575
+ markRequestErrorObserved(request, error);
1576
+ let headers;
1577
+ try {
1578
+ headers = new Headers({
1579
+ ...request.layerHeaders,
1580
+ ...request.headers
1581
+ });
1582
+ } catch {
1583
+ headers = new Headers();
1584
+ }
1585
+ const notification = dispatchRequestHook(this.#hooks, "onError", {
1586
+ operationId: request.operationId,
1587
+ signal: scope.signal,
1588
+ method: request.method.toUpperCase(),
1589
+ path: request.path,
1590
+ url: require_redact.joinUrl(request.baseUrl ?? this.#baseUrl, request.path) + require_redact.buildQuery(request.query),
1591
+ headers,
1592
+ attempt: request.attempt ?? 1,
1593
+ duration: this.#clock.now() - startedAt,
1594
+ error: failure
1595
+ });
1596
+ try {
1597
+ await waitForRequest(notification, scope.signal);
1598
+ } catch (hookError) {
1599
+ if (!scope.signal.aborted) throw hookError;
1600
+ }
1601
+ }
1602
+ throw failure;
1603
+ } finally {
1604
+ scope.cleanup();
1605
+ }
1428
1606
  }
1429
1607
  };
1430
1608
  //#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
1609
  //#region src/core/operation.ts
1443
1610
  /** Семантическая безопасность автоматического повтора операции. */
1444
1611
  const RetrySafety = Object.freeze({
@@ -1638,6 +1805,19 @@ function createServicesMiddleware(registry) {
1638
1805
  return next(prepared);
1639
1806
  };
1640
1807
  }
1808
+ /** Однократная auth-подготовка вне retry исходного запроса. */
1809
+ function createAuthPreflightMiddleware(getAuth) {
1810
+ return async (request, next) => {
1811
+ if (!request.skipAuth) {
1812
+ const auth = getAuth();
1813
+ if (auth.preflight) {
1814
+ const pending = auth.preflight(request.skipAuthRefresh !== true);
1815
+ await (request.signal ? waitForRequest(pending, request.signal) : pending);
1816
+ }
1817
+ }
1818
+ return next(request);
1819
+ };
1820
+ }
1641
1821
  /**
1642
1822
  * Подготавливает auth state до входа транспортной попытки в очередь.
1643
1823
  *
@@ -1646,7 +1826,15 @@ function createServicesMiddleware(registry) {
1646
1826
  */
1647
1827
  function createAuthPreparationMiddleware(auth) {
1648
1828
  return async (request, next) => {
1649
- if (!request.skipAuth) await auth.prepare();
1829
+ if (!request.skipAuth) {
1830
+ const pending = auth.prepare();
1831
+ try {
1832
+ await (request.signal ? waitForRequest(pending, request.signal) : pending);
1833
+ } catch (error) {
1834
+ requestAuthRecoveryState(request).preparationErrors.add(error);
1835
+ throw error;
1836
+ }
1837
+ }
1650
1838
  return next(request);
1651
1839
  };
1652
1840
  }
@@ -1681,11 +1869,15 @@ function createAttemptMiddleware() {
1681
1869
  */
1682
1870
  function createAuthRecoveryMiddleware(auth) {
1683
1871
  return async (request, next) => {
1872
+ const recovery = requestAuthRecoveryState(request);
1684
1873
  try {
1685
1874
  return await next(request);
1686
1875
  } catch (error) {
1687
- if (request.skipAuthRefresh || !require_errors.isItdApiError(error) || error.status !== 401) throw error;
1688
- if (!await auth.recover()) throw error;
1876
+ const preparationFailed = recovery.preparationErrors.delete(error);
1877
+ if (request.skipAuthRefresh || recovery.recovered || preparationFailed || !require_errors.isItdApiError(error) || error.status !== 401) throw error;
1878
+ const pending = auth.recover();
1879
+ if (!await (request.signal ? waitForRequest(pending, request.signal) : pending)) throw error;
1880
+ recovery.recovered = true;
1689
1881
  return next({
1690
1882
  ...request,
1691
1883
  skipAuthRefresh: true
@@ -1740,8 +1932,9 @@ function createRetryMiddleware(deps) {
1740
1932
  else retryAttempt += 1;
1741
1933
  const delay = nextDelay(error, retryAttempt, rateLimitAttempt, trackedRequest, policy, backoff);
1742
1934
  if (delay === void 0) throw error;
1743
- await dispatchRequestHook(deps.hooks, "onRetry", {
1935
+ const notification = dispatchRequestHook(deps.hooks, "onRetry", {
1744
1936
  operationId: request.operationId,
1937
+ signal: request.signal,
1745
1938
  method,
1746
1939
  path: request.path,
1747
1940
  url: deps.buildUrl(request),
@@ -1753,6 +1946,7 @@ function createRetryMiddleware(deps) {
1753
1946
  error,
1754
1947
  delay
1755
1948
  });
1949
+ await (request.signal ? waitForRequest(notification, request.signal) : notification);
1756
1950
  deps.logger?.debug(`повтор ${method} ${request.path}, попытка ${transportAttempt + 1} через ${delay} мс`);
1757
1951
  await sleep(deps.clock ?? systemClock, delay, request.signal);
1758
1952
  }
@@ -2099,39 +2293,6 @@ function abortable(promise, signal) {
2099
2293
  });
2100
2294
  }
2101
2295
  /**
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
2296
  * Единственное место, откуда библиотека ходит в сеть.
2136
2297
  *
2137
2298
  * Отвечает за сборку URL, заголовки, cookie, таймауты, разбор ответа и превращение любой
@@ -2161,44 +2322,58 @@ var Transport = class {
2161
2322
  const request = identifyRequest(input);
2162
2323
  const method = request.method.toUpperCase();
2163
2324
  const url = this.buildUrl(request);
2164
- const headers = await this.#buildHeaders(request, url);
2165
2325
  const attempt = request.attempt ?? 1;
2166
2326
  const timeout = request.timeout ?? this.#config.timeout;
2167
- const abort = createAbortBundle(request.signal, this.#deps.lifetimeSignal, timeout, this.#config.clock);
2327
+ const abort = createRequestAbortScope(request.signal, this.#deps.lifetimeSignal, timeout, this.#config.clock);
2168
2328
  const startedAt = this.#config.clock.now();
2169
2329
  let cleanupBody;
2170
2330
  try {
2331
+ const headers = await abortable(this.#buildHeaders(request, url), abort.signal).catch((error) => {
2332
+ throw this.#toTransportError(error, abort, request, method, timeout);
2333
+ });
2171
2334
  let body;
2172
2335
  try {
2173
2336
  const prepared = await this.#prepareBody(request, headers, abort.signal, attempt);
2174
2337
  body = prepared.body;
2175
2338
  cleanupBody = prepared.cleanup;
2176
2339
  } catch (error) {
2177
- const failure = this.#toTransportError(error, abort, request, method, timeout);
2340
+ 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
2341
  const context = {
2179
2342
  operationId: request.operationId,
2343
+ signal: abort.signal,
2180
2344
  method,
2181
2345
  path: request.path,
2182
2346
  url,
2183
2347
  headers,
2184
2348
  attempt
2185
2349
  };
2186
- await dispatchRequestHook(this.#config.hooks, "onError", {
2350
+ await this.#dispatchErrorHook(request, {
2187
2351
  ...context,
2188
2352
  duration: this.#config.clock.now() - startedAt,
2189
2353
  error: failure
2190
- });
2354
+ }, abort, timeout);
2191
2355
  throw failure;
2192
2356
  }
2193
2357
  const context = {
2194
2358
  operationId: request.operationId,
2359
+ signal: abort.signal,
2195
2360
  method,
2196
2361
  path: request.path,
2197
2362
  url,
2198
2363
  headers,
2199
2364
  attempt
2200
2365
  };
2201
- await dispatchRequestHook(this.#config.hooks, "onRequest", context);
2366
+ try {
2367
+ await abortable(dispatchRequestHook(this.#config.hooks, "onRequest", context), abort.signal);
2368
+ } catch (error) {
2369
+ const failure = abort.signal.aborted ? this.#toTransportError(error, abort, request, method, timeout) : error;
2370
+ await this.#dispatchErrorHook(request, {
2371
+ ...context,
2372
+ duration: this.#config.clock.now() - startedAt,
2373
+ error: failure
2374
+ }, abort, timeout);
2375
+ throw failure;
2376
+ }
2202
2377
  this.#config.logger?.debug(`→ ${method} ${request.path}`, {
2203
2378
  headers: require_redact.redactHeaders(headers),
2204
2379
  body: request.bodyFactory ? "[повторяемое тело]" : require_redact.redactBody(request.body)
@@ -2226,11 +2401,11 @@ var Transport = class {
2226
2401
  });
2227
2402
  } catch (error) {
2228
2403
  const duration = this.#config.clock.now() - startedAt;
2229
- await dispatchRequestHook(this.#config.hooks, "onError", {
2404
+ await this.#dispatchErrorHook(request, {
2230
2405
  ...context,
2231
2406
  duration,
2232
2407
  error
2233
- });
2408
+ }, abort, timeout);
2234
2409
  this.#config.logger?.warn(`× ${method} ${request.path} (${duration} мс): ${error instanceof Error ? error.message : String(error)}`);
2235
2410
  throw error;
2236
2411
  }
@@ -2238,14 +2413,30 @@ var Transport = class {
2238
2413
  const { limit, remaining } = readRateLimit(response.headers);
2239
2414
  this.#deps.onRateLimit(limit, remaining, request);
2240
2415
  }
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);
2416
+ if (this.#config.useCookieJar) (request.cookieJar ?? this.#deps.cookies)?.setFromResponse(response.url || url, response);
2417
+ if (response.ok && hasRequestHook(this.#config.hooks, "onResponse")) {
2418
+ const hookResponse = response.clone();
2419
+ try {
2420
+ await abortable(dispatchRequestHook(this.#config.hooks, "onResponse", {
2421
+ ...context,
2422
+ status: response.status,
2423
+ duration: this.#config.clock.now() - startedAt,
2424
+ response: hookResponse
2425
+ }), abort.signal);
2426
+ } catch (error) {
2427
+ response.body?.cancel().catch(() => {});
2428
+ const failure = abort.signal.aborted ? this.#toTransportError(error, abort, request, method, timeout) : error;
2429
+ await this.#dispatchErrorHook(request, {
2430
+ ...context,
2431
+ duration: this.#config.clock.now() - startedAt,
2432
+ error: failure
2433
+ }, abort, timeout);
2434
+ throw failure;
2435
+ } finally {
2436
+ if (!hookResponse.bodyUsed) hookResponse.body?.cancel().catch(() => {});
2437
+ }
2438
+ }
2439
+ const payload = await this.#readBodyOrFail(response, context, request, method, abort, timeout, startedAt);
2249
2440
  const duration = this.#config.clock.now() - startedAt;
2250
2441
  if (!response.ok) {
2251
2442
  const error = createApiError({
@@ -2258,11 +2449,11 @@ var Transport = class {
2258
2449
  response,
2259
2450
  body: payload
2260
2451
  });
2261
- await dispatchRequestHook(this.#config.hooks, "onError", {
2452
+ await this.#dispatchErrorHook(request, {
2262
2453
  ...context,
2263
2454
  duration,
2264
2455
  error
2265
- });
2456
+ }, abort, timeout);
2266
2457
  this.#config.logger?.warn(`← ${response.status} ${method} ${request.path} (${duration} мс): ${error.message}`);
2267
2458
  throw error;
2268
2459
  }
@@ -2278,6 +2469,21 @@ var Transport = class {
2278
2469
  }
2279
2470
  }
2280
2471
  };
2472
+ async #dispatchErrorHook(request, context, abort, timeout) {
2473
+ markRequestErrorObserved(request, context.error);
2474
+ const onAbort = () => markRequestErrorNotificationAborted(request);
2475
+ if (abort.signal.aborted) onAbort();
2476
+ else abort.signal.addEventListener("abort", onAbort, { once: true });
2477
+ try {
2478
+ await abortable(dispatchRequestHook(this.#config.hooks, "onError", context), abort.signal);
2479
+ } catch (error) {
2480
+ const failure = abort.signal.aborted ? this.#toTransportError(error, abort, request, context.method, timeout) : error;
2481
+ markRequestErrorObserved(request, failure);
2482
+ throw failure;
2483
+ } finally {
2484
+ abort.signal.removeEventListener("abort", onAbort);
2485
+ }
2486
+ }
2281
2487
  /** Подготавливает тело внутри попытки, чтобы поток можно было открыть заново при retry. */
2282
2488
  async #prepareBody(request, headers, signal, attempt) {
2283
2489
  if (request.bodyFactory) {
@@ -2320,18 +2526,17 @@ var Transport = class {
2320
2526
  };
2321
2527
  }
2322
2528
  /** Читает тело и преобразует ошибку чтения в транспортную ошибку библиотеки. */
2323
- async #readBodyOrFail(response, context, request, method, abort, timeout) {
2324
- const startedAt = this.#config.clock.now();
2529
+ async #readBodyOrFail(response, context, request, method, abort, timeout, startedAt) {
2325
2530
  try {
2326
2531
  return await abortable(readBody(response), abort.signal);
2327
2532
  } catch (error) {
2328
2533
  await response.body?.cancel().catch(() => {});
2329
2534
  const failure = this.#toTransportError(error, abort, request, method, timeout);
2330
- await dispatchRequestHook(this.#config.hooks, "onError", {
2535
+ await this.#dispatchErrorHook(request, {
2331
2536
  ...context,
2332
2537
  duration: this.#config.clock.now() - startedAt,
2333
2538
  error: failure
2334
- });
2539
+ }, abort, timeout);
2335
2540
  this.#config.logger?.warn(`× ${method} ${request.path}: не удалось прочитать тело ответа — ${failure.message}`);
2336
2541
  throw failure;
2337
2542
  }
@@ -2349,14 +2554,14 @@ var Transport = class {
2349
2554
  * Собирает общие заголовки клиента: `User-Agent`, идентификатор устройства,
2350
2555
  * заголовки конфигурации и cookie для указанного адреса.
2351
2556
  */
2352
- async platformHeaders(url) {
2557
+ async platformHeaders(url, cookieJar = this.#deps.cookies) {
2353
2558
  const headers = new Headers();
2354
2559
  headers.set("X-Requested-With", "XMLHttpRequest");
2355
2560
  if (this.#config.userAgent) setHeader(headers, "User-Agent", this.#config.userAgent);
2356
2561
  if (this.#deps.getDeviceId) setHeader(headers, "X-Device-Id", await this.#deps.getDeviceId());
2357
2562
  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);
2563
+ if (this.#config.useCookieJar && cookieJar) {
2564
+ const cookie = cookieJar.getHeader(url);
2360
2565
  if (cookie) setHeader(headers, "Cookie", cookie);
2361
2566
  }
2362
2567
  return headers;
@@ -2366,7 +2571,7 @@ var Transport = class {
2366
2571
  * Заголовки вызова применяются последними.
2367
2572
  */
2368
2573
  async #buildHeaders(request, url) {
2369
- const headers = await this.platformHeaders(url);
2574
+ const headers = await this.platformHeaders(url, request.cookieJar);
2370
2575
  if (!headers.has("Accept")) headers.set("Accept", "application/json");
2371
2576
  for (const [name, value] of Object.entries(request.layerHeaders ?? {})) setHeader(headers, name, value);
2372
2577
  for (const [name, value] of Object.entries(request.headers ?? {})) setHeader(headers, name, value);
@@ -2398,6 +2603,7 @@ var Transport = class {
2398
2603
  const ClientRuntimeStage = Object.freeze({
2399
2604
  OperationPlugins: "operation_plugins",
2400
2605
  Services: "services",
2606
+ AuthPreflight: "auth_preflight",
2401
2607
  Retry: "retry",
2402
2608
  AuthRecovery: "auth_recovery",
2403
2609
  AuthPreparation: "auth_preparation",
@@ -2473,6 +2679,10 @@ function createClientRuntime(options, internals) {
2473
2679
  name: ClientRuntimeStage.Services,
2474
2680
  middleware: createServicesMiddleware(services)
2475
2681
  },
2682
+ {
2683
+ name: ClientRuntimeStage.AuthPreflight,
2684
+ middleware: createAuthPreflightMiddleware(() => auth)
2685
+ },
2476
2686
  {
2477
2687
  name: ClientRuntimeStage.Retry,
2478
2688
  middleware: createRetryMiddleware({
@@ -2523,7 +2733,12 @@ function createClientRuntime(options, internals) {
2523
2733
  const http = new HttpClient({
2524
2734
  handler: clientHandler,
2525
2735
  plugins,
2526
- baseUrl: config.baseUrl
2736
+ baseUrl: config.baseUrl,
2737
+ timeout: config.timeout,
2738
+ clock: config.clock,
2739
+ lifetimeSignal: lifetime.signal,
2740
+ hooks: config.hooks,
2741
+ assertActive: () => internals.assertActive?.("выполнить новый запрос")
2527
2742
  });
2528
2743
  auth = internals.auth({
2529
2744
  config,
@@ -2741,11 +2956,6 @@ const OPERATIONS = freezeOperations({
2741
2956
  retrySafety: RetrySafety.Safe,
2742
2957
  bucket: "users"
2743
2958
  },
2744
- "users.search": {
2745
- method: "GET",
2746
- retrySafety: RetrySafety.Safe,
2747
- bucket: "users"
2748
- },
2749
2959
  "users.whoToFollow": {
2750
2960
  method: "GET",
2751
2961
  retrySafety: RetrySafety.Safe,
@@ -2982,11 +3192,6 @@ const OPERATIONS = freezeOperations({
2982
3192
  retrySafety: RetrySafety.Safe,
2983
3193
  bucket: "notifications"
2984
3194
  },
2985
- "hashtags.search": {
2986
- method: "GET",
2987
- retrySafety: RetrySafety.Safe,
2988
- bucket: "hashtags"
2989
- },
2990
3195
  "hashtags.trending": {
2991
3196
  method: "GET",
2992
3197
  retrySafety: RetrySafety.Safe,
@@ -4192,4 +4397,4 @@ Object.defineProperty(exports, "voidResult", {
4192
4397
  }
4193
4398
  });
4194
4399
 
4195
- //# sourceMappingURL=url-CzHqE-Fg.cjs.map
4400
+ //# sourceMappingURL=url-D6wsXm1T.cjs.map