itd-api 0.10.4 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +2 -1
  2. package/dist/events/index.cjs +27 -25
  3. package/dist/events/index.d.cts +3 -3
  4. package/dist/events/index.d.ts +3 -3
  5. package/dist/events/index.js +5 -5
  6. package/dist/index.cjs +136 -116
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.cts +18 -7
  9. package/dist/index.d.ts +18 -7
  10. package/dist/index.js +46 -30
  11. package/dist/index.js.map +1 -1
  12. package/dist/node/index.cjs +13 -13
  13. package/dist/node/index.cjs.map +1 -1
  14. package/dist/node/index.js +5 -5
  15. package/dist/rest/index.cjs +43 -40
  16. package/dist/rest/index.cjs.map +1 -1
  17. package/dist/rest/index.d.cts +3 -3
  18. package/dist/rest/index.d.ts +3 -3
  19. package/dist/rest/index.js +6 -6
  20. package/dist/shared/{events-B8cjz_Ed.cjs → events-C6tmj8aW.cjs} +63 -59
  21. package/dist/shared/events-C6tmj8aW.cjs.map +1 -0
  22. package/dist/shared/{events-DGdU8q1X.d.ts → events-CAb9G7Sf.d.ts} +2 -2
  23. package/dist/shared/{events-D2n7Yu1P.js → events-MWNcnfrq.js} +19 -15
  24. package/dist/shared/events-MWNcnfrq.js.map +1 -0
  25. package/dist/shared/{events-CK3zHntT.d.cts → events-Sc75x6rT.d.cts} +2 -2
  26. package/dist/shared/{key-value-store-B5GYVEYZ.cjs → key-value-store-DYSPsqwq.cjs} +11 -14
  27. package/dist/shared/key-value-store-DYSPsqwq.cjs.map +1 -0
  28. package/dist/shared/{key-value-store-Bt2PSMxY.js → key-value-store-w1Dn5Pjb.js} +5 -8
  29. package/dist/shared/key-value-store-w1Dn5Pjb.js.map +1 -0
  30. package/dist/shared/{multi-storage-C3Ngtop1.js → multi-storage-BLBrGN9Q.js} +6 -6
  31. package/dist/shared/multi-storage-BLBrGN9Q.js.map +1 -0
  32. package/dist/shared/{multi-storage-s-fPtjoy.cjs → multi-storage-C7-Gz6Z7.cjs} +8 -8
  33. package/dist/shared/multi-storage-C7-Gz6Z7.cjs.map +1 -0
  34. package/dist/shared/{options-DjEtbVaf.cjs → options-5tywTYId.cjs} +13 -13
  35. package/dist/shared/{options-DjEtbVaf.cjs.map → options-5tywTYId.cjs.map} +1 -1
  36. package/dist/shared/{options-i4LrKfqx.js → options-BerKicCs.js} +3 -3
  37. package/dist/shared/{options-i4LrKfqx.js.map → options-BerKicCs.js.map} +1 -1
  38. package/dist/shared/{redact-DJV0XM3n.js → redact-C5oxIvcc.js} +79 -18
  39. package/dist/shared/redact-C5oxIvcc.js.map +1 -0
  40. package/dist/shared/{redact-lAmck1Z6.cjs → redact-DoC7lSsp.cjs} +109 -24
  41. package/dist/shared/redact-DoC7lSsp.cjs.map +1 -0
  42. package/dist/shared/{render-UsOGMRzS.d.ts → render-CBroJPcV.d.ts} +178 -252
  43. package/dist/shared/{render-9F_kR6sq.d.cts → render-DM7AhTZW.d.cts} +178 -252
  44. package/dist/shared/{render-C6CJ343E.js → render-DRh5_aRe.js} +128 -135
  45. package/dist/shared/render-DRh5_aRe.js.map +1 -0
  46. package/dist/shared/{render-BrrZOSV5.cjs → render-Dnc84B4M.cjs} +234 -241
  47. package/dist/shared/render-Dnc84B4M.cjs.map +1 -0
  48. package/dist/shared/{storage-DiDe0PZM.js → storage-86wiHeXD.js} +3 -3
  49. package/dist/shared/{storage-DiDe0PZM.js.map → storage-86wiHeXD.js.map} +1 -1
  50. package/dist/shared/{storage-CZJZoaz9.cjs → storage-CIjejZDt.cjs} +4 -4
  51. package/dist/shared/{storage-CZJZoaz9.cjs.map → storage-CIjejZDt.cjs.map} +1 -1
  52. package/dist/shared/{url-yB6T_H_-.d.ts → url-BOi-_mJT.d.cts} +143 -79
  53. package/dist/shared/{url-yB6T_H_-.d.cts → url-BOi-_mJT.d.ts} +143 -79
  54. package/dist/shared/{url-Dc7wLikf.js → url-Bg61sk_Y.js} +723 -590
  55. package/dist/shared/url-Bg61sk_Y.js.map +1 -0
  56. package/dist/shared/{url-BNTKqBMB.cjs → url-DTgkCWv5.cjs} +830 -709
  57. package/dist/shared/url-DTgkCWv5.cjs.map +1 -0
  58. package/dist/shared/{errors-BmP3TKoW.cjs → validate-CFx2dISs.cjs} +84 -5
  59. package/dist/shared/validate-CFx2dISs.cjs.map +1 -0
  60. package/dist/shared/{errors-GI10kZxk.js → validate-D7_kq3g5.js} +55 -6
  61. package/dist/shared/validate-D7_kq3g5.js.map +1 -0
  62. package/dist/web/index.cjs +4 -4
  63. package/dist/web/index.cjs.map +1 -1
  64. package/dist/web/index.js +3 -3
  65. package/package.json +1 -1
  66. package/dist/shared/errors-BmP3TKoW.cjs.map +0 -1
  67. package/dist/shared/errors-GI10kZxk.js.map +0 -1
  68. package/dist/shared/events-B8cjz_Ed.cjs.map +0 -1
  69. package/dist/shared/events-D2n7Yu1P.js.map +0 -1
  70. package/dist/shared/key-value-store-B5GYVEYZ.cjs.map +0 -1
  71. package/dist/shared/key-value-store-Bt2PSMxY.js.map +0 -1
  72. package/dist/shared/multi-storage-C3Ngtop1.js.map +0 -1
  73. package/dist/shared/multi-storage-s-fPtjoy.cjs.map +0 -1
  74. package/dist/shared/redact-DJV0XM3n.js.map +0 -1
  75. package/dist/shared/redact-lAmck1Z6.cjs.map +0 -1
  76. package/dist/shared/render-BrrZOSV5.cjs.map +0 -1
  77. package/dist/shared/render-C6CJ343E.js.map +0 -1
  78. package/dist/shared/url-BNTKqBMB.cjs.map +0 -1
  79. package/dist/shared/url-Dc7wLikf.js.map +0 -1
@@ -1,5 +1,5 @@
1
- const require_errors = require("./errors-BmP3TKoW.cjs");
2
- const require_redact = require("./redact-lAmck1Z6.cjs");
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.10.4";
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 (!isRecord$1(headers)) throw new require_errors.ItdConfigError("headers должен быть объектом строк");
124
- for (const [name, value] of Object.entries(headers)) if (typeof value !== "string") throw new require_errors.ItdConfigError(`headers.${name} должен быть строкой`);
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 (!isRecord$1(hooks)) throw new require_errors.ItdConfigError("hooks должен быть объектом");
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 require_errors.ItdConfigError(`hooks.${name} должен быть функцией`);
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 && !isRecord$1(retry)) throw new require_errors.ItdConfigError("retry должен быть объектом или false");
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 require_errors.ItdConfigError(`retry.attempts должен быть целым числом от 1, получено: ${attempts}`);
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 require_errors.ItdConfigError(`retry.jitter должен быть в диапазоне 0…1, получено: ${jitter}`);
176
- if (options.shouldRetry !== void 0 && typeof options.shouldRetry !== "function") throw new require_errors.ItdConfigError("retry.shouldRetry должен быть функцией");
177
- const baseDelay = requirePositive(options.baseDelay ?? 500, "retry.baseDelay");
178
- const maxDelay = requirePositive(options.maxDelay ?? 3e4, "retry.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 require_errors.ItdConfigError(`${option}: бакета «${name}» нет. Известны: ${Object.keys(catalog.bucketLimits).join(", ")}`);
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 require_errors.ItdConfigError(`${name} должен быть целым числом от 1, получено: ${value}`);
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 require_errors.ItdConfigError(`${name} должен быть положительным числом, получено: ${value}`);
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 require_errors.ItdConfigError(`rateLimit.pacing должен быть одним из ${known.join(", ")}, получено: ${pacing}`);
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 (!isRecord$1(overrides)) throw new require_errors.ItdConfigError("rateLimit.bucketOverrides должен быть объектом");
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 (!isRecord$1(override)) throw new require_errors.ItdConfigError(`rateLimit.bucketOverrides.${name} должен быть объектом`);
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 require_errors.ItdConfigError(`rateLimit.bucketOverrides.${name}.limit должен быть положительным числом, получено: ${override.limit}`);
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 && !isRecord$1(rateLimit)) throw new require_errors.ItdConfigError("rateLimit должен быть объектом или false");
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 require_errors.ItdConfigError("rateLimit.retryDelays должен быть массивом чисел");
273
- for (const delay of retryDelays) requirePositive(delay, "rateLimit.retryDelays");
274
- requireOptionalBoolean(rateLimit.buckets, "rateLimit.buckets");
275
- if (rateLimit.bucket !== void 0 && typeof rateLimit.bucket !== "function") throw new require_errors.ItdConfigError("rateLimit.bucket должен быть функцией");
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 (!isRecord$1(services)) throw new require_errors.ItdConfigError("services должен быть объектом");
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 (!isRecord$1(value)) throw new require_errors.ItdConfigError(`services.${name} должен быть URL или объектом сервиса`);
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 (!isRecord$1(options)) throw new require_errors.ItdConfigError("опции клиента должны быть объектом");
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 require_errors.ItdConfigError(`mode должен быть одним из ${Object.values(require_redact.RuntimeMode).join(", ")}, получено: ${mode}`);
321
- const timeout = requirePositive(options.timeout ?? 3e4, "timeout");
322
- const shutdownTimeout = requirePositive(options.shutdownTimeout ?? 1e4, "shutdownTimeout");
323
- 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_errors.ItdConfigError("clock должен предоставлять методы now() и schedule()");
324
- if (options.userAgent !== void 0 && options.userAgent !== false && typeof options.userAgent !== "string") throw new require_errors.ItdConfigError("userAgent должен быть строкой или false");
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(request, context, execute) {
370
- return attemptInterceptorScope(request).reduceRight((next, { plugin, interceptor }) => async () => {
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 require_errors.ItdConfigError(`attempt interceptor плагина «${plugin}» вызвал next() больше одного раза`);
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 require_errors.ItdConfigError(`attempt interceptor плагина «${plugin}» должен вернуть Response`);
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 require_errors.ItdConfigError(`плагин «${plugin.name}»: ${field} должен быть массивом`);
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 require_errors.ItdConfigError(`плагин «${plugin.name}»: ${field} содержит пустое имя плагина`);
402
- if (!isPluginName(value)) throw new require_errors.ItdConfigError(`плагин «${plugin.name}»: имя «${value}» в ${field} должно соответствовать ${PLUGIN_NAME_FORMAT}`);
403
- if (value === plugin.name) throw new require_errors.ItdConfigError(`плагин «${plugin.name}» не может указать себя в ${field}`);
404
- if (seen.has(value)) throw new require_errors.ItdConfigError(`плагин «${plugin.name}»: ${field} повторяет имя «${value}»`);
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 require_errors.ItdConfigError("Плагин должен быть объектом с методом install()");
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 require_errors.ItdConfigError("У плагина должно быть непустое имя");
421
- if (!isPluginName(name)) throw new require_errors.ItdConfigError(`Имя плагина «${name}» должно соответствовать ${PLUGIN_NAME_FORMAT}`);
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 require_errors.ItdConfigError(`плагин «${plugin.name}» уже подключён`);
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 require_errors.ItdConfigError(`плагину «${plugin.name}» требуется плагин «${required}»`);
450
- for (const conflict of plugin.conflicts ?? []) if (byName.has(conflict)) throw new require_errors.ItdConfigError(`плагин «${plugin.name}» несовместим с плагином «${conflict}»`);
451
- for (const { plugin: other } of entries) if (other.conflicts?.includes(plugin.name)) throw new require_errors.ItdConfigError(`плагин «${plugin.name}» несовместим с плагином «${other.name}»`);
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 require_errors.ItdConfigError(`циклический порядок плагинов: ${cycle.join(" → ")}`);
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 require_errors.ItdConfigError(`нельзя отключить плагин «${name}»: от него зависит «${dependent.name}»`);
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 require_errors.ItdConfigError(`плагин «${plugin.name}» ещё отключается; дождитесь завершения unuse() или dispose()`);
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.assertCanAdd(plugin);
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 require_errors.ItdConfigError(`плагин «${plugin.name}» передал в ${kind}.use() не функцию`);
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 require_errors.ItdConfigError(`install() плагина «${plugin.name}» должен вернуть функцию или void`);
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 require_errors.ItdConfigError(`плагин «${definition.name}» не установлен`);
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 require_errors.ItdConfigError(`operation transformer не может заменить operationId «${operationId}» на «${prepared.operationId}»`);
682
- if (prepared.method.toUpperCase() !== method) throw new require_errors.ItdConfigError(`operation transformer не может заменить метод «${method}» операции «${operationId}» на «${prepared.method}»`);
683
- if (prepared.retrySafety !== void 0 && prepared.retrySafety !== retrySafety) throw new require_errors.ItdConfigError(`operation transformer не может заменить retrySafety операции «${operationId}»`);
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 require_errors.ItdConfigError(`operation transformer плагина «${plugin}» вызвал next() больше одного раза`);
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 require_errors.ItdStateError(`плагин «${entry.plugin.name}» не завершил операции за ${this.#options.shutdownTimeout} мс`));
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 require_errors.ItdStateError(`плагин «${entry.plugin.name}» не завершил teardown за ${this.#options.shutdownTimeout} мс`));
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 require_errors.ItdAbortError("Запрос отменён во время ожидания очереди");
712
+ return new require_validate.ItdAbortError("Запрос отменён во время ожидания очереди");
752
713
  }
