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