itd-api 0.9.0 → 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.
- package/README.md +4 -4
- package/dist/events/index.cjs +3 -4
- package/dist/events/index.d.cts +3 -3
- package/dist/events/index.d.ts +3 -3
- package/dist/events/index.js +4 -4
- package/dist/index.cjs +198 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -65
- package/dist/index.d.ts +11 -65
- package/dist/index.js +199 -39
- package/dist/index.js.map +1 -1
- package/dist/node/index.cjs +3 -3
- package/dist/node/index.js +3 -3
- package/dist/rest/index.cjs +4 -5
- package/dist/rest/index.d.cts +3 -3
- package/dist/rest/index.d.ts +3 -3
- package/dist/rest/index.js +5 -5
- package/dist/shared/{events-CqtkP65d.d.ts → events-BCn5LRnT.d.cts} +3 -5
- package/dist/shared/{events-C5wnTXPT.js → events-BIBO2Jif.js} +4 -4
- package/dist/shared/{events-C5wnTXPT.js.map → events-BIBO2Jif.js.map} +1 -1
- package/dist/shared/{events-FZfnEez0.cjs → events-BsRm_Fba.cjs} +3 -9
- package/dist/shared/{events-FZfnEez0.cjs.map → events-BsRm_Fba.cjs.map} +1 -1
- package/dist/shared/{events-DGAWlCaI.d.cts → events-t1uGgpkf.d.ts} +3 -5
- package/dist/shared/{multi-storage-SoW05uzO.js → multi-storage-C3Ngtop1.js} +13 -8
- package/dist/shared/multi-storage-C3Ngtop1.js.map +1 -0
- package/dist/shared/{multi-storage-CDKLa956.cjs → multi-storage-s-fPtjoy.cjs} +13 -8
- package/dist/shared/multi-storage-s-fPtjoy.cjs.map +1 -0
- package/dist/shared/{options-DOJYtoti.cjs → options-DjEtbVaf.cjs} +2 -2
- package/dist/shared/{options-DOJYtoti.cjs.map → options-DjEtbVaf.cjs.map} +1 -1
- package/dist/shared/{options-DhgGg7Ms.js → options-i4LrKfqx.js} +2 -2
- package/dist/shared/{options-DhgGg7Ms.js.map → options-i4LrKfqx.js.map} +1 -1
- package/dist/shared/{redact-Ba5iSVYZ.js → redact-DJV0XM3n.js} +40 -15
- package/dist/shared/redact-DJV0XM3n.js.map +1 -0
- package/dist/shared/{redact-BP00URmQ.cjs → redact-lAmck1Z6.cjs} +45 -14
- package/dist/shared/redact-lAmck1Z6.cjs.map +1 -0
- package/dist/shared/{render-hiWFh2s4.js → render--ZXmYEf-.js} +131 -100
- package/dist/shared/render--ZXmYEf-.js.map +1 -0
- package/dist/shared/{render-B4qis7Sb.d.ts → render-BdnBV-aT.d.ts} +112 -16
- package/dist/shared/{render-DDwy2Ame.d.cts → render-DA83gS0f.d.cts} +112 -16
- package/dist/shared/{render-Dqi8Owqk.cjs → render-DqVzx68r.cjs} +129 -104
- package/dist/shared/render-DqVzx68r.cjs.map +1 -0
- package/dist/shared/{storage-QNVxzpDB.cjs → storage-CZJZoaz9.cjs} +4 -2
- package/dist/shared/storage-CZJZoaz9.cjs.map +1 -0
- package/dist/shared/{storage-CfHKzVHf.js → storage-DiDe0PZM.js} +4 -2
- package/dist/shared/storage-DiDe0PZM.js.map +1 -0
- package/dist/shared/{url-XBsMdAcv.d.ts → url-Dbj6V6fk.d.cts} +21 -5
- package/dist/shared/{url-XBsMdAcv.d.cts → url-Dbj6V6fk.d.ts} +21 -5
- package/dist/shared/{url-Bsb3xE5U.cjs → url-Doc0XXW_.cjs} +274 -86
- package/dist/shared/url-Doc0XXW_.cjs.map +1 -0
- package/dist/shared/{url-DP33mp3s.js → url-i2f-X1hX.js} +274 -86
- package/dist/shared/url-i2f-X1hX.js.map +1 -0
- package/dist/web/index.cjs +1 -1
- package/dist/web/index.js +1 -1
- package/package.json +4 -4
- package/dist/shared/multi-storage-CDKLa956.cjs.map +0 -1
- package/dist/shared/multi-storage-SoW05uzO.js.map +0 -1
- package/dist/shared/redact-BP00URmQ.cjs.map +0 -1
- package/dist/shared/redact-Ba5iSVYZ.js.map +0 -1
- package/dist/shared/render-Dqi8Owqk.cjs.map +0 -1
- package/dist/shared/render-hiWFh2s4.js.map +0 -1
- package/dist/shared/storage-CfHKzVHf.js.map +0 -1
- package/dist/shared/storage-QNVxzpDB.cjs.map +0 -1
- package/dist/shared/url-Bsb3xE5U.cjs.map +0 -1
- package/dist/shared/url-DP33mp3s.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { O as isItdRateLimitError, _ as ItdStateError, a as ItdConfigError, b as isItdApiError, d as ItdForbiddenError, f as ItdNetworkError, g as ItdServerError, h as ItdRateLimitError, i as ItdAuthError, l as ItdFileError, m as ItdPhoneVerificationError, n as ItdApiError, o as ItdConflictError, p as ItdNotFoundError, s as ItdError, t as ItdAbortError, v as ItdTimeoutError, y as ItdValidationError } from "./errors-GI10kZxk.js";
|
|
2
|
-
import { C as
|
|
2
|
+
import { C as shouldSendCredentials, S as resolveFetch, _ as originOf, b as isBlob, d as buildQuery, g as normalizeBaseUrl, h as joinUrl, i as redactHeaders, m as isSameSite, n as redactBody, p as hostOf, s as CookieJar, v as RuntimeMode, w as shouldUseCookieJar, y as createDeviceId } from "./redact-DJV0XM3n.js";
|
|
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.
|
|
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 ItdConfigError(`плагин «${plugin.name}»: ${field} содержит пустое имя плагина`);
|
|
402
|
+
if (!isPluginName(value)) throw new ItdConfigError(`плагин «${plugin.name}»: имя «${value}» в ${field} должно соответствовать ${PLUGIN_NAME_FORMAT}`);
|
|
396
403
|
if (value === plugin.name) throw new ItdConfigError(`плагин «${plugin.name}» не может указать себя в ${field}`);
|
|
397
404
|
if (seen.has(value)) throw new 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 ItdConfigError("Плагин должен быть объектом с методом install()");
|
|
412
419
|
const name = plugin.name;
|
|
413
420
|
if (typeof name !== "string" || name.trim() === "") throw new ItdConfigError("У плагина должно быть непустое имя");
|
|
421
|
+
if (!isPluginName(name)) throw new 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
|
-
*
|
|
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 ItdAbortError(void 0, { cause: signal.reason }));
|
|
1335
|
+
}
|
|
1336
|
+
let onAbort;
|
|
1337
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
1338
|
+
onAbort = () => reject(new 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 ItdTimeoutError({
|
|
1348
|
+
timeout: request.timeout,
|
|
1349
|
+
method: request.method.toUpperCase(),
|
|
1350
|
+
path: request.path
|
|
1351
|
+
});
|
|
1352
|
+
if (scope.signal.aborted && !(fallback instanceof ItdAbortError)) return new 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.#
|
|
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.#
|
|
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.#
|
|
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: joinUrl(request.baseUrl ?? this.#baseUrl, request.path) + 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)
|
|
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 || !isItdApiError(error) || error.status !== 401) throw error;
|
|
1688
|
-
|
|
1855
|
+
if (request.skipAuthRefresh || recovery.recovered || !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
|
-
|
|
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 =
|
|
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 ItdError ? this.#toTransportError(error, abort, request, method, timeout) : new 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
|
|
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
|
-
|
|
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: redactHeaders(headers),
|
|
2204
2357
|
body: request.bodyFactory ? "[повторяемое тело]" : 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
|
|
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"))
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
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
|
|
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
|
|
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 &&
|
|
2359
|
-
const cookie =
|
|
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,
|
|
@@ -3809,4 +3997,4 @@ function resolveNotificationUrl(notification) {
|
|
|
3809
3997
|
//#endregion
|
|
3810
3998
|
export { orderPluginDefinitions as $, ITD_CATALOG as A, isRecord as B, ReportReason as C, ViewReason as D, SpanType as E, operationMethod as F, pickString as G, pickBoolean as H, operationRetrySafety as I, identityResult as J, RetrySafety as K, BUCKET_LIMITS as L, defineBuiltInOperation as M, isBuiltInOperationId as N, ViewSource as O, operationBucket as P, assertPluginRemovable as Q, DEFAULT_RATE_LIMIT_BUCKET as R, NotificationType as S, ServiceState as T, pickNumber as U, pickArray as V, pickObject as W, mergeService as X, voidResult as Y, RequestQueuePool as Z, FeedTab as _, tokenProvider as a, isRecord$1 as at, ItdErrorCode as b, normalizeNotification as c, createDeadline as ct, canonicalNotificationType as d, DEFAULT_BASE_URL as et, isKnownNotificationType as f, EventChannelStatus as g, CommentSort as h, bearerToken as i, LIBRARY_VERSION as it, OPERATIONS as j, WallAccess as k, readNotificationEvent as l, systemClock as lt, AttachmentType as m, formatNotificationText as n, STATUS_SERVICE as nt, passthroughOperation as o, requireOptionalBoolean as ot, AccessType as p, defineOperation as q, anonymousAuth as r, resolveRateLimit as rt, voidOperation as s, RateLimitPacing as st, resolveNotificationUrl as t, DEFAULT_STATUS_BASE_URL as tt, readUnreadCountEvent as u, installAsyncDisposeFallback as ut, IncidentKind as v, ReportTargetType as w, LikesVisibility as x, InteractionType as y, createClientRuntime as z };
|
|
3811
3999
|
|
|
3812
|
-
//# sourceMappingURL=url-
|
|
4000
|
+
//# sourceMappingURL=url-i2f-X1hX.js.map
|