itd-api 0.0.7 → 0.0.8
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 +20 -0
- package/dist/{chunk-3RNNZJZ4.cjs → chunk-ATCZ4T2K.cjs} +1055 -971
- package/dist/chunk-ATCZ4T2K.cjs.map +1 -0
- package/dist/{chunk-CG4SERVM.js → chunk-JIYN33FG.js} +1055 -971
- package/dist/chunk-JIYN33FG.js.map +1 -0
- package/dist/{index-DNFPX_Z1.d.cts → index-Duh31Wnx.d.cts} +362 -329
- package/dist/{index-DNFPX_Z1.d.ts → index-Duh31Wnx.d.ts} +362 -329
- package/dist/index.cjs +89 -89
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/node.cjs +116 -95
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +2 -2
- package/dist/node.d.ts +2 -2
- package/dist/node.js +30 -9
- package/dist/node.js.map +1 -1
- package/package.json +8 -4
- package/dist/chunk-3RNNZJZ4.cjs.map +0 -1
- package/dist/chunk-CG4SERVM.js.map +0 -1
|
@@ -1293,13 +1293,23 @@ function readAccessToken(payload) {
|
|
|
1293
1293
|
const token = payload.accessToken;
|
|
1294
1294
|
return typeof token === "string" && token.length > 0 ? token : void 0;
|
|
1295
1295
|
}
|
|
1296
|
+
function reportListenerError(logger, scope, error) {
|
|
1297
|
+
const message = `\u041E\u0448\u0438\u0431\u043A\u0430 \u0432 \u043E\u0431\u0440\u0430\u0431\u043E\u0442\u0447\u0438\u043A\u0435 \u0441\u043E\u0431\u044B\u0442\u0438\u044F ${scope}`;
|
|
1298
|
+
if (logger) logger.error(message, error);
|
|
1299
|
+
else console.error(`[itd-api] ${message}`, error);
|
|
1300
|
+
}
|
|
1296
1301
|
var AuthManager = class {
|
|
1297
1302
|
#config;
|
|
1298
|
-
#
|
|
1303
|
+
#send;
|
|
1299
1304
|
#jar;
|
|
1300
|
-
#emitter
|
|
1305
|
+
#emitter;
|
|
1301
1306
|
/** `undefined` — сессия ещё не читалась из хранилища. */
|
|
1302
1307
|
#session;
|
|
1308
|
+
/**
|
|
1309
|
+
* Общий промис чтения сессии из хранилища. Дедупликация: параллельные запросы на холодном
|
|
1310
|
+
* старте читают хранилище один раз и не заводят каждый свой `deviceId`.
|
|
1311
|
+
*/
|
|
1312
|
+
#loading = null;
|
|
1303
1313
|
/** Общий промис обновления: к нему присоединяются все, кто получил 401. */
|
|
1304
1314
|
#refreshing = null;
|
|
1305
1315
|
/** Общий промис входа по логину и паролю. */
|
|
@@ -1311,10 +1321,15 @@ var AuthManager = class {
|
|
|
1311
1321
|
* поэтому `clear()` его не трогает.
|
|
1312
1322
|
*/
|
|
1313
1323
|
#deviceId;
|
|
1314
|
-
|
|
1324
|
+
/** Общий промис первичной выдачи `deviceId` — чтобы параллельные запросы получили один. */
|
|
1325
|
+
#deviceIdLoading = null;
|
|
1326
|
+
constructor(config, send, jar) {
|
|
1315
1327
|
this.#config = config;
|
|
1316
|
-
this.#
|
|
1328
|
+
this.#send = send;
|
|
1317
1329
|
this.#jar = jar;
|
|
1330
|
+
this.#emitter = new Emitter(
|
|
1331
|
+
(error) => reportListenerError(config.logger, "\u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u0438", error)
|
|
1332
|
+
);
|
|
1318
1333
|
}
|
|
1319
1334
|
/** Подписка на события авторизации. */
|
|
1320
1335
|
get on() {
|
|
@@ -1356,8 +1371,14 @@ var AuthManager = class {
|
|
|
1356
1371
|
* сервер связывает с ним запись в списке сессий, и плавающее значение плодило бы
|
|
1357
1372
|
* по новой сессии на каждый старт.
|
|
1358
1373
|
*/
|
|
1359
|
-
|
|
1360
|
-
if (this.#deviceId) return this.#deviceId;
|
|
1374
|
+
getDeviceId() {
|
|
1375
|
+
if (this.#deviceId) return Promise.resolve(this.#deviceId);
|
|
1376
|
+
this.#deviceIdLoading ??= this.#resolveDeviceId().finally(() => {
|
|
1377
|
+
this.#deviceIdLoading = null;
|
|
1378
|
+
});
|
|
1379
|
+
return this.#deviceIdLoading;
|
|
1380
|
+
}
|
|
1381
|
+
async #resolveDeviceId() {
|
|
1361
1382
|
const session = await this.#loadSession();
|
|
1362
1383
|
const deviceId = this.#config.deviceId ?? session?.deviceId ?? createDeviceId();
|
|
1363
1384
|
this.#deviceId = deviceId;
|
|
@@ -1460,8 +1481,14 @@ var AuthManager = class {
|
|
|
1460
1481
|
if (this.#deviceId) await this.#saveSession({ deviceId: this.#deviceId });
|
|
1461
1482
|
this.#emitter.emit("signOut", void 0);
|
|
1462
1483
|
}
|
|
1463
|
-
|
|
1464
|
-
if (this.#session !== void 0) return this.#session;
|
|
1484
|
+
#loadSession() {
|
|
1485
|
+
if (this.#session !== void 0) return Promise.resolve(this.#session);
|
|
1486
|
+
this.#loading ??= this.#performLoad().finally(() => {
|
|
1487
|
+
this.#loading = null;
|
|
1488
|
+
});
|
|
1489
|
+
return this.#loading;
|
|
1490
|
+
}
|
|
1491
|
+
async #performLoad() {
|
|
1465
1492
|
const stored = await this.#config.storage.get() ?? null;
|
|
1466
1493
|
if (stored?.cookies) this.#jar.deserialize(stored.cookies);
|
|
1467
1494
|
const fromConfig = this.#sessionFromConfig(this.#config.auth);
|
|
@@ -1529,17 +1556,14 @@ var AuthManager = class {
|
|
|
1529
1556
|
return this.#reloginOrNull();
|
|
1530
1557
|
}
|
|
1531
1558
|
try {
|
|
1532
|
-
const payload = await this.#
|
|
1559
|
+
const payload = await this.#send({
|
|
1533
1560
|
method: "POST",
|
|
1534
1561
|
path: AUTH_PATHS.refresh,
|
|
1562
|
+
skipQueue: true,
|
|
1563
|
+
skipAuth: true,
|
|
1564
|
+
skipAuthRefresh: true
|
|
1535
1565
|
// Тела нет намеренно: сервер читает refresh-токен только из cookie — см.
|
|
1536
1566
|
// #seedRefreshCookie. По той же причине не нужен и устаревший Bearer.
|
|
1537
|
-
skipAuth: true,
|
|
1538
|
-
// Без этого 401 на самом обновлении вызвал бы новое обновление — и так по кругу.
|
|
1539
|
-
skipAuthRefresh: true,
|
|
1540
|
-
// Обновление почти всегда запускается изнутри запроса, который занимает место
|
|
1541
|
-
// в очереди и ждёт его результата. Встать в ту же очередь — значит зависнуть.
|
|
1542
|
-
skipQueue: true
|
|
1543
1567
|
});
|
|
1544
1568
|
const accessToken = readAccessToken(payload);
|
|
1545
1569
|
if (!accessToken) return this.#reloginOrNull();
|
|
@@ -1611,15 +1635,13 @@ var AuthManager = class {
|
|
|
1611
1635
|
}
|
|
1612
1636
|
async #performSignIn(credentials) {
|
|
1613
1637
|
const turnstileToken = await this.#resolveTurnstileToken(credentials);
|
|
1614
|
-
const payload = await this.#
|
|
1638
|
+
const payload = await this.#send({
|
|
1615
1639
|
method: "POST",
|
|
1616
1640
|
path: AUTH_PATHS.signIn,
|
|
1617
1641
|
body: { email: credentials.email, password: credentials.password, turnstileToken },
|
|
1642
|
+
skipQueue: true,
|
|
1618
1643
|
skipAuth: true,
|
|
1619
|
-
skipAuthRefresh: true
|
|
1620
|
-
// Отложенный вход происходит при сборке заголовков уже начатого запроса — тот держит
|
|
1621
|
-
// место в очереди и ждёт токена. См. `skipQueue` в RawRequestOptions.
|
|
1622
|
-
skipQueue: true
|
|
1644
|
+
skipAuthRefresh: true
|
|
1623
1645
|
});
|
|
1624
1646
|
const accessToken = readAccessToken(payload);
|
|
1625
1647
|
if (!accessToken) {
|
|
@@ -1745,10 +1767,12 @@ function normalizeBaseUrl(baseUrl) {
|
|
|
1745
1767
|
return parsed.origin + (parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, ""));
|
|
1746
1768
|
}
|
|
1747
1769
|
|
|
1770
|
+
// src/core/version.ts
|
|
1771
|
+
var LIBRARY_VERSION = "0.0.8";
|
|
1772
|
+
|
|
1748
1773
|
// src/core/config.ts
|
|
1749
1774
|
var DEFAULT_BASE_URL = "https://xn--d1ah4a.com";
|
|
1750
1775
|
var DEFAULT_TIMEOUT = 3e4;
|
|
1751
|
-
var LIBRARY_VERSION = "0.0.7";
|
|
1752
1776
|
var DEFAULT_USER_AGENT = `Mozilla/5.0 (compatible; itd-api/${LIBRARY_VERSION}; +https://github.com/KiowDev/itd-api)`;
|
|
1753
1777
|
var DEFAULT_RATE_LIMIT_DELAYS = Object.freeze([1e3, 5e3, 3e4, 6e4, 9e4]);
|
|
1754
1778
|
function requirePositive(value, name) {
|
|
@@ -1893,183 +1917,520 @@ function resolveConfig(options = {}) {
|
|
|
1893
1917
|
};
|
|
1894
1918
|
}
|
|
1895
1919
|
|
|
1896
|
-
// src/core/
|
|
1897
|
-
var
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
"flowtoken",
|
|
1906
|
-
"token",
|
|
1907
|
-
"turnstiletoken",
|
|
1908
|
-
"otp"
|
|
1909
|
-
]);
|
|
1910
|
-
function maskSecret(value) {
|
|
1911
|
-
if (value.length <= 8) return "\u2026";
|
|
1912
|
-
return `${value.slice(0, 4)}\u2026(${value.length})\u2026${value.slice(-3)}`;
|
|
1913
|
-
}
|
|
1914
|
-
function redactHeaders(headers) {
|
|
1915
|
-
const result = {};
|
|
1916
|
-
headers.forEach((value, name) => {
|
|
1917
|
-
if (SECRET_HEADERS.has(name.toLowerCase())) {
|
|
1918
|
-
const spaceAt = value.indexOf(" ");
|
|
1919
|
-
result[name] = spaceAt > 0 ? `${value.slice(0, spaceAt)} ${maskSecret(value.slice(spaceAt + 1))}` : maskSecret(value);
|
|
1920
|
-
return;
|
|
1921
|
-
}
|
|
1922
|
-
result[name] = value;
|
|
1923
|
-
});
|
|
1924
|
-
return result;
|
|
1925
|
-
}
|
|
1926
|
-
function redactBody(body) {
|
|
1927
|
-
if (body === null || body === void 0) return body;
|
|
1928
|
-
if (typeof FormData !== "undefined" && body instanceof FormData) return "[FormData]";
|
|
1929
|
-
if (isBlob(body)) return "[Blob]";
|
|
1930
|
-
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return "[binary]";
|
|
1931
|
-
if (Array.isArray(body)) return body.map(redactBody);
|
|
1932
|
-
if (typeof body === "object") {
|
|
1933
|
-
const result = {};
|
|
1934
|
-
for (const [key, value] of Object.entries(body)) {
|
|
1935
|
-
result[key] = SECRET_FIELDS.has(key.toLowerCase()) ? "[\u0441\u043A\u0440\u044B\u0442\u043E]" : redactBody(value);
|
|
1936
|
-
}
|
|
1937
|
-
return result;
|
|
1920
|
+
// src/core/http.ts
|
|
1921
|
+
var HttpClient = class {
|
|
1922
|
+
#handler;
|
|
1923
|
+
#plugins;
|
|
1924
|
+
#baseUrl;
|
|
1925
|
+
constructor(deps) {
|
|
1926
|
+
this.#handler = deps.handler;
|
|
1927
|
+
this.#plugins = deps.plugins;
|
|
1928
|
+
this.#baseUrl = deps.baseUrl;
|
|
1938
1929
|
}
|
|
1939
|
-
|
|
1940
|
-
|
|
1930
|
+
/** Базовый URL, к которому обращается клиент. */
|
|
1931
|
+
get baseUrl() {
|
|
1932
|
+
return this.#baseUrl;
|
|
1933
|
+
}
|
|
1934
|
+
/**
|
|
1935
|
+
* Имена опций запроса, заявленные плагинами.
|
|
1936
|
+
*
|
|
1937
|
+
* Читается ресурсами: они переносят в транспорт только известные поля, а чужие,
|
|
1938
|
+
* если их никто не заявил, отсеивают.
|
|
1939
|
+
*/
|
|
1940
|
+
get pluginOptionKeys() {
|
|
1941
|
+
return this.#plugins.optionKeys;
|
|
1942
|
+
}
|
|
1943
|
+
/**
|
|
1944
|
+
* Выполняет запрос к API через собранный конвейер.
|
|
1945
|
+
*
|
|
1946
|
+
* @typeParam T ожидаемая форма ответа после снятия обёртки `{ data: … }`
|
|
1947
|
+
* @throws {ItdApiError} если сервер ответил статусом ≥ 400
|
|
1948
|
+
* @throws {ItdTimeoutError} если истёк таймаут
|
|
1949
|
+
* @throws {ItdAbortError} если запрос отменён через `signal`
|
|
1950
|
+
* @throws {ItdNetworkError} если запрос не дошёл до сервера
|
|
1951
|
+
*/
|
|
1952
|
+
request(options) {
|
|
1953
|
+
return this.#handler(options);
|
|
1954
|
+
}
|
|
1955
|
+
};
|
|
1941
1956
|
|
|
1942
|
-
// src/core/
|
|
1943
|
-
function
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1957
|
+
// src/core/pipeline.ts
|
|
1958
|
+
function composePipeline(middlewares, final) {
|
|
1959
|
+
return middlewares.reduceRight(
|
|
1960
|
+
(next, middleware) => (request) => middleware(request, next),
|
|
1961
|
+
final
|
|
1962
|
+
);
|
|
1948
1963
|
}
|
|
1949
|
-
function
|
|
1950
|
-
return
|
|
1964
|
+
function withLayerHeaders(request, headers) {
|
|
1965
|
+
return { ...request, layerHeaders: { ...request.layerHeaders, ...headers } };
|
|
1951
1966
|
}
|
|
1952
|
-
|
|
1953
|
-
|
|
1967
|
+
|
|
1968
|
+
// src/core/retry.ts
|
|
1969
|
+
var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
1970
|
+
function isRetryable(error, method, retryWrites) {
|
|
1971
|
+
if (error instanceof ItdAbortError) return false;
|
|
1972
|
+
const safeToRepeat = retryWrites || IDEMPOTENT_METHODS.has(method);
|
|
1973
|
+
if (error instanceof ItdApiError) {
|
|
1974
|
+
if (error.status === 429) return true;
|
|
1975
|
+
if (error.status >= 500) return safeToRepeat;
|
|
1976
|
+
return false;
|
|
1977
|
+
}
|
|
1978
|
+
if (error instanceof ItdNetworkError || error instanceof ItdTimeoutError) return safeToRepeat;
|
|
1979
|
+
return false;
|
|
1954
1980
|
}
|
|
1955
|
-
function
|
|
1956
|
-
|
|
1957
|
-
const
|
|
1958
|
-
|
|
1981
|
+
function backoffDelay(attempt, options, random) {
|
|
1982
|
+
const exponential = options.baseDelay * 2 ** (attempt - 1);
|
|
1983
|
+
const capped = Math.min(exponential, options.maxDelay);
|
|
1984
|
+
const spread = capped * options.jitter * (random() * 2 - 1);
|
|
1985
|
+
return Math.max(0, Math.round(capped + spread));
|
|
1959
1986
|
}
|
|
1960
|
-
function
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1987
|
+
function createRetryScheduler(options, random = Math.random) {
|
|
1988
|
+
return (error, attempt, method) => {
|
|
1989
|
+
if (attempt >= options.attempts) return void 0;
|
|
1990
|
+
if (options.shouldRetry) {
|
|
1991
|
+
return options.shouldRetry(error, attempt) ? backoffDelay(attempt, options, random) : void 0;
|
|
1992
|
+
}
|
|
1993
|
+
if (!isRetryable(error, method, options.retryWrites)) return void 0;
|
|
1994
|
+
if (error instanceof ItdApiError && error.retryAfter !== void 0) {
|
|
1995
|
+
return error.retryAfter > options.maxDelay ? void 0 : error.retryAfter;
|
|
1996
|
+
}
|
|
1997
|
+
return backoffDelay(attempt, options, random);
|
|
1998
|
+
};
|
|
1965
1999
|
}
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
return
|
|
2000
|
+
|
|
2001
|
+
// src/core/middleware.ts
|
|
2002
|
+
function sleep(ms) {
|
|
2003
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1970
2004
|
}
|
|
1971
|
-
function
|
|
1972
|
-
|
|
1973
|
-
const value = source[field];
|
|
1974
|
-
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
2005
|
+
function createQueueMiddleware(schedule) {
|
|
2006
|
+
return (request, next) => request.skipQueue ? next(request) : schedule(() => next(request));
|
|
1975
2007
|
}
|
|
1976
|
-
function
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
2008
|
+
function createPluginsMiddleware(plugins) {
|
|
2009
|
+
return (request, next) => {
|
|
2010
|
+
if (plugins.size === 0) return next(request);
|
|
2011
|
+
return plugins.run(request, next);
|
|
2012
|
+
};
|
|
1980
2013
|
}
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
2014
|
+
async function applyAuth(request, deps) {
|
|
2015
|
+
if (request.skipAuth) return request;
|
|
2016
|
+
const headers = await deps.getAuthHeaders();
|
|
2017
|
+
return Object.keys(headers).length > 0 ? withLayerHeaders(request, headers) : request;
|
|
2018
|
+
}
|
|
2019
|
+
function createAuthMiddleware(deps) {
|
|
2020
|
+
return async (request, next) => {
|
|
2021
|
+
const authorized = await applyAuth(request, deps);
|
|
2022
|
+
try {
|
|
2023
|
+
return await next(authorized);
|
|
2024
|
+
} catch (error) {
|
|
2025
|
+
if (request.skipAuthRefresh || !deps.autoRefresh || !isItdApiError(error) || error.status !== 401) {
|
|
2026
|
+
throw error;
|
|
1993
2027
|
}
|
|
2028
|
+
const refreshed = await deps.onUnauthorized();
|
|
2029
|
+
if (!refreshed) throw error;
|
|
2030
|
+
const retried = await applyAuth({ ...request, skipAuthRefresh: true }, deps);
|
|
2031
|
+
return next(retried);
|
|
1994
2032
|
}
|
|
1995
|
-
}
|
|
1996
|
-
const violations = source.violations;
|
|
1997
|
-
if (Array.isArray(violations)) {
|
|
1998
|
-
for (const violation of violations) {
|
|
1999
|
-
if (!isRecord(violation)) continue;
|
|
2000
|
-
const field = asString(violation.field) ?? asString(violation.property);
|
|
2001
|
-
const message = asString(violation.message);
|
|
2002
|
-
if (!field || !message) continue;
|
|
2003
|
-
const existing = result[field];
|
|
2004
|
-
if (existing) existing.push(message);
|
|
2005
|
-
else result[field] = [message];
|
|
2006
|
-
}
|
|
2007
|
-
}
|
|
2008
|
-
return result;
|
|
2009
|
-
}
|
|
2010
|
-
function parseErrorBody(body, status, statusText = "") {
|
|
2011
|
-
const fallbackMessage = statusText ? `HTTP ${status} ${statusText}` : `HTTP ${status}`;
|
|
2012
|
-
if (typeof body === "string") {
|
|
2013
|
-
return {
|
|
2014
|
-
code: "UNKNOWN_ERROR",
|
|
2015
|
-
message: asString(body.trim()) ?? fallbackMessage,
|
|
2016
|
-
detail: void 0,
|
|
2017
|
-
title: void 0,
|
|
2018
|
-
fieldErrors: {},
|
|
2019
|
-
userId: void 0
|
|
2020
|
-
};
|
|
2021
|
-
}
|
|
2022
|
-
if (!isRecord(body)) {
|
|
2023
|
-
return {
|
|
2024
|
-
code: "UNKNOWN_ERROR",
|
|
2025
|
-
message: fallbackMessage,
|
|
2026
|
-
detail: void 0,
|
|
2027
|
-
title: void 0,
|
|
2028
|
-
fieldErrors: {},
|
|
2029
|
-
userId: void 0
|
|
2030
|
-
};
|
|
2031
|
-
}
|
|
2032
|
-
if (body.type === "validation") {
|
|
2033
|
-
const target = asString(body.on);
|
|
2034
|
-
return {
|
|
2035
|
-
code: "VALIDATION_ERROR",
|
|
2036
|
-
message: target ? `\u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u043D\u0435 \u043F\u0440\u043E\u0439\u0434\u0435\u043D\u0430: \u043D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0435 \u0434\u0430\u043D\u043D\u044B\u0435 \u0432 \xAB${target}\xBB` : "\u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u0432\u0445\u043E\u0434\u043D\u044B\u0445 \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435 \u043F\u0440\u043E\u0439\u0434\u0435\u043D\u0430",
|
|
2037
|
-
detail: void 0,
|
|
2038
|
-
title: void 0,
|
|
2039
|
-
fieldErrors: {},
|
|
2040
|
-
userId: void 0
|
|
2041
|
-
};
|
|
2042
|
-
}
|
|
2043
|
-
const inner = isRecord(body.error) ? body.error : body;
|
|
2044
|
-
const message = asString(inner.message) ?? asString(inner.detail) ?? asString(inner.title) ?? // `{ "error": "Invalid token" }` — так отвечает сервер на недействительный токен.
|
|
2045
|
-
asString(body.error) ?? fallbackMessage;
|
|
2046
|
-
return {
|
|
2047
|
-
code: asString(inner.code) ?? asString(body.code) ?? "UNKNOWN_ERROR",
|
|
2048
|
-
message,
|
|
2049
|
-
detail: asString(inner.detail),
|
|
2050
|
-
title: asString(inner.title),
|
|
2051
|
-
fieldErrors: { ...collectFieldErrors(body), ...collectFieldErrors(inner) },
|
|
2052
|
-
userId: asString(inner.userId) ?? asString(body.userId)
|
|
2053
2033
|
};
|
|
2054
2034
|
}
|
|
2055
|
-
function
|
|
2056
|
-
if (
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
if (Number.isFinite(date)) return Math.max(0, date - now);
|
|
2061
|
-
return void 0;
|
|
2062
|
-
}
|
|
2063
|
-
function readIntHeader(headers, name) {
|
|
2064
|
-
const raw = headers?.get(name);
|
|
2065
|
-
if (raw === null || raw === void 0) return void 0;
|
|
2066
|
-
const value = Number.parseInt(raw, 10);
|
|
2067
|
-
return Number.isFinite(value) ? value : void 0;
|
|
2035
|
+
function resolveBackoff(retry, global) {
|
|
2036
|
+
if (retry === void 0) return global;
|
|
2037
|
+
if (retry === false) return void 0;
|
|
2038
|
+
const resolved = resolveRetry(retry);
|
|
2039
|
+
return resolved ? createRetryScheduler(resolved) : void 0;
|
|
2068
2040
|
}
|
|
2069
|
-
function
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2041
|
+
function createRetryMiddleware(deps) {
|
|
2042
|
+
const globalScheduler = deps.retry ? createRetryScheduler(deps.retry) : void 0;
|
|
2043
|
+
const nextDelay = (error, attempt, method, backoff) => {
|
|
2044
|
+
if (isItdRateLimitError(error)) {
|
|
2045
|
+
const wait = error.retryAfter ?? deps.rateLimitDelays[attempt - 1];
|
|
2046
|
+
if (wait === void 0) return void 0;
|
|
2047
|
+
deps.pauseQueue?.(wait);
|
|
2048
|
+
deps.logger?.debug(`\u043B\u0438\u043C\u0438\u0442 \u0447\u0430\u0441\u0442\u043E\u0442\u044B, \u043F\u043E\u043F\u044B\u0442\u043A\u0430 ${attempt + 1} \u0447\u0435\u0440\u0435\u0437 ${wait} \u043C\u0441`);
|
|
2049
|
+
return wait;
|
|
2050
|
+
}
|
|
2051
|
+
return backoff?.(error, attempt, method);
|
|
2052
|
+
};
|
|
2053
|
+
return async (request, next) => {
|
|
2054
|
+
const method = request.method.toUpperCase();
|
|
2055
|
+
const backoff = resolveBackoff(request.retry, globalScheduler);
|
|
2056
|
+
for (let attempt = 1; ; attempt++) {
|
|
2057
|
+
try {
|
|
2058
|
+
return await next({ ...request, attempt });
|
|
2059
|
+
} catch (error) {
|
|
2060
|
+
const delay = nextDelay(error, attempt, method, backoff);
|
|
2061
|
+
if (delay === void 0) throw error;
|
|
2062
|
+
await deps.hooks.onRetry?.({
|
|
2063
|
+
method,
|
|
2064
|
+
path: request.path,
|
|
2065
|
+
url: deps.buildUrl(request),
|
|
2066
|
+
headers: new Headers(),
|
|
2067
|
+
attempt,
|
|
2068
|
+
error,
|
|
2069
|
+
delay
|
|
2070
|
+
});
|
|
2071
|
+
deps.logger?.debug(
|
|
2072
|
+
`\u043F\u043E\u0432\u0442\u043E\u0440 ${method} ${request.path}, \u043F\u043E\u043F\u044B\u0442\u043A\u0430 ${attempt + 1} \u0447\u0435\u0440\u0435\u0437 ${delay} \u043C\u0441`
|
|
2073
|
+
);
|
|
2074
|
+
await sleep(delay);
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
// src/core/plugins.ts
|
|
2081
|
+
var NO_KEYS = /* @__PURE__ */ new Set();
|
|
2082
|
+
var RESERVED_OPTION_KEYS = /* @__PURE__ */ new Set([
|
|
2083
|
+
"signal",
|
|
2084
|
+
"timeout",
|
|
2085
|
+
"headers",
|
|
2086
|
+
"retry",
|
|
2087
|
+
"method",
|
|
2088
|
+
"path",
|
|
2089
|
+
"query",
|
|
2090
|
+
"body",
|
|
2091
|
+
"skipAuth",
|
|
2092
|
+
"skipAuthRefresh",
|
|
2093
|
+
"skipQueue",
|
|
2094
|
+
"raw"
|
|
2095
|
+
]);
|
|
2096
|
+
var PluginRegistry = class {
|
|
2097
|
+
#transformers = [];
|
|
2098
|
+
#optionKeys = /* @__PURE__ */ new Set();
|
|
2099
|
+
#names = /* @__PURE__ */ new Set();
|
|
2100
|
+
/** Сколько обёрток подключено. Ноль означает, что запрос идёт прежним путём. */
|
|
2101
|
+
get size() {
|
|
2102
|
+
return this.#transformers.length;
|
|
2103
|
+
}
|
|
2104
|
+
/** Имена опций запроса, заявленные плагинами. */
|
|
2105
|
+
get optionKeys() {
|
|
2106
|
+
return this.#optionKeys.size === 0 ? NO_KEYS : this.#optionKeys;
|
|
2107
|
+
}
|
|
2108
|
+
/**
|
|
2109
|
+
* Подключает плагин.
|
|
2110
|
+
*
|
|
2111
|
+
* @throws {ItdConfigError} если плагин задан неверно, уже подключён или заявил занятое
|
|
2112
|
+
* имя опции
|
|
2113
|
+
*/
|
|
2114
|
+
add(plugin, context) {
|
|
2115
|
+
if (typeof plugin?.install !== "function") {
|
|
2116
|
+
throw new ItdConfigError("\u041F\u043B\u0430\u0433\u0438\u043D \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043E\u0431\u044A\u0435\u043A\u0442\u043E\u043C \u0441 \u043C\u0435\u0442\u043E\u0434\u043E\u043C install()");
|
|
2117
|
+
}
|
|
2118
|
+
const name = plugin.name;
|
|
2119
|
+
if (typeof name !== "string" || name.trim() === "") {
|
|
2120
|
+
throw new ItdConfigError("\u0423 \u043F\u043B\u0430\u0433\u0438\u043D\u0430 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0443\u0441\u0442\u043E\u0435 \u0438\u043C\u044F");
|
|
2121
|
+
}
|
|
2122
|
+
if (this.#names.has(name)) {
|
|
2123
|
+
throw new ItdConfigError(`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u0443\u0436\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0451\u043D`);
|
|
2124
|
+
}
|
|
2125
|
+
const keys = plugin.optionKeys ?? [];
|
|
2126
|
+
for (const key of keys) {
|
|
2127
|
+
if (typeof key !== "string" || key.trim() === "") {
|
|
2128
|
+
throw new ItdConfigError(`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u0437\u0430\u044F\u0432\u0438\u043B \u043F\u0443\u0441\u0442\u043E\u0435 \u0438\u043C\u044F \u043E\u043F\u0446\u0438\u0438`);
|
|
2129
|
+
}
|
|
2130
|
+
if (RESERVED_OPTION_KEYS.has(key)) {
|
|
2131
|
+
throw new ItdConfigError(
|
|
2132
|
+
`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u0437\u0430\u044F\u0432\u0438\u043B \u043E\u043F\u0446\u0438\u044E \xAB${key}\xBB: \u044D\u0442\u043E \u043F\u043E\u043B\u0435 \u0437\u0430\u043F\u0440\u043E\u0441\u0430, \u0438\u043C\u044F \u0437\u0430\u043D\u044F\u0442\u043E. \u0417\u0430\u043D\u044F\u0442\u044B\u0435 \u0438\u043C\u0435\u043D\u0430: ${[...RESERVED_OPTION_KEYS].join(", ")}`
|
|
2133
|
+
);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
const before = this.#transformers.length;
|
|
2137
|
+
try {
|
|
2138
|
+
plugin.install({
|
|
2139
|
+
...context,
|
|
2140
|
+
use: (transformer) => {
|
|
2141
|
+
if (typeof transformer !== "function") {
|
|
2142
|
+
throw new ItdConfigError(`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u043F\u0435\u0440\u0435\u0434\u0430\u043B \u0432 use() \u043D\u0435 \u0444\u0443\u043D\u043A\u0446\u0438\u044E`);
|
|
2143
|
+
}
|
|
2144
|
+
this.#transformers.push(transformer);
|
|
2145
|
+
}
|
|
2146
|
+
});
|
|
2147
|
+
} catch (error) {
|
|
2148
|
+
this.#transformers.length = before;
|
|
2149
|
+
throw error;
|
|
2150
|
+
}
|
|
2151
|
+
this.#names.add(name);
|
|
2152
|
+
for (const key of keys) this.#optionKeys.add(key);
|
|
2153
|
+
}
|
|
2154
|
+
/**
|
|
2155
|
+
* Прогоняет запрос через цепочку обёрток.
|
|
2156
|
+
*
|
|
2157
|
+
* Цепочка собирается на каждый запрос заново: плагин можно подключить в любой момент,
|
|
2158
|
+
* а обёрток единицы — экономить тут не на чем.
|
|
2159
|
+
*
|
|
2160
|
+
* @param execute настоящий запрос, вызывается самой внутренней обёрткой
|
|
2161
|
+
*/
|
|
2162
|
+
run(request, execute) {
|
|
2163
|
+
const chain = this.#transformers.reduceRight(
|
|
2164
|
+
(next, transformer) => (current) => transformer(current, next),
|
|
2165
|
+
execute
|
|
2166
|
+
);
|
|
2167
|
+
return chain(request);
|
|
2168
|
+
}
|
|
2169
|
+
};
|
|
2170
|
+
|
|
2171
|
+
// src/core/rate-limit.ts
|
|
2172
|
+
var RequestQueue = class {
|
|
2173
|
+
#concurrency;
|
|
2174
|
+
/** Минимальный промежуток между стартами, мс. `0` — без ограничения частоты. */
|
|
2175
|
+
#minGap;
|
|
2176
|
+
#waiting = [];
|
|
2177
|
+
#active = 0;
|
|
2178
|
+
/** Момент, раньше которого следующий запрос стартовать не должен. */
|
|
2179
|
+
#nextSlot = 0;
|
|
2180
|
+
#timer;
|
|
2181
|
+
constructor(options) {
|
|
2182
|
+
this.#concurrency = options.concurrency;
|
|
2183
|
+
this.#minGap = options.rps ? 1e3 / options.rps : 0;
|
|
2184
|
+
}
|
|
2185
|
+
/** Сколько задач выполняется прямо сейчас. */
|
|
2186
|
+
get active() {
|
|
2187
|
+
return this.#active;
|
|
2188
|
+
}
|
|
2189
|
+
/** Сколько задач ждёт очереди. */
|
|
2190
|
+
get pending() {
|
|
2191
|
+
return this.#waiting.length;
|
|
2192
|
+
}
|
|
2193
|
+
/**
|
|
2194
|
+
* Ставит задачу в очередь.
|
|
2195
|
+
*
|
|
2196
|
+
* @returns результат задачи; ошибка задачи пробрасывается без изменений
|
|
2197
|
+
*/
|
|
2198
|
+
schedule(task) {
|
|
2199
|
+
return new Promise((resolve, reject) => {
|
|
2200
|
+
const run = () => {
|
|
2201
|
+
this.#active += 1;
|
|
2202
|
+
task().then(resolve, reject).finally(() => {
|
|
2203
|
+
this.#active -= 1;
|
|
2204
|
+
this.#drain();
|
|
2205
|
+
});
|
|
2206
|
+
};
|
|
2207
|
+
this.#waiting.push({ run, cancel: reject });
|
|
2208
|
+
this.#drain();
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
2211
|
+
/**
|
|
2212
|
+
* Останавливает очередь: снимает отложенную паузу и отклоняет ещё не начатые задачи
|
|
2213
|
+
* ошибкой `ItdAbortError`. Уже выполняющиеся задачи доводятся до конца.
|
|
2214
|
+
*/
|
|
2215
|
+
stop() {
|
|
2216
|
+
if (this.#timer !== void 0) {
|
|
2217
|
+
clearTimeout(this.#timer);
|
|
2218
|
+
this.#timer = void 0;
|
|
2219
|
+
}
|
|
2220
|
+
this.#nextSlot = 0;
|
|
2221
|
+
const pending = this.#waiting.splice(0, this.#waiting.length);
|
|
2222
|
+
for (const task of pending) {
|
|
2223
|
+
task.cancel(new ItdAbortError("\u041A\u043B\u0438\u0435\u043D\u0442 \u0437\u0430\u043A\u0440\u044B\u0442, \u0437\u0430\u043F\u0440\u043E\u0441 \u043E\u0442\u043C\u0435\u043D\u0451\u043D"));
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
/**
|
|
2227
|
+
* Придерживает всю очередь на заданное время.
|
|
2228
|
+
*
|
|
2229
|
+
* Вызывается при получении `429` с заголовком `Retry-After`: тормозить нужно все запросы,
|
|
2230
|
+
* а не только тот, который наткнулся на лимит, — иначе остальные продолжат добивать API.
|
|
2231
|
+
*/
|
|
2232
|
+
pause(ms) {
|
|
2233
|
+
if (ms <= 0) return;
|
|
2234
|
+
this.#nextSlot = Math.max(this.#nextSlot, Date.now() + ms);
|
|
2235
|
+
}
|
|
2236
|
+
/** Запускает столько ожидающих задач, сколько позволяют ограничения. */
|
|
2237
|
+
#drain() {
|
|
2238
|
+
if (this.#waiting.length === 0) return;
|
|
2239
|
+
if (this.#active >= this.#concurrency) return;
|
|
2240
|
+
if (this.#timer !== void 0) return;
|
|
2241
|
+
const now = Date.now();
|
|
2242
|
+
if (this.#nextSlot > now) {
|
|
2243
|
+
this.#timer = setTimeout(() => {
|
|
2244
|
+
this.#timer = void 0;
|
|
2245
|
+
this.#drain();
|
|
2246
|
+
}, this.#nextSlot - now);
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
const next = this.#waiting.shift();
|
|
2250
|
+
if (!next) return;
|
|
2251
|
+
if (this.#minGap > 0) this.#nextSlot = now + this.#minGap;
|
|
2252
|
+
next.run();
|
|
2253
|
+
this.#drain();
|
|
2254
|
+
}
|
|
2255
|
+
};
|
|
2256
|
+
|
|
2257
|
+
// src/core/redact.ts
|
|
2258
|
+
var SECRET_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "set-cookie", "x-api-key"]);
|
|
2259
|
+
var SECRET_FIELDS = /* @__PURE__ */ new Set([
|
|
2260
|
+
"password",
|
|
2261
|
+
"oldpassword",
|
|
2262
|
+
"newpassword",
|
|
2263
|
+
"accesstoken",
|
|
2264
|
+
"refreshtoken",
|
|
2265
|
+
"currentpassword",
|
|
2266
|
+
"flowtoken",
|
|
2267
|
+
"token",
|
|
2268
|
+
"turnstiletoken",
|
|
2269
|
+
"otp"
|
|
2270
|
+
]);
|
|
2271
|
+
function maskSecret(value) {
|
|
2272
|
+
if (value.length <= 8) return "\u2026";
|
|
2273
|
+
return `${value.slice(0, 4)}\u2026(${value.length})\u2026${value.slice(-3)}`;
|
|
2274
|
+
}
|
|
2275
|
+
function redactHeaders(headers) {
|
|
2276
|
+
const result = {};
|
|
2277
|
+
headers.forEach((value, name) => {
|
|
2278
|
+
if (SECRET_HEADERS.has(name.toLowerCase())) {
|
|
2279
|
+
const spaceAt = value.indexOf(" ");
|
|
2280
|
+
result[name] = spaceAt > 0 ? `${value.slice(0, spaceAt)} ${maskSecret(value.slice(spaceAt + 1))}` : maskSecret(value);
|
|
2281
|
+
return;
|
|
2282
|
+
}
|
|
2283
|
+
result[name] = value;
|
|
2284
|
+
});
|
|
2285
|
+
return result;
|
|
2286
|
+
}
|
|
2287
|
+
function redactBody(body) {
|
|
2288
|
+
if (body === null || body === void 0) return body;
|
|
2289
|
+
if (typeof FormData !== "undefined" && body instanceof FormData) return "[FormData]";
|
|
2290
|
+
if (isBlob(body)) return "[Blob]";
|
|
2291
|
+
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return "[binary]";
|
|
2292
|
+
if (Array.isArray(body)) return body.map(redactBody);
|
|
2293
|
+
if (typeof body === "object") {
|
|
2294
|
+
const result = {};
|
|
2295
|
+
for (const [key, value] of Object.entries(body)) {
|
|
2296
|
+
result[key] = SECRET_FIELDS.has(key.toLowerCase()) ? "[\u0441\u043A\u0440\u044B\u0442\u043E]" : redactBody(value);
|
|
2297
|
+
}
|
|
2298
|
+
return result;
|
|
2299
|
+
}
|
|
2300
|
+
return body;
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
// src/core/unwrap.ts
|
|
2304
|
+
function unwrapData(body) {
|
|
2305
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) return body;
|
|
2306
|
+
const keys = Object.keys(body);
|
|
2307
|
+
if (keys.length !== 1 || keys[0] !== "data") return body;
|
|
2308
|
+
return body.data;
|
|
2309
|
+
}
|
|
2310
|
+
function isRecord(value) {
|
|
2311
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2312
|
+
}
|
|
2313
|
+
function asString(value) {
|
|
2314
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2315
|
+
}
|
|
2316
|
+
function pickArray(source, field) {
|
|
2317
|
+
if (typeof source !== "object" || source === null) return [];
|
|
2318
|
+
const value = source[field];
|
|
2319
|
+
return Array.isArray(value) ? value : [];
|
|
2320
|
+
}
|
|
2321
|
+
function pickObject(source, field) {
|
|
2322
|
+
if (typeof source !== "object" || source === null) return void 0;
|
|
2323
|
+
const value = source[field];
|
|
2324
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
2325
|
+
return value;
|
|
2326
|
+
}
|
|
2327
|
+
function pickBoolean(source, field, fallback = false) {
|
|
2328
|
+
if (typeof source !== "object" || source === null) return fallback;
|
|
2329
|
+
const value = source[field];
|
|
2330
|
+
return typeof value === "boolean" ? value : fallback;
|
|
2331
|
+
}
|
|
2332
|
+
function pickNumber(source, field, fallback) {
|
|
2333
|
+
if (typeof source !== "object" || source === null) return fallback;
|
|
2334
|
+
const value = source[field];
|
|
2335
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
2336
|
+
}
|
|
2337
|
+
function pickString(source, field) {
|
|
2338
|
+
if (typeof source !== "object" || source === null) return void 0;
|
|
2339
|
+
const value = source[field];
|
|
2340
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
// src/core/error-factory.ts
|
|
2344
|
+
function collectFieldErrors(source) {
|
|
2345
|
+
const result = {};
|
|
2346
|
+
const errors = source.errors;
|
|
2347
|
+
if (isRecord(errors)) {
|
|
2348
|
+
for (const [field, value] of Object.entries(errors)) {
|
|
2349
|
+
if (Array.isArray(value)) {
|
|
2350
|
+
const messages = value.filter((item) => typeof item === "string");
|
|
2351
|
+
if (messages.length > 0) result[field] = messages;
|
|
2352
|
+
} else if (typeof value === "string") {
|
|
2353
|
+
result[field] = [value];
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
const violations = source.violations;
|
|
2358
|
+
if (Array.isArray(violations)) {
|
|
2359
|
+
for (const violation of violations) {
|
|
2360
|
+
if (!isRecord(violation)) continue;
|
|
2361
|
+
const field = asString(violation.field) ?? asString(violation.property);
|
|
2362
|
+
const message = asString(violation.message);
|
|
2363
|
+
if (!field || !message) continue;
|
|
2364
|
+
const existing = result[field];
|
|
2365
|
+
if (existing) existing.push(message);
|
|
2366
|
+
else result[field] = [message];
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
return result;
|
|
2370
|
+
}
|
|
2371
|
+
function parseErrorBody(body, status, statusText = "") {
|
|
2372
|
+
const fallbackMessage = statusText ? `HTTP ${status} ${statusText}` : `HTTP ${status}`;
|
|
2373
|
+
if (typeof body === "string") {
|
|
2374
|
+
return {
|
|
2375
|
+
code: "UNKNOWN_ERROR",
|
|
2376
|
+
message: asString(body.trim()) ?? fallbackMessage,
|
|
2377
|
+
detail: void 0,
|
|
2378
|
+
title: void 0,
|
|
2379
|
+
fieldErrors: {},
|
|
2380
|
+
userId: void 0
|
|
2381
|
+
};
|
|
2382
|
+
}
|
|
2383
|
+
if (!isRecord(body)) {
|
|
2384
|
+
return {
|
|
2385
|
+
code: "UNKNOWN_ERROR",
|
|
2386
|
+
message: fallbackMessage,
|
|
2387
|
+
detail: void 0,
|
|
2388
|
+
title: void 0,
|
|
2389
|
+
fieldErrors: {},
|
|
2390
|
+
userId: void 0
|
|
2391
|
+
};
|
|
2392
|
+
}
|
|
2393
|
+
if (body.type === "validation") {
|
|
2394
|
+
const target = asString(body.on);
|
|
2395
|
+
return {
|
|
2396
|
+
code: "VALIDATION_ERROR",
|
|
2397
|
+
message: target ? `\u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u043D\u0435 \u043F\u0440\u043E\u0439\u0434\u0435\u043D\u0430: \u043D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0435 \u0434\u0430\u043D\u043D\u044B\u0435 \u0432 \xAB${target}\xBB` : "\u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u0432\u0445\u043E\u0434\u043D\u044B\u0445 \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435 \u043F\u0440\u043E\u0439\u0434\u0435\u043D\u0430",
|
|
2398
|
+
detail: void 0,
|
|
2399
|
+
title: void 0,
|
|
2400
|
+
fieldErrors: {},
|
|
2401
|
+
userId: void 0
|
|
2402
|
+
};
|
|
2403
|
+
}
|
|
2404
|
+
const inner = isRecord(body.error) ? body.error : body;
|
|
2405
|
+
const message = asString(inner.message) ?? asString(inner.detail) ?? asString(inner.title) ?? // `{ "error": "Invalid token" }` — так отвечает сервер на недействительный токен.
|
|
2406
|
+
asString(body.error) ?? fallbackMessage;
|
|
2407
|
+
return {
|
|
2408
|
+
code: asString(inner.code) ?? asString(body.code) ?? "UNKNOWN_ERROR",
|
|
2409
|
+
message,
|
|
2410
|
+
detail: asString(inner.detail),
|
|
2411
|
+
title: asString(inner.title),
|
|
2412
|
+
fieldErrors: { ...collectFieldErrors(body), ...collectFieldErrors(inner) },
|
|
2413
|
+
userId: asString(inner.userId) ?? asString(body.userId)
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2416
|
+
function parseRetryAfter(header, now = Date.now()) {
|
|
2417
|
+
if (!header) return void 0;
|
|
2418
|
+
const seconds = Number(header);
|
|
2419
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
|
|
2420
|
+
const date = Date.parse(header);
|
|
2421
|
+
if (Number.isFinite(date)) return Math.max(0, date - now);
|
|
2422
|
+
return void 0;
|
|
2423
|
+
}
|
|
2424
|
+
function readIntHeader(headers, name) {
|
|
2425
|
+
const raw = headers?.get(name);
|
|
2426
|
+
if (raw === null || raw === void 0) return void 0;
|
|
2427
|
+
const value = Number.parseInt(raw, 10);
|
|
2428
|
+
return Number.isFinite(value) ? value : void 0;
|
|
2429
|
+
}
|
|
2430
|
+
function readRateLimit(headers) {
|
|
2431
|
+
return {
|
|
2432
|
+
limit: readIntHeader(headers, "x-ratelimit-limit"),
|
|
2433
|
+
remaining: readIntHeader(headers, "x-ratelimit-remaining")
|
|
2073
2434
|
};
|
|
2074
2435
|
}
|
|
2075
2436
|
var REQUEST_ID_HEADERS = ["x-request-id", "x-requestid", "request-id", "x-correlation-id"];
|
|
@@ -2139,11 +2500,7 @@ function createApiError(context) {
|
|
|
2139
2500
|
return new Ctor(init);
|
|
2140
2501
|
}
|
|
2141
2502
|
|
|
2142
|
-
// src/core/
|
|
2143
|
-
function sleep(ms) {
|
|
2144
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2145
|
-
}
|
|
2146
|
-
var EMPTY_KEYS = /* @__PURE__ */ new Set();
|
|
2503
|
+
// src/core/transport.ts
|
|
2147
2504
|
function setHeader(headers, name, value) {
|
|
2148
2505
|
try {
|
|
2149
2506
|
headers.set(name, value);
|
|
@@ -2194,137 +2551,47 @@ function createAbortBundle(userSignal, timeout) {
|
|
|
2194
2551
|
}
|
|
2195
2552
|
};
|
|
2196
2553
|
}
|
|
2197
|
-
var
|
|
2554
|
+
var Transport = class {
|
|
2198
2555
|
#config;
|
|
2199
|
-
#
|
|
2200
|
-
|
|
2201
|
-
constructor(config, collaborators = {}) {
|
|
2556
|
+
#deps;
|
|
2557
|
+
constructor(config, deps) {
|
|
2202
2558
|
this.#config = config;
|
|
2203
|
-
this.#
|
|
2559
|
+
this.#deps = deps;
|
|
2204
2560
|
}
|
|
2205
|
-
/** Базовый URL, к которому обращается
|
|
2561
|
+
/** Базовый URL, к которому обращается транспорт. */
|
|
2206
2562
|
get baseUrl() {
|
|
2207
2563
|
return this.#config.baseUrl;
|
|
2208
2564
|
}
|
|
2209
2565
|
/**
|
|
2210
|
-
*
|
|
2211
|
-
*
|
|
2212
|
-
* Читается ресурсами: они переносят в транспорт только известные поля, а чужие,
|
|
2213
|
-
* если их никто не заявил, отсеивают.
|
|
2214
|
-
*/
|
|
2215
|
-
get pluginOptionKeys() {
|
|
2216
|
-
return this.#plugins?.optionKeys ?? EMPTY_KEYS;
|
|
2217
|
-
}
|
|
2218
|
-
/** Подключает список плагинов. Реестр общий с клиентом и пополняется через `itd.use()`. */
|
|
2219
|
-
usePlugins(plugins) {
|
|
2220
|
-
this.#plugins = plugins;
|
|
2221
|
-
}
|
|
2222
|
-
/**
|
|
2223
|
-
* Подключает недостающие части конвейера.
|
|
2224
|
-
*
|
|
2225
|
-
* Нужно из-за кольцевой зависимости: слой авторизации сам выполняет запросы, поэтому
|
|
2226
|
-
* не может быть передан в конструктор до создания транспорта.
|
|
2227
|
-
*/
|
|
2228
|
-
setCollaborators(collaborators) {
|
|
2229
|
-
this.#collaborators = { ...this.#collaborators, ...collaborators };
|
|
2230
|
-
}
|
|
2231
|
-
/**
|
|
2232
|
-
* Выполняет запрос к API.
|
|
2566
|
+
* Выполняет один сетевой запрос.
|
|
2233
2567
|
*
|
|
2234
|
-
* @typeParam T ожидаемая форма ответа после снятия обёртки `{ data: … }`
|
|
2235
2568
|
* @throws {ItdApiError} если сервер ответил статусом ≥ 400
|
|
2236
2569
|
* @throws {ItdTimeoutError} если истёк таймаут
|
|
2237
2570
|
* @throws {ItdAbortError} если запрос отменён через `signal`
|
|
2238
2571
|
* @throws {ItdNetworkError} если запрос не дошёл до сервера
|
|
2239
2572
|
*/
|
|
2240
|
-
async request
|
|
2241
|
-
const
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
/**
|
|
2246
|
-
* Прогоняет запрос через обёртки плагинов.
|
|
2247
|
-
*
|
|
2248
|
-
* Цепочка стоит **снаружи повторов и внутри очереди**: плагин должен увидеть запрос
|
|
2249
|
-
* и ответ по одному разу, независимо от того, сколько попыток понадобилось, — иначе,
|
|
2250
|
-
* например, текст поста зашифруется повторно на второй попытке.
|
|
2251
|
-
*/
|
|
2252
|
-
#withPlugins(options) {
|
|
2253
|
-
const plugins = this.#plugins;
|
|
2254
|
-
if (!plugins || plugins.size === 0) return this.#withRetries(options);
|
|
2255
|
-
return plugins.run(options, (request) => this.#withRetries(request));
|
|
2256
|
-
}
|
|
2257
|
-
async #withRetries(options) {
|
|
2258
|
-
const method = options.method.toUpperCase();
|
|
2259
|
-
for (let attempt = 1; ; attempt++) {
|
|
2260
|
-
try {
|
|
2261
|
-
return await this.#attempt(options, attempt);
|
|
2262
|
-
} catch (error) {
|
|
2263
|
-
const delay = this.#collaborators.nextRetryDelay?.(error, attempt, method);
|
|
2264
|
-
if (delay === void 0) throw error;
|
|
2265
|
-
await this.#config.hooks.onRetry?.({
|
|
2266
|
-
method,
|
|
2267
|
-
path: options.path,
|
|
2268
|
-
url: this.#buildUrl(options),
|
|
2269
|
-
headers: new Headers(),
|
|
2270
|
-
attempt,
|
|
2271
|
-
error,
|
|
2272
|
-
delay
|
|
2273
|
-
});
|
|
2274
|
-
this.#config.logger?.debug(
|
|
2275
|
-
`\u043F\u043E\u0432\u0442\u043E\u0440 ${method} ${options.path}, \u043F\u043E\u043F\u044B\u0442\u043A\u0430 ${attempt + 1} \u0447\u0435\u0440\u0435\u0437 ${delay} \u043C\u0441`
|
|
2276
|
-
);
|
|
2277
|
-
await sleep(delay);
|
|
2278
|
-
}
|
|
2279
|
-
}
|
|
2280
|
-
}
|
|
2281
|
-
#buildUrl(options) {
|
|
2282
|
-
return joinUrl(this.#config.baseUrl, options.path) + buildQuery(options.query);
|
|
2283
|
-
}
|
|
2284
|
-
async #buildHeaders(options, url) {
|
|
2285
|
-
const headers = new Headers();
|
|
2286
|
-
headers.set("Accept", "application/json");
|
|
2287
|
-
headers.set("X-Requested-With", "XMLHttpRequest");
|
|
2288
|
-
if (this.#config.userAgent) setHeader(headers, "User-Agent", this.#config.userAgent);
|
|
2289
|
-
if (this.#collaborators.getDeviceId) {
|
|
2290
|
-
setHeader(headers, "X-Device-Id", await this.#collaborators.getDeviceId());
|
|
2291
|
-
}
|
|
2292
|
-
for (const [name, value] of Object.entries(this.#config.headers))
|
|
2293
|
-
setHeader(headers, name, value);
|
|
2294
|
-
if (!options.skipAuth && this.#collaborators.getAuthHeaders) {
|
|
2295
|
-
const auth = await this.#collaborators.getAuthHeaders();
|
|
2296
|
-
for (const [name, value] of Object.entries(auth)) setHeader(headers, name, value);
|
|
2297
|
-
}
|
|
2298
|
-
if (this.#config.useCookieJar && this.#collaborators.getCookieHeader) {
|
|
2299
|
-
const cookie = this.#collaborators.getCookieHeader(url);
|
|
2300
|
-
if (cookie) setHeader(headers, "Cookie", cookie);
|
|
2301
|
-
}
|
|
2302
|
-
for (const [name, value] of Object.entries(options.headers ?? {})) {
|
|
2303
|
-
setHeader(headers, name, value);
|
|
2304
|
-
}
|
|
2305
|
-
return headers;
|
|
2306
|
-
}
|
|
2307
|
-
async #attempt(options, attempt) {
|
|
2308
|
-
const method = options.method.toUpperCase();
|
|
2309
|
-
const url = this.#buildUrl(options);
|
|
2310
|
-
const headers = await this.#buildHeaders(options, url);
|
|
2573
|
+
send = async (request) => {
|
|
2574
|
+
const method = request.method.toUpperCase();
|
|
2575
|
+
const url = this.buildUrl(request);
|
|
2576
|
+
const headers = await this.#buildHeaders(request, url);
|
|
2577
|
+
const attempt = request.attempt ?? 1;
|
|
2311
2578
|
let body;
|
|
2312
|
-
if (
|
|
2313
|
-
if (isRawBody(
|
|
2314
|
-
body =
|
|
2579
|
+
if (request.body !== void 0 && request.body !== null) {
|
|
2580
|
+
if (isRawBody(request.body)) {
|
|
2581
|
+
body = request.body;
|
|
2315
2582
|
} else {
|
|
2316
|
-
body = JSON.stringify(
|
|
2583
|
+
body = JSON.stringify(request.body);
|
|
2317
2584
|
if (!headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
2318
2585
|
}
|
|
2319
2586
|
}
|
|
2320
|
-
const context = { method, path:
|
|
2587
|
+
const context = { method, path: request.path, url, headers, attempt };
|
|
2321
2588
|
await this.#config.hooks.onRequest?.(context);
|
|
2322
|
-
const timeout =
|
|
2323
|
-
const abort = createAbortBundle(
|
|
2589
|
+
const timeout = request.timeout ?? this.#config.timeout;
|
|
2590
|
+
const abort = createAbortBundle(request.signal, timeout);
|
|
2324
2591
|
const startedAt = Date.now();
|
|
2325
|
-
this.#config.logger?.debug(`\u2192 ${method} ${
|
|
2592
|
+
this.#config.logger?.debug(`\u2192 ${method} ${request.path}`, {
|
|
2326
2593
|
headers: redactHeaders(headers),
|
|
2327
|
-
body: redactBody(
|
|
2594
|
+
body: redactBody(request.body)
|
|
2328
2595
|
});
|
|
2329
2596
|
let response;
|
|
2330
2597
|
try {
|
|
@@ -2337,31 +2604,24 @@ var HttpClient = class {
|
|
|
2337
2604
|
});
|
|
2338
2605
|
} catch (error) {
|
|
2339
2606
|
const duration2 = Date.now() - startedAt;
|
|
2340
|
-
const failure = this.#toTransportError(error, abort,
|
|
2607
|
+
const failure = this.#toTransportError(error, abort, request, method, timeout);
|
|
2341
2608
|
await this.#config.hooks.onError?.({ ...context, duration: duration2, error: failure });
|
|
2342
|
-
this.#config.logger?.warn(`\xD7 ${method} ${
|
|
2609
|
+
this.#config.logger?.warn(`\xD7 ${method} ${request.path} (${duration2} \u043C\u0441): ${failure.message}`);
|
|
2343
2610
|
throw failure;
|
|
2344
2611
|
} finally {
|
|
2345
2612
|
abort.cleanup();
|
|
2346
2613
|
}
|
|
2347
2614
|
const duration = Date.now() - startedAt;
|
|
2348
|
-
if (this.#
|
|
2615
|
+
if (this.#deps.onRateLimit) {
|
|
2349
2616
|
const { limit, remaining } = readRateLimit(response.headers);
|
|
2350
|
-
this.#
|
|
2617
|
+
this.#deps.onRateLimit(limit, remaining);
|
|
2351
2618
|
}
|
|
2352
|
-
if (this.#config.useCookieJar) this.#
|
|
2619
|
+
if (this.#config.useCookieJar) this.#deps.cookies?.setFromResponse(url, response);
|
|
2353
2620
|
const payload = await readBody(response);
|
|
2354
2621
|
if (!response.ok) {
|
|
2355
|
-
if (response.status === 401 && !options.skipAuthRefresh && this.#config.autoRefresh && this.#collaborators.onUnauthorized) {
|
|
2356
|
-
const refreshed = await this.#collaborators.onUnauthorized();
|
|
2357
|
-
if (refreshed) {
|
|
2358
|
-
this.#config.logger?.debug(`\u0442\u043E\u043A\u0435\u043D \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D, \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u044E ${method} ${options.path}`);
|
|
2359
|
-
return this.#attempt({ ...options, skipAuthRefresh: true }, attempt);
|
|
2360
|
-
}
|
|
2361
|
-
}
|
|
2362
2622
|
const error = createApiError({
|
|
2363
2623
|
method,
|
|
2364
|
-
path:
|
|
2624
|
+
path: request.path,
|
|
2365
2625
|
status: response.status,
|
|
2366
2626
|
statusText: response.statusText,
|
|
2367
2627
|
headers: response.headers,
|
|
@@ -2370,7 +2630,7 @@ var HttpClient = class {
|
|
|
2370
2630
|
});
|
|
2371
2631
|
await this.#config.hooks.onError?.({ ...context, duration, error });
|
|
2372
2632
|
this.#config.logger?.warn(
|
|
2373
|
-
`\u2190 ${response.status} ${method} ${
|
|
2633
|
+
`\u2190 ${response.status} ${method} ${request.path} (${duration} \u043C\u0441): ${error.message}`
|
|
2374
2634
|
);
|
|
2375
2635
|
throw error;
|
|
2376
2636
|
}
|
|
@@ -2380,219 +2640,56 @@ var HttpClient = class {
|
|
|
2380
2640
|
duration,
|
|
2381
2641
|
response
|
|
2382
2642
|
});
|
|
2383
|
-
this.#config.logger?.debug(`\u2190 ${response.status} ${method} ${
|
|
2384
|
-
return
|
|
2385
|
-
}
|
|
2386
|
-
/**
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
if (aborted && abort.timedOut()) {
|
|
2390
|
-
return new ItdTimeoutError({ timeout, method, path: options.path });
|
|
2391
|
-
}
|
|
2392
|
-
if (aborted) {
|
|
2393
|
-
return new ItdAbortError(`\u0417\u0430\u043F\u0440\u043E\u0441 ${method} ${options.path} \u043E\u0442\u043C\u0435\u043D\u0451\u043D`);
|
|
2394
|
-
}
|
|
2395
|
-
return new ItdNetworkError(
|
|
2396
|
-
`\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u044C ${method} ${options.path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
2397
|
-
{ method, path: options.path, cause: error }
|
|
2398
|
-
);
|
|
2399
|
-
}
|
|
2400
|
-
};
|
|
2401
|
-
|
|
2402
|
-
// src/core/plugins.ts
|
|
2403
|
-
var NO_KEYS = /* @__PURE__ */ new Set();
|
|
2404
|
-
var RESERVED_OPTION_KEYS = /* @__PURE__ */ new Set([
|
|
2405
|
-
"signal",
|
|
2406
|
-
"timeout",
|
|
2407
|
-
"headers",
|
|
2408
|
-
"retry",
|
|
2409
|
-
"method",
|
|
2410
|
-
"path",
|
|
2411
|
-
"query",
|
|
2412
|
-
"body",
|
|
2413
|
-
"skipAuth",
|
|
2414
|
-
"skipAuthRefresh",
|
|
2415
|
-
"skipQueue",
|
|
2416
|
-
"raw"
|
|
2417
|
-
]);
|
|
2418
|
-
var PluginRegistry = class {
|
|
2419
|
-
#transformers = [];
|
|
2420
|
-
#optionKeys = /* @__PURE__ */ new Set();
|
|
2421
|
-
#names = /* @__PURE__ */ new Set();
|
|
2422
|
-
/** Сколько обёрток подключено. Ноль означает, что запрос идёт прежним путём. */
|
|
2423
|
-
get size() {
|
|
2424
|
-
return this.#transformers.length;
|
|
2425
|
-
}
|
|
2426
|
-
/** Имена опций запроса, заявленные плагинами. */
|
|
2427
|
-
get optionKeys() {
|
|
2428
|
-
return this.#optionKeys.size === 0 ? NO_KEYS : this.#optionKeys;
|
|
2429
|
-
}
|
|
2430
|
-
/**
|
|
2431
|
-
* Подключает плагин.
|
|
2432
|
-
*
|
|
2433
|
-
* @throws {ItdConfigError} если плагин задан неверно, уже подключён или заявил занятое
|
|
2434
|
-
* имя опции
|
|
2435
|
-
*/
|
|
2436
|
-
add(plugin, context) {
|
|
2437
|
-
if (typeof plugin?.install !== "function") {
|
|
2438
|
-
throw new ItdConfigError("\u041F\u043B\u0430\u0433\u0438\u043D \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043E\u0431\u044A\u0435\u043A\u0442\u043E\u043C \u0441 \u043C\u0435\u0442\u043E\u0434\u043E\u043C install()");
|
|
2439
|
-
}
|
|
2440
|
-
const name = plugin.name;
|
|
2441
|
-
if (typeof name !== "string" || name.trim() === "") {
|
|
2442
|
-
throw new ItdConfigError("\u0423 \u043F\u043B\u0430\u0433\u0438\u043D\u0430 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0443\u0441\u0442\u043E\u0435 \u0438\u043C\u044F");
|
|
2443
|
-
}
|
|
2444
|
-
if (this.#names.has(name)) {
|
|
2445
|
-
throw new ItdConfigError(`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u0443\u0436\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0451\u043D`);
|
|
2446
|
-
}
|
|
2447
|
-
const keys = plugin.optionKeys ?? [];
|
|
2448
|
-
for (const key of keys) {
|
|
2449
|
-
if (typeof key !== "string" || key.trim() === "") {
|
|
2450
|
-
throw new ItdConfigError(`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u0437\u0430\u044F\u0432\u0438\u043B \u043F\u0443\u0441\u0442\u043E\u0435 \u0438\u043C\u044F \u043E\u043F\u0446\u0438\u0438`);
|
|
2451
|
-
}
|
|
2452
|
-
if (RESERVED_OPTION_KEYS.has(key)) {
|
|
2453
|
-
throw new ItdConfigError(
|
|
2454
|
-
`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u0437\u0430\u044F\u0432\u0438\u043B \u043E\u043F\u0446\u0438\u044E \xAB${key}\xBB: \u044D\u0442\u043E \u043F\u043E\u043B\u0435 \u0437\u0430\u043F\u0440\u043E\u0441\u0430, \u0438\u043C\u044F \u0437\u0430\u043D\u044F\u0442\u043E. \u0417\u0430\u043D\u044F\u0442\u044B\u0435 \u0438\u043C\u0435\u043D\u0430: ${[...RESERVED_OPTION_KEYS].join(", ")}`
|
|
2455
|
-
);
|
|
2456
|
-
}
|
|
2457
|
-
}
|
|
2458
|
-
const before = this.#transformers.length;
|
|
2459
|
-
try {
|
|
2460
|
-
plugin.install({
|
|
2461
|
-
...context,
|
|
2462
|
-
use: (transformer) => {
|
|
2463
|
-
if (typeof transformer !== "function") {
|
|
2464
|
-
throw new ItdConfigError(`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u043F\u0435\u0440\u0435\u0434\u0430\u043B \u0432 use() \u043D\u0435 \u0444\u0443\u043D\u043A\u0446\u0438\u044E`);
|
|
2465
|
-
}
|
|
2466
|
-
this.#transformers.push(transformer);
|
|
2467
|
-
}
|
|
2468
|
-
});
|
|
2469
|
-
} catch (error) {
|
|
2470
|
-
this.#transformers.length = before;
|
|
2471
|
-
throw error;
|
|
2472
|
-
}
|
|
2473
|
-
this.#names.add(name);
|
|
2474
|
-
for (const key of keys) this.#optionKeys.add(key);
|
|
2475
|
-
}
|
|
2476
|
-
/**
|
|
2477
|
-
* Прогоняет запрос через цепочку обёрток.
|
|
2478
|
-
*
|
|
2479
|
-
* Цепочка собирается на каждый запрос заново: плагин можно подключить в любой момент,
|
|
2480
|
-
* а обёрток единицы — экономить тут не на чем.
|
|
2481
|
-
*
|
|
2482
|
-
* @param execute настоящий запрос, вызывается самой внутренней обёрткой
|
|
2483
|
-
*/
|
|
2484
|
-
run(request, execute) {
|
|
2485
|
-
const chain = this.#transformers.reduceRight(
|
|
2486
|
-
(next, transformer) => (current) => transformer(current, next),
|
|
2487
|
-
execute
|
|
2488
|
-
);
|
|
2489
|
-
return chain(request);
|
|
2490
|
-
}
|
|
2491
|
-
};
|
|
2492
|
-
|
|
2493
|
-
// src/core/rate-limit.ts
|
|
2494
|
-
var RequestQueue = class {
|
|
2495
|
-
#concurrency;
|
|
2496
|
-
/** Минимальный промежуток между стартами, мс. `0` — без ограничения частоты. */
|
|
2497
|
-
#minGap;
|
|
2498
|
-
#waiting = [];
|
|
2499
|
-
#active = 0;
|
|
2500
|
-
/** Момент, раньше которого следующий запрос стартовать не должен. */
|
|
2501
|
-
#nextSlot = 0;
|
|
2502
|
-
#timer;
|
|
2503
|
-
constructor(options) {
|
|
2504
|
-
this.#concurrency = options.concurrency;
|
|
2505
|
-
this.#minGap = options.rps ? 1e3 / options.rps : 0;
|
|
2506
|
-
}
|
|
2507
|
-
/** Сколько задач выполняется прямо сейчас. */
|
|
2508
|
-
get active() {
|
|
2509
|
-
return this.#active;
|
|
2510
|
-
}
|
|
2511
|
-
/** Сколько задач ждёт очереди. */
|
|
2512
|
-
get pending() {
|
|
2513
|
-
return this.#waiting.length;
|
|
2514
|
-
}
|
|
2515
|
-
/**
|
|
2516
|
-
* Ставит задачу в очередь.
|
|
2517
|
-
*
|
|
2518
|
-
* @returns результат задачи; ошибка задачи пробрасывается без изменений
|
|
2519
|
-
*/
|
|
2520
|
-
schedule(task) {
|
|
2521
|
-
return new Promise((resolve, reject) => {
|
|
2522
|
-
const run = () => {
|
|
2523
|
-
this.#active += 1;
|
|
2524
|
-
task().then(resolve, reject).finally(() => {
|
|
2525
|
-
this.#active -= 1;
|
|
2526
|
-
this.#drain();
|
|
2527
|
-
});
|
|
2528
|
-
};
|
|
2529
|
-
this.#waiting.push({ run });
|
|
2530
|
-
this.#drain();
|
|
2531
|
-
});
|
|
2643
|
+
this.#config.logger?.debug(`\u2190 ${response.status} ${method} ${request.path} (${duration} \u043C\u0441)`);
|
|
2644
|
+
return request.raw ? payload : unwrapData(payload);
|
|
2645
|
+
};
|
|
2646
|
+
/** Итоговый URL со строкой запроса. Нужен и слою повторов — для хука `onRetry`. */
|
|
2647
|
+
buildUrl(request) {
|
|
2648
|
+
return joinUrl(this.#config.baseUrl, request.path) + buildQuery(request.query);
|
|
2532
2649
|
}
|
|
2533
2650
|
/**
|
|
2534
|
-
*
|
|
2651
|
+
* Собирает заголовки запроса.
|
|
2535
2652
|
*
|
|
2536
|
-
*
|
|
2537
|
-
*
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
if (this.#
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
const
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
}
|
|
2556
|
-
const
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
this.#drain();
|
|
2561
|
-
}
|
|
2562
|
-
};
|
|
2563
|
-
|
|
2564
|
-
// src/core/retry.ts
|
|
2565
|
-
var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
2566
|
-
function isRetryable(error, method, retryWrites) {
|
|
2567
|
-
if (error instanceof ItdAbortError) return false;
|
|
2568
|
-
const safeToRepeat = retryWrites || IDEMPOTENT_METHODS.has(method);
|
|
2569
|
-
if (error instanceof ItdApiError) {
|
|
2570
|
-
if (error.status === 429) return true;
|
|
2571
|
-
if (error.status >= 500) return safeToRepeat;
|
|
2572
|
-
return false;
|
|
2653
|
+
* Порядок важен: сначала умолчания библиотеки, затем заголовки клиента, затем то,
|
|
2654
|
+
* что добавили слои конвейера (авторизация), и только в самом конце — заголовки
|
|
2655
|
+
* конкретного вызова. Так пользователь может переопределить что угодно.
|
|
2656
|
+
*/
|
|
2657
|
+
async #buildHeaders(request, url) {
|
|
2658
|
+
const headers = new Headers();
|
|
2659
|
+
headers.set("Accept", "application/json");
|
|
2660
|
+
headers.set("X-Requested-With", "XMLHttpRequest");
|
|
2661
|
+
if (this.#config.userAgent) setHeader(headers, "User-Agent", this.#config.userAgent);
|
|
2662
|
+
if (this.#deps.getDeviceId) {
|
|
2663
|
+
setHeader(headers, "X-Device-Id", await this.#deps.getDeviceId());
|
|
2664
|
+
}
|
|
2665
|
+
for (const [name, value] of Object.entries(this.#config.headers))
|
|
2666
|
+
setHeader(headers, name, value);
|
|
2667
|
+
for (const [name, value] of Object.entries(request.layerHeaders ?? {}))
|
|
2668
|
+
setHeader(headers, name, value);
|
|
2669
|
+
if (this.#config.useCookieJar && this.#deps.cookies) {
|
|
2670
|
+
const cookie = this.#deps.cookies.getHeader(url);
|
|
2671
|
+
if (cookie) setHeader(headers, "Cookie", cookie);
|
|
2672
|
+
}
|
|
2673
|
+
for (const [name, value] of Object.entries(request.headers ?? {})) {
|
|
2674
|
+
setHeader(headers, name, value);
|
|
2675
|
+
}
|
|
2676
|
+
return headers;
|
|
2573
2677
|
}
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
const capped = Math.min(exponential, options.maxDelay);
|
|
2580
|
-
const spread = capped * options.jitter * (random() * 2 - 1);
|
|
2581
|
-
return Math.max(0, Math.round(capped + spread));
|
|
2582
|
-
}
|
|
2583
|
-
function createRetryScheduler(options, random = Math.random) {
|
|
2584
|
-
return (error, attempt, method) => {
|
|
2585
|
-
if (attempt >= options.attempts) return void 0;
|
|
2586
|
-
if (options.shouldRetry) {
|
|
2587
|
-
return options.shouldRetry(error, attempt) ? backoffDelay(attempt, options, random) : void 0;
|
|
2678
|
+
/** Превращает исключение `fetch` в понятную ошибку библиотеки. */
|
|
2679
|
+
#toTransportError(error, abort, request, method, timeout) {
|
|
2680
|
+
const aborted = error instanceof Error && error.name === "AbortError";
|
|
2681
|
+
if (aborted && abort.timedOut()) {
|
|
2682
|
+
return new ItdTimeoutError({ timeout, method, path: request.path });
|
|
2588
2683
|
}
|
|
2589
|
-
if (
|
|
2590
|
-
|
|
2591
|
-
return error.retryAfter > options.maxDelay ? void 0 : error.retryAfter;
|
|
2684
|
+
if (aborted) {
|
|
2685
|
+
return new ItdAbortError(`\u0417\u0430\u043F\u0440\u043E\u0441 ${method} ${request.path} \u043E\u0442\u043C\u0435\u043D\u0451\u043D`);
|
|
2592
2686
|
}
|
|
2593
|
-
return
|
|
2594
|
-
|
|
2595
|
-
}
|
|
2687
|
+
return new ItdNetworkError(
|
|
2688
|
+
`\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u044C ${method} ${request.path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
2689
|
+
{ method, path: request.path, cause: error }
|
|
2690
|
+
);
|
|
2691
|
+
}
|
|
2692
|
+
};
|
|
2596
2693
|
|
|
2597
2694
|
// src/notifications/type-map.ts
|
|
2598
2695
|
var NOTIFICATION_TYPE_ALIASES = Object.freeze({
|
|
@@ -3032,7 +3129,7 @@ var RealtimeTransportKind = Object.freeze({
|
|
|
3032
3129
|
var ItdRealtime = class {
|
|
3033
3130
|
#deps;
|
|
3034
3131
|
#options;
|
|
3035
|
-
#emitter
|
|
3132
|
+
#emitter;
|
|
3036
3133
|
#transport;
|
|
3037
3134
|
#maxAttempts;
|
|
3038
3135
|
#controller;
|
|
@@ -3054,6 +3151,11 @@ var ItdRealtime = class {
|
|
|
3054
3151
|
this.#options = options;
|
|
3055
3152
|
this.#maxAttempts = options.maxAttempts ?? MAX_RECONNECT_ATTEMPTS;
|
|
3056
3153
|
this.#transport = this.#createTransport();
|
|
3154
|
+
this.#emitter = new Emitter((error) => {
|
|
3155
|
+
const message = "\u041E\u0448\u0438\u0431\u043A\u0430 \u0432 \u043E\u0431\u0440\u0430\u0431\u043E\u0442\u0447\u0438\u043A\u0435 \u0441\u043E\u0431\u044B\u0442\u0438\u044F realtime";
|
|
3156
|
+
if (deps.logger) deps.logger.error(message, error);
|
|
3157
|
+
else console.error(`[itd-api] ${message}`, error);
|
|
3158
|
+
});
|
|
3057
3159
|
}
|
|
3058
3160
|
/** Текущее состояние соединения. */
|
|
3059
3161
|
get status() {
|
|
@@ -3105,6 +3207,7 @@ var ItdRealtime = class {
|
|
|
3105
3207
|
this.#controller = void 0;
|
|
3106
3208
|
this.#attempt = 0;
|
|
3107
3209
|
this.#setStatus(RealtimeStatus.Disconnected);
|
|
3210
|
+
this.#deps.onClose?.();
|
|
3108
3211
|
}
|
|
3109
3212
|
/** Снимает все подписки. Соединение при этом не закрывается. */
|
|
3110
3213
|
removeAllListeners() {
|
|
@@ -3403,6 +3506,14 @@ var Paginator = class {
|
|
|
3403
3506
|
}
|
|
3404
3507
|
};
|
|
3405
3508
|
|
|
3509
|
+
// src/types/options.ts
|
|
3510
|
+
var REQUEST_OPTION_KEYS = [
|
|
3511
|
+
"signal",
|
|
3512
|
+
"timeout",
|
|
3513
|
+
"headers",
|
|
3514
|
+
"retry"
|
|
3515
|
+
];
|
|
3516
|
+
|
|
3406
3517
|
// src/resources/base.ts
|
|
3407
3518
|
var BaseResource = class {
|
|
3408
3519
|
/** @internal */
|
|
@@ -3411,28 +3522,25 @@ var BaseResource = class {
|
|
|
3411
3522
|
this.http = http;
|
|
3412
3523
|
}
|
|
3413
3524
|
/**
|
|
3414
|
-
* Переносит
|
|
3525
|
+
* Переносит опции запроса в описание транспорта.
|
|
3415
3526
|
*
|
|
3416
|
-
*
|
|
3417
|
-
* {@link RequestOptions} и приносят с собой `limit`, `cursor`
|
|
3418
|
-
* запроса делать нечего.
|
|
3527
|
+
* Копируются только поля {@link REQUEST_OPTION_KEYS} и опции, заявленные плагинами:
|
|
3528
|
+
* параметры методов наследуют {@link RequestOptions} и приносят с собой `limit`, `cursor`
|
|
3529
|
+
* и прочее, чему в описании запроса делать нечего. Чужие опции плагинов библиотека
|
|
3419
3530
|
* не понимает, но обязана донести до обёрток нетронутыми.
|
|
3420
3531
|
*/
|
|
3421
3532
|
requestOptions(options) {
|
|
3422
3533
|
if (!options) return {};
|
|
3423
|
-
const result = {
|
|
3424
|
-
...options.signal !== void 0 ? { signal: options.signal } : {},
|
|
3425
|
-
...options.timeout !== void 0 ? { timeout: options.timeout } : {},
|
|
3426
|
-
...options.headers !== void 0 ? { headers: options.headers } : {},
|
|
3427
|
-
...options.retry !== void 0 ? { retry: options.retry } : {}
|
|
3428
|
-
};
|
|
3429
|
-
const pluginKeys = this.http.pluginOptionKeys;
|
|
3430
|
-
if (pluginKeys.size === 0) return result;
|
|
3431
3534
|
const source = options;
|
|
3432
|
-
const
|
|
3535
|
+
const result = {};
|
|
3536
|
+
for (const key of REQUEST_OPTION_KEYS) {
|
|
3537
|
+
const value = source[key];
|
|
3538
|
+
if (value !== void 0) result[key] = value;
|
|
3539
|
+
}
|
|
3540
|
+
const pluginKeys = this.http.pluginOptionKeys;
|
|
3433
3541
|
for (const key of pluginKeys) {
|
|
3434
3542
|
const value = source[key];
|
|
3435
|
-
if (value !== void 0)
|
|
3543
|
+
if (value !== void 0) result[key] = value;
|
|
3436
3544
|
}
|
|
3437
3545
|
return result;
|
|
3438
3546
|
}
|
|
@@ -3452,6 +3560,42 @@ var BaseResource = class {
|
|
|
3452
3560
|
...options?.start !== void 0 ? { start: options.start } : {}
|
|
3453
3561
|
});
|
|
3454
3562
|
}
|
|
3563
|
+
/**
|
|
3564
|
+
* Собирает пару «загрузка страницы + перебор» из одного описания.
|
|
3565
|
+
*
|
|
3566
|
+
* Путь, параметры запроса и разбор ответа задаются один раз; `list` и `iterate`
|
|
3567
|
+
* строятся из них.
|
|
3568
|
+
*
|
|
3569
|
+
* @example
|
|
3570
|
+
* ```ts
|
|
3571
|
+
* #feed = this.paginated<Post, FeedParams>({
|
|
3572
|
+
* path: () => '/api/posts',
|
|
3573
|
+
* query: (p) => ({ tab: p.tab, limit: p.limit }),
|
|
3574
|
+
* start: (p) => (p.cursor ? { cursor: p.cursor } : {}),
|
|
3575
|
+
* read: (body) => readCursorPage<Post>(body, 'posts'),
|
|
3576
|
+
* mode: PaginationMode.Cursor,
|
|
3577
|
+
* });
|
|
3578
|
+
* ```
|
|
3579
|
+
*/
|
|
3580
|
+
paginated(spec) {
|
|
3581
|
+
const load = async (params, state) => {
|
|
3582
|
+
const body = await this.http.request({
|
|
3583
|
+
method: "GET",
|
|
3584
|
+
path: spec.path(params),
|
|
3585
|
+
query: withPageState(spec.query(params), state),
|
|
3586
|
+
...this.requestOptions(params)
|
|
3587
|
+
});
|
|
3588
|
+
return spec.read(body, state);
|
|
3589
|
+
};
|
|
3590
|
+
return {
|
|
3591
|
+
list: (params) => load(params, spec.start(params)),
|
|
3592
|
+
iterate: (params) => this.paginate(spec.mode, (state) => load(params, state), {
|
|
3593
|
+
...params.maxPages !== void 0 ? { maxPages: params.maxPages } : {},
|
|
3594
|
+
...params.signal !== void 0 ? { signal: params.signal } : {},
|
|
3595
|
+
start: spec.start(params)
|
|
3596
|
+
})
|
|
3597
|
+
};
|
|
3598
|
+
}
|
|
3455
3599
|
};
|
|
3456
3600
|
function withPageState(query, state) {
|
|
3457
3601
|
return {
|
|
@@ -3769,6 +3913,14 @@ var AuthResource = class extends BaseResource {
|
|
|
3769
3913
|
// src/resources/comments.ts
|
|
3770
3914
|
var CommentsResource = class extends BaseResource {
|
|
3771
3915
|
#uploadFiles;
|
|
3916
|
+
/** Ответы на комментарий: `/api/comments/{id}/replies`, постраничная пагинация. */
|
|
3917
|
+
#replies = this.paginated({
|
|
3918
|
+
path: (p) => `/api/comments/${encodePathSegment(p.commentId, "commentId")}/replies`,
|
|
3919
|
+
query: (p) => ({ limit: p.limit }),
|
|
3920
|
+
start: (p) => p.page !== void 0 ? { page: p.page } : {},
|
|
3921
|
+
read: (body) => readPagedPage(body, "replies"),
|
|
3922
|
+
mode: PaginationMode.Page
|
|
3923
|
+
});
|
|
3772
3924
|
constructor(http, deps) {
|
|
3773
3925
|
super(http);
|
|
3774
3926
|
this.#uploadFiles = deps.uploadFiles;
|
|
@@ -3778,31 +3930,12 @@ var CommentsResource = class extends BaseResource {
|
|
|
3778
3930
|
*
|
|
3779
3931
|
* Здесь пагинация **постраничная**, в отличие от комментариев к посту, где курсорная.
|
|
3780
3932
|
*/
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
method: "GET",
|
|
3784
|
-
path: `/api/comments/${encodePathSegment(commentId, "commentId")}/replies`,
|
|
3785
|
-
query: { limit: params.limit, page: params.page },
|
|
3786
|
-
...this.requestOptions(params)
|
|
3787
|
-
});
|
|
3788
|
-
return readPagedPage(body, "replies");
|
|
3933
|
+
replies(commentId, params = {}) {
|
|
3934
|
+
return this.#replies.list({ ...params, commentId });
|
|
3789
3935
|
}
|
|
3790
3936
|
/** Перебирает ответы на комментарий. */
|
|
3791
3937
|
iterateReplies(commentId, params = {}) {
|
|
3792
|
-
|
|
3793
|
-
return this.paginate(
|
|
3794
|
-
PaginationMode.Page,
|
|
3795
|
-
async (state) => {
|
|
3796
|
-
const body = await this.http.request({
|
|
3797
|
-
method: "GET",
|
|
3798
|
-
path,
|
|
3799
|
-
query: withPageState({ limit: params.limit }, state),
|
|
3800
|
-
...this.requestOptions(params)
|
|
3801
|
-
});
|
|
3802
|
-
return readPagedPage(body, "replies");
|
|
3803
|
-
},
|
|
3804
|
-
{ ...params, ...params.page !== void 0 ? { start: { page: params.page } } : {} }
|
|
3805
|
-
);
|
|
3938
|
+
return this.#replies.iterate({ ...params, commentId });
|
|
3806
3939
|
}
|
|
3807
3940
|
/**
|
|
3808
3941
|
* Отвечает на комментарий.
|
|
@@ -3937,19 +4070,14 @@ function assertAllowedMime(mimeType, filename) {
|
|
|
3937
4070
|
var DEFAULT_UPLOAD_TIMEOUT = 3e5;
|
|
3938
4071
|
var FilesResource = class extends BaseResource {
|
|
3939
4072
|
#readFile;
|
|
4073
|
+
/**
|
|
4074
|
+
* @param deps.readFile чтение файлов с диска. Передаёт точка входа `itd-api/node`;
|
|
4075
|
+
* в основном бандле его нет, чтобы браузерные сборщики не пытались разрешить `node:fs`.
|
|
4076
|
+
*/
|
|
3940
4077
|
constructor(http, deps = {}) {
|
|
3941
4078
|
super(http);
|
|
3942
4079
|
this.#readFile = deps.readFile;
|
|
3943
4080
|
}
|
|
3944
|
-
/**
|
|
3945
|
-
* Подключает чтение файлов с диска.
|
|
3946
|
-
*
|
|
3947
|
-
* Вызывается точкой входа `itd-api/node`; в основном бандле работы с файловой
|
|
3948
|
-
* системой нет, чтобы браузерные сборщики не пытались разрешить `node:fs`.
|
|
3949
|
-
*/
|
|
3950
|
-
setFileReader(readFile) {
|
|
3951
|
-
this.#readFile = readFile;
|
|
3952
|
-
}
|
|
3953
4081
|
/**
|
|
3954
4082
|
* Загружает файл и возвращает его идентификатор.
|
|
3955
4083
|
*
|
|
@@ -4059,8 +4187,16 @@ var FilesResource = class extends BaseResource {
|
|
|
4059
4187
|
}
|
|
4060
4188
|
};
|
|
4061
4189
|
|
|
4062
|
-
// src/resources/
|
|
4190
|
+
// src/resources/hashtags.ts
|
|
4063
4191
|
var HashtagsResource = class extends BaseResource {
|
|
4192
|
+
/** Посты по хэштегу: `/api/hashtags/{tag}/posts`, курсорная пагинация. */
|
|
4193
|
+
#posts = this.paginated({
|
|
4194
|
+
path: (p) => `/api/hashtags/${encodePathSegment(p.tag, "tag")}/posts`,
|
|
4195
|
+
query: (p) => ({ limit: p.limit }),
|
|
4196
|
+
start: (p) => p.cursor ? { cursor: p.cursor } : {},
|
|
4197
|
+
read: (body) => readCursorPage(body, "posts"),
|
|
4198
|
+
mode: PaginationMode.Cursor
|
|
4199
|
+
});
|
|
4064
4200
|
/**
|
|
4065
4201
|
* Ищет хэштеги.
|
|
4066
4202
|
*
|
|
@@ -4087,214 +4223,52 @@ var HashtagsResource = class extends BaseResource {
|
|
|
4087
4223
|
}
|
|
4088
4224
|
/**
|
|
4089
4225
|
* Загружает страницу постов по хэштегу.
|
|
4090
|
-
*
|
|
4091
|
-
* @param tag название без решётки; кодируется автоматически, поэтому кириллица
|
|
4092
|
-
* и пробелы допустимы
|
|
4093
|
-
*/
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
});
|
|
4101
|
-
return readCursorPage(body, "posts");
|
|
4102
|
-
}
|
|
4103
|
-
/** Перебирает посты по хэштегу. */
|
|
4104
|
-
iteratePosts(tag, params = {}) {
|
|
4105
|
-
const path = `/api/hashtags/${encodePathSegment(tag, "tag")}/posts`;
|
|
4106
|
-
return this.paginate(
|
|
4107
|
-
PaginationMode.Cursor,
|
|
4108
|
-
async (state) => {
|
|
4109
|
-
const body = await this.http.request({
|
|
4110
|
-
method: "GET",
|
|
4111
|
-
path,
|
|
4112
|
-
query: withPageState({ limit: params.limit }, state),
|
|
4113
|
-
...this.requestOptions(params)
|
|
4114
|
-
});
|
|
4115
|
-
return readCursorPage(body, "posts");
|
|
4116
|
-
},
|
|
4117
|
-
{ ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
|
|
4118
|
-
);
|
|
4119
|
-
}
|
|
4120
|
-
};
|
|
4121
|
-
var SearchResource = class extends BaseResource {
|
|
4122
|
-
/**
|
|
4123
|
-
* Ищет пользователей и хэштеги одним запросом.
|
|
4124
|
-
*
|
|
4125
|
-
* @example
|
|
4126
|
-
* ```ts
|
|
4127
|
-
* const { users, hashtags } = await itd.search.all('арт');
|
|
4128
|
-
* ```
|
|
4129
|
-
*/
|
|
4130
|
-
async all(query, options = {}) {
|
|
4131
|
-
const body = await this.http.request({
|
|
4132
|
-
method: "GET",
|
|
4133
|
-
path: "/api/search",
|
|
4134
|
-
query: { q: query },
|
|
4135
|
-
...this.requestOptions(options)
|
|
4136
|
-
});
|
|
4137
|
-
return {
|
|
4138
|
-
users: pickArray(body, "users"),
|
|
4139
|
-
hashtags: pickArray(body, "hashtags")
|
|
4140
|
-
};
|
|
4141
|
-
}
|
|
4142
|
-
};
|
|
4143
|
-
var ReportsResource = class extends BaseResource {
|
|
4144
|
-
/**
|
|
4145
|
-
* Отправляет жалобу.
|
|
4146
|
-
*
|
|
4147
|
-
* Повторная жалоба на тот же объект отклоняется сервером с сообщением
|
|
4148
|
-
* «Вы уже отправляли жалобу на этот контент».
|
|
4149
|
-
*
|
|
4150
|
-
* @example
|
|
4151
|
-
* ```ts
|
|
4152
|
-
* await itd.reports.create(report.post(postId).reason('spam'));
|
|
4153
|
-
* await itd.reports.create({ targetType: 'user', targetId, reason: 'fraud' });
|
|
4154
|
-
* ```
|
|
4155
|
-
*/
|
|
4156
|
-
create(input, options = {}) {
|
|
4157
|
-
const data = resolveReport(input);
|
|
4158
|
-
return this.http.request({
|
|
4159
|
-
method: "POST",
|
|
4160
|
-
path: "/api/reports",
|
|
4161
|
-
body: data,
|
|
4162
|
-
...this.requestOptions(options)
|
|
4163
|
-
});
|
|
4164
|
-
}
|
|
4165
|
-
};
|
|
4166
|
-
var VerificationResource = class extends BaseResource {
|
|
4167
|
-
/** Загружает статус заявки. Значение `none` означает, что заявка не подавалась. */
|
|
4168
|
-
status(options = {}) {
|
|
4169
|
-
return this.http.request({
|
|
4170
|
-
method: "GET",
|
|
4171
|
-
path: "/api/verification/status",
|
|
4172
|
-
...this.requestOptions(options)
|
|
4173
|
-
});
|
|
4174
|
-
}
|
|
4175
|
-
/** Подаёт заявку на верификацию с видео. */
|
|
4176
|
-
submit(videoUrl, options = {}) {
|
|
4177
|
-
return this.http.request({
|
|
4178
|
-
method: "POST",
|
|
4179
|
-
path: "/api/verification/submit",
|
|
4180
|
-
body: { videoUrl },
|
|
4181
|
-
...this.requestOptions(options)
|
|
4182
|
-
});
|
|
4183
|
-
}
|
|
4184
|
-
};
|
|
4185
|
-
var SubscriptionResource = class extends BaseResource {
|
|
4186
|
-
/** Загружает состояние подписки и её цену. */
|
|
4187
|
-
status(options = {}) {
|
|
4188
|
-
return this.http.request({
|
|
4189
|
-
method: "GET",
|
|
4190
|
-
// Завершающий слэш обязателен.
|
|
4191
|
-
path: "/api/v1/subscription/",
|
|
4192
|
-
...this.requestOptions(options)
|
|
4193
|
-
});
|
|
4194
|
-
}
|
|
4195
|
-
/**
|
|
4196
|
-
* Запускает оплату подписки.
|
|
4197
|
-
*
|
|
4198
|
-
* Форма ответа в документации API не описана, поэтому тип результата не уточняется.
|
|
4199
|
-
*/
|
|
4200
|
-
pay(options = {}) {
|
|
4201
|
-
return this.http.request({
|
|
4202
|
-
method: "POST",
|
|
4203
|
-
path: "/api/v1/subscription/pay",
|
|
4204
|
-
...this.requestOptions(options)
|
|
4205
|
-
});
|
|
4206
|
-
}
|
|
4207
|
-
/** Включает или отключает автопродление. */
|
|
4208
|
-
setAutoRenewal(enabled, options = {}) {
|
|
4209
|
-
return this.http.request({
|
|
4210
|
-
method: "POST",
|
|
4211
|
-
path: "/api/v1/subscription/auto-renewal",
|
|
4212
|
-
body: { enabled },
|
|
4213
|
-
...this.requestOptions(options)
|
|
4214
|
-
});
|
|
4215
|
-
}
|
|
4216
|
-
/** Запускает привязку карты. */
|
|
4217
|
-
bindCard(options = {}) {
|
|
4218
|
-
return this.http.request({
|
|
4219
|
-
method: "POST",
|
|
4220
|
-
path: "/api/v1/subscription/bind-card",
|
|
4221
|
-
...this.requestOptions(options)
|
|
4222
|
-
});
|
|
4223
|
-
}
|
|
4224
|
-
/** Загружает список способов оплаты. Пустой массив, если карт нет. */
|
|
4225
|
-
async methods(options = {}) {
|
|
4226
|
-
const body = await this.http.request({
|
|
4227
|
-
method: "GET",
|
|
4228
|
-
path: "/api/v1/subscription/methods",
|
|
4229
|
-
...this.requestOptions(options)
|
|
4230
|
-
});
|
|
4231
|
-
return Array.isArray(body) ? body : [];
|
|
4232
|
-
}
|
|
4233
|
-
/** Делает способ оплаты основным. */
|
|
4234
|
-
setDefaultMethod(methodId, options = {}) {
|
|
4235
|
-
return this.http.request({
|
|
4236
|
-
method: "POST",
|
|
4237
|
-
path: `/api/v1/subscription/methods/${encodePathSegment(methodId, "methodId")}/default`,
|
|
4238
|
-
...this.requestOptions(options)
|
|
4239
|
-
});
|
|
4240
|
-
}
|
|
4241
|
-
/** Удаляет способ оплаты. */
|
|
4242
|
-
removeMethod(methodId, options = {}) {
|
|
4243
|
-
return this.http.request({
|
|
4244
|
-
method: "DELETE",
|
|
4245
|
-
path: `/api/v1/subscription/methods/${encodePathSegment(methodId, "methodId")}`,
|
|
4246
|
-
...this.requestOptions(options)
|
|
4247
|
-
});
|
|
4248
|
-
}
|
|
4249
|
-
};
|
|
4250
|
-
var PlatformResource = class extends BaseResource {
|
|
4251
|
-
/** Загружает журнал изменений. */
|
|
4252
|
-
async changelog(options = {}) {
|
|
4253
|
-
const body = await this.http.request({
|
|
4254
|
-
method: "GET",
|
|
4255
|
-
path: "/api/platform/changelog",
|
|
4256
|
-
...this.requestOptions(options)
|
|
4257
|
-
});
|
|
4258
|
-
return Array.isArray(body) ? body : [];
|
|
4259
|
-
}
|
|
4260
|
-
/** Загружает анонсы платформы. */
|
|
4261
|
-
async announcements(options = {}) {
|
|
4262
|
-
const body = await this.http.request({
|
|
4263
|
-
method: "GET",
|
|
4264
|
-
path: "/api/platform/announcements",
|
|
4265
|
-
...this.requestOptions(options)
|
|
4266
|
-
});
|
|
4267
|
-
return pickArray(body, "announcements");
|
|
4268
|
-
}
|
|
4269
|
-
/** Загружает баннер текущего события — виджет «портал». */
|
|
4270
|
-
portal(options = {}) {
|
|
4271
|
-
return this.http.request({
|
|
4272
|
-
method: "GET",
|
|
4273
|
-
path: "/api/v1/portal",
|
|
4274
|
-
...this.requestOptions(options)
|
|
4275
|
-
});
|
|
4226
|
+
*
|
|
4227
|
+
* @param tag название без решётки; кодируется автоматически, поэтому кириллица
|
|
4228
|
+
* и пробелы допустимы
|
|
4229
|
+
*/
|
|
4230
|
+
posts(tag, params = {}) {
|
|
4231
|
+
return this.#posts.list({ ...params, tag });
|
|
4232
|
+
}
|
|
4233
|
+
/** Перебирает посты по хэштегу. */
|
|
4234
|
+
iteratePosts(tag, params = {}) {
|
|
4235
|
+
return this.#posts.iterate({ ...params, tag });
|
|
4276
4236
|
}
|
|
4277
4237
|
};
|
|
4278
4238
|
|
|
4279
4239
|
// src/resources/notifications.ts
|
|
4240
|
+
var NOTIFICATION_SETTING_KEYS = [
|
|
4241
|
+
"enabled",
|
|
4242
|
+
"sound",
|
|
4243
|
+
"follows",
|
|
4244
|
+
"wallPosts",
|
|
4245
|
+
"likes",
|
|
4246
|
+
"comments",
|
|
4247
|
+
"mentions"
|
|
4248
|
+
];
|
|
4280
4249
|
var READ_BATCH_SIZE = 20;
|
|
4281
4250
|
function readSettings(body) {
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
follows: pickBoolean(body, "follows", true),
|
|
4286
|
-
wallPosts: pickBoolean(body, "wallPosts", true),
|
|
4287
|
-
likes: pickBoolean(body, "likes", true),
|
|
4288
|
-
comments: pickBoolean(body, "comments", true),
|
|
4289
|
-
mentions: pickBoolean(body, "mentions", true)
|
|
4290
|
-
};
|
|
4251
|
+
const settings = {};
|
|
4252
|
+
for (const key of NOTIFICATION_SETTING_KEYS) settings[key] = pickBoolean(body, key, true);
|
|
4253
|
+
return settings;
|
|
4291
4254
|
}
|
|
4292
4255
|
var NotificationsResource = class extends BaseResource {
|
|
4256
|
+
/** Уведомления: `/api/notifications/`, пагинация по смещению. */
|
|
4257
|
+
#list = this.paginated({
|
|
4258
|
+
// Завершающий слэш обязателен: без него сервер отвечает ошибкой.
|
|
4259
|
+
path: () => "/api/notifications/",
|
|
4260
|
+
query: (p) => ({ limit: p.limit }),
|
|
4261
|
+
start: (p) => ({ offset: p.offset ?? 0 }),
|
|
4262
|
+
read: (body, state) => {
|
|
4263
|
+
const page = readOffsetPage(body, "notifications", state.offset ?? 0);
|
|
4264
|
+
return { ...page, items: page.items.map(normalizeNotification) };
|
|
4265
|
+
},
|
|
4266
|
+
mode: PaginationMode.Offset
|
|
4267
|
+
});
|
|
4293
4268
|
/**
|
|
4294
4269
|
* Загружает страницу уведомлений.
|
|
4295
4270
|
*
|
|
4296
|
-
* Пагинация здесь основана на смещении.
|
|
4297
|
-
* и притворяется, что это курсор; библиотека отдаёт честное число.
|
|
4271
|
+
* Пагинация здесь основана на смещении.
|
|
4298
4272
|
*
|
|
4299
4273
|
* @example
|
|
4300
4274
|
* ```ts
|
|
@@ -4303,19 +4277,7 @@ var NotificationsResource = class extends BaseResource {
|
|
|
4303
4277
|
* ```
|
|
4304
4278
|
*/
|
|
4305
4279
|
list(params = {}) {
|
|
4306
|
-
return this.#
|
|
4307
|
-
}
|
|
4308
|
-
/** Общая загрузка страницы для {@link list} и {@link iterate}. */
|
|
4309
|
-
async #loadPage(params, offset) {
|
|
4310
|
-
const body = await this.http.request({
|
|
4311
|
-
method: "GET",
|
|
4312
|
-
// Завершающий слэш обязателен: без него сервер отвечает ошибкой.
|
|
4313
|
-
path: "/api/notifications/",
|
|
4314
|
-
query: { limit: params.limit, offset },
|
|
4315
|
-
...this.requestOptions(params)
|
|
4316
|
-
});
|
|
4317
|
-
const page = readOffsetPage(body, "notifications", offset);
|
|
4318
|
-
return { ...page, items: page.items.map(normalizeNotification) };
|
|
4280
|
+
return this.#list.list(params);
|
|
4319
4281
|
}
|
|
4320
4282
|
/**
|
|
4321
4283
|
* Перебирает уведомления.
|
|
@@ -4328,11 +4290,7 @@ var NotificationsResource = class extends BaseResource {
|
|
|
4328
4290
|
* ```
|
|
4329
4291
|
*/
|
|
4330
4292
|
iterate(params = {}) {
|
|
4331
|
-
return this.
|
|
4332
|
-
PaginationMode.Offset,
|
|
4333
|
-
(state) => this.#loadPage(params, state.offset ?? 0),
|
|
4334
|
-
{ ...params, ...params.offset !== void 0 ? { start: { offset: params.offset } } : {} }
|
|
4335
|
-
);
|
|
4293
|
+
return this.#list.iterate(params);
|
|
4336
4294
|
}
|
|
4337
4295
|
/** Загружает число непрочитанных уведомлений. */
|
|
4338
4296
|
async count(options = {}) {
|
|
@@ -4404,15 +4362,7 @@ var NotificationsResource = class extends BaseResource {
|
|
|
4404
4362
|
*/
|
|
4405
4363
|
async updateSettings(input, options = {}) {
|
|
4406
4364
|
const payload = {};
|
|
4407
|
-
for (const key of
|
|
4408
|
-
"enabled",
|
|
4409
|
-
"sound",
|
|
4410
|
-
"follows",
|
|
4411
|
-
"wallPosts",
|
|
4412
|
-
"likes",
|
|
4413
|
-
"comments",
|
|
4414
|
-
"mentions"
|
|
4415
|
-
]) {
|
|
4365
|
+
for (const key of NOTIFICATION_SETTING_KEYS) {
|
|
4416
4366
|
const value = input[key];
|
|
4417
4367
|
if (value !== void 0) payload[key] = value;
|
|
4418
4368
|
}
|
|
@@ -4426,9 +4376,74 @@ var NotificationsResource = class extends BaseResource {
|
|
|
4426
4376
|
}
|
|
4427
4377
|
};
|
|
4428
4378
|
|
|
4379
|
+
// src/resources/platform.ts
|
|
4380
|
+
var PlatformResource = class extends BaseResource {
|
|
4381
|
+
/** Загружает журнал изменений. */
|
|
4382
|
+
async changelog(options = {}) {
|
|
4383
|
+
const body = await this.http.request({
|
|
4384
|
+
method: "GET",
|
|
4385
|
+
path: "/api/platform/changelog",
|
|
4386
|
+
...this.requestOptions(options)
|
|
4387
|
+
});
|
|
4388
|
+
return Array.isArray(body) ? body : [];
|
|
4389
|
+
}
|
|
4390
|
+
/** Загружает анонсы платформы. */
|
|
4391
|
+
async announcements(options = {}) {
|
|
4392
|
+
const body = await this.http.request({
|
|
4393
|
+
method: "GET",
|
|
4394
|
+
path: "/api/platform/announcements",
|
|
4395
|
+
...this.requestOptions(options)
|
|
4396
|
+
});
|
|
4397
|
+
return pickArray(body, "announcements");
|
|
4398
|
+
}
|
|
4399
|
+
/** Загружает баннер текущего события — виджет «портал». */
|
|
4400
|
+
portal(options = {}) {
|
|
4401
|
+
return this.http.request({
|
|
4402
|
+
method: "GET",
|
|
4403
|
+
path: "/api/v1/portal",
|
|
4404
|
+
...this.requestOptions(options)
|
|
4405
|
+
});
|
|
4406
|
+
}
|
|
4407
|
+
};
|
|
4408
|
+
|
|
4429
4409
|
// src/resources/posts.ts
|
|
4410
|
+
function cursorStart(params) {
|
|
4411
|
+
return params.cursor ? { cursor: params.cursor } : {};
|
|
4412
|
+
}
|
|
4430
4413
|
var PostsResource = class extends BaseResource {
|
|
4431
4414
|
#uploadFiles;
|
|
4415
|
+
/** Лента: `/api/posts`, курсорная пагинация. */
|
|
4416
|
+
#feed = this.paginated({
|
|
4417
|
+
path: () => "/api/posts",
|
|
4418
|
+
query: (p) => ({ tab: p.tab, limit: p.limit }),
|
|
4419
|
+
start: cursorStart,
|
|
4420
|
+
read: (body) => readCursorPage(body, "posts"),
|
|
4421
|
+
mode: PaginationMode.Cursor
|
|
4422
|
+
});
|
|
4423
|
+
/** Стена пользователя: `/api/posts/user/{user}`. */
|
|
4424
|
+
#wall = this.paginated({
|
|
4425
|
+
path: (p) => `/api/posts/user/${encodePathSegment(p.user, "user")}`,
|
|
4426
|
+
query: (p) => ({ limit: p.limit, sort: p.sort, pinnedPostId: p.pinnedPostId }),
|
|
4427
|
+
start: cursorStart,
|
|
4428
|
+
read: (body) => readCursorPage(body, "posts"),
|
|
4429
|
+
mode: PaginationMode.Cursor
|
|
4430
|
+
});
|
|
4431
|
+
/** Понравившиеся посты пользователя: `/api/posts/user/{user}/liked`. */
|
|
4432
|
+
#liked = this.paginated({
|
|
4433
|
+
path: (p) => `/api/posts/user/${encodePathSegment(p.user, "user")}/liked`,
|
|
4434
|
+
query: (p) => ({ limit: p.limit }),
|
|
4435
|
+
start: cursorStart,
|
|
4436
|
+
read: (body) => readCursorPage(body, "posts"),
|
|
4437
|
+
mode: PaginationMode.Cursor
|
|
4438
|
+
});
|
|
4439
|
+
/** Комментарии к посту: курсор лежит рядом со списком, поэтому свой reader. */
|
|
4440
|
+
#comments = this.paginated({
|
|
4441
|
+
path: (p) => `/api/posts/${encodePathSegment(p.postId, "postId")}/comments`,
|
|
4442
|
+
query: (p) => ({ limit: p.limit, sort: p.sort }),
|
|
4443
|
+
start: cursorStart,
|
|
4444
|
+
read: (body) => readFlatCursorPage(body, "comments"),
|
|
4445
|
+
mode: PaginationMode.Cursor
|
|
4446
|
+
});
|
|
4432
4447
|
constructor(http, deps) {
|
|
4433
4448
|
super(http);
|
|
4434
4449
|
this.#uploadFiles = deps.uploadFiles;
|
|
@@ -4442,14 +4457,8 @@ var PostsResource = class extends BaseResource {
|
|
|
4442
4457
|
* const next = await itd.posts.list({ tab: FeedTab.Following, cursor: page.nextCursor ?? undefined });
|
|
4443
4458
|
* ```
|
|
4444
4459
|
*/
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
method: "GET",
|
|
4448
|
-
path: "/api/posts",
|
|
4449
|
-
query: { tab: params.tab, limit: params.limit, cursor: params.cursor },
|
|
4450
|
-
...this.requestOptions(params)
|
|
4451
|
-
});
|
|
4452
|
-
return readCursorPage(body, "posts");
|
|
4460
|
+
list(params = {}) {
|
|
4461
|
+
return this.#feed.list(params);
|
|
4453
4462
|
}
|
|
4454
4463
|
/**
|
|
4455
4464
|
* Перебирает ленту, сама подставляя курсоры.
|
|
@@ -4462,19 +4471,7 @@ var PostsResource = class extends BaseResource {
|
|
|
4462
4471
|
* ```
|
|
4463
4472
|
*/
|
|
4464
4473
|
iterate(params = {}) {
|
|
4465
|
-
return this.
|
|
4466
|
-
PaginationMode.Cursor,
|
|
4467
|
-
async (state) => {
|
|
4468
|
-
const body = await this.http.request({
|
|
4469
|
-
method: "GET",
|
|
4470
|
-
path: "/api/posts",
|
|
4471
|
-
query: withPageState({ tab: params.tab, limit: params.limit }, state),
|
|
4472
|
-
...this.requestOptions(params)
|
|
4473
|
-
});
|
|
4474
|
-
return readCursorPage(body, "posts");
|
|
4475
|
-
},
|
|
4476
|
-
{ ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
|
|
4477
|
-
);
|
|
4474
|
+
return this.#feed.iterate(params);
|
|
4478
4475
|
}
|
|
4479
4476
|
/**
|
|
4480
4477
|
* Публикует пост.
|
|
@@ -4628,66 +4625,20 @@ var PostsResource = class extends BaseResource {
|
|
|
4628
4625
|
*
|
|
4629
4626
|
* Принимает и UUID, и имя пользователя.
|
|
4630
4627
|
*/
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
method: "GET",
|
|
4634
|
-
path: `/api/posts/user/${encodePathSegment(user, "user")}`,
|
|
4635
|
-
query: {
|
|
4636
|
-
limit: params.limit,
|
|
4637
|
-
cursor: params.cursor,
|
|
4638
|
-
sort: params.sort,
|
|
4639
|
-
pinnedPostId: params.pinnedPostId
|
|
4640
|
-
},
|
|
4641
|
-
...this.requestOptions(params)
|
|
4642
|
-
});
|
|
4643
|
-
return readCursorPage(body, "posts");
|
|
4628
|
+
byUser(user, params = {}) {
|
|
4629
|
+
return this.#wall.list({ ...params, user });
|
|
4644
4630
|
}
|
|
4645
4631
|
/** Перебирает стену пользователя. Что именно в неё входит — см. {@link byUser}. */
|
|
4646
4632
|
iterateByUser(user, params = {}) {
|
|
4647
|
-
|
|
4648
|
-
return this.paginate(
|
|
4649
|
-
PaginationMode.Cursor,
|
|
4650
|
-
async (state) => {
|
|
4651
|
-
const body = await this.http.request({
|
|
4652
|
-
method: "GET",
|
|
4653
|
-
path,
|
|
4654
|
-
query: withPageState(
|
|
4655
|
-
{ limit: params.limit, sort: params.sort, pinnedPostId: params.pinnedPostId },
|
|
4656
|
-
state
|
|
4657
|
-
),
|
|
4658
|
-
...this.requestOptions(params)
|
|
4659
|
-
});
|
|
4660
|
-
return readCursorPage(body, "posts");
|
|
4661
|
-
},
|
|
4662
|
-
{ ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
|
|
4663
|
-
);
|
|
4633
|
+
return this.#wall.iterate({ ...params, user });
|
|
4664
4634
|
}
|
|
4665
4635
|
/** Загружает страницу постов, которые пользователь отметил реакцией. */
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
method: "GET",
|
|
4669
|
-
path: `/api/posts/user/${encodePathSegment(user, "user")}/liked`,
|
|
4670
|
-
query: { limit: params.limit, cursor: params.cursor },
|
|
4671
|
-
...this.requestOptions(params)
|
|
4672
|
-
});
|
|
4673
|
-
return readCursorPage(body, "posts");
|
|
4636
|
+
likedByUser(user, params = {}) {
|
|
4637
|
+
return this.#liked.list({ ...params, user });
|
|
4674
4638
|
}
|
|
4675
4639
|
/** Перебирает посты, которые пользователь отметил реакцией. */
|
|
4676
4640
|
iterateLikedByUser(user, params = {}) {
|
|
4677
|
-
|
|
4678
|
-
return this.paginate(
|
|
4679
|
-
PaginationMode.Cursor,
|
|
4680
|
-
async (state) => {
|
|
4681
|
-
const body = await this.http.request({
|
|
4682
|
-
method: "GET",
|
|
4683
|
-
path,
|
|
4684
|
-
query: withPageState({ limit: params.limit }, state),
|
|
4685
|
-
...this.requestOptions(params)
|
|
4686
|
-
});
|
|
4687
|
-
return readCursorPage(body, "posts");
|
|
4688
|
-
},
|
|
4689
|
-
{ ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
|
|
4690
|
-
);
|
|
4641
|
+
return this.#liked.iterate({ ...params, user });
|
|
4691
4642
|
}
|
|
4692
4643
|
/**
|
|
4693
4644
|
* Загружает страницу комментариев к посту.
|
|
@@ -4695,31 +4646,12 @@ var PostsResource = class extends BaseResource {
|
|
|
4695
4646
|
* У этого эндпоинта курсор и признак продолжения лежат рядом со списком, а не внутри
|
|
4696
4647
|
* объекта `pagination`, как у остальных, — разница скрыта внутри.
|
|
4697
4648
|
*/
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
method: "GET",
|
|
4701
|
-
path: `/api/posts/${encodePathSegment(postId, "postId")}/comments`,
|
|
4702
|
-
query: { limit: params.limit, cursor: params.cursor, sort: params.sort },
|
|
4703
|
-
...this.requestOptions(params)
|
|
4704
|
-
});
|
|
4705
|
-
return readFlatCursorPage(body, "comments");
|
|
4649
|
+
comments(postId, params = {}) {
|
|
4650
|
+
return this.#comments.list({ ...params, postId });
|
|
4706
4651
|
}
|
|
4707
4652
|
/** Перебирает комментарии к посту. */
|
|
4708
4653
|
iterateComments(postId, params = {}) {
|
|
4709
|
-
|
|
4710
|
-
return this.paginate(
|
|
4711
|
-
PaginationMode.Cursor,
|
|
4712
|
-
async (state) => {
|
|
4713
|
-
const body = await this.http.request({
|
|
4714
|
-
method: "GET",
|
|
4715
|
-
path,
|
|
4716
|
-
query: withPageState({ limit: params.limit, sort: params.sort }, state),
|
|
4717
|
-
...this.requestOptions(params)
|
|
4718
|
-
});
|
|
4719
|
-
return readFlatCursorPage(body, "comments");
|
|
4720
|
-
},
|
|
4721
|
-
{ ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
|
|
4722
|
-
);
|
|
4654
|
+
return this.#comments.iterate({ ...params, postId });
|
|
4723
4655
|
}
|
|
4724
4656
|
/**
|
|
4725
4657
|
* Комментирует пост.
|
|
@@ -4764,6 +4696,122 @@ var PostsResource = class extends BaseResource {
|
|
|
4764
4696
|
}
|
|
4765
4697
|
};
|
|
4766
4698
|
|
|
4699
|
+
// src/resources/reports.ts
|
|
4700
|
+
var ReportsResource = class extends BaseResource {
|
|
4701
|
+
/**
|
|
4702
|
+
* Отправляет жалобу.
|
|
4703
|
+
*
|
|
4704
|
+
* Повторная жалоба на тот же объект отклоняется сервером с сообщением
|
|
4705
|
+
* «Вы уже отправляли жалобу на этот контент».
|
|
4706
|
+
*
|
|
4707
|
+
* @example
|
|
4708
|
+
* ```ts
|
|
4709
|
+
* await itd.reports.create(report.post(postId).reason('spam'));
|
|
4710
|
+
* await itd.reports.create({ targetType: 'user', targetId, reason: 'fraud' });
|
|
4711
|
+
* ```
|
|
4712
|
+
*/
|
|
4713
|
+
create(input, options = {}) {
|
|
4714
|
+
const data = resolveReport(input);
|
|
4715
|
+
return this.http.request({
|
|
4716
|
+
method: "POST",
|
|
4717
|
+
path: "/api/reports",
|
|
4718
|
+
body: data,
|
|
4719
|
+
...this.requestOptions(options)
|
|
4720
|
+
});
|
|
4721
|
+
}
|
|
4722
|
+
};
|
|
4723
|
+
|
|
4724
|
+
// src/resources/search.ts
|
|
4725
|
+
var SearchResource = class extends BaseResource {
|
|
4726
|
+
/**
|
|
4727
|
+
* Ищет пользователей и хэштеги одним запросом.
|
|
4728
|
+
*
|
|
4729
|
+
* @example
|
|
4730
|
+
* ```ts
|
|
4731
|
+
* const { users, hashtags } = await itd.search.all('арт');
|
|
4732
|
+
* ```
|
|
4733
|
+
*/
|
|
4734
|
+
async all(query, options = {}) {
|
|
4735
|
+
const body = await this.http.request({
|
|
4736
|
+
method: "GET",
|
|
4737
|
+
path: "/api/search",
|
|
4738
|
+
query: { q: query },
|
|
4739
|
+
...this.requestOptions(options)
|
|
4740
|
+
});
|
|
4741
|
+
return {
|
|
4742
|
+
users: pickArray(body, "users"),
|
|
4743
|
+
hashtags: pickArray(body, "hashtags")
|
|
4744
|
+
};
|
|
4745
|
+
}
|
|
4746
|
+
};
|
|
4747
|
+
|
|
4748
|
+
// src/resources/subscription.ts
|
|
4749
|
+
var SubscriptionResource = class extends BaseResource {
|
|
4750
|
+
/** Загружает состояние подписки и её цену. */
|
|
4751
|
+
status(options = {}) {
|
|
4752
|
+
return this.http.request({
|
|
4753
|
+
method: "GET",
|
|
4754
|
+
// Завершающий слэш обязателен.
|
|
4755
|
+
path: "/api/v1/subscription/",
|
|
4756
|
+
...this.requestOptions(options)
|
|
4757
|
+
});
|
|
4758
|
+
}
|
|
4759
|
+
/**
|
|
4760
|
+
* Запускает оплату подписки.
|
|
4761
|
+
*
|
|
4762
|
+
* Форма ответа в документации API не описана, поэтому тип результата не уточняется.
|
|
4763
|
+
*/
|
|
4764
|
+
pay(options = {}) {
|
|
4765
|
+
return this.http.request({
|
|
4766
|
+
method: "POST",
|
|
4767
|
+
path: "/api/v1/subscription/pay",
|
|
4768
|
+
...this.requestOptions(options)
|
|
4769
|
+
});
|
|
4770
|
+
}
|
|
4771
|
+
/** Включает или отключает автопродление. */
|
|
4772
|
+
setAutoRenewal(enabled, options = {}) {
|
|
4773
|
+
return this.http.request({
|
|
4774
|
+
method: "POST",
|
|
4775
|
+
path: "/api/v1/subscription/auto-renewal",
|
|
4776
|
+
body: { enabled },
|
|
4777
|
+
...this.requestOptions(options)
|
|
4778
|
+
});
|
|
4779
|
+
}
|
|
4780
|
+
/** Запускает привязку карты. */
|
|
4781
|
+
bindCard(options = {}) {
|
|
4782
|
+
return this.http.request({
|
|
4783
|
+
method: "POST",
|
|
4784
|
+
path: "/api/v1/subscription/bind-card",
|
|
4785
|
+
...this.requestOptions(options)
|
|
4786
|
+
});
|
|
4787
|
+
}
|
|
4788
|
+
/** Загружает список способов оплаты. Пустой массив, если карт нет. */
|
|
4789
|
+
async methods(options = {}) {
|
|
4790
|
+
const body = await this.http.request({
|
|
4791
|
+
method: "GET",
|
|
4792
|
+
path: "/api/v1/subscription/methods",
|
|
4793
|
+
...this.requestOptions(options)
|
|
4794
|
+
});
|
|
4795
|
+
return Array.isArray(body) ? body : [];
|
|
4796
|
+
}
|
|
4797
|
+
/** Делает способ оплаты основным. */
|
|
4798
|
+
setDefaultMethod(methodId, options = {}) {
|
|
4799
|
+
return this.http.request({
|
|
4800
|
+
method: "POST",
|
|
4801
|
+
path: `/api/v1/subscription/methods/${encodePathSegment(methodId, "methodId")}/default`,
|
|
4802
|
+
...this.requestOptions(options)
|
|
4803
|
+
});
|
|
4804
|
+
}
|
|
4805
|
+
/** Удаляет способ оплаты. */
|
|
4806
|
+
removeMethod(methodId, options = {}) {
|
|
4807
|
+
return this.http.request({
|
|
4808
|
+
method: "DELETE",
|
|
4809
|
+
path: `/api/v1/subscription/methods/${encodePathSegment(methodId, "methodId")}`,
|
|
4810
|
+
...this.requestOptions(options)
|
|
4811
|
+
});
|
|
4812
|
+
}
|
|
4813
|
+
};
|
|
4814
|
+
|
|
4767
4815
|
// src/resources/telemetry.ts
|
|
4768
4816
|
var TelemetryResource = class extends BaseResource {
|
|
4769
4817
|
/** Идентификатор сессии телеметрии, общий для всех событий этого объекта. */
|
|
@@ -4830,6 +4878,21 @@ var TelemetryResource = class extends BaseResource {
|
|
|
4830
4878
|
|
|
4831
4879
|
// src/resources/users.ts
|
|
4832
4880
|
var UsersResource = class extends BaseResource {
|
|
4881
|
+
/**
|
|
4882
|
+
* Списки пользователей: подписчики, подписки, заблокированные.
|
|
4883
|
+
*
|
|
4884
|
+
* Путь приходит в параметрах — так один описатель обслуживает все три эндпоинта. Имена
|
|
4885
|
+
* полей перечислены с запасом: списки приходят под `users`, но альтернативное имя ничего
|
|
4886
|
+
* не стоит и спасает, если эндпоинт назовёт список по-своему. `page` уходит в запрос, хотя
|
|
4887
|
+
* сервер его сейчас не читает (см. {@link followers}): когда починят — заработает само.
|
|
4888
|
+
*/
|
|
4889
|
+
#userList = this.paginated({
|
|
4890
|
+
path: (p) => p.path,
|
|
4891
|
+
query: (p) => ({ limit: p.limit }),
|
|
4892
|
+
start: (p) => p.page !== void 0 ? { page: p.page } : {},
|
|
4893
|
+
read: (body) => readPagedPage(body, "users", "followers", "following", "blocked"),
|
|
4894
|
+
mode: PaginationMode.Page
|
|
4895
|
+
});
|
|
4833
4896
|
/** Загружает свой профиль — с подпиской и признаком подтверждённого телефона. */
|
|
4834
4897
|
me(options = {}) {
|
|
4835
4898
|
return this.http.request({
|
|
@@ -5075,38 +5138,33 @@ var UsersResource = class extends BaseResource {
|
|
|
5075
5138
|
...this.requestOptions(options)
|
|
5076
5139
|
});
|
|
5077
5140
|
}
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5141
|
+
#userPage(path, params) {
|
|
5142
|
+
return this.#userList.list({ ...params, path });
|
|
5143
|
+
}
|
|
5144
|
+
#userPaginator(path, params) {
|
|
5145
|
+
return this.#userList.iterate({ ...params, path });
|
|
5146
|
+
}
|
|
5147
|
+
};
|
|
5148
|
+
|
|
5149
|
+
// src/resources/verification.ts
|
|
5150
|
+
var VerificationResource = class extends BaseResource {
|
|
5151
|
+
/** Загружает статус заявки. Значение `none` означает, что заявка не подавалась. */
|
|
5152
|
+
status(options = {}) {
|
|
5153
|
+
return this.http.request({
|
|
5090
5154
|
method: "GET",
|
|
5091
|
-
path,
|
|
5092
|
-
|
|
5093
|
-
...this.requestOptions(params)
|
|
5155
|
+
path: "/api/verification/status",
|
|
5156
|
+
...this.requestOptions(options)
|
|
5094
5157
|
});
|
|
5095
|
-
return readPagedPage(body, "users", "followers", "following", "blocked");
|
|
5096
5158
|
}
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5159
|
+
/** Подаёт заявку на верификацию с видео. */
|
|
5160
|
+
submit(videoUrl, options = {}) {
|
|
5161
|
+
return this.http.request({
|
|
5162
|
+
method: "POST",
|
|
5163
|
+
path: "/api/verification/submit",
|
|
5164
|
+
body: { videoUrl },
|
|
5165
|
+
...this.requestOptions(options)
|
|
5100
5166
|
});
|
|
5101
5167
|
}
|
|
5102
|
-
#userPaginator(path, params) {
|
|
5103
|
-
return this.paginate(
|
|
5104
|
-
PaginationMode.Page,
|
|
5105
|
-
(state) => this.#loadUserPage(path, params, state),
|
|
5106
|
-
// Без `start` перебор начинался бы с первой страницы, молча игнорируя `page`.
|
|
5107
|
-
{ ...params, ...params.page !== void 0 ? { start: { page: params.page } } : {} }
|
|
5108
|
-
);
|
|
5109
|
-
}
|
|
5110
5168
|
};
|
|
5111
5169
|
|
|
5112
5170
|
// src/client.ts
|
|
@@ -5117,6 +5175,8 @@ var ItdClient = class {
|
|
|
5117
5175
|
#jar;
|
|
5118
5176
|
#queue;
|
|
5119
5177
|
#plugins = new PluginRegistry();
|
|
5178
|
+
/** Порождённые потоки уведомлений — чтобы `close()` мог закрыть их разом. */
|
|
5179
|
+
#streams = /* @__PURE__ */ new Set();
|
|
5120
5180
|
/** Авторизация, сессии и пароли. */
|
|
5121
5181
|
auth;
|
|
5122
5182
|
/** Профили, подписки, блокировки, приватность. */
|
|
@@ -5147,26 +5207,56 @@ var ItdClient = class {
|
|
|
5147
5207
|
* @experimental Недокументированные эндпоинты. Библиотека никогда не отправляет их сама.
|
|
5148
5208
|
*/
|
|
5149
5209
|
telemetry;
|
|
5150
|
-
constructor(options = {}) {
|
|
5151
|
-
|
|
5210
|
+
constructor(options = {}, internals = {}) {
|
|
5211
|
+
const config = resolveConfig(options);
|
|
5212
|
+
this.#config = config;
|
|
5152
5213
|
this.#jar = new CookieJar();
|
|
5153
|
-
|
|
5154
|
-
this.#
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
onUnauthorized: () => this.#authManager.onUnauthorized(),
|
|
5161
|
-
getCookieHeader: (url) => this.#jar.getHeader(url),
|
|
5162
|
-
saveCookies: (url, response) => this.#jar.setFromResponse(url, response),
|
|
5163
|
-
...this.#queue ? { schedule: this.#queue.schedule.bind(this.#queue) } : {},
|
|
5164
|
-
// Планировщик нужен, даже когда обычные повторы выключены: лимит частоты
|
|
5165
|
-
// живёт по своим правилам и настраивается отдельно, в `rateLimit`.
|
|
5166
|
-
...this.#config.retry || this.#config.rateLimit ? { nextRetryDelay: this.#createRetryScheduler() } : {},
|
|
5167
|
-
...this.#queue && this.#config.rateLimit?.respectHeaders ? { onRateLimit: this.#throttleByHeaders.bind(this) } : {}
|
|
5214
|
+
const queue = config.rateLimit ? new RequestQueue(config.rateLimit) : void 0;
|
|
5215
|
+
this.#queue = queue;
|
|
5216
|
+
let authManager;
|
|
5217
|
+
const transport = new Transport(config, {
|
|
5218
|
+
cookies: config.useCookieJar ? this.#jar : void 0,
|
|
5219
|
+
getDeviceId: () => authManager.getDeviceId(),
|
|
5220
|
+
onRateLimit: queue && config.rateLimit?.respectHeaders ? (limit, remaining) => this.#throttleByHeaders(limit, remaining) : void 0
|
|
5168
5221
|
});
|
|
5169
|
-
|
|
5222
|
+
const pluginsLayer = createPluginsMiddleware(this.#plugins);
|
|
5223
|
+
const retriesLayer = createRetryMiddleware({
|
|
5224
|
+
retry: config.retry,
|
|
5225
|
+
rateLimitDelays: config.rateLimit?.retryDelays ?? [],
|
|
5226
|
+
pauseQueue: queue ? (ms) => queue.pause(ms) : void 0,
|
|
5227
|
+
hooks: config.hooks,
|
|
5228
|
+
logger: config.logger,
|
|
5229
|
+
buildUrl: (request) => transport.buildUrl(request)
|
|
5230
|
+
});
|
|
5231
|
+
const authRetry = config.retry ? {
|
|
5232
|
+
attempts: config.retry.attempts,
|
|
5233
|
+
baseDelay: config.retry.baseDelay,
|
|
5234
|
+
maxDelay: config.retry.maxDelay,
|
|
5235
|
+
jitter: config.retry.jitter,
|
|
5236
|
+
retryWrites: true,
|
|
5237
|
+
...config.retry.shouldRetry ? { shouldRetry: config.retry.shouldRetry } : {}
|
|
5238
|
+
} : void 0;
|
|
5239
|
+
const authPipeline = composePipeline([pluginsLayer, retriesLayer], transport.send);
|
|
5240
|
+
const authHandler = (request) => authRetry && request.retry === void 0 ? authPipeline({ ...request, retry: authRetry }) : authPipeline(request);
|
|
5241
|
+
authManager = new AuthManager(config, authHandler, this.#jar);
|
|
5242
|
+
this.#authManager = authManager;
|
|
5243
|
+
const middlewares = [];
|
|
5244
|
+
if (queue) middlewares.push(createQueueMiddleware(queue.schedule.bind(queue)));
|
|
5245
|
+
middlewares.push(pluginsLayer);
|
|
5246
|
+
middlewares.push(retriesLayer);
|
|
5247
|
+
middlewares.push(
|
|
5248
|
+
createAuthMiddleware({
|
|
5249
|
+
getAuthHeaders: () => authManager.getAuthHeaders(),
|
|
5250
|
+
onUnauthorized: () => authManager.onUnauthorized(),
|
|
5251
|
+
autoRefresh: config.autoRefresh
|
|
5252
|
+
})
|
|
5253
|
+
);
|
|
5254
|
+
const handler = composePipeline(middlewares, transport.send);
|
|
5255
|
+
this.#http = new HttpClient({ handler, plugins: this.#plugins, baseUrl: config.baseUrl });
|
|
5256
|
+
this.files = new FilesResource(
|
|
5257
|
+
this.#http,
|
|
5258
|
+
internals.fileReader ? { readFile: internals.fileReader } : {}
|
|
5259
|
+
);
|
|
5170
5260
|
const uploadFiles = (files, requestOptions) => this.files.uploadMany(files, requestOptions ?? {});
|
|
5171
5261
|
this.auth = new AuthResource(this.#http, { auth: this.#authManager });
|
|
5172
5262
|
this.users = new UsersResource(this.#http);
|
|
@@ -5256,17 +5346,44 @@ var ItdClient = class {
|
|
|
5256
5346
|
* ```
|
|
5257
5347
|
*/
|
|
5258
5348
|
realtime(options = {}) {
|
|
5259
|
-
|
|
5349
|
+
let stream;
|
|
5350
|
+
stream = new ItdRealtime(
|
|
5260
5351
|
{
|
|
5261
5352
|
baseUrl: this.#config.baseUrl,
|
|
5262
5353
|
fetch: this.#config.fetch,
|
|
5263
5354
|
getToken: () => this.#authManager.getAccessToken(),
|
|
5264
5355
|
refresh: () => this.#authManager.onUnauthorized(),
|
|
5265
5356
|
fetchUnreadCount: () => this.notifications.count(),
|
|
5357
|
+
onClose: () => this.#streams.delete(stream),
|
|
5266
5358
|
logger: this.#config.logger
|
|
5267
5359
|
},
|
|
5268
5360
|
options
|
|
5269
5361
|
);
|
|
5362
|
+
this.#streams.add(stream);
|
|
5363
|
+
return stream;
|
|
5364
|
+
}
|
|
5365
|
+
/**
|
|
5366
|
+
* Освобождает ресурсы клиента: останавливает очередь запросов (снимает отложенные паузы)
|
|
5367
|
+
* и закрывает все потоки уведомлений, созданные через {@link realtime}.
|
|
5368
|
+
*
|
|
5369
|
+
* После вызова клиентом можно пользоваться снова — новые запросы поднимут всё заново,
|
|
5370
|
+
* но уже созданные потоки останутся закрытыми.
|
|
5371
|
+
*
|
|
5372
|
+
* @example
|
|
5373
|
+
* ```ts
|
|
5374
|
+
* await using itd = new ItdClient({ auth: token });
|
|
5375
|
+
* // …работа…
|
|
5376
|
+
* // close() вызовется сам на выходе из блока
|
|
5377
|
+
* ```
|
|
5378
|
+
*/
|
|
5379
|
+
async close() {
|
|
5380
|
+
for (const stream of this.#streams) stream.disconnect();
|
|
5381
|
+
this.#streams.clear();
|
|
5382
|
+
this.#queue?.stop();
|
|
5383
|
+
}
|
|
5384
|
+
/** Позволяет использовать клиент с `await using`. */
|
|
5385
|
+
[Symbol.asyncDispose]() {
|
|
5386
|
+
return this.close();
|
|
5270
5387
|
}
|
|
5271
5388
|
/** Текущая сессия целиком — чтобы сохранить её самостоятельно. */
|
|
5272
5389
|
getSession() {
|
|
@@ -5276,16 +5393,6 @@ var ItdClient = class {
|
|
|
5276
5393
|
setSession(session) {
|
|
5277
5394
|
return this.#authManager.setSession(session);
|
|
5278
5395
|
}
|
|
5279
|
-
/**
|
|
5280
|
-
* Подключает чтение файлов с диска.
|
|
5281
|
-
*
|
|
5282
|
-
* Вызывается из `itd-api/node`; напрямую обычно не нужно.
|
|
5283
|
-
*
|
|
5284
|
-
* @internal
|
|
5285
|
-
*/
|
|
5286
|
-
setFileReader(readFile) {
|
|
5287
|
-
this.files.setFileReader(readFile);
|
|
5288
|
-
}
|
|
5289
5396
|
/**
|
|
5290
5397
|
* Придерживает очередь, когда лимит сервера исчерпан.
|
|
5291
5398
|
*
|
|
@@ -5306,35 +5413,12 @@ var ItdClient = class {
|
|
|
5306
5413
|
`\u043B\u0438\u043C\u0438\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D (${remaining} \u0438\u0437 ${limit ?? "?"}), \u043E\u0447\u0435\u0440\u0435\u0434\u044C \u0436\u0434\u0451\u0442 ${first} \u043C\u0441`
|
|
5307
5414
|
);
|
|
5308
5415
|
}
|
|
5309
|
-
/**
|
|
5310
|
-
* Собирает планировщик повторов и связывает его с очередью.
|
|
5311
|
-
*
|
|
5312
|
-
* Ответ `429` обрабатывается отдельно от прочих ошибок. Причина в том, что сервер
|
|
5313
|
-
* не присылает `Retry-After` и не сообщает время сброса окна: экспоненциальный откат
|
|
5314
|
-
* в сотни миллисекунд здесь бесполезен, а окно измеряется десятками секунд. Вместо
|
|
5315
|
-
* расчёта берётся лестница пауз `rateLimit.retryDelays`, и она не зависит
|
|
5316
|
-
* от `retry.attempts`, у которого совсем другая задача.
|
|
5317
|
-
*
|
|
5318
|
-
* Пауза накладывается на всю очередь: иначе остальные запросы продолжат добивать API,
|
|
5319
|
-
* пока первый ждёт.
|
|
5320
|
-
*/
|
|
5321
|
-
#createRetryScheduler() {
|
|
5322
|
-
const retry = this.#config.retry;
|
|
5323
|
-
const scheduler = retry ? createRetryScheduler(retry) : void 0;
|
|
5324
|
-
const queue = this.#queue;
|
|
5325
|
-
const delays = this.#config.rateLimit?.retryDelays ?? [];
|
|
5326
|
-
return (error, attempt, method) => {
|
|
5327
|
-
if (isItdRateLimitError(error)) {
|
|
5328
|
-
const wait = error.retryAfter ?? delays[attempt - 1];
|
|
5329
|
-
if (wait === void 0) return void 0;
|
|
5330
|
-
queue?.pause(wait);
|
|
5331
|
-
this.#config.logger?.debug(`\u043B\u0438\u043C\u0438\u0442 \u0447\u0430\u0441\u0442\u043E\u0442\u044B, \u043F\u043E\u043F\u044B\u0442\u043A\u0430 ${attempt + 1} \u0447\u0435\u0440\u0435\u0437 ${wait} \u043C\u0441`);
|
|
5332
|
-
return wait;
|
|
5333
|
-
}
|
|
5334
|
-
return scheduler?.(error, attempt, method);
|
|
5335
|
-
};
|
|
5336
|
-
}
|
|
5337
5416
|
};
|
|
5417
|
+
if (typeof Symbol.asyncDispose !== "symbol") {
|
|
5418
|
+
const prototype = ItdClient.prototype;
|
|
5419
|
+
prototype[/* @__PURE__ */ Symbol.for("Symbol.asyncDispose")] = prototype.undefined;
|
|
5420
|
+
delete prototype.undefined;
|
|
5421
|
+
}
|
|
5338
5422
|
function createClient(options = {}) {
|
|
5339
5423
|
return new ItdClient(options);
|
|
5340
5424
|
}
|
|
@@ -5447,5 +5531,5 @@ function toDate(value) {
|
|
|
5447
5531
|
}
|
|
5448
5532
|
|
|
5449
5533
|
export { ALLOWED_MIME_TYPES, AUDIO_MIME_TYPES, AUTH_FLAG_COOKIE, AUTH_PATHS, AccessType, AttachmentType, CommentSort, DEFAULT_BASE_URL, DEFAULT_TIMEOUT, DEFAULT_USER_AGENT, DEVICE_ID_HEADER, DetectedRuntime, FeedTab, IMAGE_MIME_TYPES, InteractionType, ItdAbortError, ItdApiError, ItdApiErrorKind, ItdAuthError, ItdClient, ItdConfigError, ItdConflictError, ItdError, ItdErrorCode, ItdErrorKind, ItdForbiddenError, ItdNetworkError, ItdNotFoundError, ItdPhoneVerificationError, ItdRateLimitError, ItdRealtime, ItdServerError, ItdTimeoutError, ItdValidationError, LIBRARY_VERSION, LikesVisibility, LocalStorageTokenStorage, MAX_RECONNECT_ATTEMPTS, MemoryTokenStorage, NOTIFICATION_TYPE_ALIASES, NotificationType, OAuthProvider, PaginationMode, Paginator, RECONNECT_BACKOFF, RECONNECT_JITTER, REFRESH_COOKIE, REFRESH_COOKIE_PATH, RealtimeStatus, RealtimeTransportKind, ReportReason, ReportTargetType, RuntimeMode, STREAM_PATH, SignInStatus, SpanType, TURNSTILE_SITE_KEY, UnauthorizedStreamError, VIDEO_MIME_TYPES, ViewReason, ViewSource, WallAccess, canonicalNotificationType, comment, createClient, createTokenStorage, formatNotificationText, isBuilder, isItdApiError, isItdAuthError, isItdConflictError, isItdError, isItdForbiddenError, isItdNotFoundError, isItdPhoneVerificationError, isItdRateLimitError, isItdServerError, isItdValidationError, isKnownNotificationType, isMyProfile, normalizeNotification, poll, post, readNotificationEvent, readUnreadCountEvent, report, resolveNotificationUrl, toDate };
|
|
5450
|
-
//# sourceMappingURL=chunk-
|
|
5451
|
-
//# sourceMappingURL=chunk-
|
|
5534
|
+
//# sourceMappingURL=chunk-JIYN33FG.js.map
|
|
5535
|
+
//# sourceMappingURL=chunk-JIYN33FG.js.map
|