itd-api 0.10.4 → 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-B8cjz_Ed.cjs → events-C6tmj8aW.cjs} +63 -59
- package/dist/shared/events-C6tmj8aW.cjs.map +1 -0
- package/dist/shared/{events-DGdU8q1X.d.ts → events-CAb9G7Sf.d.ts} +2 -2
- package/dist/shared/{events-D2n7Yu1P.js → events-MWNcnfrq.js} +19 -15
- package/dist/shared/events-MWNcnfrq.js.map +1 -0
- package/dist/shared/{events-CK3zHntT.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-UsOGMRzS.d.ts → render-CBroJPcV.d.ts} +178 -252
- package/dist/shared/{render-9F_kR6sq.d.cts → render-DM7AhTZW.d.cts} +178 -252
- package/dist/shared/{render-C6CJ343E.js → render-DRh5_aRe.js} +128 -135
- package/dist/shared/render-DRh5_aRe.js.map +1 -0
- package/dist/shared/{render-BrrZOSV5.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-yB6T_H_-.d.ts → url-BOi-_mJT.d.cts} +143 -79
- package/dist/shared/{url-yB6T_H_-.d.cts → url-BOi-_mJT.d.ts} +143 -79
- package/dist/shared/{url-Dc7wLikf.js → url-Bg61sk_Y.js} +723 -590
- package/dist/shared/url-Bg61sk_Y.js.map +1 -0
- package/dist/shared/{url-BNTKqBMB.cjs → url-DTgkCWv5.cjs} +830 -709
- 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-B8cjz_Ed.cjs.map +0 -1
- package/dist/shared/events-D2n7Yu1P.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-BrrZOSV5.cjs.map +0 -1
- package/dist/shared/render-C6CJ343E.js.map +0 -1
- package/dist/shared/url-BNTKqBMB.cjs.map +0 -1
- package/dist/shared/url-Dc7wLikf.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
const
|
|
2
|
-
const require_redact = require("./redact-
|
|
1
|
+
const require_validate = require("./validate-CFx2dISs.cjs");
|
|
2
|
+
const require_redact = require("./redact-DoC7lSsp.cjs");
|
|
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 require_errors.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 require_errors.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,42 +99,21 @@ const DEFAULT_RATE_LIMIT_DELAYS = Object.freeze([
|
|
|
120
99
|
]);
|
|
121
100
|
function resolveHeaders(headers) {
|
|
122
101
|
if (headers === void 0) return {};
|
|
123
|
-
if (!
|
|
124
|
-
for (const [name, value] of Object.entries(headers)) if (typeof value !== "string") throw new
|
|
102
|
+
if (!require_validate.isObjectLike(headers)) throw new require_validate.ItdConfigError("headers должен быть объектом строк");
|
|
103
|
+
for (const [name, value] of Object.entries(headers)) if (typeof value !== "string") throw new require_validate.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 (!require_validate.isObjectLike(hooks)) throw new require_validate.ItdConfigError("hooks должен быть объектом");
|
|
130
109
|
for (const name of [
|
|
131
110
|
"onRequest",
|
|
132
111
|
"onResponse",
|
|
133
112
|
"onError",
|
|
134
113
|
"onRetry"
|
|
135
|
-
]) if (hooks[name] !== void 0 && typeof hooks[name] !== "function") throw new
|
|
114
|
+
]) if (hooks[name] !== void 0 && typeof hooks[name] !== "function") throw new require_validate.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 require_errors.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 require_errors.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 && !require_validate.isObjectLike(retry)) throw new require_validate.ItdConfigError("retry должен быть объектом или false");
|
|
171
129
|
const options = retry ?? {};
|
|
172
130
|
const attempts = options.attempts ?? 3;
|
|
173
|
-
if (!Number.isInteger(attempts) || attempts < 1) throw new
|
|
131
|
+
if (!Number.isInteger(attempts) || attempts < 1) throw new require_validate.ItdConfigError(`retry.attempts должен быть целым числом от 1, получено: ${attempts}`);
|
|
174
132
|
const jitter = options.jitter ?? .3;
|
|
175
|
-
if (!Number.isFinite(jitter) || jitter < 0 || jitter > 1) throw new
|
|
176
|
-
if (options.shouldRetry !== void 0 && typeof options.shouldRetry !== "function") throw new
|
|
177
|
-
const baseDelay =
|
|
178
|
-
const maxDelay =
|
|
133
|
+
if (!Number.isFinite(jitter) || jitter < 0 || jitter > 1) throw new require_validate.ItdConfigError(`retry.jitter должен быть в диапазоне 0…1, получено: ${jitter}`);
|
|
134
|
+
if (options.shouldRetry !== void 0 && typeof options.shouldRetry !== "function") throw new require_validate.ItdConfigError("retry.shouldRetry должен быть функцией");
|
|
135
|
+
const baseDelay = require_validate.requireNonNegative(options.baseDelay ?? 500, "retry.baseDelay");
|
|
136
|
+
const maxDelay = require_validate.requireNonNegative(options.maxDelay ?? 3e4, "retry.maxDelay");
|
|
179
137
|
if (attempts === 1) return void 0;
|
|
180
138
|
return {
|
|
181
139
|
attempts,
|
|
@@ -198,19 +156,19 @@ function resolveRetry(retry) {
|
|
|
198
156
|
*/
|
|
199
157
|
function assertKnownBucket(name, option, bucket, catalog) {
|
|
200
158
|
if (bucket !== void 0 || catalog.isKnownBucket(name)) return;
|
|
201
|
-
throw new
|
|
159
|
+
throw new require_validate.ItdConfigError(`${option}: бакета «${name}» нет. Известны: ${Object.keys(catalog.bucketLimits).join(", ")}`);
|
|
202
160
|
}
|
|
203
161
|
function requireConcurrency(value, name) {
|
|
204
|
-
if (!Number.isInteger(value) || value < 1) throw new
|
|
162
|
+
if (!Number.isInteger(value) || value < 1) throw new require_validate.ItdConfigError(`${name} должен быть целым числом от 1, получено: ${value}`);
|
|
205
163
|
return value;
|
|
206
164
|
}
|
|
207
165
|
function requireRps(value, name) {
|
|
208
|
-
if (!Number.isFinite(value) || value <= 0) throw new
|
|
166
|
+
if (!Number.isFinite(value) || value <= 0) throw new require_validate.ItdConfigError(`${name} должен быть положительным числом, получено: ${value}`);
|
|
209
167
|
return value;
|
|
210
168
|
}
|
|
211
169
|
function resolvePacing(pacing) {
|
|
212
170
|
const known = Object.values(RateLimitPacing);
|
|
213
|
-
if (pacing !== void 0 && !known.includes(pacing)) throw new
|
|
171
|
+
if (pacing !== void 0 && !known.includes(pacing)) throw new require_validate.ItdConfigError(`rateLimit.pacing должен быть одним из ${known.join(", ")}, получено: ${pacing}`);
|
|
214
172
|
return pacing ?? RateLimitPacing.React;
|
|
215
173
|
}
|
|
216
174
|
/**
|
|
@@ -223,13 +181,13 @@ 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 (!require_validate.isObjectLike(overrides)) throw new require_validate.ItdConfigError("rateLimit.bucketOverrides должен быть объектом");
|
|
227
185
|
const resolved = { ...builtIn };
|
|
228
186
|
for (const [name, override] of Object.entries(overrides)) {
|
|
229
|
-
if (!
|
|
187
|
+
if (!require_validate.isObjectLike(override)) throw new require_validate.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
|
-
if (override.limit !== void 0 && (!Number.isFinite(override.limit) || override.limit <= 0)) throw new
|
|
190
|
+
if (override.limit !== void 0 && (!Number.isFinite(override.limit) || override.limit <= 0)) throw new require_validate.ItdConfigError(`rateLimit.bucketOverrides.${name}.limit должен быть положительным числом, получено: ${override.limit}`);
|
|
233
191
|
if (override.rps !== void 0) requireRps(override.rps, `rateLimit.bucketOverrides.${name}.rps`);
|
|
234
192
|
const built = builtIn[name];
|
|
235
193
|
resolved[name] = Object.freeze({
|
|
@@ -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 && !require_validate.isObjectLike(rateLimit)) throw new require_validate.ItdConfigError("rateLimit должен быть объектом или false");
|
|
256
214
|
const defaults = {
|
|
257
215
|
concurrency: 6,
|
|
258
216
|
rps: void 0,
|
|
@@ -262,17 +220,16 @@ 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;
|
|
269
226
|
const concurrency = requireConcurrency(rateLimit.concurrency ?? 6, "rateLimit.concurrency");
|
|
270
227
|
if (rateLimit.rps !== void 0) requireRps(rateLimit.rps, "rateLimit.rps");
|
|
271
228
|
const retryDelays = rateLimit.retryDelays ?? defaults.retryDelays;
|
|
272
|
-
if (!Array.isArray(retryDelays)) throw new
|
|
273
|
-
for (const delay of retryDelays)
|
|
274
|
-
requireOptionalBoolean(rateLimit.buckets, "rateLimit.buckets");
|
|
275
|
-
if (rateLimit.bucket !== void 0 && typeof rateLimit.bucket !== "function") throw new
|
|
229
|
+
if (!Array.isArray(retryDelays)) throw new require_validate.ItdConfigError("rateLimit.retryDelays должен быть массивом чисел");
|
|
230
|
+
for (const delay of retryDelays) require_validate.requireNonNegative(delay, "rateLimit.retryDelays");
|
|
231
|
+
require_validate.requireOptionalBoolean(rateLimit.buckets, "rateLimit.buckets");
|
|
232
|
+
if (rateLimit.bucket !== void 0 && typeof rateLimit.bucket !== "function") throw new require_validate.ItdConfigError("rateLimit.bucket должен быть функцией");
|
|
276
233
|
return {
|
|
277
234
|
concurrency,
|
|
278
235
|
rps: rateLimit.rps,
|
|
@@ -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 (!require_validate.isObjectLike(services)) throw new require_validate.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 (!require_validate.isObjectLike(value)) throw new require_validate.ItdConfigError(`services.${name} должен быть URL или объектом сервиса`);
|
|
302
258
|
return {
|
|
303
259
|
...value,
|
|
304
260
|
name
|
|
@@ -315,19 +271,21 @@ function resolveServices(services) {
|
|
|
315
271
|
* @throws {ItdConfigError} при некорректных значениях
|
|
316
272
|
*/
|
|
317
273
|
function resolveRuntimeConfig(options, catalog) {
|
|
318
|
-
if (!
|
|
274
|
+
if (!require_validate.isObjectLike(options)) throw new require_validate.ItdConfigError("опции клиента должны быть объектом");
|
|
319
275
|
const mode = options.mode ?? require_redact.RuntimeMode.Auto;
|
|
320
|
-
if (!Object.values(require_redact.RuntimeMode).includes(mode)) throw new
|
|
321
|
-
const timeout =
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
if (options.
|
|
276
|
+
if (!Object.values(require_redact.RuntimeMode).includes(mode)) throw new require_validate.ItdConfigError(`mode должен быть одним из ${Object.values(require_redact.RuntimeMode).join(", ")}, получено: ${mode}`);
|
|
277
|
+
const timeout = require_validate.requireNonNegative(options.timeout ?? 3e4, "timeout");
|
|
278
|
+
const deadline = require_validate.requireNonNegative(options.deadline ?? 0, "deadline");
|
|
279
|
+
const shutdownTimeout = require_validate.requireNonNegative(options.shutdownTimeout ?? 1e4, "shutdownTimeout");
|
|
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 require_validate.ItdConfigError("clock должен предоставлять методы now() и schedule()");
|
|
281
|
+
if (options.userAgent !== void 0 && options.userAgent !== false && typeof options.userAgent !== "string") throw new require_validate.ItdConfigError("userAgent должен быть строкой или false");
|
|
325
282
|
return {
|
|
326
283
|
baseUrl: require_redact.normalizeBaseUrl(options.baseUrl ?? "https://xn--d1ah4a.com"),
|
|
327
284
|
services: resolveServices(options.services),
|
|
328
285
|
fetch: require_redact.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),
|
|
@@ -337,7 +295,7 @@ function resolveRuntimeConfig(options, catalog) {
|
|
|
337
295
|
mode,
|
|
338
296
|
useCookieJar: require_redact.shouldUseCookieJar(mode),
|
|
339
297
|
sendCredentials: require_redact.shouldSendCredentials(mode),
|
|
340
|
-
logger: resolveLogger(options.logger)
|
|
298
|
+
logger: require_redact.resolveLogger(options.logger)
|
|
341
299
|
};
|
|
342
300
|
}
|
|
343
301
|
//#endregion
|
|
@@ -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,15 +324,15 @@ 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
|
-
if (called) throw new
|
|
331
|
+
if (called) throw new require_validate.ItdConfigError(`attempt interceptor плагина «${plugin}» вызвал next() больше одного раза`);
|
|
374
332
|
called = true;
|
|
375
333
|
return next();
|
|
376
334
|
});
|
|
377
|
-
if (!isResponse(response)) throw new
|
|
335
|
+
if (!isResponse(response)) throw new require_validate.ItdConfigError(`attempt interceptor плагина «${plugin}» должен вернуть Response`);
|
|
378
336
|
return response;
|
|
379
337
|
}, execute)();
|
|
380
338
|
}
|
|
@@ -395,13 +353,13 @@ function isPluginName(value) {
|
|
|
395
353
|
function validateNameList(plugin, field) {
|
|
396
354
|
const values = plugin[field];
|
|
397
355
|
if (values === void 0) return;
|
|
398
|
-
if (!Array.isArray(values)) throw new
|
|
356
|
+
if (!Array.isArray(values)) throw new require_validate.ItdConfigError(`плагин «${plugin.name}»: ${field} должен быть массивом`);
|
|
399
357
|
const seen = /* @__PURE__ */ new Set();
|
|
400
358
|
for (const value of values) {
|
|
401
|
-
if (typeof value !== "string" || value.trim() === "") throw new
|
|
402
|
-
if (!isPluginName(value)) throw new
|
|
403
|
-
if (value === plugin.name) throw new
|
|
404
|
-
if (seen.has(value)) throw new
|
|
359
|
+
if (typeof value !== "string" || value.trim() === "") throw new require_validate.ItdConfigError(`плагин «${plugin.name}»: ${field} содержит пустое имя плагина`);
|
|
360
|
+
if (!isPluginName(value)) throw new require_validate.ItdConfigError(`плагин «${plugin.name}»: имя «${value}» в ${field} должно соответствовать ${PLUGIN_NAME_FORMAT}`);
|
|
361
|
+
if (value === plugin.name) throw new require_validate.ItdConfigError(`плагин «${plugin.name}» не может указать себя в ${field}`);
|
|
362
|
+
if (seen.has(value)) throw new require_validate.ItdConfigError(`плагин «${plugin.name}»: ${field} повторяет имя «${value}»`);
|
|
405
363
|
seen.add(value);
|
|
406
364
|
}
|
|
407
365
|
}
|
|
@@ -415,10 +373,10 @@ function validateNameList(plugin, field) {
|
|
|
415
373
|
* @internal
|
|
416
374
|
*/
|
|
417
375
|
function validatePluginDefinition(plugin) {
|
|
418
|
-
if (typeof plugin?.install !== "function") throw new
|
|
376
|
+
if (typeof plugin?.install !== "function") throw new require_validate.ItdConfigError("Плагин должен быть объектом с методом install()");
|
|
419
377
|
const name = plugin.name;
|
|
420
|
-
if (typeof name !== "string" || name.trim() === "") throw new
|
|
421
|
-
if (!isPluginName(name)) throw new
|
|
378
|
+
if (typeof name !== "string" || name.trim() === "") throw new require_validate.ItdConfigError("У плагина должно быть непустое имя");
|
|
379
|
+
if (!isPluginName(name)) throw new require_validate.ItdConfigError(`Имя плагина «${name}» должно соответствовать ${PLUGIN_NAME_FORMAT}`);
|
|
422
380
|
for (const field of RELATION_FIELDS) validateNameList(plugin, field);
|
|
423
381
|
}
|
|
424
382
|
function addEdge(from, to, edges, indegree) {
|
|
@@ -437,7 +395,7 @@ function orderPluginDefinitions(plugins) {
|
|
|
437
395
|
const byName = /* @__PURE__ */ new Map();
|
|
438
396
|
for (const [sequence, plugin] of plugins.entries()) {
|
|
439
397
|
validatePluginDefinition(plugin);
|
|
440
|
-
if (byName.has(plugin.name)) throw new
|
|
398
|
+
if (byName.has(plugin.name)) throw new require_validate.ItdConfigError(`плагин «${plugin.name}» уже подключён`);
|
|
441
399
|
const entry = {
|
|
442
400
|
plugin,
|
|
443
401
|
sequence
|
|
@@ -446,9 +404,9 @@ function orderPluginDefinitions(plugins) {
|
|
|
446
404
|
byName.set(plugin.name, entry);
|
|
447
405
|
}
|
|
448
406
|
for (const { plugin } of entries) {
|
|
449
|
-
for (const required of plugin.requires ?? []) if (!byName.has(required)) throw new
|
|
450
|
-
for (const conflict of plugin.conflicts ?? []) if (byName.has(conflict)) throw new
|
|
451
|
-
for (const { plugin: other } of entries) if (other.conflicts?.includes(plugin.name)) throw new
|
|
407
|
+
for (const required of plugin.requires ?? []) if (!byName.has(required)) throw new require_validate.ItdConfigError(`плагину «${plugin.name}» требуется плагин «${required}»`);
|
|
408
|
+
for (const conflict of plugin.conflicts ?? []) if (byName.has(conflict)) throw new require_validate.ItdConfigError(`плагин «${plugin.name}» несовместим с плагином «${conflict}»`);
|
|
409
|
+
for (const { plugin: other } of entries) if (other.conflicts?.includes(plugin.name)) throw new require_validate.ItdConfigError(`плагин «${plugin.name}» несовместим с плагином «${other.name}»`);
|
|
452
410
|
}
|
|
453
411
|
const edges = /* @__PURE__ */ new Map();
|
|
454
412
|
const indegree = /* @__PURE__ */ new Map();
|
|
@@ -482,14 +440,14 @@ function orderPluginDefinitions(plugins) {
|
|
|
482
440
|
}
|
|
483
441
|
if (ordered.length !== entries.length) {
|
|
484
442
|
const cycle = entries.filter(({ plugin }) => (indegree.get(plugin.name) ?? 0) > 0).map(({ plugin }) => plugin.name);
|
|
485
|
-
throw new
|
|
443
|
+
throw new require_validate.ItdConfigError(`циклический порядок плагинов: ${cycle.join(" → ")}`);
|
|
486
444
|
}
|
|
487
445
|
return ordered;
|
|
488
446
|
}
|
|
489
447
|
/** Проверяет, можно ли удалить плагин без нарушения обязательных зависимостей. @internal */
|
|
490
448
|
function assertPluginRemovable(plugins, name) {
|
|
491
449
|
const dependent = plugins.find((plugin) => plugin.requires?.includes(name));
|
|
492
|
-
if (dependent) throw new
|
|
450
|
+
if (dependent) throw new require_validate.ItdConfigError(`нельзя отключить плагин «${name}»: от него зависит «${dependent.name}»`);
|
|
493
451
|
}
|
|
494
452
|
//#endregion
|
|
495
453
|
//#region src/core/plugins/registry.ts
|
|
@@ -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
|
-
if (this.#removing.has(plugin.name)) throw new
|
|
530
|
-
orderPluginDefinitions([...this.#ordered.map((entry) => entry.plugin), plugin]);
|
|
491
|
+
if (this.#removing.has(plugin.name)) throw new require_validate.ItdConfigError(`плагин «${plugin.name}» ещё отключается; дождитесь завершения unuse() или dispose()`);
|
|
492
|
+
return orderPluginDefinitions([...this.#ordered.map((entry) => entry.plugin), plugin]);
|
|
531
493
|
}
|
|
532
494
|
/** Проверяет удаление без изменения реестра. @internal */
|
|
533
495
|
assertCanRemove(name) {
|
|
@@ -543,12 +505,11 @@ 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) => {
|
|
551
|
-
if (typeof value !== "function") throw new
|
|
512
|
+
if (typeof value !== "function") throw new require_validate.ItdConfigError(`плагин «${plugin.name}» передал в ${kind}.use() не функцию`);
|
|
552
513
|
values.push(value);
|
|
553
514
|
let active = true;
|
|
554
515
|
return () => {
|
|
@@ -566,7 +527,7 @@ var PluginRegistry = class {
|
|
|
566
527
|
},
|
|
567
528
|
attempts: { use: (interceptor) => register(interceptors, interceptor, "attempts") }
|
|
568
529
|
});
|
|
569
|
-
if (installed !== void 0 && typeof installed !== "function") throw new
|
|
530
|
+
if (installed !== void 0 && typeof installed !== "function") throw new require_validate.ItdConfigError(`install() плагина «${plugin.name}» должен вернуть функцию или void`);
|
|
570
531
|
const teardown = installed;
|
|
571
532
|
this.#entries.set(plugin.name, {
|
|
572
533
|
plugin,
|
|
@@ -579,7 +540,7 @@ var PluginRegistry = class {
|
|
|
579
540
|
});
|
|
580
541
|
this.#ordered = ordered.map((definition) => {
|
|
581
542
|
const entry = this.#entries.get(definition.name);
|
|
582
|
-
if (!entry) throw new
|
|
543
|
+
if (!entry) throw new require_validate.ItdConfigError(`плагин «${definition.name}» не установлен`);
|
|
583
544
|
return entry;
|
|
584
545
|
});
|
|
585
546
|
}
|
|
@@ -678,9 +639,9 @@ var PluginRegistry = class {
|
|
|
678
639
|
return interceptorScope.length === 0 ? prepared : withAttemptInterceptorScope(prepared, interceptorScope);
|
|
679
640
|
};
|
|
680
641
|
const validateContract = (prepared) => {
|
|
681
|
-
if (prepared.operationId !== operationId) throw new
|
|
682
|
-
if (prepared.method.toUpperCase() !== method) throw new
|
|
683
|
-
if (prepared.retrySafety !== void 0 && prepared.retrySafety !== retrySafety) throw new
|
|
642
|
+
if (prepared.operationId !== operationId) throw new require_validate.ItdConfigError(`operation transformer не может заменить operationId «${operationId}» на «${prepared.operationId}»`);
|
|
643
|
+
if (prepared.method.toUpperCase() !== method) throw new require_validate.ItdConfigError(`operation transformer не может заменить метод «${method}» операции «${operationId}» на «${prepared.method}»`);
|
|
644
|
+
if (prepared.retrySafety !== void 0 && prepared.retrySafety !== retrySafety) throw new require_validate.ItdConfigError(`operation transformer не может заменить retrySafety операции «${operationId}»`);
|
|
684
645
|
};
|
|
685
646
|
const chain = entries.flatMap((entry) => entry.transformers.map((transformer) => ({
|
|
686
647
|
plugin: entry.plugin.name,
|
|
@@ -688,7 +649,7 @@ var PluginRegistry = class {
|
|
|
688
649
|
}))).reduceRight((next, { plugin, transformer }) => (current) => {
|
|
689
650
|
let called = false;
|
|
690
651
|
return transformer(scoped(current), (prepared) => {
|
|
691
|
-
if (called) throw new
|
|
652
|
+
if (called) throw new require_validate.ItdConfigError(`operation transformer плагина «${plugin}» вызвал next() больше одного раза`);
|
|
692
653
|
called = true;
|
|
693
654
|
validateContract(prepared);
|
|
694
655
|
return next(scoped(prepared));
|
|
@@ -720,14 +681,14 @@ var PluginRegistry = class {
|
|
|
720
681
|
*/
|
|
721
682
|
async #release(entry, deadline) {
|
|
722
683
|
const errors = [];
|
|
723
|
-
if (!await deadline.wait(this.#waitForDrain(entry))) errors.push(new
|
|
684
|
+
if (!await deadline.wait(this.#waitForDrain(entry))) errors.push(new require_validate.ItdStateError(`плагин «${entry.plugin.name}» не завершил операции за ${this.#options.shutdownTimeout} мс`));
|
|
724
685
|
let teardownFailed = false;
|
|
725
686
|
let teardownFailure;
|
|
726
687
|
const teardown = Promise.resolve().then(() => entry.teardown?.()).catch((error) => {
|
|
727
688
|
teardownFailed = true;
|
|
728
689
|
teardownFailure = error;
|
|
729
690
|
});
|
|
730
|
-
if (!await deadline.wait(teardown)) errors.push(new
|
|
691
|
+
if (!await deadline.wait(teardown)) errors.push(new require_validate.ItdStateError(`плагин «${entry.plugin.name}» не завершил teardown за ${this.#options.shutdownTimeout} мс`));
|
|
731
692
|
else if (teardownFailed) throw teardownFailure;
|
|
732
693
|
return errors;
|
|
733
694
|
}
|
|
@@ -748,11 +709,11 @@ var PluginRegistry = class {
|
|
|
748
709
|
//#region src/core/scheduling/rate-limit.ts
|
|
749
710
|
/** Ошибка отмены запроса, который ещё не дошёл до транспорта. */
|
|
750
711
|
function queueAbortError() {
|
|
751
|
-
return new
|
|
712
|
+
return new require_validate.ItdAbortError("Запрос отменён во время ожидания очереди");
|
|
752
713
|
}
|
|
753
714
|
/** Ошибка запроса, которого застала остановка очереди. */
|
|
754
715
|
function queueStoppedError() {
|
|
755
|
-
return new
|
|
716
|
+
return new require_validate.ItdAbortError("Клиент закрыт, запрос отменён");
|
|
756
717
|
}
|
|
757
718
|
/** Длина окна лимита на сервере. */
|
|
758
719
|
const RATE_LIMIT_WINDOW = 6e4;
|
|
@@ -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 require_errors.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 require_validate.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
|
|
@@ -1203,15 +1209,15 @@ var ServiceRegistry = class {
|
|
|
1203
1209
|
*/
|
|
1204
1210
|
define(definition) {
|
|
1205
1211
|
const raw = definition?.name;
|
|
1206
|
-
if (typeof raw !== "string" || raw.trim() === "") throw new
|
|
1212
|
+
if (typeof raw !== "string" || raw.trim() === "") throw new require_validate.ItdConfigError("У сервиса должно быть непустое имя");
|
|
1207
1213
|
const name = raw.trim();
|
|
1208
|
-
if (this.#services.has(name)) throw new
|
|
1214
|
+
if (this.#services.has(name)) throw new require_validate.ItdConfigError(`Сервис «${name}» уже зарегистрирован и не может быть заменён`);
|
|
1209
1215
|
const baseUrl = require_redact.normalizeBaseUrl(definition.baseUrl);
|
|
1210
|
-
if (definition.auth !== void 0 && typeof definition.auth !== "boolean") throw new
|
|
1216
|
+
if (definition.auth !== void 0 && typeof definition.auth !== "boolean") throw new require_validate.ItdConfigError(`services.${name}.auth должен быть boolean`);
|
|
1211
1217
|
let headers;
|
|
1212
1218
|
if (definition.headers !== void 0) {
|
|
1213
|
-
if (typeof definition.headers !== "object" || definition.headers === null || Array.isArray(definition.headers)) throw new
|
|
1214
|
-
for (const [header, value] of Object.entries(definition.headers)) if (typeof value !== "string") throw new
|
|
1219
|
+
if (typeof definition.headers !== "object" || definition.headers === null || Array.isArray(definition.headers)) throw new require_validate.ItdConfigError(`services.${name}.headers должен быть объектом строк`);
|
|
1220
|
+
for (const [header, value] of Object.entries(definition.headers)) if (typeof value !== "string") throw new require_validate.ItdConfigError(`services.${name}.headers.${header} должен быть строкой`);
|
|
1215
1221
|
headers = Object.freeze({ ...definition.headers });
|
|
1216
1222
|
}
|
|
1217
1223
|
const normalized = {
|
|
@@ -1262,7 +1268,7 @@ var ServiceRegistry = class {
|
|
|
1262
1268
|
const service = this.#services.get(name);
|
|
1263
1269
|
if (!service) {
|
|
1264
1270
|
const known = [...this.#services.keys()];
|
|
1265
|
-
throw new
|
|
1271
|
+
throw new require_validate.ItdConfigError(`Сервис «${name}» не зарегистрирован. ` + (known.length > 0 ? `Известны: ${known.join(", ")}` : "Зарегистрируйте его через itd.defineService() или опцию services"));
|
|
1266
1272
|
}
|
|
1267
1273
|
return service;
|
|
1268
1274
|
}
|
|
@@ -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,89 +1327,155 @@ 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(() => {});
|
|
1334
|
-
return Promise.reject(new
|
|
1360
|
+
return Promise.reject(new require_validate.ItdAbortError(void 0, { cause: signal.reason }));
|
|
1335
1361
|
}
|
|
1336
1362
|
let onAbort;
|
|
1337
1363
|
const aborted = new Promise((_resolve, reject) => {
|
|
1338
|
-
onAbort = () => reject(new
|
|
1364
|
+
onAbort = () => reject(new require_validate.ItdAbortError(void 0, { cause: signal.reason }));
|
|
1339
1365
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
1340
1366
|
});
|
|
1341
1367
|
return Promise.race([promise, aborted]).finally(() => {
|
|
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 require_validate.ItdTimeoutError) return reason;
|
|
1386
|
+
if (fallback instanceof require_validate.ItdAbortError) return fallback;
|
|
1387
|
+
return new require_validate.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
|
-
|
|
1605
|
+
return this.#run(operationRequest(operation, options), {
|
|
1606
|
+
allowDisposed: true,
|
|
1607
|
+
reader: operation.read,
|
|
1608
|
+
disposeCleanup: true
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
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 require_validate.ItdTimeoutError({
|
|
1617
|
+
timeout: deadline,
|
|
1618
|
+
method: request.method.toUpperCase(),
|
|
1619
|
+
path: request.path,
|
|
1620
|
+
budget: require_validate.TimeoutBudget.Deadline
|
|
1621
|
+
})
|
|
1549
1622
|
});
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
return operation.read(await this.#handler(cleanupRequest), prepared);
|
|
1553
|
-
}, true);
|
|
1554
|
-
}
|
|
1555
|
-
async #runWithLifecycle(request, execute, allowDisposed = false) {
|
|
1556
|
-
if (!allowDisposed) this.#assertActive?.();
|
|
1557
|
-
const timeout = request.timeout ?? this.#timeout;
|
|
1558
|
-
const scope = createRequestAbortScope(request.signal, this.#lifetimeSignal, timeout, this.#clock);
|
|
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: require_redact.joinUrl(request.baseUrl ?? this.#baseUrl, request.path) + require_redact.buildQuery(request.query),
|
|
1591
|
-
headers,
|
|
1592
|
-
attempt: request.attempt ?? 1,
|
|
1593
|
-
duration: this.#clock.now() - startedAt,
|
|
1594
|
-
error: failure
|
|
1595
|
-
});
|
|
1596
|
-
try {
|
|
1597
|
-
await waitForRequest(notification, scope.signal);
|
|
1598
|
-
} catch (hookError) {
|
|
1599
|
-
if (!scope.signal.aborted) throw hookError;
|
|
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: require_redact.joinUrl(request.baseUrl ?? this.#baseUrl, request.path) + require_redact.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
|
/** Автоматический повтор не создаёт неприемлемого эффекта; обычно это чтение. */
|
|
@@ -1618,7 +1695,7 @@ const RetrySafety = Object.freeze({
|
|
|
1618
1695
|
});
|
|
1619
1696
|
function snapshotAnnotation(value, operationId, copies) {
|
|
1620
1697
|
if (value === null || typeof value !== "object" && typeof value !== "function") return value;
|
|
1621
|
-
if (typeof value === "function") throw new
|
|
1698
|
+
if (typeof value === "function") throw new require_validate.ItdConfigError(`annotations операции «${operationId}» не должны содержать функции`);
|
|
1622
1699
|
const existing = copies.get(value);
|
|
1623
1700
|
if (existing !== void 0) return existing;
|
|
1624
1701
|
if (Array.isArray(value)) {
|
|
@@ -1628,13 +1705,13 @@ function snapshotAnnotation(value, operationId, copies) {
|
|
|
1628
1705
|
return Object.freeze(copy);
|
|
1629
1706
|
}
|
|
1630
1707
|
const prototype = Object.getPrototypeOf(value);
|
|
1631
|
-
if (prototype !== Object.prototype && prototype !== null) throw new
|
|
1708
|
+
if (prototype !== Object.prototype && prototype !== null) throw new require_validate.ItdConfigError(`annotations операции «${operationId}» должны состоять из обычных объектов, массивов и примитивов`);
|
|
1632
1709
|
const copy = Object.create(prototype);
|
|
1633
1710
|
copies.set(value, copy);
|
|
1634
1711
|
for (const key of Reflect.ownKeys(value)) {
|
|
1635
1712
|
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1636
1713
|
if (!descriptor?.enumerable) continue;
|
|
1637
|
-
if (!("value" in descriptor)) throw new
|
|
1714
|
+
if (!("value" in descriptor)) throw new require_validate.ItdConfigError(`annotations операции «${operationId}» не должны содержать getters или setters`);
|
|
1638
1715
|
Object.defineProperty(copy, key, {
|
|
1639
1716
|
value: snapshotAnnotation(descriptor.value, operationId, copies),
|
|
1640
1717
|
enumerable: true,
|
|
@@ -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,23 +1767,30 @@ function resolveRetryPolicy(request, catalog) {
|
|
|
1690
1767
|
};
|
|
1691
1768
|
}
|
|
1692
1769
|
/**
|
|
1770
|
+
* Ошибка, после которой повтор невозможен в принципе: отмена либо истёкший общий срок операции.
|
|
1771
|
+
* Таймаут отдельной попытки сюда не входит — он повторяется как сетевой сбой.
|
|
1772
|
+
*/
|
|
1773
|
+
function isTerminal(error) {
|
|
1774
|
+
return error instanceof require_validate.ItdAbortError || error instanceof require_validate.ItdTimeoutError && error.budget === require_validate.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
|
-
if (error instanceof
|
|
1787
|
+
if (error instanceof require_validate.ItdApiError) {
|
|
1704
1788
|
if (error.status === 429) return true;
|
|
1705
1789
|
if (error.status >= 500) return repeatableOperation;
|
|
1706
1790
|
return false;
|
|
1707
1791
|
}
|
|
1708
|
-
if (error instanceof
|
|
1709
|
-
if (error instanceof
|
|
1792
|
+
if (error instanceof require_validate.ItdNetworkError || error instanceof require_validate.ItdTimeoutError) return repeatableOperation;
|
|
1793
|
+
if (error instanceof require_validate.ItdFileError) return error.retryable;
|
|
1710
1794
|
return false;
|
|
1711
1795
|
}
|
|
1712
1796
|
/** Экспоненциальная пауза со случайным разбросом. */
|
|
@@ -1726,19 +1810,21 @@ 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
|
-
if (error instanceof
|
|
1816
|
+
if (error instanceof require_validate.ItdApiError && error.retryAfter !== void 0) return error.retryAfter > options.maxDelay ? void 0 : error.retryAfter;
|
|
1733
1817
|
return backoffDelay(retryAttempt, options, random);
|
|
1734
1818
|
};
|
|
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));
|
|
1741
|
-
if (signal.aborted) return Promise.reject(new
|
|
1827
|
+
if (signal.aborted) return Promise.reject(new require_validate.ItdAbortError("Запрос отменён во время ожидания повтора"));
|
|
1742
1828
|
return new Promise((resolve, reject) => {
|
|
1743
1829
|
const cancel = clock.schedule(() => {
|
|
1744
1830
|
signal.removeEventListener("abort", onAbort);
|
|
@@ -1746,12 +1832,45 @@ function sleep(clock, ms, signal) {
|
|
|
1746
1832
|
}, ms);
|
|
1747
1833
|
const onAbort = () => {
|
|
1748
1834
|
cancel();
|
|
1749
|
-
reject(new
|
|
1835
|
+
reject(new require_validate.ItdAbortError("Запрос отменён во время ожидания повтора"));
|
|
1750
1836
|
};
|
|
1751
1837
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
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,12 +1986,12 @@ 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
|
-
if (request.skipAuthRefresh || recovery.recovered || preparationFailed || !
|
|
1994
|
+
if (request.skipAuthRefresh || recovery.recovered || preparationFailed || !require_validate.isItdApiError(error) || error.status !== 401) throw error;
|
|
1878
1995
|
const pending = auth.recover();
|
|
1879
1996
|
if (!await (request.signal ? waitForRequest(pending, request.signal) : pending)) throw error;
|
|
1880
1997
|
recovery.recovered = true;
|
|
@@ -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 && require_validate.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;
|
|
@@ -2047,7 +2162,7 @@ function readPlainTextErrorCode(value) {
|
|
|
2047
2162
|
function collectFieldErrors(source) {
|
|
2048
2163
|
const result = {};
|
|
2049
2164
|
const errors = source.errors;
|
|
2050
|
-
if (isRecord(errors)) {
|
|
2165
|
+
if (require_validate.isRecord(errors)) {
|
|
2051
2166
|
for (const [field, value] of Object.entries(errors)) if (Array.isArray(value)) {
|
|
2052
2167
|
const messages = value.filter((item) => typeof item === "string");
|
|
2053
2168
|
if (messages.length > 0) result[field] = messages;
|
|
@@ -2055,7 +2170,7 @@ function collectFieldErrors(source) {
|
|
|
2055
2170
|
}
|
|
2056
2171
|
const violations = source.violations;
|
|
2057
2172
|
if (Array.isArray(violations)) for (const violation of violations) {
|
|
2058
|
-
if (!isRecord(violation)) continue;
|
|
2173
|
+
if (!require_validate.isRecord(violation)) continue;
|
|
2059
2174
|
const field = asString(violation.field) ?? asString(violation.property);
|
|
2060
2175
|
const message = asString(violation.message);
|
|
2061
2176
|
if (!field || !message) continue;
|
|
@@ -2088,7 +2203,7 @@ function parseErrorBody(body, status, statusText = "") {
|
|
|
2088
2203
|
userId: void 0
|
|
2089
2204
|
};
|
|
2090
2205
|
}
|
|
2091
|
-
if (!isRecord(body)) return {
|
|
2206
|
+
if (!require_validate.isRecord(body)) return {
|
|
2092
2207
|
code: "UNKNOWN_ERROR",
|
|
2093
2208
|
message: fallbackMessage,
|
|
2094
2209
|
detail: void 0,
|
|
@@ -2107,7 +2222,7 @@ function parseErrorBody(body, status, statusText = "") {
|
|
|
2107
2222
|
userId: void 0
|
|
2108
2223
|
};
|
|
2109
2224
|
}
|
|
2110
|
-
const inner = isRecord(body.error) ? body.error : body;
|
|
2225
|
+
const inner = require_validate.isRecord(body.error) ? body.error : body;
|
|
2111
2226
|
const message = asString(inner.message) ?? asString(inner.detail) ?? asString(inner.title) ?? asString(body.error) ?? fallbackMessage;
|
|
2112
2227
|
return {
|
|
2113
2228
|
code: asString(inner.code) ?? asString(body.code) ?? "UNKNOWN_ERROR",
|
|
@@ -2174,30 +2289,30 @@ function getRequestId(headers) {
|
|
|
2174
2289
|
}
|
|
2175
2290
|
/** Соответствие кода ошибки конкретному классу. Приоритетнее, чем HTTP-статус. */
|
|
2176
2291
|
const CODE_TO_CLASS = {
|
|
2177
|
-
VALIDATION_ERROR:
|
|
2178
|
-
RATE_LIMIT_EXCEEDED:
|
|
2179
|
-
UNAUTHORIZED:
|
|
2180
|
-
SESSION_EXPIRED:
|
|
2181
|
-
SESSION_REVOKED:
|
|
2182
|
-
SESSION_INVALID_REFRESH_TOKEN:
|
|
2183
|
-
REFRESH_TOKEN_MISSING:
|
|
2184
|
-
SESSION_NOT_FOUND:
|
|
2185
|
-
ACCOUNT_INVALID_CREDENTIALS:
|
|
2186
|
-
ACCESS_DENIED:
|
|
2187
|
-
ENTITY_NOT_FOUND:
|
|
2188
|
-
NOT_FOUND:
|
|
2189
|
-
ENTITY_ALREADY_EXISTS:
|
|
2292
|
+
VALIDATION_ERROR: require_validate.ItdValidationError,
|
|
2293
|
+
RATE_LIMIT_EXCEEDED: require_validate.ItdRateLimitError,
|
|
2294
|
+
UNAUTHORIZED: require_validate.ItdAuthError,
|
|
2295
|
+
SESSION_EXPIRED: require_validate.ItdAuthError,
|
|
2296
|
+
SESSION_REVOKED: require_validate.ItdAuthError,
|
|
2297
|
+
SESSION_INVALID_REFRESH_TOKEN: require_validate.ItdAuthError,
|
|
2298
|
+
REFRESH_TOKEN_MISSING: require_validate.ItdAuthError,
|
|
2299
|
+
SESSION_NOT_FOUND: require_validate.ItdAuthError,
|
|
2300
|
+
ACCOUNT_INVALID_CREDENTIALS: require_validate.ItdAuthError,
|
|
2301
|
+
ACCESS_DENIED: require_validate.ItdForbiddenError,
|
|
2302
|
+
ENTITY_NOT_FOUND: require_validate.ItdNotFoundError,
|
|
2303
|
+
NOT_FOUND: require_validate.ItdNotFoundError,
|
|
2304
|
+
ENTITY_ALREADY_EXISTS: require_validate.ItdConflictError
|
|
2190
2305
|
};
|
|
2191
2306
|
/** Соответствие HTTP-статуса классу — запасной вариант, когда код ничего не говорит. */
|
|
2192
2307
|
function classByStatus(status) {
|
|
2193
|
-
if (status === 401) return
|
|
2194
|
-
if (status === 403) return
|
|
2195
|
-
if (status === 404) return
|
|
2196
|
-
if (status === 409) return
|
|
2197
|
-
if (status === 422) return
|
|
2198
|
-
if (status === 429) return
|
|
2199
|
-
if (status >= 500) return
|
|
2200
|
-
return
|
|
2308
|
+
if (status === 401) return require_validate.ItdAuthError;
|
|
2309
|
+
if (status === 403) return require_validate.ItdForbiddenError;
|
|
2310
|
+
if (status === 404) return require_validate.ItdNotFoundError;
|
|
2311
|
+
if (status === 409) return require_validate.ItdConflictError;
|
|
2312
|
+
if (status === 422) return require_validate.ItdValidationError;
|
|
2313
|
+
if (status === 429) return require_validate.ItdRateLimitError;
|
|
2314
|
+
if (status >= 500) return require_validate.ItdServerError;
|
|
2315
|
+
return require_validate.ItdApiError;
|
|
2201
2316
|
}
|
|
2202
2317
|
/**
|
|
2203
2318
|
* Готовит тело ответа к сохранению в `ItdApiError.raw`.
|
|
@@ -2208,7 +2323,7 @@ function classByStatus(status) {
|
|
|
2208
2323
|
* значения секретов заменяются заглушкой.
|
|
2209
2324
|
*/
|
|
2210
2325
|
function safeRawBody(body) {
|
|
2211
|
-
if (!isRecord(body) || body.type !== "validation" || !isRecord(body.found)) return body;
|
|
2326
|
+
if (!require_validate.isRecord(body) || body.type !== "validation" || !require_validate.isRecord(body.found)) return body;
|
|
2212
2327
|
return {
|
|
2213
2328
|
...body,
|
|
2214
2329
|
found: require_redact.redactBody(body.found)
|
|
@@ -2239,7 +2354,7 @@ function createApiError(context) {
|
|
|
2239
2354
|
response: context.response,
|
|
2240
2355
|
retryAfter: parseRetryAfter(context.headers?.get("retry-after"), context.now)
|
|
2241
2356
|
};
|
|
2242
|
-
if (parsed.code === "PHONE_VERIFICATION_REQUIRED") return new
|
|
2357
|
+
if (parsed.code === "PHONE_VERIFICATION_REQUIRED") return new require_validate.ItdPhoneVerificationError({
|
|
2243
2358
|
...init,
|
|
2244
2359
|
userId: parsed.userId
|
|
2245
2360
|
});
|
|
@@ -2252,7 +2367,7 @@ function setHeader(headers, name, value) {
|
|
|
2252
2367
|
try {
|
|
2253
2368
|
headers.set(name, value);
|
|
2254
2369
|
} catch (cause) {
|
|
2255
|
-
throw new
|
|
2370
|
+
throw new require_validate.ItdConfigError(`Некорректный HTTP-заголовок ${JSON.stringify(name)}: проверьте его имя и значение.`, { cause });
|
|
2256
2371
|
}
|
|
2257
2372
|
}
|
|
2258
2373
|
/** Тело, которое отправляется как есть, без сериализации в JSON. */
|
|
@@ -2281,33 +2396,15 @@ async function readBody(response) {
|
|
|
2281
2396
|
const text = await response.text();
|
|
2282
2397
|
return text === "" ? void 0 : text;
|
|
2283
2398
|
}
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
const error = /* @__PURE__ */ new Error("Операция прервана");
|
|
2287
|
-
error.name = "AbortError";
|
|
2288
|
-
return error;
|
|
2289
|
-
}
|
|
2290
|
-
/** Прерывает ожидание промиса при срабатывании сигнала. */
|
|
2291
|
-
function abortable(promise, signal) {
|
|
2292
|
-
if (signal.aborted) {
|
|
2293
|
-
promise.catch(() => {});
|
|
2294
|
-
return Promise.reject(createAbortError());
|
|
2295
|
-
}
|
|
2296
|
-
let onAbort;
|
|
2297
|
-
const interrupted = new Promise((_resolve, reject) => {
|
|
2298
|
-
onAbort = () => reject(createAbortError());
|
|
2299
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
2300
|
-
});
|
|
2301
|
-
return Promise.race([promise, interrupted]).finally(() => {
|
|
2302
|
-
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
2303
|
-
});
|
|
2399
|
+
function describe(error) {
|
|
2400
|
+
return error instanceof Error ? error.message : String(error);
|
|
2304
2401
|
}
|
|
2305
2402
|
/**
|
|
2306
2403
|
* Единственное место, откуда библиотека ходит в сеть.
|
|
2307
2404
|
*
|
|
2308
|
-
* Отвечает за сборку URL, заголовки, cookie,
|
|
2309
|
-
* неудачи в типизированную ошибку. Авторизация, повторы, очередь и плагины — отдельные
|
|
2310
|
-
*
|
|
2405
|
+
* Отвечает за сборку URL, заголовки, cookie, срок одной попытки, разбор ответа и превращение
|
|
2406
|
+
* любой неудачи в типизированную ошибку. Авторизация, повторы, очередь и плагины — отдельные
|
|
2407
|
+
* стадии конвейера, и транспорт о них не знает.
|
|
2311
2408
|
*/
|
|
2312
2409
|
var Transport = class {
|
|
2313
2410
|
#config;
|
|
@@ -2316,141 +2413,114 @@ var Transport = class {
|
|
|
2316
2413
|
this.#config = config;
|
|
2317
2414
|
this.#deps = deps;
|
|
2318
2415
|
}
|
|
2319
|
-
/** Базовый URL, к которому обращается транспорт. */
|
|
2320
|
-
get baseUrl() {
|
|
2321
|
-
return this.#config.baseUrl;
|
|
2322
|
-
}
|
|
2323
2416
|
/**
|
|
2324
2417
|
* Выполняет один сетевой запрос.
|
|
2325
2418
|
*
|
|
2326
2419
|
* @throws {ItdApiError} если сервер ответил статусом ≥ 400
|
|
2327
|
-
* @throws {ItdTimeoutError} если истёк
|
|
2420
|
+
* @throws {ItdTimeoutError} если истёк срок попытки
|
|
2328
2421
|
* @throws {ItdAbortError} если запрос отменён через `signal`
|
|
2329
2422
|
* @throws {ItdNetworkError} если запрос не дошёл до сервера
|
|
2330
2423
|
*/
|
|
2331
2424
|
send = async (input) => {
|
|
2332
2425
|
const request = identifyRequest(input);
|
|
2333
|
-
const method = request.method.toUpperCase();
|
|
2334
|
-
const url = this.buildUrl(request);
|
|
2335
|
-
const attempt = request.attempt ?? 1;
|
|
2336
2426
|
const timeout = request.timeout ?? this.#config.timeout;
|
|
2337
|
-
const
|
|
2338
|
-
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 require_validate.ItdTimeoutError({
|
|
2435
|
+
timeout,
|
|
2436
|
+
method,
|
|
2437
|
+
path: request.path,
|
|
2438
|
+
budget: require_validate.TimeoutBudget.Attempt
|
|
2439
|
+
})
|
|
2440
|
+
}),
|
|
2441
|
+
startedAt: this.#config.clock.now(),
|
|
2442
|
+
number: request.attempt ?? 1
|
|
2443
|
+
};
|
|
2444
|
+
const { signal } = attempt.abort;
|
|
2339
2445
|
let cleanupBody;
|
|
2340
2446
|
try {
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
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
|
+
}
|
|
2344
2453
|
let body;
|
|
2345
2454
|
try {
|
|
2346
|
-
const prepared = await this.#prepareBody(
|
|
2455
|
+
const prepared = await this.#prepareBody(attempt, headers);
|
|
2347
2456
|
body = prepared.body;
|
|
2348
2457
|
cleanupBody = prepared.cleanup;
|
|
2349
2458
|
} catch (error) {
|
|
2350
|
-
const failure =
|
|
2351
|
-
|
|
2352
|
-
operationId: request.operationId,
|
|
2353
|
-
signal: abort.signal,
|
|
2354
|
-
method,
|
|
2355
|
-
path: request.path,
|
|
2356
|
-
url,
|
|
2357
|
-
headers,
|
|
2358
|
-
attempt
|
|
2359
|
-
};
|
|
2360
|
-
await this.#dispatchErrorHook(request, {
|
|
2361
|
-
...context,
|
|
2362
|
-
duration: this.#config.clock.now() - startedAt,
|
|
2363
|
-
error: failure
|
|
2364
|
-
}, abort, timeout);
|
|
2459
|
+
const failure = signal.aborted || error instanceof require_validate.ItdError ? this.#toTransportError(attempt, error) : new require_validate.ItdConfigError(`Не удалось подготовить тело ${attempt.method} ${request.path}: ${describe(error)}`, { cause: error });
|
|
2460
|
+
await this.#report(attempt, headers, failure);
|
|
2365
2461
|
throw failure;
|
|
2366
2462
|
}
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
path: request.path,
|
|
2372
|
-
url,
|
|
2373
|
-
headers,
|
|
2374
|
-
attempt
|
|
2375
|
-
};
|
|
2376
|
-
try {
|
|
2377
|
-
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);
|
|
2378
2467
|
} catch (error) {
|
|
2379
|
-
const failure =
|
|
2380
|
-
await this.#
|
|
2381
|
-
...context,
|
|
2382
|
-
duration: this.#config.clock.now() - startedAt,
|
|
2383
|
-
error: failure
|
|
2384
|
-
}, abort, timeout);
|
|
2468
|
+
const failure = this.#abortedOr(attempt, error);
|
|
2469
|
+
await this.#report(attempt, headers, failure);
|
|
2385
2470
|
throw failure;
|
|
2386
2471
|
}
|
|
2387
|
-
this.#config.logger
|
|
2472
|
+
const logger = this.#config.logger;
|
|
2473
|
+
if (logger && require_redact.isLevelEnabled(logger, require_redact.LogLevel.Debug)) logger.debug(`→ ${attempt.method} ${request.path}`, {
|
|
2388
2474
|
headers: require_redact.redactHeaders(headers),
|
|
2389
2475
|
body: request.bodyFactory ? "[повторяемое тело]" : require_redact.redactBody(request.body)
|
|
2390
2476
|
});
|
|
2391
2477
|
let response;
|
|
2392
2478
|
try {
|
|
2393
|
-
|
|
2394
|
-
method,
|
|
2395
|
-
headers,
|
|
2396
|
-
signal: abort.signal,
|
|
2397
|
-
...body !== void 0 ? { body } : {},
|
|
2398
|
-
...this.#config.sendCredentials ? { credentials: "include" } : {}
|
|
2399
|
-
};
|
|
2400
|
-
if (typeof ReadableStream !== "undefined" && body instanceof ReadableStream) init.duplex = "half";
|
|
2401
|
-
response = await runAttemptInterceptors(request, {
|
|
2402
|
-
...context,
|
|
2403
|
-
body,
|
|
2404
|
-
signal: abort.signal
|
|
2405
|
-
}, async () => {
|
|
2406
|
-
try {
|
|
2407
|
-
return await this.#config.fetch(url, init);
|
|
2408
|
-
} catch (error) {
|
|
2409
|
-
throw this.#toTransportError(error, abort, request, method, timeout);
|
|
2410
|
-
}
|
|
2411
|
-
});
|
|
2479
|
+
response = await waitForRequest(this.#fetch(attempt, headers, body, contextOf), signal);
|
|
2412
2480
|
} catch (error) {
|
|
2413
|
-
const
|
|
2414
|
-
await this.#
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
error
|
|
2418
|
-
}, abort, timeout);
|
|
2419
|
-
this.#config.logger?.warn(`× ${method} ${request.path} (${duration} мс): ${error instanceof Error ? error.message : String(error)}`);
|
|
2420
|
-
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;
|
|
2421
2485
|
}
|
|
2422
2486
|
if (this.#deps.onRateLimit) {
|
|
2423
2487
|
const { limit, remaining } = readRateLimit(response.headers);
|
|
2424
2488
|
this.#deps.onRateLimit(limit, remaining, request);
|
|
2425
2489
|
}
|
|
2426
|
-
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);
|
|
2427
2491
|
if (response.ok && hasRequestHook(this.#config.hooks, "onResponse")) {
|
|
2428
2492
|
const hookResponse = response.clone();
|
|
2429
2493
|
try {
|
|
2430
|
-
await
|
|
2431
|
-
...
|
|
2494
|
+
await waitForRequest(dispatchRequestHook(this.#config.hooks, "onResponse", {
|
|
2495
|
+
...contextOf(),
|
|
2432
2496
|
status: response.status,
|
|
2433
|
-
duration: this.#
|
|
2497
|
+
duration: this.#elapsed(attempt),
|
|
2434
2498
|
response: hookResponse
|
|
2435
|
-
}),
|
|
2499
|
+
}), signal);
|
|
2436
2500
|
} catch (error) {
|
|
2437
2501
|
response.body?.cancel().catch(() => {});
|
|
2438
|
-
const failure =
|
|
2439
|
-
await this.#
|
|
2440
|
-
...context,
|
|
2441
|
-
duration: this.#config.clock.now() - startedAt,
|
|
2442
|
-
error: failure
|
|
2443
|
-
}, abort, timeout);
|
|
2502
|
+
const failure = this.#abortedOr(attempt, error);
|
|
2503
|
+
await this.#report(attempt, headers, failure);
|
|
2444
2504
|
throw failure;
|
|
2445
2505
|
} finally {
|
|
2446
2506
|
if (!hookResponse.bodyUsed) hookResponse.body?.cancel().catch(() => {});
|
|
2447
2507
|
}
|
|
2448
2508
|
}
|
|
2449
|
-
|
|
2450
|
-
|
|
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);
|
|
2451
2521
|
if (!response.ok) {
|
|
2452
2522
|
const error = createApiError({
|
|
2453
|
-
method,
|
|
2523
|
+
method: attempt.method,
|
|
2454
2524
|
path: request.path,
|
|
2455
2525
|
status: response.status,
|
|
2456
2526
|
now: this.#config.clock.now(),
|
|
@@ -2459,58 +2529,96 @@ var Transport = class {
|
|
|
2459
2529
|
response,
|
|
2460
2530
|
body: payload
|
|
2461
2531
|
});
|
|
2462
|
-
await this.#
|
|
2463
|
-
|
|
2464
|
-
duration,
|
|
2465
|
-
error
|
|
2466
|
-
}, abort, timeout);
|
|
2467
|
-
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}`);
|
|
2468
2534
|
throw error;
|
|
2469
2535
|
}
|
|
2470
|
-
this.#config.logger?.debug(`← ${response.status} ${method} ${request.path} (${duration} мс)`);
|
|
2536
|
+
this.#config.logger?.debug(`← ${response.status} ${attempt.method} ${request.path} (${duration} мс)`);
|
|
2471
2537
|
return request.raw ? payload : unwrapData(payload);
|
|
2472
2538
|
} finally {
|
|
2473
2539
|
try {
|
|
2474
2540
|
await cleanupBody?.();
|
|
2475
2541
|
} catch (error) {
|
|
2476
|
-
this.#config.logger?.warn(`не удалось закрыть тело ${method} ${request.path}`, error);
|
|
2542
|
+
this.#config.logger?.warn(`не удалось закрыть тело ${attempt.method} ${request.path}`, error);
|
|
2477
2543
|
} finally {
|
|
2478
|
-
abort.cleanup();
|
|
2544
|
+
attempt.abort.cleanup();
|
|
2479
2545
|
}
|
|
2480
2546
|
}
|
|
2481
2547
|
};
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2548
|
+
/**
|
|
2549
|
+
* Итоговый URL со строкой запроса. Нужен и слою повторов — для хука `onRetry`.
|
|
2550
|
+
*
|
|
2551
|
+
* Хост берётся из самого запроса, если он там задан: у сервисов платформы свои домены.
|
|
2552
|
+
*/
|
|
2553
|
+
buildUrl(request) {
|
|
2554
|
+
const base = request.baseUrl ?? this.#config.baseUrl;
|
|
2555
|
+
return require_redact.joinUrl(base, request.path) + require_redact.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);
|
|
2495
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;
|
|
2496
2602
|
}
|
|
2497
2603
|
/** Подготавливает тело внутри попытки, чтобы поток можно было открыть заново при retry. */
|
|
2498
|
-
async #prepareBody(
|
|
2604
|
+
async #prepareBody(attempt, headers) {
|
|
2605
|
+
const { request } = attempt;
|
|
2606
|
+
const { signal } = attempt.abort;
|
|
2499
2607
|
if (request.bodyFactory) {
|
|
2500
|
-
if (request.body !== void 0 && request.body !== null) throw new
|
|
2608
|
+
if (request.body !== void 0 && request.body !== null) throw new require_validate.ItdConfigError("body и bodyFactory нельзя задавать одновременно");
|
|
2501
2609
|
const pending = Promise.resolve(request.bodyFactory({
|
|
2502
2610
|
signal,
|
|
2503
|
-
attempt
|
|
2611
|
+
attempt: attempt.number
|
|
2504
2612
|
}));
|
|
2505
2613
|
let prepared;
|
|
2506
2614
|
try {
|
|
2507
|
-
prepared = await
|
|
2615
|
+
prepared = await waitForRequest(pending, signal);
|
|
2508
2616
|
} catch (error) {
|
|
2509
2617
|
if (signal.aborted) pending.then(async (late) => {
|
|
2510
2618
|
try {
|
|
2511
2619
|
await late.cleanup?.();
|
|
2512
2620
|
} catch (cleanupError) {
|
|
2513
|
-
this.#config.logger?.warn(`не удалось закрыть отложенное тело ${
|
|
2621
|
+
this.#config.logger?.warn(`не удалось закрыть отложенное тело ${attempt.method} ${request.path}`, cleanupError);
|
|
2514
2622
|
}
|
|
2515
2623
|
}).catch(() => {});
|
|
2516
2624
|
throw error;
|
|
@@ -2535,72 +2643,76 @@ var Transport = class {
|
|
|
2535
2643
|
cleanup: void 0
|
|
2536
2644
|
};
|
|
2537
2645
|
}
|
|
2538
|
-
/**
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
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);
|
|
2553
2669
|
}
|
|
2554
2670
|
/**
|
|
2555
|
-
*
|
|
2671
|
+
* Сообщает хуку `onError` об ошибке попытки и отмечает её как переданную.
|
|
2556
2672
|
*
|
|
2557
|
-
*
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
/**
|
|
2564
|
-
* Собирает общие заголовки клиента: `User-Agent`, идентификатор устройства,
|
|
2565
|
-
* заголовки конфигурации и cookie для указанного адреса.
|
|
2673
|
+
* Ожидание хука ограничено lifecycle операции; срок попытки на него не действует.
|
|
2674
|
+
* Если ошибка попытки — сама отмена операции, о ней сообщает тот уровень, который заметил
|
|
2675
|
+
* её первым; второй хук не вызывает. Отмена, заставшая хук с другой ошибкой, остаётся за
|
|
2676
|
+
* верхней границей операции.
|
|
2677
|
+
*
|
|
2678
|
+
* @throws ошибку самого хука, если операция не отменена
|
|
2566
2679
|
*/
|
|
2567
|
-
async
|
|
2568
|
-
const
|
|
2569
|
-
|
|
2570
|
-
if (this.#config.
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
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;
|
|
2576
2702
|
}
|
|
2577
|
-
return headers;
|
|
2578
|
-
}
|
|
2579
|
-
/**
|
|
2580
|
-
* Дополняет общие заголовки значением `Accept`, заголовками конвейера и вызова.
|
|
2581
|
-
* Заголовки вызова применяются последними.
|
|
2582
|
-
*/
|
|
2583
|
-
async #buildHeaders(request, url) {
|
|
2584
|
-
const headers = await this.platformHeaders(url, request.cookieJar);
|
|
2585
|
-
if (!headers.has("Accept")) headers.set("Accept", "application/json");
|
|
2586
|
-
for (const [name, value] of Object.entries(request.layerHeaders ?? {})) setHeader(headers, name, value);
|
|
2587
|
-
for (const [name, value] of Object.entries(request.headers ?? {})) setHeader(headers, name, value);
|
|
2588
|
-
return headers;
|
|
2589
2703
|
}
|
|
2590
|
-
/** Превращает исключение `fetch` в понятную ошибку библиотеки. */
|
|
2591
|
-
#toTransportError(
|
|
2592
|
-
const
|
|
2593
|
-
if (aborted &&
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
path: request.path
|
|
2597
|
-
});
|
|
2598
|
-
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;
|
|
2599
2710
|
const reason = abort.signal.reason;
|
|
2600
|
-
|
|
2711
|
+
if (reason instanceof require_validate.ItdTimeoutError) return reason;
|
|
2712
|
+
return new require_validate.ItdAbortError(`Запрос ${method} ${request.path} отменён`, reason !== void 0 ? { cause: reason } : void 0);
|
|
2601
2713
|
}
|
|
2602
|
-
if (error instanceof
|
|
2603
|
-
return new
|
|
2714
|
+
if (error instanceof require_validate.ItdError) return error;
|
|
2715
|
+
return new require_validate.ItdNetworkError(`Не удалось выполнить ${method} ${request.path}: ${describe(error)}`, {
|
|
2604
2716
|
method,
|
|
2605
2717
|
path: request.path,
|
|
2606
2718
|
cause: error
|
|
@@ -2612,6 +2724,7 @@ var Transport = class {
|
|
|
2612
2724
|
/** Имена стадий обработки запроса в порядке выполнения. @internal */
|
|
2613
2725
|
const ClientRuntimeStage = Object.freeze({
|
|
2614
2726
|
OperationPlugins: "operation_plugins",
|
|
2727
|
+
Decode: "decode",
|
|
2615
2728
|
Services: "services",
|
|
2616
2729
|
AuthPreflight: "auth_preflight",
|
|
2617
2730
|
Retry: "retry",
|
|
@@ -2659,14 +2772,27 @@ function createClientRuntime(options, internals) {
|
|
|
2659
2772
|
path: request.path
|
|
2660
2773
|
}) ?? catalog.bucketOf(request.operationId);
|
|
2661
2774
|
};
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
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 = require_redact.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
|
+
});
|
|
2666
2792
|
const queueFor = (request) => {
|
|
2667
2793
|
if (!queues) return void 0;
|
|
2668
2794
|
const key = queueKeyFor(request);
|
|
2669
|
-
return queues.for(key.destination, key.bucket);
|
|
2795
|
+
return queues.for(key.destination, key.bucket, key.definition);
|
|
2670
2796
|
};
|
|
2671
2797
|
/** Передаёт остаток из заголовков ответа бакету запроса; тот решает, тормозить ли себя. */
|
|
2672
2798
|
const observeRateLimit = (limit, remaining, request) => {
|
|
@@ -2675,16 +2801,21 @@ function createClientRuntime(options, internals) {
|
|
|
2675
2801
|
const waited = queue.observe(limit, remaining);
|
|
2676
2802
|
if (waited > 0) config.logger?.debug(`остаток лимита ${remaining} из ${limit ?? "?"}, бакет ${queue.bucket} ждёт ${waited} мс`);
|
|
2677
2803
|
};
|
|
2678
|
-
transport = new Transport({
|
|
2679
|
-
...config,
|
|
2680
|
-
hooks: config.hooks
|
|
2681
|
-
}, {
|
|
2804
|
+
transport = new Transport(config, {
|
|
2682
2805
|
onRateLimit: queues ? observeRateLimit : void 0,
|
|
2683
2806
|
cookies: config.useCookieJar ? jar : void 0,
|
|
2684
|
-
getDeviceId: () => auth.deviceId()
|
|
2685
|
-
lifetimeSignal: lifetime.signal
|
|
2807
|
+
getDeviceId: () => auth.deviceId()
|
|
2686
2808
|
});
|
|
2687
|
-
|
|
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 = [
|
|
2688
2819
|
{
|
|
2689
2820
|
name: ClientRuntimeStage.Services,
|
|
2690
2821
|
middleware: createServicesMiddleware(services)
|
|
@@ -2699,7 +2830,7 @@ function createClientRuntime(options, internals) {
|
|
|
2699
2830
|
clock: config.clock,
|
|
2700
2831
|
catalog,
|
|
2701
2832
|
retry: config.retry,
|
|
2702
|
-
rateLimitDelays: config.rateLimit?.retryDelays
|
|
2833
|
+
rateLimitDelays: config.rateLimit?.retryDelays,
|
|
2703
2834
|
pauseQueue: queues ? (ms, request) => queueFor(request)?.pause(ms) : void 0,
|
|
2704
2835
|
hooks: config.hooks,
|
|
2705
2836
|
logger: config.logger,
|
|
@@ -2712,39 +2843,41 @@ function createClientRuntime(options, internals) {
|
|
|
2712
2843
|
},
|
|
2713
2844
|
{
|
|
2714
2845
|
name: ClientRuntimeStage.AuthPreparation,
|
|
2715
|
-
middleware: createAuthPreparationMiddleware(
|
|
2846
|
+
middleware: createAuthPreparationMiddleware(() => auth)
|
|
2716
2847
|
}
|
|
2717
2848
|
];
|
|
2718
|
-
if (queues)
|
|
2849
|
+
if (queues) executionStages.push({
|
|
2719
2850
|
name: ClientRuntimeStage.Queue,
|
|
2720
2851
|
middleware: createQueueMiddleware((request, task) => {
|
|
2721
2852
|
const queue = queueFor(request);
|
|
2722
2853
|
return queue ? queue.schedule(task, request.signal) : task();
|
|
2723
2854
|
})
|
|
2724
2855
|
});
|
|
2725
|
-
|
|
2856
|
+
executionStages.push({
|
|
2726
2857
|
name: ClientRuntimeStage.Attempt,
|
|
2727
2858
|
middleware: createAttemptMiddleware()
|
|
2728
|
-
}
|
|
2729
|
-
stages.push({
|
|
2859
|
+
}, {
|
|
2730
2860
|
name: ClientRuntimeStage.AuthHeaders,
|
|
2731
2861
|
middleware: createAuthHeadersMiddleware({ currentHeaders: () => auth.currentHeaders() })
|
|
2732
2862
|
});
|
|
2733
|
-
const
|
|
2734
|
-
|
|
2863
|
+
const execution = composePipeline(executionStages.map(({ middleware }) => middleware), transport.send);
|
|
2864
|
+
/**
|
|
2865
|
+
* Проверки между стадиями операции и исполнения: обёртка плагина уже могла изменить запрос,
|
|
2866
|
+
* а после начала `dispose()` завершаются только служебные запросы финализации.
|
|
2867
|
+
*/
|
|
2868
|
+
const guardedExecution = (request) => {
|
|
2735
2869
|
try {
|
|
2736
2870
|
if (!isDisposeCleanupRequest(request)) internals.assertActive?.("выполнить новый запрос");
|
|
2737
2871
|
if (request.rateLimitBucket !== void 0) assertKnownBucket(request.rateLimitBucket, "rateLimitBucket", config.rateLimit?.bucket, catalog);
|
|
2738
2872
|
} catch (error) {
|
|
2739
2873
|
return Promise.reject(error);
|
|
2740
2874
|
}
|
|
2741
|
-
return
|
|
2875
|
+
return execution(request);
|
|
2742
2876
|
};
|
|
2743
2877
|
const http = new HttpClient({
|
|
2744
|
-
handler:
|
|
2745
|
-
plugins,
|
|
2878
|
+
handler: composePipeline(operationStages.map(({ middleware }) => middleware), guardedExecution),
|
|
2746
2879
|
baseUrl: config.baseUrl,
|
|
2747
|
-
|
|
2880
|
+
deadline: config.deadline,
|
|
2748
2881
|
clock: config.clock,
|
|
2749
2882
|
lifetimeSignal: lifetime.signal,
|
|
2750
2883
|
hooks: config.hooks,
|
|
@@ -2756,8 +2889,8 @@ function createClientRuntime(options, internals) {
|
|
|
2756
2889
|
cookies: jar
|
|
2757
2890
|
});
|
|
2758
2891
|
const stageOrder = Object.freeze([
|
|
2759
|
-
|
|
2760
|
-
...
|
|
2892
|
+
...operationStages.map(({ name }) => name),
|
|
2893
|
+
...executionStages.map(({ name }) => name),
|
|
2761
2894
|
ClientRuntimeStage.Transport
|
|
2762
2895
|
]);
|
|
2763
2896
|
return {
|
|
@@ -2790,12 +2923,11 @@ function createClientRuntime(options, internals) {
|
|
|
2790
2923
|
},
|
|
2791
2924
|
platformHeaders: (url) => transport.platformHeaders(url),
|
|
2792
2925
|
rateLimitState: () => queues?.states() ?? [],
|
|
2793
|
-
registerRateLimitBucket: (name, definition) => queues?.defineBucket(name, definition),
|
|
2794
2926
|
close: () => {
|
|
2795
2927
|
if (ownsQueues) queues?.stop();
|
|
2796
2928
|
},
|
|
2797
2929
|
dispose: async () => {
|
|
2798
|
-
lifetime.abort(new
|
|
2930
|
+
lifetime.abort(new require_validate.ItdAbortError("Клиент освобождён через dispose(), запрос отменён"));
|
|
2799
2931
|
if (ownsQueues) queues?.clear();
|
|
2800
2932
|
auth.dispose();
|
|
2801
2933
|
await plugins.dispose();
|
|
@@ -3416,20 +3548,23 @@ function operationBucket(id) {
|
|
|
3416
3548
|
}
|
|
3417
3549
|
//#endregion
|
|
3418
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
|
+
})])));
|
|
3419
3556
|
/**
|
|
3420
3557
|
* Каталог операций итд.com, которым пользуется ядро.
|
|
3421
3558
|
*
|
|
3422
3559
|
* Единственное место, где generic request executor встречается с таблицами конкретного API.
|
|
3423
|
-
* Собирается из уже существующих функций каталога: собственной логики здесь нет.
|
|
3424
3560
|
*
|
|
3425
3561
|
* @internal
|
|
3426
3562
|
*/
|
|
3427
3563
|
const ITD_CATALOG = Object.freeze({
|
|
3428
3564
|
definitionOf: (id) => isBuiltInOperationId(id) ? OPERATIONS[id] : void 0,
|
|
3429
|
-
retrySafetyOf: (id) => isBuiltInOperationId(id) ? operationRetrySafety(id) : void 0,
|
|
3430
|
-
methodOf: (id) => isBuiltInOperationId(id) ? operationMethod(id) : void 0,
|
|
3431
3565
|
bucketOf: (id) => operationBucket(id),
|
|
3432
3566
|
isKnownBucket,
|
|
3567
|
+
bucketDefinitionOf: (name) => BUCKET_DEFINITIONS[name],
|
|
3433
3568
|
bucketLimits: BUCKET_LIMITS,
|
|
3434
3569
|
bucketOverrides: DEFAULT_BUCKET_OVERRIDES,
|
|
3435
3570
|
defaultBucket: DEFAULT_RATE_LIMIT_BUCKET
|
|
@@ -3796,7 +3931,7 @@ function isKnownNotificationType(type) {
|
|
|
3796
3931
|
//#endregion
|
|
3797
3932
|
//#region src/notifications/normalize.ts
|
|
3798
3933
|
function asActor(value) {
|
|
3799
|
-
if (!isRecord(value)) return void 0;
|
|
3934
|
+
if (!require_validate.isRecord(value)) return void 0;
|
|
3800
3935
|
const id = asString(value.id);
|
|
3801
3936
|
if (!id) return void 0;
|
|
3802
3937
|
return {
|
|
@@ -3836,8 +3971,8 @@ function readActors(source) {
|
|
|
3836
3971
|
* ```
|
|
3837
3972
|
*/
|
|
3838
3973
|
function normalizeNotification(input) {
|
|
3839
|
-
const source = isRecord(input) ? input : {};
|
|
3840
|
-
const payload = isRecord(source.payload) ? source.payload : source;
|
|
3974
|
+
const source = require_validate.isRecord(input) ? input : {};
|
|
3975
|
+
const payload = require_validate.isRecord(source.payload) ? source.payload : source;
|
|
3841
3976
|
const rawType = asString(payload.type) ?? asString(source.type) ?? "";
|
|
3842
3977
|
const createdAt = asString(payload.createdAt) ?? asString(source.createdAt) ?? "";
|
|
3843
3978
|
const readAt = asString(payload.readAt) ?? asString(source.readAt);
|
|
@@ -3869,7 +4004,7 @@ function normalizeNotification(input) {
|
|
|
3869
4004
|
* счётчик непрочитанных и признак звука.
|
|
3870
4005
|
*/
|
|
3871
4006
|
function readNotificationEvent(data) {
|
|
3872
|
-
const source = isRecord(data) ? data : {};
|
|
4007
|
+
const source = require_validate.isRecord(data) ? data : {};
|
|
3873
4008
|
return {
|
|
3874
4009
|
notification: normalizeNotification(data),
|
|
3875
4010
|
unreadCount: typeof source.unreadCount === "number" ? source.unreadCount : void 0,
|
|
@@ -3882,8 +4017,8 @@ function readNotificationEvent(data) {
|
|
|
3882
4017
|
* Возвращает `undefined`, если сервер прислал событие без вложенного `payload`.
|
|
3883
4018
|
*/
|
|
3884
4019
|
function readUnreadCountEvent(data) {
|
|
3885
|
-
if (!isRecord(data)) return void 0;
|
|
3886
|
-
const payload = isRecord(data.payload) ? data.payload : void 0;
|
|
4020
|
+
if (!require_validate.isRecord(data)) return void 0;
|
|
4021
|
+
const payload = require_validate.isRecord(data.payload) ? data.payload : void 0;
|
|
3887
4022
|
if (!payload) return void 0;
|
|
3888
4023
|
return typeof payload.count === "number" ? payload.count : void 0;
|
|
3889
4024
|
}
|
|
@@ -3924,7 +4059,6 @@ function localDeviceId() {
|
|
|
3924
4059
|
function anonymousAuth() {
|
|
3925
4060
|
return {
|
|
3926
4061
|
token: () => Promise.resolve(null),
|
|
3927
|
-
prepare: () => Promise.resolve(),
|
|
3928
4062
|
currentHeaders: () => ({}),
|
|
3929
4063
|
recover: () => Promise.resolve(false),
|
|
3930
4064
|
deviceId: localDeviceId(),
|
|
@@ -3946,7 +4080,6 @@ function bearerToken(accessToken) {
|
|
|
3946
4080
|
const headers = { Authorization: `Bearer ${accessToken}` };
|
|
3947
4081
|
return {
|
|
3948
4082
|
token: () => Promise.resolve(accessToken),
|
|
3949
|
-
prepare: () => Promise.resolve(),
|
|
3950
4083
|
currentHeaders: () => headers,
|
|
3951
4084
|
recover: () => Promise.resolve(false),
|
|
3952
4085
|
deviceId: localDeviceId(),
|
|
@@ -4236,6 +4369,12 @@ Object.defineProperty(exports, "OPERATIONS", {
|
|
|
4236
4369
|
return OPERATIONS;
|
|
4237
4370
|
}
|
|
4238
4371
|
});
|
|
4372
|
+
Object.defineProperty(exports, "OperationMethod", {
|
|
4373
|
+
enumerable: true,
|
|
4374
|
+
get: function() {
|
|
4375
|
+
return OperationMethod;
|
|
4376
|
+
}
|
|
4377
|
+
});
|
|
4239
4378
|
Object.defineProperty(exports, "QrLocationPrecision", {
|
|
4240
4379
|
enumerable: true,
|
|
4241
4380
|
get: function() {
|
|
@@ -4398,18 +4537,6 @@ Object.defineProperty(exports, "isKnownNotificationType", {
|
|
|
4398
4537
|
return isKnownNotificationType;
|
|
4399
4538
|
}
|
|
4400
4539
|
});
|
|
4401
|
-
Object.defineProperty(exports, "isRecord", {
|
|
4402
|
-
enumerable: true,
|
|
4403
|
-
get: function() {
|
|
4404
|
-
return isRecord;
|
|
4405
|
-
}
|
|
4406
|
-
});
|
|
4407
|
-
Object.defineProperty(exports, "isRecord$1", {
|
|
4408
|
-
enumerable: true,
|
|
4409
|
-
get: function() {
|
|
4410
|
-
return isRecord$1;
|
|
4411
|
-
}
|
|
4412
|
-
});
|
|
4413
4540
|
Object.defineProperty(exports, "mergeService", {
|
|
4414
4541
|
enumerable: true,
|
|
4415
4542
|
get: function() {
|
|
@@ -4500,12 +4627,6 @@ Object.defineProperty(exports, "requestAbortError", {
|
|
|
4500
4627
|
return requestAbortError;
|
|
4501
4628
|
}
|
|
4502
4629
|
});
|
|
4503
|
-
Object.defineProperty(exports, "requireOptionalBoolean", {
|
|
4504
|
-
enumerable: true,
|
|
4505
|
-
get: function() {
|
|
4506
|
-
return requireOptionalBoolean;
|
|
4507
|
-
}
|
|
4508
|
-
});
|
|
4509
4630
|
Object.defineProperty(exports, "resolveNotificationUrl", {
|
|
4510
4631
|
enumerable: true,
|
|
4511
4632
|
get: function() {
|
|
@@ -4543,4 +4664,4 @@ Object.defineProperty(exports, "voidResult", {
|
|
|
4543
4664
|
}
|
|
4544
4665
|
});
|
|
4545
4666
|
|
|
4546
|
-
//# sourceMappingURL=url-
|
|
4667
|
+
//# sourceMappingURL=url-DTgkCWv5.cjs.map
|