itd-api 0.7.1 → 0.8.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 (84) hide show
  1. package/README.md +14 -14
  2. package/dist/events/index.cjs +53 -0
  3. package/dist/events/index.d.cts +3 -0
  4. package/dist/events/index.d.ts +3 -0
  5. package/dist/events/index.js +5 -0
  6. package/dist/index.cjs +269 -175
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.cts +77 -56
  9. package/dist/index.d.ts +77 -56
  10. package/dist/index.js +251 -160
  11. package/dist/index.js.map +1 -1
  12. package/dist/node/index.cjs +4 -4
  13. package/dist/node/index.d.cts +1 -1
  14. package/dist/node/index.d.ts +1 -1
  15. package/dist/node/index.js +4 -4
  16. package/dist/rest/index.cjs +72 -14
  17. package/dist/rest/index.cjs.map +1 -1
  18. package/dist/rest/index.d.cts +11 -3
  19. package/dist/rest/index.d.ts +11 -3
  20. package/dist/rest/index.js +66 -8
  21. package/dist/rest/index.js.map +1 -1
  22. package/dist/shared/{errors-DfU8M5eS.cjs → errors-BmP3TKoW.cjs} +3 -3
  23. package/dist/shared/{errors-DfU8M5eS.cjs.map → errors-BmP3TKoW.cjs.map} +1 -1
  24. package/dist/shared/{errors-Bhrd2fJd.js → errors-GI10kZxk.js} +3 -3
  25. package/dist/shared/{errors-Bhrd2fJd.js.map → errors-GI10kZxk.js.map} +1 -1
  26. package/dist/shared/events-Br2r7coi.d.cts +698 -0
  27. package/dist/shared/events-QyhQZrnC.d.ts +698 -0
  28. package/dist/shared/{websocket-CbzB1Leq.js → events-TfhKh26b.js} +770 -490
  29. package/dist/shared/events-TfhKh26b.js.map +1 -0
  30. package/dist/shared/{websocket-sYlynrr0.cjs → events-iTteVsd_.cjs} +834 -518
  31. package/dist/shared/events-iTteVsd_.cjs.map +1 -0
  32. package/dist/shared/{multi-storage-CkvTUC5m.js → multi-storage-1AEZTczk.js} +4 -4
  33. package/dist/shared/{multi-storage-CkvTUC5m.js.map → multi-storage-1AEZTczk.js.map} +1 -1
  34. package/dist/shared/{multi-storage--yTEqiod.cjs → multi-storage-CCUgkqvI.cjs} +6 -6
  35. package/dist/shared/{multi-storage--yTEqiod.cjs.map → multi-storage-CCUgkqvI.cjs.map} +1 -1
  36. package/dist/shared/{options-DtATYdLr.js → options-BNP633A5.js} +8 -6
  37. package/dist/shared/options-BNP633A5.js.map +1 -0
  38. package/dist/shared/{options-Dg5N3r1V.cjs → options-BUfoHE9G.cjs} +8 -6
  39. package/dist/shared/options-BUfoHE9G.cjs.map +1 -0
  40. package/dist/shared/{cookies-DZwFq6kr.cjs → redact-DB9EMGJx.cjs} +301 -2
  41. package/dist/shared/redact-DB9EMGJx.cjs.map +1 -0
  42. package/dist/shared/{cookies-tX2sNwxb.js → redact-FNK29Fv-.js} +224 -3
  43. package/dist/shared/redact-FNK29Fv-.js.map +1 -0
  44. package/dist/shared/{render-DMp_3Nzk.d.cts → render-BszcKF7S.d.cts} +432 -254
  45. package/dist/shared/{render-vtLixIiU.js → render-CiL1VS0b.js} +1261 -621
  46. package/dist/shared/render-CiL1VS0b.js.map +1 -0
  47. package/dist/shared/{render-DyeHJNBw.cjs → render-DNTESBSl.cjs} +1335 -671
  48. package/dist/shared/render-DNTESBSl.cjs.map +1 -0
  49. package/dist/shared/{render-DZrxhC5_.d.ts → render-DVB7T8pm.d.ts} +432 -254
  50. package/dist/shared/{storage-BPJR_k4-.cjs → storage-BLmXmzKF.cjs} +2 -2
  51. package/dist/shared/{storage-BPJR_k4-.cjs.map → storage-BLmXmzKF.cjs.map} +1 -1
  52. package/dist/shared/{storage-D86edNCB.js → storage-CCxn0Kkv.js} +2 -2
  53. package/dist/shared/{storage-D86edNCB.js.map → storage-CCxn0Kkv.js.map} +1 -1
  54. package/dist/shared/{url-CYXgxqGx.js → url-B3ocFlHj.js} +491 -425
  55. package/dist/shared/url-B3ocFlHj.js.map +1 -0
  56. package/dist/shared/{url-B6-bXHKt.d.ts → url-Be9jlWRv.d.cts} +1233 -1195
  57. package/dist/shared/{url-B6-bXHKt.d.cts → url-Be9jlWRv.d.ts} +1233 -1195
  58. package/dist/shared/{url-yjl2c8Ie.cjs → url-CFN1sT9S.cjs} +562 -478
  59. package/dist/shared/url-CFN1sT9S.cjs.map +1 -0
  60. package/dist/web/index.cjs +2 -2
  61. package/dist/web/index.js +2 -2
  62. package/package.json +16 -15
  63. package/dist/realtime/index.cjs +0 -165
  64. package/dist/realtime/index.cjs.map +0 -1
  65. package/dist/realtime/index.d.cts +0 -51
  66. package/dist/realtime/index.d.ts +0 -51
  67. package/dist/realtime/index.js +0 -120
  68. package/dist/realtime/index.js.map +0 -1
  69. package/dist/shared/auth-provider-BfogACAb.js +0 -91
  70. package/dist/shared/auth-provider-BfogACAb.js.map +0 -1
  71. package/dist/shared/auth-provider-CTJkKfgy.cjs +0 -108
  72. package/dist/shared/auth-provider-CTJkKfgy.cjs.map +0 -1
  73. package/dist/shared/cookies-DZwFq6kr.cjs.map +0 -1
  74. package/dist/shared/cookies-tX2sNwxb.js.map +0 -1
  75. package/dist/shared/options-Dg5N3r1V.cjs.map +0 -1
  76. package/dist/shared/options-DtATYdLr.js.map +0 -1
  77. package/dist/shared/render-DyeHJNBw.cjs.map +0 -1
  78. package/dist/shared/render-vtLixIiU.js.map +0 -1
  79. package/dist/shared/url-CYXgxqGx.js.map +0 -1
  80. package/dist/shared/url-yjl2c8Ie.cjs.map +0 -1
  81. package/dist/shared/websocket-BMtihD56.d.ts +0 -562
  82. package/dist/shared/websocket-CbzB1Leq.js.map +0 -1
  83. package/dist/shared/websocket-D1p32SB2.d.cts +0 -562
  84. package/dist/shared/websocket-sYlynrr0.cjs.map +0 -1
@@ -1,5 +1,5 @@
1
- const require_errors = require("./errors-DfU8M5eS.cjs");
2
- const require_cookies = require("./cookies-DZwFq6kr.cjs");
1
+ const require_errors = require("./errors-BmP3TKoW.cjs");
2
+ const require_redact = require("./redact-DB9EMGJx.cjs");
3
3
  //#region src/core/async-dispose.ts
4
4
  /**
5
5
  * Подставляет метод `await using` там, где `Symbol.asyncDispose` отсутствует.
@@ -51,120 +51,6 @@ function createDeadline(timeout, clock = systemClock) {
51
51
  };
52
52
  }
53
53
  //#endregion
54
- //#region src/core/runtime.ts
55
- /**
56
- * Как библиотека обращается с cookie.
57
- *
58
- * - `browser` — cookie ведёт браузер, запросы уходят с `credentials: 'include'`;
59
- * - `server` — cookie ведёт встроенный jar, заголовок `Cookie` подставляется вручную;
60
- * - `auto` — определяется по среде исполнения (значение по умолчанию).
61
- */
62
- const RuntimeMode = Object.freeze({
63
- /** Определяется по среде исполнения. Значение по умолчанию. */
64
- Auto: "auto",
65
- /** Cookie ведёт браузер, запросы уходят с `credentials: 'include'`. */
66
- Browser: "browser",
67
- /** Cookie ведёт встроенный jar, заголовок `Cookie` подставляется вручную. */
68
- Server: "server"
69
- });
70
- /** Распознанная среда исполнения. */
71
- const DetectedRuntime = Object.freeze({
72
- Browser: "browser",
73
- /** Есть `window`, но нет `document`; cookie ведёт нативный сетевой слой. */
74
- ReactNative: "react-native",
75
- Server: "server"
76
- });
77
- /**
78
- * Определяет среду исполнения.
79
- *
80
- * React Native выделен отдельно намеренно: там есть `window`, но нет `document`, а cookie
81
- * хранит нативный сетевой слой (`NSHTTPCookieStorage` / `OkHttp`). Свой jar в этой среде
82
- * привёл бы к отправке cookie дважды.
83
- */
84
- function detectRuntime() {
85
- if (globalThis.navigator?.product === "ReactNative") return DetectedRuntime.ReactNative;
86
- if (typeof document !== "undefined") return DetectedRuntime.Browser;
87
- return DetectedRuntime.Server;
88
- }
89
- /**
90
- * Нужно ли вести собственный cookie-jar.
91
- *
92
- * Только в серверных средах: в браузере `Set-Cookie` из JS не читается и не нужен,
93
- * в React Native cookie ведёт нативный слой.
94
- */
95
- function shouldUseCookieJar(mode) {
96
- if (mode === RuntimeMode.Browser) return false;
97
- if (mode === RuntimeMode.Server) return true;
98
- return detectRuntime() === DetectedRuntime.Server;
99
- }
100
- /**
101
- * Нужно ли отправлять запросы с `credentials: 'include'`.
102
- *
103
- * Только в браузере: именно так refresh-cookie попадает в запрос `POST /api/v1/auth/refresh`.
104
- * Требует настроенного CORS на стороне итд.com — если его нет, укажите свой прокси в `baseUrl`.
105
- */
106
- function shouldSendCredentials(mode) {
107
- if (mode === RuntimeMode.Browser) return true;
108
- if (mode === RuntimeMode.Server) return false;
109
- return detectRuntime() === DetectedRuntime.Browser;
110
- }
111
- /**
112
- * Возвращает реализацию `fetch`.
113
- *
114
- * @param custom реализация из конфигурации клиента, если пользователь её передал
115
- * @throws {ItdConfigError} если `fetch` недоступен — например, на Node ниже 18
116
- */
117
- function resolveFetch(custom) {
118
- if (custom !== void 0) {
119
- if (typeof custom !== "function") throw new require_errors.ItdConfigError("fetch должен быть функцией");
120
- return custom;
121
- }
122
- if (typeof globalThis.fetch === "function") return globalThis.fetch.bind(globalThis);
123
- throw new require_errors.ItdConfigError("В этой среде нет глобального fetch. Обновитесь до Node 18+ либо передайте свою реализацию через опцию fetch.");
124
- }
125
- /**
126
- * Проверки бинарных типов, безопасные в любой среде.
127
- *
128
- * Обычный `instanceof` здесь недостаточен: конструктора может не быть вовсе, и тогда
129
- * проверка падает с `ReferenceError`, а не возвращает `false`. `Blob` есть в Node с 18.0,
130
- * но `File` стал глобальным только в Node 20 — при заявленной поддержке Node 18 голый
131
- * `instanceof File` ронял бы любую загрузку.
132
- */
133
- function isBlob(value) {
134
- return typeof Blob !== "undefined" && value instanceof Blob;
135
- }
136
- /** @see {@link isBlob} */
137
- function isFile(value) {
138
- return typeof File !== "undefined" && value instanceof File;
139
- }
140
- /** Доступно ли потоковое чтение тела ответа — от этого зависит, сработает ли SSE. */
141
- function supportsStreamingBody() {
142
- return typeof ReadableStream !== "undefined" && typeof TextDecoder !== "undefined";
143
- }
144
- /**
145
- * Создаёт идентификатор устройства для заголовка `X-Device-Id` — UUID v4.
146
- *
147
- * `crypto.randomUUID` есть в Node 19+, браузерах и Deno, но отсутствует в Node 18 вне
148
- * защищённого контекста, поэтому предусмотрен запасной путь.
149
- */
150
- function createDeviceId() {
151
- const webCrypto = globalThis.crypto;
152
- if (typeof webCrypto?.randomUUID === "function") return webCrypto.randomUUID();
153
- const bytes = /* @__PURE__ */ new Uint8Array(16);
154
- if (typeof webCrypto?.getRandomValues === "function") webCrypto.getRandomValues(bytes);
155
- else for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
156
- bytes[6] = bytes[6] & 15 | 64;
157
- bytes[8] = bytes[8] & 63 | 128;
158
- const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
159
- return [
160
- hex.slice(0, 8),
161
- hex.slice(8, 12),
162
- hex.slice(12, 16),
163
- hex.slice(16, 20),
164
- hex.slice(20, 32)
165
- ].join("-");
166
- }
167
- //#endregion
168
54
  //#region src/core/scheduling/pacing.ts
