itd-api 0.10.3 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/events/index.cjs +27 -25
- package/dist/events/index.d.cts +3 -3
- package/dist/events/index.d.ts +3 -3
- package/dist/events/index.js +5 -5
- package/dist/index.cjs +136 -116
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -7
- package/dist/index.d.ts +18 -7
- package/dist/index.js +46 -30
- package/dist/index.js.map +1 -1
- package/dist/node/index.cjs +13 -13
- package/dist/node/index.cjs.map +1 -1
- package/dist/node/index.js +5 -5
- package/dist/rest/index.cjs +43 -40
- package/dist/rest/index.cjs.map +1 -1
- package/dist/rest/index.d.cts +3 -3
- package/dist/rest/index.d.ts +3 -3
- package/dist/rest/index.js +6 -6
- package/dist/shared/{events-BVfqWyBw.cjs → events-C6tmj8aW.cjs} +63 -59
- package/dist/shared/events-C6tmj8aW.cjs.map +1 -0
- package/dist/shared/{events-Cp7ITueA.d.ts → events-CAb9G7Sf.d.ts} +2 -2
- package/dist/shared/{events-Cwj7UPAa.js → events-MWNcnfrq.js} +19 -15
- package/dist/shared/events-MWNcnfrq.js.map +1 -0
- package/dist/shared/{events-D0Wtt-If.d.cts → events-Sc75x6rT.d.cts} +2 -2
- package/dist/shared/{key-value-store-B5GYVEYZ.cjs → key-value-store-DYSPsqwq.cjs} +11 -14
- package/dist/shared/key-value-store-DYSPsqwq.cjs.map +1 -0
- package/dist/shared/{key-value-store-Bt2PSMxY.js → key-value-store-w1Dn5Pjb.js} +5 -8
- package/dist/shared/key-value-store-w1Dn5Pjb.js.map +1 -0
- package/dist/shared/{multi-storage-C3Ngtop1.js → multi-storage-BLBrGN9Q.js} +6 -6
- package/dist/shared/multi-storage-BLBrGN9Q.js.map +1 -0
- package/dist/shared/{multi-storage-s-fPtjoy.cjs → multi-storage-C7-Gz6Z7.cjs} +8 -8
- package/dist/shared/multi-storage-C7-Gz6Z7.cjs.map +1 -0
- package/dist/shared/{options-DjEtbVaf.cjs → options-5tywTYId.cjs} +13 -13
- package/dist/shared/{options-DjEtbVaf.cjs.map → options-5tywTYId.cjs.map} +1 -1
- package/dist/shared/{options-i4LrKfqx.js → options-BerKicCs.js} +3 -3
- package/dist/shared/{options-i4LrKfqx.js.map → options-BerKicCs.js.map} +1 -1
- package/dist/shared/{redact-DJV0XM3n.js → redact-C5oxIvcc.js} +79 -18
- package/dist/shared/redact-C5oxIvcc.js.map +1 -0
- package/dist/shared/{redact-lAmck1Z6.cjs → redact-DoC7lSsp.cjs} +109 -24
- package/dist/shared/redact-DoC7lSsp.cjs.map +1 -0
- package/dist/shared/{render-DRmuHoEy.d.ts → render-CBroJPcV.d.ts} +178 -252
- package/dist/shared/{render-CJO_L2CD.d.cts → render-DM7AhTZW.d.cts} +178 -252
- package/dist/shared/{render-BT74nC1l.js → render-DRh5_aRe.js} +128 -135
- package/dist/shared/render-DRh5_aRe.js.map +1 -0
- package/dist/shared/{render-CYJSDypQ.cjs → render-Dnc84B4M.cjs} +234 -241
- package/dist/shared/render-Dnc84B4M.cjs.map +1 -0
- package/dist/shared/{storage-DiDe0PZM.js → storage-86wiHeXD.js} +3 -3
- package/dist/shared/{storage-DiDe0PZM.js.map → storage-86wiHeXD.js.map} +1 -1
- package/dist/shared/{storage-CZJZoaz9.cjs → storage-CIjejZDt.cjs} +4 -4
- package/dist/shared/{storage-CZJZoaz9.cjs.map → storage-CIjejZDt.cjs.map} +1 -1
- package/dist/shared/{url-BLnX-LYt.d.cts → url-BOi-_mJT.d.cts} +143 -79
- package/dist/shared/{url-BLnX-LYt.d.ts → url-BOi-_mJT.d.ts} +143 -79
- package/dist/shared/{url-DS0-lrl-.js → url-Bg61sk_Y.js} +741 -598
- package/dist/shared/url-Bg61sk_Y.js.map +1 -0
- package/dist/shared/{url-BnO1aWsd.cjs → url-DTgkCWv5.cjs} +848 -717
- package/dist/shared/url-DTgkCWv5.cjs.map +1 -0
- package/dist/shared/{errors-BmP3TKoW.cjs → validate-CFx2dISs.cjs} +84 -5
- package/dist/shared/validate-CFx2dISs.cjs.map +1 -0
- package/dist/shared/{errors-GI10kZxk.js → validate-D7_kq3g5.js} +55 -6
- package/dist/shared/validate-D7_kq3g5.js.map +1 -0
- package/dist/web/index.cjs +4 -4
- package/dist/web/index.cjs.map +1 -1
- package/dist/web/index.js +3 -3
- package/package.json +1 -1
- package/dist/shared/errors-BmP3TKoW.cjs.map +0 -1
- package/dist/shared/errors-GI10kZxk.js.map +0 -1
- package/dist/shared/events-BVfqWyBw.cjs.map +0 -1
- package/dist/shared/events-Cwj7UPAa.js.map +0 -1
- package/dist/shared/key-value-store-B5GYVEYZ.cjs.map +0 -1
- package/dist/shared/key-value-store-Bt2PSMxY.js.map +0 -1
- package/dist/shared/multi-storage-C3Ngtop1.js.map +0 -1
- package/dist/shared/multi-storage-s-fPtjoy.cjs.map +0 -1
- package/dist/shared/redact-DJV0XM3n.js.map +0 -1
- package/dist/shared/redact-lAmck1Z6.cjs.map +0 -1
- package/dist/shared/render-BT74nC1l.js.map +0 -1
- package/dist/shared/render-CYJSDypQ.cjs.map +0 -1
- package/dist/shared/url-BnO1aWsd.cjs.map +0 -1
- package/dist/shared/url-DS0-lrl-.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
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-
|
|
1
|
+
import { C as ItdValidationError, N as isItdRateLimitError, S as ItdTimeoutError, T as isItdApiError, _ as ItdNotFoundError, a as ItdAbortError, b as ItdServerError, c as ItdAuthError, d as ItdError, g as ItdNetworkError, h as ItdForbiddenError, i as requireOptionalBoolean, l as ItdConfigError, n as isRecord, o as ItdApiError, p as ItdFileError, r as requireNonNegative, t as isObjectLike, u as ItdConflictError, v as ItdPhoneVerificationError, w as TimeoutBudget, x as ItdStateError, y as ItdRateLimitError } from "./validate-D7_kq3g5.js";
|
|
2
|
+
import { C as shouldSendCredentials, E as LogLevel, O as isLevelEnabled, S as resolveFetch, _ as originOf, b as isBlob, d as buildQuery, g as normalizeBaseUrl, h as joinUrl, i as redactHeaders, k as resolveLogger, m as isSameSite, n as redactBody, p as hostOf, s as CookieJar, v as RuntimeMode, w as shouldUseCookieJar, y as createDeviceId } from "./redact-C5oxIvcc.js";
|
|
3
3
|
//#region src/core/async-dispose.ts
|
|
4
4
|
/**
|
|
5
5
|
* Подставляет метод `await using` там, где `Symbol.asyncDispose` отсутствует.
|
|
@@ -62,30 +62,9 @@ const RateLimitPacing = Object.freeze({
|
|
|
62
62
|
Off: "off"
|
|
63
63
|
});
|
|
64
64
|
//#endregion
|
|
65
|
-
//#region src/core/validate.ts
|
|
66
|
-
/**
|
|
67
|
-
* Проверки, общие для резолверов конфигурации.
|
|
68
|
-
*
|
|
69
|
-
* Настройки исполнения и настройки сессии разбираются в разных модулях, но сообщать
|
|
70
|
-
* об ошибке должны одинаково: пользователю всё равно, какой слой отверг его значение.
|
|
71
|
-
*/
|
|
72
|
-
/** Похоже ли значение на объект настроек, а не на массив или `null`. */
|
|
73
|
-
function isRecord$1(value) {
|
|
74
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
75
|
-
}
|
|
76
|
-
/** @throws {ItdConfigError} если значение задано и не является неотрицательным числом */
|
|
77
|
-
function requirePositive(value, name) {
|
|
78
|
-
if (!Number.isFinite(value) || value < 0) throw new ItdConfigError(`${name} должен быть неотрицательным числом, получено: ${value}`);
|
|
79
|
-
return value;
|
|
80
|
-
}
|
|
81
|
-
/** @throws {ItdConfigError} если значение задано и не является boolean */
|
|
82
|
-
function requireOptionalBoolean(value, name) {
|
|
83
|
-
if (value !== void 0 && typeof value !== "boolean") throw new ItdConfigError(`${name} должен быть boolean`);
|
|
84
|
-
}
|
|
85
|
-
//#endregion
|
|
86
65
|
//#region src/core/version.ts
|
|
87
66
|
/** Версия библиотеки. Попадает в `User-Agent`. */
|
|
88
|
-
const LIBRARY_VERSION = "0.
|
|
67
|
+
const LIBRARY_VERSION = "0.11.0";
|
|
89
68
|
//#endregion
|
|
90
69
|
//#region src/core/config.ts
|
|
91
70
|
/** Базовый URL API итд.com. Домен записан в punycode: `итд.com`. */
|
|
@@ -120,13 +99,13 @@ const DEFAULT_RATE_LIMIT_DELAYS = Object.freeze([
|
|
|
120
99
|
]);
|
|
121
100
|
function resolveHeaders(headers) {
|
|
122
101
|
if (headers === void 0) return {};
|
|
123
|
-
if (!
|
|
102
|
+
if (!isObjectLike(headers)) throw new ItdConfigError("headers должен быть объектом строк");
|
|
124
103
|
for (const [name, value] of Object.entries(headers)) if (typeof value !== "string") throw new ItdConfigError(`headers.${name} должен быть строкой`);
|
|
125
104
|
return { ...headers };
|
|
126
105
|
}
|
|
127
106
|
function resolveHooks(hooks) {
|
|
128
107
|
if (hooks === void 0) return {};
|
|
129
|
-
if (!
|
|
108
|
+
if (!isObjectLike(hooks)) throw new ItdConfigError("hooks должен быть объектом");
|
|
130
109
|
for (const name of [
|
|
131
110
|
"onRequest",
|
|
132
111
|
"onResponse",
|
|
@@ -135,27 +114,6 @@ function resolveHooks(hooks) {
|
|
|
135
114
|
]) if (hooks[name] !== void 0 && typeof hooks[name] !== "function") throw new ItdConfigError(`hooks.${name} должен быть функцией`);
|
|
136
115
|
return { ...hooks };
|
|
137
116
|
}
|
|
138
|
-
function resolveLogger(logger) {
|
|
139
|
-
if (logger === void 0 || logger === false) return void 0;
|
|
140
|
-
if (logger === true) return consoleLogger();
|
|
141
|
-
if (!isRecord$1(logger)) throw new ItdConfigError("logger должен быть boolean или объектом Logger");
|
|
142
|
-
for (const method of [
|
|
143
|
-
"debug",
|
|
144
|
-
"info",
|
|
145
|
-
"warn",
|
|
146
|
-
"error"
|
|
147
|
-
]) if (typeof logger[method] !== "function") throw new ItdConfigError(`logger.${method} должен быть функцией`);
|
|
148
|
-
return logger;
|
|
149
|
-
}
|
|
150
|
-
/** Логгер поверх `console` — включается опцией `logger: true`. */
|
|
151
|
-
function consoleLogger() {
|
|
152
|
-
return {
|
|
153
|
-
debug: (message, ...args) => console.debug(`[itd-api] ${message}`, ...args),
|
|
154
|
-
info: (message, ...args) => console.info(`[itd-api] ${message}`, ...args),
|
|
155
|
-
warn: (message, ...args) => console.warn(`[itd-api] ${message}`, ...args),
|
|
156
|
-
error: (message, ...args) => console.error(`[itd-api] ${message}`, ...args)
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
117
|
/**
|
|
160
118
|
* Приводит настройки повторов к полному виду.
|
|
161
119
|
*
|
|
@@ -167,15 +125,15 @@ function consoleLogger() {
|
|
|
167
125
|
*/
|
|
168
126
|
function resolveRetry(retry) {
|
|
169
127
|
if (retry === false) return void 0;
|
|
170
|
-
if (retry !== void 0 && !
|
|
128
|
+
if (retry !== void 0 && !isObjectLike(retry)) throw new ItdConfigError("retry должен быть объектом или false");
|
|
171
129
|
const options = retry ?? {};
|
|
172
130
|
const attempts = options.attempts ?? 3;
|
|
173
131
|
if (!Number.isInteger(attempts) || attempts < 1) throw new ItdConfigError(`retry.attempts должен быть целым числом от 1, получено: ${attempts}`);
|
|
174
132
|
const jitter = options.jitter ?? .3;
|
|
175
133
|
if (!Number.isFinite(jitter) || jitter < 0 || jitter > 1) throw new ItdConfigError(`retry.jitter должен быть в диапазоне 0…1, получено: ${jitter}`);
|
|
176
134
|
if (options.shouldRetry !== void 0 && typeof options.shouldRetry !== "function") throw new ItdConfigError("retry.shouldRetry должен быть функцией");
|
|
177
|
-
const baseDelay =
|
|
178
|
-
const maxDelay =
|
|
135
|
+
const baseDelay = requireNonNegative(options.baseDelay ?? 500, "retry.baseDelay");
|
|
136
|
+
const maxDelay = requireNonNegative(options.maxDelay ?? 3e4, "retry.maxDelay");
|
|
179
137
|
if (attempts === 1) return void 0;
|
|
180
138
|
return {
|
|
181
139
|
attempts,
|
|
@@ -223,10 +181,10 @@ function resolvePacing(pacing) {
|
|
|
223
181
|
function resolveBucketOverrides(overrides, bucket, catalog) {
|
|
224
182
|
const builtIn = catalog.bucketOverrides;
|
|
225
183
|
if (overrides === void 0) return builtIn;
|
|
226
|
-
if (!
|
|
184
|
+
if (!isObjectLike(overrides)) throw new ItdConfigError("rateLimit.bucketOverrides должен быть объектом");
|
|
227
185
|
const resolved = { ...builtIn };
|
|
228
186
|
for (const [name, override] of Object.entries(overrides)) {
|
|
229
|
-
if (!
|
|
187
|
+
if (!isObjectLike(override)) throw new ItdConfigError(`rateLimit.bucketOverrides.${name} должен быть объектом`);
|
|
230
188
|
assertKnownBucket(name, "rateLimit.bucketOverrides", bucket, catalog);
|
|
231
189
|
if (override.concurrency !== void 0) requireConcurrency(override.concurrency, `rateLimit.bucketOverrides.${name}.concurrency`);
|
|
232
190
|
if (override.limit !== void 0 && (!Number.isFinite(override.limit) || override.limit <= 0)) throw new ItdConfigError(`rateLimit.bucketOverrides.${name}.limit должен быть положительным числом, получено: ${override.limit}`);
|
|
@@ -252,7 +210,7 @@ function resolveBucketOverrides(overrides, bucket, catalog) {
|
|
|
252
210
|
*/
|
|
253
211
|
function resolveRateLimit(rateLimit, catalog) {
|
|
254
212
|
if (rateLimit === false) return void 0;
|
|
255
|
-
if (rateLimit !== void 0 && !
|
|
213
|
+
if (rateLimit !== void 0 && !isObjectLike(rateLimit)) throw new ItdConfigError("rateLimit должен быть объектом или false");
|
|
256
214
|
const defaults = {
|
|
257
215
|
concurrency: 6,
|
|
258
216
|
rps: void 0,
|
|
@@ -262,7 +220,6 @@ function resolveRateLimit(rateLimit, catalog) {
|
|
|
262
220
|
bucketConcurrency: 6,
|
|
263
221
|
bucketOverrides: catalog.bucketOverrides,
|
|
264
222
|
bucket: void 0,
|
|
265
|
-
bucketLimits: catalog.bucketLimits,
|
|
266
223
|
defaultBucket: catalog.defaultBucket
|
|
267
224
|
};
|
|
268
225
|
if (!rateLimit) return defaults;
|
|
@@ -270,7 +227,7 @@ function resolveRateLimit(rateLimit, catalog) {
|
|
|
270
227
|
if (rateLimit.rps !== void 0) requireRps(rateLimit.rps, "rateLimit.rps");
|
|
271
228
|
const retryDelays = rateLimit.retryDelays ?? defaults.retryDelays;
|
|
272
229
|
if (!Array.isArray(retryDelays)) throw new ItdConfigError("rateLimit.retryDelays должен быть массивом чисел");
|
|
273
|
-
for (const delay of retryDelays)
|
|
230
|
+
for (const delay of retryDelays) requireNonNegative(delay, "rateLimit.retryDelays");
|
|
274
231
|
requireOptionalBoolean(rateLimit.buckets, "rateLimit.buckets");
|
|
275
232
|
if (rateLimit.bucket !== void 0 && typeof rateLimit.bucket !== "function") throw new ItdConfigError("rateLimit.bucket должен быть функцией");
|
|
276
233
|
return {
|
|
@@ -282,7 +239,6 @@ function resolveRateLimit(rateLimit, catalog) {
|
|
|
282
239
|
bucketConcurrency: requireConcurrency(rateLimit.bucketConcurrency ?? concurrency, "rateLimit.bucketConcurrency"),
|
|
283
240
|
bucketOverrides: resolveBucketOverrides(rateLimit.bucketOverrides, rateLimit.bucket, catalog),
|
|
284
241
|
bucket: rateLimit.bucket,
|
|
285
|
-
bucketLimits: catalog.bucketLimits,
|
|
286
242
|
defaultBucket: catalog.defaultBucket
|
|
287
243
|
};
|
|
288
244
|
}
|
|
@@ -292,13 +248,13 @@ function resolveRateLimit(rateLimit, catalog) {
|
|
|
292
248
|
*/
|
|
293
249
|
function resolveServices(services) {
|
|
294
250
|
if (services === void 0) return [];
|
|
295
|
-
if (!
|
|
251
|
+
if (!isObjectLike(services)) throw new ItdConfigError("services должен быть объектом");
|
|
296
252
|
return Object.entries(services).map(([name, value]) => {
|
|
297
253
|
if (typeof value === "string") return {
|
|
298
254
|
name,
|
|
299
255
|
baseUrl: value
|
|
300
256
|
};
|
|
301
|
-
if (!
|
|
257
|
+
if (!isObjectLike(value)) throw new ItdConfigError(`services.${name} должен быть URL или объектом сервиса`);
|
|
302
258
|
return {
|
|
303
259
|
...value,
|
|
304
260
|
name
|
|
@@ -315,11 +271,12 @@ function resolveServices(services) {
|
|
|
315
271
|
* @throws {ItdConfigError} при некорректных значениях
|
|
316
272
|
*/
|
|
317
273
|
function resolveRuntimeConfig(options, catalog) {
|
|
318
|
-
if (!
|
|
274
|
+
if (!isObjectLike(options)) throw new ItdConfigError("опции клиента должны быть объектом");
|
|
319
275
|
const mode = options.mode ?? RuntimeMode.Auto;
|
|
320
276
|
if (!Object.values(RuntimeMode).includes(mode)) throw new ItdConfigError(`mode должен быть одним из ${Object.values(RuntimeMode).join(", ")}, получено: ${mode}`);
|
|
321
|
-
const timeout =
|
|
322
|
-
const
|
|
277
|
+
const timeout = requireNonNegative(options.timeout ?? 3e4, "timeout");
|
|
278
|
+
const deadline = requireNonNegative(options.deadline ?? 0, "deadline");
|
|
279
|
+
const shutdownTimeout = requireNonNegative(options.shutdownTimeout ?? 1e4, "shutdownTimeout");
|
|
323
280
|
if (options.clock !== void 0 && (typeof options.clock !== "object" || options.clock === null || typeof options.clock.now !== "function" || typeof options.clock.schedule !== "function")) throw new ItdConfigError("clock должен предоставлять методы now() и schedule()");
|
|
324
281
|
if (options.userAgent !== void 0 && options.userAgent !== false && typeof options.userAgent !== "string") throw new ItdConfigError("userAgent должен быть строкой или false");
|
|
325
282
|
return {
|
|
@@ -328,6 +285,7 @@ function resolveRuntimeConfig(options, catalog) {
|
|
|
328
285
|
fetch: resolveFetch(options.fetch),
|
|
329
286
|
clock: options.clock ?? systemClock,
|
|
330
287
|
timeout,
|
|
288
|
+
deadline,
|
|
331
289
|
shutdownTimeout,
|
|
332
290
|
retry: resolveRetry(options.retry),
|
|
333
291
|
rateLimit: resolveRateLimit(options.rateLimit, catalog),
|
|
@@ -358,7 +316,7 @@ function withAttemptInterceptorScope(request, interceptors) {
|
|
|
358
316
|
}
|
|
359
317
|
/** Читает снимок attempt interceptors логической операции. @internal */
|
|
360
318
|
function attemptInterceptorScope(request) {
|
|
361
|
-
return request[ATTEMPT_SCOPE]
|
|
319
|
+
return request[ATTEMPT_SCOPE];
|
|
362
320
|
}
|
|
363
321
|
/**
|
|
364
322
|
* Выполняет обёртки одной попытки.
|
|
@@ -366,8 +324,8 @@ function attemptInterceptorScope(request) {
|
|
|
366
324
|
* Каждый `next` одноразовый: interceptor может short-circuit попытку синтетическим
|
|
367
325
|
* `Response`, но не может незаметно породить два сетевых запроса внутри одного attempt.
|
|
368
326
|
*/
|
|
369
|
-
async function runAttemptInterceptors(
|
|
370
|
-
return
|
|
327
|
+
async function runAttemptInterceptors(interceptors, context, execute) {
|
|
328
|
+
return interceptors.reduceRight((next, { plugin, interceptor }) => async () => {
|
|
371
329
|
let called = false;
|
|
372
330
|
const response = await interceptor(context, () => {
|
|
373
331
|
if (called) throw new ItdConfigError(`attempt interceptor плагина «${plugin}» вызвал next() больше одного раза`);
|
|
@@ -525,9 +483,13 @@ var PluginRegistry = class {
|
|
|
525
483
|
}
|
|
526
484
|
/** Проверяет добавление без вызова `install()`. @internal */
|
|
527
485
|
assertCanAdd(plugin) {
|
|
486
|
+
this.#orderWith(plugin);
|
|
487
|
+
}
|
|
488
|
+
/** Проверяет плагин и возвращает порядок обёрток вместе с ним. */
|
|
489
|
+
#orderWith(plugin) {
|
|
528
490
|
validatePluginDefinition(plugin);
|
|
529
491
|
if (this.#removing.has(plugin.name)) throw new ItdConfigError(`плагин «${plugin.name}» ещё отключается; дождитесь завершения unuse() или dispose()`);
|
|
530
|
-
orderPluginDefinitions([...this.#ordered.map((entry) => entry.plugin), plugin]);
|
|
492
|
+
return orderPluginDefinitions([...this.#ordered.map((entry) => entry.plugin), plugin]);
|
|
531
493
|
}
|
|
532
494
|
/** Проверяет удаление без изменения реестра. @internal */
|
|
533
495
|
assertCanRemove(name) {
|
|
@@ -543,8 +505,7 @@ var PluginRegistry = class {
|
|
|
543
505
|
* @throws {ItdConfigError} если плагин задан неверно, уже подключён или нарушает зависимости
|
|
544
506
|
*/
|
|
545
507
|
add(plugin, context) {
|
|
546
|
-
this
|
|
547
|
-
const ordered = orderPluginDefinitions([...this.#ordered.map((entry) => entry.plugin), plugin]);
|
|
508
|
+
const ordered = this.#orderWith(plugin);
|
|
548
509
|
const transformers = [];
|
|
549
510
|
const interceptors = [];
|
|
550
511
|
const register = (values, value, kind) => {
|
|
@@ -787,15 +748,10 @@ var DestinationScheduler = class {
|
|
|
787
748
|
register(bucket) {
|
|
788
749
|
this.#buckets.push(bucket);
|
|
789
750
|
}
|
|
790
|
-
/** Пересчитывает
|
|
751
|
+
/** Пересчитывает запуск; существующий таймер заменяется только более ранним. */
|
|
791
752
|
changed() {
|
|
792
753
|
this.#drain();
|
|
793
754
|
}
|
|
794
|
-
/** Перепланирует пробуждение после изменения времени готовности бакета. */
|
|
795
|
-
timingChanged() {
|
|
796
|
-
this.#cancelWakeup();
|
|
797
|
-
this.#drain();
|
|
798
|
-
}
|
|
799
755
|
/** Отклоняет ожидающие задачи, сохраняя паузы и темп для повторного запуска клиента. */
|
|
800
756
|
stop() {
|
|
801
757
|
this.#cancelWakeup();
|
|
@@ -851,6 +807,70 @@ var DestinationScheduler = class {
|
|
|
851
807
|
}
|
|
852
808
|
};
|
|
853
809
|
/**
|
|
810
|
+
* Очередь ожидающих задач в порядке постановки.
|
|
811
|
+
*
|
|
812
|
+
* Постановка в хвост, снятие головы, снятие произвольного узла при отмене и полная
|
|
813
|
+
* очистка — за O(1) при любой длине: очередь рассчитана на десятки тысяч задач, разом
|
|
814
|
+
* упёршихся в паузу `429`.
|
|
815
|
+
*/
|
|
816
|
+
var WaitingList = class {
|
|
817
|
+
#head;
|
|
818
|
+
#tail;
|
|
819
|
+
#size = 0;
|
|
820
|
+
get size() {
|
|
821
|
+
return this.#size;
|
|
822
|
+
}
|
|
823
|
+
push(task) {
|
|
824
|
+
const node = {
|
|
825
|
+
task,
|
|
826
|
+
prev: this.#tail,
|
|
827
|
+
next: void 0,
|
|
828
|
+
linked: true
|
|
829
|
+
};
|
|
830
|
+
if (this.#tail) this.#tail.next = node;
|
|
831
|
+
else this.#head = node;
|
|
832
|
+
this.#tail = node;
|
|
833
|
+
this.#size += 1;
|
|
834
|
+
return node;
|
|
835
|
+
}
|
|
836
|
+
shift() {
|
|
837
|
+
const node = this.#head;
|
|
838
|
+
if (!node) return void 0;
|
|
839
|
+
this.remove(node);
|
|
840
|
+
return node.task;
|
|
841
|
+
}
|
|
842
|
+
/** Снимает узел; `false`, если он уже снят. */
|
|
843
|
+
remove(node) {
|
|
844
|
+
if (!node.linked) return false;
|
|
845
|
+
node.linked = false;
|
|
846
|
+
if (node.prev) node.prev.next = node.next;
|
|
847
|
+
else this.#head = node.next;
|
|
848
|
+
if (node.next) node.next.prev = node.prev;
|
|
849
|
+
else this.#tail = node.prev;
|
|
850
|
+
node.prev = void 0;
|
|
851
|
+
node.next = void 0;
|
|
852
|
+
this.#size -= 1;
|
|
853
|
+
return true;
|
|
854
|
+
}
|
|
855
|
+
/** Снимает все узлы; задачи возвращаются в порядке постановки. */
|
|
856
|
+
drain() {
|
|
857
|
+
const tasks = [];
|
|
858
|
+
let node = this.#head;
|
|
859
|
+
while (node) {
|
|
860
|
+
const following = node.next;
|
|
861
|
+
node.linked = false;
|
|
862
|
+
node.prev = void 0;
|
|
863
|
+
node.next = void 0;
|
|
864
|
+
tasks.push(node.task);
|
|
865
|
+
node = following;
|
|
866
|
+
}
|
|
867
|
+
this.#head = void 0;
|
|
868
|
+
this.#tail = void 0;
|
|
869
|
+
this.#size = 0;
|
|
870
|
+
return tasks;
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
/**
|
|
854
874
|
* Состояние и очередь одного серверного бакета.
|
|
855
875
|
*
|
|
856
876
|
* Класс хранит локальную конкурентность, темп и серверную квоту, а решение о старте
|
|
@@ -879,12 +899,14 @@ var BucketQueue = class {
|
|
|
879
899
|
#flatPause;
|
|
880
900
|
/** Лимит бакета до первого ответа. */
|
|
881
901
|
#seedLimit;
|
|
902
|
+
/** Начальные ограничения из каталога, с которыми создана очередь. */
|
|
903
|
+
#definition;
|
|
882
904
|
/** Последнее, что сказал сервер. Живёт и в режиме `off` — ради `rateLimitState()`. */
|
|
883
905
|
#limit;
|
|
884
906
|
#remaining;
|
|
885
907
|
#active = 0;
|
|
886
908
|
#nextSlot = 0;
|
|
887
|
-
#waiting =
|
|
909
|
+
#waiting = new WaitingList();
|
|
888
910
|
/**
|
|
889
911
|
* Оценка остатка для режима `smooth`.
|
|
890
912
|
*
|
|
@@ -893,17 +915,19 @@ var BucketQueue = class {
|
|
|
893
915
|
*/
|
|
894
916
|
#tokens = 1;
|
|
895
917
|
#tokensAt;
|
|
896
|
-
constructor(destination, bucket, scheduler, options, clock,
|
|
918
|
+
constructor(destination, bucket, scheduler, options, clock, definition) {
|
|
897
919
|
this.#destination = destination;
|
|
898
920
|
this.#bucket = bucket;
|
|
899
921
|
this.#scheduler = scheduler;
|
|
900
922
|
this.#clock = clock;
|
|
923
|
+
this.#definition = definition;
|
|
901
924
|
this.#pacing = options.pacing;
|
|
902
925
|
this.#smooth = options.buckets && options.pacing === RateLimitPacing.Smooth;
|
|
903
926
|
this.#flatPause = options.buckets ? void 0 : options.retryDelays[0] ?? 0;
|
|
904
|
-
|
|
905
|
-
this.#
|
|
906
|
-
|
|
927
|
+
const override = options.buckets ? options.bucketOverrides[bucket] : void 0;
|
|
928
|
+
this.#seedLimit = options.buckets ? override?.limit ?? definition?.limit : void 0;
|
|
929
|
+
this.#concurrency = options.buckets ? override?.concurrency ?? definition?.concurrency ?? options.bucketConcurrency : options.concurrency;
|
|
930
|
+
const rps = options.buckets ? override?.rps ?? definition?.rps : void 0;
|
|
907
931
|
this.#minGap = rps ? 1e3 / rps : 0;
|
|
908
932
|
scheduler.register(this);
|
|
909
933
|
}
|
|
@@ -911,17 +935,21 @@ var BucketQueue = class {
|
|
|
911
935
|
get bucket() {
|
|
912
936
|
return this.#bucket;
|
|
913
937
|
}
|
|
938
|
+
/** Начальные ограничения из каталога, с которыми создана очередь. */
|
|
939
|
+
get definition() {
|
|
940
|
+
return this.#definition;
|
|
941
|
+
}
|
|
914
942
|
/** Запросов бакета прошло в общую очередь и ещё не завершилось. */
|
|
915
943
|
get active() {
|
|
916
944
|
return this.#active;
|
|
917
945
|
}
|
|
918
946
|
/** Запросов бакета ждёт своей очереди. */
|
|
919
947
|
get pending() {
|
|
920
|
-
return this.#waiting.
|
|
948
|
+
return this.#waiting.size;
|
|
921
949
|
}
|
|
922
950
|
/** Есть ли ожидающая задача, для которой свободен локальный слот. */
|
|
923
951
|
get canStart() {
|
|
924
|
-
return this.#waiting.
|
|
952
|
+
return this.#waiting.size > 0 && this.#active < this.#concurrency;
|
|
925
953
|
}
|
|
926
954
|
/** Момент, раньше которого локальное ограничение не разрешает следующий старт. */
|
|
927
955
|
get readyAt() {
|
|
@@ -934,13 +962,11 @@ var BucketQueue = class {
|
|
|
934
962
|
let queued;
|
|
935
963
|
const detach = () => signal?.removeEventListener("abort", onAbort);
|
|
936
964
|
const onAbort = () => {
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
this.#
|
|
940
|
-
queued.cancel(queueAbortError());
|
|
941
|
-
this.#scheduler.timingChanged();
|
|
965
|
+
if (!this.#waiting.remove(queued)) return;
|
|
966
|
+
queued.task.cancel(queueAbortError());
|
|
967
|
+
this.#scheduler.changed();
|
|
942
968
|
};
|
|
943
|
-
queued = {
|
|
969
|
+
queued = this.#waiting.push({
|
|
944
970
|
start: (complete) => {
|
|
945
971
|
detach();
|
|
946
972
|
Promise.resolve().then(task).then(resolve, reject).finally(complete);
|
|
@@ -949,8 +975,7 @@ var BucketQueue = class {
|
|
|
949
975
|
detach();
|
|
950
976
|
reject(reason);
|
|
951
977
|
}
|
|
952
|
-
};
|
|
953
|
-
this.#waiting.push(queued);
|
|
978
|
+
});
|
|
954
979
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
955
980
|
if (signal?.aborted) onAbort();
|
|
956
981
|
else this.#scheduler.changed();
|
|
@@ -994,7 +1019,7 @@ var BucketQueue = class {
|
|
|
994
1019
|
this.#refill(capacity);
|
|
995
1020
|
if (remaining < this.#tokens) this.#tokens = remaining;
|
|
996
1021
|
const wait = this.#armPause(capacity);
|
|
997
|
-
this.#scheduler.
|
|
1022
|
+
this.#scheduler.changed();
|
|
998
1023
|
return wait;
|
|
999
1024
|
}
|
|
1000
1025
|
if (remaining > 0) return 0;
|
|
@@ -1018,7 +1043,7 @@ var BucketQueue = class {
|
|
|
1018
1043
|
limit: this.#limit,
|
|
1019
1044
|
remaining: this.#remaining,
|
|
1020
1045
|
active: this.#active,
|
|
1021
|
-
pending: this.#waiting.
|
|
1046
|
+
pending: this.#waiting.size
|
|
1022
1047
|
};
|
|
1023
1048
|
}
|
|
1024
1049
|
/** Отклоняет ещё не начатые задачи этого бакета. */
|
|
@@ -1027,9 +1052,8 @@ var BucketQueue = class {
|
|
|
1027
1052
|
}
|
|
1028
1053
|
/** Отклоняет ожидающие задачи; используется также при остановке всего направления. */
|
|
1029
1054
|
cancelWaiting(reason, notify = true) {
|
|
1030
|
-
const
|
|
1031
|
-
|
|
1032
|
-
if (notify) this.#scheduler.timingChanged();
|
|
1055
|
+
for (const task of this.#waiting.drain()) task.cancel(reason);
|
|
1056
|
+
if (notify) this.#scheduler.changed();
|
|
1033
1057
|
}
|
|
1034
1058
|
/** Лимит бакета: сказанный сервером, иначе табличный. */
|
|
1035
1059
|
#capacity() {
|
|
@@ -1063,14 +1087,21 @@ var BucketQueue = class {
|
|
|
1063
1087
|
#hold(ms) {
|
|
1064
1088
|
if (ms <= 0) return;
|
|
1065
1089
|
this.#nextSlot = Math.max(this.#nextSlot, this.#clock.now() + ms);
|
|
1066
|
-
this.#scheduler.
|
|
1090
|
+
this.#scheduler.changed();
|
|
1067
1091
|
}
|
|
1068
1092
|
};
|
|
1069
|
-
/**
|
|
1070
|
-
function
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1093
|
+
/** Совпадают ли начальные ограничения по всем трём полям. */
|
|
1094
|
+
function sameDefinition(left, right) {
|
|
1095
|
+
return left?.limit === right?.limit && left?.concurrency === right?.concurrency && left?.rps === right?.rps;
|
|
1096
|
+
}
|
|
1097
|
+
/** Ограничения бакета для текста ошибки. */
|
|
1098
|
+
function describeDefinition(definition) {
|
|
1099
|
+
const parts = [
|
|
1100
|
+
definition?.limit === void 0 ? [] : [`limit ${definition.limit}`],
|
|
1101
|
+
definition?.concurrency === void 0 ? [] : [`concurrency ${definition.concurrency}`],
|
|
1102
|
+
definition?.rps === void 0 ? [] : [`rps ${definition.rps}`]
|
|
1103
|
+
].flat();
|
|
1104
|
+
return parts.length > 0 ? parts.join(", ") : "без ограничений";
|
|
1074
1105
|
}
|
|
1075
1106
|
/**
|
|
1076
1107
|
* Очереди по парам «направление — серверный счётчик частоты».
|
|
@@ -1086,47 +1117,23 @@ var RequestQueuePool = class {
|
|
|
1086
1117
|
#clock;
|
|
1087
1118
|
/** Ключ `undefined` — основная очередь внутренних клиентов без известного направления. */
|
|
1088
1119
|
#destinations = /* @__PURE__ */ new Map();
|
|
1089
|
-
/** Динамические определения feature вместе с числом использующих их клиентов. */
|
|
1090
|
-
#featureBuckets = /* @__PURE__ */ new Map();
|
|
1091
1120
|
constructor(options, clock = systemClock) {
|
|
1092
1121
|
this.#options = options;
|
|
1093
1122
|
this.#clock = clock;
|
|
1094
1123
|
}
|
|
1095
1124
|
/**
|
|
1096
|
-
*
|
|
1125
|
+
* Очередь бакета на направлении. При `buckets: false` бакет всегда `default`.
|
|
1126
|
+
*
|
|
1127
|
+
* Начальные ограничения приходят из каталога вызывающего клиента и запоминаются при
|
|
1128
|
+
* создании очереди. Клиенты, разделяющие один пул через `ItdAccounts`, обязаны описывать
|
|
1129
|
+
* бакет одинаково: расхождение обнаруживается при обращении к уже созданной очереди.
|
|
1097
1130
|
*
|
|
1098
|
-
*
|
|
1099
|
-
* Возвращённая функция откатывает регистрацию, пока очередь бакета ещё не создана.
|
|
1131
|
+
* @throws {ItdConfigError} если очередь бакета создана с другими ограничениями
|
|
1100
1132
|
*/
|
|
1101
|
-
|
|
1102
|
-
const normalized = Object.freeze({
|
|
1103
|
-
...definition.limit === void 0 ? {} : { limit: definition.limit },
|
|
1104
|
-
...definition.concurrency === void 0 ? {} : { concurrency: definition.concurrency },
|
|
1105
|
-
...definition.rps === void 0 ? {} : { rps: definition.rps }
|
|
1106
|
-
});
|
|
1107
|
-
const existing = this.#featureBuckets.get(name);
|
|
1108
|
-
if (existing) {
|
|
1109
|
-
if (existing.definition.limit !== normalized.limit || existing.definition.concurrency !== normalized.concurrency || existing.definition.rps !== normalized.rps) throw new ItdConfigError(`Бакет feature «${name}» уже зарегистрирован с другими ограничениями`);
|
|
1110
|
-
existing.references += 1;
|
|
1111
|
-
} else this.#featureBuckets.set(name, {
|
|
1112
|
-
definition: normalized,
|
|
1113
|
-
references: 1
|
|
1114
|
-
});
|
|
1115
|
-
let released = false;
|
|
1116
|
-
return () => {
|
|
1117
|
-
if (released) return;
|
|
1118
|
-
released = true;
|
|
1119
|
-
const current = this.#featureBuckets.get(name);
|
|
1120
|
-
if (!current) return;
|
|
1121
|
-
current.references -= 1;
|
|
1122
|
-
if (current.references > 0) return;
|
|
1123
|
-
if (![...this.#destinations.values()].some((entry) => entry.buckets.has(name))) this.#featureBuckets.delete(name);
|
|
1124
|
-
};
|
|
1125
|
-
}
|
|
1126
|
-
/** Очередь бакета на направлении. При `buckets: false` бакет всегда `default`. */
|
|
1127
|
-
for(destination, bucket) {
|
|
1133
|
+
for(destination, bucket, definition) {
|
|
1128
1134
|
const fallback = this.#options.defaultBucket;
|
|
1129
1135
|
const name = this.#options.buckets ? bucket ?? fallback : fallback;
|
|
1136
|
+
const limits = this.#options.buckets ? definition : void 0;
|
|
1130
1137
|
let entry = this.#destinations.get(destination);
|
|
1131
1138
|
if (!entry) {
|
|
1132
1139
|
entry = {
|
|
@@ -1137,9 +1144,9 @@ var RequestQueuePool = class {
|
|
|
1137
1144
|
}
|
|
1138
1145
|
let queue = entry.buckets.get(name);
|
|
1139
1146
|
if (!queue) {
|
|
1140
|
-
queue = new BucketQueue(destination, name, entry.scheduler, this.#options, this.#clock,
|
|
1147
|
+
queue = new BucketQueue(destination, name, entry.scheduler, this.#options, this.#clock, limits);
|
|
1141
1148
|
entry.buckets.set(name, queue);
|
|
1142
|
-
}
|
|
1149
|
+
} else if (!sameDefinition(queue.definition, limits)) throw new ItdConfigError(`Бакет «${name}» уже используется с другими ограничениями: ${describeDefinition(queue.definition)} против ${describeDefinition(limits)}`);
|
|
1143
1150
|
return queue;
|
|
1144
1151
|
}
|
|
1145
1152
|
/** Снимки всех бакетов, о которых что-то известно. */
|
|
@@ -1159,7 +1166,6 @@ var RequestQueuePool = class {
|
|
|
1159
1166
|
clear() {
|
|
1160
1167
|
this.stop();
|
|
1161
1168
|
this.#destinations.clear();
|
|
1162
|
-
this.#featureBuckets.clear();
|
|
1163
1169
|
}
|
|
1164
1170
|
};
|
|
1165
1171
|
//#endregion
|
|
@@ -1299,10 +1305,17 @@ async function dispatchRequestHook(hooks, field, context) {
|
|
|
1299
1305
|
}
|
|
1300
1306
|
//#endregion
|
|
1301
1307
|
//#region src/core/execution/lifecycle.ts
|
|
1302
|
-
/**
|
|
1303
|
-
|
|
1308
|
+
/**
|
|
1309
|
+
* Объединяет отмену вызова, завершение владельца и срок ожидания в один сигнал.
|
|
1310
|
+
*
|
|
1311
|
+
* Истечение срока отменяет сигнал ошибкой из `expiry.error`; она же становится
|
|
1312
|
+
* `signal.reason`.
|
|
1313
|
+
*
|
|
1314
|
+
* @internal
|
|
1315
|
+
*/
|
|
1316
|
+
function createRequestAbortScope(signal, lifetimeSignal, clock, expiry) {
|
|
1304
1317
|
const controller = new AbortController();
|
|
1305
|
-
let
|
|
1318
|
+
let expired;
|
|
1306
1319
|
const link = (source) => {
|
|
1307
1320
|
if (!source) return void 0;
|
|
1308
1321
|
if (source.aborted) {
|
|
@@ -1314,20 +1327,33 @@ function createRequestAbortScope(signal, lifetimeSignal, timeout, clock) {
|
|
|
1314
1327
|
return () => source.removeEventListener("abort", onAbort);
|
|
1315
1328
|
};
|
|
1316
1329
|
const detach = [link(signal), link(lifetimeSignal)];
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
controller.
|
|
1320
|
-
|
|
1330
|
+
let cancelTimer = expiry && expiry.after > 0 ? clock.schedule(() => {
|
|
1331
|
+
cancelTimer = void 0;
|
|
1332
|
+
if (controller.signal.aborted) return;
|
|
1333
|
+
expired = expiry.error();
|
|
1334
|
+
controller.abort(expired);
|
|
1335
|
+
}, expiry.after) : void 0;
|
|
1336
|
+
const disarm = () => {
|
|
1337
|
+
cancelTimer?.();
|
|
1338
|
+
cancelTimer = void 0;
|
|
1339
|
+
};
|
|
1321
1340
|
return {
|
|
1322
1341
|
signal: controller.signal,
|
|
1323
|
-
|
|
1342
|
+
abort: (reason) => controller.abort(reason),
|
|
1343
|
+
expired: () => expired,
|
|
1344
|
+
disarm,
|
|
1324
1345
|
cleanup: () => {
|
|
1325
|
-
|
|
1346
|
+
disarm();
|
|
1326
1347
|
for (const remove of detach) remove?.();
|
|
1327
1348
|
}
|
|
1328
1349
|
};
|
|
1329
1350
|
}
|
|
1330
|
-
/**
|
|
1351
|
+
/**
|
|
1352
|
+
* Прерывает только ожидание: исходный промис может корректно завершить фоновую работу,
|
|
1353
|
+
* а его позднее отклонение не станет необработанным.
|
|
1354
|
+
*
|
|
1355
|
+
* @internal
|
|
1356
|
+
*/
|
|
1331
1357
|
function waitForRequest(promise, signal) {
|
|
1332
1358
|
if (signal.aborted) {
|
|
1333
1359
|
promise.catch(() => {});
|
|
@@ -1342,61 +1368,114 @@ function waitForRequest(promise, signal) {
|
|
|
1342
1368
|
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
1343
1369
|
});
|
|
1344
1370
|
}
|
|
1345
|
-
/**
|
|
1371
|
+
/**
|
|
1372
|
+
* Возвращает публичную ошибку, соответствующую причине остановки ожидания.
|
|
1373
|
+
*
|
|
1374
|
+
* Истёкший срок отдаёт ошибку, созданную таймером области. Отмена с причиной-ошибкой
|
|
1375
|
+
* библиотеки (истёкший срок родительской области) отдаёт саму причину; прочая отмена
|
|
1376
|
+
* оборачивается в {@link ItdAbortError}.
|
|
1377
|
+
*
|
|
1378
|
+
* @internal
|
|
1379
|
+
*/
|
|
1346
1380
|
function requestAbortError(scope, request, fallback) {
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
if (
|
|
1353
|
-
return
|
|
1381
|
+
const expired = scope.expired();
|
|
1382
|
+
if (expired) return expired;
|
|
1383
|
+
if (!scope.signal.aborted) return fallback;
|
|
1384
|
+
const reason = scope.signal.reason;
|
|
1385
|
+
if (reason instanceof ItdTimeoutError) return reason;
|
|
1386
|
+
if (fallback instanceof ItdAbortError) return fallback;
|
|
1387
|
+
return new ItdAbortError(`Запрос ${request.method.toUpperCase()} ${request.path} отменён`, { cause: reason });
|
|
1354
1388
|
}
|
|
1355
1389
|
//#endregion
|
|
1356
1390
|
//#region src/core/execution/pipeline.ts
|
|
1357
|
-
const
|
|
1358
|
-
|
|
1359
|
-
const
|
|
1360
|
-
|
|
1361
|
-
|
|
1391
|
+
const OPERATION_STATE = Symbol("itd-api.operation-state");
|
|
1392
|
+
function operationState(request) {
|
|
1393
|
+
const internal = request;
|
|
1394
|
+
let state = internal[OPERATION_STATE];
|
|
1395
|
+
if (!state) {
|
|
1396
|
+
state = {};
|
|
1397
|
+
internal[OPERATION_STATE] = state;
|
|
1398
|
+
}
|
|
1399
|
+
return state;
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Начинает новую логическую операцию со свежим служебным состоянием.
|
|
1403
|
+
*
|
|
1404
|
+
* Контейнер заменяется безусловно: пользовательский код мог передать spread-копию запроса,
|
|
1405
|
+
* уже прошедшего через pipeline. Такое повторное обращение является новой операцией и не
|
|
1406
|
+
* должно наследовать её попытки, очередь, auth recovery, reader или учёт `onError`.
|
|
1407
|
+
*
|
|
1408
|
+
* @internal
|
|
1409
|
+
*/
|
|
1410
|
+
function initializeOperationState(request, initial) {
|
|
1411
|
+
const state = {
|
|
1412
|
+
lifecycleSignal: initial.lifecycleSignal,
|
|
1413
|
+
...initial.reader === void 0 ? {} : { reader: initial.reader },
|
|
1414
|
+
...initial.disposeCleanup ? { disposeCleanup: true } : {}
|
|
1415
|
+
};
|
|
1416
|
+
return {
|
|
1417
|
+
...request,
|
|
1418
|
+
[OPERATION_STATE]: state
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
/** Функция чтения контракта операции, если запрос её несёт. @internal */
|
|
1422
|
+
function operationReaderOf(request) {
|
|
1423
|
+
return request[OPERATION_STATE]?.reader;
|
|
1424
|
+
}
|
|
1425
|
+
/** Переносит служебное состояние логической операции на новый объект запроса. @internal */
|
|
1426
|
+
function withOperationState(prepared, source) {
|
|
1427
|
+
const from = source;
|
|
1428
|
+
const to = prepared;
|
|
1429
|
+
to[OPERATION_STATE] = from[OPERATION_STATE] ?? {};
|
|
1430
|
+
return prepared;
|
|
1431
|
+
}
|
|
1432
|
+
/** Общий сигнал операции, сохранённый {@link initializeOperationState}. @internal */
|
|
1433
|
+
function lifecycleSignalOf(request) {
|
|
1434
|
+
return request[OPERATION_STATE]?.lifecycleSignal;
|
|
1435
|
+
}
|
|
1362
1436
|
/** Возвращает общее для всех retry состояние восстановления авторизации. @internal */
|
|
1363
1437
|
function requestAuthRecoveryState(request) {
|
|
1364
|
-
const
|
|
1365
|
-
const current =
|
|
1438
|
+
const state = operationState(request);
|
|
1439
|
+
const current = state.authRecovery;
|
|
1366
1440
|
if (current) return current;
|
|
1367
|
-
const
|
|
1441
|
+
const recovery = {
|
|
1368
1442
|
recovered: false,
|
|
1369
1443
|
preparationErrors: /* @__PURE__ */ new Set()
|
|
1370
1444
|
};
|
|
1371
|
-
|
|
1372
|
-
return
|
|
1445
|
+
state.authRecovery = recovery;
|
|
1446
|
+
return recovery;
|
|
1373
1447
|
}
|
|
1374
|
-
/** Создаёт
|
|
1375
|
-
function
|
|
1376
|
-
const
|
|
1377
|
-
|
|
1448
|
+
/** Создаёт общий для копий логического запроса учёт вызовов `onError`. @internal */
|
|
1449
|
+
function trackRequestErrorReporting(request) {
|
|
1450
|
+
const state = operationState(request);
|
|
1451
|
+
state.errorReporting ??= {
|
|
1378
1452
|
errors: /* @__PURE__ */ new Set(),
|
|
1379
|
-
|
|
1453
|
+
abortReported: false
|
|
1380
1454
|
};
|
|
1381
1455
|
}
|
|
1456
|
+
function errorReportingState(request) {
|
|
1457
|
+
trackRequestErrorReporting(request);
|
|
1458
|
+
return operationState(request).errorReporting;
|
|
1459
|
+
}
|
|
1382
1460
|
/** Отмечает, что ошибка логического запроса уже была передана в `onError`. @internal */
|
|
1383
|
-
function
|
|
1384
|
-
|
|
1385
|
-
request[REQUEST_ERROR_OBSERVATION_STATE]?.errors.add(error);
|
|
1461
|
+
function markRequestErrorReported(request, error) {
|
|
1462
|
+
errorReportingState(request).errors.add(error);
|
|
1386
1463
|
}
|
|
1387
1464
|
/** Была ли конкретная ошибка этой логической операции уже передана в `onError`. @internal */
|
|
1388
|
-
function
|
|
1389
|
-
return request[
|
|
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;
|
|
1465
|
+
function wasRequestErrorReported(request, error) {
|
|
1466
|
+
return request[OPERATION_STATE]?.errorReporting?.errors.has(error) ?? false;
|
|
1396
1467
|
}
|
|
1397
|
-
/**
|
|
1398
|
-
|
|
1399
|
-
|
|
1468
|
+
/**
|
|
1469
|
+
* Отмечает, что отмена операции передана в `onError`.
|
|
1470
|
+
*
|
|
1471
|
+
* @returns `false`, если это уже сделал другой уровень — повторно вызывать хук не нужно
|
|
1472
|
+
* @internal
|
|
1473
|
+
*/
|
|
1474
|
+
function claimAbortReport(request) {
|
|
1475
|
+
const state = errorReportingState(request);
|
|
1476
|
+
if (state.abortReported) return false;
|
|
1477
|
+
state.abortReported = true;
|
|
1478
|
+
return true;
|
|
1400
1479
|
}
|
|
1401
1480
|
/** Один раз присваивает низкоуровневому запросу семантический ID до входа в middleware. */
|
|
1402
1481
|
function identifyRequest(request) {
|
|
@@ -1405,56 +1484,40 @@ function identifyRequest(request) {
|
|
|
1405
1484
|
operationId: "raw"
|
|
1406
1485
|
} : request;
|
|
1407
1486
|
}
|
|
1408
|
-
/** Привязывает счётчик транспортных попыток к одной логической операции. @internal */
|
|
1409
|
-
function trackRequestAttempts(request) {
|
|
1410
|
-
if (request[REQUEST_ATTEMPT_STATE]) return request;
|
|
1411
|
-
return {
|
|
1412
|
-
...request,
|
|
1413
|
-
[REQUEST_ATTEMPT_STATE]: { value: 0 }
|
|
1414
|
-
};
|
|
1415
|
-
}
|
|
1416
1487
|
/** Начинает следующую фактическую транспортную попытку логической операции. @internal */
|
|
1417
1488
|
function beginTransportAttempt(request) {
|
|
1418
|
-
const
|
|
1419
|
-
|
|
1420
|
-
if (!state) throw new Error("request attempt state was not initialized");
|
|
1421
|
-
state.value += 1;
|
|
1489
|
+
const state = operationState(request);
|
|
1490
|
+
state.attempt = (state.attempt ?? 0) + 1;
|
|
1422
1491
|
return {
|
|
1423
|
-
...
|
|
1424
|
-
attempt: state.
|
|
1492
|
+
...request,
|
|
1493
|
+
attempt: state.attempt
|
|
1425
1494
|
};
|
|
1426
1495
|
}
|
|
1427
1496
|
/** Возвращает номер последней начатой транспортной попытки. @internal */
|
|
1428
1497
|
function currentTransportAttempt(request) {
|
|
1429
|
-
return request[
|
|
1498
|
+
return request[OPERATION_STATE]?.attempt ?? 0;
|
|
1430
1499
|
}
|
|
1431
1500
|
/**
|
|
1432
1501
|
* Вычисляет ключ очереди один раз на логическую операцию.
|
|
1433
1502
|
*
|
|
1434
1503
|
* Ключ спрашивают трижды: при постановке в очередь, при чтении заголовков ответа и при
|
|
1435
|
-
* паузе после `429`. Значение
|
|
1436
|
-
*
|
|
1504
|
+
* паузе после `429`. Значение хранится в общем состоянии операции, поэтому его видят все
|
|
1505
|
+
* копии запроса ниже по конвейеру. Ограничения бакета фиксируются вместе с ключом:
|
|
1506
|
+
* освобождение feature во время запроса не меняет очередь операции.
|
|
1437
1507
|
*
|
|
1438
1508
|
* @internal
|
|
1439
1509
|
*/
|
|
1440
1510
|
function requestQueueKey(request, compute) {
|
|
1441
|
-
const
|
|
1442
|
-
const cached =
|
|
1511
|
+
const state = operationState(request);
|
|
1512
|
+
const cached = state.queueKey;
|
|
1443
1513
|
if (cached) return cached;
|
|
1444
1514
|
const key = compute(request);
|
|
1445
|
-
|
|
1515
|
+
state.queueKey = key;
|
|
1446
1516
|
return key;
|
|
1447
1517
|
}
|
|
1448
|
-
/** Помечает запрос как часть внутренней финализации уже начатого `dispose()`. @internal */
|
|
1449
|
-
function markDisposeCleanupRequest(request) {
|
|
1450
|
-
return {
|
|
1451
|
-
...request,
|
|
1452
|
-
[DISPOSE_CLEANUP_REQUEST]: true
|
|
1453
|
-
};
|
|
1454
|
-
}
|
|
1455
1518
|
/** Разрешено ли запросу завершать внутреннюю очистку после `dispose()`. @internal */
|
|
1456
1519
|
function isDisposeCleanupRequest(request) {
|
|
1457
|
-
return request[
|
|
1520
|
+
return request[OPERATION_STATE]?.disposeCleanup === true;
|
|
1458
1521
|
}
|
|
1459
1522
|
/**
|
|
1460
1523
|
* Собирает слои в один обработчик.
|
|
@@ -1484,129 +1547,143 @@ function withLayerHeaders(request, headers) {
|
|
|
1484
1547
|
}
|
|
1485
1548
|
//#endregion
|
|
1486
1549
|
//#region src/core/execution/http.ts
|
|
1550
|
+
/** Собирает запрос конвейера из контракта операции и параметров вызова. */
|
|
1551
|
+
function operationRequest(operation, options) {
|
|
1552
|
+
return {
|
|
1553
|
+
...options,
|
|
1554
|
+
operationId: operation.id,
|
|
1555
|
+
method: operation.method,
|
|
1556
|
+
retrySafety: options.retrySafety ?? operation.retrySafety
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1487
1559
|
/**
|
|
1488
1560
|
* Точка входа ресурсов в конвейер запросов.
|
|
1489
1561
|
*
|
|
1490
|
-
*
|
|
1491
|
-
*
|
|
1562
|
+
* Заводит общий lifecycle логической операции — отмену, освобождение клиента и `deadline`, —
|
|
1563
|
+
* передаёт запрос собранной цепочке стадий и сообщает в `onError` об ошибках, которые
|
|
1564
|
+
* не дошли до транспорта. О стадиях и их порядке ресурсы не знают.
|
|
1492
1565
|
*/
|
|
1493
1566
|
var HttpClient = class {
|
|
1494
1567
|
#handler;
|
|
1495
|
-
#plugins;
|
|
1496
1568
|
#baseUrl;
|
|
1497
|
-
#
|
|
1569
|
+
#deadline;
|
|
1498
1570
|
#clock;
|
|
1499
1571
|
#lifetimeSignal;
|
|
1500
1572
|
#hooks;
|
|
1501
1573
|
#assertActive;
|
|
1574
|
+
#activeScopes = /* @__PURE__ */ new Set();
|
|
1502
1575
|
constructor(deps) {
|
|
1503
1576
|
this.#handler = deps.handler;
|
|
1504
|
-
this.#plugins = deps.plugins;
|
|
1505
1577
|
this.#baseUrl = deps.baseUrl;
|
|
1506
|
-
this.#
|
|
1578
|
+
this.#deadline = deps.deadline;
|
|
1507
1579
|
this.#clock = deps.clock;
|
|
1508
1580
|
this.#lifetimeSignal = deps.lifetimeSignal;
|
|
1509
1581
|
this.#hooks = deps.hooks;
|
|
1510
1582
|
this.#assertActive = deps.assertActive;
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
return this.#baseUrl;
|
|
1583
|
+
if (this.#lifetimeSignal && !this.#lifetimeSignal.aborted) this.#lifetimeSignal.addEventListener("abort", () => {
|
|
1584
|
+
for (const scope of this.#activeScopes) scope.abort(this.#lifetimeSignal?.reason);
|
|
1585
|
+
}, { once: true });
|
|
1515
1586
|
}
|
|
1516
1587
|
/**
|
|
1517
|
-
* Выполняет запрос
|
|
1588
|
+
* Выполняет низкоуровневый запрос через собранный конвейер.
|
|
1518
1589
|
*
|
|
1519
1590
|
* @typeParam T ожидаемая форма ответа после снятия обёртки `{ data: … }`
|
|
1520
1591
|
* @throws {ItdApiError} если сервер ответил статусом ≥ 400
|
|
1521
|
-
* @throws {ItdTimeoutError} если истёк
|
|
1592
|
+
* @throws {ItdTimeoutError} если истёк `timeout` попытки или `deadline` операции
|
|
1522
1593
|
* @throws {ItdAbortError} если запрос отменён через `signal`
|
|
1523
1594
|
* @throws {ItdNetworkError} если запрос не дошёл до сервера
|
|
1524
1595
|
*/
|
|
1525
1596
|
request(options) {
|
|
1526
|
-
|
|
1527
|
-
return this.#runWithLifecycle(request, (prepared) => this.#handler(prepared));
|
|
1597
|
+
return this.#run(identifyRequest(options));
|
|
1528
1598
|
}
|
|
1529
1599
|
/** Выполняет контракт операции; `next()` плагина возвращает результат после `read`. */
|
|
1530
1600
|
execute(operation, options) {
|
|
1531
|
-
return this.#run(operation, options);
|
|
1532
|
-
}
|
|
1533
|
-
#run(operation, options) {
|
|
1534
|
-
const request = identifyRequest({
|
|
1535
|
-
...options,
|
|
1536
|
-
operationId: operation.id,
|
|
1537
|
-
method: operation.method,
|
|
1538
|
-
retrySafety: options.retrySafety ?? operation.retrySafety
|
|
1539
|
-
});
|
|
1540
|
-
return this.#runWithLifecycle(request, async (prepared) => operation.read(await this.#handler(prepared), prepared));
|
|
1601
|
+
return this.#run(operationRequest(operation, options), { reader: operation.read });
|
|
1541
1602
|
}
|
|
1542
1603
|
/** Выполняет внутреннюю операцию финализации после начала `ItdClient.dispose()`. @internal */
|
|
1543
1604
|
cleanupOperation(operation, options) {
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
retrySafety: options.retrySafety ?? operation.retrySafety
|
|
1605
|
+
return this.#run(operationRequest(operation, options), {
|
|
1606
|
+
allowDisposed: true,
|
|
1607
|
+
reader: operation.read,
|
|
1608
|
+
disposeCleanup: true
|
|
1549
1609
|
});
|
|
1550
|
-
return this.#runWithLifecycle(request, async (prepared) => {
|
|
1551
|
-
const cleanupRequest = markDisposeCleanupRequest(prepared);
|
|
1552
|
-
return operation.read(await this.#handler(cleanupRequest), prepared);
|
|
1553
|
-
}, true);
|
|
1554
1610
|
}
|
|
1555
|
-
async #
|
|
1556
|
-
if (!allowDisposed) this.#assertActive?.();
|
|
1557
|
-
const
|
|
1558
|
-
const scope = createRequestAbortScope(request.signal,
|
|
1611
|
+
async #run(request, options = {}) {
|
|
1612
|
+
if (!options.allowDisposed) this.#assertActive?.();
|
|
1613
|
+
const deadline = request.deadline ?? this.#deadline;
|
|
1614
|
+
const scope = createRequestAbortScope(request.signal, void 0, this.#clock, {
|
|
1615
|
+
after: deadline,
|
|
1616
|
+
error: () => new ItdTimeoutError({
|
|
1617
|
+
timeout: deadline,
|
|
1618
|
+
method: request.method.toUpperCase(),
|
|
1619
|
+
path: request.path,
|
|
1620
|
+
budget: TimeoutBudget.Deadline
|
|
1621
|
+
})
|
|
1622
|
+
});
|
|
1623
|
+
if (this.#lifetimeSignal?.aborted) scope.abort(this.#lifetimeSignal.reason);
|
|
1624
|
+
else this.#activeScopes.add(scope);
|
|
1559
1625
|
const startedAt = this.#clock.now();
|
|
1560
|
-
|
|
1626
|
+
const tracked = initializeOperationState(request, {
|
|
1627
|
+
lifecycleSignal: scope.signal,
|
|
1628
|
+
reader: options.reader,
|
|
1629
|
+
disposeCleanup: options.disposeCleanup
|
|
1630
|
+
});
|
|
1561
1631
|
try {
|
|
1562
|
-
|
|
1563
|
-
...prepared,
|
|
1564
|
-
signal: scope.signal,
|
|
1565
|
-
timeout: 0
|
|
1566
|
-
}), scope.signal);
|
|
1567
|
-
return await waitForRequest(Promise.resolve(pending), scope.signal);
|
|
1632
|
+
return await waitForRequest(this.#handler(tracked), scope.signal);
|
|
1568
1633
|
} catch (error) {
|
|
1569
|
-
const failure = requestAbortError(scope,
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
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: joinUrl(request.baseUrl ?? this.#baseUrl, request.path) + 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;
|
|
1634
|
+
const failure = requestAbortError(scope, request, error);
|
|
1635
|
+
if (hasRequestHook(this.#hooks, "onError")) {
|
|
1636
|
+
if (!(wasRequestErrorReported(tracked, error) || wasRequestErrorReported(tracked, failure) || scope.signal.aborted && !claimAbortReport(tracked))) {
|
|
1637
|
+
markRequestErrorReported(tracked, error);
|
|
1638
|
+
markRequestErrorReported(tracked, failure);
|
|
1639
|
+
await this.#notifyError(tracked, scope.signal, startedAt, failure);
|
|
1600
1640
|
}
|
|
1601
1641
|
}
|
|
1602
1642
|
throw failure;
|
|
1603
1643
|
} finally {
|
|
1644
|
+
this.#activeScopes.delete(scope);
|
|
1604
1645
|
scope.cleanup();
|
|
1605
1646
|
}
|
|
1606
1647
|
}
|
|
1648
|
+
/** Сообщает об ошибке, которую транспорт не видел: очередь, авторизация, плагины, отмена. */
|
|
1649
|
+
async #notifyError(request, signal, startedAt, error) {
|
|
1650
|
+
let headers;
|
|
1651
|
+
try {
|
|
1652
|
+
headers = new Headers({
|
|
1653
|
+
...request.layerHeaders,
|
|
1654
|
+
...request.headers
|
|
1655
|
+
});
|
|
1656
|
+
} catch {
|
|
1657
|
+
headers = new Headers();
|
|
1658
|
+
}
|
|
1659
|
+
const notification = dispatchRequestHook(this.#hooks, "onError", {
|
|
1660
|
+
operationId: request.operationId,
|
|
1661
|
+
signal,
|
|
1662
|
+
method: request.method.toUpperCase(),
|
|
1663
|
+
path: request.path,
|
|
1664
|
+
url: joinUrl(request.baseUrl ?? this.#baseUrl, request.path) + buildQuery(request.query),
|
|
1665
|
+
headers,
|
|
1666
|
+
attempt: currentTransportAttempt(request) || 1,
|
|
1667
|
+
duration: this.#clock.now() - startedAt,
|
|
1668
|
+
error
|
|
1669
|
+
});
|
|
1670
|
+
try {
|
|
1671
|
+
await waitForRequest(notification, signal);
|
|
1672
|
+
} catch (hookError) {
|
|
1673
|
+
if (!signal.aborted) throw hookError;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1607
1676
|
};
|
|
1608
1677
|
//#endregion
|
|
1609
1678
|
//#region src/core/operation.ts
|
|
1679
|
+
/** HTTP-метод операции. */
|
|
1680
|
+
const OperationMethod = Object.freeze({
|
|
1681
|
+
Get: "GET",
|
|
1682
|
+
Post: "POST",
|
|
1683
|
+
Put: "PUT",
|
|
1684
|
+
Patch: "PATCH",
|
|
1685
|
+
Delete: "DELETE"
|
|
1686
|
+
});
|
|
1610
1687
|
/** Семантическая безопасность автоматического повтора операции. */
|
|
1611
1688
|
const RetrySafety = Object.freeze({
|
|
1612
1689
|
/** Автоматический повтор не создаёт неприемлемого эффекта; обычно это чтение. */
|
|
@@ -1676,7 +1753,7 @@ function isRequestBodyReplayable(request) {
|
|
|
1676
1753
|
/** Разрешает retrySafety запроса из явного override, каталога либо raw fallback. */
|
|
1677
1754
|
function resolveRetryPolicy(request, catalog) {
|
|
1678
1755
|
let retrySafety;
|
|
1679
|
-
const known = catalog.
|
|
1756
|
+
const known = catalog.definitionOf(request.operationId)?.retrySafety;
|
|
1680
1757
|
if (request.retrySafety !== void 0) retrySafety = request.retrySafety;
|
|
1681
1758
|
else if (known !== void 0) retrySafety = known;
|
|
1682
1759
|
else if (request.operationId === "raw" && SAFE_RAW_METHODS.has(request.method.toUpperCase())) retrySafety = RetrySafety.Safe;
|
|
@@ -1690,15 +1767,22 @@ function resolveRetryPolicy(request, catalog) {
|
|
|
1690
1767
|
};
|
|
1691
1768
|
}
|
|
1692
1769
|
/**
|
|
1770
|
+
* Ошибка, после которой повтор невозможен в принципе: отмена либо истёкший общий срок операции.
|
|
1771
|
+
* Таймаут отдельной попытки сюда не входит — он повторяется как сетевой сбой.
|
|
1772
|
+
*/
|
|
1773
|
+
function isTerminal(error) {
|
|
1774
|
+
return error instanceof ItdAbortError || error instanceof ItdTimeoutError && error.budget === TimeoutBudget.Deadline;
|
|
1775
|
+
}
|
|
1776
|
+
/**
|
|
1693
1777
|
* Стоит ли повторять запрос после этой ошибки.
|
|
1694
1778
|
*
|
|
1695
1779
|
* `429` гарантирует, что операция не была обработана, поэтому допускает даже unsafe retry.
|
|
1696
|
-
* Обрыв сети,
|
|
1780
|
+
* Обрыв сети, таймаут попытки и `5xx` такой гарантии не дают и требуют safe/idempotent операции.
|
|
1697
1781
|
* Ошибка подготовки файла возникает до обращения к серверу и потому зависит только от
|
|
1698
1782
|
* собственного признака retryable и повторяемости тела.
|
|
1699
1783
|
*/
|
|
1700
1784
|
function isRetryable(error, policy) {
|
|
1701
|
-
if (error
|
|
1785
|
+
if (isTerminal(error) || !policy.bodyReplayable) return false;
|
|
1702
1786
|
const repeatableOperation = policy.retrySafety === RetrySafety.Safe || policy.retrySafety === RetrySafety.Idempotent;
|
|
1703
1787
|
if (error instanceof ItdApiError) {
|
|
1704
1788
|
if (error.status === 429) return true;
|
|
@@ -1726,7 +1810,7 @@ function backoffDelay(retryAttempt, options, random) {
|
|
|
1726
1810
|
function createRetryScheduler(options, random = Math.random) {
|
|
1727
1811
|
return (error, retryAttempt, policy) => {
|
|
1728
1812
|
if (retryAttempt >= options.attempts) return void 0;
|
|
1729
|
-
if (error
|
|
1813
|
+
if (isTerminal(error) || !policy.bodyReplayable) return void 0;
|
|
1730
1814
|
if (options.shouldRetry) return options.shouldRetry(error, retryAttempt, policy) ? backoffDelay(retryAttempt, options, random) : void 0;
|
|
1731
1815
|
if (!isRetryable(error, policy)) return void 0;
|
|
1732
1816
|
if (error instanceof ItdApiError && error.retryAfter !== void 0) return error.retryAfter > options.maxDelay ? void 0 : error.retryAfter;
|
|
@@ -1735,6 +1819,8 @@ function createRetryScheduler(options, random = Math.random) {
|
|
|
1735
1819
|
}
|
|
1736
1820
|
//#endregion
|
|
1737
1821
|
//#region src/core/execution/middleware.ts
|
|
1822
|
+
/** Сигнал для запроса без lifecycle: никогда не срабатывает. */
|
|
1823
|
+
const IDLE_SIGNAL = new AbortController().signal;
|
|
1738
1824
|
/** Ожидание повтора, которое уважает отмену запроса. */
|
|
1739
1825
|
function sleep(clock, ms, signal) {
|
|
1740
1826
|
if (!signal) return new Promise((resolve) => clock.schedule(resolve, ms));
|
|
@@ -1752,6 +1838,39 @@ function sleep(clock, ms, signal) {
|
|
|
1752
1838
|
});
|
|
1753
1839
|
}
|
|
1754
1840
|
/**
|
|
1841
|
+
* Слой плагинов: обёртки логической операции.
|
|
1842
|
+
*
|
|
1843
|
+
* Transformer видит запрос и результат по одному разу, независимо от повторов и обновления
|
|
1844
|
+
* авторизации, и получает запрос с пользовательским `signal`; слои ниже — с общим сигналом
|
|
1845
|
+
* операции. Служебное состояние операции переносится на запрос, собранный обёрткой заново.
|
|
1846
|
+
* Здесь же к операции привязывается снимок attempt interceptors; сами они выполняются
|
|
1847
|
+
* транспортом на каждой попытке.
|
|
1848
|
+
*/
|
|
1849
|
+
function createPluginsMiddleware(plugins) {
|
|
1850
|
+
return (request, next) => {
|
|
1851
|
+
const lifecycle = lifecycleSignalOf(request) ?? request.signal;
|
|
1852
|
+
return plugins.run(request, (prepared) => {
|
|
1853
|
+
return next(withOperationState({
|
|
1854
|
+
...prepared,
|
|
1855
|
+
...lifecycle ? { signal: lifecycle } : {}
|
|
1856
|
+
}, request));
|
|
1857
|
+
}, lifecycle ?? IDLE_SIGNAL);
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
/**
|
|
1861
|
+
* Слой контракта операции: превращает разобранное тело ответа в публичный результат.
|
|
1862
|
+
*
|
|
1863
|
+
* Обёртки операции получают готовый результат метода, а не форму ответа сервера. У
|
|
1864
|
+
* `raw`-запроса функции чтения нет, тело возвращается как есть.
|
|
1865
|
+
*/
|
|
1866
|
+
function createDecodeMiddleware() {
|
|
1867
|
+
return async (request, next) => {
|
|
1868
|
+
const body = await next(request);
|
|
1869
|
+
const read = operationReaderOf(request);
|
|
1870
|
+
return read ? read(body, request) : body;
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
/**
|
|
1755
1874
|
* Слой очереди: ограничение конкурентности и частоты.
|
|
1756
1875
|
*
|
|
1757
1876
|
* Должен стоять непосредственно вокруг одной транспортной попытки: тогда ожидание retry
|
|
@@ -1763,13 +1882,6 @@ function createQueueMiddleware(schedule) {
|
|
|
1763
1882
|
return (request, next) => request.skipQueue ? next(request) : schedule(request, () => next(request));
|
|
1764
1883
|
}
|
|
1765
1884
|
/**
|
|
1766
|
-
* Слой плагинов.
|
|
1767
|
-
*
|
|
1768
|
-
* Стоит снаружи повторов и очереди: operation transformers видят запрос и ответ по одному
|
|
1769
|
-
* разу, иначе, например, текст поста зашифруется дважды. Здесь же к операции привязывается
|
|
1770
|
-
* snapshot attempt interceptors; сами они выполняются транспортом на каждой попытке.
|
|
1771
|
-
*/
|
|
1772
|
-
/**
|
|
1773
1885
|
* Слой сервисов.
|
|
1774
1886
|
*
|
|
1775
1887
|
* Запросу с полем `service` подставляет хост сервиса, его заголовки и `skipAuth`, если
|
|
@@ -1807,15 +1919,16 @@ function createServicesMiddleware(registry) {
|
|
|
1807
1919
|
}
|
|
1808
1920
|
/** Однократная auth-подготовка вне retry исходного запроса. */
|
|
1809
1921
|
function createAuthPreflightMiddleware(getAuth) {
|
|
1810
|
-
return
|
|
1811
|
-
if (
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1922
|
+
return (request, next) => {
|
|
1923
|
+
if (request.skipAuth) return next(request);
|
|
1924
|
+
const auth = getAuth();
|
|
1925
|
+
const preflight = auth.preflight;
|
|
1926
|
+
if (!preflight) return next(request);
|
|
1927
|
+
return (async () => {
|
|
1928
|
+
const pending = preflight.call(auth, request.skipAuthRefresh !== true);
|
|
1929
|
+
await (request.signal ? waitForRequest(pending, request.signal) : pending);
|
|
1930
|
+
return next(request);
|
|
1931
|
+
})();
|
|
1819
1932
|
};
|
|
1820
1933
|
}
|
|
1821
1934
|
/**
|
|
@@ -1824,18 +1937,22 @@ function createAuthPreflightMiddleware(getAuth) {
|
|
|
1824
1937
|
* Загрузка storage, внешний `getToken` и ленивый sign-in могут быть асинхронными; sign-in
|
|
1825
1938
|
* сам входит в ту же queue. Поэтому эти действия обязаны завершиться до захвата её слота.
|
|
1826
1939
|
*/
|
|
1827
|
-
function createAuthPreparationMiddleware(
|
|
1828
|
-
return
|
|
1829
|
-
if (
|
|
1830
|
-
|
|
1940
|
+
function createAuthPreparationMiddleware(getAuth) {
|
|
1941
|
+
return (request, next) => {
|
|
1942
|
+
if (request.skipAuth) return next(request);
|
|
1943
|
+
const auth = getAuth();
|
|
1944
|
+
const prepare = auth.prepare;
|
|
1945
|
+
if (!prepare) return next(request);
|
|
1946
|
+
return (async () => {
|
|
1831
1947
|
try {
|
|
1948
|
+
const pending = prepare.call(auth);
|
|
1832
1949
|
await (request.signal ? waitForRequest(pending, request.signal) : pending);
|
|
1833
1950
|
} catch (error) {
|
|
1834
1951
|
requestAuthRecoveryState(request).preparationErrors.add(error);
|
|
1835
1952
|
throw error;
|
|
1836
1953
|
}
|
|
1837
|
-
|
|
1838
|
-
|
|
1954
|
+
return next(request);
|
|
1955
|
+
})();
|
|
1839
1956
|
};
|
|
1840
1957
|
}
|
|
1841
1958
|
/**
|
|
@@ -1869,10 +1986,10 @@ function createAttemptMiddleware() {
|
|
|
1869
1986
|
*/
|
|
1870
1987
|
function createAuthRecoveryMiddleware(auth) {
|
|
1871
1988
|
return async (request, next) => {
|
|
1872
|
-
const recovery = requestAuthRecoveryState(request);
|
|
1873
1989
|
try {
|
|
1874
1990
|
return await next(request);
|
|
1875
1991
|
} catch (error) {
|
|
1992
|
+
const recovery = requestAuthRecoveryState(request);
|
|
1876
1993
|
const preparationFailed = recovery.preparationErrors.delete(error);
|
|
1877
1994
|
if (request.skipAuthRefresh || recovery.recovered || preparationFailed || !isItdApiError(error) || error.status !== 401) throw error;
|
|
1878
1995
|
const pending = auth.recover();
|
|
@@ -1901,55 +2018,62 @@ function resolveBackoff(retry, global) {
|
|
|
1901
2018
|
* Слой повторов.
|
|
1902
2019
|
*
|
|
1903
2020
|
* Ответ `429` обрабатывается отдельно от прочих ошибок лестницей пауз и с придержанием
|
|
1904
|
-
* всей очереди; сетевые
|
|
1905
|
-
* у отдельного запроса имеет приоритет над глобальной.
|
|
2021
|
+
* всей очереди; сетевые сбои, таймаут попытки и `5xx` — экспоненциальным откатом.
|
|
2022
|
+
* Настройка `retry` у отдельного запроса имеет приоритет над глобальной.
|
|
1906
2023
|
*/
|
|
1907
2024
|
function createRetryMiddleware(deps) {
|
|
1908
2025
|
const globalScheduler = deps.retry ? createRetryScheduler(deps.retry) : void 0;
|
|
1909
|
-
const
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
2026
|
+
const ladder = deps.rateLimitDelays;
|
|
2027
|
+
const ladderCeiling = ladder && ladder.length > 0 ? Math.max(...ladder) : 0;
|
|
2028
|
+
/** Пауза по лестнице `429`; `undefined` — лестница пройдена или пауза сервера слишком велика. */
|
|
2029
|
+
const ladderDelay = (error, rateLimitAttempt, request, policy) => {
|
|
2030
|
+
if (!ladder || !policy.bodyReplayable || rateLimitAttempt > ladder.length) return void 0;
|
|
2031
|
+
if (error.retryAfter !== void 0 && error.retryAfter > ladderCeiling) return void 0;
|
|
2032
|
+
const wait = error.retryAfter ?? ladder[rateLimitAttempt - 1];
|
|
2033
|
+
if (wait === void 0) return void 0;
|
|
2034
|
+
deps.pauseQueue?.(wait, request);
|
|
2035
|
+
deps.logger?.info(`лимит частоты ${request.method.toUpperCase()} ${request.path}, повтор ${rateLimitAttempt} через ${wait} мс`);
|
|
2036
|
+
return wait;
|
|
1919
2037
|
};
|
|
1920
|
-
return
|
|
1921
|
-
const
|
|
2038
|
+
return (request, next) => {
|
|
2039
|
+
const backoff = resolveBackoff(request.retry, globalScheduler);
|
|
2040
|
+
if (!backoff && (!ladder || ladder.length === 0)) return next(request);
|
|
1922
2041
|
const method = request.method.toUpperCase();
|
|
1923
2042
|
const policy = resolveRetryPolicy(request, deps.catalog);
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
2043
|
+
return (async () => {
|
|
2044
|
+
let retryAttempt = 0;
|
|
2045
|
+
let rateLimitAttempt = 0;
|
|
2046
|
+
for (;;) try {
|
|
2047
|
+
return await next(request);
|
|
2048
|
+
} catch (error) {
|
|
2049
|
+
if (request.signal?.aborted) throw error;
|
|
2050
|
+
const transportAttempt = currentTransportAttempt(request);
|
|
2051
|
+
const rateLimited = ladder !== void 0 && isItdRateLimitError(error);
|
|
2052
|
+
if (rateLimited) rateLimitAttempt += 1;
|
|
2053
|
+
else retryAttempt += 1;
|
|
2054
|
+
const delay = rateLimited ? ladderDelay(error, rateLimitAttempt, request, policy) : backoff?.(error, retryAttempt, policy);
|
|
2055
|
+
if (delay === void 0) throw error;
|
|
2056
|
+
if (hasRequestHook(deps.hooks, "onRetry")) {
|
|
2057
|
+
const notification = dispatchRequestHook(deps.hooks, "onRetry", {
|
|
2058
|
+
operationId: request.operationId,
|
|
2059
|
+
signal: request.signal,
|
|
2060
|
+
method,
|
|
2061
|
+
path: request.path,
|
|
2062
|
+
url: deps.buildUrl(request),
|
|
2063
|
+
headers: new Headers({
|
|
2064
|
+
...request.layerHeaders,
|
|
2065
|
+
...request.headers
|
|
2066
|
+
}),
|
|
2067
|
+
attempt: transportAttempt,
|
|
2068
|
+
error,
|
|
2069
|
+
delay
|
|
2070
|
+
});
|
|
2071
|
+
await (request.signal ? waitForRequest(notification, request.signal) : notification);
|
|
2072
|
+
}
|
|
2073
|
+
deps.logger?.info(`повтор ${method} ${request.path}, попытка ${transportAttempt + 1} через ${delay} мс`);
|
|
2074
|
+
await sleep(deps.clock ?? systemClock, delay, request.signal);
|
|
2075
|
+
}
|
|
2076
|
+
})();
|
|
1953
2077
|
};
|
|
1954
2078
|
}
|
|
1955
2079
|
//#endregion
|
|
@@ -1979,15 +2103,6 @@ function unwrapData(body) {
|
|
|
1979
2103
|
if (keys.length !== 1 || keys[0] !== "data") return body;
|
|
1980
2104
|
return body.data;
|
|
1981
2105
|
}
|
|
1982
|
-
/**
|
|
1983
|
-
* Обычный объект — не `null`, не массив.
|
|
1984
|
-
*
|
|
1985
|
-
* Живёт здесь, а не в каждом разборщике ответа: одна и та же проверка нужна и фабрике
|
|
1986
|
-
* ошибок, и приведению уведомлений.
|
|
1987
|
-
*/
|
|
1988
|
-
function isRecord(value) {
|
|
1989
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1990
|
-
}
|
|
1991
2106
|
/** Непустая строка либо `undefined`. Отличается от {@link pickString} тем, что берёт значение, а не поле. */
|
|
1992
2107
|
function asString(value) {
|
|
1993
2108
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
@@ -2031,6 +2146,13 @@ function pickString(source, field) {
|
|
|
2031
2146
|
//#endregion
|
|
2032
2147
|
//#region src/core/error-factory.ts
|
|
2033
2148
|
/**
|
|
2149
|
+
* Отличает строковый машинный код (`NOT_FOUND`) от обычного текста и HTML страницы прокси.
|
|
2150
|
+
* Ограничение длины не даёт случайно принять большое служебное тело за код ошибки.
|
|
2151
|
+
*/
|
|
2152
|
+
function readPlainTextErrorCode(value) {
|
|
2153
|
+
return value.length <= 128 && /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$/.test(value) ? value : void 0;
|
|
2154
|
+
}
|
|
2155
|
+
/**
|
|
2034
2156
|
* Сводит ошибки по полям к единой форме `{ поле: [сообщения] }`.
|
|
2035
2157
|
*
|
|
2036
2158
|
* Понимает две документированные формы:
|
|
@@ -2070,14 +2192,17 @@ function collectFieldErrors(source) {
|
|
|
2070
2192
|
*/
|
|
2071
2193
|
function parseErrorBody(body, status, statusText = "") {
|
|
2072
2194
|
const fallbackMessage = statusText ? `HTTP ${status} ${statusText}` : `HTTP ${status}`;
|
|
2073
|
-
if (typeof body === "string")
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2195
|
+
if (typeof body === "string") {
|
|
2196
|
+
const message = asString(body.trim()) ?? fallbackMessage;
|
|
2197
|
+
return {
|
|
2198
|
+
code: readPlainTextErrorCode(message) ?? "UNKNOWN_ERROR",
|
|
2199
|
+
message,
|
|
2200
|
+
detail: void 0,
|
|
2201
|
+
title: void 0,
|
|
2202
|
+
fieldErrors: {},
|
|
2203
|
+
userId: void 0
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2081
2206
|
if (!isRecord(body)) return {
|
|
2082
2207
|
code: "UNKNOWN_ERROR",
|
|
2083
2208
|
message: fallbackMessage,
|
|
@@ -2271,33 +2396,15 @@ async function readBody(response) {
|
|
|
2271
2396
|
const text = await response.text();
|
|
2272
2397
|
return text === "" ? void 0 : text;
|
|
2273
2398
|
}
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
const error = /* @__PURE__ */ new Error("Операция прервана");
|
|
2277
|
-
error.name = "AbortError";
|
|
2278
|
-
return error;
|
|
2279
|
-
}
|
|
2280
|
-
/** Прерывает ожидание промиса при срабатывании сигнала. */
|
|
2281
|
-
function abortable(promise, signal) {
|
|
2282
|
-
if (signal.aborted) {
|
|
2283
|
-
promise.catch(() => {});
|
|
2284
|
-
return Promise.reject(createAbortError());
|
|
2285
|
-
}
|
|
2286
|
-
let onAbort;
|
|
2287
|
-
const interrupted = new Promise((_resolve, reject) => {
|
|
2288
|
-
onAbort = () => reject(createAbortError());
|
|
2289
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
2290
|
-
});
|
|
2291
|
-
return Promise.race([promise, interrupted]).finally(() => {
|
|
2292
|
-
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
2293
|
-
});
|
|
2399
|
+
function describe(error) {
|
|
2400
|
+
return error instanceof Error ? error.message : String(error);
|
|
2294
2401
|
}
|
|
2295
2402
|
/**
|
|
2296
2403
|
* Единственное место, откуда библиотека ходит в сеть.
|
|
2297
2404
|
*
|
|
2298
|
-
* Отвечает за сборку URL, заголовки, cookie,
|
|
2299
|
-
* неудачи в типизированную ошибку. Авторизация, повторы, очередь и плагины — отдельные
|
|
2300
|
-
*
|
|
2405
|
+
* Отвечает за сборку URL, заголовки, cookie, срок одной попытки, разбор ответа и превращение
|
|
2406
|
+
* любой неудачи в типизированную ошибку. Авторизация, повторы, очередь и плагины — отдельные
|
|
2407
|
+
* стадии конвейера, и транспорт о них не знает.
|
|
2301
2408
|
*/
|
|
2302
2409
|
var Transport = class {
|
|
2303
2410
|
#config;
|
|
@@ -2306,141 +2413,114 @@ var Transport = class {
|
|
|
2306
2413
|
this.#config = config;
|
|
2307
2414
|
this.#deps = deps;
|
|
2308
2415
|
}
|
|
2309
|
-
/** Базовый URL, к которому обращается транспорт. */
|
|
2310
|
-
get baseUrl() {
|
|
2311
|
-
return this.#config.baseUrl;
|
|
2312
|
-
}
|
|
2313
2416
|
/**
|
|
2314
2417
|
* Выполняет один сетевой запрос.
|
|
2315
2418
|
*
|
|
2316
2419
|
* @throws {ItdApiError} если сервер ответил статусом ≥ 400
|
|
2317
|
-
* @throws {ItdTimeoutError} если истёк
|
|
2420
|
+
* @throws {ItdTimeoutError} если истёк срок попытки
|
|
2318
2421
|
* @throws {ItdAbortError} если запрос отменён через `signal`
|
|
2319
2422
|
* @throws {ItdNetworkError} если запрос не дошёл до сервера
|
|
2320
2423
|
*/
|
|
2321
2424
|
send = async (input) => {
|
|
2322
2425
|
const request = identifyRequest(input);
|
|
2323
|
-
const method = request.method.toUpperCase();
|
|
2324
|
-
const url = this.buildUrl(request);
|
|
2325
|
-
const attempt = request.attempt ?? 1;
|
|
2326
2426
|
const timeout = request.timeout ?? this.#config.timeout;
|
|
2327
|
-
const
|
|
2328
|
-
const
|
|
2427
|
+
const method = request.method.toUpperCase();
|
|
2428
|
+
const attempt = {
|
|
2429
|
+
request,
|
|
2430
|
+
method,
|
|
2431
|
+
url: this.buildUrl(request),
|
|
2432
|
+
abort: createRequestAbortScope(request.signal, void 0, this.#config.clock, {
|
|
2433
|
+
after: timeout,
|
|
2434
|
+
error: () => new ItdTimeoutError({
|
|
2435
|
+
timeout,
|
|
2436
|
+
method,
|
|
2437
|
+
path: request.path,
|
|
2438
|
+
budget: TimeoutBudget.Attempt
|
|
2439
|
+
})
|
|
2440
|
+
}),
|
|
2441
|
+
startedAt: this.#config.clock.now(),
|
|
2442
|
+
number: request.attempt ?? 1
|
|
2443
|
+
};
|
|
2444
|
+
const { signal } = attempt.abort;
|
|
2329
2445
|
let cleanupBody;
|
|
2330
2446
|
try {
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2447
|
+
let headers;
|
|
2448
|
+
try {
|
|
2449
|
+
headers = await waitForRequest(this.#buildHeaders(request, attempt.url), signal);
|
|
2450
|
+
} catch (error) {
|
|
2451
|
+
throw this.#toTransportError(attempt, error);
|
|
2452
|
+
}
|
|
2334
2453
|
let body;
|
|
2335
2454
|
try {
|
|
2336
|
-
const prepared = await this.#prepareBody(
|
|
2455
|
+
const prepared = await this.#prepareBody(attempt, headers);
|
|
2337
2456
|
body = prepared.body;
|
|
2338
2457
|
cleanupBody = prepared.cleanup;
|
|
2339
2458
|
} catch (error) {
|
|
2340
|
-
const failure =
|
|
2341
|
-
|
|
2342
|
-
operationId: request.operationId,
|
|
2343
|
-
signal: abort.signal,
|
|
2344
|
-
method,
|
|
2345
|
-
path: request.path,
|
|
2346
|
-
url,
|
|
2347
|
-
headers,
|
|
2348
|
-
attempt
|
|
2349
|
-
};
|
|
2350
|
-
await this.#dispatchErrorHook(request, {
|
|
2351
|
-
...context,
|
|
2352
|
-
duration: this.#config.clock.now() - startedAt,
|
|
2353
|
-
error: failure
|
|
2354
|
-
}, abort, timeout);
|
|
2459
|
+
const failure = signal.aborted || error instanceof ItdError ? this.#toTransportError(attempt, error) : new ItdConfigError(`Не удалось подготовить тело ${attempt.method} ${request.path}: ${describe(error)}`, { cause: error });
|
|
2460
|
+
await this.#report(attempt, headers, failure);
|
|
2355
2461
|
throw failure;
|
|
2356
2462
|
}
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
path: request.path,
|
|
2362
|
-
url,
|
|
2363
|
-
headers,
|
|
2364
|
-
attempt
|
|
2365
|
-
};
|
|
2366
|
-
try {
|
|
2367
|
-
await abortable(dispatchRequestHook(this.#config.hooks, "onRequest", context), abort.signal);
|
|
2463
|
+
let context;
|
|
2464
|
+
const contextOf = () => context ??= this.#context(attempt, headers);
|
|
2465
|
+
if (hasRequestHook(this.#config.hooks, "onRequest")) try {
|
|
2466
|
+
await waitForRequest(dispatchRequestHook(this.#config.hooks, "onRequest", { ...contextOf() }), signal);
|
|
2368
2467
|
} catch (error) {
|
|
2369
|
-
const failure =
|
|
2370
|
-
await this.#
|
|
2371
|
-
...context,
|
|
2372
|
-
duration: this.#config.clock.now() - startedAt,
|
|
2373
|
-
error: failure
|
|
2374
|
-
}, abort, timeout);
|
|
2468
|
+
const failure = this.#abortedOr(attempt, error);
|
|
2469
|
+
await this.#report(attempt, headers, failure);
|
|
2375
2470
|
throw failure;
|
|
2376
2471
|
}
|
|
2377
|
-
this.#config.logger
|
|
2472
|
+
const logger = this.#config.logger;
|
|
2473
|
+
if (logger && isLevelEnabled(logger, LogLevel.Debug)) logger.debug(`→ ${attempt.method} ${request.path}`, {
|
|
2378
2474
|
headers: redactHeaders(headers),
|
|
2379
2475
|
body: request.bodyFactory ? "[повторяемое тело]" : redactBody(request.body)
|
|
2380
2476
|
});
|
|
2381
2477
|
let response;
|
|
2382
2478
|
try {
|
|
2383
|
-
|
|
2384
|
-
method,
|
|
2385
|
-
headers,
|
|
2386
|
-
signal: abort.signal,
|
|
2387
|
-
...body !== void 0 ? { body } : {},
|
|
2388
|
-
...this.#config.sendCredentials ? { credentials: "include" } : {}
|
|
2389
|
-
};
|
|
2390
|
-
if (typeof ReadableStream !== "undefined" && body instanceof ReadableStream) init.duplex = "half";
|
|
2391
|
-
response = await runAttemptInterceptors(request, {
|
|
2392
|
-
...context,
|
|
2393
|
-
body,
|
|
2394
|
-
signal: abort.signal
|
|
2395
|
-
}, async () => {
|
|
2396
|
-
try {
|
|
2397
|
-
return await this.#config.fetch(url, init);
|
|
2398
|
-
} catch (error) {
|
|
2399
|
-
throw this.#toTransportError(error, abort, request, method, timeout);
|
|
2400
|
-
}
|
|
2401
|
-
});
|
|
2479
|
+
response = await waitForRequest(this.#fetch(attempt, headers, body, contextOf), signal);
|
|
2402
2480
|
} catch (error) {
|
|
2403
|
-
const
|
|
2404
|
-
await this.#
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
error
|
|
2408
|
-
}, abort, timeout);
|
|
2409
|
-
this.#config.logger?.warn(`× ${method} ${request.path} (${duration} мс): ${error instanceof Error ? error.message : String(error)}`);
|
|
2410
|
-
throw error;
|
|
2481
|
+
const failure = this.#abortedOr(attempt, error);
|
|
2482
|
+
await this.#report(attempt, headers, failure);
|
|
2483
|
+
this.#config.logger?.warn(`× ${attempt.method} ${request.path} (${this.#elapsed(attempt)} мс): ${describe(failure)}`);
|
|
2484
|
+
throw failure;
|
|
2411
2485
|
}
|
|
2412
2486
|
if (this.#deps.onRateLimit) {
|
|
2413
2487
|
const { limit, remaining } = readRateLimit(response.headers);
|
|
2414
2488
|
this.#deps.onRateLimit(limit, remaining, request);
|
|
2415
2489
|
}
|
|
2416
|
-
if (this.#config.useCookieJar) (request.cookieJar ?? this.#deps.cookies)?.setFromResponse(response.url || url, response);
|
|
2490
|
+
if (this.#config.useCookieJar) (request.cookieJar ?? this.#deps.cookies)?.setFromResponse(response.url || attempt.url, response);
|
|
2417
2491
|
if (response.ok && hasRequestHook(this.#config.hooks, "onResponse")) {
|
|
2418
2492
|
const hookResponse = response.clone();
|
|
2419
2493
|
try {
|
|
2420
|
-
await
|
|
2421
|
-
...
|
|
2494
|
+
await waitForRequest(dispatchRequestHook(this.#config.hooks, "onResponse", {
|
|
2495
|
+
...contextOf(),
|
|
2422
2496
|
status: response.status,
|
|
2423
|
-
duration: this.#
|
|
2497
|
+
duration: this.#elapsed(attempt),
|
|
2424
2498
|
response: hookResponse
|
|
2425
|
-
}),
|
|
2499
|
+
}), signal);
|
|
2426
2500
|
} catch (error) {
|
|
2427
2501
|
response.body?.cancel().catch(() => {});
|
|
2428
|
-
const failure =
|
|
2429
|
-
await this.#
|
|
2430
|
-
...context,
|
|
2431
|
-
duration: this.#config.clock.now() - startedAt,
|
|
2432
|
-
error: failure
|
|
2433
|
-
}, abort, timeout);
|
|
2502
|
+
const failure = this.#abortedOr(attempt, error);
|
|
2503
|
+
await this.#report(attempt, headers, failure);
|
|
2434
2504
|
throw failure;
|
|
2435
2505
|
} finally {
|
|
2436
2506
|
if (!hookResponse.bodyUsed) hookResponse.body?.cancel().catch(() => {});
|
|
2437
2507
|
}
|
|
2438
2508
|
}
|
|
2439
|
-
|
|
2440
|
-
|
|
2509
|
+
let payload;
|
|
2510
|
+
try {
|
|
2511
|
+
payload = await waitForRequest(readBody(response), signal);
|
|
2512
|
+
attempt.abort.disarm();
|
|
2513
|
+
} catch (error) {
|
|
2514
|
+
await response.body?.cancel().catch(() => {});
|
|
2515
|
+
const failure = this.#toTransportError(attempt, error);
|
|
2516
|
+
await this.#report(attempt, headers, failure);
|
|
2517
|
+
this.#config.logger?.warn(`× ${attempt.method} ${request.path}: не удалось прочитать тело ответа — ${failure.message}`);
|
|
2518
|
+
throw failure;
|
|
2519
|
+
}
|
|
2520
|
+
const duration = this.#elapsed(attempt);
|
|
2441
2521
|
if (!response.ok) {
|
|
2442
2522
|
const error = createApiError({
|
|
2443
|
-
method,
|
|
2523
|
+
method: attempt.method,
|
|
2444
2524
|
path: request.path,
|
|
2445
2525
|
status: response.status,
|
|
2446
2526
|
now: this.#config.clock.now(),
|
|
@@ -2449,58 +2529,96 @@ var Transport = class {
|
|
|
2449
2529
|
response,
|
|
2450
2530
|
body: payload
|
|
2451
2531
|
});
|
|
2452
|
-
await this.#
|
|
2453
|
-
|
|
2454
|
-
duration,
|
|
2455
|
-
error
|
|
2456
|
-
}, abort, timeout);
|
|
2457
|
-
this.#config.logger?.warn(`← ${response.status} ${method} ${request.path} (${duration} мс): ${error.message}`);
|
|
2532
|
+
await this.#report(attempt, headers, error);
|
|
2533
|
+
this.#config.logger?.warn(`← ${response.status} ${attempt.method} ${request.path} (${duration} мс): ${error.message}`);
|
|
2458
2534
|
throw error;
|
|
2459
2535
|
}
|
|
2460
|
-
this.#config.logger?.debug(`← ${response.status} ${method} ${request.path} (${duration} мс)`);
|
|
2536
|
+
this.#config.logger?.debug(`← ${response.status} ${attempt.method} ${request.path} (${duration} мс)`);
|
|
2461
2537
|
return request.raw ? payload : unwrapData(payload);
|
|
2462
2538
|
} finally {
|
|
2463
2539
|
try {
|
|
2464
2540
|
await cleanupBody?.();
|
|
2465
2541
|
} catch (error) {
|
|
2466
|
-
this.#config.logger?.warn(`не удалось закрыть тело ${method} ${request.path}`, error);
|
|
2542
|
+
this.#config.logger?.warn(`не удалось закрыть тело ${attempt.method} ${request.path}`, error);
|
|
2467
2543
|
} finally {
|
|
2468
|
-
abort.cleanup();
|
|
2544
|
+
attempt.abort.cleanup();
|
|
2469
2545
|
}
|
|
2470
2546
|
}
|
|
2471
2547
|
};
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2548
|
+
/**
|
|
2549
|
+
* Итоговый URL со строкой запроса. Нужен и слою повторов — для хука `onRetry`.
|
|
2550
|
+
*
|
|
2551
|
+
* Хост берётся из самого запроса, если он там задан: у сервисов платформы свои домены.
|
|
2552
|
+
*/
|
|
2553
|
+
buildUrl(request) {
|
|
2554
|
+
const base = request.baseUrl ?? this.#config.baseUrl;
|
|
2555
|
+
return joinUrl(base, request.path) + buildQuery(request.query);
|
|
2556
|
+
}
|
|
2557
|
+
/**
|
|
2558
|
+
* Собирает общие заголовки клиента: `User-Agent`, идентификатор устройства,
|
|
2559
|
+
* заголовки конфигурации и cookie для указанного адреса.
|
|
2560
|
+
*/
|
|
2561
|
+
async platformHeaders(url, cookieJar = this.#deps.cookies) {
|
|
2562
|
+
const headers = new Headers();
|
|
2563
|
+
headers.set("X-Requested-With", "XMLHttpRequest");
|
|
2564
|
+
if (this.#config.userAgent) setHeader(headers, "User-Agent", this.#config.userAgent);
|
|
2565
|
+
if (this.#deps.getDeviceId) setHeader(headers, "X-Device-Id", await this.#deps.getDeviceId());
|
|
2566
|
+
for (const [name, value] of Object.entries(this.#config.headers)) setHeader(headers, name, value);
|
|
2567
|
+
if (this.#config.useCookieJar && cookieJar) {
|
|
2568
|
+
const cookie = cookieJar.getHeader(url);
|
|
2569
|
+
if (cookie) setHeader(headers, "Cookie", cookie);
|
|
2485
2570
|
}
|
|
2571
|
+
return headers;
|
|
2572
|
+
}
|
|
2573
|
+
/** Данные попытки для хуков `onRequest`, `onResponse` и перехватчиков. */
|
|
2574
|
+
#context(attempt, headers) {
|
|
2575
|
+
return {
|
|
2576
|
+
operationId: attempt.request.operationId,
|
|
2577
|
+
signal: attempt.abort.signal,
|
|
2578
|
+
method: attempt.method,
|
|
2579
|
+
path: attempt.request.path,
|
|
2580
|
+
url: attempt.url,
|
|
2581
|
+
headers,
|
|
2582
|
+
attempt: attempt.number
|
|
2583
|
+
};
|
|
2584
|
+
}
|
|
2585
|
+
#elapsed(attempt) {
|
|
2586
|
+
return this.#config.clock.now() - attempt.startedAt;
|
|
2587
|
+
}
|
|
2588
|
+
/** Ошибка хука: отмена во время его работы становится транспортной ошибкой, остальное — как есть. */
|
|
2589
|
+
#abortedOr(attempt, error) {
|
|
2590
|
+
return attempt.abort.signal.aborted ? this.#toTransportError(attempt, error) : error;
|
|
2591
|
+
}
|
|
2592
|
+
/**
|
|
2593
|
+
* Дополняет общие заголовки значением `Accept`, заголовками конвейера и вызова.
|
|
2594
|
+
* Заголовки вызова применяются последними.
|
|
2595
|
+
*/
|
|
2596
|
+
async #buildHeaders(request, url) {
|
|
2597
|
+
const headers = await this.platformHeaders(url, request.cookieJar);
|
|
2598
|
+
if (!headers.has("Accept")) headers.set("Accept", "application/json");
|
|
2599
|
+
for (const [name, value] of Object.entries(request.layerHeaders ?? {})) setHeader(headers, name, value);
|
|
2600
|
+
for (const [name, value] of Object.entries(request.headers ?? {})) setHeader(headers, name, value);
|
|
2601
|
+
return headers;
|
|
2486
2602
|
}
|
|
2487
2603
|
/** Подготавливает тело внутри попытки, чтобы поток можно было открыть заново при retry. */
|
|
2488
|
-
async #prepareBody(
|
|
2604
|
+
async #prepareBody(attempt, headers) {
|
|
2605
|
+
const { request } = attempt;
|
|
2606
|
+
const { signal } = attempt.abort;
|
|
2489
2607
|
if (request.bodyFactory) {
|
|
2490
2608
|
if (request.body !== void 0 && request.body !== null) throw new ItdConfigError("body и bodyFactory нельзя задавать одновременно");
|
|
2491
2609
|
const pending = Promise.resolve(request.bodyFactory({
|
|
2492
2610
|
signal,
|
|
2493
|
-
attempt
|
|
2611
|
+
attempt: attempt.number
|
|
2494
2612
|
}));
|
|
2495
2613
|
let prepared;
|
|
2496
2614
|
try {
|
|
2497
|
-
prepared = await
|
|
2615
|
+
prepared = await waitForRequest(pending, signal);
|
|
2498
2616
|
} catch (error) {
|
|
2499
2617
|
if (signal.aborted) pending.then(async (late) => {
|
|
2500
2618
|
try {
|
|
2501
2619
|
await late.cleanup?.();
|
|
2502
2620
|
} catch (cleanupError) {
|
|
2503
|
-
this.#config.logger?.warn(`не удалось закрыть отложенное тело ${
|
|
2621
|
+
this.#config.logger?.warn(`не удалось закрыть отложенное тело ${attempt.method} ${request.path}`, cleanupError);
|
|
2504
2622
|
}
|
|
2505
2623
|
}).catch(() => {});
|
|
2506
2624
|
throw error;
|
|
@@ -2525,72 +2643,76 @@ var Transport = class {
|
|
|
2525
2643
|
cleanup: void 0
|
|
2526
2644
|
};
|
|
2527
2645
|
}
|
|
2528
|
-
/**
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2646
|
+
/** Вызывает `fetch` через перехватчики попытки; сбой сети становится ошибкой библиотеки. */
|
|
2647
|
+
#fetch(attempt, headers, body, contextOf) {
|
|
2648
|
+
const init = {
|
|
2649
|
+
method: attempt.method,
|
|
2650
|
+
headers,
|
|
2651
|
+
signal: attempt.abort.signal,
|
|
2652
|
+
...body !== void 0 ? { body } : {},
|
|
2653
|
+
...this.#config.sendCredentials ? { credentials: "include" } : {}
|
|
2654
|
+
};
|
|
2655
|
+
if (typeof ReadableStream !== "undefined" && body instanceof ReadableStream) init.duplex = "half";
|
|
2656
|
+
const execute = async () => {
|
|
2657
|
+
try {
|
|
2658
|
+
return await this.#config.fetch(attempt.url, init);
|
|
2659
|
+
} catch (error) {
|
|
2660
|
+
throw this.#toTransportError(attempt, error);
|
|
2661
|
+
}
|
|
2662
|
+
};
|
|
2663
|
+
const interceptors = attemptInterceptorScope(attempt.request);
|
|
2664
|
+
if (!interceptors) return execute();
|
|
2665
|
+
return runAttemptInterceptors(interceptors, {
|
|
2666
|
+
...contextOf(),
|
|
2667
|
+
body
|
|
2668
|
+
}, execute);
|
|
2543
2669
|
}
|
|
2544
2670
|
/**
|
|
2545
|
-
*
|
|
2671
|
+
* Сообщает хуку `onError` об ошибке попытки и отмечает её как переданную.
|
|
2546
2672
|
*
|
|
2547
|
-
*
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
/**
|
|
2554
|
-
* Собирает общие заголовки клиента: `User-Agent`, идентификатор устройства,
|
|
2555
|
-
* заголовки конфигурации и cookie для указанного адреса.
|
|
2673
|
+
* Ожидание хука ограничено lifecycle операции; срок попытки на него не действует.
|
|
2674
|
+
* Если ошибка попытки — сама отмена операции, о ней сообщает тот уровень, который заметил
|
|
2675
|
+
* её первым; второй хук не вызывает. Отмена, заставшая хук с другой ошибкой, остаётся за
|
|
2676
|
+
* верхней границей операции.
|
|
2677
|
+
*
|
|
2678
|
+
* @throws ошибку самого хука, если операция не отменена
|
|
2556
2679
|
*/
|
|
2557
|
-
async
|
|
2558
|
-
const
|
|
2559
|
-
|
|
2560
|
-
if (this.#config.
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2680
|
+
async #report(attempt, headers, error) {
|
|
2681
|
+
const { request } = attempt;
|
|
2682
|
+
attempt.abort.disarm();
|
|
2683
|
+
if (!hasRequestHook(this.#config.hooks, "onError")) return;
|
|
2684
|
+
markRequestErrorReported(request, error);
|
|
2685
|
+
const lifecycle = request.signal;
|
|
2686
|
+
const notify = () => dispatchRequestHook(this.#config.hooks, "onError", {
|
|
2687
|
+
...this.#context(attempt, headers),
|
|
2688
|
+
duration: this.#elapsed(attempt),
|
|
2689
|
+
error
|
|
2690
|
+
});
|
|
2691
|
+
if (lifecycle?.aborted) {
|
|
2692
|
+
if (claimAbortReport(request)) notify().catch(() => {});
|
|
2693
|
+
return;
|
|
2694
|
+
}
|
|
2695
|
+
try {
|
|
2696
|
+
const pending = notify();
|
|
2697
|
+
await (lifecycle ? waitForRequest(pending, lifecycle) : pending);
|
|
2698
|
+
} catch (hookError) {
|
|
2699
|
+
if (lifecycle?.aborted) return;
|
|
2700
|
+
markRequestErrorReported(request, hookError);
|
|
2701
|
+
throw hookError;
|
|
2566
2702
|
}
|
|
2567
|
-
return headers;
|
|
2568
|
-
}
|
|
2569
|
-
/**
|
|
2570
|
-
* Дополняет общие заголовки значением `Accept`, заголовками конвейера и вызова.
|
|
2571
|
-
* Заголовки вызова применяются последними.
|
|
2572
|
-
*/
|
|
2573
|
-
async #buildHeaders(request, url) {
|
|
2574
|
-
const headers = await this.platformHeaders(url, request.cookieJar);
|
|
2575
|
-
if (!headers.has("Accept")) headers.set("Accept", "application/json");
|
|
2576
|
-
for (const [name, value] of Object.entries(request.layerHeaders ?? {})) setHeader(headers, name, value);
|
|
2577
|
-
for (const [name, value] of Object.entries(request.headers ?? {})) setHeader(headers, name, value);
|
|
2578
|
-
return headers;
|
|
2579
2703
|
}
|
|
2580
|
-
/** Превращает исключение `fetch` в понятную ошибку библиотеки. */
|
|
2581
|
-
#toTransportError(
|
|
2582
|
-
const
|
|
2583
|
-
if (aborted &&
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
path: request.path
|
|
2587
|
-
});
|
|
2588
|
-
if (aborted) {
|
|
2704
|
+
/** Превращает исключение `fetch` или отмену в понятную ошибку библиотеки. */
|
|
2705
|
+
#toTransportError(attempt, error) {
|
|
2706
|
+
const { abort, method, request } = attempt;
|
|
2707
|
+
if (abort.signal.aborted || error instanceof Error && error.name === "AbortError") {
|
|
2708
|
+
const expired = abort.expired();
|
|
2709
|
+
if (expired) return expired;
|
|
2589
2710
|
const reason = abort.signal.reason;
|
|
2711
|
+
if (reason instanceof ItdTimeoutError) return reason;
|
|
2590
2712
|
return new ItdAbortError(`Запрос ${method} ${request.path} отменён`, reason !== void 0 ? { cause: reason } : void 0);
|
|
2591
2713
|
}
|
|
2592
2714
|
if (error instanceof ItdError) return error;
|
|
2593
|
-
return new ItdNetworkError(`Не удалось выполнить ${method} ${request.path}: ${
|
|
2715
|
+
return new ItdNetworkError(`Не удалось выполнить ${method} ${request.path}: ${describe(error)}`, {
|
|
2594
2716
|
method,
|
|
2595
2717
|
path: request.path,
|
|
2596
2718
|
cause: error
|
|
@@ -2602,6 +2724,7 @@ var Transport = class {
|
|
|
2602
2724
|
/** Имена стадий обработки запроса в порядке выполнения. @internal */
|
|
2603
2725
|
const ClientRuntimeStage = Object.freeze({
|
|
2604
2726
|
OperationPlugins: "operation_plugins",
|
|
2727
|
+
Decode: "decode",
|
|
2605
2728
|
Services: "services",
|
|
2606
2729
|
AuthPreflight: "auth_preflight",
|
|
2607
2730
|
Retry: "retry",
|
|
@@ -2649,14 +2772,27 @@ function createClientRuntime(options, internals) {
|
|
|
2649
2772
|
path: request.path
|
|
2650
2773
|
}) ?? catalog.bucketOf(request.operationId);
|
|
2651
2774
|
};
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2775
|
+
/** Origin направления, мемоизированный по базовому адресу. */
|
|
2776
|
+
const origins = /* @__PURE__ */ new Map();
|
|
2777
|
+
const destinationOf = (baseUrl) => {
|
|
2778
|
+
if (origins.has(baseUrl)) return origins.get(baseUrl);
|
|
2779
|
+
if (origins.size >= 256) origins.clear();
|
|
2780
|
+
const origin = originOf(baseUrl) || void 0;
|
|
2781
|
+
origins.set(baseUrl, origin);
|
|
2782
|
+
return origin;
|
|
2783
|
+
};
|
|
2784
|
+
const queueKeyFor = (request) => requestQueueKey(request, (target) => {
|
|
2785
|
+
const bucket = bucketFor(target);
|
|
2786
|
+
return {
|
|
2787
|
+
destination: destinationOf(target.baseUrl ?? config.baseUrl),
|
|
2788
|
+
bucket,
|
|
2789
|
+
definition: catalog.bucketDefinitionOf(bucket)
|
|
2790
|
+
};
|
|
2791
|
+
});
|
|
2656
2792
|
const queueFor = (request) => {
|
|
2657
2793
|
if (!queues) return void 0;
|
|
2658
2794
|
const key = queueKeyFor(request);
|
|
2659
|
-
return queues.for(key.destination, key.bucket);
|
|
2795
|
+
return queues.for(key.destination, key.bucket, key.definition);
|
|
2660
2796
|
};
|
|
2661
2797
|
/** Передаёт остаток из заголовков ответа бакету запроса; тот решает, тормозить ли себя. */
|
|
2662
2798
|
const observeRateLimit = (limit, remaining, request) => {
|
|
@@ -2665,16 +2801,21 @@ function createClientRuntime(options, internals) {
|
|
|
2665
2801
|
const waited = queue.observe(limit, remaining);
|
|
2666
2802
|
if (waited > 0) config.logger?.debug(`остаток лимита ${remaining} из ${limit ?? "?"}, бакет ${queue.bucket} ждёт ${waited} мс`);
|
|
2667
2803
|
};
|
|
2668
|
-
transport = new Transport({
|
|
2669
|
-
...config,
|
|
2670
|
-
hooks: config.hooks
|
|
2671
|
-
}, {
|
|
2804
|
+
transport = new Transport(config, {
|
|
2672
2805
|
onRateLimit: queues ? observeRateLimit : void 0,
|
|
2673
2806
|
cookies: config.useCookieJar ? jar : void 0,
|
|
2674
|
-
getDeviceId: () => auth.deviceId()
|
|
2675
|
-
lifetimeSignal: lifetime.signal
|
|
2807
|
+
getDeviceId: () => auth.deviceId()
|
|
2676
2808
|
});
|
|
2677
|
-
|
|
2809
|
+
/** Стадии логической операции: выполняются один раз на вызов метода. */
|
|
2810
|
+
const operationStages = [{
|
|
2811
|
+
name: ClientRuntimeStage.OperationPlugins,
|
|
2812
|
+
middleware: createPluginsMiddleware(plugins)
|
|
2813
|
+
}, {
|
|
2814
|
+
name: ClientRuntimeStage.Decode,
|
|
2815
|
+
middleware: createDecodeMiddleware()
|
|
2816
|
+
}];
|
|
2817
|
+
/** Стадии исполнения: от выбора сервиса до транспортной попытки. */
|
|
2818
|
+
const executionStages = [
|
|
2678
2819
|
{
|
|
2679
2820
|
name: ClientRuntimeStage.Services,
|
|
2680
2821
|
middleware: createServicesMiddleware(services)
|
|
@@ -2689,7 +2830,7 @@ function createClientRuntime(options, internals) {
|
|
|
2689
2830
|
clock: config.clock,
|
|
2690
2831
|
catalog,
|
|
2691
2832
|
retry: config.retry,
|
|
2692
|
-
rateLimitDelays: config.rateLimit?.retryDelays
|
|
2833
|
+
rateLimitDelays: config.rateLimit?.retryDelays,
|
|
2693
2834
|
pauseQueue: queues ? (ms, request) => queueFor(request)?.pause(ms) : void 0,
|
|
2694
2835
|
hooks: config.hooks,
|
|
2695
2836
|
logger: config.logger,
|
|
@@ -2702,39 +2843,41 @@ function createClientRuntime(options, internals) {
|
|
|
2702
2843
|
},
|
|
2703
2844
|
{
|
|
2704
2845
|
name: ClientRuntimeStage.AuthPreparation,
|
|
2705
|
-
middleware: createAuthPreparationMiddleware(
|
|
2846
|
+
middleware: createAuthPreparationMiddleware(() => auth)
|
|
2706
2847
|
}
|
|
2707
2848
|
];
|
|
2708
|
-
if (queues)
|
|
2849
|
+
if (queues) executionStages.push({
|
|
2709
2850
|
name: ClientRuntimeStage.Queue,
|
|
2710
2851
|
middleware: createQueueMiddleware((request, task) => {
|
|
2711
2852
|
const queue = queueFor(request);
|
|
2712
2853
|
return queue ? queue.schedule(task, request.signal) : task();
|
|
2713
2854
|
})
|
|
2714
2855
|
});
|
|
2715
|
-
|
|
2856
|
+
executionStages.push({
|
|
2716
2857
|
name: ClientRuntimeStage.Attempt,
|
|
2717
2858
|
middleware: createAttemptMiddleware()
|
|
2718
|
-
}
|
|
2719
|
-
stages.push({
|
|
2859
|
+
}, {
|
|
2720
2860
|
name: ClientRuntimeStage.AuthHeaders,
|
|
2721
2861
|
middleware: createAuthHeadersMiddleware({ currentHeaders: () => auth.currentHeaders() })
|
|
2722
2862
|
});
|
|
2723
|
-
const
|
|
2724
|
-
|
|
2863
|
+
const execution = composePipeline(executionStages.map(({ middleware }) => middleware), transport.send);
|
|
2864
|
+
/**
|
|
2865
|
+
* Проверки между стадиями операции и исполнения: обёртка плагина уже могла изменить запрос,
|
|
2866
|
+
* а после начала `dispose()` завершаются только служебные запросы финализации.
|
|
2867
|
+
*/
|
|
2868
|
+
const guardedExecution = (request) => {
|
|
2725
2869
|
try {
|
|
2726
2870
|
if (!isDisposeCleanupRequest(request)) internals.assertActive?.("выполнить новый запрос");
|
|
2727
2871
|
if (request.rateLimitBucket !== void 0) assertKnownBucket(request.rateLimitBucket, "rateLimitBucket", config.rateLimit?.bucket, catalog);
|
|
2728
2872
|
} catch (error) {
|
|
2729
2873
|
return Promise.reject(error);
|
|
2730
2874
|
}
|
|
2731
|
-
return
|
|
2875
|
+
return execution(request);
|
|
2732
2876
|
};
|
|
2733
2877
|
const http = new HttpClient({
|
|
2734
|
-
handler:
|
|
2735
|
-
plugins,
|
|
2878
|
+
handler: composePipeline(operationStages.map(({ middleware }) => middleware), guardedExecution),
|
|
2736
2879
|
baseUrl: config.baseUrl,
|
|
2737
|
-
|
|
2880
|
+
deadline: config.deadline,
|
|
2738
2881
|
clock: config.clock,
|
|
2739
2882
|
lifetimeSignal: lifetime.signal,
|
|
2740
2883
|
hooks: config.hooks,
|
|
@@ -2746,8 +2889,8 @@ function createClientRuntime(options, internals) {
|
|
|
2746
2889
|
cookies: jar
|
|
2747
2890
|
});
|
|
2748
2891
|
const stageOrder = Object.freeze([
|
|
2749
|
-
|
|
2750
|
-
...
|
|
2892
|
+
...operationStages.map(({ name }) => name),
|
|
2893
|
+
...executionStages.map(({ name }) => name),
|
|
2751
2894
|
ClientRuntimeStage.Transport
|
|
2752
2895
|
]);
|
|
2753
2896
|
return {
|
|
@@ -2780,7 +2923,6 @@ function createClientRuntime(options, internals) {
|
|
|
2780
2923
|
},
|
|
2781
2924
|
platformHeaders: (url) => transport.platformHeaders(url),
|
|
2782
2925
|
rateLimitState: () => queues?.states() ?? [],
|
|
2783
|
-
registerRateLimitBucket: (name, definition) => queues?.defineBucket(name, definition),
|
|
2784
2926
|
close: () => {
|
|
2785
2927
|
if (ownsQueues) queues?.stop();
|
|
2786
2928
|
},
|
|
@@ -3406,20 +3548,23 @@ function operationBucket(id) {
|
|
|
3406
3548
|
}
|
|
3407
3549
|
//#endregion
|
|
3408
3550
|
//#region src/domain/catalog.ts
|
|
3551
|
+
/** Ограничения встроенных бакетов: ёмкость из таблицы плюс встроенные поправки. */
|
|
3552
|
+
const BUCKET_DEFINITIONS = Object.freeze(Object.fromEntries(Object.entries(BUCKET_LIMITS).map(([name, limit]) => [name, Object.freeze({
|
|
3553
|
+
limit,
|
|
3554
|
+
...DEFAULT_BUCKET_OVERRIDES[name]
|
|
3555
|
+
})])));
|
|
3409
3556
|
/**
|
|
3410
3557
|
* Каталог операций итд.com, которым пользуется ядро.
|
|
3411
3558
|
*
|
|
3412
3559
|
* Единственное место, где generic request executor встречается с таблицами конкретного API.
|
|
3413
|
-
* Собирается из уже существующих функций каталога: собственной логики здесь нет.
|
|
3414
3560
|
*
|
|
3415
3561
|
* @internal
|
|
3416
3562
|
*/
|
|
3417
3563
|
const ITD_CATALOG = Object.freeze({
|
|
3418
3564
|
definitionOf: (id) => isBuiltInOperationId(id) ? OPERATIONS[id] : void 0,
|
|
3419
|
-
retrySafetyOf: (id) => isBuiltInOperationId(id) ? operationRetrySafety(id) : void 0,
|
|
3420
|
-
methodOf: (id) => isBuiltInOperationId(id) ? operationMethod(id) : void 0,
|
|
3421
3565
|
bucketOf: (id) => operationBucket(id),
|
|
3422
3566
|
isKnownBucket,
|
|
3567
|
+
bucketDefinitionOf: (name) => BUCKET_DEFINITIONS[name],
|
|
3423
3568
|
bucketLimits: BUCKET_LIMITS,
|
|
3424
3569
|
bucketOverrides: DEFAULT_BUCKET_OVERRIDES,
|
|
3425
3570
|
defaultBucket: DEFAULT_RATE_LIMIT_BUCKET
|
|
@@ -3914,7 +4059,6 @@ function localDeviceId() {
|
|
|
3914
4059
|
function anonymousAuth() {
|
|
3915
4060
|
return {
|
|
3916
4061
|
token: () => Promise.resolve(null),
|
|
3917
|
-
prepare: () => Promise.resolve(),
|
|
3918
4062
|
currentHeaders: () => ({}),
|
|
3919
4063
|
recover: () => Promise.resolve(false),
|
|
3920
4064
|
deviceId: localDeviceId(),
|
|
@@ -3936,7 +4080,6 @@ function bearerToken(accessToken) {
|
|
|
3936
4080
|
const headers = { Authorization: `Bearer ${accessToken}` };
|
|
3937
4081
|
return {
|
|
3938
4082
|
token: () => Promise.resolve(accessToken),
|
|
3939
|
-
prepare: () => Promise.resolve(),
|
|
3940
4083
|
currentHeaders: () => headers,
|
|
3941
4084
|
recover: () => Promise.resolve(false),
|
|
3942
4085
|
deviceId: localDeviceId(),
|
|
@@ -4106,6 +4249,6 @@ function resolveNotificationUrl(notification) {
|
|
|
4106
4249
|
return clickUrl || "/notifications";
|
|
4107
4250
|
}
|
|
4108
4251
|
//#endregion
|
|
4109
|
-
export { identityResult as $, SpanType as A, operationRetrySafety as B, ItdErrorCode as C, ReportReason as D, QrLocationPrecision as E, OPERATIONS as F,
|
|
4252
|
+
export { identityResult as $, SpanType as A, operationRetrySafety as B, ItdErrorCode as C, ReportReason as D, QrLocationPrecision as E, OPERATIONS as F, pickArray as G, DEFAULT_RATE_LIMIT_BUCKET as H, defineBuiltInOperation as I, pickObject as J, pickBoolean as K, isBuiltInOperationId as L, ViewSource as M, WallAccess as N, ReportTargetType as O, ITD_CATALOG as P, defineOperation as Q, operationBucket as R, InteractionType as S, NotificationType as T, createClientRuntime as U, BUCKET_LIMITS as V, createApiError as W, OperationMethod as X, pickString as Y, RetrySafety as Z, CaptchaType as _, tokenProvider as a, assertPluginRemovable as at, FeedTab as b, normalizeNotification as c, DEFAULT_STATUS_BASE_URL as ct, canonicalNotificationType as d, LIBRARY_VERSION as dt, voidResult as et, isKnownNotificationType as f, RateLimitPacing as ft, CaptchaField as g, CaptchaChoice as h, installAsyncDisposeFallback as ht, bearerToken as i, RequestQueuePool as it, ViewReason as j, ServiceState as k, readNotificationEvent as l, STATUS_SERVICE as lt, AttachmentType as m, systemClock as mt, formatNotificationText as n, requestAbortError as nt, passthroughOperation as o, orderPluginDefinitions as ot, AccessType as p, createDeadline as pt, pickNumber as q, anonymousAuth as r, mergeService as rt, voidOperation as s, DEFAULT_BASE_URL as st, resolveNotificationUrl as t, createRequestAbortScope as tt, readUnreadCountEvent as u, resolveRateLimit as ut, CommentSort as v, LikesVisibility as w, IncidentKind as x, EventChannelStatus as y, operationMethod as z };
|
|
4110
4253
|
|
|
4111
|
-
//# sourceMappingURL=url-
|
|
4254
|
+
//# sourceMappingURL=url-Bg61sk_Y.js.map
|