itd-api 0.0.6 → 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-K7NBFQEE.cjs → chunk-ATCZ4T2K.cjs} +1130 -987
- package/dist/chunk-ATCZ4T2K.cjs.map +1 -0
- package/dist/{chunk-CCRQI3ON.js → chunk-JIYN33FG.js} +1127 -988
- package/dist/chunk-JIYN33FG.js.map +1 -0
- package/dist/{index-CDGXKy8r.d.cts → index-Duh31Wnx.d.cts} +591 -443
- package/dist/{index-CDGXKy8r.d.ts → index-Duh31Wnx.d.ts} +591 -443
- package/dist/index.cjs +101 -85
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/node.cjs +128 -91
- 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-CCRQI3ON.js.map +0 -1
- package/dist/chunk-K7NBFQEE.cjs.map +0 -1
|
@@ -616,14 +616,18 @@ var RealtimeStatus = Object.freeze({
|
|
|
616
616
|
Error: "error",
|
|
617
617
|
Disconnected: "disconnected"
|
|
618
618
|
});
|
|
619
|
-
var
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
var LikesVisibility = Object.freeze({
|
|
623
|
-
Everyone: "everyone",
|
|
619
|
+
var AccessType = Object.freeze({
|
|
620
|
+
/** Никто. */
|
|
621
|
+
Nobody: "nobody",
|
|
624
622
|
/** Только взаимные подписки. */
|
|
625
|
-
Mutual: "mutual"
|
|
623
|
+
Mutual: "mutual",
|
|
624
|
+
/** Подписчики. */
|
|
625
|
+
Followers: "followers",
|
|
626
|
+
/** Все. */
|
|
627
|
+
Everyone: "everyone"
|
|
626
628
|
});
|
|
629
|
+
var WallAccess = AccessType;
|
|
630
|
+
var LikesVisibility = AccessType;
|
|
627
631
|
var NotificationType = Object.freeze({
|
|
628
632
|
/** Реакция на пост. Старое имя — `like`. */
|
|
629
633
|
PostReaction: "post_reaction",
|
|
@@ -652,6 +656,36 @@ var NotificationType = Object.freeze({
|
|
|
652
656
|
/** Верификация отклонена. Приходит только по REST. */
|
|
653
657
|
VerificationRejected: "verification_rejected"
|
|
654
658
|
});
|
|
659
|
+
var InteractionType = Object.freeze({
|
|
660
|
+
/** Открытие фотографии. */
|
|
661
|
+
PhotoOpen: 1,
|
|
662
|
+
/** Прогресс просмотра видео. Несёт поля `pm`/`dm`. */
|
|
663
|
+
VideoProgress: 2
|
|
664
|
+
});
|
|
665
|
+
var ViewSource = Object.freeze({
|
|
666
|
+
FeedGlobal: 1,
|
|
667
|
+
FeedFollowing: 2,
|
|
668
|
+
FeedClan: 3,
|
|
669
|
+
Profile: 4,
|
|
670
|
+
Hashtag: 5,
|
|
671
|
+
PostPage: 6,
|
|
672
|
+
Link: 7,
|
|
673
|
+
Search: 8
|
|
674
|
+
});
|
|
675
|
+
var ViewReason = Object.freeze({
|
|
676
|
+
/** Пост ушёл из зоны видимости при обычной прокрутке. */
|
|
677
|
+
Normal: 0,
|
|
678
|
+
/** Потеря фокуса окна. */
|
|
679
|
+
Blur: 1,
|
|
680
|
+
/** Вкладка скрыта. */
|
|
681
|
+
Hidden: 2,
|
|
682
|
+
/** Уход со страницы (`pagehide`). */
|
|
683
|
+
PageHide: 3,
|
|
684
|
+
/** Элемент перестал наблюдаться. */
|
|
685
|
+
Unobserve: 4,
|
|
686
|
+
/** Достигнут порог времени просмотра. */
|
|
687
|
+
ThresholdMet: 5
|
|
688
|
+
});
|
|
655
689
|
var ItdErrorCode = Object.freeze({
|
|
656
690
|
BAD_REQUEST: "BAD_REQUEST",
|
|
657
691
|
UNAUTHORIZED: "UNAUTHORIZED",
|
|
@@ -1261,13 +1295,23 @@ function readAccessToken(payload) {
|
|
|
1261
1295
|
const token = payload.accessToken;
|
|
1262
1296
|
return typeof token === "string" && token.length > 0 ? token : void 0;
|
|
1263
1297
|
}
|
|
1298
|
+
function reportListenerError(logger, scope, error) {
|
|
1299
|
+
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}`;
|
|
1300
|
+
if (logger) logger.error(message, error);
|
|
1301
|
+
else console.error(`[itd-api] ${message}`, error);
|
|
1302
|
+
}
|
|
1264
1303
|
var AuthManager = class {
|
|
1265
1304
|
#config;
|
|
1266
|
-
#
|
|
1305
|
+
#send;
|
|
1267
1306
|
#jar;
|
|
1268
|
-
#emitter
|
|
1307
|
+
#emitter;
|
|
1269
1308
|
/** `undefined` — сессия ещё не читалась из хранилища. */
|
|
1270
1309
|
#session;
|
|
1310
|
+
/**
|
|
1311
|
+
* Общий промис чтения сессии из хранилища. Дедупликация: параллельные запросы на холодном
|
|
1312
|
+
* старте читают хранилище один раз и не заводят каждый свой `deviceId`.
|
|
1313
|
+
*/
|
|
1314
|
+
#loading = null;
|
|
1271
1315
|
/** Общий промис обновления: к нему присоединяются все, кто получил 401. */
|
|
1272
1316
|
#refreshing = null;
|
|
1273
1317
|
/** Общий промис входа по логину и паролю. */
|
|
@@ -1279,10 +1323,15 @@ var AuthManager = class {
|
|
|
1279
1323
|
* поэтому `clear()` его не трогает.
|
|
1280
1324
|
*/
|
|
1281
1325
|
#deviceId;
|
|
1282
|
-
|
|
1326
|
+
/** Общий промис первичной выдачи `deviceId` — чтобы параллельные запросы получили один. */
|
|
1327
|
+
#deviceIdLoading = null;
|
|
1328
|
+
constructor(config, send, jar) {
|
|
1283
1329
|
this.#config = config;
|
|
1284
|
-
this.#
|
|
1330
|
+
this.#send = send;
|
|
1285
1331
|
this.#jar = jar;
|
|
1332
|
+
this.#emitter = new Emitter(
|
|
1333
|
+
(error) => reportListenerError(config.logger, "\u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u0438", error)
|
|
1334
|
+
);
|
|
1286
1335
|
}
|
|
1287
1336
|
/** Подписка на события авторизации. */
|
|
1288
1337
|
get on() {
|
|
@@ -1324,8 +1373,14 @@ var AuthManager = class {
|
|
|
1324
1373
|
* сервер связывает с ним запись в списке сессий, и плавающее значение плодило бы
|
|
1325
1374
|
* по новой сессии на каждый старт.
|
|
1326
1375
|
*/
|
|
1327
|
-
|
|
1328
|
-
if (this.#deviceId) return this.#deviceId;
|
|
1376
|
+
getDeviceId() {
|
|
1377
|
+
if (this.#deviceId) return Promise.resolve(this.#deviceId);
|
|
1378
|
+
this.#deviceIdLoading ??= this.#resolveDeviceId().finally(() => {
|
|
1379
|
+
this.#deviceIdLoading = null;
|
|
1380
|
+
});
|
|
1381
|
+
return this.#deviceIdLoading;
|
|
1382
|
+
}
|
|
1383
|
+
async #resolveDeviceId() {
|
|
1329
1384
|
const session = await this.#loadSession();
|
|
1330
1385
|
const deviceId = this.#config.deviceId ?? session?.deviceId ?? createDeviceId();
|
|
1331
1386
|
this.#deviceId = deviceId;
|
|
@@ -1428,8 +1483,14 @@ var AuthManager = class {
|
|
|
1428
1483
|
if (this.#deviceId) await this.#saveSession({ deviceId: this.#deviceId });
|
|
1429
1484
|
this.#emitter.emit("signOut", void 0);
|
|
1430
1485
|
}
|
|
1431
|
-
|
|
1432
|
-
if (this.#session !== void 0) return this.#session;
|
|
1486
|
+
#loadSession() {
|
|
1487
|
+
if (this.#session !== void 0) return Promise.resolve(this.#session);
|
|
1488
|
+
this.#loading ??= this.#performLoad().finally(() => {
|
|
1489
|
+
this.#loading = null;
|
|
1490
|
+
});
|
|
1491
|
+
return this.#loading;
|
|
1492
|
+
}
|
|
1493
|
+
async #performLoad() {
|
|
1433
1494
|
const stored = await this.#config.storage.get() ?? null;
|
|
1434
1495
|
if (stored?.cookies) this.#jar.deserialize(stored.cookies);
|
|
1435
1496
|
const fromConfig = this.#sessionFromConfig(this.#config.auth);
|
|
@@ -1497,17 +1558,14 @@ var AuthManager = class {
|
|
|
1497
1558
|
return this.#reloginOrNull();
|
|
1498
1559
|
}
|
|
1499
1560
|
try {
|
|
1500
|
-
const payload = await this.#
|
|
1561
|
+
const payload = await this.#send({
|
|
1501
1562
|
method: "POST",
|
|
1502
1563
|
path: AUTH_PATHS.refresh,
|
|
1564
|
+
skipQueue: true,
|
|
1565
|
+
skipAuth: true,
|
|
1566
|
+
skipAuthRefresh: true
|
|
1503
1567
|
// Тела нет намеренно: сервер читает refresh-токен только из cookie — см.
|
|
1504
1568
|
// #seedRefreshCookie. По той же причине не нужен и устаревший Bearer.
|
|
1505
|
-
skipAuth: true,
|
|
1506
|
-
// Без этого 401 на самом обновлении вызвал бы новое обновление — и так по кругу.
|
|
1507
|
-
skipAuthRefresh: true,
|
|
1508
|
-
// Обновление почти всегда запускается изнутри запроса, который занимает место
|
|
1509
|
-
// в очереди и ждёт его результата. Встать в ту же очередь — значит зависнуть.
|
|
1510
|
-
skipQueue: true
|
|
1511
1569
|
});
|
|
1512
1570
|
const accessToken = readAccessToken(payload);
|
|
1513
1571
|
if (!accessToken) return this.#reloginOrNull();
|
|
@@ -1579,15 +1637,13 @@ var AuthManager = class {
|
|
|
1579
1637
|
}
|
|
1580
1638
|
async #performSignIn(credentials) {
|
|
1581
1639
|
const turnstileToken = await this.#resolveTurnstileToken(credentials);
|
|
1582
|
-
const payload = await this.#
|
|
1640
|
+
const payload = await this.#send({
|
|
1583
1641
|
method: "POST",
|
|
1584
1642
|
path: AUTH_PATHS.signIn,
|
|
1585
1643
|
body: { email: credentials.email, password: credentials.password, turnstileToken },
|
|
1644
|
+
skipQueue: true,
|
|
1586
1645
|
skipAuth: true,
|
|
1587
|
-
skipAuthRefresh: true
|
|
1588
|
-
// Отложенный вход происходит при сборке заголовков уже начатого запроса — тот держит
|
|
1589
|
-
// место в очереди и ждёт токена. См. `skipQueue` в RawRequestOptions.
|
|
1590
|
-
skipQueue: true
|
|
1646
|
+
skipAuthRefresh: true
|
|
1591
1647
|
});
|
|
1592
1648
|
const accessToken = readAccessToken(payload);
|
|
1593
1649
|
if (!accessToken) {
|
|
@@ -1713,10 +1769,12 @@ function normalizeBaseUrl(baseUrl) {
|
|
|
1713
1769
|
return parsed.origin + (parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, ""));
|
|
1714
1770
|
}
|
|
1715
1771
|
|
|
1772
|
+
// src/core/version.ts
|
|
1773
|
+
var LIBRARY_VERSION = "0.0.8";
|
|
1774
|
+
|
|
1716
1775
|
// src/core/config.ts
|
|
1717
1776
|
var DEFAULT_BASE_URL = "https://xn--d1ah4a.com";
|
|
1718
1777
|
var DEFAULT_TIMEOUT = 3e4;
|
|
1719
|
-
var LIBRARY_VERSION = "0.0.6";
|
|
1720
1778
|
var DEFAULT_USER_AGENT = `Mozilla/5.0 (compatible; itd-api/${LIBRARY_VERSION}; +https://github.com/KiowDev/itd-api)`;
|
|
1721
1779
|
var DEFAULT_RATE_LIMIT_DELAYS = Object.freeze([1e3, 5e3, 3e4, 6e4, 9e4]);
|
|
1722
1780
|
function requirePositive(value, name) {
|
|
@@ -1861,150 +1919,487 @@ function resolveConfig(options = {}) {
|
|
|
1861
1919
|
};
|
|
1862
1920
|
}
|
|
1863
1921
|
|
|
1864
|
-
// src/core/
|
|
1865
|
-
var
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1922
|
+
// src/core/http.ts
|
|
1923
|
+
var HttpClient = class {
|
|
1924
|
+
#handler;
|
|
1925
|
+
#plugins;
|
|
1926
|
+
#baseUrl;
|
|
1927
|
+
constructor(deps) {
|
|
1928
|
+
this.#handler = deps.handler;
|
|
1929
|
+
this.#plugins = deps.plugins;
|
|
1930
|
+
this.#baseUrl = deps.baseUrl;
|
|
1931
|
+
}
|
|
1932
|
+
/** Базовый URL, к которому обращается клиент. */
|
|
1933
|
+
get baseUrl() {
|
|
1934
|
+
return this.#baseUrl;
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Имена опций запроса, заявленные плагинами.
|
|
1938
|
+
*
|
|
1939
|
+
* Читается ресурсами: они переносят в транспорт только известные поля, а чужие,
|
|
1940
|
+
* если их никто не заявил, отсеивают.
|
|
1941
|
+
*/
|
|
1942
|
+
get pluginOptionKeys() {
|
|
1943
|
+
return this.#plugins.optionKeys;
|
|
1944
|
+
}
|
|
1945
|
+
/**
|
|
1946
|
+
* Выполняет запрос к API через собранный конвейер.
|
|
1947
|
+
*
|
|
1948
|
+
* @typeParam T ожидаемая форма ответа после снятия обёртки `{ data: … }`
|
|
1949
|
+
* @throws {ItdApiError} если сервер ответил статусом ≥ 400
|
|
1950
|
+
* @throws {ItdTimeoutError} если истёк таймаут
|
|
1951
|
+
* @throws {ItdAbortError} если запрос отменён через `signal`
|
|
1952
|
+
* @throws {ItdNetworkError} если запрос не дошёл до сервера
|
|
1953
|
+
*/
|
|
1954
|
+
request(options) {
|
|
1955
|
+
return this.#handler(options);
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1958
|
+
|
|
1959
|
+
// src/core/pipeline.ts
|
|
1960
|
+
function composePipeline(middlewares, final) {
|
|
1961
|
+
return middlewares.reduceRight(
|
|
1962
|
+
(next, middleware) => (request) => middleware(request, next),
|
|
1963
|
+
final
|
|
1964
|
+
);
|
|
1881
1965
|
}
|
|
1882
|
-
function
|
|
1883
|
-
|
|
1884
|
-
headers.forEach((value, name) => {
|
|
1885
|
-
if (SECRET_HEADERS.has(name.toLowerCase())) {
|
|
1886
|
-
const spaceAt = value.indexOf(" ");
|
|
1887
|
-
result[name] = spaceAt > 0 ? `${value.slice(0, spaceAt)} ${maskSecret(value.slice(spaceAt + 1))}` : maskSecret(value);
|
|
1888
|
-
return;
|
|
1889
|
-
}
|
|
1890
|
-
result[name] = value;
|
|
1891
|
-
});
|
|
1892
|
-
return result;
|
|
1966
|
+
function withLayerHeaders(request, headers) {
|
|
1967
|
+
return { ...request, layerHeaders: { ...request.layerHeaders, ...headers } };
|
|
1893
1968
|
}
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
if (
|
|
1899
|
-
|
|
1900
|
-
if (
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
}
|
|
1905
|
-
return result;
|
|
1969
|
+
|
|
1970
|
+
// src/core/retry.ts
|
|
1971
|
+
var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
1972
|
+
function isRetryable(error, method, retryWrites) {
|
|
1973
|
+
if (error instanceof ItdAbortError) return false;
|
|
1974
|
+
const safeToRepeat = retryWrites || IDEMPOTENT_METHODS.has(method);
|
|
1975
|
+
if (error instanceof ItdApiError) {
|
|
1976
|
+
if (error.status === 429) return true;
|
|
1977
|
+
if (error.status >= 500) return safeToRepeat;
|
|
1978
|
+
return false;
|
|
1906
1979
|
}
|
|
1907
|
-
return
|
|
1980
|
+
if (error instanceof ItdNetworkError || error instanceof ItdTimeoutError) return safeToRepeat;
|
|
1981
|
+
return false;
|
|
1908
1982
|
}
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
if (keys.length !== 1 || keys[0] !== "data") return body;
|
|
1915
|
-
return body.data;
|
|
1983
|
+
function backoffDelay(attempt, options, random) {
|
|
1984
|
+
const exponential = options.baseDelay * 2 ** (attempt - 1);
|
|
1985
|
+
const capped = Math.min(exponential, options.maxDelay);
|
|
1986
|
+
const spread = capped * options.jitter * (random() * 2 - 1);
|
|
1987
|
+
return Math.max(0, Math.round(capped + spread));
|
|
1916
1988
|
}
|
|
1917
|
-
function
|
|
1918
|
-
return
|
|
1989
|
+
function createRetryScheduler(options, random = Math.random) {
|
|
1990
|
+
return (error, attempt, method) => {
|
|
1991
|
+
if (attempt >= options.attempts) return void 0;
|
|
1992
|
+
if (options.shouldRetry) {
|
|
1993
|
+
return options.shouldRetry(error, attempt) ? backoffDelay(attempt, options, random) : void 0;
|
|
1994
|
+
}
|
|
1995
|
+
if (!isRetryable(error, method, options.retryWrites)) return void 0;
|
|
1996
|
+
if (error instanceof ItdApiError && error.retryAfter !== void 0) {
|
|
1997
|
+
return error.retryAfter > options.maxDelay ? void 0 : error.retryAfter;
|
|
1998
|
+
}
|
|
1999
|
+
return backoffDelay(attempt, options, random);
|
|
2000
|
+
};
|
|
1919
2001
|
}
|
|
1920
|
-
|
|
1921
|
-
|
|
2002
|
+
|
|
2003
|
+
// src/core/middleware.ts
|
|
2004
|
+
function sleep(ms) {
|
|
2005
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1922
2006
|
}
|
|
1923
|
-
function
|
|
1924
|
-
|
|
1925
|
-
const value = source[field];
|
|
1926
|
-
return Array.isArray(value) ? value : [];
|
|
2007
|
+
function createQueueMiddleware(schedule) {
|
|
2008
|
+
return (request, next) => request.skipQueue ? next(request) : schedule(() => next(request));
|
|
1927
2009
|
}
|
|
1928
|
-
function
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
2010
|
+
function createPluginsMiddleware(plugins) {
|
|
2011
|
+
return (request, next) => {
|
|
2012
|
+
if (plugins.size === 0) return next(request);
|
|
2013
|
+
return plugins.run(request, next);
|
|
2014
|
+
};
|
|
1933
2015
|
}
|
|
1934
|
-
function
|
|
1935
|
-
if (
|
|
1936
|
-
const
|
|
1937
|
-
return
|
|
2016
|
+
async function applyAuth(request, deps) {
|
|
2017
|
+
if (request.skipAuth) return request;
|
|
2018
|
+
const headers = await deps.getAuthHeaders();
|
|
2019
|
+
return Object.keys(headers).length > 0 ? withLayerHeaders(request, headers) : request;
|
|
1938
2020
|
}
|
|
1939
|
-
function
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
2021
|
+
function createAuthMiddleware(deps) {
|
|
2022
|
+
return async (request, next) => {
|
|
2023
|
+
const authorized = await applyAuth(request, deps);
|
|
2024
|
+
try {
|
|
2025
|
+
return await next(authorized);
|
|
2026
|
+
} catch (error) {
|
|
2027
|
+
if (request.skipAuthRefresh || !deps.autoRefresh || !isItdApiError(error) || error.status !== 401) {
|
|
2028
|
+
throw error;
|
|
2029
|
+
}
|
|
2030
|
+
const refreshed = await deps.onUnauthorized();
|
|
2031
|
+
if (!refreshed) throw error;
|
|
2032
|
+
const retried = await applyAuth({ ...request, skipAuthRefresh: true }, deps);
|
|
2033
|
+
return next(retried);
|
|
2034
|
+
}
|
|
2035
|
+
};
|
|
1943
2036
|
}
|
|
1944
|
-
function
|
|
1945
|
-
if (
|
|
1946
|
-
|
|
1947
|
-
|
|
2037
|
+
function resolveBackoff(retry, global) {
|
|
2038
|
+
if (retry === void 0) return global;
|
|
2039
|
+
if (retry === false) return void 0;
|
|
2040
|
+
const resolved = resolveRetry(retry);
|
|
2041
|
+
return resolved ? createRetryScheduler(resolved) : void 0;
|
|
1948
2042
|
}
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
if (messages.length > 0) result[field] = messages;
|
|
1959
|
-
} else if (typeof value === "string") {
|
|
1960
|
-
result[field] = [value];
|
|
1961
|
-
}
|
|
2043
|
+
function createRetryMiddleware(deps) {
|
|
2044
|
+
const globalScheduler = deps.retry ? createRetryScheduler(deps.retry) : void 0;
|
|
2045
|
+
const nextDelay = (error, attempt, method, backoff) => {
|
|
2046
|
+
if (isItdRateLimitError(error)) {
|
|
2047
|
+
const wait = error.retryAfter ?? deps.rateLimitDelays[attempt - 1];
|
|
2048
|
+
if (wait === void 0) return void 0;
|
|
2049
|
+
deps.pauseQueue?.(wait);
|
|
2050
|
+
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`);
|
|
2051
|
+
return wait;
|
|
1962
2052
|
}
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
2053
|
+
return backoff?.(error, attempt, method);
|
|
2054
|
+
};
|
|
2055
|
+
return async (request, next) => {
|
|
2056
|
+
const method = request.method.toUpperCase();
|
|
2057
|
+
const backoff = resolveBackoff(request.retry, globalScheduler);
|
|
2058
|
+
for (let attempt = 1; ; attempt++) {
|
|
2059
|
+
try {
|
|
2060
|
+
return await next({ ...request, attempt });
|
|
2061
|
+
} catch (error) {
|
|
2062
|
+
const delay = nextDelay(error, attempt, method, backoff);
|
|
2063
|
+
if (delay === void 0) throw error;
|
|
2064
|
+
await deps.hooks.onRetry?.({
|
|
2065
|
+
method,
|
|
2066
|
+
path: request.path,
|
|
2067
|
+
url: deps.buildUrl(request),
|
|
2068
|
+
headers: new Headers(),
|
|
2069
|
+
attempt,
|
|
2070
|
+
error,
|
|
2071
|
+
delay
|
|
2072
|
+
});
|
|
2073
|
+
deps.logger?.debug(
|
|
2074
|
+
`\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`
|
|
2075
|
+
);
|
|
2076
|
+
await sleep(delay);
|
|
2077
|
+
}
|
|
1974
2078
|
}
|
|
1975
|
-
}
|
|
1976
|
-
return result;
|
|
2079
|
+
};
|
|
1977
2080
|
}
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
2081
|
+
|
|
2082
|
+
// src/core/plugins.ts
|
|
2083
|
+
var NO_KEYS = /* @__PURE__ */ new Set();
|
|
2084
|
+
var RESERVED_OPTION_KEYS = /* @__PURE__ */ new Set([
|
|
2085
|
+
"signal",
|
|
2086
|
+
"timeout",
|
|
2087
|
+
"headers",
|
|
2088
|
+
"retry",
|
|
2089
|
+
"method",
|
|
2090
|
+
"path",
|
|
2091
|
+
"query",
|
|
2092
|
+
"body",
|
|
2093
|
+
"skipAuth",
|
|
2094
|
+
"skipAuthRefresh",
|
|
2095
|
+
"skipQueue",
|
|
2096
|
+
"raw"
|
|
2097
|
+
]);
|
|
2098
|
+
var PluginRegistry = class {
|
|
2099
|
+
#transformers = [];
|
|
2100
|
+
#optionKeys = /* @__PURE__ */ new Set();
|
|
2101
|
+
#names = /* @__PURE__ */ new Set();
|
|
2102
|
+
/** Сколько обёрток подключено. Ноль означает, что запрос идёт прежним путём. */
|
|
2103
|
+
get size() {
|
|
2104
|
+
return this.#transformers.length;
|
|
1989
2105
|
}
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
message: fallbackMessage,
|
|
1994
|
-
detail: void 0,
|
|
1995
|
-
title: void 0,
|
|
1996
|
-
fieldErrors: {},
|
|
1997
|
-
userId: void 0
|
|
1998
|
-
};
|
|
2106
|
+
/** Имена опций запроса, заявленные плагинами. */
|
|
2107
|
+
get optionKeys() {
|
|
2108
|
+
return this.#optionKeys.size === 0 ? NO_KEYS : this.#optionKeys;
|
|
1999
2109
|
}
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2110
|
+
/**
|
|
2111
|
+
* Подключает плагин.
|
|
2112
|
+
*
|
|
2113
|
+
* @throws {ItdConfigError} если плагин задан неверно, уже подключён или заявил занятое
|
|
2114
|
+
* имя опции
|
|
2115
|
+
*/
|
|
2116
|
+
add(plugin, context) {
|
|
2117
|
+
if (typeof plugin?.install !== "function") {
|
|
2118
|
+
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()");
|
|
2119
|
+
}
|
|
2120
|
+
const name = plugin.name;
|
|
2121
|
+
if (typeof name !== "string" || name.trim() === "") {
|
|
2122
|
+
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");
|
|
2123
|
+
}
|
|
2124
|
+
if (this.#names.has(name)) {
|
|
2125
|
+
throw new ItdConfigError(`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u0443\u0436\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0451\u043D`);
|
|
2126
|
+
}
|
|
2127
|
+
const keys = plugin.optionKeys ?? [];
|
|
2128
|
+
for (const key of keys) {
|
|
2129
|
+
if (typeof key !== "string" || key.trim() === "") {
|
|
2130
|
+
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`);
|
|
2131
|
+
}
|
|
2132
|
+
if (RESERVED_OPTION_KEYS.has(key)) {
|
|
2133
|
+
throw new ItdConfigError(
|
|
2134
|
+
`\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(", ")}`
|
|
2135
|
+
);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
const before = this.#transformers.length;
|
|
2139
|
+
try {
|
|
2140
|
+
plugin.install({
|
|
2141
|
+
...context,
|
|
2142
|
+
use: (transformer) => {
|
|
2143
|
+
if (typeof transformer !== "function") {
|
|
2144
|
+
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`);
|
|
2145
|
+
}
|
|
2146
|
+
this.#transformers.push(transformer);
|
|
2147
|
+
}
|
|
2148
|
+
});
|
|
2149
|
+
} catch (error) {
|
|
2150
|
+
this.#transformers.length = before;
|
|
2151
|
+
throw error;
|
|
2152
|
+
}
|
|
2153
|
+
this.#names.add(name);
|
|
2154
|
+
for (const key of keys) this.#optionKeys.add(key);
|
|
2155
|
+
}
|
|
2156
|
+
/**
|
|
2157
|
+
* Прогоняет запрос через цепочку обёрток.
|
|
2158
|
+
*
|
|
2159
|
+
* Цепочка собирается на каждый запрос заново: плагин можно подключить в любой момент,
|
|
2160
|
+
* а обёрток единицы — экономить тут не на чем.
|
|
2161
|
+
*
|
|
2162
|
+
* @param execute настоящий запрос, вызывается самой внутренней обёрткой
|
|
2163
|
+
*/
|
|
2164
|
+
run(request, execute) {
|
|
2165
|
+
const chain = this.#transformers.reduceRight(
|
|
2166
|
+
(next, transformer) => (current) => transformer(current, next),
|
|
2167
|
+
execute
|
|
2168
|
+
);
|
|
2169
|
+
return chain(request);
|
|
2170
|
+
}
|
|
2171
|
+
};
|
|
2172
|
+
|
|
2173
|
+
// src/core/rate-limit.ts
|
|
2174
|
+
var RequestQueue = class {
|
|
2175
|
+
#concurrency;
|
|
2176
|
+
/** Минимальный промежуток между стартами, мс. `0` — без ограничения частоты. */
|
|
2177
|
+
#minGap;
|
|
2178
|
+
#waiting = [];
|
|
2179
|
+
#active = 0;
|
|
2180
|
+
/** Момент, раньше которого следующий запрос стартовать не должен. */
|
|
2181
|
+
#nextSlot = 0;
|
|
2182
|
+
#timer;
|
|
2183
|
+
constructor(options) {
|
|
2184
|
+
this.#concurrency = options.concurrency;
|
|
2185
|
+
this.#minGap = options.rps ? 1e3 / options.rps : 0;
|
|
2186
|
+
}
|
|
2187
|
+
/** Сколько задач выполняется прямо сейчас. */
|
|
2188
|
+
get active() {
|
|
2189
|
+
return this.#active;
|
|
2190
|
+
}
|
|
2191
|
+
/** Сколько задач ждёт очереди. */
|
|
2192
|
+
get pending() {
|
|
2193
|
+
return this.#waiting.length;
|
|
2194
|
+
}
|
|
2195
|
+
/**
|
|
2196
|
+
* Ставит задачу в очередь.
|
|
2197
|
+
*
|
|
2198
|
+
* @returns результат задачи; ошибка задачи пробрасывается без изменений
|
|
2199
|
+
*/
|
|
2200
|
+
schedule(task) {
|
|
2201
|
+
return new Promise((resolve, reject) => {
|
|
2202
|
+
const run = () => {
|
|
2203
|
+
this.#active += 1;
|
|
2204
|
+
task().then(resolve, reject).finally(() => {
|
|
2205
|
+
this.#active -= 1;
|
|
2206
|
+
this.#drain();
|
|
2207
|
+
});
|
|
2208
|
+
};
|
|
2209
|
+
this.#waiting.push({ run, cancel: reject });
|
|
2210
|
+
this.#drain();
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
/**
|
|
2214
|
+
* Останавливает очередь: снимает отложенную паузу и отклоняет ещё не начатые задачи
|
|
2215
|
+
* ошибкой `ItdAbortError`. Уже выполняющиеся задачи доводятся до конца.
|
|
2216
|
+
*/
|
|
2217
|
+
stop() {
|
|
2218
|
+
if (this.#timer !== void 0) {
|
|
2219
|
+
clearTimeout(this.#timer);
|
|
2220
|
+
this.#timer = void 0;
|
|
2221
|
+
}
|
|
2222
|
+
this.#nextSlot = 0;
|
|
2223
|
+
const pending = this.#waiting.splice(0, this.#waiting.length);
|
|
2224
|
+
for (const task of pending) {
|
|
2225
|
+
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"));
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
/**
|
|
2229
|
+
* Придерживает всю очередь на заданное время.
|
|
2230
|
+
*
|
|
2231
|
+
* Вызывается при получении `429` с заголовком `Retry-After`: тормозить нужно все запросы,
|
|
2232
|
+
* а не только тот, который наткнулся на лимит, — иначе остальные продолжат добивать API.
|
|
2233
|
+
*/
|
|
2234
|
+
pause(ms) {
|
|
2235
|
+
if (ms <= 0) return;
|
|
2236
|
+
this.#nextSlot = Math.max(this.#nextSlot, Date.now() + ms);
|
|
2237
|
+
}
|
|
2238
|
+
/** Запускает столько ожидающих задач, сколько позволяют ограничения. */
|
|
2239
|
+
#drain() {
|
|
2240
|
+
if (this.#waiting.length === 0) return;
|
|
2241
|
+
if (this.#active >= this.#concurrency) return;
|
|
2242
|
+
if (this.#timer !== void 0) return;
|
|
2243
|
+
const now = Date.now();
|
|
2244
|
+
if (this.#nextSlot > now) {
|
|
2245
|
+
this.#timer = setTimeout(() => {
|
|
2246
|
+
this.#timer = void 0;
|
|
2247
|
+
this.#drain();
|
|
2248
|
+
}, this.#nextSlot - now);
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
const next = this.#waiting.shift();
|
|
2252
|
+
if (!next) return;
|
|
2253
|
+
if (this.#minGap > 0) this.#nextSlot = now + this.#minGap;
|
|
2254
|
+
next.run();
|
|
2255
|
+
this.#drain();
|
|
2256
|
+
}
|
|
2257
|
+
};
|
|
2258
|
+
|
|
2259
|
+
// src/core/redact.ts
|
|
2260
|
+
var SECRET_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "set-cookie", "x-api-key"]);
|
|
2261
|
+
var SECRET_FIELDS = /* @__PURE__ */ new Set([
|
|
2262
|
+
"password",
|
|
2263
|
+
"oldpassword",
|
|
2264
|
+
"newpassword",
|
|
2265
|
+
"accesstoken",
|
|
2266
|
+
"refreshtoken",
|
|
2267
|
+
"currentpassword",
|
|
2268
|
+
"flowtoken",
|
|
2269
|
+
"token",
|
|
2270
|
+
"turnstiletoken",
|
|
2271
|
+
"otp"
|
|
2272
|
+
]);
|
|
2273
|
+
function maskSecret(value) {
|
|
2274
|
+
if (value.length <= 8) return "\u2026";
|
|
2275
|
+
return `${value.slice(0, 4)}\u2026(${value.length})\u2026${value.slice(-3)}`;
|
|
2276
|
+
}
|
|
2277
|
+
function redactHeaders(headers) {
|
|
2278
|
+
const result = {};
|
|
2279
|
+
headers.forEach((value, name) => {
|
|
2280
|
+
if (SECRET_HEADERS.has(name.toLowerCase())) {
|
|
2281
|
+
const spaceAt = value.indexOf(" ");
|
|
2282
|
+
result[name] = spaceAt > 0 ? `${value.slice(0, spaceAt)} ${maskSecret(value.slice(spaceAt + 1))}` : maskSecret(value);
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
result[name] = value;
|
|
2286
|
+
});
|
|
2287
|
+
return result;
|
|
2288
|
+
}
|
|
2289
|
+
function redactBody(body) {
|
|
2290
|
+
if (body === null || body === void 0) return body;
|
|
2291
|
+
if (typeof FormData !== "undefined" && body instanceof FormData) return "[FormData]";
|
|
2292
|
+
if (isBlob(body)) return "[Blob]";
|
|
2293
|
+
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return "[binary]";
|
|
2294
|
+
if (Array.isArray(body)) return body.map(redactBody);
|
|
2295
|
+
if (typeof body === "object") {
|
|
2296
|
+
const result = {};
|
|
2297
|
+
for (const [key, value] of Object.entries(body)) {
|
|
2298
|
+
result[key] = SECRET_FIELDS.has(key.toLowerCase()) ? "[\u0441\u043A\u0440\u044B\u0442\u043E]" : redactBody(value);
|
|
2299
|
+
}
|
|
2300
|
+
return result;
|
|
2301
|
+
}
|
|
2302
|
+
return body;
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
// src/core/unwrap.ts
|
|
2306
|
+
function unwrapData(body) {
|
|
2307
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) return body;
|
|
2308
|
+
const keys = Object.keys(body);
|
|
2309
|
+
if (keys.length !== 1 || keys[0] !== "data") return body;
|
|
2310
|
+
return body.data;
|
|
2311
|
+
}
|
|
2312
|
+
function isRecord(value) {
|
|
2313
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2314
|
+
}
|
|
2315
|
+
function asString(value) {
|
|
2316
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2317
|
+
}
|
|
2318
|
+
function pickArray(source, field) {
|
|
2319
|
+
if (typeof source !== "object" || source === null) return [];
|
|
2320
|
+
const value = source[field];
|
|
2321
|
+
return Array.isArray(value) ? value : [];
|
|
2322
|
+
}
|
|
2323
|
+
function pickObject(source, field) {
|
|
2324
|
+
if (typeof source !== "object" || source === null) return void 0;
|
|
2325
|
+
const value = source[field];
|
|
2326
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
2327
|
+
return value;
|
|
2328
|
+
}
|
|
2329
|
+
function pickBoolean(source, field, fallback = false) {
|
|
2330
|
+
if (typeof source !== "object" || source === null) return fallback;
|
|
2331
|
+
const value = source[field];
|
|
2332
|
+
return typeof value === "boolean" ? value : fallback;
|
|
2333
|
+
}
|
|
2334
|
+
function pickNumber(source, field, fallback) {
|
|
2335
|
+
if (typeof source !== "object" || source === null) return fallback;
|
|
2336
|
+
const value = source[field];
|
|
2337
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
2338
|
+
}
|
|
2339
|
+
function pickString(source, field) {
|
|
2340
|
+
if (typeof source !== "object" || source === null) return void 0;
|
|
2341
|
+
const value = source[field];
|
|
2342
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
// src/core/error-factory.ts
|
|
2346
|
+
function collectFieldErrors(source) {
|
|
2347
|
+
const result = {};
|
|
2348
|
+
const errors = source.errors;
|
|
2349
|
+
if (isRecord(errors)) {
|
|
2350
|
+
for (const [field, value] of Object.entries(errors)) {
|
|
2351
|
+
if (Array.isArray(value)) {
|
|
2352
|
+
const messages = value.filter((item) => typeof item === "string");
|
|
2353
|
+
if (messages.length > 0) result[field] = messages;
|
|
2354
|
+
} else if (typeof value === "string") {
|
|
2355
|
+
result[field] = [value];
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
const violations = source.violations;
|
|
2360
|
+
if (Array.isArray(violations)) {
|
|
2361
|
+
for (const violation of violations) {
|
|
2362
|
+
if (!isRecord(violation)) continue;
|
|
2363
|
+
const field = asString(violation.field) ?? asString(violation.property);
|
|
2364
|
+
const message = asString(violation.message);
|
|
2365
|
+
if (!field || !message) continue;
|
|
2366
|
+
const existing = result[field];
|
|
2367
|
+
if (existing) existing.push(message);
|
|
2368
|
+
else result[field] = [message];
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
return result;
|
|
2372
|
+
}
|
|
2373
|
+
function parseErrorBody(body, status, statusText = "") {
|
|
2374
|
+
const fallbackMessage = statusText ? `HTTP ${status} ${statusText}` : `HTTP ${status}`;
|
|
2375
|
+
if (typeof body === "string") {
|
|
2376
|
+
return {
|
|
2377
|
+
code: "UNKNOWN_ERROR",
|
|
2378
|
+
message: asString(body.trim()) ?? fallbackMessage,
|
|
2379
|
+
detail: void 0,
|
|
2380
|
+
title: void 0,
|
|
2381
|
+
fieldErrors: {},
|
|
2382
|
+
userId: void 0
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
if (!isRecord(body)) {
|
|
2386
|
+
return {
|
|
2387
|
+
code: "UNKNOWN_ERROR",
|
|
2388
|
+
message: fallbackMessage,
|
|
2389
|
+
detail: void 0,
|
|
2390
|
+
title: void 0,
|
|
2391
|
+
fieldErrors: {},
|
|
2392
|
+
userId: void 0
|
|
2393
|
+
};
|
|
2394
|
+
}
|
|
2395
|
+
if (body.type === "validation") {
|
|
2396
|
+
const target = asString(body.on);
|
|
2397
|
+
return {
|
|
2398
|
+
code: "VALIDATION_ERROR",
|
|
2399
|
+
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",
|
|
2400
|
+
detail: void 0,
|
|
2401
|
+
title: void 0,
|
|
2402
|
+
fieldErrors: {},
|
|
2008
2403
|
userId: void 0
|
|
2009
2404
|
};
|
|
2010
2405
|
}
|
|
@@ -2107,11 +2502,7 @@ function createApiError(context) {
|
|
|
2107
2502
|
return new Ctor(init);
|
|
2108
2503
|
}
|
|
2109
2504
|
|
|
2110
|
-
// src/core/
|
|
2111
|
-
function sleep(ms) {
|
|
2112
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2113
|
-
}
|
|
2114
|
-
var EMPTY_KEYS = /* @__PURE__ */ new Set();
|
|
2505
|
+
// src/core/transport.ts
|
|
2115
2506
|
function setHeader(headers, name, value) {
|
|
2116
2507
|
try {
|
|
2117
2508
|
headers.set(name, value);
|
|
@@ -2158,141 +2549,51 @@ function createAbortBundle(userSignal, timeout) {
|
|
|
2158
2549
|
timedOut: () => timedOut,
|
|
2159
2550
|
cleanup: () => {
|
|
2160
2551
|
if (timer !== void 0) clearTimeout(timer);
|
|
2161
|
-
userSignal?.removeEventListener("abort", onUserAbort);
|
|
2162
|
-
}
|
|
2163
|
-
};
|
|
2164
|
-
}
|
|
2165
|
-
var HttpClient = class {
|
|
2166
|
-
#config;
|
|
2167
|
-
#collaborators;
|
|
2168
|
-
#plugins;
|
|
2169
|
-
constructor(config, collaborators = {}) {
|
|
2170
|
-
this.#config = config;
|
|
2171
|
-
this.#collaborators = collaborators;
|
|
2172
|
-
}
|
|
2173
|
-
/** Базовый URL, к которому обращается клиент. */
|
|
2174
|
-
get baseUrl() {
|
|
2175
|
-
return this.#config.baseUrl;
|
|
2176
|
-
}
|
|
2177
|
-
/**
|
|
2178
|
-
* Имена опций запроса, заявленные плагинами.
|
|
2179
|
-
*
|
|
2180
|
-
* Читается ресурсами: они переносят в транспорт только известные поля, а чужие,
|
|
2181
|
-
* если их никто не заявил, отсеивают.
|
|
2182
|
-
*/
|
|
2183
|
-
get pluginOptionKeys() {
|
|
2184
|
-
return this.#plugins?.optionKeys ?? EMPTY_KEYS;
|
|
2185
|
-
}
|
|
2186
|
-
/** Подключает список плагинов. Реестр общий с клиентом и пополняется через `itd.use()`. */
|
|
2187
|
-
usePlugins(plugins) {
|
|
2188
|
-
this.#plugins = plugins;
|
|
2189
|
-
}
|
|
2190
|
-
/**
|
|
2191
|
-
* Подключает недостающие части конвейера.
|
|
2192
|
-
*
|
|
2193
|
-
* Нужно из-за кольцевой зависимости: слой авторизации сам выполняет запросы, поэтому
|
|
2194
|
-
* не может быть передан в конструктор до создания транспорта.
|
|
2195
|
-
*/
|
|
2196
|
-
setCollaborators(collaborators) {
|
|
2197
|
-
this.#collaborators = { ...this.#collaborators, ...collaborators };
|
|
2198
|
-
}
|
|
2199
|
-
/**
|
|
2200
|
-
* Выполняет запрос к API.
|
|
2201
|
-
*
|
|
2202
|
-
* @typeParam T ожидаемая форма ответа после снятия обёртки `{ data: … }`
|
|
2203
|
-
* @throws {ItdApiError} если сервер ответил статусом ≥ 400
|
|
2204
|
-
* @throws {ItdTimeoutError} если истёк таймаут
|
|
2205
|
-
* @throws {ItdAbortError} если запрос отменён через `signal`
|
|
2206
|
-
* @throws {ItdNetworkError} если запрос не дошёл до сервера
|
|
2207
|
-
*/
|
|
2208
|
-
async request(options) {
|
|
2209
|
-
const task = () => this.#withPlugins(options);
|
|
2210
|
-
if (!this.#collaborators.schedule || options.skipQueue) return task();
|
|
2211
|
-
return this.#collaborators.schedule(task);
|
|
2212
|
-
}
|
|
2213
|
-
/**
|
|
2214
|
-
* Прогоняет запрос через обёртки плагинов.
|
|
2215
|
-
*
|
|
2216
|
-
* Цепочка стоит **снаружи повторов и внутри очереди**: плагин должен увидеть запрос
|
|
2217
|
-
* и ответ по одному разу, независимо от того, сколько попыток понадобилось, — иначе,
|
|
2218
|
-
* например, текст поста зашифруется повторно на второй попытке.
|
|
2219
|
-
*/
|
|
2220
|
-
#withPlugins(options) {
|
|
2221
|
-
const plugins = this.#plugins;
|
|
2222
|
-
if (!plugins || plugins.size === 0) return this.#withRetries(options);
|
|
2223
|
-
return plugins.run(options, (request) => this.#withRetries(request));
|
|
2224
|
-
}
|
|
2225
|
-
async #withRetries(options) {
|
|
2226
|
-
const method = options.method.toUpperCase();
|
|
2227
|
-
for (let attempt = 1; ; attempt++) {
|
|
2228
|
-
try {
|
|
2229
|
-
return await this.#attempt(options, attempt);
|
|
2230
|
-
} catch (error) {
|
|
2231
|
-
const delay = this.#collaborators.nextRetryDelay?.(error, attempt, method);
|
|
2232
|
-
if (delay === void 0) throw error;
|
|
2233
|
-
await this.#config.hooks.onRetry?.({
|
|
2234
|
-
method,
|
|
2235
|
-
path: options.path,
|
|
2236
|
-
url: this.#buildUrl(options),
|
|
2237
|
-
headers: new Headers(),
|
|
2238
|
-
attempt,
|
|
2239
|
-
error,
|
|
2240
|
-
delay
|
|
2241
|
-
});
|
|
2242
|
-
this.#config.logger?.debug(
|
|
2243
|
-
`\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`
|
|
2244
|
-
);
|
|
2245
|
-
await sleep(delay);
|
|
2246
|
-
}
|
|
2552
|
+
userSignal?.removeEventListener("abort", onUserAbort);
|
|
2247
2553
|
}
|
|
2554
|
+
};
|
|
2555
|
+
}
|
|
2556
|
+
var Transport = class {
|
|
2557
|
+
#config;
|
|
2558
|
+
#deps;
|
|
2559
|
+
constructor(config, deps) {
|
|
2560
|
+
this.#config = config;
|
|
2561
|
+
this.#deps = deps;
|
|
2248
2562
|
}
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
async #buildHeaders(options, url) {
|
|
2253
|
-
const headers = new Headers();
|
|
2254
|
-
headers.set("Accept", "application/json");
|
|
2255
|
-
headers.set("X-Requested-With", "XMLHttpRequest");
|
|
2256
|
-
if (this.#config.userAgent) setHeader(headers, "User-Agent", this.#config.userAgent);
|
|
2257
|
-
if (this.#collaborators.getDeviceId) {
|
|
2258
|
-
setHeader(headers, "X-Device-Id", await this.#collaborators.getDeviceId());
|
|
2259
|
-
}
|
|
2260
|
-
for (const [name, value] of Object.entries(this.#config.headers))
|
|
2261
|
-
setHeader(headers, name, value);
|
|
2262
|
-
if (!options.skipAuth && this.#collaborators.getAuthHeaders) {
|
|
2263
|
-
const auth = await this.#collaborators.getAuthHeaders();
|
|
2264
|
-
for (const [name, value] of Object.entries(auth)) setHeader(headers, name, value);
|
|
2265
|
-
}
|
|
2266
|
-
if (this.#config.useCookieJar && this.#collaborators.getCookieHeader) {
|
|
2267
|
-
const cookie = this.#collaborators.getCookieHeader(url);
|
|
2268
|
-
if (cookie) setHeader(headers, "Cookie", cookie);
|
|
2269
|
-
}
|
|
2270
|
-
for (const [name, value] of Object.entries(options.headers ?? {})) {
|
|
2271
|
-
setHeader(headers, name, value);
|
|
2272
|
-
}
|
|
2273
|
-
return headers;
|
|
2563
|
+
/** Базовый URL, к которому обращается транспорт. */
|
|
2564
|
+
get baseUrl() {
|
|
2565
|
+
return this.#config.baseUrl;
|
|
2274
2566
|
}
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2567
|
+
/**
|
|
2568
|
+
* Выполняет один сетевой запрос.
|
|
2569
|
+
*
|
|
2570
|
+
* @throws {ItdApiError} если сервер ответил статусом ≥ 400
|
|
2571
|
+
* @throws {ItdTimeoutError} если истёк таймаут
|
|
2572
|
+
* @throws {ItdAbortError} если запрос отменён через `signal`
|
|
2573
|
+
* @throws {ItdNetworkError} если запрос не дошёл до сервера
|
|
2574
|
+
*/
|
|
2575
|
+
send = async (request) => {
|
|
2576
|
+
const method = request.method.toUpperCase();
|
|
2577
|
+
const url = this.buildUrl(request);
|
|
2578
|
+
const headers = await this.#buildHeaders(request, url);
|
|
2579
|
+
const attempt = request.attempt ?? 1;
|
|
2279
2580
|
let body;
|
|
2280
|
-
if (
|
|
2281
|
-
if (isRawBody(
|
|
2282
|
-
body =
|
|
2581
|
+
if (request.body !== void 0 && request.body !== null) {
|
|
2582
|
+
if (isRawBody(request.body)) {
|
|
2583
|
+
body = request.body;
|
|
2283
2584
|
} else {
|
|
2284
|
-
body = JSON.stringify(
|
|
2585
|
+
body = JSON.stringify(request.body);
|
|
2285
2586
|
if (!headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
2286
2587
|
}
|
|
2287
2588
|
}
|
|
2288
|
-
const context = { method, path:
|
|
2589
|
+
const context = { method, path: request.path, url, headers, attempt };
|
|
2289
2590
|
await this.#config.hooks.onRequest?.(context);
|
|
2290
|
-
const timeout =
|
|
2291
|
-
const abort = createAbortBundle(
|
|
2591
|
+
const timeout = request.timeout ?? this.#config.timeout;
|
|
2592
|
+
const abort = createAbortBundle(request.signal, timeout);
|
|
2292
2593
|
const startedAt = Date.now();
|
|
2293
|
-
this.#config.logger?.debug(`\u2192 ${method} ${
|
|
2594
|
+
this.#config.logger?.debug(`\u2192 ${method} ${request.path}`, {
|
|
2294
2595
|
headers: redactHeaders(headers),
|
|
2295
|
-
body: redactBody(
|
|
2596
|
+
body: redactBody(request.body)
|
|
2296
2597
|
});
|
|
2297
2598
|
let response;
|
|
2298
2599
|
try {
|
|
@@ -2305,31 +2606,24 @@ var HttpClient = class {
|
|
|
2305
2606
|
});
|
|
2306
2607
|
} catch (error) {
|
|
2307
2608
|
const duration2 = Date.now() - startedAt;
|
|
2308
|
-
const failure = this.#toTransportError(error, abort,
|
|
2609
|
+
const failure = this.#toTransportError(error, abort, request, method, timeout);
|
|
2309
2610
|
await this.#config.hooks.onError?.({ ...context, duration: duration2, error: failure });
|
|
2310
|
-
this.#config.logger?.warn(`\xD7 ${method} ${
|
|
2611
|
+
this.#config.logger?.warn(`\xD7 ${method} ${request.path} (${duration2} \u043C\u0441): ${failure.message}`);
|
|
2311
2612
|
throw failure;
|
|
2312
2613
|
} finally {
|
|
2313
2614
|
abort.cleanup();
|
|
2314
2615
|
}
|
|
2315
2616
|
const duration = Date.now() - startedAt;
|
|
2316
|
-
if (this.#
|
|
2617
|
+
if (this.#deps.onRateLimit) {
|
|
2317
2618
|
const { limit, remaining } = readRateLimit(response.headers);
|
|
2318
|
-
this.#
|
|
2619
|
+
this.#deps.onRateLimit(limit, remaining);
|
|
2319
2620
|
}
|
|
2320
|
-
if (this.#config.useCookieJar) this.#
|
|
2621
|
+
if (this.#config.useCookieJar) this.#deps.cookies?.setFromResponse(url, response);
|
|
2321
2622
|
const payload = await readBody(response);
|
|
2322
2623
|
if (!response.ok) {
|
|
2323
|
-
if (response.status === 401 && !options.skipAuthRefresh && this.#config.autoRefresh && this.#collaborators.onUnauthorized) {
|
|
2324
|
-
const refreshed = await this.#collaborators.onUnauthorized();
|
|
2325
|
-
if (refreshed) {
|
|
2326
|
-
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}`);
|
|
2327
|
-
return this.#attempt({ ...options, skipAuthRefresh: true }, attempt);
|
|
2328
|
-
}
|
|
2329
|
-
}
|
|
2330
2624
|
const error = createApiError({
|
|
2331
2625
|
method,
|
|
2332
|
-
path:
|
|
2626
|
+
path: request.path,
|
|
2333
2627
|
status: response.status,
|
|
2334
2628
|
statusText: response.statusText,
|
|
2335
2629
|
headers: response.headers,
|
|
@@ -2338,7 +2632,7 @@ var HttpClient = class {
|
|
|
2338
2632
|
});
|
|
2339
2633
|
await this.#config.hooks.onError?.({ ...context, duration, error });
|
|
2340
2634
|
this.#config.logger?.warn(
|
|
2341
|
-
`\u2190 ${response.status} ${method} ${
|
|
2635
|
+
`\u2190 ${response.status} ${method} ${request.path} (${duration} \u043C\u0441): ${error.message}`
|
|
2342
2636
|
);
|
|
2343
2637
|
throw error;
|
|
2344
2638
|
}
|
|
@@ -2348,220 +2642,57 @@ var HttpClient = class {
|
|
|
2348
2642
|
duration,
|
|
2349
2643
|
response
|
|
2350
2644
|
});
|
|
2351
|
-
this.#config.logger?.debug(`\u2190 ${response.status} ${method} ${
|
|
2352
|
-
return
|
|
2353
|
-
}
|
|
2354
|
-
/**
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
if (aborted && abort.timedOut()) {
|
|
2358
|
-
return new ItdTimeoutError({ timeout, method, path: options.path });
|
|
2359
|
-
}
|
|
2360
|
-
if (aborted) {
|
|
2361
|
-
return new ItdAbortError(`\u0417\u0430\u043F\u0440\u043E\u0441 ${method} ${options.path} \u043E\u0442\u043C\u0435\u043D\u0451\u043D`);
|
|
2362
|
-
}
|
|
2363
|
-
return new ItdNetworkError(
|
|
2364
|
-
`\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)}`,
|
|
2365
|
-
{ method, path: options.path, cause: error }
|
|
2366
|
-
);
|
|
2367
|
-
}
|
|
2368
|
-
};
|
|
2369
|
-
|
|
2370
|
-
// src/core/plugins.ts
|
|
2371
|
-
var NO_KEYS = /* @__PURE__ */ new Set();
|
|
2372
|
-
var RESERVED_OPTION_KEYS = /* @__PURE__ */ new Set([
|
|
2373
|
-
"signal",
|
|
2374
|
-
"timeout",
|
|
2375
|
-
"headers",
|
|
2376
|
-
"retry",
|
|
2377
|
-
"method",
|
|
2378
|
-
"path",
|
|
2379
|
-
"query",
|
|
2380
|
-
"body",
|
|
2381
|
-
"skipAuth",
|
|
2382
|
-
"skipAuthRefresh",
|
|
2383
|
-
"skipQueue",
|
|
2384
|
-
"raw"
|
|
2385
|
-
]);
|
|
2386
|
-
var PluginRegistry = class {
|
|
2387
|
-
#transformers = [];
|
|
2388
|
-
#optionKeys = /* @__PURE__ */ new Set();
|
|
2389
|
-
#names = /* @__PURE__ */ new Set();
|
|
2390
|
-
/** Сколько обёрток подключено. Ноль означает, что запрос идёт прежним путём. */
|
|
2391
|
-
get size() {
|
|
2392
|
-
return this.#transformers.length;
|
|
2393
|
-
}
|
|
2394
|
-
/** Имена опций запроса, заявленные плагинами. */
|
|
2395
|
-
get optionKeys() {
|
|
2396
|
-
return this.#optionKeys.size === 0 ? NO_KEYS : this.#optionKeys;
|
|
2645
|
+
this.#config.logger?.debug(`\u2190 ${response.status} ${method} ${request.path} (${duration} \u043C\u0441)`);
|
|
2646
|
+
return request.raw ? payload : unwrapData(payload);
|
|
2647
|
+
};
|
|
2648
|
+
/** Итоговый URL со строкой запроса. Нужен и слою повторов — для хука `onRetry`. */
|
|
2649
|
+
buildUrl(request) {
|
|
2650
|
+
return joinUrl(this.#config.baseUrl, request.path) + buildQuery(request.query);
|
|
2397
2651
|
}
|
|
2398
2652
|
/**
|
|
2399
|
-
*
|
|
2653
|
+
* Собирает заголовки запроса.
|
|
2400
2654
|
*
|
|
2401
|
-
*
|
|
2402
|
-
*
|
|
2655
|
+
* Порядок важен: сначала умолчания библиотеки, затем заголовки клиента, затем то,
|
|
2656
|
+
* что добавили слои конвейера (авторизация), и только в самом конце — заголовки
|
|
2657
|
+
* конкретного вызова. Так пользователь может переопределить что угодно.
|
|
2403
2658
|
*/
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2659
|
+
async #buildHeaders(request, url) {
|
|
2660
|
+
const headers = new Headers();
|
|
2661
|
+
headers.set("Accept", "application/json");
|
|
2662
|
+
headers.set("X-Requested-With", "XMLHttpRequest");
|
|
2663
|
+
if (this.#config.userAgent) setHeader(headers, "User-Agent", this.#config.userAgent);
|
|
2664
|
+
if (this.#deps.getDeviceId) {
|
|
2665
|
+
setHeader(headers, "X-Device-Id", await this.#deps.getDeviceId());
|
|
2407
2666
|
}
|
|
2408
|
-
const name
|
|
2409
|
-
|
|
2410
|
-
|
|
2667
|
+
for (const [name, value] of Object.entries(this.#config.headers))
|
|
2668
|
+
setHeader(headers, name, value);
|
|
2669
|
+
for (const [name, value] of Object.entries(request.layerHeaders ?? {}))
|
|
2670
|
+
setHeader(headers, name, value);
|
|
2671
|
+
if (this.#config.useCookieJar && this.#deps.cookies) {
|
|
2672
|
+
const cookie = this.#deps.cookies.getHeader(url);
|
|
2673
|
+
if (cookie) setHeader(headers, "Cookie", cookie);
|
|
2411
2674
|
}
|
|
2412
|
-
|
|
2413
|
-
|
|
2675
|
+
for (const [name, value] of Object.entries(request.headers ?? {})) {
|
|
2676
|
+
setHeader(headers, name, value);
|
|
2414
2677
|
}
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
`\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(", ")}`
|
|
2423
|
-
);
|
|
2424
|
-
}
|
|
2678
|
+
return headers;
|
|
2679
|
+
}
|
|
2680
|
+
/** Превращает исключение `fetch` в понятную ошибку библиотеки. */
|
|
2681
|
+
#toTransportError(error, abort, request, method, timeout) {
|
|
2682
|
+
const aborted = error instanceof Error && error.name === "AbortError";
|
|
2683
|
+
if (aborted && abort.timedOut()) {
|
|
2684
|
+
return new ItdTimeoutError({ timeout, method, path: request.path });
|
|
2425
2685
|
}
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
plugin.install({
|
|
2429
|
-
...context,
|
|
2430
|
-
use: (transformer) => {
|
|
2431
|
-
if (typeof transformer !== "function") {
|
|
2432
|
-
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`);
|
|
2433
|
-
}
|
|
2434
|
-
this.#transformers.push(transformer);
|
|
2435
|
-
}
|
|
2436
|
-
});
|
|
2437
|
-
} catch (error) {
|
|
2438
|
-
this.#transformers.length = before;
|
|
2439
|
-
throw error;
|
|
2686
|
+
if (aborted) {
|
|
2687
|
+
return new ItdAbortError(`\u0417\u0430\u043F\u0440\u043E\u0441 ${method} ${request.path} \u043E\u0442\u043C\u0435\u043D\u0451\u043D`);
|
|
2440
2688
|
}
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
/**
|
|
2445
|
-
* Прогоняет запрос через цепочку обёрток.
|
|
2446
|
-
*
|
|
2447
|
-
* Цепочка собирается на каждый запрос заново: плагин можно подключить в любой момент,
|
|
2448
|
-
* а обёрток единицы — экономить тут не на чем.
|
|
2449
|
-
*
|
|
2450
|
-
* @param execute настоящий запрос, вызывается самой внутренней обёрткой
|
|
2451
|
-
*/
|
|
2452
|
-
run(request, execute) {
|
|
2453
|
-
const chain = this.#transformers.reduceRight(
|
|
2454
|
-
(next, transformer) => (current) => transformer(current, next),
|
|
2455
|
-
execute
|
|
2689
|
+
return new ItdNetworkError(
|
|
2690
|
+
`\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)}`,
|
|
2691
|
+
{ method, path: request.path, cause: error }
|
|
2456
2692
|
);
|
|
2457
|
-
return chain(request);
|
|
2458
|
-
}
|
|
2459
|
-
};
|
|
2460
|
-
|
|
2461
|
-
// src/core/rate-limit.ts
|
|
2462
|
-
var RequestQueue = class {
|
|
2463
|
-
#concurrency;
|
|
2464
|
-
/** Минимальный промежуток между стартами, мс. `0` — без ограничения частоты. */
|
|
2465
|
-
#minGap;
|
|
2466
|
-
#waiting = [];
|
|
2467
|
-
#active = 0;
|
|
2468
|
-
/** Момент, раньше которого следующий запрос стартовать не должен. */
|
|
2469
|
-
#nextSlot = 0;
|
|
2470
|
-
#timer;
|
|
2471
|
-
constructor(options) {
|
|
2472
|
-
this.#concurrency = options.concurrency;
|
|
2473
|
-
this.#minGap = options.rps ? 1e3 / options.rps : 0;
|
|
2474
|
-
}
|
|
2475
|
-
/** Сколько задач выполняется прямо сейчас. */
|
|
2476
|
-
get active() {
|
|
2477
|
-
return this.#active;
|
|
2478
|
-
}
|
|
2479
|
-
/** Сколько задач ждёт очереди. */
|
|
2480
|
-
get pending() {
|
|
2481
|
-
return this.#waiting.length;
|
|
2482
|
-
}
|
|
2483
|
-
/**
|
|
2484
|
-
* Ставит задачу в очередь.
|
|
2485
|
-
*
|
|
2486
|
-
* @returns результат задачи; ошибка задачи пробрасывается без изменений
|
|
2487
|
-
*/
|
|
2488
|
-
schedule(task) {
|
|
2489
|
-
return new Promise((resolve, reject) => {
|
|
2490
|
-
const run = () => {
|
|
2491
|
-
this.#active += 1;
|
|
2492
|
-
task().then(resolve, reject).finally(() => {
|
|
2493
|
-
this.#active -= 1;
|
|
2494
|
-
this.#drain();
|
|
2495
|
-
});
|
|
2496
|
-
};
|
|
2497
|
-
this.#waiting.push({ run });
|
|
2498
|
-
this.#drain();
|
|
2499
|
-
});
|
|
2500
|
-
}
|
|
2501
|
-
/**
|
|
2502
|
-
* Придерживает всю очередь на заданное время.
|
|
2503
|
-
*
|
|
2504
|
-
* Вызывается при получении `429` с заголовком `Retry-After`: тормозить нужно все запросы,
|
|
2505
|
-
* а не только тот, который наткнулся на лимит, — иначе остальные продолжат добивать API.
|
|
2506
|
-
*/
|
|
2507
|
-
pause(ms) {
|
|
2508
|
-
if (ms <= 0) return;
|
|
2509
|
-
this.#nextSlot = Math.max(this.#nextSlot, Date.now() + ms);
|
|
2510
|
-
}
|
|
2511
|
-
/** Запускает столько ожидающих задач, сколько позволяют ограничения. */
|
|
2512
|
-
#drain() {
|
|
2513
|
-
if (this.#waiting.length === 0) return;
|
|
2514
|
-
if (this.#active >= this.#concurrency) return;
|
|
2515
|
-
if (this.#timer !== void 0) return;
|
|
2516
|
-
const now = Date.now();
|
|
2517
|
-
if (this.#nextSlot > now) {
|
|
2518
|
-
this.#timer = setTimeout(() => {
|
|
2519
|
-
this.#timer = void 0;
|
|
2520
|
-
this.#drain();
|
|
2521
|
-
}, this.#nextSlot - now);
|
|
2522
|
-
return;
|
|
2523
|
-
}
|
|
2524
|
-
const next = this.#waiting.shift();
|
|
2525
|
-
if (!next) return;
|
|
2526
|
-
if (this.#minGap > 0) this.#nextSlot = now + this.#minGap;
|
|
2527
|
-
next.run();
|
|
2528
|
-
this.#drain();
|
|
2529
2693
|
}
|
|
2530
2694
|
};
|
|
2531
2695
|
|
|
2532
|
-
// src/core/retry.ts
|
|
2533
|
-
var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
2534
|
-
function isRetryable(error, method, retryWrites) {
|
|
2535
|
-
if (error instanceof ItdAbortError) return false;
|
|
2536
|
-
const safeToRepeat = retryWrites || IDEMPOTENT_METHODS.has(method);
|
|
2537
|
-
if (error instanceof ItdApiError) {
|
|
2538
|
-
if (error.status === 429) return true;
|
|
2539
|
-
if (error.status >= 500) return safeToRepeat;
|
|
2540
|
-
return false;
|
|
2541
|
-
}
|
|
2542
|
-
if (error instanceof ItdNetworkError || error instanceof ItdTimeoutError) return safeToRepeat;
|
|
2543
|
-
return false;
|
|
2544
|
-
}
|
|
2545
|
-
function backoffDelay(attempt, options, random) {
|
|
2546
|
-
const exponential = options.baseDelay * 2 ** (attempt - 1);
|
|
2547
|
-
const capped = Math.min(exponential, options.maxDelay);
|
|
2548
|
-
const spread = capped * options.jitter * (random() * 2 - 1);
|
|
2549
|
-
return Math.max(0, Math.round(capped + spread));
|
|
2550
|
-
}
|
|
2551
|
-
function createRetryScheduler(options, random = Math.random) {
|
|
2552
|
-
return (error, attempt, method) => {
|
|
2553
|
-
if (attempt >= options.attempts) return void 0;
|
|
2554
|
-
if (options.shouldRetry) {
|
|
2555
|
-
return options.shouldRetry(error, attempt) ? backoffDelay(attempt, options, random) : void 0;
|
|
2556
|
-
}
|
|
2557
|
-
if (!isRetryable(error, method, options.retryWrites)) return void 0;
|
|
2558
|
-
if (error instanceof ItdApiError && error.retryAfter !== void 0) {
|
|
2559
|
-
return error.retryAfter > options.maxDelay ? void 0 : error.retryAfter;
|
|
2560
|
-
}
|
|
2561
|
-
return backoffDelay(attempt, options, random);
|
|
2562
|
-
};
|
|
2563
|
-
}
|
|
2564
|
-
|
|
2565
2696
|
// src/notifications/type-map.ts
|
|
2566
2697
|
var NOTIFICATION_TYPE_ALIASES = Object.freeze({
|
|
2567
2698
|
like: NotificationType.PostReaction,
|
|
@@ -3000,7 +3131,7 @@ var RealtimeTransportKind = Object.freeze({
|
|
|
3000
3131
|
var ItdRealtime = class {
|
|
3001
3132
|
#deps;
|
|
3002
3133
|
#options;
|
|
3003
|
-
#emitter
|
|
3134
|
+
#emitter;
|
|
3004
3135
|
#transport;
|
|
3005
3136
|
#maxAttempts;
|
|
3006
3137
|
#controller;
|
|
@@ -3022,6 +3153,11 @@ var ItdRealtime = class {
|
|
|
3022
3153
|
this.#options = options;
|
|
3023
3154
|
this.#maxAttempts = options.maxAttempts ?? MAX_RECONNECT_ATTEMPTS;
|
|
3024
3155
|
this.#transport = this.#createTransport();
|
|
3156
|
+
this.#emitter = new Emitter((error) => {
|
|
3157
|
+
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";
|
|
3158
|
+
if (deps.logger) deps.logger.error(message, error);
|
|
3159
|
+
else console.error(`[itd-api] ${message}`, error);
|
|
3160
|
+
});
|
|
3025
3161
|
}
|
|
3026
3162
|
/** Текущее состояние соединения. */
|
|
3027
3163
|
get status() {
|
|
@@ -3073,6 +3209,7 @@ var ItdRealtime = class {
|
|
|
3073
3209
|
this.#controller = void 0;
|
|
3074
3210
|
this.#attempt = 0;
|
|
3075
3211
|
this.#setStatus(RealtimeStatus.Disconnected);
|
|
3212
|
+
this.#deps.onClose?.();
|
|
3076
3213
|
}
|
|
3077
3214
|
/** Снимает все подписки. Соединение при этом не закрывается. */
|
|
3078
3215
|
removeAllListeners() {
|
|
@@ -3371,6 +3508,14 @@ var Paginator = class {
|
|
|
3371
3508
|
}
|
|
3372
3509
|
};
|
|
3373
3510
|
|
|
3511
|
+
// src/types/options.ts
|
|
3512
|
+
var REQUEST_OPTION_KEYS = [
|
|
3513
|
+
"signal",
|
|
3514
|
+
"timeout",
|
|
3515
|
+
"headers",
|
|
3516
|
+
"retry"
|
|
3517
|
+
];
|
|
3518
|
+
|
|
3374
3519
|
// src/resources/base.ts
|
|
3375
3520
|
var BaseResource = class {
|
|
3376
3521
|
/** @internal */
|
|
@@ -3379,28 +3524,25 @@ var BaseResource = class {
|
|
|
3379
3524
|
this.http = http;
|
|
3380
3525
|
}
|
|
3381
3526
|
/**
|
|
3382
|
-
* Переносит
|
|
3527
|
+
* Переносит опции запроса в описание транспорта.
|
|
3383
3528
|
*
|
|
3384
|
-
*
|
|
3385
|
-
* {@link RequestOptions} и приносят с собой `limit`, `cursor`
|
|
3386
|
-
* запроса делать нечего.
|
|
3529
|
+
* Копируются только поля {@link REQUEST_OPTION_KEYS} и опции, заявленные плагинами:
|
|
3530
|
+
* параметры методов наследуют {@link RequestOptions} и приносят с собой `limit`, `cursor`
|
|
3531
|
+
* и прочее, чему в описании запроса делать нечего. Чужие опции плагинов библиотека
|
|
3387
3532
|
* не понимает, но обязана донести до обёрток нетронутыми.
|
|
3388
3533
|
*/
|
|
3389
3534
|
requestOptions(options) {
|
|
3390
3535
|
if (!options) return {};
|
|
3391
|
-
const result = {
|
|
3392
|
-
...options.signal !== void 0 ? { signal: options.signal } : {},
|
|
3393
|
-
...options.timeout !== void 0 ? { timeout: options.timeout } : {},
|
|
3394
|
-
...options.headers !== void 0 ? { headers: options.headers } : {},
|
|
3395
|
-
...options.retry !== void 0 ? { retry: options.retry } : {}
|
|
3396
|
-
};
|
|
3397
|
-
const pluginKeys = this.http.pluginOptionKeys;
|
|
3398
|
-
if (pluginKeys.size === 0) return result;
|
|
3399
3536
|
const source = options;
|
|
3400
|
-
const
|
|
3537
|
+
const result = {};
|
|
3538
|
+
for (const key of REQUEST_OPTION_KEYS) {
|
|
3539
|
+
const value = source[key];
|
|
3540
|
+
if (value !== void 0) result[key] = value;
|
|
3541
|
+
}
|
|
3542
|
+
const pluginKeys = this.http.pluginOptionKeys;
|
|
3401
3543
|
for (const key of pluginKeys) {
|
|
3402
3544
|
const value = source[key];
|
|
3403
|
-
if (value !== void 0)
|
|
3545
|
+
if (value !== void 0) result[key] = value;
|
|
3404
3546
|
}
|
|
3405
3547
|
return result;
|
|
3406
3548
|
}
|
|
@@ -3420,6 +3562,42 @@ var BaseResource = class {
|
|
|
3420
3562
|
...options?.start !== void 0 ? { start: options.start } : {}
|
|
3421
3563
|
});
|
|
3422
3564
|
}
|
|
3565
|
+
/**
|
|
3566
|
+
* Собирает пару «загрузка страницы + перебор» из одного описания.
|
|
3567
|
+
*
|
|
3568
|
+
* Путь, параметры запроса и разбор ответа задаются один раз; `list` и `iterate`
|
|
3569
|
+
* строятся из них.
|
|
3570
|
+
*
|
|
3571
|
+
* @example
|
|
3572
|
+
* ```ts
|
|
3573
|
+
* #feed = this.paginated<Post, FeedParams>({
|
|
3574
|
+
* path: () => '/api/posts',
|
|
3575
|
+
* query: (p) => ({ tab: p.tab, limit: p.limit }),
|
|
3576
|
+
* start: (p) => (p.cursor ? { cursor: p.cursor } : {}),
|
|
3577
|
+
* read: (body) => readCursorPage<Post>(body, 'posts'),
|
|
3578
|
+
* mode: PaginationMode.Cursor,
|
|
3579
|
+
* });
|
|
3580
|
+
* ```
|
|
3581
|
+
*/
|
|
3582
|
+
paginated(spec) {
|
|
3583
|
+
const load = async (params, state) => {
|
|
3584
|
+
const body = await this.http.request({
|
|
3585
|
+
method: "GET",
|
|
3586
|
+
path: spec.path(params),
|
|
3587
|
+
query: withPageState(spec.query(params), state),
|
|
3588
|
+
...this.requestOptions(params)
|
|
3589
|
+
});
|
|
3590
|
+
return spec.read(body, state);
|
|
3591
|
+
};
|
|
3592
|
+
return {
|
|
3593
|
+
list: (params) => load(params, spec.start(params)),
|
|
3594
|
+
iterate: (params) => this.paginate(spec.mode, (state) => load(params, state), {
|
|
3595
|
+
...params.maxPages !== void 0 ? { maxPages: params.maxPages } : {},
|
|
3596
|
+
...params.signal !== void 0 ? { signal: params.signal } : {},
|
|
3597
|
+
start: spec.start(params)
|
|
3598
|
+
})
|
|
3599
|
+
};
|
|
3600
|
+
}
|
|
3423
3601
|
};
|
|
3424
3602
|
function withPageState(query, state) {
|
|
3425
3603
|
return {
|
|
@@ -3737,6 +3915,14 @@ var AuthResource = class extends BaseResource {
|
|
|
3737
3915
|
// src/resources/comments.ts
|
|
3738
3916
|
var CommentsResource = class extends BaseResource {
|
|
3739
3917
|
#uploadFiles;
|
|
3918
|
+
/** Ответы на комментарий: `/api/comments/{id}/replies`, постраничная пагинация. */
|
|
3919
|
+
#replies = this.paginated({
|
|
3920
|
+
path: (p) => `/api/comments/${encodePathSegment(p.commentId, "commentId")}/replies`,
|
|
3921
|
+
query: (p) => ({ limit: p.limit }),
|
|
3922
|
+
start: (p) => p.page !== void 0 ? { page: p.page } : {},
|
|
3923
|
+
read: (body) => readPagedPage(body, "replies"),
|
|
3924
|
+
mode: PaginationMode.Page
|
|
3925
|
+
});
|
|
3740
3926
|
constructor(http, deps) {
|
|
3741
3927
|
super(http);
|
|
3742
3928
|
this.#uploadFiles = deps.uploadFiles;
|
|
@@ -3746,31 +3932,12 @@ var CommentsResource = class extends BaseResource {
|
|
|
3746
3932
|
*
|
|
3747
3933
|
* Здесь пагинация **постраничная**, в отличие от комментариев к посту, где курсорная.
|
|
3748
3934
|
*/
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
method: "GET",
|
|
3752
|
-
path: `/api/comments/${encodePathSegment(commentId, "commentId")}/replies`,
|
|
3753
|
-
query: { limit: params.limit, page: params.page },
|
|
3754
|
-
...this.requestOptions(params)
|
|
3755
|
-
});
|
|
3756
|
-
return readPagedPage(body, "replies");
|
|
3935
|
+
replies(commentId, params = {}) {
|
|
3936
|
+
return this.#replies.list({ ...params, commentId });
|
|
3757
3937
|
}
|
|
3758
3938
|
/** Перебирает ответы на комментарий. */
|
|
3759
3939
|
iterateReplies(commentId, params = {}) {
|
|
3760
|
-
|
|
3761
|
-
return this.paginate(
|
|
3762
|
-
PaginationMode.Page,
|
|
3763
|
-
async (state) => {
|
|
3764
|
-
const body = await this.http.request({
|
|
3765
|
-
method: "GET",
|
|
3766
|
-
path,
|
|
3767
|
-
query: withPageState({ limit: params.limit }, state),
|
|
3768
|
-
...this.requestOptions(params)
|
|
3769
|
-
});
|
|
3770
|
-
return readPagedPage(body, "replies");
|
|
3771
|
-
},
|
|
3772
|
-
{ ...params, ...params.page !== void 0 ? { start: { page: params.page } } : {} }
|
|
3773
|
-
);
|
|
3940
|
+
return this.#replies.iterate({ ...params, commentId });
|
|
3774
3941
|
}
|
|
3775
3942
|
/**
|
|
3776
3943
|
* Отвечает на комментарий.
|
|
@@ -3905,19 +4072,14 @@ function assertAllowedMime(mimeType, filename) {
|
|
|
3905
4072
|
var DEFAULT_UPLOAD_TIMEOUT = 3e5;
|
|
3906
4073
|
var FilesResource = class extends BaseResource {
|
|
3907
4074
|
#readFile;
|
|
4075
|
+
/**
|
|
4076
|
+
* @param deps.readFile чтение файлов с диска. Передаёт точка входа `itd-api/node`;
|
|
4077
|
+
* в основном бандле его нет, чтобы браузерные сборщики не пытались разрешить `node:fs`.
|
|
4078
|
+
*/
|
|
3908
4079
|
constructor(http, deps = {}) {
|
|
3909
4080
|
super(http);
|
|
3910
4081
|
this.#readFile = deps.readFile;
|
|
3911
4082
|
}
|
|
3912
|
-
/**
|
|
3913
|
-
* Подключает чтение файлов с диска.
|
|
3914
|
-
*
|
|
3915
|
-
* Вызывается точкой входа `itd-api/node`; в основном бандле работы с файловой
|
|
3916
|
-
* системой нет, чтобы браузерные сборщики не пытались разрешить `node:fs`.
|
|
3917
|
-
*/
|
|
3918
|
-
setFileReader(readFile) {
|
|
3919
|
-
this.#readFile = readFile;
|
|
3920
|
-
}
|
|
3921
4083
|
/**
|
|
3922
4084
|
* Загружает файл и возвращает его идентификатор.
|
|
3923
4085
|
*
|
|
@@ -4027,8 +4189,16 @@ var FilesResource = class extends BaseResource {
|
|
|
4027
4189
|
}
|
|
4028
4190
|
};
|
|
4029
4191
|
|
|
4030
|
-
// src/resources/
|
|
4192
|
+
// src/resources/hashtags.ts
|
|
4031
4193
|
var HashtagsResource = class extends BaseResource {
|
|
4194
|
+
/** Посты по хэштегу: `/api/hashtags/{tag}/posts`, курсорная пагинация. */
|
|
4195
|
+
#posts = this.paginated({
|
|
4196
|
+
path: (p) => `/api/hashtags/${encodePathSegment(p.tag, "tag")}/posts`,
|
|
4197
|
+
query: (p) => ({ limit: p.limit }),
|
|
4198
|
+
start: (p) => p.cursor ? { cursor: p.cursor } : {},
|
|
4199
|
+
read: (body) => readCursorPage(body, "posts"),
|
|
4200
|
+
mode: PaginationMode.Cursor
|
|
4201
|
+
});
|
|
4032
4202
|
/**
|
|
4033
4203
|
* Ищет хэштеги.
|
|
4034
4204
|
*
|
|
@@ -4059,253 +4229,48 @@ var HashtagsResource = class extends BaseResource {
|
|
|
4059
4229
|
* @param tag название без решётки; кодируется автоматически, поэтому кириллица
|
|
4060
4230
|
* и пробелы допустимы
|
|
4061
4231
|
*/
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
method: "GET",
|
|
4065
|
-
path: `/api/hashtags/${encodePathSegment(tag, "tag")}/posts`,
|
|
4066
|
-
query: { limit: params.limit, cursor: params.cursor },
|
|
4067
|
-
...this.requestOptions(params)
|
|
4068
|
-
});
|
|
4069
|
-
return readCursorPage(body, "posts");
|
|
4232
|
+
posts(tag, params = {}) {
|
|
4233
|
+
return this.#posts.list({ ...params, tag });
|
|
4070
4234
|
}
|
|
4071
4235
|
/** Перебирает посты по хэштегу. */
|
|
4072
4236
|
iteratePosts(tag, params = {}) {
|
|
4073
|
-
|
|
4074
|
-
return this.paginate(
|
|
4075
|
-
PaginationMode.Cursor,
|
|
4076
|
-
async (state) => {
|
|
4077
|
-
const body = await this.http.request({
|
|
4078
|
-
method: "GET",
|
|
4079
|
-
path,
|
|
4080
|
-
query: withPageState({ limit: params.limit }, state),
|
|
4081
|
-
...this.requestOptions(params)
|
|
4082
|
-
});
|
|
4083
|
-
return readCursorPage(body, "posts");
|
|
4084
|
-
},
|
|
4085
|
-
{ ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
|
|
4086
|
-
);
|
|
4087
|
-
}
|
|
4088
|
-
};
|
|
4089
|
-
var SearchResource = class extends BaseResource {
|
|
4090
|
-
/**
|
|
4091
|
-
* Ищет пользователей и хэштеги одним запросом.
|
|
4092
|
-
*
|
|
4093
|
-
* @example
|
|
4094
|
-
* ```ts
|
|
4095
|
-
* const { users, hashtags } = await itd.search.all('арт');
|
|
4096
|
-
* ```
|
|
4097
|
-
*/
|
|
4098
|
-
async all(query, options = {}) {
|
|
4099
|
-
const body = await this.http.request({
|
|
4100
|
-
method: "GET",
|
|
4101
|
-
path: "/api/search",
|
|
4102
|
-
query: { q: query },
|
|
4103
|
-
...this.requestOptions(options)
|
|
4104
|
-
});
|
|
4105
|
-
return {
|
|
4106
|
-
users: pickArray(body, "users"),
|
|
4107
|
-
hashtags: pickArray(body, "hashtags")
|
|
4108
|
-
};
|
|
4109
|
-
}
|
|
4110
|
-
};
|
|
4111
|
-
var ReportsResource = class extends BaseResource {
|
|
4112
|
-
/**
|
|
4113
|
-
* Отправляет жалобу.
|
|
4114
|
-
*
|
|
4115
|
-
* Повторная жалоба на тот же объект отклоняется сервером с сообщением
|
|
4116
|
-
* «Вы уже отправляли жалобу на этот контент».
|
|
4117
|
-
*
|
|
4118
|
-
* @example
|
|
4119
|
-
* ```ts
|
|
4120
|
-
* await itd.reports.create(report.post(postId).reason('spam'));
|
|
4121
|
-
* await itd.reports.create({ targetType: 'user', targetId, reason: 'fraud' });
|
|
4122
|
-
* ```
|
|
4123
|
-
*/
|
|
4124
|
-
create(input, options = {}) {
|
|
4125
|
-
const data = resolveReport(input);
|
|
4126
|
-
return this.http.request({
|
|
4127
|
-
method: "POST",
|
|
4128
|
-
path: "/api/reports",
|
|
4129
|
-
body: data,
|
|
4130
|
-
...this.requestOptions(options)
|
|
4131
|
-
});
|
|
4132
|
-
}
|
|
4133
|
-
};
|
|
4134
|
-
var VerificationResource = class extends BaseResource {
|
|
4135
|
-
/** Загружает статус заявки. Значение `none` означает, что заявка не подавалась. */
|
|
4136
|
-
status(options = {}) {
|
|
4137
|
-
return this.http.request({
|
|
4138
|
-
method: "GET",
|
|
4139
|
-
path: "/api/verification/status",
|
|
4140
|
-
...this.requestOptions(options)
|
|
4141
|
-
});
|
|
4142
|
-
}
|
|
4143
|
-
/** Подаёт заявку на верификацию с видео. */
|
|
4144
|
-
submit(videoUrl, options = {}) {
|
|
4145
|
-
return this.http.request({
|
|
4146
|
-
method: "POST",
|
|
4147
|
-
path: "/api/verification/submit",
|
|
4148
|
-
body: { videoUrl },
|
|
4149
|
-
...this.requestOptions(options)
|
|
4150
|
-
});
|
|
4151
|
-
}
|
|
4152
|
-
};
|
|
4153
|
-
var SubscriptionResource = class extends BaseResource {
|
|
4154
|
-
/** Загружает состояние подписки и её цену. */
|
|
4155
|
-
status(options = {}) {
|
|
4156
|
-
return this.http.request({
|
|
4157
|
-
method: "GET",
|
|
4158
|
-
// Завершающий слэш обязателен.
|
|
4159
|
-
path: "/api/v1/subscription/",
|
|
4160
|
-
...this.requestOptions(options)
|
|
4161
|
-
});
|
|
4162
|
-
}
|
|
4163
|
-
/**
|
|
4164
|
-
* Запускает оплату подписки.
|
|
4165
|
-
*
|
|
4166
|
-
* Форма ответа в документации API не описана, поэтому тип результата не уточняется.
|
|
4167
|
-
*/
|
|
4168
|
-
pay(options = {}) {
|
|
4169
|
-
return this.http.request({
|
|
4170
|
-
method: "POST",
|
|
4171
|
-
path: "/api/v1/subscription/pay",
|
|
4172
|
-
...this.requestOptions(options)
|
|
4173
|
-
});
|
|
4174
|
-
}
|
|
4175
|
-
/** Включает или отключает автопродление. */
|
|
4176
|
-
setAutoRenewal(enabled, options = {}) {
|
|
4177
|
-
return this.http.request({
|
|
4178
|
-
method: "POST",
|
|
4179
|
-
path: "/api/v1/subscription/auto-renewal",
|
|
4180
|
-
body: { enabled },
|
|
4181
|
-
...this.requestOptions(options)
|
|
4182
|
-
});
|
|
4183
|
-
}
|
|
4184
|
-
/** Запускает привязку карты. */
|
|
4185
|
-
bindCard(options = {}) {
|
|
4186
|
-
return this.http.request({
|
|
4187
|
-
method: "POST",
|
|
4188
|
-
path: "/api/v1/subscription/bind-card",
|
|
4189
|
-
...this.requestOptions(options)
|
|
4190
|
-
});
|
|
4191
|
-
}
|
|
4192
|
-
/** Загружает список способов оплаты. Пустой массив, если карт нет. */
|
|
4193
|
-
async methods(options = {}) {
|
|
4194
|
-
const body = await this.http.request({
|
|
4195
|
-
method: "GET",
|
|
4196
|
-
path: "/api/v1/subscription/methods",
|
|
4197
|
-
...this.requestOptions(options)
|
|
4198
|
-
});
|
|
4199
|
-
return Array.isArray(body) ? body : [];
|
|
4200
|
-
}
|
|
4201
|
-
/** Делает способ оплаты основным. */
|
|
4202
|
-
setDefaultMethod(methodId, options = {}) {
|
|
4203
|
-
return this.http.request({
|
|
4204
|
-
method: "POST",
|
|
4205
|
-
path: `/api/v1/subscription/methods/${encodePathSegment(methodId, "methodId")}/default`,
|
|
4206
|
-
...this.requestOptions(options)
|
|
4207
|
-
});
|
|
4208
|
-
}
|
|
4209
|
-
/** Удаляет способ оплаты. */
|
|
4210
|
-
removeMethod(methodId, options = {}) {
|
|
4211
|
-
return this.http.request({
|
|
4212
|
-
method: "DELETE",
|
|
4213
|
-
path: `/api/v1/subscription/methods/${encodePathSegment(methodId, "methodId")}`,
|
|
4214
|
-
...this.requestOptions(options)
|
|
4215
|
-
});
|
|
4216
|
-
}
|
|
4217
|
-
};
|
|
4218
|
-
var PlatformResource = class extends BaseResource {
|
|
4219
|
-
/** Загружает журнал изменений. */
|
|
4220
|
-
async changelog(options = {}) {
|
|
4221
|
-
const body = await this.http.request({
|
|
4222
|
-
method: "GET",
|
|
4223
|
-
path: "/api/platform/changelog",
|
|
4224
|
-
...this.requestOptions(options)
|
|
4225
|
-
});
|
|
4226
|
-
return Array.isArray(body) ? body : [];
|
|
4227
|
-
}
|
|
4228
|
-
/** Загружает анонсы платформы. */
|
|
4229
|
-
async announcements(options = {}) {
|
|
4230
|
-
const body = await this.http.request({
|
|
4231
|
-
method: "GET",
|
|
4232
|
-
path: "/api/platform/announcements",
|
|
4233
|
-
...this.requestOptions(options)
|
|
4234
|
-
});
|
|
4235
|
-
return pickArray(body, "announcements");
|
|
4236
|
-
}
|
|
4237
|
-
/** Загружает баннер текущего события — виджет «портал». */
|
|
4238
|
-
portal(options = {}) {
|
|
4239
|
-
return this.http.request({
|
|
4240
|
-
method: "GET",
|
|
4241
|
-
path: "/api/v1/portal",
|
|
4242
|
-
...this.requestOptions(options)
|
|
4243
|
-
});
|
|
4244
|
-
}
|
|
4245
|
-
};
|
|
4246
|
-
var TelemetryResource = class extends BaseResource {
|
|
4247
|
-
/**
|
|
4248
|
-
* Отправляет время просмотра постов.
|
|
4249
|
-
*
|
|
4250
|
-
* @experimental Имена полей на проводе сжаты (`ai`, `v`, `s`), и их соответствие
|
|
4251
|
-
* смыслу **не проверено** на реальных запросах. Может измениться без предупреждения.
|
|
4252
|
-
*/
|
|
4253
|
-
dwell(entries, options = {}) {
|
|
4254
|
-
return this.http.request({
|
|
4255
|
-
method: "POST",
|
|
4256
|
-
path: "/api/v1/i",
|
|
4257
|
-
body: {
|
|
4258
|
-
items: entries.map((entry) => ({
|
|
4259
|
-
ai: entry.postId,
|
|
4260
|
-
v: entry.duration,
|
|
4261
|
-
...entry.vs ? { s: entry.vs } : {}
|
|
4262
|
-
}))
|
|
4263
|
-
},
|
|
4264
|
-
...this.requestOptions(options)
|
|
4265
|
-
});
|
|
4266
|
-
}
|
|
4267
|
-
/**
|
|
4268
|
-
* Отправляет события взаимодействия с контентом.
|
|
4269
|
-
*
|
|
4270
|
-
* @experimental См. предупреждение у {@link TelemetryResource}.
|
|
4271
|
-
*/
|
|
4272
|
-
interaction(entries, options = {}) {
|
|
4273
|
-
return this.http.request({
|
|
4274
|
-
method: "POST",
|
|
4275
|
-
path: "/api/v1/x",
|
|
4276
|
-
body: {
|
|
4277
|
-
items: entries.map((entry) => ({
|
|
4278
|
-
t: entry.type,
|
|
4279
|
-
...entry.value !== void 0 ? { v: entry.value } : {},
|
|
4280
|
-
...entry.postId ? { ai: entry.postId } : {},
|
|
4281
|
-
...entry.attachmentId ? { mi: entry.attachmentId } : {},
|
|
4282
|
-
...entry.vs ? { s: entry.vs } : {}
|
|
4283
|
-
}))
|
|
4284
|
-
},
|
|
4285
|
-
...this.requestOptions(options)
|
|
4286
|
-
});
|
|
4237
|
+
return this.#posts.iterate({ ...params, tag });
|
|
4287
4238
|
}
|
|
4288
4239
|
};
|
|
4289
4240
|
|
|
4290
4241
|
// src/resources/notifications.ts
|
|
4242
|
+
var NOTIFICATION_SETTING_KEYS = [
|
|
4243
|
+
"enabled",
|
|
4244
|
+
"sound",
|
|
4245
|
+
"follows",
|
|
4246
|
+
"wallPosts",
|
|
4247
|
+
"likes",
|
|
4248
|
+
"comments",
|
|
4249
|
+
"mentions"
|
|
4250
|
+
];
|
|
4291
4251
|
var READ_BATCH_SIZE = 20;
|
|
4292
4252
|
function readSettings(body) {
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
follows: pickBoolean(body, "follows", true),
|
|
4297
|
-
wallPosts: pickBoolean(body, "wallPosts", true),
|
|
4298
|
-
likes: pickBoolean(body, "likes", true),
|
|
4299
|
-
comments: pickBoolean(body, "comments", true),
|
|
4300
|
-
mentions: pickBoolean(body, "mentions", true)
|
|
4301
|
-
};
|
|
4253
|
+
const settings = {};
|
|
4254
|
+
for (const key of NOTIFICATION_SETTING_KEYS) settings[key] = pickBoolean(body, key, true);
|
|
4255
|
+
return settings;
|
|
4302
4256
|
}
|
|
4303
4257
|
var NotificationsResource = class extends BaseResource {
|
|
4258
|
+
/** Уведомления: `/api/notifications/`, пагинация по смещению. */
|
|
4259
|
+
#list = this.paginated({
|
|
4260
|
+
// Завершающий слэш обязателен: без него сервер отвечает ошибкой.
|
|
4261
|
+
path: () => "/api/notifications/",
|
|
4262
|
+
query: (p) => ({ limit: p.limit }),
|
|
4263
|
+
start: (p) => ({ offset: p.offset ?? 0 }),
|
|
4264
|
+
read: (body, state) => {
|
|
4265
|
+
const page = readOffsetPage(body, "notifications", state.offset ?? 0);
|
|
4266
|
+
return { ...page, items: page.items.map(normalizeNotification) };
|
|
4267
|
+
},
|
|
4268
|
+
mode: PaginationMode.Offset
|
|
4269
|
+
});
|
|
4304
4270
|
/**
|
|
4305
4271
|
* Загружает страницу уведомлений.
|
|
4306
4272
|
*
|
|
4307
|
-
* Пагинация здесь основана на смещении.
|
|
4308
|
-
* и притворяется, что это курсор; библиотека отдаёт честное число.
|
|
4273
|
+
* Пагинация здесь основана на смещении.
|
|
4309
4274
|
*
|
|
4310
4275
|
* @example
|
|
4311
4276
|
* ```ts
|
|
@@ -4314,19 +4279,7 @@ var NotificationsResource = class extends BaseResource {
|
|
|
4314
4279
|
* ```
|
|
4315
4280
|
*/
|
|
4316
4281
|
list(params = {}) {
|
|
4317
|
-
return this.#
|
|
4318
|
-
}
|
|
4319
|
-
/** Общая загрузка страницы для {@link list} и {@link iterate}. */
|
|
4320
|
-
async #loadPage(params, offset) {
|
|
4321
|
-
const body = await this.http.request({
|
|
4322
|
-
method: "GET",
|
|
4323
|
-
// Завершающий слэш обязателен: без него сервер отвечает ошибкой.
|
|
4324
|
-
path: "/api/notifications/",
|
|
4325
|
-
query: { limit: params.limit, offset },
|
|
4326
|
-
...this.requestOptions(params)
|
|
4327
|
-
});
|
|
4328
|
-
const page = readOffsetPage(body, "notifications", offset);
|
|
4329
|
-
return { ...page, items: page.items.map(normalizeNotification) };
|
|
4282
|
+
return this.#list.list(params);
|
|
4330
4283
|
}
|
|
4331
4284
|
/**
|
|
4332
4285
|
* Перебирает уведомления.
|
|
@@ -4339,11 +4292,7 @@ var NotificationsResource = class extends BaseResource {
|
|
|
4339
4292
|
* ```
|
|
4340
4293
|
*/
|
|
4341
4294
|
iterate(params = {}) {
|
|
4342
|
-
return this.
|
|
4343
|
-
PaginationMode.Offset,
|
|
4344
|
-
(state) => this.#loadPage(params, state.offset ?? 0),
|
|
4345
|
-
{ ...params, ...params.offset !== void 0 ? { start: { offset: params.offset } } : {} }
|
|
4346
|
-
);
|
|
4295
|
+
return this.#list.iterate(params);
|
|
4347
4296
|
}
|
|
4348
4297
|
/** Загружает число непрочитанных уведомлений. */
|
|
4349
4298
|
async count(options = {}) {
|
|
@@ -4415,15 +4364,7 @@ var NotificationsResource = class extends BaseResource {
|
|
|
4415
4364
|
*/
|
|
4416
4365
|
async updateSettings(input, options = {}) {
|
|
4417
4366
|
const payload = {};
|
|
4418
|
-
for (const key of
|
|
4419
|
-
"enabled",
|
|
4420
|
-
"sound",
|
|
4421
|
-
"follows",
|
|
4422
|
-
"wallPosts",
|
|
4423
|
-
"likes",
|
|
4424
|
-
"comments",
|
|
4425
|
-
"mentions"
|
|
4426
|
-
]) {
|
|
4367
|
+
for (const key of NOTIFICATION_SETTING_KEYS) {
|
|
4427
4368
|
const value = input[key];
|
|
4428
4369
|
if (value !== void 0) payload[key] = value;
|
|
4429
4370
|
}
|
|
@@ -4433,13 +4374,78 @@ var NotificationsResource = class extends BaseResource {
|
|
|
4433
4374
|
body: payload,
|
|
4434
4375
|
...this.requestOptions(options)
|
|
4435
4376
|
});
|
|
4436
|
-
return readSettings(body);
|
|
4377
|
+
return readSettings(body);
|
|
4378
|
+
}
|
|
4379
|
+
};
|
|
4380
|
+
|
|
4381
|
+
// src/resources/platform.ts
|
|
4382
|
+
var PlatformResource = class extends BaseResource {
|
|
4383
|
+
/** Загружает журнал изменений. */
|
|
4384
|
+
async changelog(options = {}) {
|
|
4385
|
+
const body = await this.http.request({
|
|
4386
|
+
method: "GET",
|
|
4387
|
+
path: "/api/platform/changelog",
|
|
4388
|
+
...this.requestOptions(options)
|
|
4389
|
+
});
|
|
4390
|
+
return Array.isArray(body) ? body : [];
|
|
4391
|
+
}
|
|
4392
|
+
/** Загружает анонсы платформы. */
|
|
4393
|
+
async announcements(options = {}) {
|
|
4394
|
+
const body = await this.http.request({
|
|
4395
|
+
method: "GET",
|
|
4396
|
+
path: "/api/platform/announcements",
|
|
4397
|
+
...this.requestOptions(options)
|
|
4398
|
+
});
|
|
4399
|
+
return pickArray(body, "announcements");
|
|
4400
|
+
}
|
|
4401
|
+
/** Загружает баннер текущего события — виджет «портал». */
|
|
4402
|
+
portal(options = {}) {
|
|
4403
|
+
return this.http.request({
|
|
4404
|
+
method: "GET",
|
|
4405
|
+
path: "/api/v1/portal",
|
|
4406
|
+
...this.requestOptions(options)
|
|
4407
|
+
});
|
|
4437
4408
|
}
|
|
4438
4409
|
};
|
|
4439
4410
|
|
|
4440
4411
|
// src/resources/posts.ts
|
|
4412
|
+
function cursorStart(params) {
|
|
4413
|
+
return params.cursor ? { cursor: params.cursor } : {};
|
|
4414
|
+
}
|
|
4441
4415
|
var PostsResource = class extends BaseResource {
|
|
4442
4416
|
#uploadFiles;
|
|
4417
|
+
/** Лента: `/api/posts`, курсорная пагинация. */
|
|
4418
|
+
#feed = this.paginated({
|
|
4419
|
+
path: () => "/api/posts",
|
|
4420
|
+
query: (p) => ({ tab: p.tab, limit: p.limit }),
|
|
4421
|
+
start: cursorStart,
|
|
4422
|
+
read: (body) => readCursorPage(body, "posts"),
|
|
4423
|
+
mode: PaginationMode.Cursor
|
|
4424
|
+
});
|
|
4425
|
+
/** Стена пользователя: `/api/posts/user/{user}`. */
|
|
4426
|
+
#wall = this.paginated({
|
|
4427
|
+
path: (p) => `/api/posts/user/${encodePathSegment(p.user, "user")}`,
|
|
4428
|
+
query: (p) => ({ limit: p.limit, sort: p.sort, pinnedPostId: p.pinnedPostId }),
|
|
4429
|
+
start: cursorStart,
|
|
4430
|
+
read: (body) => readCursorPage(body, "posts"),
|
|
4431
|
+
mode: PaginationMode.Cursor
|
|
4432
|
+
});
|
|
4433
|
+
/** Понравившиеся посты пользователя: `/api/posts/user/{user}/liked`. */
|
|
4434
|
+
#liked = this.paginated({
|
|
4435
|
+
path: (p) => `/api/posts/user/${encodePathSegment(p.user, "user")}/liked`,
|
|
4436
|
+
query: (p) => ({ limit: p.limit }),
|
|
4437
|
+
start: cursorStart,
|
|
4438
|
+
read: (body) => readCursorPage(body, "posts"),
|
|
4439
|
+
mode: PaginationMode.Cursor
|
|
4440
|
+
});
|
|
4441
|
+
/** Комментарии к посту: курсор лежит рядом со списком, поэтому свой reader. */
|
|
4442
|
+
#comments = this.paginated({
|
|
4443
|
+
path: (p) => `/api/posts/${encodePathSegment(p.postId, "postId")}/comments`,
|
|
4444
|
+
query: (p) => ({ limit: p.limit, sort: p.sort }),
|
|
4445
|
+
start: cursorStart,
|
|
4446
|
+
read: (body) => readFlatCursorPage(body, "comments"),
|
|
4447
|
+
mode: PaginationMode.Cursor
|
|
4448
|
+
});
|
|
4443
4449
|
constructor(http, deps) {
|
|
4444
4450
|
super(http);
|
|
4445
4451
|
this.#uploadFiles = deps.uploadFiles;
|
|
@@ -4453,14 +4459,8 @@ var PostsResource = class extends BaseResource {
|
|
|
4453
4459
|
* const next = await itd.posts.list({ tab: FeedTab.Following, cursor: page.nextCursor ?? undefined });
|
|
4454
4460
|
* ```
|
|
4455
4461
|
*/
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
method: "GET",
|
|
4459
|
-
path: "/api/posts",
|
|
4460
|
-
query: { tab: params.tab, limit: params.limit, cursor: params.cursor },
|
|
4461
|
-
...this.requestOptions(params)
|
|
4462
|
-
});
|
|
4463
|
-
return readCursorPage(body, "posts");
|
|
4462
|
+
list(params = {}) {
|
|
4463
|
+
return this.#feed.list(params);
|
|
4464
4464
|
}
|
|
4465
4465
|
/**
|
|
4466
4466
|
* Перебирает ленту, сама подставляя курсоры.
|
|
@@ -4473,19 +4473,7 @@ var PostsResource = class extends BaseResource {
|
|
|
4473
4473
|
* ```
|
|
4474
4474
|
*/
|
|
4475
4475
|
iterate(params = {}) {
|
|
4476
|
-
return this.
|
|
4477
|
-
PaginationMode.Cursor,
|
|
4478
|
-
async (state) => {
|
|
4479
|
-
const body = await this.http.request({
|
|
4480
|
-
method: "GET",
|
|
4481
|
-
path: "/api/posts",
|
|
4482
|
-
query: withPageState({ tab: params.tab, limit: params.limit }, state),
|
|
4483
|
-
...this.requestOptions(params)
|
|
4484
|
-
});
|
|
4485
|
-
return readCursorPage(body, "posts");
|
|
4486
|
-
},
|
|
4487
|
-
{ ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
|
|
4488
|
-
);
|
|
4476
|
+
return this.#feed.iterate(params);
|
|
4489
4477
|
}
|
|
4490
4478
|
/**
|
|
4491
4479
|
* Публикует пост.
|
|
@@ -4639,66 +4627,20 @@ var PostsResource = class extends BaseResource {
|
|
|
4639
4627
|
*
|
|
4640
4628
|
* Принимает и UUID, и имя пользователя.
|
|
4641
4629
|
*/
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
method: "GET",
|
|
4645
|
-
path: `/api/posts/user/${encodePathSegment(user, "user")}`,
|
|
4646
|
-
query: {
|
|
4647
|
-
limit: params.limit,
|
|
4648
|
-
cursor: params.cursor,
|
|
4649
|
-
sort: params.sort,
|
|
4650
|
-
pinnedPostId: params.pinnedPostId
|
|
4651
|
-
},
|
|
4652
|
-
...this.requestOptions(params)
|
|
4653
|
-
});
|
|
4654
|
-
return readCursorPage(body, "posts");
|
|
4630
|
+
byUser(user, params = {}) {
|
|
4631
|
+
return this.#wall.list({ ...params, user });
|
|
4655
4632
|
}
|
|
4656
4633
|
/** Перебирает стену пользователя. Что именно в неё входит — см. {@link byUser}. */
|
|
4657
4634
|
iterateByUser(user, params = {}) {
|
|
4658
|
-
|
|
4659
|
-
return this.paginate(
|
|
4660
|
-
PaginationMode.Cursor,
|
|
4661
|
-
async (state) => {
|
|
4662
|
-
const body = await this.http.request({
|
|
4663
|
-
method: "GET",
|
|
4664
|
-
path,
|
|
4665
|
-
query: withPageState(
|
|
4666
|
-
{ limit: params.limit, sort: params.sort, pinnedPostId: params.pinnedPostId },
|
|
4667
|
-
state
|
|
4668
|
-
),
|
|
4669
|
-
...this.requestOptions(params)
|
|
4670
|
-
});
|
|
4671
|
-
return readCursorPage(body, "posts");
|
|
4672
|
-
},
|
|
4673
|
-
{ ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
|
|
4674
|
-
);
|
|
4635
|
+
return this.#wall.iterate({ ...params, user });
|
|
4675
4636
|
}
|
|
4676
4637
|
/** Загружает страницу постов, которые пользователь отметил реакцией. */
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
method: "GET",
|
|
4680
|
-
path: `/api/posts/user/${encodePathSegment(user, "user")}/liked`,
|
|
4681
|
-
query: { limit: params.limit, cursor: params.cursor },
|
|
4682
|
-
...this.requestOptions(params)
|
|
4683
|
-
});
|
|
4684
|
-
return readCursorPage(body, "posts");
|
|
4638
|
+
likedByUser(user, params = {}) {
|
|
4639
|
+
return this.#liked.list({ ...params, user });
|
|
4685
4640
|
}
|
|
4686
4641
|
/** Перебирает посты, которые пользователь отметил реакцией. */
|
|
4687
4642
|
iterateLikedByUser(user, params = {}) {
|
|
4688
|
-
|
|
4689
|
-
return this.paginate(
|
|
4690
|
-
PaginationMode.Cursor,
|
|
4691
|
-
async (state) => {
|
|
4692
|
-
const body = await this.http.request({
|
|
4693
|
-
method: "GET",
|
|
4694
|
-
path,
|
|
4695
|
-
query: withPageState({ limit: params.limit }, state),
|
|
4696
|
-
...this.requestOptions(params)
|
|
4697
|
-
});
|
|
4698
|
-
return readCursorPage(body, "posts");
|
|
4699
|
-
},
|
|
4700
|
-
{ ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
|
|
4701
|
-
);
|
|
4643
|
+
return this.#liked.iterate({ ...params, user });
|
|
4702
4644
|
}
|
|
4703
4645
|
/**
|
|
4704
4646
|
* Загружает страницу комментариев к посту.
|
|
@@ -4706,31 +4648,12 @@ var PostsResource = class extends BaseResource {
|
|
|
4706
4648
|
* У этого эндпоинта курсор и признак продолжения лежат рядом со списком, а не внутри
|
|
4707
4649
|
* объекта `pagination`, как у остальных, — разница скрыта внутри.
|
|
4708
4650
|
*/
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
method: "GET",
|
|
4712
|
-
path: `/api/posts/${encodePathSegment(postId, "postId")}/comments`,
|
|
4713
|
-
query: { limit: params.limit, cursor: params.cursor, sort: params.sort },
|
|
4714
|
-
...this.requestOptions(params)
|
|
4715
|
-
});
|
|
4716
|
-
return readFlatCursorPage(body, "comments");
|
|
4651
|
+
comments(postId, params = {}) {
|
|
4652
|
+
return this.#comments.list({ ...params, postId });
|
|
4717
4653
|
}
|
|
4718
4654
|
/** Перебирает комментарии к посту. */
|
|
4719
4655
|
iterateComments(postId, params = {}) {
|
|
4720
|
-
|
|
4721
|
-
return this.paginate(
|
|
4722
|
-
PaginationMode.Cursor,
|
|
4723
|
-
async (state) => {
|
|
4724
|
-
const body = await this.http.request({
|
|
4725
|
-
method: "GET",
|
|
4726
|
-
path,
|
|
4727
|
-
query: withPageState({ limit: params.limit, sort: params.sort }, state),
|
|
4728
|
-
...this.requestOptions(params)
|
|
4729
|
-
});
|
|
4730
|
-
return readFlatCursorPage(body, "comments");
|
|
4731
|
-
},
|
|
4732
|
-
{ ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
|
|
4733
|
-
);
|
|
4656
|
+
return this.#comments.iterate({ ...params, postId });
|
|
4734
4657
|
}
|
|
4735
4658
|
/**
|
|
4736
4659
|
* Комментирует пост.
|
|
@@ -4775,8 +4698,203 @@ var PostsResource = class extends BaseResource {
|
|
|
4775
4698
|
}
|
|
4776
4699
|
};
|
|
4777
4700
|
|
|
4701
|
+
// src/resources/reports.ts
|
|
4702
|
+
var ReportsResource = class extends BaseResource {
|
|
4703
|
+
/**
|
|
4704
|
+
* Отправляет жалобу.
|
|
4705
|
+
*
|
|
4706
|
+
* Повторная жалоба на тот же объект отклоняется сервером с сообщением
|
|
4707
|
+
* «Вы уже отправляли жалобу на этот контент».
|
|
4708
|
+
*
|
|
4709
|
+
* @example
|
|
4710
|
+
* ```ts
|
|
4711
|
+
* await itd.reports.create(report.post(postId).reason('spam'));
|
|
4712
|
+
* await itd.reports.create({ targetType: 'user', targetId, reason: 'fraud' });
|
|
4713
|
+
* ```
|
|
4714
|
+
*/
|
|
4715
|
+
create(input, options = {}) {
|
|
4716
|
+
const data = resolveReport(input);
|
|
4717
|
+
return this.http.request({
|
|
4718
|
+
method: "POST",
|
|
4719
|
+
path: "/api/reports",
|
|
4720
|
+
body: data,
|
|
4721
|
+
...this.requestOptions(options)
|
|
4722
|
+
});
|
|
4723
|
+
}
|
|
4724
|
+
};
|
|
4725
|
+
|
|
4726
|
+
// src/resources/search.ts
|
|
4727
|
+
var SearchResource = class extends BaseResource {
|
|
4728
|
+
/**
|
|
4729
|
+
* Ищет пользователей и хэштеги одним запросом.
|
|
4730
|
+
*
|
|
4731
|
+
* @example
|
|
4732
|
+
* ```ts
|
|
4733
|
+
* const { users, hashtags } = await itd.search.all('арт');
|
|
4734
|
+
* ```
|
|
4735
|
+
*/
|
|
4736
|
+
async all(query, options = {}) {
|
|
4737
|
+
const body = await this.http.request({
|
|
4738
|
+
method: "GET",
|
|
4739
|
+
path: "/api/search",
|
|
4740
|
+
query: { q: query },
|
|
4741
|
+
...this.requestOptions(options)
|
|
4742
|
+
});
|
|
4743
|
+
return {
|
|
4744
|
+
users: pickArray(body, "users"),
|
|
4745
|
+
hashtags: pickArray(body, "hashtags")
|
|
4746
|
+
};
|
|
4747
|
+
}
|
|
4748
|
+
};
|
|
4749
|
+
|
|
4750
|
+
// src/resources/subscription.ts
|
|
4751
|
+
var SubscriptionResource = class extends BaseResource {
|
|
4752
|
+
/** Загружает состояние подписки и её цену. */
|
|
4753
|
+
status(options = {}) {
|
|
4754
|
+
return this.http.request({
|
|
4755
|
+
method: "GET",
|
|
4756
|
+
// Завершающий слэш обязателен.
|
|
4757
|
+
path: "/api/v1/subscription/",
|
|
4758
|
+
...this.requestOptions(options)
|
|
4759
|
+
});
|
|
4760
|
+
}
|
|
4761
|
+
/**
|
|
4762
|
+
* Запускает оплату подписки.
|
|
4763
|
+
*
|
|
4764
|
+
* Форма ответа в документации API не описана, поэтому тип результата не уточняется.
|
|
4765
|
+
*/
|
|
4766
|
+
pay(options = {}) {
|
|
4767
|
+
return this.http.request({
|
|
4768
|
+
method: "POST",
|
|
4769
|
+
path: "/api/v1/subscription/pay",
|
|
4770
|
+
...this.requestOptions(options)
|
|
4771
|
+
});
|
|
4772
|
+
}
|
|
4773
|
+
/** Включает или отключает автопродление. */
|
|
4774
|
+
setAutoRenewal(enabled, options = {}) {
|
|
4775
|
+
return this.http.request({
|
|
4776
|
+
method: "POST",
|
|
4777
|
+
path: "/api/v1/subscription/auto-renewal",
|
|
4778
|
+
body: { enabled },
|
|
4779
|
+
...this.requestOptions(options)
|
|
4780
|
+
});
|
|
4781
|
+
}
|
|
4782
|
+
/** Запускает привязку карты. */
|
|
4783
|
+
bindCard(options = {}) {
|
|
4784
|
+
return this.http.request({
|
|
4785
|
+
method: "POST",
|
|
4786
|
+
path: "/api/v1/subscription/bind-card",
|
|
4787
|
+
...this.requestOptions(options)
|
|
4788
|
+
});
|
|
4789
|
+
}
|
|
4790
|
+
/** Загружает список способов оплаты. Пустой массив, если карт нет. */
|
|
4791
|
+
async methods(options = {}) {
|
|
4792
|
+
const body = await this.http.request({
|
|
4793
|
+
method: "GET",
|
|
4794
|
+
path: "/api/v1/subscription/methods",
|
|
4795
|
+
...this.requestOptions(options)
|
|
4796
|
+
});
|
|
4797
|
+
return Array.isArray(body) ? body : [];
|
|
4798
|
+
}
|
|
4799
|
+
/** Делает способ оплаты основным. */
|
|
4800
|
+
setDefaultMethod(methodId, options = {}) {
|
|
4801
|
+
return this.http.request({
|
|
4802
|
+
method: "POST",
|
|
4803
|
+
path: `/api/v1/subscription/methods/${encodePathSegment(methodId, "methodId")}/default`,
|
|
4804
|
+
...this.requestOptions(options)
|
|
4805
|
+
});
|
|
4806
|
+
}
|
|
4807
|
+
/** Удаляет способ оплаты. */
|
|
4808
|
+
removeMethod(methodId, options = {}) {
|
|
4809
|
+
return this.http.request({
|
|
4810
|
+
method: "DELETE",
|
|
4811
|
+
path: `/api/v1/subscription/methods/${encodePathSegment(methodId, "methodId")}`,
|
|
4812
|
+
...this.requestOptions(options)
|
|
4813
|
+
});
|
|
4814
|
+
}
|
|
4815
|
+
};
|
|
4816
|
+
|
|
4817
|
+
// src/resources/telemetry.ts
|
|
4818
|
+
var TelemetryResource = class extends BaseResource {
|
|
4819
|
+
/** Идентификатор сессии телеметрии, общий для всех событий этого объекта. */
|
|
4820
|
+
#sessionId;
|
|
4821
|
+
/**
|
|
4822
|
+
* Идентификатор сессии телеметрии (`sid`).
|
|
4823
|
+
*
|
|
4824
|
+
* Создаётся лениво при первом обращении и далее неизменен.
|
|
4825
|
+
*/
|
|
4826
|
+
get sessionId() {
|
|
4827
|
+
this.#sessionId ??= createDeviceId();
|
|
4828
|
+
return this.#sessionId;
|
|
4829
|
+
}
|
|
4830
|
+
/**
|
|
4831
|
+
* Отправляет события просмотра постов (`POST /api/v1/i`).
|
|
4832
|
+
*
|
|
4833
|
+
* @experimental См. предупреждение у {@link TelemetryResource}.
|
|
4834
|
+
*/
|
|
4835
|
+
dwell(entries, options = {}) {
|
|
4836
|
+
return this.http.request({
|
|
4837
|
+
method: "POST",
|
|
4838
|
+
path: "/api/v1/i",
|
|
4839
|
+
body: {
|
|
4840
|
+
sid: options.sid ?? this.sessionId,
|
|
4841
|
+
e: entries.map((entry) => ({
|
|
4842
|
+
md: entry.durationMs ?? entry.exitAt - entry.enterAt,
|
|
4843
|
+
et: entry.enterAt,
|
|
4844
|
+
xt: entry.exitAt,
|
|
4845
|
+
r: entry.reason,
|
|
4846
|
+
v: entry.vs,
|
|
4847
|
+
...entry.sourceContext !== void 0 ? { sc: entry.sourceContext } : {},
|
|
4848
|
+
...entry.source !== void 0 ? { s: entry.source } : {},
|
|
4849
|
+
...entry.repeat ? { b: 1 } : {}
|
|
4850
|
+
}))
|
|
4851
|
+
},
|
|
4852
|
+
...this.requestOptions(options)
|
|
4853
|
+
});
|
|
4854
|
+
}
|
|
4855
|
+
/**
|
|
4856
|
+
* Отправляет события взаимодействия с контентом (`POST /api/v1/x`).
|
|
4857
|
+
*
|
|
4858
|
+
* @experimental См. предупреждение у {@link TelemetryResource}.
|
|
4859
|
+
*/
|
|
4860
|
+
interaction(entries, options = {}) {
|
|
4861
|
+
return this.http.request({
|
|
4862
|
+
method: "POST",
|
|
4863
|
+
path: "/api/v1/x",
|
|
4864
|
+
body: {
|
|
4865
|
+
sid: options.sid ?? this.sessionId,
|
|
4866
|
+
e: entries.map((entry) => ({
|
|
4867
|
+
t: entry.type,
|
|
4868
|
+
v: entry.vs,
|
|
4869
|
+
ai: entry.postId,
|
|
4870
|
+
...entry.mediaIndex !== void 0 ? { mi: entry.mediaIndex } : {},
|
|
4871
|
+
...entry.source !== void 0 ? { s: entry.source } : {},
|
|
4872
|
+
...entry.positionMs !== void 0 ? { pm: Math.round(entry.positionMs) } : {},
|
|
4873
|
+
...entry.durationMs !== void 0 ? { dm: Math.round(entry.durationMs) } : {}
|
|
4874
|
+
}))
|
|
4875
|
+
},
|
|
4876
|
+
...this.requestOptions(options)
|
|
4877
|
+
});
|
|
4878
|
+
}
|
|
4879
|
+
};
|
|
4880
|
+
|
|
4778
4881
|
// src/resources/users.ts
|
|
4779
4882
|
var UsersResource = class extends BaseResource {
|
|
4883
|
+
/**
|
|
4884
|
+
* Списки пользователей: подписчики, подписки, заблокированные.
|
|
4885
|
+
*
|
|
4886
|
+
* Путь приходит в параметрах — так один описатель обслуживает все три эндпоинта. Имена
|
|
4887
|
+
* полей перечислены с запасом: списки приходят под `users`, но альтернативное имя ничего
|
|
4888
|
+
* не стоит и спасает, если эндпоинт назовёт список по-своему. `page` уходит в запрос, хотя
|
|
4889
|
+
* сервер его сейчас не читает (см. {@link followers}): когда починят — заработает само.
|
|
4890
|
+
*/
|
|
4891
|
+
#userList = this.paginated({
|
|
4892
|
+
path: (p) => p.path,
|
|
4893
|
+
query: (p) => ({ limit: p.limit }),
|
|
4894
|
+
start: (p) => p.page !== void 0 ? { page: p.page } : {},
|
|
4895
|
+
read: (body) => readPagedPage(body, "users", "followers", "following", "blocked"),
|
|
4896
|
+
mode: PaginationMode.Page
|
|
4897
|
+
});
|
|
4780
4898
|
/** Загружает свой профиль — с подпиской и признаком подтверждённого телефона. */
|
|
4781
4899
|
me(options = {}) {
|
|
4782
4900
|
return this.http.request({
|
|
@@ -5022,38 +5140,33 @@ var UsersResource = class extends BaseResource {
|
|
|
5022
5140
|
...this.requestOptions(options)
|
|
5023
5141
|
});
|
|
5024
5142
|
}
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5143
|
+
#userPage(path, params) {
|
|
5144
|
+
return this.#userList.list({ ...params, path });
|
|
5145
|
+
}
|
|
5146
|
+
#userPaginator(path, params) {
|
|
5147
|
+
return this.#userList.iterate({ ...params, path });
|
|
5148
|
+
}
|
|
5149
|
+
};
|
|
5150
|
+
|
|
5151
|
+
// src/resources/verification.ts
|
|
5152
|
+
var VerificationResource = class extends BaseResource {
|
|
5153
|
+
/** Загружает статус заявки. Значение `none` означает, что заявка не подавалась. */
|
|
5154
|
+
status(options = {}) {
|
|
5155
|
+
return this.http.request({
|
|
5037
5156
|
method: "GET",
|
|
5038
|
-
path,
|
|
5039
|
-
|
|
5040
|
-
...this.requestOptions(params)
|
|
5157
|
+
path: "/api/verification/status",
|
|
5158
|
+
...this.requestOptions(options)
|
|
5041
5159
|
});
|
|
5042
|
-
return readPagedPage(body, "users", "followers", "following", "blocked");
|
|
5043
5160
|
}
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5161
|
+
/** Подаёт заявку на верификацию с видео. */
|
|
5162
|
+
submit(videoUrl, options = {}) {
|
|
5163
|
+
return this.http.request({
|
|
5164
|
+
method: "POST",
|
|
5165
|
+
path: "/api/verification/submit",
|
|
5166
|
+
body: { videoUrl },
|
|
5167
|
+
...this.requestOptions(options)
|
|
5047
5168
|
});
|
|
5048
5169
|
}
|
|
5049
|
-
#userPaginator(path, params) {
|
|
5050
|
-
return this.paginate(
|
|
5051
|
-
PaginationMode.Page,
|
|
5052
|
-
(state) => this.#loadUserPage(path, params, state),
|
|
5053
|
-
// Без `start` перебор начинался бы с первой страницы, молча игнорируя `page`.
|
|
5054
|
-
{ ...params, ...params.page !== void 0 ? { start: { page: params.page } } : {} }
|
|
5055
|
-
);
|
|
5056
|
-
}
|
|
5057
5170
|
};
|
|
5058
5171
|
|
|
5059
5172
|
// src/client.ts
|
|
@@ -5064,6 +5177,8 @@ var ItdClient = class {
|
|
|
5064
5177
|
#jar;
|
|
5065
5178
|
#queue;
|
|
5066
5179
|
#plugins = new PluginRegistry();
|
|
5180
|
+
/** Порождённые потоки уведомлений — чтобы `close()` мог закрыть их разом. */
|
|
5181
|
+
#streams = /* @__PURE__ */ new Set();
|
|
5067
5182
|
/** Авторизация, сессии и пароли. */
|
|
5068
5183
|
auth;
|
|
5069
5184
|
/** Профили, подписки, блокировки, приватность. */
|
|
@@ -5094,26 +5209,56 @@ var ItdClient = class {
|
|
|
5094
5209
|
* @experimental Недокументированные эндпоинты. Библиотека никогда не отправляет их сама.
|
|
5095
5210
|
*/
|
|
5096
5211
|
telemetry;
|
|
5097
|
-
constructor(options = {}) {
|
|
5098
|
-
|
|
5212
|
+
constructor(options = {}, internals = {}) {
|
|
5213
|
+
const config = resolveConfig(options);
|
|
5214
|
+
this.#config = config;
|
|
5099
5215
|
this.#jar = new CookieJar();
|
|
5100
|
-
|
|
5101
|
-
this.#
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
onUnauthorized: () => this.#authManager.onUnauthorized(),
|
|
5108
|
-
getCookieHeader: (url) => this.#jar.getHeader(url),
|
|
5109
|
-
saveCookies: (url, response) => this.#jar.setFromResponse(url, response),
|
|
5110
|
-
...this.#queue ? { schedule: this.#queue.schedule.bind(this.#queue) } : {},
|
|
5111
|
-
// Планировщик нужен, даже когда обычные повторы выключены: лимит частоты
|
|
5112
|
-
// живёт по своим правилам и настраивается отдельно, в `rateLimit`.
|
|
5113
|
-
...this.#config.retry || this.#config.rateLimit ? { nextRetryDelay: this.#createRetryScheduler() } : {},
|
|
5114
|
-
...this.#queue && this.#config.rateLimit?.respectHeaders ? { onRateLimit: this.#throttleByHeaders.bind(this) } : {}
|
|
5216
|
+
const queue = config.rateLimit ? new RequestQueue(config.rateLimit) : void 0;
|
|
5217
|
+
this.#queue = queue;
|
|
5218
|
+
let authManager;
|
|
5219
|
+
const transport = new Transport(config, {
|
|
5220
|
+
cookies: config.useCookieJar ? this.#jar : void 0,
|
|
5221
|
+
getDeviceId: () => authManager.getDeviceId(),
|
|
5222
|
+
onRateLimit: queue && config.rateLimit?.respectHeaders ? (limit, remaining) => this.#throttleByHeaders(limit, remaining) : void 0
|
|
5115
5223
|
});
|
|
5116
|
-
|
|
5224
|
+
const pluginsLayer = createPluginsMiddleware(this.#plugins);
|
|
5225
|
+
const retriesLayer = createRetryMiddleware({
|
|
5226
|
+
retry: config.retry,
|
|
5227
|
+
rateLimitDelays: config.rateLimit?.retryDelays ?? [],
|
|
5228
|
+
pauseQueue: queue ? (ms) => queue.pause(ms) : void 0,
|
|
5229
|
+
hooks: config.hooks,
|
|
5230
|
+
logger: config.logger,
|
|
5231
|
+
buildUrl: (request) => transport.buildUrl(request)
|
|
5232
|
+
});
|
|
5233
|
+
const authRetry = config.retry ? {
|
|
5234
|
+
attempts: config.retry.attempts,
|
|
5235
|
+
baseDelay: config.retry.baseDelay,
|
|
5236
|
+
maxDelay: config.retry.maxDelay,
|
|
5237
|
+
jitter: config.retry.jitter,
|
|
5238
|
+
retryWrites: true,
|
|
5239
|
+
...config.retry.shouldRetry ? { shouldRetry: config.retry.shouldRetry } : {}
|
|
5240
|
+
} : void 0;
|
|
5241
|
+
const authPipeline = composePipeline([pluginsLayer, retriesLayer], transport.send);
|
|
5242
|
+
const authHandler = (request) => authRetry && request.retry === void 0 ? authPipeline({ ...request, retry: authRetry }) : authPipeline(request);
|
|
5243
|
+
authManager = new AuthManager(config, authHandler, this.#jar);
|
|
5244
|
+
this.#authManager = authManager;
|
|
5245
|
+
const middlewares = [];
|
|
5246
|
+
if (queue) middlewares.push(createQueueMiddleware(queue.schedule.bind(queue)));
|
|
5247
|
+
middlewares.push(pluginsLayer);
|
|
5248
|
+
middlewares.push(retriesLayer);
|
|
5249
|
+
middlewares.push(
|
|
5250
|
+
createAuthMiddleware({
|
|
5251
|
+
getAuthHeaders: () => authManager.getAuthHeaders(),
|
|
5252
|
+
onUnauthorized: () => authManager.onUnauthorized(),
|
|
5253
|
+
autoRefresh: config.autoRefresh
|
|
5254
|
+
})
|
|
5255
|
+
);
|
|
5256
|
+
const handler = composePipeline(middlewares, transport.send);
|
|
5257
|
+
this.#http = new HttpClient({ handler, plugins: this.#plugins, baseUrl: config.baseUrl });
|
|
5258
|
+
this.files = new FilesResource(
|
|
5259
|
+
this.#http,
|
|
5260
|
+
internals.fileReader ? { readFile: internals.fileReader } : {}
|
|
5261
|
+
);
|
|
5117
5262
|
const uploadFiles = (files, requestOptions) => this.files.uploadMany(files, requestOptions ?? {});
|
|
5118
5263
|
this.auth = new AuthResource(this.#http, { auth: this.#authManager });
|
|
5119
5264
|
this.users = new UsersResource(this.#http);
|
|
@@ -5203,17 +5348,44 @@ var ItdClient = class {
|
|
|
5203
5348
|
* ```
|
|
5204
5349
|
*/
|
|
5205
5350
|
realtime(options = {}) {
|
|
5206
|
-
|
|
5351
|
+
let stream;
|
|
5352
|
+
stream = new ItdRealtime(
|
|
5207
5353
|
{
|
|
5208
5354
|
baseUrl: this.#config.baseUrl,
|
|
5209
5355
|
fetch: this.#config.fetch,
|
|
5210
5356
|
getToken: () => this.#authManager.getAccessToken(),
|
|
5211
5357
|
refresh: () => this.#authManager.onUnauthorized(),
|
|
5212
5358
|
fetchUnreadCount: () => this.notifications.count(),
|
|
5359
|
+
onClose: () => this.#streams.delete(stream),
|
|
5213
5360
|
logger: this.#config.logger
|
|
5214
5361
|
},
|
|
5215
5362
|
options
|
|
5216
5363
|
);
|
|
5364
|
+
this.#streams.add(stream);
|
|
5365
|
+
return stream;
|
|
5366
|
+
}
|
|
5367
|
+
/**
|
|
5368
|
+
* Освобождает ресурсы клиента: останавливает очередь запросов (снимает отложенные паузы)
|
|
5369
|
+
* и закрывает все потоки уведомлений, созданные через {@link realtime}.
|
|
5370
|
+
*
|
|
5371
|
+
* После вызова клиентом можно пользоваться снова — новые запросы поднимут всё заново,
|
|
5372
|
+
* но уже созданные потоки останутся закрытыми.
|
|
5373
|
+
*
|
|
5374
|
+
* @example
|
|
5375
|
+
* ```ts
|
|
5376
|
+
* await using itd = new ItdClient({ auth: token });
|
|
5377
|
+
* // …работа…
|
|
5378
|
+
* // close() вызовется сам на выходе из блока
|
|
5379
|
+
* ```
|
|
5380
|
+
*/
|
|
5381
|
+
async close() {
|
|
5382
|
+
for (const stream of this.#streams) stream.disconnect();
|
|
5383
|
+
this.#streams.clear();
|
|
5384
|
+
this.#queue?.stop();
|
|
5385
|
+
}
|
|
5386
|
+
/** Позволяет использовать клиент с `await using`. */
|
|
5387
|
+
[Symbol.asyncDispose]() {
|
|
5388
|
+
return this.close();
|
|
5217
5389
|
}
|
|
5218
5390
|
/** Текущая сессия целиком — чтобы сохранить её самостоятельно. */
|
|
5219
5391
|
getSession() {
|
|
@@ -5223,16 +5395,6 @@ var ItdClient = class {
|
|
|
5223
5395
|
setSession(session) {
|
|
5224
5396
|
return this.#authManager.setSession(session);
|
|
5225
5397
|
}
|
|
5226
|
-
/**
|
|
5227
|
-
* Подключает чтение файлов с диска.
|
|
5228
|
-
*
|
|
5229
|
-
* Вызывается из `itd-api/node`; напрямую обычно не нужно.
|
|
5230
|
-
*
|
|
5231
|
-
* @internal
|
|
5232
|
-
*/
|
|
5233
|
-
setFileReader(readFile) {
|
|
5234
|
-
this.files.setFileReader(readFile);
|
|
5235
|
-
}
|
|
5236
5398
|
/**
|
|
5237
5399
|
* Придерживает очередь, когда лимит сервера исчерпан.
|
|
5238
5400
|
*
|
|
@@ -5253,35 +5415,12 @@ var ItdClient = class {
|
|
|
5253
5415
|
`\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`
|
|
5254
5416
|
);
|
|
5255
5417
|
}
|
|
5256
|
-
/**
|
|
5257
|
-
* Собирает планировщик повторов и связывает его с очередью.
|
|
5258
|
-
*
|
|
5259
|
-
* Ответ `429` обрабатывается отдельно от прочих ошибок. Причина в том, что сервер
|
|
5260
|
-
* не присылает `Retry-After` и не сообщает время сброса окна: экспоненциальный откат
|
|
5261
|
-
* в сотни миллисекунд здесь бесполезен, а окно измеряется десятками секунд. Вместо
|
|
5262
|
-
* расчёта берётся лестница пауз `rateLimit.retryDelays`, и она не зависит
|
|
5263
|
-
* от `retry.attempts`, у которого совсем другая задача.
|
|
5264
|
-
*
|
|
5265
|
-
* Пауза накладывается на всю очередь: иначе остальные запросы продолжат добивать API,
|
|
5266
|
-
* пока первый ждёт.
|
|
5267
|
-
*/
|
|
5268
|
-
#createRetryScheduler() {
|
|
5269
|
-
const retry = this.#config.retry;
|
|
5270
|
-
const scheduler = retry ? createRetryScheduler(retry) : void 0;
|
|
5271
|
-
const queue = this.#queue;
|
|
5272
|
-
const delays = this.#config.rateLimit?.retryDelays ?? [];
|
|
5273
|
-
return (error, attempt, method) => {
|
|
5274
|
-
if (isItdRateLimitError(error)) {
|
|
5275
|
-
const wait = error.retryAfter ?? delays[attempt - 1];
|
|
5276
|
-
if (wait === void 0) return void 0;
|
|
5277
|
-
queue?.pause(wait);
|
|
5278
|
-
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`);
|
|
5279
|
-
return wait;
|
|
5280
|
-
}
|
|
5281
|
-
return scheduler?.(error, attempt, method);
|
|
5282
|
-
};
|
|
5283
|
-
}
|
|
5284
5418
|
};
|
|
5419
|
+
if (typeof Symbol.asyncDispose !== "symbol") {
|
|
5420
|
+
const prototype = ItdClient.prototype;
|
|
5421
|
+
prototype[/* @__PURE__ */ Symbol.for("Symbol.asyncDispose")] = prototype.undefined;
|
|
5422
|
+
delete prototype.undefined;
|
|
5423
|
+
}
|
|
5285
5424
|
function createClient(options = {}) {
|
|
5286
5425
|
return new ItdClient(options);
|
|
5287
5426
|
}
|
|
@@ -5397,6 +5536,7 @@ exports.ALLOWED_MIME_TYPES = ALLOWED_MIME_TYPES;
|
|
|
5397
5536
|
exports.AUDIO_MIME_TYPES = AUDIO_MIME_TYPES;
|
|
5398
5537
|
exports.AUTH_FLAG_COOKIE = AUTH_FLAG_COOKIE;
|
|
5399
5538
|
exports.AUTH_PATHS = AUTH_PATHS;
|
|
5539
|
+
exports.AccessType = AccessType;
|
|
5400
5540
|
exports.AttachmentType = AttachmentType;
|
|
5401
5541
|
exports.CommentSort = CommentSort;
|
|
5402
5542
|
exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
|
|
@@ -5406,6 +5546,7 @@ exports.DEVICE_ID_HEADER = DEVICE_ID_HEADER;
|
|
|
5406
5546
|
exports.DetectedRuntime = DetectedRuntime;
|
|
5407
5547
|
exports.FeedTab = FeedTab;
|
|
5408
5548
|
exports.IMAGE_MIME_TYPES = IMAGE_MIME_TYPES;
|
|
5549
|
+
exports.InteractionType = InteractionType;
|
|
5409
5550
|
exports.ItdAbortError = ItdAbortError;
|
|
5410
5551
|
exports.ItdApiError = ItdApiError;
|
|
5411
5552
|
exports.ItdApiErrorKind = ItdApiErrorKind;
|
|
@@ -5450,6 +5591,8 @@ exports.SpanType = SpanType;
|
|
|
5450
5591
|
exports.TURNSTILE_SITE_KEY = TURNSTILE_SITE_KEY;
|
|
5451
5592
|
exports.UnauthorizedStreamError = UnauthorizedStreamError;
|
|
5452
5593
|
exports.VIDEO_MIME_TYPES = VIDEO_MIME_TYPES;
|
|
5594
|
+
exports.ViewReason = ViewReason;
|
|
5595
|
+
exports.ViewSource = ViewSource;
|
|
5453
5596
|
exports.WallAccess = WallAccess;
|
|
5454
5597
|
exports.canonicalNotificationType = canonicalNotificationType;
|
|
5455
5598
|
exports.comment = comment;
|
|
@@ -5477,5 +5620,5 @@ exports.readUnreadCountEvent = readUnreadCountEvent;
|
|
|
5477
5620
|
exports.report = report;
|
|
5478
5621
|
exports.resolveNotificationUrl = resolveNotificationUrl;
|
|
5479
5622
|
exports.toDate = toDate;
|
|
5480
|
-
//# sourceMappingURL=chunk-
|
|
5481
|
-
//# sourceMappingURL=chunk-
|
|
5623
|
+
//# sourceMappingURL=chunk-ATCZ4T2K.cjs.map
|
|
5624
|
+
//# sourceMappingURL=chunk-ATCZ4T2K.cjs.map
|