753
714
  /** Ошибка запроса, которого застала остановка очереди. */
754
715
  function queueStoppedError() {
755
- return new require_errors.ItdAbortError("Клиент закрыт, запрос отменён");
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, featureDefinition) {
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
- this.#seedLimit = options.buckets ? featureDefinition?.limit ?? seedLimit(bucket, options) : void 0;
905
- this.#concurrency = options.buckets ? featureDefinition?.concurrency ?? options.bucketOverrides[bucket]?.concurrency ?? options.bucketConcurrency : options.concurrency;
906
- const rps = options.buckets ? featureDefinition?.rps ?? options.bucketOverrides[bucket]?.rps : void 0;
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.length;
948
+ return this.#waiting.size;
921
949
  }
922
950
  /** Есть ли ожидающая задача, для которой свободен локальный слот. */
923
951
  get canStart() {
924
- return this.#waiting.length > 0 && this.#active < this.#concurrency;
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
- const index = this.#waiting.indexOf(queued);
938
- if (index < 0) return;
939
- this.#waiting.splice(index, 1);
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.timingChanged();
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.length
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 pending = this.#waiting.splice(0, this.#waiting.length);
1031
- for (const task of pending) task.cancel(reason);
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.timingChanged();
1090
+ this.#scheduler.changed();
1067
1091
  }
1068
1092
  };