169
55
  /** Реакция на остаток лимита из заголовков ответа. */
170
56
  const RateLimitPacing = Object.freeze({
@@ -199,7 +85,7 @@ function requireOptionalBoolean(value, name) {
199
85
  //#endregion
200
86
  //#region src/core/version.ts
201
87
  /** Версия библиотеки. Попадает в `User-Agent`. */
202
- const LIBRARY_VERSION = "0.7.1";
88
+ const LIBRARY_VERSION = "0.8.0";
203
89
  //#endregion
204
90
  //#region src/core/config.ts
205
91
  /** Базовый URL API итд.com. Домен записан в punycode: `итд.com`. */
@@ -208,12 +94,6 @@ const DEFAULT_BASE_URL = "https://xn--d1ah4a.com";
208
94
  const DEFAULT_STATUS_BASE_URL = "https://xn--80a7abcbg.xn--d1ah4a.com";
209
95
  /** Имя встроенного сервиса статуса. */
210
96
  const STATUS_SERVICE = "status";
211
- /** Сервисы, зарегистрированные у любого клиента. */
212
- const BUILT_IN_SERVICES = Object.freeze([Object.freeze({
213
- name: STATUS_SERVICE,
214
- baseUrl: DEFAULT_STATUS_BASE_URL,
215
- auth: false
216
- })]);
217
97
  /**
218
98
  * `User-Agent` по умолчанию.
219
99
  *
@@ -324,6 +204,10 @@ function requireConcurrency(value, name) {
324
204
  if (!Number.isInteger(value) || value < 1) throw new require_errors.ItdConfigError(`${name} должен быть целым числом от 1, получено: ${value}`);
325
205
  return value;
326
206
  }
207
+ function requireRps(value, name) {
208
+ if (!Number.isFinite(value) || value <= 0) throw new require_errors.ItdConfigError(`${name} должен быть положительным числом, получено: ${value}`);
209
+ return value;
210
+ }
327
211
  function resolvePacing(pacing) {
328
212
  const known = Object.values(RateLimitPacing);
329
213
  if (pacing !== void 0 && !known.includes(pacing)) throw new require_errors.ItdConfigError(`rateLimit.pacing должен быть одним из ${known.join(", ")}, получено: ${pacing}`);
@@ -346,9 +230,11 @@ function resolveBucketOverrides(overrides, bucket, catalog) {
346
230
  assertKnownBucket(name, "rateLimit.bucketOverrides", bucket, catalog);
347
231
  if (override.concurrency !== void 0) requireConcurrency(override.concurrency, `rateLimit.bucketOverrides.${name}.concurrency`);
348
232
  if (override.limit !== void 0 && (!Number.isFinite(override.limit) || override.limit <= 0)) throw new require_errors.ItdConfigError(`rateLimit.bucketOverrides.${name}.limit должен быть положительным числом, получено: ${override.limit}`);
233
+ if (override.rps !== void 0) requireRps(override.rps, `rateLimit.bucketOverrides.${name}.rps`);
349
234
  const built = builtIn[name];
350
235
  resolved[name] = Object.freeze({
351
236
  concurrency: override.concurrency ?? built?.concurrency,
237
+ rps: override.rps ?? built?.rps,
352
238
  limit: override.limit ?? built?.limit
353
239
  });
354
240
  }
@@ -381,7 +267,7 @@ function resolveRateLimit(rateLimit, catalog) {
381
267
  };
382
268
  if (!rateLimit) return defaults;
383
269
  const concurrency = requireConcurrency(rateLimit.concurrency ?? 6, "rateLimit.concurrency");
384
- if (rateLimit.rps !== void 0 && (!Number.isFinite(rateLimit.rps) || rateLimit.rps <= 0)) throw new require_errors.ItdConfigError(`rateLimit.rps должен быть положительным числом, получено: ${rateLimit.rps}`);
270
+ if (rateLimit.rps !== void 0) requireRps(rateLimit.rps, "rateLimit.rps");
385
271
  const retryDelays = rateLimit.retryDelays ?? defaults.retryDelays;
386
272
  if (!Array.isArray(retryDelays)) throw new require_errors.ItdConfigError("rateLimit.retryDelays должен быть массивом чисел");
387
273
  for (const delay of retryDelays) requirePositive(delay, "rateLimit.retryDelays");
@@ -430,16 +316,16 @@ function resolveServices(services) {
430
316
  */
431
317
  function resolveRuntimeConfig(options, catalog) {
432
318
  if (!isRecord$1(options)) throw new require_errors.ItdConfigError("опции клиента должны быть объектом");
433
- const mode = options.mode ?? RuntimeMode.Auto;
434
- if (!Object.values(RuntimeMode).includes(mode)) throw new require_errors.ItdConfigError(`mode должен быть одним из ${Object.values(RuntimeMode).join(", ")}, получено: ${mode}`);
319
+ 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}`);
435
321
  const timeout = requirePositive(options.timeout ?? 3e4, "timeout");
436
322
  const shutdownTimeout = requirePositive(options.shutdownTimeout ?? 1e4, "shutdownTimeout");
437
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()");
438
324
  if (options.userAgent !== void 0 && options.userAgent !== false && typeof options.userAgent !== "string") throw new require_errors.ItdConfigError("userAgent должен быть строкой или false");
439
325
  return {
440
- baseUrl: require_cookies.normalizeBaseUrl(options.baseUrl ?? "https://xn--d1ah4a.com"),
326
+ baseUrl: require_redact.normalizeBaseUrl(options.baseUrl ?? "https://xn--d1ah4a.com"),
441
327
  services: resolveServices(options.services),
442
- fetch: resolveFetch(options.fetch),
328
+ fetch: require_redact.resolveFetch(options.fetch),
443
329
  clock: options.clock ?? systemClock,
444
330
  timeout,
445
331
  shutdownTimeout,
@@ -449,8 +335,8 @@ function resolveRuntimeConfig(options, catalog) {
449
335
  headers: resolveHeaders(options.headers),
450
336
  userAgent: options.userAgent === false ? void 0 : options.userAgent ?? DEFAULT_USER_AGENT,
451
337
  mode,
452
- useCookieJar: shouldUseCookieJar(mode),
453
- sendCredentials: shouldSendCredentials(mode),
338
+ useCookieJar: require_redact.shouldUseCookieJar(mode),
339
+ sendCredentials: require_redact.shouldSendCredentials(mode),
454
340
  logger: resolveLogger(options.logger)
455
341
  };
456
342
  }
@@ -666,7 +552,10 @@ var PluginRegistry = class {
666
552
  };
667
553
  const installed = plugin.install({
668
554
  ...context,
669
- operations: { use: (transformer) => register(transformers, transformer, "operations") },
555
+ operations: {
556
+ get: this.#options.operationMetadata,
557
+ use: (transformer) => register(transformers, transformer, "operations")
558
+ },
670
559
  attempts: { use: (interceptor) => register(interceptors, interceptor, "attempts") }
671
560
  });
672
561
  if (installed !== void 0 && typeof installed !== "function") throw new require_errors.ItdConfigError(`install() плагина «${plugin.name}» должен вернуть функцию или void`);
@@ -705,8 +594,9 @@ var PluginRegistry = class {
705
594
  this.#removing.add(name);
706
595
  const deadline = createDeadline(this.#options.shutdownTimeout, this.#options.clock);
707
596
  const cleanup = this.#trackCleanup((async () => {
708
- const expired = await this.#release(entry, deadline);
709
- if (expired) throw expired;
597
+ const expirationErrors = await this.#release(entry, deadline);
598
+ if (expirationErrors.length === 1) throw expirationErrors[0];
599
+ if (expirationErrors.length > 1) throw new AggregateError(expirationErrors, `Не удалось отключить плагин «${name}»`);
710
600
  })());
711
601
  try {
712
602
  await cleanup;
@@ -734,8 +624,7 @@ var PluginRegistry = class {
734
624
  const previous = await Promise.allSettled(previousCleanups);
735
625
  for (const result of previous) if (result.status === "rejected") errors.push(result.reason);
736
626
  for (const entry of entries) try {
737
- const expired = await this.#release(entry, deadline);
738
- if (expired) errors.push(expired);
627
+ errors.push(...await this.#release(entry, deadline));
739
628
  } catch (error) {
740
629
  errors.push(error);
741
630
  } finally {
@@ -761,13 +650,29 @@ var PluginRegistry = class {
761
650
  * @param execute выполнение логической операции, вызываемое самым внутренним transformer
762
651
  */
763
652
  async run(request, execute) {
764
- const entries = [...this.#ordered];
653
+ const ordered = this.#ordered;
654
+ if (ordered.length === 0) return execute(request);
655
+ const entries = [...ordered];
765
656
  for (const entry of entries) entry.activeRequests += 1;
766
657
  const interceptorScope = entries.flatMap((entry) => entry.interceptors.map((interceptor) => ({
767
658
  plugin: entry.plugin.name,
768
659
  interceptor
769
660
  })));
770
- const scoped = (current) => withAttemptInterceptorScope(current, interceptorScope);
661
+ const operationId = request.operationId;
662
+ const method = request.method.toUpperCase();
663
+ const retrySafety = request.retrySafety;
664
+ const scoped = (current) => {
665
+ const prepared = current.retrySafety === void 0 ? {
666
+ ...current,
667
+ retrySafety
668
+ } : current;
669
+ return interceptorScope.length === 0 ? prepared : withAttemptInterceptorScope(prepared, interceptorScope);
670
+ };
671
+ const validateContract = (prepared) => {
672
+ if (prepared.operationId !== operationId) throw new require_errors.ItdConfigError(`operation transformer не может заменить operationId «${operationId}» на «${prepared.operationId}»`);
673
+ if (prepared.method.toUpperCase() !== method) throw new require_errors.ItdConfigError(`operation transformer не может заменить метод «${method}» операции «${operationId}» на «${prepared.method}»`);
674
+ if (prepared.retrySafety !== void 0 && prepared.retrySafety !== retrySafety) throw new require_errors.ItdConfigError(`operation transformer не может заменить retrySafety операции «${operationId}»`);
675
+ };
771
676
  const chain = entries.flatMap((entry) => entry.transformers.map((transformer) => ({
772
677
  plugin: entry.plugin.name,
773
678
  transformer
@@ -776,9 +681,13 @@ var PluginRegistry = class {
776
681
  return transformer(scoped(current), (prepared) => {
777
682
  if (called) throw new require_errors.ItdConfigError(`operation transformer плагина «${plugin}» вызвал next() больше одного раза`);
778
683
  called = true;
684
+ validateContract(prepared);
779
685
  return next(scoped(prepared));
780
686
  });
781
- }, (current) => execute(scoped(current)));
687
+ }, (current) => {
688
+ validateContract(current);
689
+ return execute(scoped(current));
690
+ });
782
691
  try {
783
692
  return await chain(scoped(request));
784
693
  } finally {
@@ -795,14 +704,23 @@ var PluginRegistry = class {
795
704
  /**
796
705
  * Дожидается операций плагина и освобождает его ресурсы.
797
706
  *
798
- * `teardown` выполняется в любом случае, в том числе после истечения срока.
707
+ * Функция очистки запускается после ожидания активных операций. Обе стадии ограничены
708
+ * общим сроком.
799
709
  *
800
- * @returns ошибка истёкшего срока, если ждать пришлось дольше отведённого
710
+ * @returns ошибки истёкшего срока для незавершённых стадий
801
711
  */
802
712
  async #release(entry, deadline) {
803
- const finished = await deadline.wait(this.#waitForDrain(entry));
804
- await entry.teardown?.();
805
- return finished ? void 0 : new require_errors.ItdStateError(`плагин «${entry.plugin.name}» не завершил операции за ${this.#options.shutdownTimeout} мс; ожидание прекращено, ресурсы плагина освобождены`);
713
+ const errors = [];
714
+ if (!await deadline.wait(this.#waitForDrain(entry))) errors.push(new require_errors.ItdStateError(`плагин «${entry.plugin.name}» не завершил операции за ${this.#options.shutdownTimeout} мс`));
715
+ let teardownFailed = false;
716
+ let teardownFailure;
717
+ const teardown = Promise.resolve().then(() => entry.teardown?.()).catch((error) => {
718
+ teardownFailed = true;
719
+ teardownFailure = error;
720
+ });
721
+ if (!await deadline.wait(teardown)) errors.push(new require_errors.ItdStateError(`плагин «${entry.plugin.name}» не завершил teardown за ${this.#options.shutdownTimeout} мс`));
722
+ else if (teardownFailed) throw teardownFailure;
723
+ return errors;
806
724
  }
807
725
  #waitForDrain(entry) {
808
726
  if (entry.activeRequests === 0) return Promise.resolve();
@@ -827,148 +745,118 @@ function queueAbortError() {
827
745
  function queueStoppedError() {
828
746
  return new require_errors.ItdAbortError("Клиент закрыт, запрос отменён");
829
747
  }
748
+ /** Длина окна лимита на сервере. */
749
+ const RATE_LIMIT_WINDOW = 6e4;
830
750
  /**
831
- * Очередь запросов: ограничивает одновременность и частоту.
832
- *
833
- * Нужна прежде всего ботам: без неё цикл по сотне постов уходит в API одним залпом
834
- * и упирается в `RATE_LIMIT_EXCEEDED`.
751
+ * Пауза при исчерпании бакета неизвестной ёмкости.
835
752
  *
836
- * Частота выдерживается равномерным разносом стартов (`1000 / rps` между запросами),
837
- * а не окном со счётчиком: так нагрузка ровная, без всплеска в начале каждой секунды.
753
+ * Ёмкость приходит в заголовке вместе с остатком, поэтому случай возможен только у чужого
754
+ * прокси, который прислал `remaining` без `limit`.
755
+ */
756
+ const UNKNOWN_CAPACITY_PAUSE = 1e3;
757
+ /**
758
+ * Единый планировщик одного направления.
838
759
  *
839
- * @internal
760
+ * Он выбирает готовый бакет и только перед фактическим запуском атомарно занимает общий
761
+ * и локальный слоты. Одно ближайшее пробуждение обслуживает все бакеты направления.
840
762
  */
841
- var RequestQueue = class {
763
+ var DestinationScheduler = class {
842
764
  #concurrency;
843
- /** Минимальный промежуток между стартами, мс. `0` — без ограничения частоты. */
844
765
  #minGap;
845
- #onDispatch;
846
- #waiting = [];
766
+ #clock;
767
+ #buckets = [];
847
768
  #active = 0;
848
- /** Момент, раньше которого следующий запрос стартовать не должен. */
849
769
  #nextSlot = 0;
850
- #clock;
770
+ #cursor = 0;
851
771
  #cancelTimer;
852
- constructor(options, clock = systemClock) {
772
+ #wakeupAt;
773
+ constructor(options, clock) {
853
774
  this.#concurrency = options.concurrency;
854
775
  this.#minGap = options.rps ? 1e3 / options.rps : 0;
855
- this.#onDispatch = options.onDispatch;
856
776
  this.#clock = clock;
857
777
  }
858
- /** Сколько задач выполняется прямо сейчас. */
859
- get active() {
860
- return this.#active;
778
+ register(bucket) {
779
+ this.#buckets.push(bucket);
861
780
  }
862
- /** Сколько задач ждёт очереди. */
863
- get pending() {
864
- return this.#waiting.length;
781
+ /** Пересчитывает ближайший запуск после изменения очереди или ограничения. */
782
+ changed() {
783
+ this.#drain();
865
784
  }
866
- /**
867
- * Ставит задачу в очередь.
868
- *
869
- * @returns результат задачи; ошибка задачи пробрасывается без изменений
870
- */
871
- schedule(task, signal) {
872
- if (signal?.aborted) return Promise.reject(queueAbortError());
873
- return new Promise((resolve, reject) => {
874
- let queued;
875
- const detach = () => signal?.removeEventListener("abort", onAbort);
876
- const onAbort = () => {
877
- const index = this.#waiting.indexOf(queued);
878
- if (index < 0) return;
879
- this.#waiting.splice(index, 1);
880
- queued.cancel(queueAbortError());
881
- this.#drain();
882
- };
883
- queued = {
884
- run: () => {
885
- detach();
886
- this.#active += 1;
887
- this.#onDispatch?.();
888
- Promise.resolve().then(task).then(resolve, reject).finally(() => {
889
- this.#active -= 1;
890
- this.#drain();
891
- });
892
- },
893
- cancel: (reason) => {
894
- detach();
895
- reject(reason);
896
- }
897
- };
898
- this.#waiting.push(queued);
899
- signal?.addEventListener("abort", onAbort, { once: true });
900
- if (signal?.aborted) onAbort();
901
- else this.#drain();
902
- });
785
+ /** Перепланирует пробуждение после изменения времени готовности бакета. */
786
+ timingChanged() {
787
+ this.#cancelWakeup();
788
+ this.#drain();
903
789
  }
904
- /**
905
- * Останавливает очередь: снимает отложенную паузу и отклоняет ещё не начатые задачи
906
- * ошибкой `ItdAbortError`. Уже выполняющиеся задачи доводятся до конца.
907
- */
790
+ /** Отклоняет ожидающие задачи, сохраняя паузы и темп для повторного запуска клиента. */
908
791
  stop() {
909
- if (this.#cancelTimer) {
910
- this.#cancelTimer();
911
- this.#cancelTimer = void 0;
912
- }
913
- const pending = this.#waiting.splice(0, this.#waiting.length);
914
- for (const task of pending) task.cancel(queueStoppedError());
792
+ this.#cancelWakeup();
793
+ for (const bucket of this.#buckets) bucket.cancelWaiting(queueStoppedError(), false);
915
794
  }
916
- /** Придерживает очередь на заданное время: ждут все её задачи, а не только одна. */
917
- pause(ms) {
918
- if (ms <= 0) return;
919
- this.#nextSlot = Math.max(this.#nextSlot, this.#clock.now() + ms);
920
- }
921
- /** Запускает столько ожидающих задач, сколько позволяют ограничения. */
922
795
  #drain() {
923
- if (this.#waiting.length === 0) {
924
- if (this.#cancelTimer) {
925
- this.#cancelTimer();
926
- this.#cancelTimer = void 0;
927
- }
928
- return;
929
- }
930
796
  if (this.#active >= this.#concurrency) return;
931
- if (this.#cancelTimer) return;
932
797
  const now = this.#clock.now();
933
- if (this.#nextSlot > now) {
934
- this.#cancelTimer = this.#clock.schedule(() => {
935
- this.#cancelTimer = void 0;
936
- this.#drain();
937
- }, this.#nextSlot - now);
798
+ let selected;
799
+ let selectedIndex = -1;
800
+ let earliest = Number.POSITIVE_INFINITY;
801
+ for (let offset = 0; offset < this.#buckets.length; offset += 1) {
802
+ const index = (this.#cursor + offset) % this.#buckets.length;
803
+ const bucket = this.#buckets[index];
804
+ if (!bucket?.canStart) continue;
805
+ const readyAt = Math.max(this.#nextSlot, bucket.readyAt);
806
+ if (readyAt < earliest) earliest = readyAt;
807
+ if (readyAt <= now) {
808
+ selected = bucket;
809
+ selectedIndex = index;
810
+ break;
811
+ }
812
+ }
813
+ if (!selected) {
814
+ if (Number.isFinite(earliest)) this.#armWakeup(Math.max(0, earliest - now));
815
+ else this.#cancelWakeup();
938
816
  return;
939
817
  }
940
- const next = this.#waiting.shift();
941
- if (!next) return;
818
+ this.#cursor = (selectedIndex + 1) % this.#buckets.length;
819
+ this.#active += 1;
942
820
  if (this.#minGap > 0) this.#nextSlot = now + this.#minGap;
943
- next.run();
821
+ selected.dispatch(now, () => {
822
+ this.#active -= 1;
823
+ this.changed();
824
+ });
944
825
  this.#drain();
945
826
  }
827
+ #armWakeup(delay) {
828
+ const wakeupAt = this.#clock.now() + delay;
829
+ if (this.#cancelTimer && (this.#wakeupAt ?? Number.POSITIVE_INFINITY) <= wakeupAt) return;
830
+ this.#cancelWakeup();
831
+ this.#wakeupAt = wakeupAt;
832
+ this.#cancelTimer = this.#clock.schedule(() => {
833
+ this.#cancelTimer = void 0;
834
+ this.#wakeupAt = void 0;
835
+ this.#drain();
836
+ }, delay);
837
+ }
838
+ #cancelWakeup() {
839
+ this.#cancelTimer?.();
840
+ this.#cancelTimer = void 0;
841
+ this.#wakeupAt = void 0;
842
+ }
946
843
  };
947
- /** Длина окна лимита на сервере. */
948
- const RATE_LIMIT_WINDOW = 6e4;
949
844
  /**
950
- * Пауза при исчерпании бакета неизвестной ёмкости.
845
+ * Состояние и очередь одного серверного бакета.
951
846
  *
952
- * Ёмкость приходит в заголовке вместе с остатком, поэтому случай возможен только у чужого
953
- * прокси, который прислал `remaining` без `limit`.
954
- */
955
- const UNKNOWN_CAPACITY_PAUSE = 1e3;
956
- /**
957
- * Очередь одного бакета поверх общей очереди направления.
958
- *
959
- * Задача занимает слот бакета, затем общий слот направления, поэтому суммарная
960
- * одновременность остаётся равной `concurrency`. Пауза бакета удерживает задачу до
961
- * захвата общего слота: притормозивший счётчик не занимает общую ёмкость.
847
+ * Класс хранит локальную конкурентность, темп и серверную квоту, а решение о старте
848
+ * делегирует общему планировщику направления.
962
849
  *
963
850
  * @internal
964
851
  */
965
852
  var BucketQueue = class {
966
853
  #destination;
967
854
  #bucket;
968
- #gate;
969
- #shared;
855
+ #scheduler;
970
856
  #clock;
971
857
  #pacing;
858
+ #concurrency;
859
+ #minGap;
972
860
  /** Ровный темп. Требует раздельных бакетов: без них ёмкость счётчика неизвестна. */
973
861
  #smooth;
974
862
  /**
@@ -985,6 +873,9 @@ var BucketQueue = class {
985
873
  /** Последнее, что сказал сервер. Живёт и в режиме `off` — ради `rateLimitState()`. */
986
874
  #limit;
987
875
  #remaining;
876
+ #active = 0;
877
+ #nextSlot = 0;
878
+ #waiting = [];
988
879
  /**
989
880
  * Оценка остатка для режима `smooth`.
990
881
  *
@@ -993,24 +884,19 @@ var BucketQueue = class {
993
884
  */
994
885
  #tokens = 1;
995
886
  #tokensAt;
996
- /**
997
- * Номер поколения очереди. `stop()` увеличивает его, отсекая задачи, которые уже взяли
998
- * слот бакета, но до общей очереди ещё не дошли.
999
- */
1000
- #generation = 0;
1001
- constructor(destination, bucket, shared, options, clock) {
887
+ constructor(destination, bucket, scheduler, options, clock, featureDefinition) {
1002
888
  this.#destination = destination;
1003
889
  this.#bucket = bucket;
1004
- this.#shared = shared;
890
+ this.#scheduler = scheduler;
1005
891
  this.#clock = clock;
1006
892
  this.#pacing = options.pacing;
1007
893
  this.#smooth = options.buckets && options.pacing === RateLimitPacing.Smooth;
1008
894
  this.#flatPause = options.buckets ? void 0 : options.retryDelays[0] ?? 0;
1009
- this.#seedLimit = options.buckets ? seedLimit(bucket, options) : void 0;
1010
- this.#gate = new RequestQueue({
1011
- concurrency: options.buckets ? options.bucketOverrides[bucket]?.concurrency ?? options.bucketConcurrency : options.concurrency,
1012
- onDispatch: this.#smooth ? () => this.#spend() : void 0
1013
- }, clock);
895
+ this.#seedLimit = options.buckets ? featureDefinition?.limit ?? seedLimit(bucket, options) : void 0;
896
+ this.#concurrency = options.buckets ? featureDefinition?.concurrency ?? options.bucketOverrides[bucket]?.concurrency ?? options.bucketConcurrency : options.concurrency;
897
+ const rps = options.buckets ? featureDefinition?.rps ?? options.bucketOverrides[bucket]?.rps : void 0;
898
+ this.#minGap = rps ? 1e3 / rps : 0;
899
+ scheduler.register(this);
1014
900
  }
1015
901
  /** Имя счётчика. При `buckets: false` — всегда `default`, каким бы ни был запрос. */
1016
902
  get bucket() {
@@ -1018,19 +904,63 @@ var BucketQueue = class {
1018
904
  }
1019
905
  /** Запросов бакета прошло в общую очередь и ещё не завершилось. */
1020
906
  get active() {
1021
- return this.#gate.active;
907
+ return this.#active;
1022
908
  }
1023
909
  /** Запросов бакета ждёт своей очереди. */
1024
910
  get pending() {
1025
- return this.#gate.pending;
911
+ return this.#waiting.length;
912
+ }
913
+ /** Есть ли ожидающая задача, для которой свободен локальный слот. */
914
+ get canStart() {
915
+ return this.#waiting.length > 0 && this.#active < this.#concurrency;
1026
916
  }
1027
- /** Ставит запрос в очередь: сначала слот бакета, затем общий слот направления. */
917
+ /** Момент, раньше которого локальное ограничение не разрешает следующий старт. */
918
+ get readyAt() {
919
+ return this.#nextSlot;
920
+ }
921
+ /** Ставит запрос в очередь единого планировщика направления. */
1028
922
  schedule(task, signal) {
1029
- const generation = this.#generation;
1030
- return this.#gate.schedule(() => {
1031
- if (generation !== this.#generation) return Promise.reject(queueStoppedError());
1032
- return this.#shared.schedule(task, signal);
1033
- }, signal);
923
+ if (signal?.aborted) return Promise.reject(queueAbortError());
924
+ return new Promise((resolve, reject) => {
925
+ let queued;
926
+ const detach = () => signal?.removeEventListener("abort", onAbort);
927
+ const onAbort = () => {
928
+ const index = this.#waiting.indexOf(queued);
929
+ if (index < 0) return;
930
+ this.#waiting.splice(index, 1);
931
+ queued.cancel(queueAbortError());
932
+ this.#scheduler.timingChanged();
933
+ };
934
+ queued = {
935
+ start: (complete) => {
936
+ detach();
937
+ Promise.resolve().then(task).then(resolve, reject).finally(complete);
938
+ },
939
+ cancel: (reason) => {
940
+ detach();
941
+ reject(reason);
942
+ }
943
+ };
944
+ this.#waiting.push(queued);
945
+ signal?.addEventListener("abort", onAbort, { once: true });
946
+ if (signal?.aborted) onAbort();
947
+ else this.#scheduler.changed();
948
+ });
949
+ }
950
+ /** Запускает первую задачу; вызывается только планировщиком направления. */
951
+ dispatch(now, complete) {
952
+ const task = this.#waiting.shift();
953
+ if (!task) {
954
+ complete();
955
+ return;
956
+ }
957
+ this.#active += 1;
958
+ if (this.#smooth) this.#spend();
959
+ if (this.#minGap > 0) this.#nextSlot = Math.max(this.#nextSlot, now + this.#minGap);
960
+ task.start(() => {
961
+ this.#active -= 1;
962
+ complete();
963
+ });
1034
964
  }
1035
965
  /**
1036
966
  * Учитывает заголовки ответа.
@@ -1046,7 +976,7 @@ var BucketQueue = class {
1046
976
  if (this.#pacing === RateLimitPacing.Off || remaining === void 0) return 0;
1047
977
  if (this.#flatPause !== void 0) {
1048
978
  if (remaining > 0) return 0;
1049
- this.#gate.pause(this.#flatPause);
979
+ this.#hold(this.#flatPause);
1050
980
  return this.#flatPause;
1051
981
  }
1052
982
  const capacity = this.#capacity();
@@ -1054,11 +984,13 @@ var BucketQueue = class {
1054
984
  if (capacity === void 0) return 0;
1055
985
  this.#refill(capacity);
1056
986
  if (remaining < this.#tokens) this.#tokens = remaining;
1057
- return this.#armPause(capacity);
987
+ const wait = this.#armPause(capacity);
988
+ this.#scheduler.timingChanged();
989
+ return wait;
1058
990
  }
1059
991
  if (remaining > 0) return 0;
1060
992
  const wait = capacity === void 0 ? UNKNOWN_CAPACITY_PAUSE : Math.ceil(RATE_LIMIT_WINDOW / Math.max(capacity, 1));
1061
- this.#gate.pause(wait);
993
+ this.#hold(wait);
1062
994
  return wait;
1063
995
  }
1064
996
  /** Придерживает бакет на названное время — путь ответа `429`. Оценка остатка обнуляется. */
@@ -1067,7 +999,7 @@ var BucketQueue = class {
1067
999
  this.#tokens = 0;
1068
1000
  this.#tokensAt = this.#clock.now();
1069
1001
  }
1070
- this.#gate.pause(ms);
1002
+ this.#hold(ms);
1071
1003
  }
1072
1004
  /** Снимок для `rateLimitState()`. */
1073
1005
  state() {
@@ -1076,17 +1008,19 @@ var BucketQueue = class {
1076
1008
  bucket: this.#bucket,
1077
1009
  limit: this.#limit,
1078
1010
  remaining: this.#remaining,
1079
- active: this.#gate.active,
1080
- pending: this.#gate.pending
1011
+ active: this.#active,
1012
+ pending: this.#waiting.length
1081
1013
  };
1082
1014
  }
1083
- /**
1084
- * Останавливает уровень бакета. Общая очередь направления гасится пулом.
1085
- *
1086
- */
1015
+ /** Отклоняет ещё не начатые задачи этого бакета. */
1087
1016
  stop() {
1088
- this.#generation += 1;
1089
- this.#gate.stop();
1017
+ this.cancelWaiting(queueStoppedError());
1018
+ }
1019
+ /** Отклоняет ожидающие задачи; используется также при остановке всего направления. */
1020
+ cancelWaiting(reason, notify = true) {
1021
+ const pending = this.#waiting.splice(0, this.#waiting.length);
1022
+ for (const task of pending) task.cancel(reason);
1023
+ if (notify) this.#scheduler.timingChanged();
1090
1024
  }
1091
1025
  /** Лимит бакета: сказанный сервером, иначе табличный. */
1092
1026
  #capacity() {
@@ -1113,9 +1047,15 @@ var BucketQueue = class {
1113
1047
  #armPause(capacity) {
1114
1048
  if (this.#tokens >= 1) return 0;
1115
1049
  const wait = Math.ceil((1 - this.#tokens) * RATE_LIMIT_WINDOW / capacity);
1116
- this.#gate.pause(wait);
1050
+ this.#nextSlot = Math.max(this.#nextSlot, this.#clock.now() + wait);
1117
1051
  return wait;
1118
1052
  }
1053
+ /** Отодвигает ближайший допустимый старт и перепланирует общее пробуждение. */
1054
+ #hold(ms) {
1055
+ if (ms <= 0) return;
1056
+ this.#nextSlot = Math.max(this.#nextSlot, this.#clock.now() + ms);
1057
+ this.#scheduler.timingChanged();
1058
+ }
1119
1059
  };
1120
1060
  /** Лимит бакета до первого ответа: поправка пользователя важнее табличного значения. */
1121
1061
  function seedLimit(bucket, options) {
@@ -1137,10 +1077,43 @@ var RequestQueuePool = class {
1137
1077
  #clock;
1138
1078
  /** Ключ `undefined` — основная очередь внутренних клиентов без известного направления. */
1139
1079
  #destinations = /* @__PURE__ */ new Map();
1080
+ /** Динамические определения feature вместе с числом использующих их клиентов. */
1081
+ #featureBuckets = /* @__PURE__ */ new Map();
1140
1082
  constructor(options, clock = systemClock) {
1141
1083
  this.#options = options;
1142
1084
  this.#clock = clock;
1143
1085
  }
1086
+ /**
1087
+ * Регистрирует бакет подключаемого feature.
1088
+ *
1089
+ * Повтор той же декларации разрешён клиентам, разделяющим один pool через `ItdAccounts`.
1090
+ * Возвращённая функция откатывает регистрацию, пока очередь бакета ещё не создана.
1091
+ */
1092
+ defineBucket(name, definition) {
1093
+ const normalized = Object.freeze({
1094
+ ...definition.limit === void 0 ? {} : { limit: definition.limit },
1095
+ ...definition.concurrency === void 0 ? {} : { concurrency: definition.concurrency },
1096
+ ...definition.rps === void 0 ? {} : { rps: definition.rps }
1097
+ });
1098
+ const existing = this.#featureBuckets.get(name);
1099
+ if (existing) {
1100
+ if (existing.definition.limit !== normalized.limit || existing.definition.concurrency !== normalized.concurrency || existing.definition.rps !== normalized.rps) throw new require_errors.ItdConfigError(`Бакет feature «${name}» уже зарегистрирован с другими ограничениями`);
1101
+ existing.references += 1;
1102
+ } else this.#featureBuckets.set(name, {
1103
+ definition: normalized,
1104
+ references: 1
1105
+ });
1106
+ let released = false;
1107
+ return () => {
1108
+ if (released) return;
1109
+ released = true;
1110
+ const current = this.#featureBuckets.get(name);
1111
+ if (!current) return;
1112
+ current.references -= 1;
1113
+ if (current.references > 0) return;
1114
+ if (![...this.#destinations.values()].some((entry) => entry.buckets.has(name))) this.#featureBuckets.delete(name);
1115
+ };
1116
+ }
1144
1117
  /** Очередь бакета на направлении. При `buckets: false` бакет всегда `default`. */
1145
1118
  for(destination, bucket) {
1146
1119
  const fallback = this.#options.defaultBucket;
@@ -1148,14 +1121,14 @@ var RequestQueuePool = class {
1148
1121
  let entry = this.#destinations.get(destination);
1149
1122
  if (!entry) {
1150
1123
  entry = {
1151
- shared: new RequestQueue(this.#options, this.#clock),
1124
+ scheduler: new DestinationScheduler(this.#options, this.#clock),
1152
1125
  buckets: /* @__PURE__ */ new Map()
1153
1126
  };
1154
1127
  this.#destinations.set(destination, entry);
1155
1128
  }
1156
1129
  let queue = entry.buckets.get(name);
1157
1130
  if (!queue) {
1158
- queue = new BucketQueue(destination, name, entry.shared, this.#options, this.#clock);
1131
+ queue = new BucketQueue(destination, name, entry.scheduler, this.#options, this.#clock, this.#featureBuckets.get(name)?.definition);
1159
1132
  entry.buckets.set(name, queue);
1160
1133
  }
1161
1134
  return queue;
@@ -1167,19 +1140,17 @@ var RequestQueuePool = class {
1167
1140
  return states;
1168
1141
  }
1169
1142
  /**
1170
- * Останавливает оба уровня всех очередей: ожидающие задачи отклоняются, а состояние
1171
- * счётчиков и отложенные паузы сохраняются до следующего запуска — путь `close()`.
1143
+ * Останавливает планировщики: ожидающие задачи отклоняются, а состояние счётчиков
1144
+ * и отложенные паузы сохраняются до следующего запуска — путь `close()`.
1172
1145
  */
1173
1146
  stop() {
1174
- for (const entry of this.#destinations.values()) {
1175
- for (const queue of entry.buckets.values()) queue.stop();
1176
- entry.shared.stop();
1177
- }
1147
+ for (const entry of this.#destinations.values()) entry.scheduler.stop();
1178
1148
  }
1179
1149
  /** Останавливает очереди и забывает всё, что известно о счётчиках, — путь `dispose()`. */
1180
1150
  clear() {
1181
1151
  this.stop();
1182
1152
  this.#destinations.clear();
1153
+ this.#featureBuckets.clear();
1183
1154
  }
1184
1155
  };
1185
1156
  //#endregion
@@ -1213,7 +1184,7 @@ var ServiceRegistry = class {
1213
1184
  #primaryHost;
1214
1185
  /** @param primaryBaseUrl базовый URL клиента */
1215
1186
  constructor(primaryBaseUrl) {
1216
- this.#primaryHost = primaryBaseUrl ? require_cookies.hostOf(primaryBaseUrl) : "";
1187
+ this.#primaryHost = primaryBaseUrl ? require_redact.hostOf(primaryBaseUrl) : "";
1217
1188
  }
1218
1189
  /**
1219
1190
  * Регистрирует сервис. Имя очищается от краевых пробелов, базовый URL приводится
@@ -1226,7 +1197,7 @@ var ServiceRegistry = class {
1226
1197
  if (typeof raw !== "string" || raw.trim() === "") throw new require_errors.ItdConfigError("У сервиса должно быть непустое имя");
1227
1198
  const name = raw.trim();
1228
1199
  if (this.#services.has(name)) throw new require_errors.ItdConfigError(`Сервис «${name}» уже зарегистрирован и не может быть заменён`);
1229
- const baseUrl = require_cookies.normalizeBaseUrl(definition.baseUrl);
1200
+ const baseUrl = require_redact.normalizeBaseUrl(definition.baseUrl);
1230
1201
  if (definition.auth !== void 0 && typeof definition.auth !== "boolean") throw new require_errors.ItdConfigError(`services.${name}.auth должен быть boolean`);
1231
1202
  let headers;
1232
1203
  if (definition.headers !== void 0) {
@@ -1238,7 +1209,7 @@ var ServiceRegistry = class {
1238
1209
  ...definition,
1239
1210
  name,
1240
1211
  baseUrl,
1241
- auth: definition.auth ?? require_cookies.isSameSite(this.#primaryHost, require_cookies.hostOf(baseUrl)),
1212
+ auth: definition.auth ?? require_redact.isSameSite(this.#primaryHost, require_redact.hostOf(baseUrl)),
1242
1213
  ...headers ? { headers } : {}
1243
1214
  };
1244
1215
  this.#services.set(name, Object.freeze(normalized));
@@ -1251,6 +1222,28 @@ var ServiceRegistry = class {
1251
1222
  has(name) {
1252
1223
  return this.#services.has(name);
1253
1224
  }
1225
+ /** Удаляет сервис при откате атомарной установки feature. @internal */
1226
+ delete(name) {
1227
+ return this.#services.delete(name);
1228
+ }
1229
+ /**
1230
+ * Атомарно заменяет существующее определение и возвращает прежнее.
1231
+ * Используется feature, накладывающим default на сервис из опций клиента.
1232
+ *
1233
+ * @internal
1234
+ */
1235
+ replace(definition) {
1236
+ const name = definition.name.trim();
1237
+ const previous = this.require(name);
1238
+ this.#services.delete(name);
1239
+ try {
1240
+ this.define(definition);
1241
+ } catch (error) {
1242
+ this.#services.set(name, previous);
1243
+ throw error;
1244
+ }
1245
+ return previous;
1246
+ }
1254
1247
  /**
1255
1248
  * Определение сервиса.
1256
1249
  *
@@ -1281,7 +1274,7 @@ var ServiceRegistry = class {
1281
1274
  * @internal
1282
1275
  */
1283
1276
  isPrimarySite(baseUrl) {
1284
- return require_cookies.isSameSite(this.#primaryHost, require_cookies.hostOf(baseUrl));
1277
+ return require_redact.isSameSite(this.#primaryHost, require_redact.hostOf(baseUrl));
1285
1278
  }
1286
1279
  };
1287
1280
  //#endregion
@@ -1343,14 +1336,14 @@ function markDisposeCleanupRequest(request) {
1343
1336
  [DISPOSE_CLEANUP_REQUEST]: true
1344
1337
  };
1345
1338
  }
1346
- /** Разрешено ли запросу завершать внутреннюю очистку после перехода клиента в terminal state. @internal */
1339
+ /** Разрешено ли запросу завершать внутреннюю очистку после `dispose()`. @internal */
1347
1340
  function isDisposeCleanupRequest(request) {
1348
1341
  return request[DISPOSE_CLEANUP_REQUEST] === true;
1349
1342
  }
1350
1343
  /**
1351
1344
  * Собирает слои в один обработчик.
1352
1345
  *
1353
- * Первый слой оказывается самым внешним. Порядок задаётся внутренним client runtime.
1346
+ * Первый слой становится внешним.
1354
1347
  *
1355
1348
  * @example
1356
1349
  * ```ts
@@ -1378,24 +1371,17 @@ function withLayerHeaders(request, headers) {
1378
1371
  /**
1379
1372
  * Точка входа ресурсов в конвейер запросов.
1380
1373
  *
1381
- * Принимает готовый обработчик цепочку слоёв поверх транспорта, собранную
1382
- * во внутреннем runtime клиента, — и отдаёт ресурсам методы `request`/`operation`.
1374
+ * Принимает готовую цепочку обработки и предоставляет ресурсам методы `request`/`execute`.
1383
1375
  * О слоях и их порядке ресурсы не знают.
1384
1376
  */
1385
1377
  var HttpClient = class {
1386
1378
  #handler;
1379
+ #plugins;
1387
1380
  #baseUrl;
1388
- #catalog;
1389
1381
  constructor(deps) {
1390
1382
  this.#handler = deps.handler;
1383
+ this.#plugins = deps.plugins;
1391
1384
  this.#baseUrl = deps.baseUrl;
1392
- this.#catalog = deps.catalog;
1393
- }
1394
- /** Метод операции из каталога. Отсутствие описания — ошибка сборки клиента, не запроса. */
1395
- #methodOf(operationId) {
1396
- const method = this.#catalog.methodOf(operationId);
1397
- if (method === void 0) throw new require_errors.ItdConfigError(`каталог операций не знает «${operationId}»`);
1398
- return method;
1399
1385
  }
1400
1386
  /** Базовый URL, к которому обращается клиент. */
1401
1387
  get baseUrl() {
@@ -1411,23 +1397,34 @@ var HttpClient = class {
1411
1397
  * @throws {ItdNetworkError} если запрос не дошёл до сервера
1412
1398
  */
1413
1399
  request(options) {
1414
- return this.#handler(identifyRequest(options));
1400
+ const request = identifyRequest(options);
1401
+ return this.#plugins.run(request, (prepared) => this.#handler(prepared));
1415
1402
  }
1416
- /** Выполняет встроенную семантическую операцию, подставляя её HTTP-метод из каталога. */
1417
- operation(operationId, options) {
1418
- return this.request({
1403
+ /** Выполняет контракт операции; `next()` плагина возвращает результат после `read`. */
1404
+ execute(operation, options) {
1405
+ return this.#run(operation, options);
1406
+ }
1407
+ #run(operation, options) {
1408
+ const request = identifyRequest({
1419
1409
  ...options,
1420
- operationId,
1421
- method: this.#methodOf(operationId)
1410
+ operationId: operation.id,
1411
+ method: operation.method,
1412
+ retrySafety: options.retrySafety ?? operation.retrySafety
1422
1413
  });
1414
+ return this.#plugins.run(request, async (prepared) => operation.read(await this.#handler(prepared), prepared));
1423
1415
  }
1424
1416
  /** Выполняет внутреннюю операцию финализации после начала `ItdClient.dispose()`. @internal */
1425
- cleanupOperation(operationId, options) {
1426
- return this.request(markDisposeCleanupRequest({
1417
+ cleanupOperation(operation, options) {
1418
+ const request = identifyRequest({
1427
1419
  ...options,
1428
- operationId,
1429
- method: this.#methodOf(operationId)
1430
- }));
1420
+ operationId: operation.id,
1421
+ method: operation.method,
1422
+ retrySafety: options.retrySafety ?? operation.retrySafety
1423
+ });
1424
+ return this.#plugins.run(request, async (prepared) => {
1425
+ const cleanupRequest = markDisposeCleanupRequest(prepared);
1426
+ return operation.read(await this.#handler(cleanupRequest), prepared);
1427
+ });
1431
1428
  }
1432
1429
  };
1433
1430
  //#endregion
@@ -1452,6 +1449,50 @@ const RetrySafety = Object.freeze({
1452
1449
  /** Повтор может создать ещё один побочный эффект. */
1453
1450
  Unsafe: "unsafe"
1454
1451
  });
1452
+ function snapshotAnnotation(value, operationId, copies) {
1453
+ if (value === null || typeof value !== "object" && typeof value !== "function") return value;
1454
+ if (typeof value === "function") throw new require_errors.ItdConfigError(`annotations операции «${operationId}» не должны содержать функции`);
1455
+ const existing = copies.get(value);
1456
+ if (existing !== void 0) return existing;
1457
+ if (Array.isArray(value)) {
1458
+ const copy = [];
1459
+ copies.set(value, copy);
1460
+ for (const item of value) copy.push(snapshotAnnotation(item, operationId, copies));
1461
+ return Object.freeze(copy);
1462
+ }
1463
+ const prototype = Object.getPrototypeOf(value);
1464
+ if (prototype !== Object.prototype && prototype !== null) throw new require_errors.ItdConfigError(`annotations операции «${operationId}» должны состоять из обычных объектов, массивов и примитивов`);
1465
+ const copy = Object.create(prototype);
1466
+ copies.set(value, copy);
1467
+ for (const key of Reflect.ownKeys(value)) {
1468
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
1469
+ if (!descriptor?.enumerable) continue;
1470
+ if (!("value" in descriptor)) throw new require_errors.ItdConfigError(`annotations операции «${operationId}» не должны содержать getters или setters`);
1471
+ Object.defineProperty(copy, key, {
1472
+ value: snapshotAnnotation(descriptor.value, operationId, copies),
1473
+ enumerable: true,
1474
+ writable: true,
1475
+ configurable: true
1476
+ });
1477
+ }
1478
+ return Object.freeze(copy);
1479
+ }
1480
+ /** Создаёт неизменяемый контракт результата. @internal */
1481
+ function defineOperation(id, metadata, read) {
1482
+ const annotations = metadata.annotations === void 0 ? void 0 : snapshotAnnotation(metadata.annotations, id, /* @__PURE__ */ new Map());
1483
+ return Object.freeze({
1484
+ id,
1485
+ ...metadata,
1486
+ ...annotations === void 0 ? {} : { annotations },
1487
+ read
1488
+ });
1489
+ }
1490
+ /** Возвращает разобранное тело без дополнительного преобразования. @internal */
1491
+ function identityResult(body) {
1492
+ return body;
1493
+ }
1494
+ /** Отбрасывает служебное тело операции без результата. @internal */
1495
+ function voidResult() {}
1455
1496
  //#endregion
1456
1497
  //#region src/core/scheduling/retry.ts
1457
1498
  /** Методы raw-запроса, безопасность которых гарантирована самим HTTP-контрактом. */
@@ -1561,9 +1602,6 @@ function createQueueMiddleware(schedule) {
1561
1602
  * разу, иначе, например, текст поста зашифруется дважды. Здесь же к операции привязывается
1562
1603
  * snapshot attempt interceptors; сами они выполняются транспортом на каждой попытке.
1563
1604
  */
1564
- function createPluginsMiddleware(plugins) {
1565
- return (request, next) => plugins.run(request, next);
1566
- }
1567
1605
  /**
1568
1606
  * Слой сервисов.
1569
1607
  *
@@ -1577,7 +1615,7 @@ function createServicesMiddleware(registry) {
1577
1615
  const service = request.service === void 0 ? void 0 : registry.require(request.service);
1578
1616
  let prepared = request;
1579
1617
  if (request.baseUrl !== void 0) {
1580
- const baseUrl = require_cookies.normalizeBaseUrl(request.baseUrl);
1618
+ const baseUrl = require_redact.normalizeBaseUrl(request.baseUrl);
1581
1619
  if (baseUrl !== request.baseUrl) prepared = {
1582
1620
  ...prepared,
1583
1621
  baseUrl
@@ -1721,90 +1759,6 @@ function createRetryMiddleware(deps) {
1721
1759
  };
1722
1760
  }
1723
1761
  //#endregion
1724
- //#region src/core/redact.ts
1725
- /** Заголовки, значение которых нельзя писать в лог целиком. */
1726
- const SECRET_HEADERS = /* @__PURE__ */ new Set([
1727
- "authorization",
1728
- "cookie",
1729
- "set-cookie",
1730
- "x-api-key"
1731
- ]);
1732
- /** Поля тела запроса, значение которых нельзя писать в лог. */
1733
- const SECRET_FIELDS = /* @__PURE__ */ new Set([
1734
- "password",
1735
- "oldpassword",
1736
- "newpassword",
1737
- "accesstoken",
1738
- "refreshtoken",
1739
- "currentpassword",
1740
- "flowtoken",
1741
- "token",
1742
- "turnstiletoken",
1743
- "otp"
1744
- ]);
1745
- /** Query-параметры, содержащие секреты. */
1746
- const SECRET_QUERY_PARAMS = /* @__PURE__ */ new Set(["token", "access_token"]);
1747
- /**
1748
- * Прячет середину секрета, оставляя концы для сопоставления.
1749
- *
1750
- * @example
1751
- * ```ts
1752
- * maskSecret('eyJhbGciOiJIUzI1NiJ9.abc'); // 'eyJh…(24)…abc'
1753
- * ```
1754
- */
1755
- function maskSecret(value) {
1756
- if (value.length <= 8) return "…";
1757
- return `${value.slice(0, 4)}…(${value.length})…${value.slice(-3)}`;
1758
- }
1759
- /** Маскирует секретные query-параметры URL перед записью в логи и ошибки. */
1760
- function redactUrl(url) {
1761
- try {
1762
- const parsed = new URL(url);
1763
- const entries = [...parsed.searchParams.entries()];
1764
- parsed.search = "";
1765
- for (const [name, value] of entries) parsed.searchParams.append(name, SECRET_QUERY_PARAMS.has(name.toLowerCase()) ? maskSecret(value) : value);
1766
- return parsed.toString();
1767
- } catch {
1768
- return url.replace(/([?&](?:token|access_token)=)[^&#]*/gi, "$1[скрыто]");
1769
- }
1770
- }
1771
- /**
1772
- * Готовит заголовки к записи в лог.
1773
- *
1774
- * `Authorization` и `Cookie` маскируются: логи часто уезжают в системы сбора,
1775
- * и токен доступа в них попадать не должен.
1776
- */
1777
- function redactHeaders(headers) {
1778
- const result = {};
1779
- headers.forEach((value, name) => {
1780
- if (SECRET_HEADERS.has(name.toLowerCase())) {
1781
- const spaceAt = value.indexOf(" ");
1782
- result[name] = spaceAt > 0 ? `${value.slice(0, spaceAt)} ${maskSecret(value.slice(spaceAt + 1))}` : maskSecret(value);
1783
- return;
1784
- }
1785
- result[name] = value;
1786
- });
1787
- return result;
1788
- }
1789
- /**
1790
- * Готовит тело запроса к записи в лог: пароли, токены и коды OTP заменяются заглушкой.
1791
- *
1792
- * Обходит вложенные объекты и массивы. `FormData` и бинарные тела не раскрываются вовсе.
1793
- */
1794
- function redactBody(body) {
1795
- if (body === null || body === void 0) return body;
1796
- if (typeof FormData !== "undefined" && body instanceof FormData) return "[FormData]";
1797
- if (isBlob(body)) return "[Blob]";
1798
- if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return "[binary]";
1799
- if (Array.isArray(body)) return body.map(redactBody);
1800
- if (typeof body === "object") {
1801
- const result = {};
1802
- for (const [key, value] of Object.entries(body)) result[key] = SECRET_FIELDS.has(key.toLowerCase()) ? "[скрыто]" : redactBody(value);
1803
- return result;
1804
- }
1805
- return body;
1806
- }
1807
- //#endregion
1808
1762
  //#region src/core/unwrap.ts
1809
1763
  /**
1810
1764
  * Снимает внешнюю обёртку `{ data: … }`.
@@ -2053,7 +2007,7 @@ function safeRawBody(body) {
2053
2007
  if (!isRecord(body) || body.type !== "validation" || !isRecord(body.found)) return body;
2054
2008
  return {
2055
2009
  ...body,
2056
- found: redactBody(body.found)
2010
+ found: require_redact.redactBody(body.found)
2057
2011
  };
2058
2012
  }
2059
2013
  /**
@@ -2100,7 +2054,7 @@ function setHeader(headers, name, value) {
2100
2054
  /** Тело, которое отправляется как есть, без сериализации в JSON. */
2101
2055
  function isRawBody(body) {
2102
2056
  if (typeof body !== "object" || body === null) return typeof body === "string";
2103
- return typeof FormData !== "undefined" && body instanceof FormData || isBlob(body) || typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams || typeof ReadableStream !== "undefined" && body instanceof ReadableStream || body instanceof ArrayBuffer || ArrayBuffer.isView(body);
2057
+ return typeof FormData !== "undefined" && body instanceof FormData || require_redact.isBlob(body) || typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams || typeof ReadableStream !== "undefined" && body instanceof ReadableStream || body instanceof ArrayBuffer || ArrayBuffer.isView(body);
2104
2058
  }
2105
2059
  /**
2106
2060
  * Читает тело ответа один раз.
@@ -2246,8 +2200,8 @@ var Transport = class {
2246
2200
  };
2247
2201
  await dispatchRequestHook(this.#config.hooks, "onRequest", context);
2248
2202
  this.#config.logger?.debug(`→ ${method} ${request.path}`, {
2249
- headers: redactHeaders(headers),
2250
- body: request.bodyFactory ? "[повторяемое тело]" : redactBody(request.body)
2203
+ headers: require_redact.redactHeaders(headers),
2204
+ body: request.bodyFactory ? "[повторяемое тело]" : require_redact.redactBody(request.body)
2251
2205
  });
2252
2206
  let response;
2253
2207
  try {
@@ -2389,7 +2343,7 @@ var Transport = class {
2389
2343
  */
2390
2344
  buildUrl(request) {
2391
2345
  const base = request.baseUrl ?? this.#config.baseUrl;
2392
- return require_cookies.joinUrl(base, request.path) + require_cookies.buildQuery(request.query);
2346
+ return require_redact.joinUrl(base, request.path) + require_redact.buildQuery(request.query);
2393
2347
  }
2394
2348
  /**
2395
2349
  * Собирает общие заголовки клиента: `User-Agent`, идентификатор устройства,
@@ -2440,7 +2394,7 @@ var Transport = class {
2440
2394
  };
2441
2395
  //#endregion
2442
2396
  //#region src/core/execution/client-runtime.ts
2443
- /** Имена стадий основного request pipeline в порядке выполнения. @internal */
2397
+ /** Имена стадий обработки запроса в порядке выполнения. @internal */
2444
2398
  const ClientRuntimeStage = Object.freeze({
2445
2399
  OperationPlugins: "operation_plugins",
2446
2400
  Services: "services",
@@ -2452,26 +2406,21 @@ const ClientRuntimeStage = Object.freeze({
2452
2406
  AuthHeaders: "auth_headers",
2453
2407
  Transport: "transport"
2454
2408
  });
2455
- /** Регистрирует встроенные сервисы и накладывает пользовательские overrides. */
2409
+ /** Регистрирует сервисы, заранее объявленные в опциях клиента. */
2456
2410
  function createServiceRegistry(config) {
2457
2411
  const services = new ServiceRegistry(config.baseUrl);
2458
- const overrides = new Map(config.services.map((service) => [service.name.trim(), service]));
2459
- for (const builtIn of BUILT_IN_SERVICES) {
2460
- const override = overrides.get(builtIn.name);
2461
- overrides.delete(builtIn.name);
2462
- services.define(override ? mergeService(builtIn, override) : builtIn);
2463
- }
2464
- for (const service of overrides.values()) services.define(service);
2412
+ for (const service of config.services) services.define(service);
2465
2413
  return services;
2466
2414
  }
2467
- /** Собирает внутренний runtime клиента и единственный request pipeline. @internal */
2415
+ /** Собирает внутренние компоненты и цепочку запросов клиента. @internal */
2468
2416
  function createClientRuntime(options, internals) {
2469
2417
  const catalog = internals.catalog;
2470
2418
  const config = resolveRuntimeConfig(options, catalog);
2471
- const jar = new require_cookies.CookieJar();
2419
+ const jar = new require_redact.CookieJar();
2472
2420
  const plugins = new PluginRegistry({
2473
2421
  shutdownTimeout: config.shutdownTimeout,
2474
- clock: config.clock
2422
+ clock: config.clock,
2423
+ operationMetadata: (operationId) => catalog.definitionOf(operationId)
2475
2424
  });
2476
2425
  const services = createServiceRegistry(config);
2477
2426
  const lifetime = new AbortController();
@@ -2495,7 +2444,7 @@ function createClientRuntime(options, internals) {
2495
2444
  }) ?? catalog.bucketOf(request.operationId);
2496
2445
  };
2497
2446
  const queueKeyFor = (request) => requestQueueKey(request, (target) => ({
2498
- destination: require_cookies.originOf(transport.buildUrl(target)) || void 0,
2447
+ destination: require_redact.originOf(transport.buildUrl(target)) || void 0,
2499
2448
  bucket: bucketFor(target)
2500
2449
  }));
2501
2450
  const queueFor = (request) => {
@@ -2520,10 +2469,6 @@ function createClientRuntime(options, internals) {
2520
2469
  lifetimeSignal: lifetime.signal
2521
2470
  });
2522
2471
  const stages = [
2523
- {
2524
- name: ClientRuntimeStage.OperationPlugins,
2525
- middleware: createPluginsMiddleware(plugins)
2526
- },
2527
2472
  {
2528
2473
  name: ClientRuntimeStage.Services,
2529
2474
  middleware: createServicesMiddleware(services)
@@ -2575,17 +2520,21 @@ function createClientRuntime(options, internals) {
2575
2520
  }
2576
2521
  return handler(request);
2577
2522
  };
2523
+ const http = new HttpClient({
2524
+ handler: clientHandler,
2525
+ plugins,
2526
+ baseUrl: config.baseUrl
2527
+ });
2578
2528
  auth = internals.auth({
2579
2529
  config,
2580
- handler: clientHandler,
2530
+ http,
2581
2531
  cookies: jar
2582
2532
  });
2583
- const http = new HttpClient({
2584
- handler: clientHandler,
2585
- baseUrl: config.baseUrl,
2586
- catalog
2587
- });
2588
- const stageOrder = Object.freeze([...stages.map(({ name }) => name), ClientRuntimeStage.Transport]);
2533
+ const stageOrder = Object.freeze([
2534
+ ClientRuntimeStage.OperationPlugins,
2535
+ ...stages.map(({ name }) => name),
2536
+ ClientRuntimeStage.Transport
2537
+ ]);
2589
2538
  return {
2590
2539
  config,
2591
2540
  http,
@@ -2594,8 +2543,27 @@ function createClientRuntime(options, internals) {
2594
2543
  plugins,
2595
2544
  services,
2596
2545
  stageOrder,
2546
+ connection: (serviceName) => {
2547
+ const service = serviceName === void 0 ? void 0 : services.require(serviceName);
2548
+ const baseUrl = service?.baseUrl ?? config.baseUrl;
2549
+ return Object.freeze({
2550
+ baseUrl,
2551
+ authorize: service?.auth ?? true,
2552
+ fetch: config.fetch,
2553
+ clock: config.clock,
2554
+ logger: config.logger,
2555
+ baseHeaders: async (url) => {
2556
+ const headers = await transport.platformHeaders(url);
2557
+ for (const [name, value] of Object.entries(service?.headers ?? {})) headers.set(name, value);
2558
+ return headers;
2559
+ },
2560
+ getToken: () => auth.token(),
2561
+ refreshAuth: () => auth.recover()
2562
+ });
2563
+ },
2597
2564
  platformHeaders: (url) => transport.platformHeaders(url),
2598
2565
  rateLimitState: () => queues?.states() ?? [],
2566
+ registerRateLimitBucket: (name, definition) => queues?.defineBucket(name, definition),
2599
2567
  close: () => {
2600
2568
  if (ownsQueues) queues?.stop();
2601
2569
  },
@@ -2995,12 +2963,12 @@ const OPERATIONS = freezeOperations({
2995
2963
  method: "PUT",
2996
2964
  retrySafety: RetrySafety.Idempotent
2997
2965
  },
2998
- "realtime.poll.updates": {
2966
+ "events.notifications.poll.updates": {
2999
2967
  method: "GET",
3000
2968
  retrySafety: RetrySafety.Safe,
3001
2969
  bucket: "notifications"
3002
2970
  },
3003
- "realtime.poll.unread": {
2971
+ "events.notifications.poll.unread": {
3004
2972
  method: "GET",
3005
2973
  retrySafety: RetrySafety.Safe,
3006
2974
  bucket: "notifications"
@@ -3084,10 +3052,6 @@ const OPERATIONS = freezeOperations({
3084
3052
  method: "GET",
3085
3053
  retrySafety: RetrySafety.Safe
3086
3054
  },
3087
- "platform.status": {
3088
- method: "GET",
3089
- retrySafety: RetrySafety.Safe
3090
- },
3091
3055
  "telemetry.dwell": {
3092
3056
  method: "POST",
3093
3057
  retrySafety: RetrySafety.Unsafe
@@ -3097,6 +3061,10 @@ const OPERATIONS = freezeOperations({
3097
3061
  retrySafety: RetrySafety.Unsafe
3098
3062
  }
3099
3063
  });
3064
+ /** Создаёт контракт результата встроенной операции с проверяемым ID. */
3065
+ function defineBuiltInOperation(id, read = identityResult) {
3066
+ return defineOperation(id, OPERATIONS[id], read);
3067
+ }
3100
3068
  const BUILT_IN_IDS = new Set(Object.keys(OPERATIONS));
3101
3069
  /** Проверяет принадлежность ID встроенному каталогу. */
3102
3070
  function isBuiltInOperationId(value) {
@@ -3131,6 +3099,7 @@ function operationBucket(id) {
3131
3099
  * @internal
3132
3100
  */
3133
3101
  const ITD_CATALOG = Object.freeze({
3102
+ definitionOf: (id) => isBuiltInOperationId(id) ? OPERATIONS[id] : void 0,
3134
3103
  retrySafetyOf: (id) => isBuiltInOperationId(id) ? operationRetrySafety(id) : void 0,
3135
3104
  methodOf: (id) => isBuiltInOperationId(id) ? operationMethod(id) : void 0,
3136
3105
  bucketOf: (id) => operationBucket(id),
@@ -3217,8 +3186,8 @@ const ReportReason = Object.freeze({
3217
3186
  Fraud: "fraud",
3218
3187
  Other: "other"
3219
3188
  });
3220
- /** Состояние realtime-соединения. */
3221
- const RealtimeStatus = Object.freeze({
3189
+ /** Состояние соединения событийного канала. */
3190
+ const EventChannelStatus = Object.freeze({
3222
3191
  Connecting: "connecting",
3223
3192
  Connected: "connected",
3224
3193
  Error: "error",
@@ -3537,6 +3506,103 @@ function readUnreadCountEvent(data) {
3537
3506
  return typeof payload.count === "number" ? payload.count : void 0;
3538
3507
  }
3539
3508
  //#endregion
3509
+ //#region src/operations/common.ts
3510
+ /** Контракт встроенной операции без дополнительного преобразования результата. */
3511
+ function passthroughOperation(id) {
3512
+ return defineBuiltInOperation(id);
3513
+ }
3514
+ /** Контракт встроенной операции без возвращаемого значения. */
3515
+ function voidOperation(id) {
3516
+ return defineBuiltInOperation(id, voidResult);
3517
+ }
3518
+ //#endregion
3519
+ //#region src/core/auth-provider.ts
3520
+ /**
3521
+ * Идентификатор устройства, общий для провайдеров без сессии.
3522
+ *
3523
+ * Значение своё у каждого провайдера: сервер различает по нему записи в списке сессий,
3524
+ * поэтому делить его между клиентами нельзя. Хранилища нет, так что перезапуск процесса
3525
+ * значение не переживает — в этом и разница с полноценной сессией.
3526
+ */
3527
+ function localDeviceId() {
3528
+ let deviceId;
3529
+ return () => {
3530
+ deviceId ??= require_redact.createDeviceId();
3531
+ return Promise.resolve(deviceId);
3532
+ };
3533
+ }
3534
+ /**
3535
+ * Авторизации нет: заголовок не подставляется, ответ `401` не восстанавливается.
3536
+ *
3537
+ * @example
3538
+ * ```ts
3539
+ * const api = createRestClient(); // публичные эндпоинты доступны и без токена
3540
+ * ```
3541
+ */
3542
+ function anonymousAuth() {
3543
+ return {
3544
+ token: () => Promise.resolve(null),
3545
+ prepare: () => Promise.resolve(),
3546
+ currentHeaders: () => ({}),
3547
+ recover: () => Promise.resolve(false),
3548
+ deviceId: localDeviceId(),
3549
+ dispose: () => {}
3550
+ };
3551
+ }
3552
+ /**
3553
+ * Готовый Bearer-токен: ни хранилища, ни продления.
3554
+ *
3555
+ * Ответ `401` уходит вызывающему коду как есть — обновить токен провайдеру нечем.
3556
+ * Для сессии, которая продлевает себя сама, нужен полный клиент.
3557
+ *
3558
+ * @example
3559
+ * ```ts
3560
+ * const api = createRestClient({ auth: bearerToken(process.env.ITD_TOKEN) });
3561
+ * ```
3562
+ */
3563
+ function bearerToken(accessToken) {
3564
+ const headers = { Authorization: `Bearer ${accessToken}` };
3565
+ return {
3566
+ token: () => Promise.resolve(accessToken),
3567
+ prepare: () => Promise.resolve(),
3568
+ currentHeaders: () => headers,
3569
+ recover: () => Promise.resolve(false),
3570
+ deviceId: localDeviceId(),
3571
+ dispose: () => {}
3572
+ };
3573
+ }
3574
+ /**
3575
+ * Токен из внешнего источника — хранилища секретов, кэша, соседнего сервиса.
3576
+ *
3577
+ * Источник спрашивается на стадии подготовки, до входа в очередь: там ожидание безопасно,
3578
+ * а слот транспорта ещё не занят. Значение держится до следующей подготовки, потому что
3579
+ * подстановка заголовков обязана быть синхронной.
3580
+ *
3581
+ * @example
3582
+ * ```ts
3583
+ * const api = createRestClient({ auth: tokenProvider(() => vault.read('itd')) });
3584
+ * ```
3585
+ */
3586
+ function tokenProvider(getToken) {
3587
+ let cached = null;
3588
+ const read = async () => {
3589
+ cached = await getToken();
3590
+ return cached;
3591
+ };
3592
+ return {
3593
+ token: read,
3594
+ prepare: async () => {
3595
+ await read();
3596
+ },
3597
+ currentHeaders: () => cached ? { Authorization: `Bearer ${cached}` } : {},
3598
+ recover: () => Promise.resolve(false),
3599
+ deviceId: localDeviceId(),
3600
+ dispose: () => {
3601
+ cached = null;
3602
+ }
3603
+ };
3604
+ }
3605
+ //#endregion
3540
3606
  //#region src/notifications/text.ts
3541
3607
  /** Имя, которое подставляется, если участник неизвестен. */
3542
3608
  const UNKNOWN_ACTOR = "Пользователь";
@@ -3704,6 +3770,18 @@ Object.defineProperty(exports, "DEFAULT_RATE_LIMIT_BUCKET", {
3704
3770
  return DEFAULT_RATE_LIMIT_BUCKET;
3705
3771
  }
3706
3772
  });
3773
+ Object.defineProperty(exports, "DEFAULT_STATUS_BASE_URL", {
3774
+ enumerable: true,
3775
+ get: function() {
3776
+ return DEFAULT_STATUS_BASE_URL;
3777
+ }
3778
+ });
3779
+ Object.defineProperty(exports, "EventChannelStatus", {
3780
+ enumerable: true,
3781
+ get: function() {
3782
+ return EventChannelStatus;
3783
+ }
3784
+ });
3707
3785
  Object.defineProperty(exports, "FeedTab", {
3708
3786
  enumerable: true,
3709
3787
  get: function() {
@@ -3764,12 +3842,6 @@ Object.defineProperty(exports, "RateLimitPacing", {
3764
3842
  return RateLimitPacing;
3765
3843
  }
3766
3844
  });
3767
- Object.defineProperty(exports, "RealtimeStatus", {
3768
- enumerable: true,
3769
- get: function() {
3770
- return RealtimeStatus;
3771
- }
3772
- });
3773
3845
  Object.defineProperty(exports, "ReportReason", {
3774
3846
  enumerable: true,
3775
3847
  get: function() {
@@ -3794,12 +3866,6 @@ Object.defineProperty(exports, "RetrySafety", {
3794
3866
  return RetrySafety;
3795
3867
  }
3796
3868
  });
3797
- Object.defineProperty(exports, "RuntimeMode", {
3798
- enumerable: true,
3799
- get: function() {
3800
- return RuntimeMode;
3801
- }
3802
- });
3803
3869
  Object.defineProperty(exports, "STATUS_SERVICE", {
3804
3870
  enumerable: true,
3805
3871
  get: function() {
@@ -3836,12 +3902,24 @@ Object.defineProperty(exports, "WallAccess", {
3836
3902
  return WallAccess;
3837
3903
  }
3838
3904
  });
3905
+ Object.defineProperty(exports, "anonymousAuth", {
3906
+ enumerable: true,
3907
+ get: function() {
3908
+ return anonymousAuth;
3909
+ }
3910
+ });
3839
3911
  Object.defineProperty(exports, "assertPluginRemovable", {
3840
3912
  enumerable: true,
3841
3913
  get: function() {
3842
3914
  return assertPluginRemovable;
3843
3915
  }
3844
3916
  });
3917
+ Object.defineProperty(exports, "bearerToken", {
3918
+ enumerable: true,
3919
+ get: function() {
3920
+ return bearerToken;
3921
+ }
3922
+ });
3845
3923
  Object.defineProperty(exports, "canonicalNotificationType", {
3846
3924
  enumerable: true,
3847
3925
  get: function() {
@@ -3860,40 +3938,40 @@ Object.defineProperty(exports, "createDeadline", {
3860
3938
  return createDeadline;
3861
3939
  }
3862
3940
  });
3863
- Object.defineProperty(exports, "createDeviceId", {
3941
+ Object.defineProperty(exports, "defineBuiltInOperation", {
3864
3942
  enumerable: true,
3865
3943
  get: function() {
3866
- return createDeviceId;
3944
+ return defineBuiltInOperation;
3867
3945
  }
3868
3946
  });
3869
- Object.defineProperty(exports, "formatNotificationText", {
3947
+ Object.defineProperty(exports, "defineOperation", {
3870
3948
  enumerable: true,
3871
3949
  get: function() {
3872
- return formatNotificationText;
3950
+ return defineOperation;
3873
3951
  }
3874
3952
  });
3875
- Object.defineProperty(exports, "installAsyncDisposeFallback", {
3953
+ Object.defineProperty(exports, "formatNotificationText", {
3876
3954
  enumerable: true,
3877
3955
  get: function() {
3878
- return installAsyncDisposeFallback;
3956
+ return formatNotificationText;
3879
3957
  }
3880
3958
  });
3881
- Object.defineProperty(exports, "isBlob", {
3959
+ Object.defineProperty(exports, "identityResult", {
3882
3960
  enumerable: true,
3883
3961
  get: function() {
3884
- return isBlob;
3962
+ return identityResult;
3885
3963
  }
3886
3964
  });
3887
- Object.defineProperty(exports, "isBuiltInOperationId", {
3965
+ Object.defineProperty(exports, "installAsyncDisposeFallback", {
3888
3966
  enumerable: true,
3889
3967
  get: function() {
3890
- return isBuiltInOperationId;
3968
+ return installAsyncDisposeFallback;
3891
3969
  }
3892
3970
  });
3893
- Object.defineProperty(exports, "isFile", {
3971
+ Object.defineProperty(exports, "isBuiltInOperationId", {
3894
3972
  enumerable: true,
3895
3973
  get: function() {
3896
- return isFile;
3974
+ return isBuiltInOperationId;
3897
3975
  }
3898
3976
  });
3899
3977
  Object.defineProperty(exports, "isKnownNotificationType", {
@@ -3914,10 +3992,10 @@ Object.defineProperty(exports, "isRecord$1", {
3914
3992
  return isRecord$1;
3915
3993
  }
3916
3994
  });
3917
- Object.defineProperty(exports, "maskSecret", {
3995
+ Object.defineProperty(exports, "mergeService", {
3918
3996
  enumerable: true,
3919
3997
  get: function() {
3920
- return maskSecret;
3998
+ return mergeService;
3921
3999
  }
3922
4000
  });
3923
4001
  Object.defineProperty(exports, "normalizeNotification", {
@@ -3950,6 +4028,12 @@ Object.defineProperty(exports, "orderPluginDefinitions", {
3950
4028
  return orderPluginDefinitions;
3951
4029
  }
3952
4030
  });
4031
+ Object.defineProperty(exports, "passthroughOperation", {
4032
+ enumerable: true,
4033
+ get: function() {
4034
+ return passthroughOperation;
4035
+ }
4036
+ });
3953
4037
  Object.defineProperty(exports, "pickArray", {
3954
4038
  enumerable: true,
3955
4039
  get: function() {
@@ -3992,12 +4076,6 @@ Object.defineProperty(exports, "readUnreadCountEvent", {
3992
4076
  return readUnreadCountEvent;
3993
4077
  }
3994
4078
  });
3995
- Object.defineProperty(exports, "redactUrl", {
3996
- enumerable: true,
3997
- get: function() {
3998
- return redactUrl;
3999
- }
4000
- });
4001
4079
  Object.defineProperty(exports, "requireOptionalBoolean", {
4002
4080
  enumerable: true,
4003
4081
  get: function() {
@@ -4016,17 +4094,23 @@ Object.defineProperty(exports, "resolveRateLimit", {
4016
4094
  return resolveRateLimit;
4017
4095
  }
4018
4096
  });
4019
- Object.defineProperty(exports, "supportsStreamingBody", {
4097
+ Object.defineProperty(exports, "systemClock", {
4020
4098
  enumerable: true,
4021
4099
  get: function() {
4022
- return supportsStreamingBody;
4100
+ return systemClock;
4023
4101
  }
4024
4102
  });
4025
- Object.defineProperty(exports, "systemClock", {
4103
+ Object.defineProperty(exports, "tokenProvider", {
4026
4104
  enumerable: true,
4027
4105
  get: function() {
4028
- return systemClock;
4106
+ return tokenProvider;
4107
+ }
4108
+ });
4109
+ Object.defineProperty(exports, "voidOperation", {
4110
+ enumerable: true,
4111
+ get: function() {
4112
+ return voidOperation;
4029
4113
  }
4030
4114
  });
4031
4115
 
4032
- //# sourceMappingURL=url-yjl2c8Ie.cjs.map
4116
+ //# sourceMappingURL=url-CFN1sT9S.cjs.map