itd-api 0.10.3 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/events/index.cjs +27 -25
- package/dist/events/index.d.cts +3 -3
- package/dist/events/index.d.ts +3 -3
- package/dist/events/index.js +5 -5
- package/dist/index.cjs +136 -116
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -7
- package/dist/index.d.ts +18 -7
- package/dist/index.js +46 -30
- package/dist/index.js.map +1 -1
- package/dist/node/index.cjs +13 -13
- package/dist/node/index.cjs.map +1 -1
- package/dist/node/index.js +5 -5
- package/dist/rest/index.cjs +43 -40
- package/dist/rest/index.cjs.map +1 -1
- package/dist/rest/index.d.cts +3 -3
- package/dist/rest/index.d.ts +3 -3
- package/dist/rest/index.js +6 -6
- package/dist/shared/{events-BVfqWyBw.cjs → events-C6tmj8aW.cjs} +63 -59
- package/dist/shared/events-C6tmj8aW.cjs.map +1 -0
- package/dist/shared/{events-Cp7ITueA.d.ts → events-CAb9G7Sf.d.ts} +2 -2
- package/dist/shared/{events-Cwj7UPAa.js → events-MWNcnfrq.js} +19 -15
- package/dist/shared/events-MWNcnfrq.js.map +1 -0
- package/dist/shared/{events-D0Wtt-If.d.cts → events-Sc75x6rT.d.cts} +2 -2
- package/dist/shared/{key-value-store-B5GYVEYZ.cjs → key-value-store-DYSPsqwq.cjs} +11 -14
- package/dist/shared/key-value-store-DYSPsqwq.cjs.map +1 -0
- package/dist/shared/{key-value-store-Bt2PSMxY.js → key-value-store-w1Dn5Pjb.js} +5 -8
- package/dist/shared/key-value-store-w1Dn5Pjb.js.map +1 -0
- package/dist/shared/{multi-storage-C3Ngtop1.js → multi-storage-BLBrGN9Q.js} +6 -6
- package/dist/shared/multi-storage-BLBrGN9Q.js.map +1 -0
- package/dist/shared/{multi-storage-s-fPtjoy.cjs → multi-storage-C7-Gz6Z7.cjs} +8 -8
- package/dist/shared/multi-storage-C7-Gz6Z7.cjs.map +1 -0
- package/dist/shared/{options-DjEtbVaf.cjs → options-5tywTYId.cjs} +13 -13
- package/dist/shared/{options-DjEtbVaf.cjs.map → options-5tywTYId.cjs.map} +1 -1
- package/dist/shared/{options-i4LrKfqx.js → options-BerKicCs.js} +3 -3
- package/dist/shared/{options-i4LrKfqx.js.map → options-BerKicCs.js.map} +1 -1
- package/dist/shared/{redact-DJV0XM3n.js → redact-C5oxIvcc.js} +79 -18
- package/dist/shared/redact-C5oxIvcc.js.map +1 -0
- package/dist/shared/{redact-lAmck1Z6.cjs → redact-DoC7lSsp.cjs} +109 -24
- package/dist/shared/redact-DoC7lSsp.cjs.map +1 -0
- package/dist/shared/{render-DRmuHoEy.d.ts → render-CBroJPcV.d.ts} +178 -252
- package/dist/shared/{render-CJO_L2CD.d.cts → render-DM7AhTZW.d.cts} +178 -252
- package/dist/shared/{render-BT74nC1l.js → render-DRh5_aRe.js} +128 -135
- package/dist/shared/render-DRh5_aRe.js.map +1 -0
- package/dist/shared/{render-CYJSDypQ.cjs → render-Dnc84B4M.cjs} +234 -241
- package/dist/shared/render-Dnc84B4M.cjs.map +1 -0
- package/dist/shared/{storage-DiDe0PZM.js → storage-86wiHeXD.js} +3 -3
- package/dist/shared/{storage-DiDe0PZM.js.map → storage-86wiHeXD.js.map} +1 -1
- package/dist/shared/{storage-CZJZoaz9.cjs → storage-CIjejZDt.cjs} +4 -4
- package/dist/shared/{storage-CZJZoaz9.cjs.map → storage-CIjejZDt.cjs.map} +1 -1
- package/dist/shared/{url-BLnX-LYt.d.cts → url-BOi-_mJT.d.cts} +143 -79
- package/dist/shared/{url-BLnX-LYt.d.ts → url-BOi-_mJT.d.ts} +143 -79
- package/dist/shared/{url-DS0-lrl-.js → url-Bg61sk_Y.js} +741 -598
- package/dist/shared/url-Bg61sk_Y.js.map +1 -0
- package/dist/shared/{url-BnO1aWsd.cjs → url-DTgkCWv5.cjs} +848 -717
- package/dist/shared/url-DTgkCWv5.cjs.map +1 -0
- package/dist/shared/{errors-BmP3TKoW.cjs → validate-CFx2dISs.cjs} +84 -5
- package/dist/shared/validate-CFx2dISs.cjs.map +1 -0
- package/dist/shared/{errors-GI10kZxk.js → validate-D7_kq3g5.js} +55 -6
- package/dist/shared/validate-D7_kq3g5.js.map +1 -0
- package/dist/web/index.cjs +4 -4
- package/dist/web/index.cjs.map +1 -1
- package/dist/web/index.js +3 -3
- package/package.json +1 -1
- package/dist/shared/errors-BmP3TKoW.cjs.map +0 -1
- package/dist/shared/errors-GI10kZxk.js.map +0 -1
- package/dist/shared/events-BVfqWyBw.cjs.map +0 -1
- package/dist/shared/events-Cwj7UPAa.js.map +0 -1
- package/dist/shared/key-value-store-B5GYVEYZ.cjs.map +0 -1
- package/dist/shared/key-value-store-Bt2PSMxY.js.map +0 -1
- package/dist/shared/multi-storage-C3Ngtop1.js.map +0 -1
- package/dist/shared/multi-storage-s-fPtjoy.cjs.map +0 -1
- package/dist/shared/redact-DJV0XM3n.js.map +0 -1
- package/dist/shared/redact-lAmck1Z6.cjs.map +0 -1
- package/dist/shared/render-BT74nC1l.js.map +0 -1
- package/dist/shared/render-CYJSDypQ.cjs.map +0 -1
- package/dist/shared/url-BnO1aWsd.cjs.map +0 -1
- package/dist/shared/url-DS0-lrl-.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
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`.
|
|
1097
1126
|
*
|
|
1098
|
-
*
|
|
1099
|
-
*
|
|
1127
|
+
* Начальные ограничения приходят из каталога вызывающего клиента и запоминаются при
|
|
1128
|
+
* создании очереди. Клиенты, разделяющие один пул через `ItdAccounts`, обязаны описывать
|
|
1129
|
+
* бакет одинаково: расхождение обнаруживается при обращении к уже созданной очереди.
|
|
1130
|
+
*
|
|
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[
|
|
1465
|
+
function wasRequestErrorReported(request, error) {
|
|
1466
|
+
return request[OPERATION_STATE]?.errorReporting?.errors.has(error) ?? false;
|
|
1390
1467
|
}
|
|
1391
|
-
/**
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1468
|
+
/**
|
|
1469
|
+
* Отмечает, что отмена операции передана в `onError`.
|
|
1470
|
+
*
|
|
1471
|
+
* @returns `false`, если это уже сделал другой уровень — повторно вызывать хук не нужно
|
|
1472
|
+
* @internal
|
|
1473
|
+
*/
|
|
1474
|
+
function claimAbortReport(request) {
|
|
1475
|
+
const state = errorReportingState(request);
|
|
1476
|
+
if (state.abortReported) return false;
|
|
1477
|
+
state.abortReported = true;
|
|
1478
|
+
return true;
|
|
1400
1479
|
}
|
|
1401
1480
|
/** Один раз присваивает низкоуровневому запросу семантический ID до входа в middleware. */
|
|
1402
1481
|
function identifyRequest(request) {
|
|
@@ -1405,56 +1484,40 @@ function identifyRequest(request) {
|
|
|
1405
1484
|
operationId: "raw"
|
|
1406
1485
|
} : request;
|
|
1407
1486
|
}
|
|
1408
|
-
/** Привязывает счётчик транспортных попыток к одной логической операции. @internal */
|
|
1409
|
-
function trackRequestAttempts(request) {
|
|
1410
|
-
if (request[REQUEST_ATTEMPT_STATE]) return request;
|
|
1411
|
-
return {
|
|
1412
|
-
...request,
|
|
1413
|
-
[REQUEST_ATTEMPT_STATE]: { value: 0 }
|
|
1414
|
-
};
|
|
1415
|
-
}
|
|
1416
1487
|
/** Начинает следующую фактическую транспортную попытку логической операции. @internal */
|
|
1417
1488
|
function beginTransportAttempt(request) {
|
|
1418
|
-
const
|
|
1419
|
-
|
|
1420
|
-
if (!state) throw new Error("request attempt state was not initialized");
|
|
1421
|
-
state.value += 1;
|
|
1489
|
+
const state = operationState(request);
|
|
1490
|
+
state.attempt = (state.attempt ?? 0) + 1;
|
|
1422
1491
|
return {
|
|
1423
|
-
...
|
|
1424
|
-
attempt: state.
|
|
1492
|
+
...request,
|
|
1493
|
+
attempt: state.attempt
|
|
1425
1494
|
};
|
|
1426
1495
|
}
|
|
1427
1496
|
/** Возвращает номер последней начатой транспортной попытки. @internal */
|
|
1428
1497
|
function currentTransportAttempt(request) {
|
|
1429
|
-
return request[
|
|
1498
|
+
return request[OPERATION_STATE]?.attempt ?? 0;
|
|
1430
1499
|
}
|
|
1431
1500
|
/**
|
|
1432
1501
|
* Вычисляет ключ очереди один раз на логическую операцию.
|
|
1433
1502
|
*
|
|
1434
1503
|
* Ключ спрашивают трижды: при постановке в очередь, при чтении заголовков ответа и при
|
|
1435
|
-
* паузе после `429`. Значение
|
|
1436
|
-
*
|
|
1504
|
+
* паузе после `429`. Значение хранится в общем состоянии операции, поэтому его видят все
|
|
1505
|
+
* копии запроса ниже по конвейеру. Ограничения бакета фиксируются вместе с ключом:
|
|
1506
|
+
* освобождение feature во время запроса не меняет очередь операции.
|
|
1437
1507
|
*
|
|
1438
1508
|
* @internal
|
|
1439
1509
|
*/
|
|
1440
1510
|
function requestQueueKey(request, compute) {
|
|
1441
|
-
const
|
|
1442
|
-
const cached =
|
|
1511
|
+
const state = operationState(request);
|
|
1512
|
+
const cached = state.queueKey;
|
|
1443
1513
|
if (cached) return cached;
|
|
1444
1514
|
const key = compute(request);
|
|
1445
|
-
|
|
1515
|
+
state.queueKey = key;
|
|
1446
1516
|
return key;
|
|
1447
1517
|
}
|
|
1448
|
-
/** Помечает запрос как часть внутренней финализации уже начатого `dispose()`. @internal */
|
|
1449
|
-
function markDisposeCleanupRequest(request) {
|
|
1450
|
-
return {
|
|
1451
|
-
...request,
|
|
1452
|
-
[DISPOSE_CLEANUP_REQUEST]: true
|
|
1453
|
-
};
|
|
1454
|
-
}
|
|
1455
1518
|
/** Разрешено ли запросу завершать внутреннюю очистку после `dispose()`. @internal */
|
|
1456
1519
|
function isDisposeCleanupRequest(request) {
|
|
1457
|
-
return request[
|
|
1520
|
+
return request[OPERATION_STATE]?.disposeCleanup === true;
|
|
1458
1521
|
}
|
|
1459
1522
|
/**
|
|
1460
1523
|
* Собирает слои в один обработчик.
|
|
@@ -1484,129 +1547,143 @@ function withLayerHeaders(request, headers) {
|
|
|
1484
1547
|
}
|
|
1485
1548
|
//#endregion
|
|
1486
1549
|
//#region src/core/execution/http.ts
|
|
1550
|
+
/** Собирает запрос конвейера из контракта операции и параметров вызова. */
|
|
1551
|
+
function operationRequest(operation, options) {
|
|
1552
|
+
return {
|
|
1553
|
+
...options,
|
|
1554
|
+
operationId: operation.id,
|
|
1555
|
+
method: operation.method,
|
|
1556
|
+
retrySafety: options.retrySafety ?? operation.retrySafety
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1487
1559
|
/**
|
|
1488
1560
|
* Точка входа ресурсов в конвейер запросов.
|
|
1489
1561
|
*
|
|
1490
|
-
*
|
|
1491
|
-
*
|
|
1562
|
+
* Заводит общий lifecycle логической операции — отмену, освобождение клиента и `deadline`, —
|
|
1563
|
+
* передаёт запрос собранной цепочке стадий и сообщает в `onError` об ошибках, которые
|
|
1564
|
+
* не дошли до транспорта. О стадиях и их порядке ресурсы не знают.
|
|
1492
1565
|
*/
|
|
1493
1566
|
var HttpClient = class {
|
|
1494
1567
|
#handler;
|
|
1495
|
-
#plugins;
|
|
1496
1568
|
#baseUrl;
|
|
1497
|
-
#
|
|
1569
|
+
#deadline;
|
|
1498
1570
|
#clock;
|
|
1499
1571
|
#lifetimeSignal;
|
|
1500
1572
|
#hooks;
|
|
1501
1573
|
#assertActive;
|
|
1574
|
+
#activeScopes = /* @__PURE__ */ new Set();
|
|
1502
1575
|
constructor(deps) {
|
|
1503
1576
|
this.#handler = deps.handler;
|
|
1504
|
-
this.#plugins = deps.plugins;
|
|
1505
1577
|
this.#baseUrl = deps.baseUrl;
|
|
1506
|
-
this.#
|
|
1578
|
+
this.#deadline = deps.deadline;
|
|
1507
1579
|
this.#clock = deps.clock;
|
|
1508
1580
|
this.#lifetimeSignal = deps.lifetimeSignal;
|
|
1509
1581
|
this.#hooks = deps.hooks;
|
|
1510
1582
|
this.#assertActive = deps.assertActive;
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
return this.#baseUrl;
|
|
1583
|
+
if (this.#lifetimeSignal && !this.#lifetimeSignal.aborted) this.#lifetimeSignal.addEventListener("abort", () => {
|
|
1584
|
+
for (const scope of this.#activeScopes) scope.abort(this.#lifetimeSignal?.reason);
|
|
1585
|
+
}, { once: true });
|
|
1515
1586
|
}
|
|
1516
1587
|
/**
|
|
1517
|
-
* Выполняет запрос
|
|
1588
|
+
* Выполняет низкоуровневый запрос через собранный конвейер.
|
|
1518
1589
|
*
|
|
1519
1590
|
* @typeParam T ожидаемая форма ответа после снятия обёртки `{ data: … }`
|
|
1520
1591
|
* @throws {ItdApiError} если сервер ответил статусом ≥ 400
|
|
1521
|
-
* @throws {ItdTimeoutError} если истёк
|
|
1592
|
+
* @throws {ItdTimeoutError} если истёк `timeout` попытки или `deadline` операции
|
|
1522
1593
|
* @throws {ItdAbortError} если запрос отменён через `signal`
|
|
1523
1594
|
* @throws {ItdNetworkError} если запрос не дошёл до сервера
|
|
1524
1595
|
*/
|
|
1525
1596
|
request(options) {
|
|
1526
|
-
|
|
1527
|
-
return this.#runWithLifecycle(request, (prepared) => this.#handler(prepared));
|
|
1597
|
+
return this.#run(identifyRequest(options));
|
|
1528
1598
|
}
|
|
1529
1599
|
/** Выполняет контракт операции; `next()` плагина возвращает результат после `read`. */
|
|
1530
1600
|
execute(operation, options) {
|
|
1531
|
-
return this.#run(operation, options);
|
|
1532
|
-
}
|
|
1533
|
-
#run(operation, options) {
|
|
1534
|
-
const request = identifyRequest({
|
|
1535
|
-
...options,
|
|
1536
|
-
operationId: operation.id,
|
|
1537
|
-
method: operation.method,
|
|
1538
|
-
retrySafety: options.retrySafety ?? operation.retrySafety
|
|
1539
|
-
});
|
|
1540
|
-
return this.#runWithLifecycle(request, async (prepared) => operation.read(await this.#handler(prepared), prepared));
|
|
1601
|
+
return this.#run(operationRequest(operation, options), { reader: operation.read });
|
|
1541
1602
|
}
|
|
1542
1603
|
/** Выполняет внутреннюю операцию финализации после начала `ItdClient.dispose()`. @internal */
|
|
1543
1604
|
cleanupOperation(operation, options) {
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
retrySafety: options.retrySafety ?? operation.retrySafety
|
|
1605
|
+
return this.#run(operationRequest(operation, options), {
|
|
1606
|
+
allowDisposed: true,
|
|
1607
|
+
reader: operation.read,
|
|
1608
|
+
disposeCleanup: true
|
|
1549
1609
|
});
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
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
|
+
})
|
|
1622
|
+
});
|
|
1623
|
+
if (this.#lifetimeSignal?.aborted) scope.abort(this.#lifetimeSignal.reason);
|
|
1624
|
+
else this.#activeScopes.add(scope);
|
|
1559
1625
|
const startedAt = this.#clock.now();
|
|
1560
|
-
|
|
1626
|
+
const tracked = initializeOperationState(request, {
|
|
1627
|
+
lifecycleSignal: scope.signal,
|
|
1628
|
+
reader: options.reader,
|
|
1629
|
+
disposeCleanup: options.disposeCleanup
|
|
1630
|
+
});
|
|
1561
1631
|
try {
|
|
1562
|
-
|
|
1563
|
-
...prepared,
|
|
1564
|
-
signal: scope.signal,
|
|
1565
|
-
timeout: 0
|
|
1566
|
-
}), scope.signal);
|
|
1567
|
-
return await waitForRequest(Promise.resolve(pending), scope.signal);
|
|
1632
|
+
return await waitForRequest(this.#handler(tracked), scope.signal);
|
|
1568
1633
|
} catch (error) {
|
|
1569
|
-
const failure = requestAbortError(scope,
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
markRequestErrorObserved(request, error);
|
|
1576
|
-
let headers;
|
|
1577
|
-
try {
|
|
1578
|
-
headers = new Headers({
|
|
1579
|
-
...request.layerHeaders,
|
|
1580
|
-
...request.headers
|
|
1581
|
-
});
|
|
1582
|
-
} catch {
|
|
1583
|
-
headers = new Headers();
|
|
1584
|
-
}
|
|
1585
|
-
const notification = dispatchRequestHook(this.#hooks, "onError", {
|
|
1586
|
-
operationId: request.operationId,
|
|
1587
|
-
signal: scope.signal,
|
|
1588
|
-
method: request.method.toUpperCase(),
|
|
1589
|
-
path: request.path,
|
|
1590
|
-
url: 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;
|
|
@@ -2031,6 +2146,13 @@ function pickString(source, field) {
|
|
|
2031
2146
|
//#endregion
|
|
2032
2147
|
//#region src/core/error-factory.ts
|
|
2033
2148
|
/**
|
|
2149
|
+
* Отличает строковый машинный код (`NOT_FOUND`) от обычного текста и HTML страницы прокси.
|
|
2150
|
+
* Ограничение длины не даёт случайно принять большое служебное тело за код ошибки.
|
|
2151
|
+
*/
|
|
2152
|
+
function readPlainTextErrorCode(value) {
|
|
2153
|
+
return value.length <= 128 && /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$/.test(value) ? value : void 0;
|
|
2154
|
+
}
|
|
2155
|
+
/**
|
|
2034
2156
|
* Сводит ошибки по полям к единой форме `{ поле: [сообщения] }`.
|
|
2035
2157
|
*
|
|
2036
2158
|
* Понимает две документированные формы:
|
|
@@ -2040,7 +2162,7 @@ function pickString(source, field) {
|
|
|
2040
2162
|
function collectFieldErrors(source) {
|
|
2041
2163
|
const result = {};
|
|
2042
2164
|
const errors = source.errors;
|
|
2043
|
-
if (isRecord(errors)) {
|
|
2165
|
+
if (require_validate.isRecord(errors)) {
|
|
2044
2166
|
for (const [field, value] of Object.entries(errors)) if (Array.isArray(value)) {
|
|
2045
2167
|
const messages = value.filter((item) => typeof item === "string");
|
|
2046
2168
|
if (messages.length > 0) result[field] = messages;
|
|
@@ -2048,7 +2170,7 @@ function collectFieldErrors(source) {
|
|
|
2048
2170
|
}
|
|
2049
2171
|
const violations = source.violations;
|
|
2050
2172
|
if (Array.isArray(violations)) for (const violation of violations) {
|
|
2051
|
-
if (!isRecord(violation)) continue;
|
|
2173
|
+
if (!require_validate.isRecord(violation)) continue;
|
|
2052
2174
|
const field = asString(violation.field) ?? asString(violation.property);
|
|
2053
2175
|
const message = asString(violation.message);
|
|
2054
2176
|
if (!field || !message) continue;
|
|
@@ -2070,15 +2192,18 @@ function collectFieldErrors(source) {
|
|
|
2070
2192
|
*/
|
|
2071
2193
|
function parseErrorBody(body, status, statusText = "") {
|
|
2072
2194
|
const fallbackMessage = statusText ? `HTTP ${status} ${statusText}` : `HTTP ${status}`;
|
|
2073
|
-
if (typeof body === "string")
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2195
|
+
if (typeof body === "string") {
|
|
2196
|
+
const message = asString(body.trim()) ?? fallbackMessage;
|
|
2197
|
+
return {
|
|
2198
|
+
code: readPlainTextErrorCode(message) ?? "UNKNOWN_ERROR",
|
|
2199
|
+
message,
|
|
2200
|
+
detail: void 0,
|
|
2201
|
+
title: void 0,
|
|
2202
|
+
fieldErrors: {},
|
|
2203
|
+
userId: void 0
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
if (!require_validate.isRecord(body)) return {
|
|
2082
2207
|
code: "UNKNOWN_ERROR",
|
|
2083
2208
|
message: fallbackMessage,
|
|
2084
2209
|
detail: void 0,
|
|
@@ -2097,7 +2222,7 @@ function parseErrorBody(body, status, statusText = "") {
|
|
|
2097
2222
|
userId: void 0
|
|
2098
2223
|
};
|
|
2099
2224
|
}
|
|
2100
|
-
const inner = isRecord(body.error) ? body.error : body;
|
|
2225
|
+
const inner = require_validate.isRecord(body.error) ? body.error : body;
|
|
2101
2226
|
const message = asString(inner.message) ?? asString(inner.detail) ?? asString(inner.title) ?? asString(body.error) ?? fallbackMessage;
|
|
2102
2227
|
return {
|
|
2103
2228
|
code: asString(inner.code) ?? asString(body.code) ?? "UNKNOWN_ERROR",
|
|
@@ -2164,30 +2289,30 @@ function getRequestId(headers) {
|
|
|
2164
2289
|
}
|
|
2165
2290
|
/** Соответствие кода ошибки конкретному классу. Приоритетнее, чем HTTP-статус. */
|
|
2166
2291
|
const CODE_TO_CLASS = {
|
|
2167
|
-
VALIDATION_ERROR:
|
|
2168
|
-
RATE_LIMIT_EXCEEDED:
|
|
2169
|
-
UNAUTHORIZED:
|
|
2170
|
-
SESSION_EXPIRED:
|
|
2171
|
-
SESSION_REVOKED:
|
|
2172
|
-
SESSION_INVALID_REFRESH_TOKEN:
|
|
2173
|
-
REFRESH_TOKEN_MISSING:
|
|
2174
|
-
SESSION_NOT_FOUND:
|
|
2175
|
-
ACCOUNT_INVALID_CREDENTIALS:
|
|
2176
|
-
ACCESS_DENIED:
|
|
2177
|
-
ENTITY_NOT_FOUND:
|
|
2178
|
-
NOT_FOUND:
|
|
2179
|
-
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
|
|
2180
2305
|
};
|
|
2181
2306
|
/** Соответствие HTTP-статуса классу — запасной вариант, когда код ничего не говорит. */
|
|
2182
2307
|
function classByStatus(status) {
|
|
2183
|
-
if (status === 401) return
|
|
2184
|
-
if (status === 403) return
|
|
2185
|
-
if (status === 404) return
|
|
2186
|
-
if (status === 409) return
|
|
2187
|
-
if (status === 422) return
|
|
2188
|
-
if (status === 429) return
|
|
2189
|
-
if (status >= 500) return
|
|
2190
|
-
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;
|
|
2191
2316
|
}
|
|
2192
2317
|
/**
|
|
2193
2318
|
* Готовит тело ответа к сохранению в `ItdApiError.raw`.
|
|
@@ -2198,7 +2323,7 @@ function classByStatus(status) {
|
|
|
2198
2323
|
* значения секретов заменяются заглушкой.
|
|
2199
2324
|
*/
|
|
2200
2325
|
function safeRawBody(body) {
|
|
2201
|
-
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;
|
|
2202
2327
|
return {
|
|
2203
2328
|
...body,
|
|
2204
2329
|
found: require_redact.redactBody(body.found)
|
|
@@ -2229,7 +2354,7 @@ function createApiError(context) {
|
|
|
2229
2354
|
response: context.response,
|
|
2230
2355
|
retryAfter: parseRetryAfter(context.headers?.get("retry-after"), context.now)
|
|
2231
2356
|
};
|
|
2232
|
-
if (parsed.code === "PHONE_VERIFICATION_REQUIRED") return new
|
|
2357
|
+
if (parsed.code === "PHONE_VERIFICATION_REQUIRED") return new require_validate.ItdPhoneVerificationError({
|
|
2233
2358
|
...init,
|
|
2234
2359
|
userId: parsed.userId
|
|
2235
2360
|
});
|
|
@@ -2242,7 +2367,7 @@ function setHeader(headers, name, value) {
|
|
|
2242
2367
|
try {
|
|
2243
2368
|
headers.set(name, value);
|
|
2244
2369
|
} catch (cause) {
|
|
2245
|
-
throw new
|
|
2370
|
+
throw new require_validate.ItdConfigError(`Некорректный HTTP-заголовок ${JSON.stringify(name)}: проверьте его имя и значение.`, { cause });
|
|
2246
2371
|
}
|
|
2247
2372
|
}
|
|
2248
2373
|
/** Тело, которое отправляется как есть, без сериализации в JSON. */
|
|
@@ -2271,33 +2396,15 @@ async function readBody(response) {
|
|
|
2271
2396
|
const text = await response.text();
|
|
2272
2397
|
return text === "" ? void 0 : text;
|
|
2273
2398
|
}
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
const error = /* @__PURE__ */ new Error("Операция прервана");
|
|
2277
|
-
error.name = "AbortError";
|
|
2278
|
-
return error;
|
|
2279
|
-
}
|
|
2280
|
-
/** Прерывает ожидание промиса при срабатывании сигнала. */
|
|
2281
|
-
function abortable(promise, signal) {
|
|
2282
|
-
if (signal.aborted) {
|
|
2283
|
-
promise.catch(() => {});
|
|
2284
|
-
return Promise.reject(createAbortError());
|
|
2285
|
-
}
|
|
2286
|
-
let onAbort;
|
|
2287
|
-
const interrupted = new Promise((_resolve, reject) => {
|
|
2288
|
-
onAbort = () => reject(createAbortError());
|
|
2289
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
2290
|
-
});
|
|
2291
|
-
return Promise.race([promise, interrupted]).finally(() => {
|
|
2292
|
-
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
2293
|
-
});
|
|
2399
|
+
function describe(error) {
|
|
2400
|
+
return error instanceof Error ? error.message : String(error);
|
|
2294
2401
|
}
|
|
2295
2402
|
/**
|
|
2296
2403
|
* Единственное место, откуда библиотека ходит в сеть.
|
|
2297
2404
|
*
|
|
2298
|
-
* Отвечает за сборку URL, заголовки, cookie,
|
|
2299
|
-
* неудачи в типизированную ошибку. Авторизация, повторы, очередь и плагины — отдельные
|
|
2300
|
-
*
|
|
2405
|
+
* Отвечает за сборку URL, заголовки, cookie, срок одной попытки, разбор ответа и превращение
|
|
2406
|
+
* любой неудачи в типизированную ошибку. Авторизация, повторы, очередь и плагины — отдельные
|
|
2407
|
+
* стадии конвейера, и транспорт о них не знает.
|
|
2301
2408
|
*/
|
|
2302
2409
|
var Transport = class {
|
|
2303
2410
|
#config;
|
|
@@ -2306,141 +2413,114 @@ var Transport = class {
|
|
|
2306
2413
|
this.#config = config;
|
|
2307
2414
|
this.#deps = deps;
|
|
2308
2415
|
}
|
|
2309
|
-
/** Базовый URL, к которому обращается транспорт. */
|
|
2310
|
-
get baseUrl() {
|
|
2311
|
-
return this.#config.baseUrl;
|
|
2312
|
-
}
|
|
2313
2416
|
/**
|
|
2314
2417
|
* Выполняет один сетевой запрос.
|
|
2315
2418
|
*
|
|
2316
2419
|
* @throws {ItdApiError} если сервер ответил статусом ≥ 400
|
|
2317
|
-
* @throws {ItdTimeoutError} если истёк
|
|
2420
|
+
* @throws {ItdTimeoutError} если истёк срок попытки
|
|
2318
2421
|
* @throws {ItdAbortError} если запрос отменён через `signal`
|
|
2319
2422
|
* @throws {ItdNetworkError} если запрос не дошёл до сервера
|
|
2320
2423
|
*/
|
|
2321
2424
|
send = async (input) => {
|
|
2322
2425
|
const request = identifyRequest(input);
|
|
2323
|
-
const method = request.method.toUpperCase();
|
|
2324
|
-
const url = this.buildUrl(request);
|
|
2325
|
-
const attempt = request.attempt ?? 1;
|
|
2326
2426
|
const timeout = request.timeout ?? this.#config.timeout;
|
|
2327
|
-
const
|
|
2328
|
-
const
|
|
2427
|
+
const method = request.method.toUpperCase();
|
|
2428
|
+
const attempt = {
|
|
2429
|
+
request,
|
|
2430
|
+
method,
|
|
2431
|
+
url: this.buildUrl(request),
|
|
2432
|
+
abort: createRequestAbortScope(request.signal, void 0, this.#config.clock, {
|
|
2433
|
+
after: timeout,
|
|
2434
|
+
error: () => new 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;
|
|
2329
2445
|
let cleanupBody;
|
|
2330
2446
|
try {
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2447
|
+
let headers;
|
|
2448
|
+
try {
|
|
2449
|
+
headers = await waitForRequest(this.#buildHeaders(request, attempt.url), signal);
|
|
2450
|
+
} catch (error) {
|
|
2451
|
+
throw this.#toTransportError(attempt, error);
|
|
2452
|
+
}
|
|
2334
2453
|
let body;
|
|
2335
2454
|
try {
|
|
2336
|
-
const prepared = await this.#prepareBody(
|
|
2455
|
+
const prepared = await this.#prepareBody(attempt, headers);
|
|
2337
2456
|
body = prepared.body;
|
|
2338
2457
|
cleanupBody = prepared.cleanup;
|
|
2339
2458
|
} catch (error) {
|
|
2340
|
-
const failure =
|
|
2341
|
-
|
|
2342
|
-
operationId: request.operationId,
|
|
2343
|
-
signal: abort.signal,
|
|
2344
|
-
method,
|
|
2345
|
-
path: request.path,
|
|
2346
|
-
url,
|
|
2347
|
-
headers,
|
|
2348
|
-
attempt
|
|
2349
|
-
};
|
|
2350
|
-
await this.#dispatchErrorHook(request, {
|
|
2351
|
-
...context,
|
|
2352
|
-
duration: this.#config.clock.now() - startedAt,
|
|
2353
|
-
error: failure
|
|
2354
|
-
}, abort, timeout);
|
|
2459
|
+
const failure = signal.aborted || error instanceof 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);
|
|
2355
2461
|
throw failure;
|
|
2356
2462
|
}
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
path: request.path,
|
|
2362
|
-
url,
|
|
2363
|
-
headers,
|
|
2364
|
-
attempt
|
|
2365
|
-
};
|
|
2366
|
-
try {
|
|
2367
|
-
await abortable(dispatchRequestHook(this.#config.hooks, "onRequest", context), abort.signal);
|
|
2463
|
+
let context;
|
|
2464
|
+
const contextOf = () => context ??= this.#context(attempt, headers);
|
|
2465
|
+
if (hasRequestHook(this.#config.hooks, "onRequest")) try {
|
|
2466
|
+
await waitForRequest(dispatchRequestHook(this.#config.hooks, "onRequest", { ...contextOf() }), signal);
|
|
2368
2467
|
} catch (error) {
|
|
2369
|
-
const failure =
|
|
2370
|
-
await this.#
|
|
2371
|
-
...context,
|
|
2372
|
-
duration: this.#config.clock.now() - startedAt,
|
|
2373
|
-
error: failure
|
|
2374
|
-
}, abort, timeout);
|
|
2468
|
+
const failure = this.#abortedOr(attempt, error);
|
|
2469
|
+
await this.#report(attempt, headers, failure);
|
|
2375
2470
|
throw failure;
|
|
2376
2471
|
}
|
|
2377
|
-
this.#config.logger
|
|
2472
|
+
const logger = this.#config.logger;
|
|
2473
|
+
if (logger && require_redact.isLevelEnabled(logger, require_redact.LogLevel.Debug)) logger.debug(`→ ${attempt.method} ${request.path}`, {
|
|
2378
2474
|
headers: require_redact.redactHeaders(headers),
|
|
2379
2475
|
body: request.bodyFactory ? "[повторяемое тело]" : require_redact.redactBody(request.body)
|
|
2380
2476
|
});
|
|
2381
2477
|
let response;
|
|
2382
2478
|
try {
|
|
2383
|
-
|
|
2384
|
-
method,
|
|
2385
|
-
headers,
|
|
2386
|
-
signal: abort.signal,
|
|
2387
|
-
...body !== void 0 ? { body } : {},
|
|
2388
|
-
...this.#config.sendCredentials ? { credentials: "include" } : {}
|
|
2389
|
-
};
|
|
2390
|
-
if (typeof ReadableStream !== "undefined" && body instanceof ReadableStream) init.duplex = "half";
|
|
2391
|
-
response = await runAttemptInterceptors(request, {
|
|
2392
|
-
...context,
|
|
2393
|
-
body,
|
|
2394
|
-
signal: abort.signal
|
|
2395
|
-
}, async () => {
|
|
2396
|
-
try {
|
|
2397
|
-
return await this.#config.fetch(url, init);
|
|
2398
|
-
} catch (error) {
|
|
2399
|
-
throw this.#toTransportError(error, abort, request, method, timeout);
|
|
2400
|
-
}
|
|
2401
|
-
});
|
|
2479
|
+
response = await waitForRequest(this.#fetch(attempt, headers, body, contextOf), signal);
|
|
2402
2480
|
} catch (error) {
|
|
2403
|
-
const
|
|
2404
|
-
await this.#
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
error
|
|
2408
|
-
}, abort, timeout);
|
|
2409
|
-
this.#config.logger?.warn(`× ${method} ${request.path} (${duration} мс): ${error instanceof Error ? error.message : String(error)}`);
|
|
2410
|
-
throw error;
|
|
2481
|
+
const failure = this.#abortedOr(attempt, error);
|
|
2482
|
+
await this.#report(attempt, headers, failure);
|
|
2483
|
+
this.#config.logger?.warn(`× ${attempt.method} ${request.path} (${this.#elapsed(attempt)} мс): ${describe(failure)}`);
|
|
2484
|
+
throw failure;
|
|
2411
2485
|
}
|
|
2412
2486
|
if (this.#deps.onRateLimit) {
|
|
2413
2487
|
const { limit, remaining } = readRateLimit(response.headers);
|
|
2414
2488
|
this.#deps.onRateLimit(limit, remaining, request);
|
|
2415
2489
|
}
|
|
2416
|
-
if (this.#config.useCookieJar) (request.cookieJar ?? this.#deps.cookies)?.setFromResponse(response.url || url, response);
|
|
2490
|
+
if (this.#config.useCookieJar) (request.cookieJar ?? this.#deps.cookies)?.setFromResponse(response.url || attempt.url, response);
|
|
2417
2491
|
if (response.ok && hasRequestHook(this.#config.hooks, "onResponse")) {
|
|
2418
2492
|
const hookResponse = response.clone();
|
|
2419
2493
|
try {
|
|
2420
|
-
await
|
|
2421
|
-
...
|
|
2494
|
+
await waitForRequest(dispatchRequestHook(this.#config.hooks, "onResponse", {
|
|
2495
|
+
...contextOf(),
|
|
2422
2496
|
status: response.status,
|
|
2423
|
-
duration: this.#
|
|
2497
|
+
duration: this.#elapsed(attempt),
|
|
2424
2498
|
response: hookResponse
|
|
2425
|
-
}),
|
|
2499
|
+
}), signal);
|
|
2426
2500
|
} catch (error) {
|
|
2427
2501
|
response.body?.cancel().catch(() => {});
|
|
2428
|
-
const failure =
|
|
2429
|
-
await this.#
|
|
2430
|
-
...context,
|
|
2431
|
-
duration: this.#config.clock.now() - startedAt,
|
|
2432
|
-
error: failure
|
|
2433
|
-
}, abort, timeout);
|
|
2502
|
+
const failure = this.#abortedOr(attempt, error);
|
|
2503
|
+
await this.#report(attempt, headers, failure);
|
|
2434
2504
|
throw failure;
|
|
2435
2505
|
} finally {
|
|
2436
2506
|
if (!hookResponse.bodyUsed) hookResponse.body?.cancel().catch(() => {});
|
|
2437
2507
|
}
|
|
2438
2508
|
}
|
|
2439
|
-
|
|
2440
|
-
|
|
2509
|
+
let payload;
|
|
2510
|
+
try {
|
|
2511
|
+
payload = await waitForRequest(readBody(response), signal);
|
|
2512
|
+
attempt.abort.disarm();
|
|
2513
|
+
} catch (error) {
|
|
2514
|
+
await response.body?.cancel().catch(() => {});
|
|
2515
|
+
const failure = this.#toTransportError(attempt, error);
|
|
2516
|
+
await this.#report(attempt, headers, failure);
|
|
2517
|
+
this.#config.logger?.warn(`× ${attempt.method} ${request.path}: не удалось прочитать тело ответа — ${failure.message}`);
|
|
2518
|
+
throw failure;
|
|
2519
|
+
}
|
|
2520
|
+
const duration = this.#elapsed(attempt);
|
|
2441
2521
|
if (!response.ok) {
|
|
2442
2522
|
const error = createApiError({
|
|
2443
|
-
method,
|
|
2523
|
+
method: attempt.method,
|
|
2444
2524
|
path: request.path,
|
|
2445
2525
|
status: response.status,
|
|
2446
2526
|
now: this.#config.clock.now(),
|
|
@@ -2449,58 +2529,96 @@ var Transport = class {
|
|
|
2449
2529
|
response,
|
|
2450
2530
|
body: payload
|
|
2451
2531
|
});
|
|
2452
|
-
await this.#
|
|
2453
|
-
|
|
2454
|
-
duration,
|
|
2455
|
-
error
|
|
2456
|
-
}, abort, timeout);
|
|
2457
|
-
this.#config.logger?.warn(`← ${response.status} ${method} ${request.path} (${duration} мс): ${error.message}`);
|
|
2532
|
+
await this.#report(attempt, headers, error);
|
|
2533
|
+
this.#config.logger?.warn(`← ${response.status} ${attempt.method} ${request.path} (${duration} мс): ${error.message}`);
|
|
2458
2534
|
throw error;
|
|
2459
2535
|
}
|
|
2460
|
-
this.#config.logger?.debug(`← ${response.status} ${method} ${request.path} (${duration} мс)`);
|
|
2536
|
+
this.#config.logger?.debug(`← ${response.status} ${attempt.method} ${request.path} (${duration} мс)`);
|
|
2461
2537
|
return request.raw ? payload : unwrapData(payload);
|
|
2462
2538
|
} finally {
|
|
2463
2539
|
try {
|
|
2464
2540
|
await cleanupBody?.();
|
|
2465
2541
|
} catch (error) {
|
|
2466
|
-
this.#config.logger?.warn(`не удалось закрыть тело ${method} ${request.path}`, error);
|
|
2542
|
+
this.#config.logger?.warn(`не удалось закрыть тело ${attempt.method} ${request.path}`, error);
|
|
2467
2543
|
} finally {
|
|
2468
|
-
abort.cleanup();
|
|
2544
|
+
attempt.abort.cleanup();
|
|
2469
2545
|
}
|
|
2470
2546
|
}
|
|
2471
2547
|
};
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2548
|
+
/**
|
|
2549
|
+
* Итоговый URL со строкой запроса. Нужен и слою повторов — для хука `onRetry`.
|
|
2550
|
+
*
|
|
2551
|
+
* Хост берётся из самого запроса, если он там задан: у сервисов платформы свои домены.
|
|
2552
|
+
*/
|
|
2553
|
+
buildUrl(request) {
|
|
2554
|
+
const base = request.baseUrl ?? this.#config.baseUrl;
|
|
2555
|
+
return 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);
|
|
2485
2570
|
}
|
|
2571
|
+
return headers;
|
|
2572
|
+
}
|
|
2573
|
+
/** Данные попытки для хуков `onRequest`, `onResponse` и перехватчиков. */
|
|
2574
|
+
#context(attempt, headers) {
|
|
2575
|
+
return {
|
|
2576
|
+
operationId: attempt.request.operationId,
|
|
2577
|
+
signal: attempt.abort.signal,
|
|
2578
|
+
method: attempt.method,
|
|
2579
|
+
path: attempt.request.path,
|
|
2580
|
+
url: attempt.url,
|
|
2581
|
+
headers,
|
|
2582
|
+
attempt: attempt.number
|
|
2583
|
+
};
|
|
2584
|
+
}
|
|
2585
|
+
#elapsed(attempt) {
|
|
2586
|
+
return this.#config.clock.now() - attempt.startedAt;
|
|
2587
|
+
}
|
|
2588
|
+
/** Ошибка хука: отмена во время его работы становится транспортной ошибкой, остальное — как есть. */
|
|
2589
|
+
#abortedOr(attempt, error) {
|
|
2590
|
+
return attempt.abort.signal.aborted ? this.#toTransportError(attempt, error) : error;
|
|
2591
|
+
}
|
|
2592
|
+
/**
|
|
2593
|
+
* Дополняет общие заголовки значением `Accept`, заголовками конвейера и вызова.
|
|
2594
|
+
* Заголовки вызова применяются последними.
|
|
2595
|
+
*/
|
|
2596
|
+
async #buildHeaders(request, url) {
|
|
2597
|
+
const headers = await this.platformHeaders(url, request.cookieJar);
|
|
2598
|
+
if (!headers.has("Accept")) headers.set("Accept", "application/json");
|
|
2599
|
+
for (const [name, value] of Object.entries(request.layerHeaders ?? {})) setHeader(headers, name, value);
|
|
2600
|
+
for (const [name, value] of Object.entries(request.headers ?? {})) setHeader(headers, name, value);
|
|
2601
|
+
return headers;
|
|
2486
2602
|
}
|
|
2487
2603
|
/** Подготавливает тело внутри попытки, чтобы поток можно было открыть заново при retry. */
|
|
2488
|
-
async #prepareBody(
|
|
2604
|
+
async #prepareBody(attempt, headers) {
|
|
2605
|
+
const { request } = attempt;
|
|
2606
|
+
const { signal } = attempt.abort;
|
|
2489
2607
|
if (request.bodyFactory) {
|
|
2490
|
-
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 нельзя задавать одновременно");
|
|
2491
2609
|
const pending = Promise.resolve(request.bodyFactory({
|
|
2492
2610
|
signal,
|
|
2493
|
-
attempt
|
|
2611
|
+
attempt: attempt.number
|
|
2494
2612
|
}));
|
|
2495
2613
|
let prepared;
|
|
2496
2614
|
try {
|
|
2497
|
-
prepared = await
|
|
2615
|
+
prepared = await waitForRequest(pending, signal);
|
|
2498
2616
|
} catch (error) {
|
|
2499
2617
|
if (signal.aborted) pending.then(async (late) => {
|
|
2500
2618
|
try {
|
|
2501
2619
|
await late.cleanup?.();
|
|
2502
2620
|
} catch (cleanupError) {
|
|
2503
|
-
this.#config.logger?.warn(`не удалось закрыть отложенное тело ${
|
|
2621
|
+
this.#config.logger?.warn(`не удалось закрыть отложенное тело ${attempt.method} ${request.path}`, cleanupError);
|
|
2504
2622
|
}
|
|
2505
2623
|
}).catch(() => {});
|
|
2506
2624
|
throw error;
|
|
@@ -2525,72 +2643,76 @@ var Transport = class {
|
|
|
2525
2643
|
cleanup: void 0
|
|
2526
2644
|
};
|
|
2527
2645
|
}
|
|
2528
|
-
/**
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2646
|
+
/** Вызывает `fetch` через перехватчики попытки; сбой сети становится ошибкой библиотеки. */
|
|
2647
|
+
#fetch(attempt, headers, body, contextOf) {
|
|
2648
|
+
const init = {
|
|
2649
|
+
method: attempt.method,
|
|
2650
|
+
headers,
|
|
2651
|
+
signal: attempt.abort.signal,
|
|
2652
|
+
...body !== void 0 ? { body } : {},
|
|
2653
|
+
...this.#config.sendCredentials ? { credentials: "include" } : {}
|
|
2654
|
+
};
|
|
2655
|
+
if (typeof ReadableStream !== "undefined" && body instanceof ReadableStream) init.duplex = "half";
|
|
2656
|
+
const execute = async () => {
|
|
2657
|
+
try {
|
|
2658
|
+
return await this.#config.fetch(attempt.url, init);
|
|
2659
|
+
} catch (error) {
|
|
2660
|
+
throw this.#toTransportError(attempt, error);
|
|
2661
|
+
}
|
|
2662
|
+
};
|
|
2663
|
+
const interceptors = attemptInterceptorScope(attempt.request);
|
|
2664
|
+
if (!interceptors) return execute();
|
|
2665
|
+
return runAttemptInterceptors(interceptors, {
|
|
2666
|
+
...contextOf(),
|
|
2667
|
+
body
|
|
2668
|
+
}, execute);
|
|
2543
2669
|
}
|
|
2544
2670
|
/**
|
|
2545
|
-
*
|
|
2671
|
+
* Сообщает хуку `onError` об ошибке попытки и отмечает её как переданную.
|
|
2546
2672
|
*
|
|
2547
|
-
*
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
/**
|
|
2554
|
-
* Собирает общие заголовки клиента: `User-Agent`, идентификатор устройства,
|
|
2555
|
-
* заголовки конфигурации и cookie для указанного адреса.
|
|
2673
|
+
* Ожидание хука ограничено lifecycle операции; срок попытки на него не действует.
|
|
2674
|
+
* Если ошибка попытки — сама отмена операции, о ней сообщает тот уровень, который заметил
|
|
2675
|
+
* её первым; второй хук не вызывает. Отмена, заставшая хук с другой ошибкой, остаётся за
|
|
2676
|
+
* верхней границей операции.
|
|
2677
|
+
*
|
|
2678
|
+
* @throws ошибку самого хука, если операция не отменена
|
|
2556
2679
|
*/
|
|
2557
|
-
async
|
|
2558
|
-
const
|
|
2559
|
-
|
|
2560
|
-
if (this.#config.
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2680
|
+
async #report(attempt, headers, error) {
|
|
2681
|
+
const { request } = attempt;
|
|
2682
|
+
attempt.abort.disarm();
|
|
2683
|
+
if (!hasRequestHook(this.#config.hooks, "onError")) return;
|
|
2684
|
+
markRequestErrorReported(request, error);
|
|
2685
|
+
const lifecycle = request.signal;
|
|
2686
|
+
const notify = () => dispatchRequestHook(this.#config.hooks, "onError", {
|
|
2687
|
+
...this.#context(attempt, headers),
|
|
2688
|
+
duration: this.#elapsed(attempt),
|
|
2689
|
+
error
|
|
2690
|
+
});
|
|
2691
|
+
if (lifecycle?.aborted) {
|
|
2692
|
+
if (claimAbortReport(request)) notify().catch(() => {});
|
|
2693
|
+
return;
|
|
2694
|
+
}
|
|
2695
|
+
try {
|
|
2696
|
+
const pending = notify();
|
|
2697
|
+
await (lifecycle ? waitForRequest(pending, lifecycle) : pending);
|
|
2698
|
+
} catch (hookError) {
|
|
2699
|
+
if (lifecycle?.aborted) return;
|
|
2700
|
+
markRequestErrorReported(request, hookError);
|
|
2701
|
+
throw hookError;
|
|
2566
2702
|
}
|
|
2567
|
-
return headers;
|
|
2568
|
-
}
|
|
2569
|
-
/**
|
|
2570
|
-
* Дополняет общие заголовки значением `Accept`, заголовками конвейера и вызова.
|
|
2571
|
-
* Заголовки вызова применяются последними.
|
|
2572
|
-
*/
|
|
2573
|
-
async #buildHeaders(request, url) {
|
|
2574
|
-
const headers = await this.platformHeaders(url, request.cookieJar);
|
|
2575
|
-
if (!headers.has("Accept")) headers.set("Accept", "application/json");
|
|
2576
|
-
for (const [name, value] of Object.entries(request.layerHeaders ?? {})) setHeader(headers, name, value);
|
|
2577
|
-
for (const [name, value] of Object.entries(request.headers ?? {})) setHeader(headers, name, value);
|
|
2578
|
-
return headers;
|
|
2579
2703
|
}
|
|
2580
|
-
/** Превращает исключение `fetch` в понятную ошибку библиотеки. */
|
|
2581
|
-
#toTransportError(
|
|
2582
|
-
const
|
|
2583
|
-
if (aborted &&
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
path: request.path
|
|
2587
|
-
});
|
|
2588
|
-
if (aborted) {
|
|
2704
|
+
/** Превращает исключение `fetch` или отмену в понятную ошибку библиотеки. */
|
|
2705
|
+
#toTransportError(attempt, error) {
|
|
2706
|
+
const { abort, method, request } = attempt;
|
|
2707
|
+
if (abort.signal.aborted || error instanceof Error && error.name === "AbortError") {
|
|
2708
|
+
const expired = abort.expired();
|
|
2709
|
+
if (expired) return expired;
|
|
2589
2710
|
const reason = abort.signal.reason;
|
|
2590
|
-
|
|
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);
|
|
2591
2713
|
}
|
|
2592
|
-
if (error instanceof
|
|
2593
|
-
return new
|
|
2714
|
+
if (error instanceof require_validate.ItdError) return error;
|
|
2715
|
+
return new require_validate.ItdNetworkError(`Не удалось выполнить ${method} ${request.path}: ${describe(error)}`, {
|
|
2594
2716
|
method,
|
|
2595
2717
|
path: request.path,
|
|
2596
2718
|
cause: error
|
|
@@ -2602,6 +2724,7 @@ var Transport = class {
|
|
|
2602
2724
|
/** Имена стадий обработки запроса в порядке выполнения. @internal */
|
|
2603
2725
|
const ClientRuntimeStage = Object.freeze({
|
|
2604
2726
|
OperationPlugins: "operation_plugins",
|
|
2727
|
+
Decode: "decode",
|
|
2605
2728
|
Services: "services",
|
|
2606
2729
|
AuthPreflight: "auth_preflight",
|
|
2607
2730
|
Retry: "retry",
|
|
@@ -2649,14 +2772,27 @@ function createClientRuntime(options, internals) {
|
|
|
2649
2772
|
path: request.path
|
|
2650
2773
|
}) ?? catalog.bucketOf(request.operationId);
|
|
2651
2774
|
};
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2775
|
+
/** Origin направления, мемоизированный по базовому адресу. */
|
|
2776
|
+
const origins = /* @__PURE__ */ new Map();
|
|
2777
|
+
const destinationOf = (baseUrl) => {
|
|
2778
|
+
if (origins.has(baseUrl)) return origins.get(baseUrl);
|
|
2779
|
+
if (origins.size >= 256) origins.clear();
|
|
2780
|
+
const origin = 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
|
+
});
|
|
2656
2792
|
const queueFor = (request) => {
|
|
2657
2793
|
if (!queues) return void 0;
|
|
2658
2794
|
const key = queueKeyFor(request);
|
|
2659
|
-
return queues.for(key.destination, key.bucket);
|
|
2795
|
+
return queues.for(key.destination, key.bucket, key.definition);
|
|
2660
2796
|
};
|
|
2661
2797
|
/** Передаёт остаток из заголовков ответа бакету запроса; тот решает, тормозить ли себя. */
|
|
2662
2798
|
const observeRateLimit = (limit, remaining, request) => {
|
|
@@ -2665,16 +2801,21 @@ function createClientRuntime(options, internals) {
|
|
|
2665
2801
|
const waited = queue.observe(limit, remaining);
|
|
2666
2802
|
if (waited > 0) config.logger?.debug(`остаток лимита ${remaining} из ${limit ?? "?"}, бакет ${queue.bucket} ждёт ${waited} мс`);
|
|
2667
2803
|
};
|
|
2668
|
-
transport = new Transport({
|
|
2669
|
-
...config,
|
|
2670
|
-
hooks: config.hooks
|
|
2671
|
-
}, {
|
|
2804
|
+
transport = new Transport(config, {
|
|
2672
2805
|
onRateLimit: queues ? observeRateLimit : void 0,
|
|
2673
2806
|
cookies: config.useCookieJar ? jar : void 0,
|
|
2674
|
-
getDeviceId: () => auth.deviceId()
|
|
2675
|
-
lifetimeSignal: lifetime.signal
|
|
2807
|
+
getDeviceId: () => auth.deviceId()
|
|
2676
2808
|
});
|
|
2677
|
-
|
|
2809
|
+
/** Стадии логической операции: выполняются один раз на вызов метода. */
|
|
2810
|
+
const operationStages = [{
|
|
2811
|
+
name: ClientRuntimeStage.OperationPlugins,
|
|
2812
|
+
middleware: createPluginsMiddleware(plugins)
|
|
2813
|
+
}, {
|
|
2814
|
+
name: ClientRuntimeStage.Decode,
|
|
2815
|
+
middleware: createDecodeMiddleware()
|
|
2816
|
+
}];
|
|
2817
|
+
/** Стадии исполнения: от выбора сервиса до транспортной попытки. */
|
|
2818
|
+
const executionStages = [
|
|
2678
2819
|
{
|
|
2679
2820
|
name: ClientRuntimeStage.Services,
|
|
2680
2821
|
middleware: createServicesMiddleware(services)
|
|
@@ -2689,7 +2830,7 @@ function createClientRuntime(options, internals) {
|
|
|
2689
2830
|
clock: config.clock,
|
|
2690
2831
|
catalog,
|
|
2691
2832
|
retry: config.retry,
|
|
2692
|
-
rateLimitDelays: config.rateLimit?.retryDelays
|
|
2833
|
+
rateLimitDelays: config.rateLimit?.retryDelays,
|
|
2693
2834
|
pauseQueue: queues ? (ms, request) => queueFor(request)?.pause(ms) : void 0,
|
|
2694
2835
|
hooks: config.hooks,
|
|
2695
2836
|
logger: config.logger,
|
|
@@ -2702,39 +2843,41 @@ function createClientRuntime(options, internals) {
|
|
|
2702
2843
|
},
|
|
2703
2844
|
{
|
|
2704
2845
|
name: ClientRuntimeStage.AuthPreparation,
|
|
2705
|
-
middleware: createAuthPreparationMiddleware(
|
|
2846
|
+
middleware: createAuthPreparationMiddleware(() => auth)
|
|
2706
2847
|
}
|
|
2707
2848
|
];
|
|
2708
|
-
if (queues)
|
|
2849
|
+
if (queues) executionStages.push({
|
|
2709
2850
|
name: ClientRuntimeStage.Queue,
|
|
2710
2851
|
middleware: createQueueMiddleware((request, task) => {
|
|
2711
2852
|
const queue = queueFor(request);
|
|
2712
2853
|
return queue ? queue.schedule(task, request.signal) : task();
|
|
2713
2854
|
})
|
|
2714
2855
|
});
|
|
2715
|
-
|
|
2856
|
+
executionStages.push({
|
|
2716
2857
|
name: ClientRuntimeStage.Attempt,
|
|
2717
2858
|
middleware: createAttemptMiddleware()
|
|
2718
|
-
}
|
|
2719
|
-
stages.push({
|
|
2859
|
+
}, {
|
|
2720
2860
|
name: ClientRuntimeStage.AuthHeaders,
|
|
2721
2861
|
middleware: createAuthHeadersMiddleware({ currentHeaders: () => auth.currentHeaders() })
|
|
2722
2862
|
});
|
|
2723
|
-
const
|
|
2724
|
-
|
|
2863
|
+
const execution = composePipeline(executionStages.map(({ middleware }) => middleware), transport.send);
|
|
2864
|
+
/**
|
|
2865
|
+
* Проверки между стадиями операции и исполнения: обёртка плагина уже могла изменить запрос,
|
|
2866
|
+
* а после начала `dispose()` завершаются только служебные запросы финализации.
|
|
2867
|
+
*/
|
|
2868
|
+
const guardedExecution = (request) => {
|
|
2725
2869
|
try {
|
|
2726
2870
|
if (!isDisposeCleanupRequest(request)) internals.assertActive?.("выполнить новый запрос");
|
|
2727
2871
|
if (request.rateLimitBucket !== void 0) assertKnownBucket(request.rateLimitBucket, "rateLimitBucket", config.rateLimit?.bucket, catalog);
|
|
2728
2872
|
} catch (error) {
|
|
2729
2873
|
return Promise.reject(error);
|
|
2730
2874
|
}
|
|
2731
|
-
return
|
|
2875
|
+
return execution(request);
|
|
2732
2876
|
};
|
|
2733
2877
|
const http = new HttpClient({
|
|
2734
|
-
handler:
|
|
2735
|
-
plugins,
|
|
2878
|
+
handler: composePipeline(operationStages.map(({ middleware }) => middleware), guardedExecution),
|
|
2736
2879
|
baseUrl: config.baseUrl,
|
|
2737
|
-
|
|
2880
|
+
deadline: config.deadline,
|
|
2738
2881
|
clock: config.clock,
|
|
2739
2882
|
lifetimeSignal: lifetime.signal,
|
|
2740
2883
|
hooks: config.hooks,
|
|
@@ -2746,8 +2889,8 @@ function createClientRuntime(options, internals) {
|
|
|
2746
2889
|
cookies: jar
|
|
2747
2890
|
});
|
|
2748
2891
|
const stageOrder = Object.freeze([
|
|
2749
|
-
|
|
2750
|
-
...
|
|
2892
|
+
...operationStages.map(({ name }) => name),
|
|
2893
|
+
...executionStages.map(({ name }) => name),
|
|
2751
2894
|
ClientRuntimeStage.Transport
|
|
2752
2895
|
]);
|
|
2753
2896
|
return {
|
|
@@ -2780,12 +2923,11 @@ function createClientRuntime(options, internals) {
|
|
|
2780
2923
|
},
|
|
2781
2924
|
platformHeaders: (url) => transport.platformHeaders(url),
|
|
2782
2925
|
rateLimitState: () => queues?.states() ?? [],
|
|
2783
|
-
registerRateLimitBucket: (name, definition) => queues?.defineBucket(name, definition),
|
|
2784
2926
|
close: () => {
|
|
2785
2927
|
if (ownsQueues) queues?.stop();
|
|
2786
2928
|
},
|
|
2787
2929
|
dispose: async () => {
|
|
2788
|
-
lifetime.abort(new
|
|
2930
|
+
lifetime.abort(new require_validate.ItdAbortError("Клиент освобождён через dispose(), запрос отменён"));
|
|
2789
2931
|
if (ownsQueues) queues?.clear();
|
|
2790
2932
|
auth.dispose();
|
|
2791
2933
|
await plugins.dispose();
|
|
@@ -3406,20 +3548,23 @@ function operationBucket(id) {
|
|
|
3406
3548
|
}
|
|
3407
3549
|
//#endregion
|
|
3408
3550
|
//#region src/domain/catalog.ts
|
|
3551
|
+
/** Ограничения встроенных бакетов: ёмкость из таблицы плюс встроенные поправки. */
|
|
3552
|
+
const BUCKET_DEFINITIONS = Object.freeze(Object.fromEntries(Object.entries(BUCKET_LIMITS).map(([name, limit]) => [name, Object.freeze({
|
|
3553
|
+
limit,
|
|
3554
|
+
...DEFAULT_BUCKET_OVERRIDES[name]
|
|
3555
|
+
})])));
|
|
3409
3556
|
/**
|
|
3410
3557
|
* Каталог операций итд.com, которым пользуется ядро.
|
|
3411
3558
|
*
|
|
3412
3559
|
* Единственное место, где generic request executor встречается с таблицами конкретного API.
|
|
3413
|
-
* Собирается из уже существующих функций каталога: собственной логики здесь нет.
|
|
3414
3560
|
*
|
|
3415
3561
|
* @internal
|
|
3416
3562
|
*/
|
|
3417
3563
|
const ITD_CATALOG = Object.freeze({
|
|
3418
3564
|
definitionOf: (id) => isBuiltInOperationId(id) ? OPERATIONS[id] : void 0,
|
|
3419
|
-
retrySafetyOf: (id) => isBuiltInOperationId(id) ? operationRetrySafety(id) : void 0,
|
|
3420
|
-
methodOf: (id) => isBuiltInOperationId(id) ? operationMethod(id) : void 0,
|
|
3421
3565
|
bucketOf: (id) => operationBucket(id),
|
|
3422
3566
|
isKnownBucket,
|
|
3567
|
+
bucketDefinitionOf: (name) => BUCKET_DEFINITIONS[name],
|
|
3423
3568
|
bucketLimits: BUCKET_LIMITS,
|
|
3424
3569
|
bucketOverrides: DEFAULT_BUCKET_OVERRIDES,
|
|
3425
3570
|
defaultBucket: DEFAULT_RATE_LIMIT_BUCKET
|
|
@@ -3786,7 +3931,7 @@ function isKnownNotificationType(type) {
|
|
|
3786
3931
|
//#endregion
|
|
3787
3932
|
//#region src/notifications/normalize.ts
|
|
3788
3933
|
function asActor(value) {
|
|
3789
|
-
if (!isRecord(value)) return void 0;
|
|
3934
|
+
if (!require_validate.isRecord(value)) return void 0;
|
|
3790
3935
|
const id = asString(value.id);
|
|
3791
3936
|
if (!id) return void 0;
|
|
3792
3937
|
return {
|
|
@@ -3826,8 +3971,8 @@ function readActors(source) {
|
|
|
3826
3971
|
* ```
|
|
3827
3972
|
*/
|
|
3828
3973
|
function normalizeNotification(input) {
|
|
3829
|
-
const source = isRecord(input) ? input : {};
|
|
3830
|
-
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;
|
|
3831
3976
|
const rawType = asString(payload.type) ?? asString(source.type) ?? "";
|
|
3832
3977
|
const createdAt = asString(payload.createdAt) ?? asString(source.createdAt) ?? "";
|
|
3833
3978
|
const readAt = asString(payload.readAt) ?? asString(source.readAt);
|
|
@@ -3859,7 +4004,7 @@ function normalizeNotification(input) {
|
|
|
3859
4004
|
* счётчик непрочитанных и признак звука.
|
|
3860
4005
|
*/
|
|
3861
4006
|
function readNotificationEvent(data) {
|
|
3862
|
-
const source = isRecord(data) ? data : {};
|
|
4007
|
+
const source = require_validate.isRecord(data) ? data : {};
|
|
3863
4008
|
return {
|
|
3864
4009
|
notification: normalizeNotification(data),
|
|
3865
4010
|
unreadCount: typeof source.unreadCount === "number" ? source.unreadCount : void 0,
|
|
@@ -3872,8 +4017,8 @@ function readNotificationEvent(data) {
|
|
|
3872
4017
|
* Возвращает `undefined`, если сервер прислал событие без вложенного `payload`.
|
|
3873
4018
|
*/
|
|
3874
4019
|
function readUnreadCountEvent(data) {
|
|
3875
|
-
if (!isRecord(data)) return void 0;
|
|
3876
|
-
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;
|
|
3877
4022
|
if (!payload) return void 0;
|
|
3878
4023
|
return typeof payload.count === "number" ? payload.count : void 0;
|
|
3879
4024
|
}
|
|
@@ -3914,7 +4059,6 @@ function localDeviceId() {
|
|
|
3914
4059
|
function anonymousAuth() {
|
|
3915
4060
|
return {
|
|
3916
4061
|
token: () => Promise.resolve(null),
|
|
3917
|
-
prepare: () => Promise.resolve(),
|
|
3918
4062
|
currentHeaders: () => ({}),
|
|
3919
4063
|
recover: () => Promise.resolve(false),
|
|
3920
4064
|
deviceId: localDeviceId(),
|
|
@@ -3936,7 +4080,6 @@ function bearerToken(accessToken) {
|
|
|
3936
4080
|
const headers = { Authorization: `Bearer ${accessToken}` };
|
|
3937
4081
|
return {
|
|
3938
4082
|
token: () => Promise.resolve(accessToken),
|
|
3939
|
-
prepare: () => Promise.resolve(),
|
|
3940
4083
|
currentHeaders: () => headers,
|
|
3941
4084
|
recover: () => Promise.resolve(false),
|
|
3942
4085
|
deviceId: localDeviceId(),
|
|
@@ -4226,6 +4369,12 @@ Object.defineProperty(exports, "OPERATIONS", {
|
|
|
4226
4369
|
return OPERATIONS;
|
|
4227
4370
|
}
|
|
4228
4371
|
});
|
|
4372
|
+
Object.defineProperty(exports, "OperationMethod", {
|
|
4373
|
+
enumerable: true,
|
|
4374
|
+
get: function() {
|
|
4375
|
+
return OperationMethod;
|
|
4376
|
+
}
|
|
4377
|
+
});
|
|
4229
4378
|
Object.defineProperty(exports, "QrLocationPrecision", {
|
|
4230
4379
|
enumerable: true,
|
|
4231
4380
|
get: function() {
|
|
@@ -4388,18 +4537,6 @@ Object.defineProperty(exports, "isKnownNotificationType", {
|
|
|
4388
4537
|
return isKnownNotificationType;
|
|
4389
4538
|
}
|
|
4390
4539
|
});
|
|
4391
|
-
Object.defineProperty(exports, "isRecord", {
|
|
4392
|
-
enumerable: true,
|
|
4393
|
-
get: function() {
|
|
4394
|
-
return isRecord;
|
|
4395
|
-
}
|
|
4396
|
-
});
|
|
4397
|
-
Object.defineProperty(exports, "isRecord$1", {
|
|
4398
|
-
enumerable: true,
|
|
4399
|
-
get: function() {
|
|
4400
|
-
return isRecord$1;
|
|
4401
|
-
}
|
|
4402
|
-
});
|
|
4403
4540
|
Object.defineProperty(exports, "mergeService", {
|
|
4404
4541
|
enumerable: true,
|
|
4405
4542
|
get: function() {
|
|
@@ -4490,12 +4627,6 @@ Object.defineProperty(exports, "requestAbortError", {
|
|
|
4490
4627
|
return requestAbortError;
|
|
4491
4628
|
}
|
|
4492
4629
|
});
|
|
4493
|
-
Object.defineProperty(exports, "requireOptionalBoolean", {
|
|
4494
|
-
enumerable: true,
|
|
4495
|
-
get: function() {
|
|
4496
|
-
return requireOptionalBoolean;
|
|
4497
|
-
}
|
|
4498
|
-
});
|
|
4499
4630
|
Object.defineProperty(exports, "resolveNotificationUrl", {
|
|
4500
4631
|
enumerable: true,
|
|
4501
4632
|
get: function() {
|
|
@@ -4533,4 +4664,4 @@ Object.defineProperty(exports, "voidResult", {
|
|
|
4533
4664
|
}
|
|
4534
4665
|
});
|
|
4535
4666
|
|
|
4536
|
-
//# sourceMappingURL=url-
|
|
4667
|
+
//# sourceMappingURL=url-DTgkCWv5.cjs.map
|