itd-api 0.7.2 → 0.9.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 +72 -27
- 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 +234 -200
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -64
- package/dist/index.d.ts +70 -64
- package/dist/index.js +202 -179
- package/dist/index.js.map +1 -1
- package/dist/node/index.cjs +6 -5
- package/dist/node/index.cjs.map +1 -1
- package/dist/node/index.d.cts +2 -2
- package/dist/node/index.d.ts +2 -2
- package/dist/node/index.js +5 -4
- package/dist/node/index.js.map +1 -1
- package/dist/rest/index.cjs +32 -26
- package/dist/rest/index.cjs.map +1 -1
- package/dist/rest/index.d.cts +9 -7
- package/dist/rest/index.d.ts +9 -7
- package/dist/rest/index.js +19 -21
- 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/{websocket-BLR8eVJV.js → events-C5wnTXPT.js} +770 -490
- package/dist/shared/events-C5wnTXPT.js.map +1 -0
- package/dist/shared/events-CqtkP65d.d.ts +698 -0
- package/dist/shared/events-DGAWlCaI.d.cts +698 -0
- package/dist/shared/{websocket-C_eI4H2o.cjs → events-FZfnEez0.cjs} +834 -518
- package/dist/shared/events-FZfnEez0.cjs.map +1 -0
- package/dist/shared/{storage-BPJR_k4-.cjs → key-value-store-B5GYVEYZ.cjs} +2 -86
- package/dist/shared/key-value-store-B5GYVEYZ.cjs.map +1 -0
- package/dist/shared/{storage-D86edNCB.js → key-value-store-Bt2PSMxY.js} +3 -69
- package/dist/shared/key-value-store-Bt2PSMxY.js.map +1 -0
- package/dist/shared/key-value-store-COZaNHZS.d.cts +69 -0
- package/dist/shared/key-value-store-COZaNHZS.d.ts +69 -0
- package/dist/shared/{multi-storage-DccjD7Ww.d.ts → multi-storage-BEL7tcKn.d.cts} +3 -2
- package/dist/shared/{multi-storage--yTEqiod.cjs → multi-storage-CDKLa956.cjs} +11 -10
- package/dist/shared/{multi-storage--yTEqiod.cjs.map → multi-storage-CDKLa956.cjs.map} +1 -1
- package/dist/shared/{multi-storage-CjAPB5Kq.d.cts → multi-storage-LkljOSzA.d.ts} +3 -2
- package/dist/shared/{multi-storage-CkvTUC5m.js → multi-storage-SoW05uzO.js} +5 -4
- package/dist/shared/{multi-storage-CkvTUC5m.js.map → multi-storage-SoW05uzO.js.map} +1 -1
- package/dist/shared/{options-Dg5N3r1V.cjs → options-DOJYtoti.cjs} +8 -6
- package/dist/shared/options-DOJYtoti.cjs.map +1 -0
- package/dist/shared/{options-DtATYdLr.js → options-DhgGg7Ms.js} +8 -6
- package/dist/shared/options-DhgGg7Ms.js.map +1 -0
- package/dist/shared/{cookies-DZwFq6kr.cjs → redact-BP00URmQ.cjs} +312 -2
- package/dist/shared/redact-BP00URmQ.cjs.map +1 -0
- package/dist/shared/{cookies-tX2sNwxb.js → redact-Ba5iSVYZ.js} +235 -3
- package/dist/shared/redact-Ba5iSVYZ.js.map +1 -0
- package/dist/shared/{render-CgwKdOzu.d.ts → render-B4qis7Sb.d.ts} +775 -313
- package/dist/shared/{render-DO0F5YSm.d.cts → render-DDwy2Ame.d.cts} +775 -313
- package/dist/shared/{render-mcuELiYi.cjs → render-Dqi8Owqk.cjs} +1464 -662
- package/dist/shared/render-Dqi8Owqk.cjs.map +1 -0
- package/dist/shared/{render-C6HRPs10.js → render-hiWFh2s4.js} +1366 -618
- package/dist/shared/render-hiWFh2s4.js.map +1 -0
- package/dist/shared/storage-BQrtKon9.d.cts +86 -0
- package/dist/shared/storage-CTRGT5wF.d.ts +86 -0
- package/dist/shared/storage-CfHKzVHf.js +71 -0
- package/dist/shared/storage-CfHKzVHf.js.map +1 -0
- package/dist/shared/storage-QNVxzpDB.cjs +88 -0
- package/dist/shared/storage-QNVxzpDB.cjs.map +1 -0
- package/dist/shared/{url-BaMCQpYH.cjs → url-Bsb3xE5U.cjs} +577 -466
- package/dist/shared/url-Bsb3xE5U.cjs.map +1 -0
- package/dist/shared/{url-IU0xN9wX.js → url-DP33mp3s.js} +508 -409
- package/dist/shared/url-DP33mp3s.js.map +1 -0
- package/dist/shared/{url-DTfZ2toq.d.ts → url-XBsMdAcv.d.cts} +1313 -1200
- package/dist/shared/{url-DTfZ2toq.d.cts → url-XBsMdAcv.d.ts} +1313 -1200
- package/dist/web/index.cjs +5 -4
- package/dist/web/index.cjs.map +1 -1
- package/dist/web/index.d.cts +1 -1
- package/dist/web/index.d.ts +1 -1
- package/dist/web/index.js +3 -2
- package/dist/web/index.js.map +1 -1
- 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-CG8oCQ9F.cjs +0 -108
- package/dist/shared/auth-provider-CG8oCQ9F.cjs.map +0 -1
- package/dist/shared/auth-provider-mYqxsSVa.js +0 -91
- package/dist/shared/auth-provider-mYqxsSVa.js.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-C6HRPs10.js.map +0 -1
- package/dist/shared/render-mcuELiYi.cjs.map +0 -1
- package/dist/shared/storage-BPJR_k4-.cjs.map +0 -1
- package/dist/shared/storage-C_eICCep.d.cts +0 -152
- package/dist/shared/storage-C_eICCep.d.ts +0 -152
- package/dist/shared/storage-D86edNCB.js.map +0 -1
- package/dist/shared/url-BaMCQpYH.cjs.map +0 -1
- package/dist/shared/url-IU0xN9wX.js.map +0 -1
- package/dist/shared/websocket-BLR8eVJV.js.map +0 -1
- package/dist/shared/websocket-C_eI4H2o.cjs.map +0 -1
- package/dist/shared/websocket-DF7XIMiX.d.cts +0 -562
- package/dist/shared/websocket-DYKBr8HF.d.ts +0 -562
|
@@ -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-Ba5iSVYZ.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.9.0";
|
|
203
89
|
//#endregion
|
|
204
90
|
//#region src/core/config.ts
|
|
205
91
|
/** Базовый URL API итд.com. Домен записан в punycode: `итд.com`. */
|
|
@@ -318,6 +204,10 @@ function requireConcurrency(value, name) {
|
|
|
318
204
|
if (!Number.isInteger(value) || value < 1) throw new ItdConfigError(`${name} должен быть целым числом от 1, получено: ${value}`);
|
|
319
205
|
return value;
|
|
320
206
|
}
|
|
207
|
+
function requireRps(value, name) {
|
|
208
|
+
if (!Number.isFinite(value) || value <= 0) throw new ItdConfigError(`${name} должен быть положительным числом, получено: ${value}`);
|
|
209
|
+
return value;
|
|
210
|
+
}
|
|
321
211
|
function resolvePacing(pacing) {
|
|
322
212
|
const known = Object.values(RateLimitPacing);
|
|
323
213
|
if (pacing !== void 0 && !known.includes(pacing)) throw new ItdConfigError(`rateLimit.pacing должен быть одним из ${known.join(", ")}, получено: ${pacing}`);
|
|
@@ -340,9 +230,11 @@ function resolveBucketOverrides(overrides, bucket, catalog) {
|
|
|
340
230
|
assertKnownBucket(name, "rateLimit.bucketOverrides", bucket, catalog);
|
|
341
231
|
if (override.concurrency !== void 0) requireConcurrency(override.concurrency, `rateLimit.bucketOverrides.${name}.concurrency`);
|
|
342
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`);
|
|
343
234
|
const built = builtIn[name];
|
|
344
235
|
resolved[name] = Object.freeze({
|
|
345
236
|
concurrency: override.concurrency ?? built?.concurrency,
|
|
237
|
+
rps: override.rps ?? built?.rps,
|
|
346
238
|
limit: override.limit ?? built?.limit
|
|
347
239
|
});
|
|
348
240
|
}
|
|
@@ -375,7 +267,7 @@ function resolveRateLimit(rateLimit, catalog) {
|
|
|
375
267
|
};
|
|
376
268
|
if (!rateLimit) return defaults;
|
|
377
269
|
const concurrency = requireConcurrency(rateLimit.concurrency ?? 6, "rateLimit.concurrency");
|
|
378
|
-
if (rateLimit.rps !== void 0
|
|
270
|
+
if (rateLimit.rps !== void 0) requireRps(rateLimit.rps, "rateLimit.rps");
|
|
379
271
|
const retryDelays = rateLimit.retryDelays ?? defaults.retryDelays;
|
|
380
272
|
if (!Array.isArray(retryDelays)) throw new ItdConfigError("rateLimit.retryDelays должен быть массивом чисел");
|
|
381
273
|
for (const delay of retryDelays) requirePositive(delay, "rateLimit.retryDelays");
|
|
@@ -660,7 +552,10 @@ var PluginRegistry = class {
|
|
|
660
552
|
};
|
|
661
553
|
const installed = plugin.install({
|
|
662
554
|
...context,
|
|
663
|
-
operations: {
|
|
555
|
+
operations: {
|
|
556
|
+
get: this.#options.operationMetadata,
|
|
557
|
+
use: (transformer) => register(transformers, transformer, "operations")
|
|
558
|
+
},
|
|
664
559
|
attempts: { use: (interceptor) => register(interceptors, interceptor, "attempts") }
|
|
665
560
|
});
|
|
666
561
|
if (installed !== void 0 && typeof installed !== "function") throw new ItdConfigError(`install() плагина «${plugin.name}» должен вернуть функцию или void`);
|
|
@@ -699,8 +594,9 @@ var PluginRegistry = class {
|
|
|
699
594
|
this.#removing.add(name);
|
|
700
595
|
const deadline = createDeadline(this.#options.shutdownTimeout, this.#options.clock);
|
|
701
596
|
const cleanup = this.#trackCleanup((async () => {
|
|
702
|
-
const
|
|
703
|
-
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}»`);
|
|
704
600
|
})());
|
|
705
601
|
try {
|
|
706
602
|
await cleanup;
|
|
@@ -728,8 +624,7 @@ var PluginRegistry = class {
|
|
|
728
624
|
const previous = await Promise.allSettled(previousCleanups);
|
|
729
625
|
for (const result of previous) if (result.status === "rejected") errors.push(result.reason);
|
|
730
626
|
for (const entry of entries) try {
|
|
731
|
-
|
|
732
|
-
if (expired) errors.push(expired);
|
|
627
|
+
errors.push(...await this.#release(entry, deadline));
|
|
733
628
|
} catch (error) {
|
|
734
629
|
errors.push(error);
|
|
735
630
|
} finally {
|
|
@@ -755,13 +650,29 @@ var PluginRegistry = class {
|
|
|
755
650
|
* @param execute выполнение логической операции, вызываемое самым внутренним transformer
|
|
756
651
|
*/
|
|
757
652
|
async run(request, execute) {
|
|
758
|
-
const
|
|
653
|
+
const ordered = this.#ordered;
|
|
654
|
+
if (ordered.length === 0) return execute(request);
|
|
655
|
+
const entries = [...ordered];
|
|
759
656
|
for (const entry of entries) entry.activeRequests += 1;
|
|
760
657
|
const interceptorScope = entries.flatMap((entry) => entry.interceptors.map((interceptor) => ({
|
|
761
658
|
plugin: entry.plugin.name,
|
|
762
659
|
interceptor
|
|
763
660
|
})));
|
|
764
|
-
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
|
+
};
|
|
765
676
|
const chain = entries.flatMap((entry) => entry.transformers.map((transformer) => ({
|
|
766
677
|
plugin: entry.plugin.name,
|
|
767
678
|
transformer
|
|
@@ -770,9 +681,13 @@ var PluginRegistry = class {
|
|
|
770
681
|
return transformer(scoped(current), (prepared) => {
|
|
771
682
|
if (called) throw new ItdConfigError(`operation transformer плагина «${plugin}» вызвал next() больше одного раза`);
|
|
772
683
|
called = true;
|
|
684
|
+
validateContract(prepared);
|
|
773
685
|
return next(scoped(prepared));
|
|
774
686
|
});
|
|
775
|
-
}, (current) =>
|
|
687
|
+
}, (current) => {
|
|
688
|
+
validateContract(current);
|
|
689
|
+
return execute(scoped(current));
|
|
690
|
+
});
|
|
776
691
|
try {
|
|
777
692
|
return await chain(scoped(request));
|
|
778
693
|
} finally {
|
|
@@ -789,14 +704,23 @@ var PluginRegistry = class {
|
|
|
789
704
|
/**
|
|
790
705
|
* Дожидается операций плагина и освобождает его ресурсы.
|
|
791
706
|
*
|
|
792
|
-
*
|
|
707
|
+
* Функция очистки запускается после ожидания активных операций. Обе стадии ограничены
|
|
708
|
+
* общим сроком.
|
|
793
709
|
*
|
|
794
|
-
* @returns
|
|
710
|
+
* @returns ошибки истёкшего срока для незавершённых стадий
|
|
795
711
|
*/
|
|
796
712
|
async #release(entry, deadline) {
|
|
797
|
-
const
|
|
798
|
-
await entry.
|
|
799
|
-
|
|
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;
|
|
800
724
|
}
|
|
801
725
|
#waitForDrain(entry) {
|
|
802
726
|
if (entry.activeRequests === 0) return Promise.resolve();
|
|
@@ -821,148 +745,118 @@ function queueAbortError() {
|
|
|
821
745
|
function queueStoppedError() {
|
|
822
746
|
return new ItdAbortError("Клиент закрыт, запрос отменён");
|
|
823
747
|
}
|
|
748
|
+
/** Длина окна лимита на сервере. */
|
|
749
|
+
const RATE_LIMIT_WINDOW = 6e4;
|
|
824
750
|
/**
|
|
825
|
-
*
|
|
826
|
-
*
|
|
827
|
-
* Нужна прежде всего ботам: без неё цикл по сотне постов уходит в API одним залпом
|
|
828
|
-
* и упирается в `RATE_LIMIT_EXCEEDED`.
|
|
751
|
+
* Пауза при исчерпании бакета неизвестной ёмкости.
|
|
829
752
|
*
|
|
830
|
-
*
|
|
831
|
-
*
|
|
753
|
+
* Ёмкость приходит в заголовке вместе с остатком, поэтому случай возможен только у чужого
|
|
754
|
+
* прокси, который прислал `remaining` без `limit`.
|
|
755
|
+
*/
|
|
756
|
+
const UNKNOWN_CAPACITY_PAUSE = 1e3;
|
|
757
|
+
/**
|
|
758
|
+
* Единый планировщик одного направления.
|
|
832
759
|
*
|
|
833
|
-
*
|
|
760
|
+
* Он выбирает готовый бакет и только перед фактическим запуском атомарно занимает общий
|
|
761
|
+
* и локальный слоты. Одно ближайшее пробуждение обслуживает все бакеты направления.
|
|
834
762
|
*/
|
|
835
|
-
var
|
|
763
|
+
var DestinationScheduler = class {
|
|
836
764
|
#concurrency;
|
|
837
|
-
/** Минимальный промежуток между стартами, мс. `0` — без ограничения частоты. */
|
|
838
765
|
#minGap;
|
|
839
|
-
#
|
|
840
|
-
#
|
|
766
|
+
#clock;
|
|
767
|
+
#buckets = [];
|
|
841
768
|
#active = 0;
|
|
842
|
-
/** Момент, раньше которого следующий запрос стартовать не должен. */
|
|
843
769
|
#nextSlot = 0;
|
|
844
|
-
#
|
|
770
|
+
#cursor = 0;
|
|
845
771
|
#cancelTimer;
|
|
846
|
-
|
|
772
|
+
#wakeupAt;
|
|
773
|
+
constructor(options, clock) {
|
|
847
774
|
this.#concurrency = options.concurrency;
|
|
848
775
|
this.#minGap = options.rps ? 1e3 / options.rps : 0;
|
|
849
|
-
this.#onDispatch = options.onDispatch;
|
|
850
776
|
this.#clock = clock;
|
|
851
777
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
return this.#active;
|
|
778
|
+
register(bucket) {
|
|
779
|
+
this.#buckets.push(bucket);
|
|
855
780
|
}
|
|
856
|
-
/**
|
|
857
|
-
|
|
858
|
-
|
|
781
|
+
/** Пересчитывает ближайший запуск после изменения очереди или ограничения. */
|
|
782
|
+
changed() {
|
|
783
|
+
this.#drain();
|
|
859
784
|
}
|
|
860
|
-
/**
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
*/
|
|
865
|
-
schedule(task, signal) {
|
|
866
|
-
if (signal?.aborted) return Promise.reject(queueAbortError());
|
|
867
|
-
return new Promise((resolve, reject) => {
|
|
868
|
-
let queued;
|
|
869
|
-
const detach = () => signal?.removeEventListener("abort", onAbort);
|
|
870
|
-
const onAbort = () => {
|
|
871
|
-
const index = this.#waiting.indexOf(queued);
|
|
872
|
-
if (index < 0) return;
|
|
873
|
-
this.#waiting.splice(index, 1);
|
|
874
|
-
queued.cancel(queueAbortError());
|
|
875
|
-
this.#drain();
|
|
876
|
-
};
|
|
877
|
-
queued = {
|
|
878
|
-
run: () => {
|
|
879
|
-
detach();
|
|
880
|
-
this.#active += 1;
|
|
881
|
-
this.#onDispatch?.();
|
|
882
|
-
Promise.resolve().then(task).then(resolve, reject).finally(() => {
|
|
883
|
-
this.#active -= 1;
|
|
884
|
-
this.#drain();
|
|
885
|
-
});
|
|
886
|
-
},
|
|
887
|
-
cancel: (reason) => {
|
|
888
|
-
detach();
|
|
889
|
-
reject(reason);
|
|
890
|
-
}
|
|
891
|
-
};
|
|
892
|
-
this.#waiting.push(queued);
|
|
893
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
894
|
-
if (signal?.aborted) onAbort();
|
|
895
|
-
else this.#drain();
|
|
896
|
-
});
|
|
785
|
+
/** Перепланирует пробуждение после изменения времени готовности бакета. */
|
|
786
|
+
timingChanged() {
|
|
787
|
+
this.#cancelWakeup();
|
|
788
|
+
this.#drain();
|
|
897
789
|
}
|
|
898
|
-
/**
|
|
899
|
-
* Останавливает очередь: снимает отложенную паузу и отклоняет ещё не начатые задачи
|
|
900
|
-
* ошибкой `ItdAbortError`. Уже выполняющиеся задачи доводятся до конца.
|
|
901
|
-
*/
|
|
790
|
+
/** Отклоняет ожидающие задачи, сохраняя паузы и темп для повторного запуска клиента. */
|
|
902
791
|
stop() {
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
this.#cancelTimer = void 0;
|
|
906
|
-
}
|
|
907
|
-
const pending = this.#waiting.splice(0, this.#waiting.length);
|
|
908
|
-
for (const task of pending) task.cancel(queueStoppedError());
|
|
792
|
+
this.#cancelWakeup();
|
|
793
|
+
for (const bucket of this.#buckets) bucket.cancelWaiting(queueStoppedError(), false);
|
|
909
794
|
}
|
|
910
|
-
/** Придерживает очередь на заданное время: ждут все её задачи, а не только одна. */
|
|
911
|
-
pause(ms) {
|
|
912
|
-
if (ms <= 0) return;
|
|
913
|
-
this.#nextSlot = Math.max(this.#nextSlot, this.#clock.now() + ms);
|
|
914
|
-
}
|
|
915
|
-
/** Запускает столько ожидающих задач, сколько позволяют ограничения. */
|
|
916
795
|
#drain() {
|
|
917
|
-
if (this.#waiting.length === 0) {
|
|
918
|
-
if (this.#cancelTimer) {
|
|
919
|
-
this.#cancelTimer();
|
|
920
|
-
this.#cancelTimer = void 0;
|
|
921
|
-
}
|
|
922
|
-
return;
|
|
923
|
-
}
|
|
924
796
|
if (this.#active >= this.#concurrency) return;
|
|
925
|
-
if (this.#cancelTimer) return;
|
|
926
797
|
const now = this.#clock.now();
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
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();
|
|
932
816
|
return;
|
|
933
817
|
}
|
|
934
|
-
|
|
935
|
-
|
|
818
|
+
this.#cursor = (selectedIndex + 1) % this.#buckets.length;
|
|
819
|
+
this.#active += 1;
|
|
936
820
|
if (this.#minGap > 0) this.#nextSlot = now + this.#minGap;
|
|
937
|
-
|
|
821
|
+
selected.dispatch(now, () => {
|
|
822
|
+
this.#active -= 1;
|
|
823
|
+
this.changed();
|
|
824
|
+
});
|
|
938
825
|
this.#drain();
|
|
939
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
|
+
}
|
|
940
843
|
};
|
|
941
|
-
/** Длина окна лимита на сервере. */
|
|
942
|
-
const RATE_LIMIT_WINDOW = 6e4;
|
|
943
|
-
/**
|
|
944
|
-
* Пауза при исчерпании бакета неизвестной ёмкости.
|
|
945
|
-
*
|
|
946
|
-
* Ёмкость приходит в заголовке вместе с остатком, поэтому случай возможен только у чужого
|
|
947
|
-
* прокси, который прислал `remaining` без `limit`.
|
|
948
|
-
*/
|
|
949
|
-
const UNKNOWN_CAPACITY_PAUSE = 1e3;
|
|
950
844
|
/**
|
|
951
|
-
*
|
|
845
|
+
* Состояние и очередь одного серверного бакета.
|
|
952
846
|
*
|
|
953
|
-
*
|
|
954
|
-
*
|
|
955
|
-
* захвата общего слота: притормозивший счётчик не занимает общую ёмкость.
|
|
847
|
+
* Класс хранит локальную конкурентность, темп и серверную квоту, а решение о старте
|
|
848
|
+
* делегирует общему планировщику направления.
|
|
956
849
|
*
|
|
957
850
|
* @internal
|
|
958
851
|
*/
|
|
959
852
|
var BucketQueue = class {
|
|
960
853
|
#destination;
|
|
961
854
|
#bucket;
|
|
962
|
-
#
|
|
963
|
-
#shared;
|
|
855
|
+
#scheduler;
|
|
964
856
|
#clock;
|
|
965
857
|
#pacing;
|
|
858
|
+
#concurrency;
|
|
859
|
+
#minGap;
|
|
966
860
|
/** Ровный темп. Требует раздельных бакетов: без них ёмкость счётчика неизвестна. */
|
|
967
861
|
#smooth;
|
|
968
862
|
/**
|
|
@@ -979,6 +873,9 @@ var BucketQueue = class {
|
|
|
979
873
|
/** Последнее, что сказал сервер. Живёт и в режиме `off` — ради `rateLimitState()`. */
|
|
980
874
|
#limit;
|
|
981
875
|
#remaining;
|
|
876
|
+
#active = 0;
|
|
877
|
+
#nextSlot = 0;
|
|
878
|
+
#waiting = [];
|
|
982
879
|
/**
|
|
983
880
|
* Оценка остатка для режима `smooth`.
|
|
984
881
|
*
|
|
@@ -987,24 +884,19 @@ var BucketQueue = class {
|
|
|
987
884
|
*/
|
|
988
885
|
#tokens = 1;
|
|
989
886
|
#tokensAt;
|
|
990
|
-
|
|
991
|
-
* Номер поколения очереди. `stop()` увеличивает его, отсекая задачи, которые уже взяли
|
|
992
|
-
* слот бакета, но до общей очереди ещё не дошли.
|
|
993
|
-
*/
|
|
994
|
-
#generation = 0;
|
|
995
|
-
constructor(destination, bucket, shared, options, clock, featureDefinition) {
|
|
887
|
+
constructor(destination, bucket, scheduler, options, clock, featureDefinition) {
|
|
996
888
|
this.#destination = destination;
|
|
997
889
|
this.#bucket = bucket;
|
|
998
|
-
this.#
|
|
890
|
+
this.#scheduler = scheduler;
|
|
999
891
|
this.#clock = clock;
|
|
1000
892
|
this.#pacing = options.pacing;
|
|
1001
893
|
this.#smooth = options.buckets && options.pacing === RateLimitPacing.Smooth;
|
|
1002
894
|
this.#flatPause = options.buckets ? void 0 : options.retryDelays[0] ?? 0;
|
|
1003
895
|
this.#seedLimit = options.buckets ? featureDefinition?.limit ?? seedLimit(bucket, options) : void 0;
|
|
1004
|
-
this.#
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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);
|
|
1008
900
|
}
|
|
1009
901
|
/** Имя счётчика. При `buckets: false` — всегда `default`, каким бы ни был запрос. */
|
|
1010
902
|
get bucket() {
|
|
@@ -1012,19 +904,63 @@ var BucketQueue = class {
|
|
|
1012
904
|
}
|
|
1013
905
|
/** Запросов бакета прошло в общую очередь и ещё не завершилось. */
|
|
1014
906
|
get active() {
|
|
1015
|
-
return this.#
|
|
907
|
+
return this.#active;
|
|
1016
908
|
}
|
|
1017
909
|
/** Запросов бакета ждёт своей очереди. */
|
|
1018
910
|
get pending() {
|
|
1019
|
-
return this.#
|
|
911
|
+
return this.#waiting.length;
|
|
1020
912
|
}
|
|
1021
|
-
/**
|
|
913
|
+
/** Есть ли ожидающая задача, для которой свободен локальный слот. */
|
|
914
|
+
get canStart() {
|
|
915
|
+
return this.#waiting.length > 0 && this.#active < this.#concurrency;
|
|
916
|
+
}
|
|
917
|
+
/** Момент, раньше которого локальное ограничение не разрешает следующий старт. */
|
|
918
|
+
get readyAt() {
|
|
919
|
+
return this.#nextSlot;
|
|
920
|
+
}
|
|
921
|
+
/** Ставит запрос в очередь единого планировщика направления. */
|
|
1022
922
|
schedule(task, signal) {
|
|
1023
|
-
|
|
1024
|
-
return
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
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
|
+
});
|
|
1028
964
|
}
|
|
1029
965
|
/**
|
|
1030
966
|
* Учитывает заголовки ответа.
|
|
@@ -1040,7 +976,7 @@ var BucketQueue = class {
|
|
|
1040
976
|
if (this.#pacing === RateLimitPacing.Off || remaining === void 0) return 0;
|
|
1041
977
|
if (this.#flatPause !== void 0) {
|
|
1042
978
|
if (remaining > 0) return 0;
|
|
1043
|
-
this.#
|
|
979
|
+
this.#hold(this.#flatPause);
|
|
1044
980
|
return this.#flatPause;
|
|
1045
981
|
}
|
|
1046
982
|
const capacity = this.#capacity();
|
|
@@ -1048,11 +984,13 @@ var BucketQueue = class {
|
|
|
1048
984
|
if (capacity === void 0) return 0;
|
|
1049
985
|
this.#refill(capacity);
|
|
1050
986
|
if (remaining < this.#tokens) this.#tokens = remaining;
|
|
1051
|
-
|
|
987
|
+
const wait = this.#armPause(capacity);
|
|
988
|
+
this.#scheduler.timingChanged();
|
|
989
|
+
return wait;
|
|
1052
990
|
}
|
|
1053
991
|
if (remaining > 0) return 0;
|
|
1054
992
|
const wait = capacity === void 0 ? UNKNOWN_CAPACITY_PAUSE : Math.ceil(RATE_LIMIT_WINDOW / Math.max(capacity, 1));
|
|
1055
|
-
this.#
|
|
993
|
+
this.#hold(wait);
|
|
1056
994
|
return wait;
|
|
1057
995
|
}
|
|
1058
996
|
/** Придерживает бакет на названное время — путь ответа `429`. Оценка остатка обнуляется. */
|
|
@@ -1061,7 +999,7 @@ var BucketQueue = class {
|
|
|
1061
999
|
this.#tokens = 0;
|
|
1062
1000
|
this.#tokensAt = this.#clock.now();
|
|
1063
1001
|
}
|
|
1064
|
-
this.#
|
|
1002
|
+
this.#hold(ms);
|
|
1065
1003
|
}
|
|
1066
1004
|
/** Снимок для `rateLimitState()`. */
|
|
1067
1005
|
state() {
|
|
@@ -1070,17 +1008,19 @@ var BucketQueue = class {
|
|
|
1070
1008
|
bucket: this.#bucket,
|
|
1071
1009
|
limit: this.#limit,
|
|
1072
1010
|
remaining: this.#remaining,
|
|
1073
|
-
active: this.#
|
|
1074
|
-
pending: this.#
|
|
1011
|
+
active: this.#active,
|
|
1012
|
+
pending: this.#waiting.length
|
|
1075
1013
|
};
|
|
1076
1014
|
}
|
|
1077
|
-
/**
|
|
1078
|
-
* Останавливает уровень бакета. Общая очередь направления гасится пулом.
|
|
1079
|
-
*
|
|
1080
|
-
*/
|
|
1015
|
+
/** Отклоняет ещё не начатые задачи этого бакета. */
|
|
1081
1016
|
stop() {
|
|
1082
|
-
this
|
|
1083
|
-
|
|
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();
|
|
1084
1024
|
}
|
|
1085
1025
|
/** Лимит бакета: сказанный сервером, иначе табличный. */
|
|
1086
1026
|
#capacity() {
|
|
@@ -1107,9 +1047,15 @@ var BucketQueue = class {
|
|
|
1107
1047
|
#armPause(capacity) {
|
|
1108
1048
|
if (this.#tokens >= 1) return 0;
|
|
1109
1049
|
const wait = Math.ceil((1 - this.#tokens) * RATE_LIMIT_WINDOW / capacity);
|
|
1110
|
-
this.#
|
|
1050
|
+
this.#nextSlot = Math.max(this.#nextSlot, this.#clock.now() + wait);
|
|
1111
1051
|
return wait;
|
|
1112
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
|
+
}
|
|
1113
1059
|
};
|
|
1114
1060
|
/** Лимит бакета до первого ответа: поправка пользователя важнее табличного значения. */
|
|
1115
1061
|
function seedLimit(bucket, options) {
|
|
@@ -1146,11 +1092,12 @@ var RequestQueuePool = class {
|
|
|
1146
1092
|
defineBucket(name, definition) {
|
|
1147
1093
|
const normalized = Object.freeze({
|
|
1148
1094
|
...definition.limit === void 0 ? {} : { limit: definition.limit },
|
|
1149
|
-
...definition.concurrency === void 0 ? {} : { concurrency: definition.concurrency }
|
|
1095
|
+
...definition.concurrency === void 0 ? {} : { concurrency: definition.concurrency },
|
|
1096
|
+
...definition.rps === void 0 ? {} : { rps: definition.rps }
|
|
1150
1097
|
});
|
|
1151
1098
|
const existing = this.#featureBuckets.get(name);
|
|
1152
1099
|
if (existing) {
|
|
1153
|
-
if (existing.definition.limit !== normalized.limit || existing.definition.concurrency !== normalized.concurrency) throw new ItdConfigError(`Бакет feature «${name}» уже зарегистрирован с другими ограничениями`);
|
|
1100
|
+
if (existing.definition.limit !== normalized.limit || existing.definition.concurrency !== normalized.concurrency || existing.definition.rps !== normalized.rps) throw new ItdConfigError(`Бакет feature «${name}» уже зарегистрирован с другими ограничениями`);
|
|
1154
1101
|
existing.references += 1;
|
|
1155
1102
|
} else this.#featureBuckets.set(name, {
|
|
1156
1103
|
definition: normalized,
|
|
@@ -1174,14 +1121,14 @@ var RequestQueuePool = class {
|
|
|
1174
1121
|
let entry = this.#destinations.get(destination);
|
|
1175
1122
|
if (!entry) {
|
|
1176
1123
|
entry = {
|
|
1177
|
-
|
|
1124
|
+
scheduler: new DestinationScheduler(this.#options, this.#clock),
|
|
1178
1125
|
buckets: /* @__PURE__ */ new Map()
|
|
1179
1126
|
};
|
|
1180
1127
|
this.#destinations.set(destination, entry);
|
|
1181
1128
|
}
|
|
1182
1129
|
let queue = entry.buckets.get(name);
|
|
1183
1130
|
if (!queue) {
|
|
1184
|
-
queue = new BucketQueue(destination, name, entry.
|
|
1131
|
+
queue = new BucketQueue(destination, name, entry.scheduler, this.#options, this.#clock, this.#featureBuckets.get(name)?.definition);
|
|
1185
1132
|
entry.buckets.set(name, queue);
|
|
1186
1133
|
}
|
|
1187
1134
|
return queue;
|
|
@@ -1193,14 +1140,11 @@ var RequestQueuePool = class {
|
|
|
1193
1140
|
return states;
|
|
1194
1141
|
}
|
|
1195
1142
|
/**
|
|
1196
|
-
* Останавливает
|
|
1197
|
-
*
|
|
1143
|
+
* Останавливает планировщики: ожидающие задачи отклоняются, а состояние счётчиков
|
|
1144
|
+
* и отложенные паузы сохраняются до следующего запуска — путь `close()`.
|
|
1198
1145
|
*/
|
|
1199
1146
|
stop() {
|
|
1200
|
-
for (const entry of this.#destinations.values())
|
|
1201
|
-
for (const queue of entry.buckets.values()) queue.stop();
|
|
1202
|
-
entry.shared.stop();
|
|
1203
|
-
}
|
|
1147
|
+
for (const entry of this.#destinations.values()) entry.scheduler.stop();
|
|
1204
1148
|
}
|
|
1205
1149
|
/** Останавливает очереди и забывает всё, что известно о счётчиках, — путь `dispose()`. */
|
|
1206
1150
|
clear() {
|
|
@@ -1392,14 +1336,14 @@ function markDisposeCleanupRequest(request) {
|
|
|
1392
1336
|
[DISPOSE_CLEANUP_REQUEST]: true
|
|
1393
1337
|
};
|
|
1394
1338
|
}
|
|
1395
|
-
/** Разрешено ли запросу завершать внутреннюю очистку после
|
|
1339
|
+
/** Разрешено ли запросу завершать внутреннюю очистку после `dispose()`. @internal */
|
|
1396
1340
|
function isDisposeCleanupRequest(request) {
|
|
1397
1341
|
return request[DISPOSE_CLEANUP_REQUEST] === true;
|
|
1398
1342
|
}
|
|
1399
1343
|
/**
|
|
1400
1344
|
* Собирает слои в один обработчик.
|
|
1401
1345
|
*
|
|
1402
|
-
* Первый слой
|
|
1346
|
+
* Первый слой становится внешним.
|
|
1403
1347
|
*
|
|
1404
1348
|
* @example
|
|
1405
1349
|
* ```ts
|
|
@@ -1427,24 +1371,17 @@ function withLayerHeaders(request, headers) {
|
|
|
1427
1371
|
/**
|
|
1428
1372
|
* Точка входа ресурсов в конвейер запросов.
|
|
1429
1373
|
*
|
|
1430
|
-
* Принимает
|
|
1431
|
-
* во внутреннем runtime клиента, — и отдаёт ресурсам методы `request`/`operation`.
|
|
1374
|
+
* Принимает готовую цепочку обработки и предоставляет ресурсам методы `request`/`execute`.
|
|
1432
1375
|
* О слоях и их порядке ресурсы не знают.
|
|
1433
1376
|
*/
|
|
1434
1377
|
var HttpClient = class {
|
|
1435
1378
|
#handler;
|
|
1379
|
+
#plugins;
|
|
1436
1380
|
#baseUrl;
|
|
1437
|
-
#catalog;
|
|
1438
1381
|
constructor(deps) {
|
|
1439
1382
|
this.#handler = deps.handler;
|
|
1383
|
+
this.#plugins = deps.plugins;
|
|
1440
1384
|
this.#baseUrl = deps.baseUrl;
|
|
1441
|
-
this.#catalog = deps.catalog;
|
|
1442
|
-
}
|
|
1443
|
-
/** Метод операции из каталога. Отсутствие описания — ошибка сборки клиента, не запроса. */
|
|
1444
|
-
#methodOf(operationId) {
|
|
1445
|
-
const method = this.#catalog.methodOf(operationId);
|
|
1446
|
-
if (method === void 0) throw new ItdConfigError(`каталог операций не знает «${operationId}»`);
|
|
1447
|
-
return method;
|
|
1448
1385
|
}
|
|
1449
1386
|
/** Базовый URL, к которому обращается клиент. */
|
|
1450
1387
|
get baseUrl() {
|
|
@@ -1460,23 +1397,34 @@ var HttpClient = class {
|
|
|
1460
1397
|
* @throws {ItdNetworkError} если запрос не дошёл до сервера
|
|
1461
1398
|
*/
|
|
1462
1399
|
request(options) {
|
|
1463
|
-
|
|
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);
|
|
1464
1406
|
}
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
return this.request({
|
|
1407
|
+
#run(operation, options) {
|
|
1408
|
+
const request = identifyRequest({
|
|
1468
1409
|
...options,
|
|
1469
|
-
operationId,
|
|
1470
|
-
method:
|
|
1410
|
+
operationId: operation.id,
|
|
1411
|
+
method: operation.method,
|
|
1412
|
+
retrySafety: options.retrySafety ?? operation.retrySafety
|
|
1471
1413
|
});
|
|
1414
|
+
return this.#plugins.run(request, async (prepared) => operation.read(await this.#handler(prepared), prepared));
|
|
1472
1415
|
}
|
|
1473
1416
|
/** Выполняет внутреннюю операцию финализации после начала `ItdClient.dispose()`. @internal */
|
|
1474
|
-
cleanupOperation(
|
|
1475
|
-
|
|
1417
|
+
cleanupOperation(operation, options) {
|
|
1418
|
+
const request = identifyRequest({
|
|
1476
1419
|
...options,
|
|
1477
|
-
operationId,
|
|
1478
|
-
method:
|
|
1479
|
-
|
|
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
|
+
});
|
|
1480
1428
|
}
|
|
1481
1429
|
};
|
|
1482
1430
|
//#endregion
|
|
@@ -1501,6 +1449,50 @@ const RetrySafety = Object.freeze({
|
|
|
1501
1449
|
/** Повтор может создать ещё один побочный эффект. */
|
|
1502
1450
|
Unsafe: "unsafe"
|
|
1503
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() {}
|
|
1504
1496
|
//#endregion
|
|
1505
1497
|
//#region src/core/scheduling/retry.ts
|
|
1506
1498
|
/** Методы raw-запроса, безопасность которых гарантирована самим HTTP-контрактом. */
|
|
@@ -1610,9 +1602,6 @@ function createQueueMiddleware(schedule) {
|
|
|
1610
1602
|
* разу, иначе, например, текст поста зашифруется дважды. Здесь же к операции привязывается
|
|
1611
1603
|
* snapshot attempt interceptors; сами они выполняются транспортом на каждой попытке.
|
|
1612
1604
|
*/
|
|
1613
|
-
function createPluginsMiddleware(plugins) {
|
|
1614
|
-
return (request, next) => plugins.run(request, next);
|
|
1615
|
-
}
|
|
1616
1605
|
/**
|
|
1617
1606
|
* Слой сервисов.
|
|
1618
1607
|
*
|
|
@@ -1770,90 +1759,6 @@ function createRetryMiddleware(deps) {
|
|
|
1770
1759
|
};
|
|
1771
1760
|
}
|
|
1772
1761
|
//#endregion
|
|
1773
|
-
//#region src/core/redact.ts
|
|
1774
|
-
/** Заголовки, значение которых нельзя писать в лог целиком. */
|
|
1775
|
-
const SECRET_HEADERS = /* @__PURE__ */ new Set([
|
|
1776
|
-
"authorization",
|
|
1777
|
-
"cookie",
|
|
1778
|
-
"set-cookie",
|
|
1779
|
-
"x-api-key"
|
|
1780
|
-
]);
|
|
1781
|
-
/** Поля тела запроса, значение которых нельзя писать в лог. */
|
|
1782
|
-
const SECRET_FIELDS = /* @__PURE__ */ new Set([
|
|
1783
|
-
"password",
|
|
1784
|
-
"oldpassword",
|
|
1785
|
-
"newpassword",
|
|
1786
|
-
"accesstoken",
|
|
1787
|
-
"refreshtoken",
|
|
1788
|
-
"currentpassword",
|
|
1789
|
-
"flowtoken",
|
|
1790
|
-
"token",
|
|
1791
|
-
"turnstiletoken",
|
|
1792
|
-
"otp"
|
|
1793
|
-
]);
|
|
1794
|
-
/** Query-параметры, содержащие секреты. */
|
|
1795
|
-
const SECRET_QUERY_PARAMS = /* @__PURE__ */ new Set(["token", "access_token"]);
|
|
1796
|
-
/**
|
|
1797
|
-
* Прячет середину секрета, оставляя концы для сопоставления.
|
|
1798
|
-
*
|
|
1799
|
-
* @example
|
|
1800
|
-
* ```ts
|
|
1801
|
-
* maskSecret('eyJhbGciOiJIUzI1NiJ9.abc'); // 'eyJh…(24)…abc'
|
|
1802
|
-
* ```
|
|
1803
|
-
*/
|
|
1804
|
-
function maskSecret(value) {
|
|
1805
|
-
if (value.length <= 8) return "…";
|
|
1806
|
-
return `${value.slice(0, 4)}…(${value.length})…${value.slice(-3)}`;
|
|
1807
|
-
}
|
|
1808
|
-
/** Маскирует секретные query-параметры URL перед записью в логи и ошибки. */
|
|
1809
|
-
function redactUrl(url) {
|
|
1810
|
-
try {
|
|
1811
|
-
const parsed = new URL(url);
|
|
1812
|
-
const entries = [...parsed.searchParams.entries()];
|
|
1813
|
-
parsed.search = "";
|
|
1814
|
-
for (const [name, value] of entries) parsed.searchParams.append(name, SECRET_QUERY_PARAMS.has(name.toLowerCase()) ? maskSecret(value) : value);
|
|
1815
|
-
return parsed.toString();
|
|
1816
|
-
} catch {
|
|
1817
|
-
return url.replace(/([?&](?:token|access_token)=)[^&#]*/gi, "$1[скрыто]");
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
/**
|
|
1821
|
-
* Готовит заголовки к записи в лог.
|
|
1822
|
-
*
|
|
1823
|
-
* `Authorization` и `Cookie` маскируются: логи часто уезжают в системы сбора,
|
|
1824
|
-
* и токен доступа в них попадать не должен.
|
|
1825
|
-
*/
|
|
1826
|
-
function redactHeaders(headers) {
|
|
1827
|
-
const result = {};
|
|
1828
|
-
headers.forEach((value, name) => {
|
|
1829
|
-
if (SECRET_HEADERS.has(name.toLowerCase())) {
|
|
1830
|
-
const spaceAt = value.indexOf(" ");
|
|
1831
|
-
result[name] = spaceAt > 0 ? `${value.slice(0, spaceAt)} ${maskSecret(value.slice(spaceAt + 1))}` : maskSecret(value);
|
|
1832
|
-
return;
|
|
1833
|
-
}
|
|
1834
|
-
result[name] = value;
|
|
1835
|
-
});
|
|
1836
|
-
return result;
|
|
1837
|
-
}
|
|
1838
|
-
/**
|
|
1839
|
-
* Готовит тело запроса к записи в лог: пароли, токены и коды OTP заменяются заглушкой.
|
|
1840
|
-
*
|
|
1841
|
-
* Обходит вложенные объекты и массивы. `FormData` и бинарные тела не раскрываются вовсе.
|
|
1842
|
-
*/
|
|
1843
|
-
function redactBody(body) {
|
|
1844
|
-
if (body === null || body === void 0) return body;
|
|
1845
|
-
if (typeof FormData !== "undefined" && body instanceof FormData) return "[FormData]";
|
|
1846
|
-
if (isBlob(body)) return "[Blob]";
|
|
1847
|
-
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return "[binary]";
|
|
1848
|
-
if (Array.isArray(body)) return body.map(redactBody);
|
|
1849
|
-
if (typeof body === "object") {
|
|
1850
|
-
const result = {};
|
|
1851
|
-
for (const [key, value] of Object.entries(body)) result[key] = SECRET_FIELDS.has(key.toLowerCase()) ? "[скрыто]" : redactBody(value);
|
|
1852
|
-
return result;
|
|
1853
|
-
}
|
|
1854
|
-
return body;
|
|
1855
|
-
}
|
|
1856
|
-
//#endregion
|
|
1857
1762
|
//#region src/core/unwrap.ts
|
|
1858
1763
|
/**
|
|
1859
1764
|
* Снимает внешнюю обёртку `{ data: … }`.
|
|
@@ -2489,7 +2394,7 @@ var Transport = class {
|
|
|
2489
2394
|
};
|
|
2490
2395
|
//#endregion
|
|
2491
2396
|
//#region src/core/execution/client-runtime.ts
|
|
2492
|
-
/** Имена стадий
|
|
2397
|
+
/** Имена стадий обработки запроса в порядке выполнения. @internal */
|
|
2493
2398
|
const ClientRuntimeStage = Object.freeze({
|
|
2494
2399
|
OperationPlugins: "operation_plugins",
|
|
2495
2400
|
Services: "services",
|
|
@@ -2507,14 +2412,15 @@ function createServiceRegistry(config) {
|
|
|
2507
2412
|
for (const service of config.services) services.define(service);
|
|
2508
2413
|
return services;
|
|
2509
2414
|
}
|
|
2510
|
-
/** Собирает
|
|
2415
|
+
/** Собирает внутренние компоненты и цепочку запросов клиента. @internal */
|
|
2511
2416
|
function createClientRuntime(options, internals) {
|
|
2512
2417
|
const catalog = internals.catalog;
|
|
2513
2418
|
const config = resolveRuntimeConfig(options, catalog);
|
|
2514
2419
|
const jar = new CookieJar();
|
|
2515
2420
|
const plugins = new PluginRegistry({
|
|
2516
2421
|
shutdownTimeout: config.shutdownTimeout,
|
|
2517
|
-
clock: config.clock
|
|
2422
|
+
clock: config.clock,
|
|
2423
|
+
operationMetadata: (operationId) => catalog.definitionOf(operationId)
|
|
2518
2424
|
});
|
|
2519
2425
|
const services = createServiceRegistry(config);
|
|
2520
2426
|
const lifetime = new AbortController();
|
|
@@ -2563,10 +2469,6 @@ function createClientRuntime(options, internals) {
|
|
|
2563
2469
|
lifetimeSignal: lifetime.signal
|
|
2564
2470
|
});
|
|
2565
2471
|
const stages = [
|
|
2566
|
-
{
|
|
2567
|
-
name: ClientRuntimeStage.OperationPlugins,
|
|
2568
|
-
middleware: createPluginsMiddleware(plugins)
|
|
2569
|
-
},
|
|
2570
2472
|
{
|
|
2571
2473
|
name: ClientRuntimeStage.Services,
|
|
2572
2474
|
middleware: createServicesMiddleware(services)
|
|
@@ -2618,17 +2520,21 @@ function createClientRuntime(options, internals) {
|
|
|
2618
2520
|
}
|
|
2619
2521
|
return handler(request);
|
|
2620
2522
|
};
|
|
2523
|
+
const http = new HttpClient({
|
|
2524
|
+
handler: clientHandler,
|
|
2525
|
+
plugins,
|
|
2526
|
+
baseUrl: config.baseUrl
|
|
2527
|
+
});
|
|
2621
2528
|
auth = internals.auth({
|
|
2622
2529
|
config,
|
|
2623
|
-
|
|
2530
|
+
http,
|
|
2624
2531
|
cookies: jar
|
|
2625
2532
|
});
|
|
2626
|
-
const
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
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
|
+
]);
|
|
2632
2538
|
return {
|
|
2633
2539
|
config,
|
|
2634
2540
|
http,
|
|
@@ -2637,6 +2543,24 @@ function createClientRuntime(options, internals) {
|
|
|
2637
2543
|
plugins,
|
|
2638
2544
|
services,
|
|
2639
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
|
+
},
|
|
2640
2564
|
platformHeaders: (url) => transport.platformHeaders(url),
|
|
2641
2565
|
rateLimitState: () => queues?.states() ?? [],
|
|
2642
2566
|
registerRateLimitBucket: (name, definition) => queues?.defineBucket(name, definition),
|
|
@@ -2684,7 +2608,16 @@ const BUCKET_LIMITS = Object.freeze({
|
|
|
2684
2608
|
"posts.create": 5,
|
|
2685
2609
|
"users.updateMe": 3,
|
|
2686
2610
|
"reports.create": 3,
|
|
2687
|
-
"verification.submit": 3
|
|
2611
|
+
"verification.submit": 3,
|
|
2612
|
+
shop: 150,
|
|
2613
|
+
"shop.delivery.cities": 60,
|
|
2614
|
+
"shop.delivery.calculate": 45,
|
|
2615
|
+
"shop.delivery.points": 30,
|
|
2616
|
+
"shop.consents.record": 15,
|
|
2617
|
+
"shop.orders.pay": 13,
|
|
2618
|
+
"shop.orders.verifyAccessCode": 13,
|
|
2619
|
+
"shop.orders.create": 12,
|
|
2620
|
+
"shop.orders.requestAccessCode": 4
|
|
2688
2621
|
});
|
|
2689
2622
|
/** Счётчик, из которого списывается путь без собственного правила на сервере. */
|
|
2690
2623
|
const DEFAULT_RATE_LIMIT_BUCKET = "default";
|
|
@@ -3039,12 +2972,12 @@ const OPERATIONS = freezeOperations({
|
|
|
3039
2972
|
method: "PUT",
|
|
3040
2973
|
retrySafety: RetrySafety.Idempotent
|
|
3041
2974
|
},
|
|
3042
|
-
"
|
|
2975
|
+
"events.notifications.poll.updates": {
|
|
3043
2976
|
method: "GET",
|
|
3044
2977
|
retrySafety: RetrySafety.Safe,
|
|
3045
2978
|
bucket: "notifications"
|
|
3046
2979
|
},
|
|
3047
|
-
"
|
|
2980
|
+
"events.notifications.poll.unread": {
|
|
3048
2981
|
method: "GET",
|
|
3049
2982
|
retrySafety: RetrySafety.Safe,
|
|
3050
2983
|
bucket: "notifications"
|
|
@@ -3135,8 +3068,75 @@ const OPERATIONS = freezeOperations({
|
|
|
3135
3068
|
"telemetry.interaction": {
|
|
3136
3069
|
method: "POST",
|
|
3137
3070
|
retrySafety: RetrySafety.Unsafe
|
|
3071
|
+
},
|
|
3072
|
+
"shop.products.list": {
|
|
3073
|
+
method: "GET",
|
|
3074
|
+
retrySafety: RetrySafety.Safe,
|
|
3075
|
+
bucket: "shop"
|
|
3076
|
+
},
|
|
3077
|
+
"shop.products.get": {
|
|
3078
|
+
method: "GET",
|
|
3079
|
+
retrySafety: RetrySafety.Safe,
|
|
3080
|
+
bucket: "shop"
|
|
3081
|
+
},
|
|
3082
|
+
"shop.delivery.countries": {
|
|
3083
|
+
method: "GET",
|
|
3084
|
+
retrySafety: RetrySafety.Safe,
|
|
3085
|
+
bucket: "shop"
|
|
3086
|
+
},
|
|
3087
|
+
"shop.delivery.cities": {
|
|
3088
|
+
method: "GET",
|
|
3089
|
+
retrySafety: RetrySafety.Safe,
|
|
3090
|
+
bucket: "shop.delivery.cities"
|
|
3091
|
+
},
|
|
3092
|
+
"shop.delivery.points": {
|
|
3093
|
+
method: "GET",
|
|
3094
|
+
retrySafety: RetrySafety.Safe,
|
|
3095
|
+
bucket: "shop.delivery.points"
|
|
3096
|
+
},
|
|
3097
|
+
"shop.delivery.calculate": {
|
|
3098
|
+
method: "POST",
|
|
3099
|
+
retrySafety: RetrySafety.Safe,
|
|
3100
|
+
bucket: "shop.delivery.calculate"
|
|
3101
|
+
},
|
|
3102
|
+
"shop.orders.create": {
|
|
3103
|
+
method: "POST",
|
|
3104
|
+
retrySafety: RetrySafety.Idempotent,
|
|
3105
|
+
bucket: "shop.orders.create"
|
|
3106
|
+
},
|
|
3107
|
+
"shop.orders.list": {
|
|
3108
|
+
method: "GET",
|
|
3109
|
+
retrySafety: RetrySafety.Safe
|
|
3110
|
+
},
|
|
3111
|
+
"shop.orders.get": {
|
|
3112
|
+
method: "GET",
|
|
3113
|
+
retrySafety: RetrySafety.Safe
|
|
3114
|
+
},
|
|
3115
|
+
"shop.orders.pay": {
|
|
3116
|
+
method: "POST",
|
|
3117
|
+
retrySafety: RetrySafety.Unsafe,
|
|
3118
|
+
bucket: "shop.orders.pay"
|
|
3119
|
+
},
|
|
3120
|
+
"shop.orders.requestAccessCode": {
|
|
3121
|
+
method: "POST",
|
|
3122
|
+
retrySafety: RetrySafety.Unsafe,
|
|
3123
|
+
bucket: "shop.orders.requestAccessCode"
|
|
3124
|
+
},
|
|
3125
|
+
"shop.orders.verifyAccessCode": {
|
|
3126
|
+
method: "POST",
|
|
3127
|
+
retrySafety: RetrySafety.Unsafe,
|
|
3128
|
+
bucket: "shop.orders.verifyAccessCode"
|
|
3129
|
+
},
|
|
3130
|
+
"shop.consents.record": {
|
|
3131
|
+
method: "POST",
|
|
3132
|
+
retrySafety: RetrySafety.Unsafe,
|
|
3133
|
+
bucket: "shop.consents.record"
|
|
3138
3134
|
}
|
|
3139
3135
|
});
|
|
3136
|
+
/** Создаёт контракт результата встроенной операции с проверяемым ID. */
|
|
3137
|
+
function defineBuiltInOperation(id, read = identityResult) {
|
|
3138
|
+
return defineOperation(id, OPERATIONS[id], read);
|
|
3139
|
+
}
|
|
3140
3140
|
const BUILT_IN_IDS = new Set(Object.keys(OPERATIONS));
|
|
3141
3141
|
/** Проверяет принадлежность ID встроенному каталогу. */
|
|
3142
3142
|
function isBuiltInOperationId(value) {
|
|
@@ -3171,6 +3171,7 @@ function operationBucket(id) {
|
|
|
3171
3171
|
* @internal
|
|
3172
3172
|
*/
|
|
3173
3173
|
const ITD_CATALOG = Object.freeze({
|
|
3174
|
+
definitionOf: (id) => isBuiltInOperationId(id) ? OPERATIONS[id] : void 0,
|
|
3174
3175
|
retrySafetyOf: (id) => isBuiltInOperationId(id) ? operationRetrySafety(id) : void 0,
|
|
3175
3176
|
methodOf: (id) => isBuiltInOperationId(id) ? operationMethod(id) : void 0,
|
|
3176
3177
|
bucketOf: (id) => operationBucket(id),
|
|
@@ -3257,8 +3258,8 @@ const ReportReason = Object.freeze({
|
|
|
3257
3258
|
Fraud: "fraud",
|
|
3258
3259
|
Other: "other"
|
|
3259
3260
|
});
|
|
3260
|
-
/** Состояние
|
|
3261
|
-
const
|
|
3261
|
+
/** Состояние соединения событийного канала. */
|
|
3262
|
+
const EventChannelStatus = Object.freeze({
|
|
3262
3263
|
Connecting: "connecting",
|
|
3263
3264
|
Connected: "connected",
|
|
3264
3265
|
Error: "error",
|
|
@@ -3427,6 +3428,7 @@ const ItdErrorCode = Object.freeze({
|
|
|
3427
3428
|
SESSION_NOT_FOUND: "SESSION_NOT_FOUND",
|
|
3428
3429
|
MISSING_FLOW_TOKEN: "MISSING_FLOW_TOKEN",
|
|
3429
3430
|
PROFILE_USERNAME_TAKEN: "PROFILE_USERNAME_TAKEN",
|
|
3431
|
+
PROFILE_USERNAME_RESERVED: "PROFILE_USERNAME_RESERVED",
|
|
3430
3432
|
PROFILE_RESTRICTION_ACTIVE: "PROFILE_RESTRICTION_ACTIVE",
|
|
3431
3433
|
PROFILE_MODIFICATION_RESTRICTED: "PROFILE_MODIFICATION_RESTRICTED",
|
|
3432
3434
|
CONTENT_MODERATION_FAILED: "CONTENT_MODERATION_FAILED",
|
|
@@ -3577,6 +3579,103 @@ function readUnreadCountEvent(data) {
|
|
|
3577
3579
|
return typeof payload.count === "number" ? payload.count : void 0;
|
|
3578
3580
|
}
|
|
3579
3581
|
//#endregion
|
|
3582
|
+
//#region src/operations/common.ts
|
|
3583
|
+
/** Контракт встроенной операции без дополнительного преобразования результата. */
|
|
3584
|
+
function passthroughOperation(id) {
|
|
3585
|
+
return defineBuiltInOperation(id);
|
|
3586
|
+
}
|
|
3587
|
+
/** Контракт встроенной операции без возвращаемого значения. */
|
|
3588
|
+
function voidOperation(id) {
|
|
3589
|
+
return defineBuiltInOperation(id, voidResult);
|
|
3590
|
+
}
|
|
3591
|
+
//#endregion
|
|
3592
|
+
//#region src/core/auth-provider.ts
|
|
3593
|
+
/**
|
|
3594
|
+
* Идентификатор устройства, общий для провайдеров без сессии.
|
|
3595
|
+
*
|
|
3596
|
+
* Значение своё у каждого провайдера: сервер различает по нему записи в списке сессий,
|
|
3597
|
+
* поэтому делить его между клиентами нельзя. Хранилища нет, так что перезапуск процесса
|
|
3598
|
+
* значение не переживает — в этом и разница с полноценной сессией.
|
|
3599
|
+
*/
|
|
3600
|
+
function localDeviceId() {
|
|
3601
|
+
let deviceId;
|
|
3602
|
+
return () => {
|
|
3603
|
+
deviceId ??= createDeviceId();
|
|
3604
|
+
return Promise.resolve(deviceId);
|
|
3605
|
+
};
|
|
3606
|
+
}
|
|
3607
|
+
/**
|
|
3608
|
+
* Авторизации нет: заголовок не подставляется, ответ `401` не восстанавливается.
|
|
3609
|
+
*
|
|
3610
|
+
* @example
|
|
3611
|
+
* ```ts
|
|
3612
|
+
* const api = createRestClient(); // публичные эндпоинты доступны и без токена
|
|
3613
|
+
* ```
|
|
3614
|
+
*/
|
|
3615
|
+
function anonymousAuth() {
|
|
3616
|
+
return {
|
|
3617
|
+
token: () => Promise.resolve(null),
|
|
3618
|
+
prepare: () => Promise.resolve(),
|
|
3619
|
+
currentHeaders: () => ({}),
|
|
3620
|
+
recover: () => Promise.resolve(false),
|
|
3621
|
+
deviceId: localDeviceId(),
|
|
3622
|
+
dispose: () => {}
|
|
3623
|
+
};
|
|
3624
|
+
}
|
|
3625
|
+
/**
|
|
3626
|
+
* Готовый Bearer-токен: ни хранилища, ни продления.
|
|
3627
|
+
*
|
|
3628
|
+
* Ответ `401` уходит вызывающему коду как есть — обновить токен провайдеру нечем.
|
|
3629
|
+
* Для сессии, которая продлевает себя сама, нужен полный клиент.
|
|
3630
|
+
*
|
|
3631
|
+
* @example
|
|
3632
|
+
* ```ts
|
|
3633
|
+
* const api = createRestClient({ auth: bearerToken(process.env.ITD_TOKEN) });
|
|
3634
|
+
* ```
|
|
3635
|
+
*/
|
|
3636
|
+
function bearerToken(accessToken) {
|
|
3637
|
+
const headers = { Authorization: `Bearer ${accessToken}` };
|
|
3638
|
+
return {
|
|
3639
|
+
token: () => Promise.resolve(accessToken),
|
|
3640
|
+
prepare: () => Promise.resolve(),
|
|
3641
|
+
currentHeaders: () => headers,
|
|
3642
|
+
recover: () => Promise.resolve(false),
|
|
3643
|
+
deviceId: localDeviceId(),
|
|
3644
|
+
dispose: () => {}
|
|
3645
|
+
};
|
|
3646
|
+
}
|
|
3647
|
+
/**
|
|
3648
|
+
* Токен из внешнего источника — хранилища секретов, кэша, соседнего сервиса.
|
|
3649
|
+
*
|
|
3650
|
+
* Источник спрашивается на стадии подготовки, до входа в очередь: там ожидание безопасно,
|
|
3651
|
+
* а слот транспорта ещё не занят. Значение держится до следующей подготовки, потому что
|
|
3652
|
+
* подстановка заголовков обязана быть синхронной.
|
|
3653
|
+
*
|
|
3654
|
+
* @example
|
|
3655
|
+
* ```ts
|
|
3656
|
+
* const api = createRestClient({ auth: tokenProvider(() => vault.read('itd')) });
|
|
3657
|
+
* ```
|
|
3658
|
+
*/
|
|
3659
|
+
function tokenProvider(getToken) {
|
|
3660
|
+
let cached = null;
|
|
3661
|
+
const read = async () => {
|
|
3662
|
+
cached = await getToken();
|
|
3663
|
+
return cached;
|
|
3664
|
+
};
|
|
3665
|
+
return {
|
|
3666
|
+
token: read,
|
|
3667
|
+
prepare: async () => {
|
|
3668
|
+
await read();
|
|
3669
|
+
},
|
|
3670
|
+
currentHeaders: () => cached ? { Authorization: `Bearer ${cached}` } : {},
|
|
3671
|
+
recover: () => Promise.resolve(false),
|
|
3672
|
+
deviceId: localDeviceId(),
|
|
3673
|
+
dispose: () => {
|
|
3674
|
+
cached = null;
|
|
3675
|
+
}
|
|
3676
|
+
};
|
|
3677
|
+
}
|
|
3678
|
+
//#endregion
|
|
3580
3679
|
//#region src/notifications/text.ts
|
|
3581
3680
|
/** Имя, которое подставляется, если участник неизвестен. */
|
|
3582
3681
|
const UNKNOWN_ACTOR = "Пользователь";
|
|
@@ -3708,6 +3807,6 @@ function resolveNotificationUrl(notification) {
|
|
|
3708
3807
|
return clickUrl || "/notifications";
|
|
3709
3808
|
}
|
|
3710
3809
|
//#endregion
|
|
3711
|
-
export {
|
|
3810
|
+
export { orderPluginDefinitions 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, assertPluginRemovable as Q, DEFAULT_RATE_LIMIT_BUCKET as R, NotificationType as S, ServiceState as T, pickNumber as U, pickArray as V, pickObject as W, mergeService as X, voidResult as Y, RequestQueuePool as Z, FeedTab as _, tokenProvider as a, isRecord$1 as at, ItdErrorCode as b, normalizeNotification as c, createDeadline as ct, canonicalNotificationType as d, DEFAULT_BASE_URL as et, isKnownNotificationType as f, EventChannelStatus as g, CommentSort as h, bearerToken as i, LIBRARY_VERSION as it, OPERATIONS as j, WallAccess as k, readNotificationEvent as l, systemClock as lt, AttachmentType as m, formatNotificationText as n, STATUS_SERVICE as nt, passthroughOperation as o, requireOptionalBoolean as ot, AccessType as p, defineOperation as q, anonymousAuth as r, resolveRateLimit as rt, voidOperation as s, RateLimitPacing as st, resolveNotificationUrl as t, DEFAULT_STATUS_BASE_URL as tt, readUnreadCountEvent as u, installAsyncDisposeFallback as ut, IncidentKind as v, ReportTargetType as w, LikesVisibility as x, InteractionType as y, createClientRuntime as z };
|
|
3712
3811
|
|
|
3713
|
-
//# sourceMappingURL=url-
|
|
3812
|
+
//# sourceMappingURL=url-DP33mp3s.js.map
|