1069
- /** Лимит бакета до первого ответа: поправка пользователя важнее табличного значения. */
1070
- function seedLimit(bucket, options) {
1071
- const override = options.bucketOverrides[bucket]?.limit;
1072
- if (override !== void 0) return override;
1073
- return options.bucketLimits[bucket];
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
- * Регистрирует бакет подключаемого feature.
1125
+ * Очередь бакета на направлении. При `buckets: false` бакет всегда `default`.
1126
+ *
1127
+ * Начальные ограничения приходят из каталога вызывающего клиента и запоминаются при
1128
+ * создании очереди. Клиенты, разделяющие один пул через `ItdAccounts`, обязаны описывать
1129
+ * бакет одинаково: расхождение обнаруживается при обращении к уже созданной очереди.
1097
1130
  *
1098
- * Повтор той же декларации разрешён клиентам, разделяющим один pool через `ItdAccounts`.
1099
- * Возвращённая функция откатывает регистрацию, пока очередь бакета ещё не создана.
1131
+ * @throws {ItdConfigError} если очередь бакета создана с другими ограничениями
1100
1132
  */
1101
- defineBucket(name, definition) {
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, this.#featureBuckets.get(name)?.definition);
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 require_errors.ItdConfigError("У сервиса должно быть непустое имя");
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 require_errors.ItdConfigError(`Сервис «${name}» уже зарегистрирован и не может быть заменён`);
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 require_errors.ItdConfigError(`services.${name}.auth должен быть boolean`);
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 require_errors.ItdConfigError(`services.${name}.headers должен быть объектом строк`);
1214
- for (const [header, value] of Object.entries(definition.headers)) if (typeof value !== "string") throw new require_errors.ItdConfigError(`services.${name}.headers.${header} должен быть строкой`);
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 require_errors.ItdConfigError(`Сервис «${name}» не зарегистрирован. ` + (known.length > 0 ? `Известны: ${known.join(", ")}` : "Зарегистрируйте его через itd.defineService() или опцию services"));
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
- /** Объединяет отмену вызова, завершение клиента и общий таймаут логической операции. @internal */
1303
- function createRequestAbortScope(signal, lifetimeSignal, timeout, clock) {
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 timeoutReached = false;
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
- const cancelTimer = timeout > 0 ? clock.schedule(() => {
1318
- timeoutReached = true;
1319
- controller.abort();
1320
- }, timeout) : void 0;
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
- timedOut: () => timeoutReached,
1342
+ abort: (reason) => controller.abort(reason),
1343
+ expired: () => expired,
1344
+ disarm,
1324
1345
  cleanup: () => {
1325
- cancelTimer?.();
1346
+ disarm();
1326
1347
  for (const remove of detach) remove?.();
1327
1348
  }
1328
1349
  };
1329
1350
  }
1330
- /** Прерывает только ожидание; исходный промис может корректно завершить общую фоновую работу. @internal */
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 require_errors.ItdAbortError(void 0, { cause: signal.reason }));
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 require_errors.ItdAbortError(void 0, { cause: signal.reason }));
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
- /** Возвращает публичную ошибку, соответствующую причине остановки операции. @internal */
1371
+ /**
1372
+ * Возвращает публичную ошибку, соответствующую причине остановки ожидания.
1373
+ *
1374
+ * Истёкший срок отдаёт ошибку, созданную таймером области. Отмена с причиной-ошибкой
1375
+ * библиотеки (истёкший срок родительской области) отдаёт саму причину; прочая отмена
1376
+ * оборачивается в {@link ItdAbortError}.
1377
+ *
1378
+ * @internal
1379
+ */
1346
1380
  function requestAbortError(scope, request, fallback) {
1347
- if (scope.timedOut()) return new require_errors.ItdTimeoutError({
1348
- timeout: request.timeout,
1349
- method: request.method.toUpperCase(),
1350
- path: request.path
1351
- });
1352
- if (scope.signal.aborted && !(fallback instanceof require_errors.ItdAbortError)) return new require_errors.ItdAbortError(`Запрос ${request.method.toUpperCase()} ${request.path} отменён`, { cause: scope.signal.reason });
1353
- return fallback;
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 REQUEST_ATTEMPT_STATE = Symbol("itd-api.request-attempt-state");
1358
- const REQUEST_QUEUE_KEY = Symbol("itd-api.request-queue-key");
1359
- const REQUEST_AUTH_RECOVERY_STATE = Symbol("itd-api.request-auth-recovery-state");
1360
- const REQUEST_ERROR_OBSERVATION_STATE = Symbol("itd-api.request-error-observation-state");
1361
- const DISPOSE_CLEANUP_REQUEST = Symbol("itd-api.dispose-cleanup-request");
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 internal = request;
1365
- const current = internal[REQUEST_AUTH_RECOVERY_STATE];
1438
+ const state = operationState(request);
1439
+ const current = state.authRecovery;
1366
1440
  if (current) return current;
1367
- const state = {
1441
+ const recovery = {
1368
1442
  recovered: false,
1369
1443
  preparationErrors: /* @__PURE__ */ new Set()
1370
1444
  };
1371
- internal[REQUEST_AUTH_RECOVERY_STATE] = state;
1372
- return state;
1445
+ state.authRecovery = recovery;
1446
+ return recovery;
1373
1447
  }
1374
- /** Создаёт общую для копий логического запроса отметку вызова `onError`. @internal */
1375
- function trackRequestErrorObservation(request) {
1376
- const internal = request;
1377
- internal[REQUEST_ERROR_OBSERVATION_STATE] ??= {
1448
+ /** Создаёт общий для копий логического запроса учёт вызовов `onError`. @internal */
1449
+ function trackRequestErrorReporting(request) {
1450
+ const state = operationState(request);
1451
+ state.errorReporting ??= {
1378
1452
  errors: /* @__PURE__ */ new Set(),
1379
- lifecycleAbortedDuringNotification: false
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 markRequestErrorObserved(request, error) {
1384
- trackRequestErrorObservation(request);
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 wasRequestErrorObserved(request, error) {
1389
- return request[REQUEST_ERROR_OBSERVATION_STATE]?.errors.has(error) ?? false;
1390
- }
1391
- /** Отмечает отмену lifecycle во время уже начатого локального `onError`. @internal */
1392
- function markRequestErrorNotificationAborted(request) {
1393
- trackRequestErrorObservation(request);
1394
- const state = request[REQUEST_ERROR_OBSERVATION_STATE];
1395
- if (state) state.lifecycleAbortedDuringNotification = true;
1465
+ function wasRequestErrorReported(request, error) {
1466
+ return request[OPERATION_STATE]?.errorReporting?.errors.has(error) ?? false;
1396
1467
  }
1397
- /** Нужно ли верхней границе не запускать тот же `onError` повторно после отмены. @internal */
1398
- function wasRequestErrorNotificationAborted(request) {
1399
- return request[REQUEST_ERROR_OBSERVATION_STATE]?.lifecycleAbortedDuringNotification ?? false;
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 tracked = trackRequestAttempts(request);
1419
- const state = tracked[REQUEST_ATTEMPT_STATE];
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
- ...tracked,
1424
- attempt: state.value
1492
+ ...request,
1493
+ attempt: state.attempt
1425
1494
  };
1426
1495
  }
1427
1496
  /** Возвращает номер последней начатой транспортной попытки. @internal */
1428
1497
  function currentTransportAttempt(request) {
1429
- return request[REQUEST_ATTEMPT_STATE]?.value ?? 0;
1498
+ return request[OPERATION_STATE]?.attempt ?? 0;
1430
1499
  }
1431
1500
  /**
1432
1501
  * Вычисляет ключ очереди один раз на логическую операцию.
1433
1502
  *
1434
1503
  * Ключ спрашивают трижды: при постановке в очередь, при чтении заголовков ответа и при
1435
- * паузе после `429`. Значение пишется прямо в объект запроса слои ниже копируют его
1436
- * через spread, и перечислимое символьное поле переходит в копии.
1504
+ * паузе после `429`. Значение хранится в общем состоянии операции, поэтому его видят все
1505
+ * копии запроса ниже по конвейеру. Ограничения бакета фиксируются вместе с ключом:
1506
+ * освобождение feature во время запроса не меняет очередь операции.
1437
1507
  *
1438
1508
  * @internal
1439
1509
  */
1440
1510
  function requestQueueKey(request, compute) {
1441
- const internal = request;
1442
- const cached = internal[REQUEST_QUEUE_KEY];
1511
+ const state = operationState(request);
1512
+ const cached = state.queueKey;
1443
1513
  if (cached) return cached;
1444
1514
  const key = compute(request);
1445
- internal[REQUEST_QUEUE_KEY] = key;
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[DISPOSE_CLEANUP_REQUEST] === true;
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
- * Принимает готовую цепочку обработки и предоставляет ресурсам методы `request`/`execute`.
1491
- * О слоях и их порядке ресурсы не знают.
1562
+ * Заводит общий lifecycle логической операции отмену, освобождение клиента и `deadline`, —
1563
+ * передаёт запрос собранной цепочке стадий и сообщает в `onError` об ошибках, которые
1564
+ * не дошли до транспорта. О стадиях и их порядке ресурсы не знают.
1492
1565
  */
1493
1566
  var HttpClient = class {
1494
1567
  #handler;
1495
- #plugins;
1496
1568
  #baseUrl;
1497
- #timeout;
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.#timeout = deps.timeout;
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
- /** Базовый URL, к которому обращается клиент. */
1513
- get baseUrl() {
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
- * Выполняет запрос к API через собранный конвейер.
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
- const request = identifyRequest(options);
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
- const request = identifyRequest({
1545
- ...options,
1546
- operationId: operation.id,
1547
- method: operation.method,
1548
- retrySafety: options.retrySafety ?? operation.retrySafety
1605
+ return this.#run(operationRequest(operation, options), {
1606
+ allowDisposed: true,
1607
+ reader: operation.read,
1608
+ disposeCleanup: true
1609
+ });
1610
+ }
1611
+ async #run(request, options = {}) {
1612
+ if (!options.allowDisposed) this.#assertActive?.();
1613
+ const deadline = request.deadline ?? this.#deadline;
1614
+ const scope = createRequestAbortScope(request.signal, void 0, this.#clock, {
1615
+ after: deadline,
1616
+ error: () => new require_validate.ItdTimeoutError({
1617
+ timeout: deadline,
1618
+ method: request.method.toUpperCase(),
1619
+ path: request.path,
1620
+ budget: require_validate.TimeoutBudget.Deadline
1621
+ })
1549
1622
  });
1550
- return this.#runWithLifecycle(request, async (prepared) => {
1551
- const cleanupRequest = markDisposeCleanupRequest(prepared);
1552
- return operation.read(await this.#handler(cleanupRequest), prepared);
1553
- }, true);
1554
- }
1555
- async #runWithLifecycle(request, execute, allowDisposed = false) {
1556
- if (!allowDisposed) this.#assertActive?.();
1557
- const timeout = request.timeout ?? this.#timeout;
1558
- const scope = createRequestAbortScope(request.signal, this.#lifetimeSignal, timeout, this.#clock);
1623
+ if (this.#lifetimeSignal?.aborted) scope.abort(this.#lifetimeSignal.reason);
1624
+ else this.#activeScopes.add(scope);
1559
1625
  const startedAt = this.#clock.now();
1560
- trackRequestErrorObservation(request);
1626
+ const tracked = initializeOperationState(request, {
1627
+ lifecycleSignal: scope.signal,
1628
+ reader: options.reader,
1629
+ disposeCleanup: options.disposeCleanup
1630
+ });
1561
1631
  try {
1562
- const pending = this.#plugins.run(request, (prepared) => execute({
1563
- ...prepared,
1564
- signal: scope.signal,
1565
- timeout: 0
1566
- }), scope.signal);
1567
- return await waitForRequest(Promise.resolve(pending), scope.signal);
1632
+ return await waitForRequest(this.#handler(tracked), scope.signal);
1568
1633
  } catch (error) {
1569
- const failure = requestAbortError(scope, {
1570
- timeout,
1571
- method: request.method,
1572
- path: request.path
1573
- }, error);
1574
- if (!(wasRequestErrorObserved(request, error) || scope.signal.aborted && wasRequestErrorNotificationAborted(request))) {
1575
- markRequestErrorObserved(request, error);
1576
- let headers;
1577
- try {
1578
- headers = new Headers({
1579
- ...request.layerHeaders,
1580
- ...request.headers
1581
- });
1582
- } catch {
1583
- headers = new Headers();
1584
- }
1585
- const notification = dispatchRequestHook(this.#hooks, "onError", {
1586
- operationId: request.operationId,
1587
- signal: scope.signal,
1588
- method: request.method.toUpperCase(),
1589
- path: request.path,
1590
- url: require_redact.joinUrl(request.baseUrl ?? this.#baseUrl, request.path) + require_redact.buildQuery(request.query),
1591
- headers,
1592
- attempt: request.attempt ?? 1,
1593
- duration: this.#clock.now() - startedAt,
1594
- error: failure
1595
- });
1596
- try {
1597
- await waitForRequest(notification, scope.signal);
1598
- } catch (hookError) {
1599
- if (!scope.signal.aborted) throw hookError;
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 require_errors.ItdConfigError(`annotations операции «${operationId}» не должны содержать функции`);
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 require_errors.ItdConfigError(`annotations операции «${operationId}» должны состоять из обычных объектов, массивов и примитивов`);
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 require_errors.ItdConfigError(`annotations операции «${operationId}» не должны содержать getters или setters`);
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.retrySafetyOf(request.operationId);
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
- * Обрыв сети, timeout и `5xx` такой гарантии не дают и требуют safe/idempotent операции.
1780
+ * Обрыв сети, таймаут попытки и `5xx` такой гарантии не дают и требуют safe/idempotent операции.
1697
1781
  * Ошибка подготовки файла возникает до обращения к серверу и потому зависит только от
1698
1782
  * собственного признака retryable и повторяемости тела.
1699
1783
  */
1700
1784
  function isRetryable(error, policy) {
1701
- if (error instanceof require_errors.ItdAbortError || !policy.bodyReplayable) return false;
1785
+ if (isTerminal(error) || !policy.bodyReplayable) return false;
1702
1786
  const repeatableOperation = policy.retrySafety === RetrySafety.Safe || policy.retrySafety === RetrySafety.Idempotent;
1703
- if (error instanceof require_errors.ItdApiError) {
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 require_errors.ItdNetworkError || error instanceof require_errors.ItdTimeoutError) return repeatableOperation;
1709
- if (error instanceof require_errors.ItdFileError) return error.retryable;
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 instanceof require_errors.ItdAbortError || !policy.bodyReplayable) return void 0;
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 require_errors.ItdApiError && error.retryAfter !== void 0) return error.retryAfter > options.maxDelay ? void 0 : error.retryAfter;
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 require_errors.ItdAbortError("Запрос отменён во время ожидания повтора"));
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 require_errors.ItdAbortError("Запрос отменён во время ожидания повтора"));
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 async (request, next) => {
1811
- if (!request.skipAuth) {
1812
- const auth = getAuth();
1813
- if (auth.preflight) {
1814
- const pending = auth.preflight(request.skipAuthRefresh !== true);
1815
- await (request.signal ? waitForRequest(pending, request.signal) : pending);
1816
- }
1817
- }
1818
- return next(request);
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(auth) {
1828
- return async (request, next) => {
1829
- if (!request.skipAuth) {
1830
- const pending = auth.prepare();
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
- return next(request);
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 || !require_errors.isItdApiError(error) || error.status !== 401) throw error;
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
- * всей очереди; сетевые сбои и `5xx` — экспоненциальным откатом. Настройка `retry`
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 nextDelay = (error, retryAttempt, rateLimitAttempt, request, policy, backoff) => {
1910
- if (require_errors.isItdRateLimitError(error)) {
1911
- if (!policy.bodyReplayable) return void 0;
1912
- const wait = error.retryAfter ?? deps.rateLimitDelays[rateLimitAttempt - 1];
1913
- if (wait === void 0) return void 0;
1914
- deps.pauseQueue?.(wait, request);
1915
- deps.logger?.debug(`лимит частоты, повтор ${rateLimitAttempt} через ${wait} мс`);
1916
- return wait;
1917
- }
1918
- return backoff?.(error, retryAttempt, policy);
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 async (request, next) => {
1921
- const trackedRequest = trackRequestAttempts(request);
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
- const backoff = resolveBackoff(request.retry, globalScheduler);
1925
- let retryAttempt = 0;
1926
- let rateLimitAttempt = 0;
1927
- for (;;) try {
1928
- return await next(trackedRequest);
1929
- } catch (error) {
1930
- const transportAttempt = currentTransportAttempt(trackedRequest);
1931
- if (require_errors.isItdRateLimitError(error)) rateLimitAttempt += 1;
1932
- else retryAttempt += 1;
1933
- const delay = nextDelay(error, retryAttempt, rateLimitAttempt, trackedRequest, policy, backoff);
1934
- if (delay === void 0) throw error;
1935
- const notification = dispatchRequestHook(deps.hooks, "onRetry", {
1936
- operationId: request.operationId,
1937
- signal: request.signal,
1938
- method,
1939
- path: request.path,
1940
- url: deps.buildUrl(request),
1941
- headers: new Headers({
1942
- ...request.layerHeaders,
1943
- ...request.headers
1944
- }),
1945
- attempt: transportAttempt,
1946
- error,
1947
- delay
1948
- });
1949
- await (request.signal ? waitForRequest(notification, request.signal) : notification);
1950
- deps.logger?.debug(`повтор ${method} ${request.path}, попытка ${transportAttempt + 1} через ${delay} мс`);
1951
- await sleep(deps.clock ?? systemClock, delay, request.signal);
1952
- }
2043
+ return (async () => {
2044
+ let retryAttempt = 0;
2045
+ let rateLimitAttempt = 0;
2046
+ for (;;) try {
2047
+ return await next(request);
2048
+ } catch (error) {
2049
+ if (request.signal?.aborted) throw error;
2050
+ const transportAttempt = currentTransportAttempt(request);
2051
+ const rateLimited = ladder !== void 0 && require_validate.isItdRateLimitError(error);
2052
+ if (rateLimited) rateLimitAttempt += 1;
2053
+ else retryAttempt += 1;
2054
+ const delay = rateLimited ? ladderDelay(error, rateLimitAttempt, request, policy) : backoff?.(error, retryAttempt, policy);
2055
+ if (delay === void 0) throw error;
2056
+ if (hasRequestHook(deps.hooks, "onRetry")) {
2057
+ const notification = dispatchRequestHook(deps.hooks, "onRetry", {
2058
+ operationId: request.operationId,
2059
+ signal: request.signal,
2060
+ method,
2061
+ path: request.path,
2062
+ url: deps.buildUrl(request),
2063
+ headers: new Headers({
2064
+ ...request.layerHeaders,
2065
+ ...request.headers
2066
+ }),
2067
+ attempt: transportAttempt,
2068
+ error,
2069
+ delay
2070
+ });
2071
+ await (request.signal ? waitForRequest(notification, request.signal) : notification);
2072
+ }
2073
+ deps.logger?.info(`повтор ${method} ${request.path}, попытка ${transportAttempt + 1} через ${delay} мс`);
2074
+ await sleep(deps.clock ?? systemClock, delay, request.signal);
2075
+ }
2076
+ })();
1953
2077
  };
1954
2078
  }
1955
2079
  //#endregion
@@ -1979,15 +2103,6 @@ function unwrapData(body) {
1979
2103
  if (keys.length !== 1 || keys[0] !== "data") return body;
1980
2104
  return body.data;
1981
2105
  }
1982
- /**
1983
- * Обычный объект — не `null`, не массив.
1984
- *
1985
- * Живёт здесь, а не в каждом разборщике ответа: одна и та же проверка нужна и фабрике
1986
- * ошибок, и приведению уведомлений.
1987
- */
1988
- function isRecord(value) {
1989
- return typeof value === "object" && value !== null && !Array.isArray(value);
1990
- }
1991
2106
  /** Непустая строка либо `undefined`. Отличается от {@link pickString} тем, что берёт значение, а не поле. */
1992
2107
  function asString(value) {
1993
2108
  return typeof value === "string" && value.length > 0 ? value : void 0;
@@ -2047,7 +2162,7 @@ function readPlainTextErrorCode(value) {
2047
2162
  function collectFieldErrors(source) {
2048
2163
  const result = {};
2049
2164
  const errors = source.errors;
2050
- if (isRecord(errors)) {
2165
+ if (require_validate.isRecord(errors)) {
2051
2166
  for (const [field, value] of Object.entries(errors)) if (Array.isArray(value)) {
2052
2167
  const messages = value.filter((item) => typeof item === "string");
2053
2168
  if (messages.length > 0) result[field] = messages;
@@ -2055,7 +2170,7 @@ function collectFieldErrors(source) {
2055
2170
  }
2056
2171
  const violations = source.violations;
2057
2172
  if (Array.isArray(violations)) for (const violation of violations) {
2058
- if (!isRecord(violation)) continue;
2173
+ if (!require_validate.isRecord(violation)) continue;
2059
2174
  const field = asString(violation.field) ?? asString(violation.property);
2060
2175
  const message = asString(violation.message);
2061
2176
  if (!field || !message) continue;
@@ -2088,7 +2203,7 @@ function parseErrorBody(body, status, statusText = "") {
2088
2203
  userId: void 0
2089
2204
  };
2090
2205
  }
2091
- if (!isRecord(body)) return {
2206
+ if (!require_validate.isRecord(body)) return {
2092
2207
  code: "UNKNOWN_ERROR",
2093
2208
  message: fallbackMessage,
2094
2209
  detail: void 0,
@@ -2107,7 +2222,7 @@ function parseErrorBody(body, status, statusText = "") {
2107
2222
  userId: void 0
2108
2223
  };
2109
2224
  }
2110
- const inner = isRecord(body.error) ? body.error : body;
2225
+ const inner = require_validate.isRecord(body.error) ? body.error : body;
2111
2226
  const message = asString(inner.message) ?? asString(inner.detail) ?? asString(inner.title) ?? asString(body.error) ?? fallbackMessage;
2112
2227
  return {
2113
2228
  code: asString(inner.code) ?? asString(body.code) ?? "UNKNOWN_ERROR",
@@ -2174,30 +2289,30 @@ function getRequestId(headers) {
2174
2289
  }
2175
2290
  /** Соответствие кода ошибки конкретному классу. Приоритетнее, чем HTTP-статус. */
2176
2291
  const CODE_TO_CLASS = {
2177
- VALIDATION_ERROR: require_errors.ItdValidationError,
2178
- RATE_LIMIT_EXCEEDED: require_errors.ItdRateLimitError,
2179
- UNAUTHORIZED: require_errors.ItdAuthError,
2180
- SESSION_EXPIRED: require_errors.ItdAuthError,
2181
- SESSION_REVOKED: require_errors.ItdAuthError,
2182
- SESSION_INVALID_REFRESH_TOKEN: require_errors.ItdAuthError,
2183
- REFRESH_TOKEN_MISSING: require_errors.ItdAuthError,
2184
- SESSION_NOT_FOUND: require_errors.ItdAuthError,
2185
- ACCOUNT_INVALID_CREDENTIALS: require_errors.ItdAuthError,
2186
- ACCESS_DENIED: require_errors.ItdForbiddenError,
2187
- ENTITY_NOT_FOUND: require_errors.ItdNotFoundError,
2188
- NOT_FOUND: require_errors.ItdNotFoundError,
2189
- ENTITY_ALREADY_EXISTS: require_errors.ItdConflictError
2292
+ VALIDATION_ERROR: require_validate.ItdValidationError,
2293
+ RATE_LIMIT_EXCEEDED: require_validate.ItdRateLimitError,
2294
+ UNAUTHORIZED: require_validate.ItdAuthError,
2295
+ SESSION_EXPIRED: require_validate.ItdAuthError,
2296
+ SESSION_REVOKED: require_validate.ItdAuthError,
2297
+ SESSION_INVALID_REFRESH_TOKEN: require_validate.ItdAuthError,
2298
+ REFRESH_TOKEN_MISSING: require_validate.ItdAuthError,
2299
+ SESSION_NOT_FOUND: require_validate.ItdAuthError,
2300
+ ACCOUNT_INVALID_CREDENTIALS: require_validate.ItdAuthError,
2301
+ ACCESS_DENIED: require_validate.ItdForbiddenError,
2302
+ ENTITY_NOT_FOUND: require_validate.ItdNotFoundError,
2303
+ NOT_FOUND: require_validate.ItdNotFoundError,
2304
+ ENTITY_ALREADY_EXISTS: require_validate.ItdConflictError
2190
2305
  };
2191
2306
  /** Соответствие HTTP-статуса классу — запасной вариант, когда код ничего не говорит. */
2192
2307
  function classByStatus(status) {
2193
- if (status === 401) return require_errors.ItdAuthError;
2194
- if (status === 403) return require_errors.ItdForbiddenError;
2195
- if (status === 404) return require_errors.ItdNotFoundError;
2196
- if (status === 409) return require_errors.ItdConflictError;
2197
- if (status === 422) return require_errors.ItdValidationError;
2198
- if (status === 429) return require_errors.ItdRateLimitError;
2199
- if (status >= 500) return require_errors.ItdServerError;
2200
- return require_errors.ItdApiError;
2308
+ if (status === 401) return require_validate.ItdAuthError;
2309
+ if (status === 403) return require_validate.ItdForbiddenError;
2310
+ if (status === 404) return require_validate.ItdNotFoundError;
2311
+ if (status === 409) return require_validate.ItdConflictError;
2312
+ if (status === 422) return require_validate.ItdValidationError;
2313
+ if (status === 429) return require_validate.ItdRateLimitError;
2314
+ if (status >= 500) return require_validate.ItdServerError;
2315
+ return require_validate.ItdApiError;
2201
2316
  }
2202
2317
  /**
2203
2318
  * Готовит тело ответа к сохранению в `ItdApiError.raw`.
@@ -2208,7 +2323,7 @@ function classByStatus(status) {
2208
2323
  * значения секретов заменяются заглушкой.
2209
2324
  */
2210
2325
  function safeRawBody(body) {
2211
- if (!isRecord(body) || body.type !== "validation" || !isRecord(body.found)) return body;
2326
+ if (!require_validate.isRecord(body) || body.type !== "validation" || !require_validate.isRecord(body.found)) return body;
2212
2327
  return {
2213
2328
  ...body,
2214
2329
  found: require_redact.redactBody(body.found)
@@ -2239,7 +2354,7 @@ function createApiError(context) {
2239
2354
  response: context.response,
2240
2355
  retryAfter: parseRetryAfter(context.headers?.get("retry-after"), context.now)
2241
2356
  };
2242
- if (parsed.code === "PHONE_VERIFICATION_REQUIRED") return new require_errors.ItdPhoneVerificationError({
2357
+ if (parsed.code === "PHONE_VERIFICATION_REQUIRED") return new require_validate.ItdPhoneVerificationError({
2243
2358
  ...init,
2244
2359
  userId: parsed.userId
2245
2360
  });
@@ -2252,7 +2367,7 @@ function setHeader(headers, name, value) {
2252
2367
  try {
2253
2368
  headers.set(name, value);
2254
2369
  } catch (cause) {
2255
- throw new require_errors.ItdConfigError(`Некорректный HTTP-заголовок ${JSON.stringify(name)}: проверьте его имя и значение.`, { cause });
2370
+ throw new require_validate.ItdConfigError(`Некорректный HTTP-заголовок ${JSON.stringify(name)}: проверьте его имя и значение.`, { cause });
2256
2371
  }
2257
2372
  }
2258
2373
  /** Тело, которое отправляется как есть, без сериализации в JSON. */
@@ -2281,33 +2396,15 @@ async function readBody(response) {
2281
2396
  const text = await response.text();
2282
2397
  return text === "" ? void 0 : text;
2283
2398
  }
2284
- /** Ошибка отмены в формате `fetch`. */
2285
- function createAbortError() {
2286
- const error = /* @__PURE__ */ new Error("Операция прервана");
2287
- error.name = "AbortError";
2288
- return error;
2289
- }
2290
- /** Прерывает ожидание промиса при срабатывании сигнала. */
2291
- function abortable(promise, signal) {
2292
- if (signal.aborted) {
2293
- promise.catch(() => {});
2294
- return Promise.reject(createAbortError());
2295
- }
2296
- let onAbort;
2297
- const interrupted = new Promise((_resolve, reject) => {
2298
- onAbort = () => reject(createAbortError());
2299
- signal.addEventListener("abort", onAbort, { once: true });
2300
- });
2301
- return Promise.race([promise, interrupted]).finally(() => {
2302
- if (onAbort) signal.removeEventListener("abort", onAbort);
2303
- });
2399
+ function describe(error) {
2400
+ return error instanceof Error ? error.message : String(error);
2304
2401
  }
2305
2402
  /**
2306
2403
  * Единственное место, откуда библиотека ходит в сеть.
2307
2404
  *
2308
- * Отвечает за сборку URL, заголовки, cookie, таймауты, разбор ответа и превращение любой
2309
- * неудачи в типизированную ошибку. Авторизация, повторы, очередь и плагины — отдельные
2310
- * слои конвейера, и транспорт о них не знает.
2405
+ * Отвечает за сборку URL, заголовки, cookie, срок одной попытки, разбор ответа и превращение
2406
+ * любой неудачи в типизированную ошибку. Авторизация, повторы, очередь и плагины — отдельные
2407
+ * стадии конвейера, и транспорт о них не знает.
2311
2408
  */
2312
2409
  var Transport = class {
2313
2410
  #config;
@@ -2316,141 +2413,114 @@ var Transport = class {
2316
2413
  this.#config = config;
2317
2414
  this.#deps = deps;
2318
2415
  }
2319
- /** Базовый URL, к которому обращается транспорт. */
2320
- get baseUrl() {
2321
- return this.#config.baseUrl;
2322
- }
2323
2416
  /**
2324
2417
  * Выполняет один сетевой запрос.
2325
2418
  *
2326
2419
  * @throws {ItdApiError} если сервер ответил статусом ≥ 400
2327
- * @throws {ItdTimeoutError} если истёк таймаут
2420
+ * @throws {ItdTimeoutError} если истёк срок попытки
2328
2421
  * @throws {ItdAbortError} если запрос отменён через `signal`
2329
2422
  * @throws {ItdNetworkError} если запрос не дошёл до сервера
2330
2423
  */
2331
2424
  send = async (input) => {
2332
2425
  const request = identifyRequest(input);
2333
- const method = request.method.toUpperCase();
2334
- const url = this.buildUrl(request);
2335
- const attempt = request.attempt ?? 1;
2336
2426
  const timeout = request.timeout ?? this.#config.timeout;
2337
- const abort = createRequestAbortScope(request.signal, this.#deps.lifetimeSignal, timeout, this.#config.clock);
2338
- const startedAt = this.#config.clock.now();
2427
+ const method = request.method.toUpperCase();
2428
+ const attempt = {
2429
+ request,
2430
+ method,
2431
+ url: this.buildUrl(request),
2432
+ abort: createRequestAbortScope(request.signal, void 0, this.#config.clock, {
2433
+ after: timeout,
2434
+ error: () => new require_validate.ItdTimeoutError({
2435
+ timeout,
2436
+ method,
2437
+ path: request.path,
2438
+ budget: require_validate.TimeoutBudget.Attempt
2439
+ })
2440
+ }),
2441
+ startedAt: this.#config.clock.now(),
2442
+ number: request.attempt ?? 1
2443
+ };
2444
+ const { signal } = attempt.abort;
2339
2445
  let cleanupBody;
2340
2446
  try {
2341
- const headers = await abortable(this.#buildHeaders(request, url), abort.signal).catch((error) => {
2342
- throw this.#toTransportError(error, abort, request, method, timeout);
2343
- });
2447
+ let headers;
2448
+ try {
2449
+ headers = await waitForRequest(this.#buildHeaders(request, attempt.url), signal);
2450
+ } catch (error) {
2451
+ throw this.#toTransportError(attempt, error);
2452
+ }
2344
2453
  let body;
2345
2454
  try {
2346
- const prepared = await this.#prepareBody(request, headers, abort.signal, attempt);
2455
+ const prepared = await this.#prepareBody(attempt, headers);
2347
2456
  body = prepared.body;
2348
2457
  cleanupBody = prepared.cleanup;
2349
2458
  } catch (error) {
2350
- const failure = abort.signal.aborted || error instanceof require_errors.ItdError ? this.#toTransportError(error, abort, request, method, timeout) : new require_errors.ItdConfigError(`Не удалось подготовить тело ${method} ${request.path}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
2351
- const context = {
2352
- operationId: request.operationId,
2353
- signal: abort.signal,
2354
- method,
2355
- path: request.path,
2356
- url,
2357
- headers,
2358
- attempt
2359
- };
2360
- await this.#dispatchErrorHook(request, {
2361
- ...context,
2362
- duration: this.#config.clock.now() - startedAt,
2363
- error: failure
2364
- }, abort, timeout);
2459
+ const failure = signal.aborted || error instanceof require_validate.ItdError ? this.#toTransportError(attempt, error) : new require_validate.ItdConfigError(`Не удалось подготовить тело ${attempt.method} ${request.path}: ${describe(error)}`, { cause: error });
2460
+ await this.#report(attempt, headers, failure);
2365
2461
  throw failure;
2366
2462
  }
2367
- const context = {
2368
- operationId: request.operationId,
2369
- signal: abort.signal,
2370
- method,
2371
- path: request.path,
2372
- url,
2373
- headers,
2374
- attempt
2375
- };
2376
- try {
2377
- await abortable(dispatchRequestHook(this.#config.hooks, "onRequest", context), abort.signal);
2463
+ let context;
2464
+ const contextOf = () => context ??= this.#context(attempt, headers);
2465
+ if (hasRequestHook(this.#config.hooks, "onRequest")) try {
2466
+ await waitForRequest(dispatchRequestHook(this.#config.hooks, "onRequest", { ...contextOf() }), signal);
2378
2467
  } catch (error) {
2379
- const failure = abort.signal.aborted ? this.#toTransportError(error, abort, request, method, timeout) : error;
2380
- await this.#dispatchErrorHook(request, {
2381
- ...context,
2382
- duration: this.#config.clock.now() - startedAt,
2383
- error: failure
2384
- }, abort, timeout);
2468
+ const failure = this.#abortedOr(attempt, error);
2469
+ await this.#report(attempt, headers, failure);
2385
2470
  throw failure;
2386
2471
  }
2387
- this.#config.logger?.debug(`→ ${method} ${request.path}`, {
2472
+ const logger = this.#config.logger;
2473
+ if (logger && require_redact.isLevelEnabled(logger, require_redact.LogLevel.Debug)) logger.debug(`→ ${attempt.method} ${request.path}`, {
2388
2474
  headers: require_redact.redactHeaders(headers),
2389
2475
  body: request.bodyFactory ? "[повторяемое тело]" : require_redact.redactBody(request.body)
2390
2476
  });
2391
2477
  let response;
2392
2478
  try {
2393
- const init = {
2394
- method,
2395
- headers,
2396
- signal: abort.signal,
2397
- ...body !== void 0 ? { body } : {},
2398
- ...this.#config.sendCredentials ? { credentials: "include" } : {}
2399
- };
2400
- if (typeof ReadableStream !== "undefined" && body instanceof ReadableStream) init.duplex = "half";
2401
- response = await runAttemptInterceptors(request, {
2402
- ...context,
2403
- body,
2404
- signal: abort.signal
2405
- }, async () => {
2406
- try {
2407
- return await this.#config.fetch(url, init);
2408
- } catch (error) {
2409
- throw this.#toTransportError(error, abort, request, method, timeout);
2410
- }
2411
- });
2479
+ response = await waitForRequest(this.#fetch(attempt, headers, body, contextOf), signal);
2412
2480
  } catch (error) {
2413
- const duration = this.#config.clock.now() - startedAt;
2414
- await this.#dispatchErrorHook(request, {
2415
- ...context,
2416
- duration,
2417
- error
2418
- }, abort, timeout);
2419
- this.#config.logger?.warn(`× ${method} ${request.path} (${duration} мс): ${error instanceof Error ? error.message : String(error)}`);
2420
- throw error;
2481
+ const failure = this.#abortedOr(attempt, error);
2482
+ await this.#report(attempt, headers, failure);
2483
+ this.#config.logger?.warn(`× ${attempt.method} ${request.path} (${this.#elapsed(attempt)} мс): ${describe(failure)}`);
2484
+ throw failure;
2421
2485
  }
2422
2486
  if (this.#deps.onRateLimit) {
2423
2487
  const { limit, remaining } = readRateLimit(response.headers);
2424
2488
  this.#deps.onRateLimit(limit, remaining, request);
2425
2489
  }
2426
- if (this.#config.useCookieJar) (request.cookieJar ?? this.#deps.cookies)?.setFromResponse(response.url || url, response);
2490
+ if (this.#config.useCookieJar) (request.cookieJar ?? this.#deps.cookies)?.setFromResponse(response.url || attempt.url, response);
2427
2491
  if (response.ok && hasRequestHook(this.#config.hooks, "onResponse")) {
2428
2492
  const hookResponse = response.clone();
2429
2493
  try {
2430
- await abortable(dispatchRequestHook(this.#config.hooks, "onResponse", {
2431
- ...context,
2494
+ await waitForRequest(dispatchRequestHook(this.#config.hooks, "onResponse", {
2495
+ ...contextOf(),
2432
2496
  status: response.status,
2433
- duration: this.#config.clock.now() - startedAt,
2497
+ duration: this.#elapsed(attempt),
2434
2498
  response: hookResponse
2435
- }), abort.signal);
2499
+ }), signal);
2436
2500
  } catch (error) {
2437
2501
  response.body?.cancel().catch(() => {});
2438
- const failure = abort.signal.aborted ? this.#toTransportError(error, abort, request, method, timeout) : error;
2439
- await this.#dispatchErrorHook(request, {
2440
- ...context,
2441
- duration: this.#config.clock.now() - startedAt,
2442
- error: failure
2443
- }, abort, timeout);
2502
+ const failure = this.#abortedOr(attempt, error);
2503
+ await this.#report(attempt, headers, failure);
2444
2504
  throw failure;
2445
2505
  } finally {
2446
2506
  if (!hookResponse.bodyUsed) hookResponse.body?.cancel().catch(() => {});
2447
2507
  }
2448
2508
  }
2449
- const payload = await this.#readBodyOrFail(response, context, request, method, abort, timeout, startedAt);
2450
- const duration = this.#config.clock.now() - startedAt;
2509
+ let payload;
2510
+ try {
2511
+ payload = await waitForRequest(readBody(response), signal);
2512
+ attempt.abort.disarm();
2513
+ } catch (error) {
2514
+ await response.body?.cancel().catch(() => {});
2515
+ const failure = this.#toTransportError(attempt, error);
2516
+ await this.#report(attempt, headers, failure);
2517
+ this.#config.logger?.warn(`× ${attempt.method} ${request.path}: не удалось прочитать тело ответа — ${failure.message}`);
2518
+ throw failure;
2519
+ }
2520
+ const duration = this.#elapsed(attempt);
2451
2521
  if (!response.ok) {
2452
2522
  const error = createApiError({
2453
- method,
2523
+ method: attempt.method,
2454
2524
  path: request.path,
2455
2525
  status: response.status,
2456
2526
  now: this.#config.clock.now(),
@@ -2459,58 +2529,96 @@ var Transport = class {
2459
2529
  response,
2460
2530
  body: payload
2461
2531
  });
2462
- await this.#dispatchErrorHook(request, {
2463
- ...context,
2464
- duration,
2465
- error
2466
- }, abort, timeout);
2467
- this.#config.logger?.warn(`← ${response.status} ${method} ${request.path} (${duration} мс): ${error.message}`);
2532
+ await this.#report(attempt, headers, error);
2533
+ this.#config.logger?.warn(`← ${response.status} ${attempt.method} ${request.path} (${duration} мс): ${error.message}`);
2468
2534
  throw error;
2469
2535
  }
2470
- this.#config.logger?.debug(`← ${response.status} ${method} ${request.path} (${duration} мс)`);
2536
+ this.#config.logger?.debug(`← ${response.status} ${attempt.method} ${request.path} (${duration} мс)`);
2471
2537
  return request.raw ? payload : unwrapData(payload);
2472
2538
  } finally {
2473
2539
  try {
2474
2540
  await cleanupBody?.();
2475
2541
  } catch (error) {
2476
- this.#config.logger?.warn(`не удалось закрыть тело ${method} ${request.path}`, error);
2542
+ this.#config.logger?.warn(`не удалось закрыть тело ${attempt.method} ${request.path}`, error);
2477
2543
  } finally {
2478
- abort.cleanup();
2544
+ attempt.abort.cleanup();
2479
2545
  }
2480
2546
  }
2481
2547
  };
2482
- async #dispatchErrorHook(request, context, abort, timeout) {
2483
- markRequestErrorObserved(request, context.error);
2484
- const onAbort = () => markRequestErrorNotificationAborted(request);
2485
- if (abort.signal.aborted) onAbort();
2486
- else abort.signal.addEventListener("abort", onAbort, { once: true });
2487
- try {
2488
- await abortable(dispatchRequestHook(this.#config.hooks, "onError", context), abort.signal);
2489
- } catch (error) {
2490
- const failure = abort.signal.aborted ? this.#toTransportError(error, abort, request, context.method, timeout) : error;
2491
- markRequestErrorObserved(request, failure);
2492
- throw failure;
2493
- } finally {
2494
- abort.signal.removeEventListener("abort", onAbort);
2548
+ /**
2549
+ * Итоговый URL со строкой запроса. Нужен и слою повторов — для хука `onRetry`.
2550
+ *
2551
+ * Хост берётся из самого запроса, если он там задан: у сервисов платформы свои домены.
2552
+ */
2553
+ buildUrl(request) {
2554
+ const base = request.baseUrl ?? this.#config.baseUrl;
2555
+ return require_redact.joinUrl(base, request.path) + require_redact.buildQuery(request.query);
2556
+ }
2557
+ /**
2558
+ * Собирает общие заголовки клиента: `User-Agent`, идентификатор устройства,
2559
+ * заголовки конфигурации и cookie для указанного адреса.
2560
+ */
2561
+ async platformHeaders(url, cookieJar = this.#deps.cookies) {
2562
+ const headers = new Headers();
2563
+ headers.set("X-Requested-With", "XMLHttpRequest");
2564
+ if (this.#config.userAgent) setHeader(headers, "User-Agent", this.#config.userAgent);
2565
+ if (this.#deps.getDeviceId) setHeader(headers, "X-Device-Id", await this.#deps.getDeviceId());
2566
+ for (const [name, value] of Object.entries(this.#config.headers)) setHeader(headers, name, value);
2567
+ if (this.#config.useCookieJar && cookieJar) {
2568
+ const cookie = cookieJar.getHeader(url);
2569
+ if (cookie) setHeader(headers, "Cookie", cookie);
2495
2570
  }
2571
+ return headers;
2572
+ }
2573
+ /** Данные попытки для хуков `onRequest`, `onResponse` и перехватчиков. */
2574
+ #context(attempt, headers) {
2575
+ return {
2576
+ operationId: attempt.request.operationId,
2577
+ signal: attempt.abort.signal,
2578
+ method: attempt.method,
2579
+ path: attempt.request.path,
2580
+ url: attempt.url,
2581
+ headers,
2582
+ attempt: attempt.number
2583
+ };
2584
+ }
2585
+ #elapsed(attempt) {
2586
+ return this.#config.clock.now() - attempt.startedAt;
2587
+ }
2588
+ /** Ошибка хука: отмена во время его работы становится транспортной ошибкой, остальное — как есть. */
2589
+ #abortedOr(attempt, error) {
2590
+ return attempt.abort.signal.aborted ? this.#toTransportError(attempt, error) : error;
2591
+ }
2592
+ /**
2593
+ * Дополняет общие заголовки значением `Accept`, заголовками конвейера и вызова.
2594
+ * Заголовки вызова применяются последними.
2595
+ */
2596
+ async #buildHeaders(request, url) {
2597
+ const headers = await this.platformHeaders(url, request.cookieJar);
2598
+ if (!headers.has("Accept")) headers.set("Accept", "application/json");
2599
+ for (const [name, value] of Object.entries(request.layerHeaders ?? {})) setHeader(headers, name, value);
2600
+ for (const [name, value] of Object.entries(request.headers ?? {})) setHeader(headers, name, value);
2601
+ return headers;
2496
2602
  }
2497
2603
  /** Подготавливает тело внутри попытки, чтобы поток можно было открыть заново при retry. */
2498
- async #prepareBody(request, headers, signal, attempt) {
2604
+ async #prepareBody(attempt, headers) {
2605
+ const { request } = attempt;
2606
+ const { signal } = attempt.abort;
2499
2607
  if (request.bodyFactory) {
2500
- if (request.body !== void 0 && request.body !== null) throw new require_errors.ItdConfigError("body и bodyFactory нельзя задавать одновременно");
2608
+ if (request.body !== void 0 && request.body !== null) throw new require_validate.ItdConfigError("body и bodyFactory нельзя задавать одновременно");
2501
2609
  const pending = Promise.resolve(request.bodyFactory({
2502
2610
  signal,
2503
- attempt
2611
+ attempt: attempt.number
2504
2612
  }));
2505
2613
  let prepared;
2506
2614
  try {
2507
- prepared = await abortable(pending, signal);
2615
+ prepared = await waitForRequest(pending, signal);
2508
2616
  } catch (error) {
2509
2617
  if (signal.aborted) pending.then(async (late) => {
2510
2618
  try {
2511
2619
  await late.cleanup?.();
2512
2620
  } catch (cleanupError) {
2513
- this.#config.logger?.warn(`не удалось закрыть отложенное тело ${request.method} ${request.path}`, cleanupError);
2621
+ this.#config.logger?.warn(`не удалось закрыть отложенное тело ${attempt.method} ${request.path}`, cleanupError);
2514
2622
  }
2515
2623
  }).catch(() => {});
2516
2624
  throw error;
@@ -2535,72 +2643,76 @@ var Transport = class {
2535
2643
  cleanup: void 0
2536
2644
  };
2537
2645
  }
2538
- /** Читает тело и преобразует ошибку чтения в транспортную ошибку библиотеки. */
2539
- async #readBodyOrFail(response, context, request, method, abort, timeout, startedAt) {
2540
- try {
2541
- return await abortable(readBody(response), abort.signal);
2542
- } catch (error) {
2543
- await response.body?.cancel().catch(() => {});
2544
- const failure = this.#toTransportError(error, abort, request, method, timeout);
2545
- await this.#dispatchErrorHook(request, {
2546
- ...context,
2547
- duration: this.#config.clock.now() - startedAt,
2548
- error: failure
2549
- }, abort, timeout);
2550
- this.#config.logger?.warn(`× ${method} ${request.path}: не удалось прочитать тело ответа — ${failure.message}`);
2551
- throw failure;
2552
- }
2646
+ /** Вызывает `fetch` через перехватчики попытки; сбой сети становится ошибкой библиотеки. */
2647
+ #fetch(attempt, headers, body, contextOf) {
2648
+ const init = {
2649
+ method: attempt.method,
2650
+ headers,
2651
+ signal: attempt.abort.signal,
2652
+ ...body !== void 0 ? { body } : {},
2653
+ ...this.#config.sendCredentials ? { credentials: "include" } : {}
2654
+ };
2655
+ if (typeof ReadableStream !== "undefined" && body instanceof ReadableStream) init.duplex = "half";
2656
+ const execute = async () => {
2657
+ try {
2658
+ return await this.#config.fetch(attempt.url, init);
2659
+ } catch (error) {
2660
+ throw this.#toTransportError(attempt, error);
2661
+ }
2662
+ };
2663
+ const interceptors = attemptInterceptorScope(attempt.request);
2664
+ if (!interceptors) return execute();
2665
+ return runAttemptInterceptors(interceptors, {
2666
+ ...contextOf(),
2667
+ body
2668
+ }, execute);
2553
2669
  }
2554
2670
  /**
2555
- * Итоговый URL со строкой запроса. Нужен и слою повторов для хука `onRetry`.
2671
+ * Сообщает хуку `onError` об ошибке попытки и отмечает её как переданную.
2556
2672
  *
2557
- * Хост берётся из самого запроса, если он там задан: у сервисов платформы свои домены.
2558
- */
2559
- buildUrl(request) {
2560
- const base = request.baseUrl ?? this.#config.baseUrl;
2561
- return require_redact.joinUrl(base, request.path) + require_redact.buildQuery(request.query);
2562
- }
2563
- /**
2564
- * Собирает общие заголовки клиента: `User-Agent`, идентификатор устройства,
2565
- * заголовки конфигурации и cookie для указанного адреса.
2673
+ * Ожидание хука ограничено lifecycle операции; срок попытки на него не действует.
2674
+ * Если ошибка попытки — сама отмена операции, о ней сообщает тот уровень, который заметил
2675
+ * её первым; второй хук не вызывает. Отмена, заставшая хук с другой ошибкой, остаётся за
2676
+ * верхней границей операции.
2677
+ *
2678
+ * @throws ошибку самого хука, если операция не отменена
2566
2679
  */
2567
- async platformHeaders(url, cookieJar = this.#deps.cookies) {
2568
- const headers = new Headers();
2569
- headers.set("X-Requested-With", "XMLHttpRequest");
2570
- if (this.#config.userAgent) setHeader(headers, "User-Agent", this.#config.userAgent);
2571
- if (this.#deps.getDeviceId) setHeader(headers, "X-Device-Id", await this.#deps.getDeviceId());
2572
- for (const [name, value] of Object.entries(this.#config.headers)) setHeader(headers, name, value);
2573
- if (this.#config.useCookieJar && cookieJar) {
2574
- const cookie = cookieJar.getHeader(url);
2575
- if (cookie) setHeader(headers, "Cookie", cookie);
2680
+ async #report(attempt, headers, error) {
2681
+ const { request } = attempt;
2682
+ attempt.abort.disarm();
2683
+ if (!hasRequestHook(this.#config.hooks, "onError")) return;
2684
+ markRequestErrorReported(request, error);
2685
+ const lifecycle = request.signal;
2686
+ const notify = () => dispatchRequestHook(this.#config.hooks, "onError", {
2687
+ ...this.#context(attempt, headers),
2688
+ duration: this.#elapsed(attempt),
2689
+ error
2690
+ });
2691
+ if (lifecycle?.aborted) {
2692
+ if (claimAbortReport(request)) notify().catch(() => {});
2693
+ return;
2694
+ }
2695
+ try {
2696
+ const pending = notify();
2697
+ await (lifecycle ? waitForRequest(pending, lifecycle) : pending);
2698
+ } catch (hookError) {
2699
+ if (lifecycle?.aborted) return;
2700
+ markRequestErrorReported(request, hookError);
2701
+ throw hookError;
2576
2702
  }
2577
- return headers;
2578
- }
2579
- /**
2580
- * Дополняет общие заголовки значением `Accept`, заголовками конвейера и вызова.
2581
- * Заголовки вызова применяются последними.
2582
- */
2583
- async #buildHeaders(request, url) {
2584
- const headers = await this.platformHeaders(url, request.cookieJar);
2585
- if (!headers.has("Accept")) headers.set("Accept", "application/json");
2586
- for (const [name, value] of Object.entries(request.layerHeaders ?? {})) setHeader(headers, name, value);
2587
- for (const [name, value] of Object.entries(request.headers ?? {})) setHeader(headers, name, value);
2588
- return headers;
2589
2703
  }
2590
- /** Превращает исключение `fetch` в понятную ошибку библиотеки. */
2591
- #toTransportError(error, abort, request, method, timeout) {
2592
- const aborted = abort.signal.aborted || error instanceof Error && error.name === "AbortError";
2593
- if (aborted && abort.timedOut()) return new require_errors.ItdTimeoutError({
2594
- timeout,
2595
- method,
2596
- path: request.path
2597
- });
2598
- if (aborted) {
2704
+ /** Превращает исключение `fetch` или отмену в понятную ошибку библиотеки. */
2705
+ #toTransportError(attempt, error) {
2706
+ const { abort, method, request } = attempt;
2707
+ if (abort.signal.aborted || error instanceof Error && error.name === "AbortError") {
2708
+ const expired = abort.expired();
2709
+ if (expired) return expired;
2599
2710
  const reason = abort.signal.reason;
2600
- return new require_errors.ItdAbortError(`Запрос ${method} ${request.path} отменён`, reason !== void 0 ? { cause: reason } : void 0);
2711
+ if (reason instanceof require_validate.ItdTimeoutError) return reason;
2712
+ return new require_validate.ItdAbortError(`Запрос ${method} ${request.path} отменён`, reason !== void 0 ? { cause: reason } : void 0);
2601
2713
  }
2602
- if (error instanceof require_errors.ItdError) return error;
2603
- return new require_errors.ItdNetworkError(`Не удалось выполнить ${method} ${request.path}: ${error instanceof Error ? error.message : String(error)}`, {
2714
+ if (error instanceof require_validate.ItdError) return error;
2715
+ return new require_validate.ItdNetworkError(`Не удалось выполнить ${method} ${request.path}: ${describe(error)}`, {
2604
2716
  method,
2605
2717
  path: request.path,
2606
2718
  cause: error
@@ -2612,6 +2724,7 @@ var Transport = class {
2612
2724
  /** Имена стадий обработки запроса в порядке выполнения. @internal */
2613
2725
  const ClientRuntimeStage = Object.freeze({
2614
2726
  OperationPlugins: "operation_plugins",
2727
+ Decode: "decode",
2615
2728
  Services: "services",
2616
2729
  AuthPreflight: "auth_preflight",
2617
2730
  Retry: "retry",
@@ -2659,14 +2772,27 @@ function createClientRuntime(options, internals) {
2659
2772
  path: request.path
2660
2773
  }) ?? catalog.bucketOf(request.operationId);
2661
2774
  };
2662
- const queueKeyFor = (request) => requestQueueKey(request, (target) => ({
2663
- destination: require_redact.originOf(transport.buildUrl(target)) || void 0,
2664
- bucket: bucketFor(target)
2665
- }));
2775
+ /** Origin направления, мемоизированный по базовому адресу. */
2776
+ const origins = /* @__PURE__ */ new Map();
2777
+ const destinationOf = (baseUrl) => {
2778
+ if (origins.has(baseUrl)) return origins.get(baseUrl);
2779
+ if (origins.size >= 256) origins.clear();
2780
+ const origin = require_redact.originOf(baseUrl) || void 0;
2781
+ origins.set(baseUrl, origin);
2782
+ return origin;
2783
+ };
2784
+ const queueKeyFor = (request) => requestQueueKey(request, (target) => {
2785
+ const bucket = bucketFor(target);
2786
+ return {
2787
+ destination: destinationOf(target.baseUrl ?? config.baseUrl),
2788
+ bucket,
2789
+ definition: catalog.bucketDefinitionOf(bucket)
2790
+ };
2791
+ });
2666
2792
  const queueFor = (request) => {
2667
2793
  if (!queues) return void 0;
2668
2794
  const key = queueKeyFor(request);
2669
- return queues.for(key.destination, key.bucket);
2795
+ return queues.for(key.destination, key.bucket, key.definition);
2670
2796
  };
2671
2797
  /** Передаёт остаток из заголовков ответа бакету запроса; тот решает, тормозить ли себя. */
2672
2798
  const observeRateLimit = (limit, remaining, request) => {
@@ -2675,16 +2801,21 @@ function createClientRuntime(options, internals) {
2675
2801
  const waited = queue.observe(limit, remaining);
2676
2802
  if (waited > 0) config.logger?.debug(`остаток лимита ${remaining} из ${limit ?? "?"}, бакет ${queue.bucket} ждёт ${waited} мс`);
2677
2803
  };
2678
- transport = new Transport({
2679
- ...config,
2680
- hooks: config.hooks
2681
- }, {
2804
+ transport = new Transport(config, {
2682
2805
  onRateLimit: queues ? observeRateLimit : void 0,
2683
2806
  cookies: config.useCookieJar ? jar : void 0,
2684
- getDeviceId: () => auth.deviceId(),
2685
- lifetimeSignal: lifetime.signal
2807
+ getDeviceId: () => auth.deviceId()
2686
2808
  });
2687
- const stages = [
2809
+ /** Стадии логической операции: выполняются один раз на вызов метода. */
2810
+ const operationStages = [{
2811
+ name: ClientRuntimeStage.OperationPlugins,
2812
+ middleware: createPluginsMiddleware(plugins)
2813
+ }, {
2814
+ name: ClientRuntimeStage.Decode,
2815
+ middleware: createDecodeMiddleware()
2816
+ }];
2817
+ /** Стадии исполнения: от выбора сервиса до транспортной попытки. */
2818
+ const executionStages = [
2688
2819
  {
2689
2820
  name: ClientRuntimeStage.Services,
2690
2821
  middleware: createServicesMiddleware(services)
@@ -2699,7 +2830,7 @@ function createClientRuntime(options, internals) {
2699
2830
  clock: config.clock,
2700
2831
  catalog,
2701
2832
  retry: config.retry,
2702
- rateLimitDelays: config.rateLimit?.retryDelays ?? [],
2833
+ rateLimitDelays: config.rateLimit?.retryDelays,
2703
2834
  pauseQueue: queues ? (ms, request) => queueFor(request)?.pause(ms) : void 0,
2704
2835
  hooks: config.hooks,
2705
2836
  logger: config.logger,
@@ -2712,39 +2843,41 @@ function createClientRuntime(options, internals) {
2712
2843
  },
2713
2844
  {
2714
2845
  name: ClientRuntimeStage.AuthPreparation,
2715
- middleware: createAuthPreparationMiddleware({ prepare: () => auth.prepare() })
2846
+ middleware: createAuthPreparationMiddleware(() => auth)
2716
2847
  }
2717
2848
  ];
2718
- if (queues) stages.push({
2849
+ if (queues) executionStages.push({
2719
2850
  name: ClientRuntimeStage.Queue,
2720
2851
  middleware: createQueueMiddleware((request, task) => {
2721
2852
  const queue = queueFor(request);
2722
2853
  return queue ? queue.schedule(task, request.signal) : task();
2723
2854
  })
2724
2855
  });
2725
- stages.push({
2856
+ executionStages.push({
2726
2857
  name: ClientRuntimeStage.Attempt,
2727
2858
  middleware: createAttemptMiddleware()
2728
- });
2729
- stages.push({
2859
+ }, {
2730
2860
  name: ClientRuntimeStage.AuthHeaders,
2731
2861
  middleware: createAuthHeadersMiddleware({ currentHeaders: () => auth.currentHeaders() })
2732
2862
  });
2733
- const handler = composePipeline(stages.map(({ middleware }) => middleware), transport.send);
2734
- const clientHandler = (request) => {
2863
+ const execution = composePipeline(executionStages.map(({ middleware }) => middleware), transport.send);
2864
+ /**
2865
+ * Проверки между стадиями операции и исполнения: обёртка плагина уже могла изменить запрос,
2866
+ * а после начала `dispose()` завершаются только служебные запросы финализации.
2867
+ */
2868
+ const guardedExecution = (request) => {
2735
2869
  try {
2736
2870
  if (!isDisposeCleanupRequest(request)) internals.assertActive?.("выполнить новый запрос");
2737
2871
  if (request.rateLimitBucket !== void 0) assertKnownBucket(request.rateLimitBucket, "rateLimitBucket", config.rateLimit?.bucket, catalog);
2738
2872
  } catch (error) {
2739
2873
  return Promise.reject(error);
2740
2874
  }
2741
- return handler(request);
2875
+ return execution(request);
2742
2876
  };
2743
2877
  const http = new HttpClient({
2744
- handler: clientHandler,
2745
- plugins,
2878
+ handler: composePipeline(operationStages.map(({ middleware }) => middleware), guardedExecution),
2746
2879
  baseUrl: config.baseUrl,
2747
- timeout: config.timeout,
2880
+ deadline: config.deadline,
2748
2881
  clock: config.clock,
2749
2882
  lifetimeSignal: lifetime.signal,
2750
2883
  hooks: config.hooks,
@@ -2756,8 +2889,8 @@ function createClientRuntime(options, internals) {
2756
2889
  cookies: jar
2757
2890
  });
2758
2891
  const stageOrder = Object.freeze([
2759
- ClientRuntimeStage.OperationPlugins,
2760
- ...stages.map(({ name }) => name),
2892
+ ...operationStages.map(({ name }) => name),
2893
+ ...executionStages.map(({ name }) => name),
2761
2894
  ClientRuntimeStage.Transport
2762
2895
  ]);
2763
2896
  return {
@@ -2790,12 +2923,11 @@ function createClientRuntime(options, internals) {
2790
2923
  },
2791
2924
  platformHeaders: (url) => transport.platformHeaders(url),
2792
2925
  rateLimitState: () => queues?.states() ?? [],
2793
- registerRateLimitBucket: (name, definition) => queues?.defineBucket(name, definition),
2794
2926
  close: () => {
2795
2927
  if (ownsQueues) queues?.stop();
2796
2928
  },
2797
2929
  dispose: async () => {
2798
- lifetime.abort(new require_errors.ItdAbortError("Клиент освобождён через dispose(), запрос отменён"));
2930
+ lifetime.abort(new require_validate.ItdAbortError("Клиент освобождён через dispose(), запрос отменён"));
2799
2931
  if (ownsQueues) queues?.clear();
2800
2932
  auth.dispose();
2801
2933
  await plugins.dispose();
@@ -3416,20 +3548,23 @@ function operationBucket(id) {
3416
3548
  }
3417
3549
  //#endregion
3418
3550
  //#region src/domain/catalog.ts
3551
+ /** Ограничения встроенных бакетов: ёмкость из таблицы плюс встроенные поправки. */
3552
+ const BUCKET_DEFINITIONS = Object.freeze(Object.fromEntries(Object.entries(BUCKET_LIMITS).map(([name, limit]) => [name, Object.freeze({
3553
+ limit,
3554
+ ...DEFAULT_BUCKET_OVERRIDES[name]
3555
+ })])));
3419
3556
  /**
3420
3557
  * Каталог операций итд.com, которым пользуется ядро.
3421
3558
  *
3422
3559
  * Единственное место, где generic request executor встречается с таблицами конкретного API.
3423
- * Собирается из уже существующих функций каталога: собственной логики здесь нет.
3424
3560
  *
3425
3561
  * @internal
3426
3562
  */
3427
3563
  const ITD_CATALOG = Object.freeze({
3428
3564
  definitionOf: (id) => isBuiltInOperationId(id) ? OPERATIONS[id] : void 0,
3429
- retrySafetyOf: (id) => isBuiltInOperationId(id) ? operationRetrySafety(id) : void 0,
3430
- methodOf: (id) => isBuiltInOperationId(id) ? operationMethod(id) : void 0,
3431
3565
  bucketOf: (id) => operationBucket(id),
3432
3566
  isKnownBucket,
3567
+ bucketDefinitionOf: (name) => BUCKET_DEFINITIONS[name],
3433
3568
  bucketLimits: BUCKET_LIMITS,
3434
3569
  bucketOverrides: DEFAULT_BUCKET_OVERRIDES,
3435
3570
  defaultBucket: DEFAULT_RATE_LIMIT_BUCKET
@@ -3796,7 +3931,7 @@ function isKnownNotificationType(type) {
3796
3931
  //#endregion
3797
3932
  //#region src/notifications/normalize.ts
3798
3933
  function asActor(value) {
3799
- if (!isRecord(value)) return void 0;
3934
+ if (!require_validate.isRecord(value)) return void 0;
3800
3935
  const id = asString(value.id);
3801
3936
  if (!id) return void 0;
3802
3937
  return {
@@ -3836,8 +3971,8 @@ function readActors(source) {
3836
3971
  * ```
3837
3972
  */
3838
3973
  function normalizeNotification(input) {
3839
- const source = isRecord(input) ? input : {};
3840
- const payload = isRecord(source.payload) ? source.payload : source;
3974
+ const source = require_validate.isRecord(input) ? input : {};
3975
+ const payload = require_validate.isRecord(source.payload) ? source.payload : source;
3841
3976
  const rawType = asString(payload.type) ?? asString(source.type) ?? "";
3842
3977
  const createdAt = asString(payload.createdAt) ?? asString(source.createdAt) ?? "";
3843
3978
  const readAt = asString(payload.readAt) ?? asString(source.readAt);
@@ -3869,7 +4004,7 @@ function normalizeNotification(input) {
3869
4004
  * счётчик непрочитанных и признак звука.
3870
4005
  */
3871
4006
  function readNotificationEvent(data) {
3872
- const source = isRecord(data) ? data : {};
4007
+ const source = require_validate.isRecord(data) ? data : {};
3873
4008
  return {
3874
4009
  notification: normalizeNotification(data),
3875
4010
  unreadCount: typeof source.unreadCount === "number" ? source.unreadCount : void 0,
@@ -3882,8 +4017,8 @@ function readNotificationEvent(data) {
3882
4017
  * Возвращает `undefined`, если сервер прислал событие без вложенного `payload`.
3883
4018
  */
3884
4019
  function readUnreadCountEvent(data) {
3885
- if (!isRecord(data)) return void 0;
3886
- const payload = isRecord(data.payload) ? data.payload : void 0;
4020
+ if (!require_validate.isRecord(data)) return void 0;
4021
+ const payload = require_validate.isRecord(data.payload) ? data.payload : void 0;
3887
4022
  if (!payload) return void 0;
3888
4023
  return typeof payload.count === "number" ? payload.count : void 0;
3889
4024
  }
@@ -3924,7 +4059,6 @@ function localDeviceId() {
3924
4059
  function anonymousAuth() {
3925
4060
  return {
3926
4061
  token: () => Promise.resolve(null),
3927
- prepare: () => Promise.resolve(),
3928
4062
  currentHeaders: () => ({}),
3929
4063
  recover: () => Promise.resolve(false),
3930
4064
  deviceId: localDeviceId(),
@@ -3946,7 +4080,6 @@ function bearerToken(accessToken) {
3946
4080
  const headers = { Authorization: `Bearer ${accessToken}` };
3947
4081
  return {
3948
4082
  token: () => Promise.resolve(accessToken),
3949
- prepare: () => Promise.resolve(),
3950
4083
  currentHeaders: () => headers,
3951
4084
  recover: () => Promise.resolve(false),
3952
4085
  deviceId: localDeviceId(),
@@ -4236,6 +4369,12 @@ Object.defineProperty(exports, "OPERATIONS", {
4236
4369
  return OPERATIONS;
4237
4370
  }
4238
4371
  });
4372
+ Object.defineProperty(exports, "OperationMethod", {
4373
+ enumerable: true,
4374
+ get: function() {
4375
+ return OperationMethod;
4376
+ }
4377
+ });
4239
4378
  Object.defineProperty(exports, "QrLocationPrecision", {
4240
4379
  enumerable: true,
4241
4380
  get: function() {
@@ -4398,18 +4537,6 @@ Object.defineProperty(exports, "isKnownNotificationType", {
4398
4537
  return isKnownNotificationType;
4399
4538
  }
4400
4539
  });
4401
- Object.defineProperty(exports, "isRecord", {
4402
- enumerable: true,
4403
- get: function() {
4404
- return isRecord;
4405
- }
4406
- });
4407
- Object.defineProperty(exports, "isRecord$1", {
4408
- enumerable: true,
4409
- get: function() {
4410
- return isRecord$1;
4411
- }
4412
- });
4413
4540
  Object.defineProperty(exports, "mergeService", {
4414
4541
  enumerable: true,
4415
4542
  get: function() {
@@ -4500,12 +4627,6 @@ Object.defineProperty(exports, "requestAbortError", {
4500
4627
  return requestAbortError;
4501
4628
  }
4502
4629
  });
4503
- Object.defineProperty(exports, "requireOptionalBoolean", {
4504
- enumerable: true,
4505
- get: function() {
4506
- return requireOptionalBoolean;
4507
- }
4508
- });
4509
4630
  Object.defineProperty(exports, "resolveNotificationUrl", {
4510
4631
  enumerable: true,
4511
4632
  get: function() {
@@ -4543,4 +4664,4 @@ Object.defineProperty(exports, "voidResult", {
4543
4664
  }
4544
4665
  });
4545
4666
 
4546
- //# sourceMappingURL=url-BNTKqBMB.cjs.map
4667
+ //# sourceMappingURL=url-DTgkCWv5.cjs.map