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.
- package/README.md +14 -14
- package/dist/events/index.cjs +53 -0
- package/dist/events/index.d.cts +3 -0
- package/dist/events/index.d.ts +3 -0
- package/dist/events/index.js +5 -0
- package/dist/index.cjs +269 -175
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -56
- package/dist/index.d.ts +77 -56
- package/dist/index.js +251 -160
- package/dist/index.js.map +1 -1
- package/dist/node/index.cjs +4 -4
- package/dist/node/index.d.cts +1 -1
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.js +4 -4
- package/dist/rest/index.cjs +72 -14
- package/dist/rest/index.cjs.map +1 -1
- package/dist/rest/index.d.cts +11 -3
- package/dist/rest/index.d.ts +11 -3
- package/dist/rest/index.js +66 -8
- package/dist/rest/index.js.map +1 -1
- package/dist/shared/{errors-DfU8M5eS.cjs → errors-BmP3TKoW.cjs} +3 -3
- package/dist/shared/{errors-DfU8M5eS.cjs.map → errors-BmP3TKoW.cjs.map} +1 -1
- package/dist/shared/{errors-Bhrd2fJd.js → errors-GI10kZxk.js} +3 -3
- package/dist/shared/{errors-Bhrd2fJd.js.map → errors-GI10kZxk.js.map} +1 -1
- package/dist/shared/events-Br2r7coi.d.cts +698 -0
- package/dist/shared/events-QyhQZrnC.d.ts +698 -0
- package/dist/shared/{websocket-CbzB1Leq.js → events-TfhKh26b.js} +770 -490
- package/dist/shared/events-TfhKh26b.js.map +1 -0
- package/dist/shared/{websocket-sYlynrr0.cjs → events-iTteVsd_.cjs} +834 -518
- package/dist/shared/events-iTteVsd_.cjs.map +1 -0
- package/dist/shared/{multi-storage-CkvTUC5m.js → multi-storage-1AEZTczk.js} +4 -4
- package/dist/shared/{multi-storage-CkvTUC5m.js.map → multi-storage-1AEZTczk.js.map} +1 -1
- package/dist/shared/{multi-storage--yTEqiod.cjs → multi-storage-CCUgkqvI.cjs} +6 -6
- package/dist/shared/{multi-storage--yTEqiod.cjs.map → multi-storage-CCUgkqvI.cjs.map} +1 -1
- package/dist/shared/{options-DtATYdLr.js → options-BNP633A5.js} +8 -6
- package/dist/shared/options-BNP633A5.js.map +1 -0
- package/dist/shared/{options-Dg5N3r1V.cjs → options-BUfoHE9G.cjs} +8 -6
- package/dist/shared/options-BUfoHE9G.cjs.map +1 -0
- package/dist/shared/{cookies-DZwFq6kr.cjs → redact-DB9EMGJx.cjs} +301 -2
- package/dist/shared/redact-DB9EMGJx.cjs.map +1 -0
- package/dist/shared/{cookies-tX2sNwxb.js → redact-FNK29Fv-.js} +224 -3
- package/dist/shared/redact-FNK29Fv-.js.map +1 -0
- package/dist/shared/{render-DMp_3Nzk.d.cts → render-BszcKF7S.d.cts} +432 -254
- package/dist/shared/{render-vtLixIiU.js → render-CiL1VS0b.js} +1261 -621
- package/dist/shared/render-CiL1VS0b.js.map +1 -0
- package/dist/shared/{render-DyeHJNBw.cjs → render-DNTESBSl.cjs} +1335 -671
- package/dist/shared/render-DNTESBSl.cjs.map +1 -0
- package/dist/shared/{render-DZrxhC5_.d.ts → render-DVB7T8pm.d.ts} +432 -254
- package/dist/shared/{storage-BPJR_k4-.cjs → storage-BLmXmzKF.cjs} +2 -2
- package/dist/shared/{storage-BPJR_k4-.cjs.map → storage-BLmXmzKF.cjs.map} +1 -1
- package/dist/shared/{storage-D86edNCB.js → storage-CCxn0Kkv.js} +2 -2
- package/dist/shared/{storage-D86edNCB.js.map → storage-CCxn0Kkv.js.map} +1 -1
- package/dist/shared/{url-CYXgxqGx.js → url-B3ocFlHj.js} +491 -425
- package/dist/shared/url-B3ocFlHj.js.map +1 -0
- package/dist/shared/{url-B6-bXHKt.d.ts → url-Be9jlWRv.d.cts} +1233 -1195
- package/dist/shared/{url-B6-bXHKt.d.cts → url-Be9jlWRv.d.ts} +1233 -1195
- package/dist/shared/{url-yjl2c8Ie.cjs → url-CFN1sT9S.cjs} +562 -478
- package/dist/shared/url-CFN1sT9S.cjs.map +1 -0
- package/dist/web/index.cjs +2 -2
- package/dist/web/index.js +2 -2
- package/package.json +16 -15
- package/dist/realtime/index.cjs +0 -165
- package/dist/realtime/index.cjs.map +0 -1
- package/dist/realtime/index.d.cts +0 -51
- package/dist/realtime/index.d.ts +0 -51
- package/dist/realtime/index.js +0 -120
- package/dist/realtime/index.js.map +0 -1
- package/dist/shared/auth-provider-BfogACAb.js +0 -91
- package/dist/shared/auth-provider-BfogACAb.js.map +0 -1
- package/dist/shared/auth-provider-CTJkKfgy.cjs +0 -108
- package/dist/shared/auth-provider-CTJkKfgy.cjs.map +0 -1
- package/dist/shared/cookies-DZwFq6kr.cjs.map +0 -1
- package/dist/shared/cookies-tX2sNwxb.js.map +0 -1
- package/dist/shared/options-Dg5N3r1V.cjs.map +0 -1
- package/dist/shared/options-DtATYdLr.js.map +0 -1
- package/dist/shared/render-DyeHJNBw.cjs.map +0 -1
- package/dist/shared/render-vtLixIiU.js.map +0 -1
- package/dist/shared/url-CYXgxqGx.js.map +0 -1
- package/dist/shared/url-yjl2c8Ie.cjs.map +0 -1
- package/dist/shared/websocket-BMtihD56.d.ts +0 -562
- package/dist/shared/websocket-CbzB1Leq.js.map +0 -1
- package/dist/shared/websocket-D1p32SB2.d.cts +0 -562
- package/dist/shared/websocket-sYlynrr0.cjs.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { O as isItdRateLimitError, _ as ItdStateError, a as ItdConfigError, b as isItdApiError, d as ItdForbiddenError, f as ItdNetworkError, g as ItdServerError, h as ItdRateLimitError, i as ItdAuthError, l as ItdFileError, m as ItdPhoneVerificationError, n as ItdApiError, o as ItdConflictError, p as ItdNotFoundError, s as ItdError, t as ItdAbortError, v as ItdTimeoutError, y as ItdValidationError } from "./errors-
|
|
2
|
-
import {
|
|
1
|
+
import { O as isItdRateLimitError, _ as ItdStateError, a as ItdConfigError, b as isItdApiError, d as ItdForbiddenError, f as ItdNetworkError, g as ItdServerError, h as ItdRateLimitError, i as ItdAuthError, l as ItdFileError, m as ItdPhoneVerificationError, n as ItdApiError, o as ItdConflictError, p as ItdNotFoundError, s as ItdError, t as ItdAbortError, v as ItdTimeoutError, y as ItdValidationError } from "./errors-GI10kZxk.js";
|
|
2
|
+
import { C as shouldUseCookieJar, S as shouldSendCredentials, _ as RuntimeMode, f as hostOf, g as originOf, h as normalizeBaseUrl, i as redactHeaders, m as joinUrl, n as redactBody, p as isSameSite, s as CookieJar, u as buildQuery, v as createDeviceId, x as resolveFetch, y as isBlob } from "./redact-FNK29Fv-.js";
|
|
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 ItdConfigError("fetch должен быть функцией");
|
|
120
|
-
return custom;
|
|
121
|
-
}
|
|
122
|
-
if (typeof globalThis.fetch === "function") return globalThis.fetch.bind(globalThis);
|
|
123
|
-
throw new 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.
|
|
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 ItdConfigError(`${name} должен быть целым числом от 1, получено: ${value}`);
|
|
325
205
|
return value;
|
|
326
206
|
}
|
|
207
|
+
function requireRps(value, name) {
|
|
208
|
+
if (!Number.isFinite(value) || value <= 0) throw new 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 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 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
|
|
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 ItdConfigError("rateLimit.retryDelays должен быть массивом чисел");
|
|
387
273
|
for (const delay of retryDelays) requirePositive(delay, "rateLimit.retryDelays");
|
|
@@ -666,7 +552,10 @@ var PluginRegistry = class {
|
|
|
666
552
|
};
|
|
667
553
|
const installed = plugin.install({
|
|
668
554
|
...context,
|
|
669
|
-
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 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
|
|
709
|
-
if (
|
|
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
|
-
|
|
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
|
|
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
|
|
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 ItdConfigError(`operation transformer не может заменить operationId «${operationId}» на «${prepared.operationId}»`);
|
|
673
|
+
if (prepared.method.toUpperCase() !== method) throw new ItdConfigError(`operation transformer не может заменить метод «${method}» операции «${operationId}» на «${prepared.method}»`);
|
|
674
|
+
if (prepared.retrySafety !== void 0 && prepared.retrySafety !== retrySafety) throw new 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 ItdConfigError(`operation transformer плагина «${plugin}» вызвал next() больше одного раза`);
|
|
778
683
|
called = true;
|
|
684
|
+
validateContract(prepared);
|
|
779
685
|
return next(scoped(prepared));
|
|
780
686
|
});
|
|
781
|
-
}, (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
|
-
*
|
|
707
|
+
* Функция очистки запускается после ожидания активных операций. Обе стадии ограничены
|
|
708
|
+
* общим сроком.
|
|
799
709
|
*
|
|
800
|
-
* @returns
|
|
710
|
+
* @returns ошибки истёкшего срока для незавершённых стадий
|
|
801
711
|
*/
|
|
802
712
|
async #release(entry, deadline) {
|
|
803
|
-
const
|
|
804
|
-
await entry.
|
|
805
|
-
|
|
713
|
+
const errors = [];
|
|
714
|
+
if (!await deadline.wait(this.#waitForDrain(entry))) errors.push(new 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 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 ItdAbortError("Клиент закрыт, запрос отменён");
|
|
829
747
|
}
|
|
748
|
+
/** Длина окна лимита на сервере. */
|
|
749
|
+
const RATE_LIMIT_WINDOW = 6e4;
|
|
830
750
|
/**
|
|
831
|
-
*
|
|
832
|
-
*
|
|
833
|
-
* Нужна прежде всего ботам: без неё цикл по сотне постов уходит в API одним залпом
|
|
834
|
-
* и упирается в `RATE_LIMIT_EXCEEDED`.
|
|
751
|
+
* Пауза при исчерпании бакета неизвестной ёмкости.
|
|
835
752
|
*
|
|
836
|
-
*
|
|
837
|
-
*
|
|
753
|
+
* Ёмкость приходит в заголовке вместе с остатком, поэтому случай возможен только у чужого
|
|
754
|
+
* прокси, который прислал `remaining` без `limit`.
|
|
755
|
+
*/
|
|
756
|
+
const UNKNOWN_CAPACITY_PAUSE = 1e3;
|
|
757
|
+
/**
|
|
758
|
+
* Единый планировщик одного направления.
|
|
838
759
|
*
|
|
839
|
-
*
|
|
760
|
+
* Он выбирает готовый бакет и только перед фактическим запуском атомарно занимает общий
|
|
761
|
+
* и локальный слоты. Одно ближайшее пробуждение обслуживает все бакеты направления.
|
|
840
762
|
*/
|
|
841
|
-
var
|
|
763
|
+
var DestinationScheduler = class {
|
|
842
764
|
#concurrency;
|
|
843
|
-
/** Минимальный промежуток между стартами, мс. `0` — без ограничения частоты. */
|
|
844
765
|
#minGap;
|
|
845
|
-
#
|
|
846
|
-
#
|
|
766
|
+
#clock;
|
|
767
|
+
#buckets = [];
|
|
847
768
|
#active = 0;
|
|
848
|
-
/** Момент, раньше которого следующий запрос стартовать не должен. */
|
|
849
769
|
#nextSlot = 0;
|
|
850
|
-
#
|
|
770
|
+
#cursor = 0;
|
|
851
771
|
#cancelTimer;
|
|
852
|
-
|
|
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
|
-
|
|
860
|
-
return this.#active;
|
|
778
|
+
register(bucket) {
|
|
779
|
+
this.#buckets.push(bucket);
|
|
861
780
|
}
|
|
862
|
-
/**
|
|
863
|
-
|
|
864
|
-
|
|
781
|
+
/** Пересчитывает ближайший запуск после изменения очереди или ограничения. */
|
|
782
|
+
changed() {
|
|
783
|
+
this.#drain();
|
|
865
784
|
}
|
|
866
|
-
/**
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
-
|
|
910
|
-
|
|
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
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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
|
-
|
|
941
|
-
|
|
818
|
+
this.#cursor = (selectedIndex + 1) % this.#buckets.length;
|
|
819
|
+
this.#active += 1;
|
|
942
820
|
if (this.#minGap > 0) this.#nextSlot = now + this.#minGap;
|
|
943
|
-
|
|
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
|
-
*
|
|
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
|
-
#
|
|
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.#
|
|
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.#
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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.#
|
|
907
|
+
return this.#active;
|
|
1022
908
|
}
|
|
1023
909
|
/** Запросов бакета ждёт своей очереди. */
|
|
1024
910
|
get pending() {
|
|
1025
|
-
return this.#
|
|
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
|
-
|
|
1030
|
-
return
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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.#
|
|
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
|
-
|
|
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.#
|
|
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.#
|
|
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.#
|
|
1080
|
-
pending: this.#
|
|
1011
|
+
active: this.#active,
|
|
1012
|
+
pending: this.#waiting.length
|
|
1081
1013
|
};
|
|
1082
1014
|
}
|
|
1083
|
-
/**
|
|
1084
|
-
* Останавливает уровень бакета. Общая очередь направления гасится пулом.
|
|
1085
|
-
*
|
|
1086
|
-
*/
|
|
1015
|
+
/** Отклоняет ещё не начатые задачи этого бакета. */
|
|
1087
1016
|
stop() {
|
|
1088
|
-
this
|
|
1089
|
-
|
|
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.#
|
|
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 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
|
-
|
|
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.
|
|
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
|
-
*
|
|
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
|
|
@@ -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
|
*
|
|
@@ -1343,14 +1336,14 @@ function markDisposeCleanupRequest(request) {
|
|
|
1343
1336
|
[DISPOSE_CLEANUP_REQUEST]: true
|
|
1344
1337
|
};
|
|
1345
1338
|
}
|
|
1346
|
-
/** Разрешено ли запросу завершать внутреннюю очистку после
|
|
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
|
-
* Первый слой
|
|
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 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
|
-
|
|
1400
|
+
const request = identifyRequest(options);
|
|
1401
|
+
return this.#plugins.run(request, (prepared) => this.#handler(prepared));
|
|
1402
|
+
}
|
|
1403
|
+
/** Выполняет контракт операции; `next()` плагина возвращает результат после `read`. */
|
|
1404
|
+
execute(operation, options) {
|
|
1405
|
+
return this.#run(operation, options);
|
|
1415
1406
|
}
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
return this.request({
|
|
1407
|
+
#run(operation, options) {
|
|
1408
|
+
const request = identifyRequest({
|
|
1419
1409
|
...options,
|
|
1420
|
-
operationId,
|
|
1421
|
-
method:
|
|
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(
|
|
1426
|
-
|
|
1417
|
+
cleanupOperation(operation, options) {
|
|
1418
|
+
const request = identifyRequest({
|
|
1427
1419
|
...options,
|
|
1428
|
-
operationId,
|
|
1429
|
-
method:
|
|
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 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 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 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
|
*
|
|
@@ -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: … }`.
|
|
@@ -2440,7 +2394,7 @@ var Transport = class {
|
|
|
2440
2394
|
};
|
|
2441
2395
|
//#endregion
|
|
2442
2396
|
//#region src/core/execution/client-runtime.ts
|
|
2443
|
-
/** Имена стадий
|
|
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
|
-
/** Регистрирует
|
|
2409
|
+
/** Регистрирует сервисы, заранее объявленные в опциях клиента. */
|
|
2456
2410
|
function createServiceRegistry(config) {
|
|
2457
2411
|
const services = new ServiceRegistry(config.baseUrl);
|
|
2458
|
-
const
|
|
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
|
-
/** Собирает
|
|
2415
|
+
/** Собирает внутренние компоненты и цепочку запросов клиента. @internal */
|
|
2468
2416
|
function createClientRuntime(options, internals) {
|
|
2469
2417
|
const catalog = internals.catalog;
|
|
2470
2418
|
const config = resolveRuntimeConfig(options, catalog);
|
|
2471
2419
|
const jar = new 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();
|
|
@@ -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
|
-
|
|
2530
|
+
http,
|
|
2581
2531
|
cookies: jar
|
|
2582
2532
|
});
|
|
2583
|
-
const
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
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
|
-
"
|
|
2966
|
+
"events.notifications.poll.updates": {
|
|
2999
2967
|
method: "GET",
|
|
3000
2968
|
retrySafety: RetrySafety.Safe,
|
|
3001
2969
|
bucket: "notifications"
|
|
3002
2970
|
},
|
|
3003
|
-
"
|
|
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
|
-
/** Состояние
|
|
3221
|
-
const
|
|
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 ??= 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 = "Пользователь";
|
|
@@ -3668,6 +3734,6 @@ function resolveNotificationUrl(notification) {
|
|
|
3668
3734
|
return clickUrl || "/notifications";
|
|
3669
3735
|
}
|
|
3670
3736
|
//#endregion
|
|
3671
|
-
export {
|
|
3737
|
+
export { DEFAULT_BASE_URL as $, ITD_CATALOG as A, isRecord as B, ReportReason as C, ViewReason as D, SpanType as E, operationMethod as F, pickString as G, pickBoolean as H, operationRetrySafety as I, identityResult as J, RetrySafety as K, BUCKET_LIMITS as L, defineBuiltInOperation as M, isBuiltInOperationId as N, ViewSource as O, operationBucket as P, orderPluginDefinitions as Q, DEFAULT_RATE_LIMIT_BUCKET as R, NotificationType as S, ServiceState as T, pickNumber as U, pickArray as V, pickObject as W, RequestQueuePool as X, mergeService as Y, assertPluginRemovable as Z, FeedTab as _, tokenProvider as a, requireOptionalBoolean as at, ItdErrorCode as b, normalizeNotification as c, systemClock as ct, canonicalNotificationType as d, DEFAULT_STATUS_BASE_URL as et, isKnownNotificationType as f, EventChannelStatus as g, CommentSort as h, bearerToken as i, isRecord$1 as it, OPERATIONS as j, WallAccess as k, readNotificationEvent as l, installAsyncDisposeFallback as lt, AttachmentType as m, formatNotificationText as n, resolveRateLimit as nt, passthroughOperation as o, RateLimitPacing as ot, AccessType as p, defineOperation as q, anonymousAuth as r, LIBRARY_VERSION as rt, voidOperation as s, createDeadline as st, resolveNotificationUrl as t, STATUS_SERVICE as tt, readUnreadCountEvent as u, IncidentKind as v, ReportTargetType as w, LikesVisibility as x, InteractionType as y, createClientRuntime as z };
|
|
3672
3738
|
|
|
3673
|
-
//# sourceMappingURL=url-
|
|
3739
|
+
//# sourceMappingURL=url-B3ocFlHj.js.map
|