itd-api 0.0.7 → 0.0.9
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 +94 -10
- package/dist/{chunk-3RNNZJZ4.cjs → chunk-HTF2MOM4.cjs} +1552 -1081
- package/dist/chunk-HTF2MOM4.cjs.map +1 -0
- package/dist/{chunk-CG4SERVM.js → chunk-MYAU2WJU.js} +1542 -1082
- package/dist/chunk-MYAU2WJU.js.map +1 -0
- package/dist/index.cjs +133 -89
- package/dist/index.d.cts +4206 -1
- package/dist/index.d.ts +4206 -1
- package/dist/index.js +1 -1
- package/dist/node.cjs +165 -96
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +3 -3
- package/dist/node.d.ts +3 -3
- package/dist/node.js +35 -10
- package/dist/node.js.map +1 -1
- package/package.json +8 -4
- package/dist/chunk-3RNNZJZ4.cjs.map +0 -1
- package/dist/chunk-CG4SERVM.js.map +0 -1
- package/dist/index-DNFPX_Z1.d.cts +0 -3900
- package/dist/index-DNFPX_Z1.d.ts +0 -3900
|
@@ -614,6 +614,20 @@ var RealtimeStatus = Object.freeze({
|
|
|
614
614
|
Error: "error",
|
|
615
615
|
Disconnected: "disconnected"
|
|
616
616
|
});
|
|
617
|
+
var ServiceState = Object.freeze({
|
|
618
|
+
/** Работает штатно. */
|
|
619
|
+
Operational: "operational",
|
|
620
|
+
/** Работает с деградацией. */
|
|
621
|
+
Degraded: "degraded",
|
|
622
|
+
/** Недоступен. */
|
|
623
|
+
Downtime: "downtime"
|
|
624
|
+
});
|
|
625
|
+
var IncidentKind = Object.freeze({
|
|
626
|
+
/** Недоступен. */
|
|
627
|
+
Down: "down",
|
|
628
|
+
/** Деградация. */
|
|
629
|
+
Degraded: "deg"
|
|
630
|
+
});
|
|
617
631
|
var AccessType = Object.freeze({
|
|
618
632
|
/** Никто. */
|
|
619
633
|
Nobody: "nobody",
|
|
@@ -1283,8 +1297,18 @@ function hasLocalStorage() {
|
|
|
1283
1297
|
|
|
1284
1298
|
// src/core/auth.ts
|
|
1285
1299
|
var AUTH_PATHS = {
|
|
1300
|
+
signUp: "/api/v1/auth/sign-up",
|
|
1286
1301
|
signIn: "/api/v1/auth/sign-in",
|
|
1287
|
-
|
|
1302
|
+
verifyOtp: "/api/v1/auth/verify-otp",
|
|
1303
|
+
resendOtp: "/api/v1/auth/resend-otp",
|
|
1304
|
+
refresh: "/api/v1/auth/refresh",
|
|
1305
|
+
logout: "/api/v1/auth/logout",
|
|
1306
|
+
forgotPassword: "/api/v1/auth/forgot-password",
|
|
1307
|
+
resetPassword: "/api/v1/auth/reset-password",
|
|
1308
|
+
changePassword: "/api/v1/auth/change-password",
|
|
1309
|
+
sessions: "/api/v1/auth/sessions",
|
|
1310
|
+
/** Префикс внешнего входа: к нему дописывается имя провайдера. */
|
|
1311
|
+
oauthLogin: "/api/v1/auth/login"
|
|
1288
1312
|
};
|
|
1289
1313
|
var TURNSTILE_SITE_KEY = "0x4AAAAAACHhxczw6fJGwPBg";
|
|
1290
1314
|
var DEVICE_ID_HEADER = "X-Device-Id";
|
|
@@ -1293,13 +1317,23 @@ function readAccessToken(payload) {
|
|
|
1293
1317
|
const token = payload.accessToken;
|
|
1294
1318
|
return typeof token === "string" && token.length > 0 ? token : void 0;
|
|
1295
1319
|
}
|
|
1320
|
+
function reportListenerError(logger, scope, error) {
|
|
1321
|
+
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}`;
|
|
1322
|
+
if (logger) logger.error(message, error);
|
|
1323
|
+
else console.error(`[itd-api] ${message}`, error);
|
|
1324
|
+
}
|
|
1296
1325
|
var AuthManager = class {
|
|
1297
1326
|
#config;
|
|
1298
|
-
#
|
|
1327
|
+
#send;
|
|
1299
1328
|
#jar;
|
|
1300
|
-
#emitter
|
|
1329
|
+
#emitter;
|
|
1301
1330
|
/** `undefined` — сессия ещё не читалась из хранилища. */
|
|
1302
1331
|
#session;
|
|
1332
|
+
/**
|
|
1333
|
+
* Общий промис чтения сессии из хранилища. Дедупликация: параллельные запросы на холодном
|
|
1334
|
+
* старте читают хранилище один раз и не заводят каждый свой `deviceId`.
|
|
1335
|
+
*/
|
|
1336
|
+
#loading = null;
|
|
1303
1337
|
/** Общий промис обновления: к нему присоединяются все, кто получил 401. */
|
|
1304
1338
|
#refreshing = null;
|
|
1305
1339
|
/** Общий промис входа по логину и паролю. */
|
|
@@ -1311,10 +1345,15 @@ var AuthManager = class {
|
|
|
1311
1345
|
* поэтому `clear()` его не трогает.
|
|
1312
1346
|
*/
|
|
1313
1347
|
#deviceId;
|
|
1314
|
-
|
|
1348
|
+
/** Общий промис первичной выдачи `deviceId` — чтобы параллельные запросы получили один. */
|
|
1349
|
+
#deviceIdLoading = null;
|
|
1350
|
+
constructor(config, send, jar) {
|
|
1315
1351
|
this.#config = config;
|
|
1316
|
-
this.#
|
|
1352
|
+
this.#send = send;
|
|
1317
1353
|
this.#jar = jar;
|
|
1354
|
+
this.#emitter = new Emitter(
|
|
1355
|
+
(error) => reportListenerError(config.logger, "\u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u0438", error)
|
|
1356
|
+
);
|
|
1318
1357
|
}
|
|
1319
1358
|
/** Подписка на события авторизации. */
|
|
1320
1359
|
get on() {
|
|
@@ -1341,7 +1380,7 @@ var AuthManager = class {
|
|
|
1341
1380
|
/** То же самое, но без чтения хранилища — для вызовов, где сессия уже загружена. */
|
|
1342
1381
|
#hasRefreshSession() {
|
|
1343
1382
|
if (!this.#config.useCookieJar) return true;
|
|
1344
|
-
if (this.#jar.has(AUTH_FLAG_COOKIE)) return true;
|
|
1383
|
+
if (this.#jar.has(AUTH_FLAG_COOKIE, this.#config.baseUrl)) return true;
|
|
1345
1384
|
return Boolean(this.#session?.refreshToken);
|
|
1346
1385
|
}
|
|
1347
1386
|
/** Заголовки авторизации для очередного запроса. Пустой объект, если токена нет. */
|
|
@@ -1356,8 +1395,14 @@ var AuthManager = class {
|
|
|
1356
1395
|
* сервер связывает с ним запись в списке сессий, и плавающее значение плодило бы
|
|
1357
1396
|
* по новой сессии на каждый старт.
|
|
1358
1397
|
*/
|
|
1359
|
-
|
|
1360
|
-
if (this.#deviceId) return this.#deviceId;
|
|
1398
|
+
getDeviceId() {
|
|
1399
|
+
if (this.#deviceId) return Promise.resolve(this.#deviceId);
|
|
1400
|
+
this.#deviceIdLoading ??= this.#resolveDeviceId().finally(() => {
|
|
1401
|
+
this.#deviceIdLoading = null;
|
|
1402
|
+
});
|
|
1403
|
+
return this.#deviceIdLoading;
|
|
1404
|
+
}
|
|
1405
|
+
async #resolveDeviceId() {
|
|
1361
1406
|
const session = await this.#loadSession();
|
|
1362
1407
|
const deviceId = this.#config.deviceId ?? session?.deviceId ?? createDeviceId();
|
|
1363
1408
|
this.#deviceId = deviceId;
|
|
@@ -1440,12 +1485,14 @@ var AuthManager = class {
|
|
|
1440
1485
|
async getSession() {
|
|
1441
1486
|
return this.#loadSession();
|
|
1442
1487
|
}
|
|
1443
|
-
/** Заменяет сессию целиком. */
|
|
1488
|
+
/** Заменяет сессию и связанные с ней cookie целиком. */
|
|
1444
1489
|
async setSession(session) {
|
|
1490
|
+
this.#jar.clear();
|
|
1445
1491
|
this.#jar.deserialize(session.cookies);
|
|
1446
|
-
this.#deviceId
|
|
1447
|
-
|
|
1492
|
+
if (session.deviceId) this.#deviceId = session.deviceId;
|
|
1493
|
+
this.#session = session;
|
|
1448
1494
|
this.#seedRefreshCookie();
|
|
1495
|
+
await this.#saveSession(session);
|
|
1449
1496
|
}
|
|
1450
1497
|
/**
|
|
1451
1498
|
* Забывает сессию и cookie. Сетевой запрос не выполняется.
|
|
@@ -1460,8 +1507,14 @@ var AuthManager = class {
|
|
|
1460
1507
|
if (this.#deviceId) await this.#saveSession({ deviceId: this.#deviceId });
|
|
1461
1508
|
this.#emitter.emit("signOut", void 0);
|
|
1462
1509
|
}
|
|
1463
|
-
|
|
1464
|
-
if (this.#session !== void 0) return this.#session;
|
|
1510
|
+
#loadSession() {
|
|
1511
|
+
if (this.#session !== void 0) return Promise.resolve(this.#session);
|
|
1512
|
+
this.#loading ??= this.#performLoad().finally(() => {
|
|
1513
|
+
this.#loading = null;
|
|
1514
|
+
});
|
|
1515
|
+
return this.#loading;
|
|
1516
|
+
}
|
|
1517
|
+
async #performLoad() {
|
|
1465
1518
|
const stored = await this.#config.storage.get() ?? null;
|
|
1466
1519
|
if (stored?.cookies) this.#jar.deserialize(stored.cookies);
|
|
1467
1520
|
const fromConfig = this.#sessionFromConfig(this.#config.auth);
|
|
@@ -1529,17 +1582,14 @@ var AuthManager = class {
|
|
|
1529
1582
|
return this.#reloginOrNull();
|
|
1530
1583
|
}
|
|
1531
1584
|
try {
|
|
1532
|
-
const payload = await this.#
|
|
1585
|
+
const payload = await this.#send({
|
|
1533
1586
|
method: "POST",
|
|
1534
1587
|
path: AUTH_PATHS.refresh,
|
|
1588
|
+
skipQueue: true,
|
|
1589
|
+
skipAuth: true,
|
|
1590
|
+
skipAuthRefresh: true
|
|
1535
1591
|
// Тела нет намеренно: сервер читает refresh-токен только из cookie — см.
|
|
1536
1592
|
// #seedRefreshCookie. По той же причине не нужен и устаревший Bearer.
|
|
1537
|
-
skipAuth: true,
|
|
1538
|
-
// Без этого 401 на самом обновлении вызвал бы новое обновление — и так по кругу.
|
|
1539
|
-
skipAuthRefresh: true,
|
|
1540
|
-
// Обновление почти всегда запускается изнутри запроса, который занимает место
|
|
1541
|
-
// в очереди и ждёт его результата. Встать в ту же очередь — значит зависнуть.
|
|
1542
|
-
skipQueue: true
|
|
1543
1593
|
});
|
|
1544
1594
|
const accessToken = readAccessToken(payload);
|
|
1545
1595
|
if (!accessToken) return this.#reloginOrNull();
|
|
@@ -1606,20 +1656,18 @@ var AuthManager = class {
|
|
|
1606
1656
|
}
|
|
1607
1657
|
if (credentials.turnstileToken) return credentials.turnstileToken;
|
|
1608
1658
|
throw new ItdConfigError(
|
|
1609
|
-
"\u0412\u0445\u043E\u0434 \u043F\u043E email \u0438 \u043F\u0430\u0440\u043E\u043B\u044E \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0442\u043E\u043A\u0435\u043D \u043A\u0430\u043F\u0447\u0438 Cloudflare Turnstile: \u0431\u0435\u0437 \u043D\u0435\u0433\u043E \u0441\u0435\u0440\u0432\u0435\u0440 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 422. \u041F\u0435\u0440\u0435\u0434\u0430\u0439\u0442\u0435 auth.getTurnstileToken (\u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u0442\u043E\u043A\u0435\u043D\u0430) \u043B\u0438\u0431\u043E \u0440\u0430\u0437\u043E\u0432\u044B\u0439 auth.turnstileToken. \u041A\u043B\u044E\u0447 \u0432\u0438\u0434\u0436\u0435\u0442\u0430 \u2014 TURNSTILE_SITE_KEY. \u0412 Node \u0442\u043E\u043A\u0435\u043D \u0443\u043C\u0435\u0435\u0442 \u0434\u043E\u0431\u044B\u0432\u0430\u0442\u044C \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u044B\u0439 \u043F\u0430\u043A\u0435\u0442: npm i itd-api
|
|
1659
|
+
"\u0412\u0445\u043E\u0434 \u043F\u043E email \u0438 \u043F\u0430\u0440\u043E\u043B\u044E \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0442\u043E\u043A\u0435\u043D \u043A\u0430\u043F\u0447\u0438 Cloudflare Turnstile: \u0431\u0435\u0437 \u043D\u0435\u0433\u043E \u0441\u0435\u0440\u0432\u0435\u0440 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 422. \u041F\u0435\u0440\u0435\u0434\u0430\u0439\u0442\u0435 auth.getTurnstileToken (\u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u0442\u043E\u043A\u0435\u043D\u0430) \u043B\u0438\u0431\u043E \u0440\u0430\u0437\u043E\u0432\u044B\u0439 auth.turnstileToken. \u041A\u043B\u044E\u0447 \u0432\u0438\u0434\u0436\u0435\u0442\u0430 \u2014 TURNSTILE_SITE_KEY. \u0412 Node \u0442\u043E\u043A\u0435\u043D \u0443\u043C\u0435\u0435\u0442 \u0434\u043E\u0431\u044B\u0432\u0430\u0442\u044C \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u044B\u0439 \u043F\u0430\u043A\u0435\u0442: npm i @itd-api/turnstile, \u0437\u0430\u0442\u0435\u043C getTurnstileToken: createTurnstileSolver()."
|
|
1610
1660
|
);
|
|
1611
1661
|
}
|
|
1612
1662
|
async #performSignIn(credentials) {
|
|
1613
1663
|
const turnstileToken = await this.#resolveTurnstileToken(credentials);
|
|
1614
|
-
const payload = await this.#
|
|
1664
|
+
const payload = await this.#send({
|
|
1615
1665
|
method: "POST",
|
|
1616
1666
|
path: AUTH_PATHS.signIn,
|
|
1617
1667
|
body: { email: credentials.email, password: credentials.password, turnstileToken },
|
|
1668
|
+
skipQueue: true,
|
|
1618
1669
|
skipAuth: true,
|
|
1619
|
-
skipAuthRefresh: true
|
|
1620
|
-
// Отложенный вход происходит при сборке заголовков уже начатого запроса — тот держит
|
|
1621
|
-
// место в очереди и ждёт токена. См. `skipQueue` в RawRequestOptions.
|
|
1622
|
-
skipQueue: true
|
|
1670
|
+
skipAuthRefresh: true
|
|
1623
1671
|
});
|
|
1624
1672
|
const accessToken = readAccessToken(payload);
|
|
1625
1673
|
if (!accessToken) {
|
|
@@ -1653,6 +1701,7 @@ var MemoryTokenStorage = class {
|
|
|
1653
1701
|
var LocalStorageTokenStorage = class {
|
|
1654
1702
|
#key;
|
|
1655
1703
|
#fallback = new MemoryTokenStorage();
|
|
1704
|
+
/** Доступен ли `localStorage` для дальнейших операций. */
|
|
1656
1705
|
#available;
|
|
1657
1706
|
/** @param key ключ в `localStorage`. По умолчанию `itd-api:session`. */
|
|
1658
1707
|
constructor(key = "itd-api:session") {
|
|
@@ -1671,26 +1720,31 @@ var LocalStorageTokenStorage = class {
|
|
|
1671
1720
|
}
|
|
1672
1721
|
}
|
|
1673
1722
|
set(session) {
|
|
1674
|
-
if (
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
this.#fallback.set(session);
|
|
1723
|
+
if (this.#available) {
|
|
1724
|
+
try {
|
|
1725
|
+
globalThis.localStorage.setItem(this.#key, JSON.stringify(session));
|
|
1726
|
+
return;
|
|
1727
|
+
} catch {
|
|
1728
|
+
this.#degrade();
|
|
1729
|
+
}
|
|
1682
1730
|
}
|
|
1731
|
+
this.#fallback.set(session);
|
|
1683
1732
|
}
|
|
1684
1733
|
clear() {
|
|
1685
|
-
if (
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
this.#fallback.clear();
|
|
1734
|
+
if (this.#available) {
|
|
1735
|
+
try {
|
|
1736
|
+
globalThis.localStorage.removeItem(this.#key);
|
|
1737
|
+
return;
|
|
1738
|
+
} catch {
|
|
1739
|
+
this.#degrade();
|
|
1740
|
+
}
|
|
1693
1741
|
}
|
|
1742
|
+
this.#fallback.clear();
|
|
1743
|
+
}
|
|
1744
|
+
/** Переводит хранилище в память без переноса прежнего значения. */
|
|
1745
|
+
#degrade() {
|
|
1746
|
+
this.#available = false;
|
|
1747
|
+
this.#fallback.clear();
|
|
1694
1748
|
}
|
|
1695
1749
|
};
|
|
1696
1750
|
function createTokenStorage(handlers) {
|
|
@@ -1745,10 +1799,17 @@ function normalizeBaseUrl(baseUrl) {
|
|
|
1745
1799
|
return parsed.origin + (parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, ""));
|
|
1746
1800
|
}
|
|
1747
1801
|
|
|
1802
|
+
// src/core/version.ts
|
|
1803
|
+
var LIBRARY_VERSION = "0.0.9";
|
|
1804
|
+
|
|
1748
1805
|
// src/core/config.ts
|
|
1749
1806
|
var DEFAULT_BASE_URL = "https://xn--d1ah4a.com";
|
|
1807
|
+
var DEFAULT_STATUS_BASE_URL = "https://xn--80a7abcbg.xn--d1ah4a.com";
|
|
1808
|
+
var STATUS_SERVICE = "status";
|
|
1809
|
+
var BUILT_IN_SERVICES = Object.freeze([
|
|
1810
|
+
Object.freeze({ name: STATUS_SERVICE, baseUrl: DEFAULT_STATUS_BASE_URL, auth: false })
|
|
1811
|
+
]);
|
|
1750
1812
|
var DEFAULT_TIMEOUT = 3e4;
|
|
1751
|
-
var LIBRARY_VERSION = "0.0.7";
|
|
1752
1813
|
var DEFAULT_USER_AGENT = `Mozilla/5.0 (compatible; itd-api/${LIBRARY_VERSION}; +https://github.com/KiowDev/itd-api)`;
|
|
1753
1814
|
var DEFAULT_RATE_LIMIT_DELAYS = Object.freeze([1e3, 5e3, 3e4, 6e4, 9e4]);
|
|
1754
1815
|
function requirePositive(value, name) {
|
|
@@ -1860,6 +1921,12 @@ function validateAuth(auth) {
|
|
|
1860
1921
|
"auth \u043D\u0435 \u0440\u0430\u0441\u043F\u043E\u0437\u043D\u0430\u043D. \u041E\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044F \u0441\u0442\u0440\u043E\u043A\u0430 \u0441 accessToken \u043B\u0438\u0431\u043E \u043E\u0431\u044A\u0435\u043A\u0442 { accessToken }, { email, password } \u0438\u043B\u0438 { getToken }"
|
|
1861
1922
|
);
|
|
1862
1923
|
}
|
|
1924
|
+
function resolveServices(services) {
|
|
1925
|
+
if (!services) return [];
|
|
1926
|
+
return Object.entries(services).map(
|
|
1927
|
+
([name, value]) => typeof value === "string" ? { name, baseUrl: value } : { ...value, name }
|
|
1928
|
+
);
|
|
1929
|
+
}
|
|
1863
1930
|
function resolveConfig(options = {}) {
|
|
1864
1931
|
const mode = options.mode ?? RuntimeMode.Auto;
|
|
1865
1932
|
if (!Object.values(RuntimeMode).includes(mode)) {
|
|
@@ -1873,6 +1940,7 @@ function resolveConfig(options = {}) {
|
|
|
1873
1940
|
}
|
|
1874
1941
|
return {
|
|
1875
1942
|
baseUrl: normalizeBaseUrl(options.baseUrl ?? DEFAULT_BASE_URL),
|
|
1943
|
+
services: resolveServices(options.services),
|
|
1876
1944
|
auth: validateAuth(options.auth),
|
|
1877
1945
|
storage: options.storage ?? new MemoryTokenStorage(),
|
|
1878
1946
|
autoRefresh: options.autoRefresh ?? true,
|
|
@@ -1893,176 +1961,629 @@ function resolveConfig(options = {}) {
|
|
|
1893
1961
|
};
|
|
1894
1962
|
}
|
|
1895
1963
|
|
|
1896
|
-
// src/core/
|
|
1897
|
-
var
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
"flowtoken",
|
|
1906
|
-
"token",
|
|
1907
|
-
"turnstiletoken",
|
|
1908
|
-
"otp"
|
|
1909
|
-
]);
|
|
1910
|
-
function maskSecret(value) {
|
|
1911
|
-
if (value.length <= 8) return "\u2026";
|
|
1912
|
-
return `${value.slice(0, 4)}\u2026(${value.length})\u2026${value.slice(-3)}`;
|
|
1913
|
-
}
|
|
1914
|
-
function redactHeaders(headers) {
|
|
1915
|
-
const result = {};
|
|
1916
|
-
headers.forEach((value, name) => {
|
|
1917
|
-
if (SECRET_HEADERS.has(name.toLowerCase())) {
|
|
1918
|
-
const spaceAt = value.indexOf(" ");
|
|
1919
|
-
result[name] = spaceAt > 0 ? `${value.slice(0, spaceAt)} ${maskSecret(value.slice(spaceAt + 1))}` : maskSecret(value);
|
|
1920
|
-
return;
|
|
1921
|
-
}
|
|
1922
|
-
result[name] = value;
|
|
1923
|
-
});
|
|
1924
|
-
return result;
|
|
1925
|
-
}
|
|
1926
|
-
function redactBody(body) {
|
|
1927
|
-
if (body === null || body === void 0) return body;
|
|
1928
|
-
if (typeof FormData !== "undefined" && body instanceof FormData) return "[FormData]";
|
|
1929
|
-
if (isBlob(body)) return "[Blob]";
|
|
1930
|
-
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return "[binary]";
|
|
1931
|
-
if (Array.isArray(body)) return body.map(redactBody);
|
|
1932
|
-
if (typeof body === "object") {
|
|
1933
|
-
const result = {};
|
|
1934
|
-
for (const [key, value] of Object.entries(body)) {
|
|
1935
|
-
result[key] = SECRET_FIELDS.has(key.toLowerCase()) ? "[\u0441\u043A\u0440\u044B\u0442\u043E]" : redactBody(value);
|
|
1936
|
-
}
|
|
1937
|
-
return result;
|
|
1964
|
+
// src/core/http.ts
|
|
1965
|
+
var HttpClient = class {
|
|
1966
|
+
#handler;
|
|
1967
|
+
#plugins;
|
|
1968
|
+
#baseUrl;
|
|
1969
|
+
constructor(deps) {
|
|
1970
|
+
this.#handler = deps.handler;
|
|
1971
|
+
this.#plugins = deps.plugins;
|
|
1972
|
+
this.#baseUrl = deps.baseUrl;
|
|
1938
1973
|
}
|
|
1939
|
-
|
|
1974
|
+
/** Базовый URL, к которому обращается клиент. */
|
|
1975
|
+
get baseUrl() {
|
|
1976
|
+
return this.#baseUrl;
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Имена опций запроса, заявленные плагинами.
|
|
1980
|
+
*
|
|
1981
|
+
* Читается ресурсами: они переносят в транспорт только известные поля, а чужие,
|
|
1982
|
+
* если их никто не заявил, отсеивают.
|
|
1983
|
+
*/
|
|
1984
|
+
get pluginOptionKeys() {
|
|
1985
|
+
return this.#plugins.optionKeys;
|
|
1986
|
+
}
|
|
1987
|
+
/**
|
|
1988
|
+
* Выполняет запрос к API через собранный конвейер.
|
|
1989
|
+
*
|
|
1990
|
+
* @typeParam T ожидаемая форма ответа после снятия обёртки `{ data: … }`
|
|
1991
|
+
* @throws {ItdApiError} если сервер ответил статусом ≥ 400
|
|
1992
|
+
* @throws {ItdTimeoutError} если истёк таймаут
|
|
1993
|
+
* @throws {ItdAbortError} если запрос отменён через `signal`
|
|
1994
|
+
* @throws {ItdNetworkError} если запрос не дошёл до сервера
|
|
1995
|
+
*/
|
|
1996
|
+
request(options) {
|
|
1997
|
+
return this.#handler(options);
|
|
1998
|
+
}
|
|
1999
|
+
};
|
|
2000
|
+
|
|
2001
|
+
// src/core/pipeline.ts
|
|
2002
|
+
function composePipeline(middlewares, final) {
|
|
2003
|
+
return middlewares.reduceRight(
|
|
2004
|
+
(next, middleware) => (request) => middleware(request, next),
|
|
2005
|
+
final
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
2008
|
+
function withLayerHeaders(request, headers) {
|
|
2009
|
+
return { ...request, layerHeaders: { ...request.layerHeaders, ...headers } };
|
|
1940
2010
|
}
|
|
1941
2011
|
|
|
1942
|
-
// src/core/
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
2012
|
+
// src/core/retry.ts
|
|
2013
|
+
var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
2014
|
+
function isRetryable(error, method, retryWrites) {
|
|
2015
|
+
if (error instanceof ItdAbortError) return false;
|
|
2016
|
+
const safeToRepeat = retryWrites || IDEMPOTENT_METHODS.has(method);
|
|
2017
|
+
if (error instanceof ItdApiError) {
|
|
2018
|
+
if (error.status === 429) return true;
|
|
2019
|
+
if (error.status >= 500) return safeToRepeat;
|
|
2020
|
+
return false;
|
|
2021
|
+
}
|
|
2022
|
+
if (error instanceof ItdNetworkError || error instanceof ItdTimeoutError) return safeToRepeat;
|
|
2023
|
+
return false;
|
|
1948
2024
|
}
|
|
1949
|
-
function
|
|
1950
|
-
|
|
2025
|
+
function backoffDelay(attempt, options, random) {
|
|
2026
|
+
const exponential = options.baseDelay * 2 ** (attempt - 1);
|
|
2027
|
+
const capped = Math.min(exponential, options.maxDelay);
|
|
2028
|
+
const spread = capped * options.jitter * (random() * 2 - 1);
|
|
2029
|
+
return Math.max(0, Math.round(capped + spread));
|
|
1951
2030
|
}
|
|
1952
|
-
function
|
|
1953
|
-
return
|
|
2031
|
+
function createRetryScheduler(options, random = Math.random) {
|
|
2032
|
+
return (error, attempt, method) => {
|
|
2033
|
+
if (attempt >= options.attempts) return void 0;
|
|
2034
|
+
if (options.shouldRetry) {
|
|
2035
|
+
return options.shouldRetry(error, attempt) ? backoffDelay(attempt, options, random) : void 0;
|
|
2036
|
+
}
|
|
2037
|
+
if (!isRetryable(error, method, options.retryWrites)) return void 0;
|
|
2038
|
+
if (error instanceof ItdApiError && error.retryAfter !== void 0) {
|
|
2039
|
+
return error.retryAfter > options.maxDelay ? void 0 : error.retryAfter;
|
|
2040
|
+
}
|
|
2041
|
+
return backoffDelay(attempt, options, random);
|
|
2042
|
+
};
|
|
1954
2043
|
}
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
return
|
|
2044
|
+
|
|
2045
|
+
// src/core/middleware.ts
|
|
2046
|
+
function sleep(ms) {
|
|
2047
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1959
2048
|
}
|
|
1960
|
-
function
|
|
1961
|
-
|
|
1962
|
-
const value = source[field];
|
|
1963
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
1964
|
-
return value;
|
|
2049
|
+
function createQueueMiddleware(schedule) {
|
|
2050
|
+
return (request, next) => request.skipQueue ? next(request) : schedule(request, () => next(request));
|
|
1965
2051
|
}
|
|
1966
|
-
function
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
2052
|
+
function createPluginsMiddleware(plugins) {
|
|
2053
|
+
return (request, next) => {
|
|
2054
|
+
if (plugins.size === 0) return next(request);
|
|
2055
|
+
return plugins.run(request, next);
|
|
2056
|
+
};
|
|
1970
2057
|
}
|
|
1971
|
-
function
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
2058
|
+
function createServicesMiddleware(registry) {
|
|
2059
|
+
return async (request, next) => {
|
|
2060
|
+
if (request.service === void 0) return next(request);
|
|
2061
|
+
const service = registry.require(request.service);
|
|
2062
|
+
let prepared = request.baseUrl === void 0 ? { ...request, baseUrl: service.baseUrl } : request;
|
|
2063
|
+
if (service.headers) prepared = withLayerHeaders(prepared, service.headers);
|
|
2064
|
+
if (service.auth === false && prepared.skipAuth === void 0) {
|
|
2065
|
+
prepared = { ...prepared, skipAuth: true };
|
|
2066
|
+
}
|
|
2067
|
+
return next(prepared);
|
|
2068
|
+
};
|
|
1975
2069
|
}
|
|
1976
|
-
function
|
|
1977
|
-
if (
|
|
1978
|
-
const
|
|
1979
|
-
return
|
|
2070
|
+
async function applyAuth(request, deps) {
|
|
2071
|
+
if (request.skipAuth) return request;
|
|
2072
|
+
const headers = await deps.getAuthHeaders();
|
|
2073
|
+
return Object.keys(headers).length > 0 ? withLayerHeaders(request, headers) : request;
|
|
1980
2074
|
}
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
const messages = value.filter((item) => typeof item === "string");
|
|
1990
|
-
if (messages.length > 0) result[field] = messages;
|
|
1991
|
-
} else if (typeof value === "string") {
|
|
1992
|
-
result[field] = [value];
|
|
2075
|
+
function createAuthMiddleware(deps) {
|
|
2076
|
+
return async (request, next) => {
|
|
2077
|
+
const authorized = await applyAuth(request, deps);
|
|
2078
|
+
try {
|
|
2079
|
+
return await next(authorized);
|
|
2080
|
+
} catch (error) {
|
|
2081
|
+
if (request.skipAuthRefresh || !deps.autoRefresh || !isItdApiError(error) || error.status !== 401) {
|
|
2082
|
+
throw error;
|
|
1993
2083
|
}
|
|
2084
|
+
const refreshed = await deps.onUnauthorized();
|
|
2085
|
+
if (!refreshed) throw error;
|
|
2086
|
+
const retried = await applyAuth({ ...request, skipAuthRefresh: true }, deps);
|
|
2087
|
+
return next(retried);
|
|
1994
2088
|
}
|
|
1995
|
-
}
|
|
1996
|
-
const violations = source.violations;
|
|
1997
|
-
if (Array.isArray(violations)) {
|
|
1998
|
-
for (const violation of violations) {
|
|
1999
|
-
if (!isRecord(violation)) continue;
|
|
2000
|
-
const field = asString(violation.field) ?? asString(violation.property);
|
|
2001
|
-
const message = asString(violation.message);
|
|
2002
|
-
if (!field || !message) continue;
|
|
2003
|
-
const existing = result[field];
|
|
2004
|
-
if (existing) existing.push(message);
|
|
2005
|
-
else result[field] = [message];
|
|
2006
|
-
}
|
|
2007
|
-
}
|
|
2008
|
-
return result;
|
|
2009
|
-
}
|
|
2010
|
-
function parseErrorBody(body, status, statusText = "") {
|
|
2011
|
-
const fallbackMessage = statusText ? `HTTP ${status} ${statusText}` : `HTTP ${status}`;
|
|
2012
|
-
if (typeof body === "string") {
|
|
2013
|
-
return {
|
|
2014
|
-
code: "UNKNOWN_ERROR",
|
|
2015
|
-
message: asString(body.trim()) ?? fallbackMessage,
|
|
2016
|
-
detail: void 0,
|
|
2017
|
-
title: void 0,
|
|
2018
|
-
fieldErrors: {},
|
|
2019
|
-
userId: void 0
|
|
2020
|
-
};
|
|
2021
|
-
}
|
|
2022
|
-
if (!isRecord(body)) {
|
|
2023
|
-
return {
|
|
2024
|
-
code: "UNKNOWN_ERROR",
|
|
2025
|
-
message: fallbackMessage,
|
|
2026
|
-
detail: void 0,
|
|
2027
|
-
title: void 0,
|
|
2028
|
-
fieldErrors: {},
|
|
2029
|
-
userId: void 0
|
|
2030
|
-
};
|
|
2031
|
-
}
|
|
2032
|
-
if (body.type === "validation") {
|
|
2033
|
-
const target = asString(body.on);
|
|
2034
|
-
return {
|
|
2035
|
-
code: "VALIDATION_ERROR",
|
|
2036
|
-
message: target ? `\u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u043D\u0435 \u043F\u0440\u043E\u0439\u0434\u0435\u043D\u0430: \u043D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0435 \u0434\u0430\u043D\u043D\u044B\u0435 \u0432 \xAB${target}\xBB` : "\u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u0432\u0445\u043E\u0434\u043D\u044B\u0445 \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435 \u043F\u0440\u043E\u0439\u0434\u0435\u043D\u0430",
|
|
2037
|
-
detail: void 0,
|
|
2038
|
-
title: void 0,
|
|
2039
|
-
fieldErrors: {},
|
|
2040
|
-
userId: void 0
|
|
2041
|
-
};
|
|
2042
|
-
}
|
|
2043
|
-
const inner = isRecord(body.error) ? body.error : body;
|
|
2044
|
-
const message = asString(inner.message) ?? asString(inner.detail) ?? asString(inner.title) ?? // `{ "error": "Invalid token" }` — так отвечает сервер на недействительный токен.
|
|
2045
|
-
asString(body.error) ?? fallbackMessage;
|
|
2046
|
-
return {
|
|
2047
|
-
code: asString(inner.code) ?? asString(body.code) ?? "UNKNOWN_ERROR",
|
|
2048
|
-
message,
|
|
2049
|
-
detail: asString(inner.detail),
|
|
2050
|
-
title: asString(inner.title),
|
|
2051
|
-
fieldErrors: { ...collectFieldErrors(body), ...collectFieldErrors(inner) },
|
|
2052
|
-
userId: asString(inner.userId) ?? asString(body.userId)
|
|
2053
2089
|
};
|
|
2054
2090
|
}
|
|
2055
|
-
function
|
|
2056
|
-
if (
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
if (Number.isFinite(date)) return Math.max(0, date - now);
|
|
2061
|
-
return void 0;
|
|
2091
|
+
function resolveBackoff(retry, global) {
|
|
2092
|
+
if (retry === void 0) return global;
|
|
2093
|
+
if (retry === false) return void 0;
|
|
2094
|
+
const resolved = resolveRetry(retry);
|
|
2095
|
+
return resolved ? createRetryScheduler(resolved) : void 0;
|
|
2062
2096
|
}
|
|
2063
|
-
function
|
|
2064
|
-
const
|
|
2065
|
-
|
|
2097
|
+
function createRetryMiddleware(deps) {
|
|
2098
|
+
const globalScheduler = deps.retry ? createRetryScheduler(deps.retry) : void 0;
|
|
2099
|
+
const nextDelay = (error, attempt, request, method, backoff) => {
|
|
2100
|
+
if (isItdRateLimitError(error)) {
|
|
2101
|
+
const wait = error.retryAfter ?? deps.rateLimitDelays[attempt - 1];
|
|
2102
|
+
if (wait === void 0) return void 0;
|
|
2103
|
+
deps.pauseQueue?.(wait, request);
|
|
2104
|
+
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`);
|
|
2105
|
+
return wait;
|
|
2106
|
+
}
|
|
2107
|
+
return backoff?.(error, attempt, method);
|
|
2108
|
+
};
|
|
2109
|
+
return async (request, next) => {
|
|
2110
|
+
const method = request.method.toUpperCase();
|
|
2111
|
+
const backoff = resolveBackoff(request.retry, globalScheduler);
|
|
2112
|
+
for (let attempt = 1; ; attempt++) {
|
|
2113
|
+
try {
|
|
2114
|
+
return await next({ ...request, attempt });
|
|
2115
|
+
} catch (error) {
|
|
2116
|
+
const delay = nextDelay(error, attempt, request, method, backoff);
|
|
2117
|
+
if (delay === void 0) throw error;
|
|
2118
|
+
await deps.hooks.onRetry?.({
|
|
2119
|
+
method,
|
|
2120
|
+
path: request.path,
|
|
2121
|
+
url: deps.buildUrl(request),
|
|
2122
|
+
// Умолчания транспорта добавляются после слоя повторов и сюда не входят.
|
|
2123
|
+
headers: new Headers({ ...request.layerHeaders, ...request.headers }),
|
|
2124
|
+
attempt,
|
|
2125
|
+
error,
|
|
2126
|
+
delay
|
|
2127
|
+
});
|
|
2128
|
+
deps.logger?.debug(
|
|
2129
|
+
`\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`
|
|
2130
|
+
);
|
|
2131
|
+
await sleep(delay);
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
};
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
// src/core/plugins.ts
|
|
2138
|
+
var NO_KEYS = /* @__PURE__ */ new Set();
|
|
2139
|
+
var RESERVED_OPTION_KEYS = /* @__PURE__ */ new Set([
|
|
2140
|
+
"signal",
|
|
2141
|
+
"timeout",
|
|
2142
|
+
"headers",
|
|
2143
|
+
"retry",
|
|
2144
|
+
"method",
|
|
2145
|
+
"path",
|
|
2146
|
+
"service",
|
|
2147
|
+
"baseUrl",
|
|
2148
|
+
"query",
|
|
2149
|
+
"body",
|
|
2150
|
+
"skipAuth",
|
|
2151
|
+
"skipAuthRefresh",
|
|
2152
|
+
"skipQueue",
|
|
2153
|
+
"raw"
|
|
2154
|
+
]);
|
|
2155
|
+
var PluginRegistry = class {
|
|
2156
|
+
#transformers = [];
|
|
2157
|
+
#optionKeys = /* @__PURE__ */ new Set();
|
|
2158
|
+
#names = /* @__PURE__ */ new Set();
|
|
2159
|
+
/** Сколько обёрток подключено. Ноль означает, что запрос идёт прежним путём. */
|
|
2160
|
+
get size() {
|
|
2161
|
+
return this.#transformers.length;
|
|
2162
|
+
}
|
|
2163
|
+
/** Имена опций запроса, заявленные плагинами. */
|
|
2164
|
+
get optionKeys() {
|
|
2165
|
+
return this.#optionKeys.size === 0 ? NO_KEYS : this.#optionKeys;
|
|
2166
|
+
}
|
|
2167
|
+
/**
|
|
2168
|
+
* Подключает плагин.
|
|
2169
|
+
*
|
|
2170
|
+
* @throws {ItdConfigError} если плагин задан неверно, уже подключён или заявил занятое
|
|
2171
|
+
* имя опции
|
|
2172
|
+
*/
|
|
2173
|
+
add(plugin, context) {
|
|
2174
|
+
if (typeof plugin?.install !== "function") {
|
|
2175
|
+
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()");
|
|
2176
|
+
}
|
|
2177
|
+
const name = plugin.name;
|
|
2178
|
+
if (typeof name !== "string" || name.trim() === "") {
|
|
2179
|
+
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");
|
|
2180
|
+
}
|
|
2181
|
+
if (this.#names.has(name)) {
|
|
2182
|
+
throw new ItdConfigError(`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u0443\u0436\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0451\u043D`);
|
|
2183
|
+
}
|
|
2184
|
+
const keys = plugin.optionKeys ?? [];
|
|
2185
|
+
for (const key of keys) {
|
|
2186
|
+
if (typeof key !== "string" || key.trim() === "") {
|
|
2187
|
+
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`);
|
|
2188
|
+
}
|
|
2189
|
+
if (RESERVED_OPTION_KEYS.has(key)) {
|
|
2190
|
+
throw new ItdConfigError(
|
|
2191
|
+
`\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(", ")}`
|
|
2192
|
+
);
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
const before = this.#transformers.length;
|
|
2196
|
+
try {
|
|
2197
|
+
plugin.install({
|
|
2198
|
+
...context,
|
|
2199
|
+
use: (transformer) => {
|
|
2200
|
+
if (typeof transformer !== "function") {
|
|
2201
|
+
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`);
|
|
2202
|
+
}
|
|
2203
|
+
this.#transformers.push(transformer);
|
|
2204
|
+
}
|
|
2205
|
+
});
|
|
2206
|
+
} catch (error) {
|
|
2207
|
+
this.#transformers.length = before;
|
|
2208
|
+
throw error;
|
|
2209
|
+
}
|
|
2210
|
+
this.#names.add(name);
|
|
2211
|
+
for (const key of keys) this.#optionKeys.add(key);
|
|
2212
|
+
}
|
|
2213
|
+
/**
|
|
2214
|
+
* Прогоняет запрос через цепочку обёрток.
|
|
2215
|
+
*
|
|
2216
|
+
* Цепочка собирается на каждый запрос заново: плагин можно подключить в любой момент,
|
|
2217
|
+
* а обёрток единицы — экономить тут не на чем.
|
|
2218
|
+
*
|
|
2219
|
+
* @param execute настоящий запрос, вызывается самой внутренней обёрткой
|
|
2220
|
+
*/
|
|
2221
|
+
run(request, execute) {
|
|
2222
|
+
const chain = this.#transformers.reduceRight(
|
|
2223
|
+
(next, transformer) => (current) => transformer(current, next),
|
|
2224
|
+
execute
|
|
2225
|
+
);
|
|
2226
|
+
return chain(request);
|
|
2227
|
+
}
|
|
2228
|
+
};
|
|
2229
|
+
|
|
2230
|
+
// src/core/rate-limit.ts
|
|
2231
|
+
var RequestQueue = class {
|
|
2232
|
+
#concurrency;
|
|
2233
|
+
/** Минимальный промежуток между стартами, мс. `0` — без ограничения частоты. */
|
|
2234
|
+
#minGap;
|
|
2235
|
+
#waiting = [];
|
|
2236
|
+
#active = 0;
|
|
2237
|
+
/** Момент, раньше которого следующий запрос стартовать не должен. */
|
|
2238
|
+
#nextSlot = 0;
|
|
2239
|
+
#timer;
|
|
2240
|
+
constructor(options) {
|
|
2241
|
+
this.#concurrency = options.concurrency;
|
|
2242
|
+
this.#minGap = options.rps ? 1e3 / options.rps : 0;
|
|
2243
|
+
}
|
|
2244
|
+
/** Сколько задач выполняется прямо сейчас. */
|
|
2245
|
+
get active() {
|
|
2246
|
+
return this.#active;
|
|
2247
|
+
}
|
|
2248
|
+
/** Сколько задач ждёт очереди. */
|
|
2249
|
+
get pending() {
|
|
2250
|
+
return this.#waiting.length;
|
|
2251
|
+
}
|
|
2252
|
+
/**
|
|
2253
|
+
* Ставит задачу в очередь.
|
|
2254
|
+
*
|
|
2255
|
+
* @returns результат задачи; ошибка задачи пробрасывается без изменений
|
|
2256
|
+
*/
|
|
2257
|
+
schedule(task) {
|
|
2258
|
+
return new Promise((resolve, reject) => {
|
|
2259
|
+
const run = () => {
|
|
2260
|
+
this.#active += 1;
|
|
2261
|
+
task().then(resolve, reject).finally(() => {
|
|
2262
|
+
this.#active -= 1;
|
|
2263
|
+
this.#drain();
|
|
2264
|
+
});
|
|
2265
|
+
};
|
|
2266
|
+
this.#waiting.push({ run, cancel: reject });
|
|
2267
|
+
this.#drain();
|
|
2268
|
+
});
|
|
2269
|
+
}
|
|
2270
|
+
/**
|
|
2271
|
+
* Останавливает очередь: снимает отложенную паузу и отклоняет ещё не начатые задачи
|
|
2272
|
+
* ошибкой `ItdAbortError`. Уже выполняющиеся задачи доводятся до конца.
|
|
2273
|
+
*/
|
|
2274
|
+
stop() {
|
|
2275
|
+
if (this.#timer !== void 0) {
|
|
2276
|
+
clearTimeout(this.#timer);
|
|
2277
|
+
this.#timer = void 0;
|
|
2278
|
+
}
|
|
2279
|
+
this.#nextSlot = 0;
|
|
2280
|
+
const pending = this.#waiting.splice(0, this.#waiting.length);
|
|
2281
|
+
for (const task of pending) {
|
|
2282
|
+
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"));
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
/**
|
|
2286
|
+
* Придерживает всю очередь на заданное время.
|
|
2287
|
+
*
|
|
2288
|
+
* Вызывается при получении `429` с заголовком `Retry-After`: тормозить нужно все запросы,
|
|
2289
|
+
* а не только тот, который наткнулся на лимит, — иначе остальные продолжат добивать API.
|
|
2290
|
+
*/
|
|
2291
|
+
pause(ms) {
|
|
2292
|
+
if (ms <= 0) return;
|
|
2293
|
+
this.#nextSlot = Math.max(this.#nextSlot, Date.now() + ms);
|
|
2294
|
+
}
|
|
2295
|
+
/** Запускает столько ожидающих задач, сколько позволяют ограничения. */
|
|
2296
|
+
#drain() {
|
|
2297
|
+
if (this.#waiting.length === 0) return;
|
|
2298
|
+
if (this.#active >= this.#concurrency) return;
|
|
2299
|
+
if (this.#timer !== void 0) return;
|
|
2300
|
+
const now = Date.now();
|
|
2301
|
+
if (this.#nextSlot > now) {
|
|
2302
|
+
this.#timer = setTimeout(() => {
|
|
2303
|
+
this.#timer = void 0;
|
|
2304
|
+
this.#drain();
|
|
2305
|
+
}, this.#nextSlot - now);
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
const next = this.#waiting.shift();
|
|
2309
|
+
if (!next) return;
|
|
2310
|
+
if (this.#minGap > 0) this.#nextSlot = now + this.#minGap;
|
|
2311
|
+
next.run();
|
|
2312
|
+
this.#drain();
|
|
2313
|
+
}
|
|
2314
|
+
};
|
|
2315
|
+
var RequestQueuePool = class {
|
|
2316
|
+
#options;
|
|
2317
|
+
#main;
|
|
2318
|
+
/** Очереди сервисов заводятся при первом запросе — обычно не нужна ни одна. */
|
|
2319
|
+
#byService = /* @__PURE__ */ new Map();
|
|
2320
|
+
constructor(options) {
|
|
2321
|
+
this.#options = options;
|
|
2322
|
+
this.#main = new RequestQueue(options);
|
|
2323
|
+
}
|
|
2324
|
+
/** Очередь хоста. */
|
|
2325
|
+
for(service) {
|
|
2326
|
+
if (service === void 0) return this.#main;
|
|
2327
|
+
let queue = this.#byService.get(service);
|
|
2328
|
+
if (!queue) {
|
|
2329
|
+
queue = new RequestQueue(this.#options);
|
|
2330
|
+
this.#byService.set(service, queue);
|
|
2331
|
+
}
|
|
2332
|
+
return queue;
|
|
2333
|
+
}
|
|
2334
|
+
/** Останавливает все очереди. */
|
|
2335
|
+
stop() {
|
|
2336
|
+
this.#main.stop();
|
|
2337
|
+
for (const queue of this.#byService.values()) queue.stop();
|
|
2338
|
+
}
|
|
2339
|
+
};
|
|
2340
|
+
|
|
2341
|
+
// src/core/services.ts
|
|
2342
|
+
function hostOf(url) {
|
|
2343
|
+
try {
|
|
2344
|
+
return new URL(url).hostname.toLowerCase();
|
|
2345
|
+
} catch {
|
|
2346
|
+
return "";
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
function isSameSite(primaryHost, host) {
|
|
2350
|
+
if (!primaryHost || !host) return false;
|
|
2351
|
+
return host === primaryHost || host.endsWith(`.${primaryHost}`);
|
|
2352
|
+
}
|
|
2353
|
+
var ServiceRegistry = class {
|
|
2354
|
+
#services = /* @__PURE__ */ new Map();
|
|
2355
|
+
/** Хост основного API. */
|
|
2356
|
+
#primaryHost;
|
|
2357
|
+
/** @param primaryBaseUrl базовый URL клиента */
|
|
2358
|
+
constructor(primaryBaseUrl) {
|
|
2359
|
+
this.#primaryHost = primaryBaseUrl ? hostOf(primaryBaseUrl) : "";
|
|
2360
|
+
}
|
|
2361
|
+
/**
|
|
2362
|
+
* Регистрирует сервис. Имя очищается от краевых пробелов, базовый URL приводится
|
|
2363
|
+
* к каноничному виду, а незаданный `auth` выводится из хоста.
|
|
2364
|
+
*
|
|
2365
|
+
* @throws {ItdConfigError} если имя пустое, имя занято или `baseUrl` не абсолютный URL
|
|
2366
|
+
*/
|
|
2367
|
+
define(definition) {
|
|
2368
|
+
const raw = definition?.name;
|
|
2369
|
+
if (typeof raw !== "string" || raw.trim() === "") {
|
|
2370
|
+
throw new ItdConfigError("\u0423 \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0443\u0441\u0442\u043E\u0435 \u0438\u043C\u044F");
|
|
2371
|
+
}
|
|
2372
|
+
const name = raw.trim();
|
|
2373
|
+
if (this.#services.has(name)) {
|
|
2374
|
+
throw new ItdConfigError(`\u0421\u0435\u0440\u0432\u0438\u0441 \xAB${name}\xBB \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u043D \u0438 \u043D\u0435 \u043C\u043E\u0436\u0435\u0442 \u0431\u044B\u0442\u044C \u0437\u0430\u043C\u0435\u043D\u0451\u043D`);
|
|
2375
|
+
}
|
|
2376
|
+
const baseUrl = normalizeBaseUrl(definition.baseUrl);
|
|
2377
|
+
this.#services.set(name, {
|
|
2378
|
+
...definition,
|
|
2379
|
+
name,
|
|
2380
|
+
baseUrl,
|
|
2381
|
+
auth: definition.auth ?? isSameSite(this.#primaryHost, hostOf(baseUrl))
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
/** Определение сервиса либо `undefined`, если такого нет. */
|
|
2385
|
+
get(name) {
|
|
2386
|
+
return this.#services.get(name);
|
|
2387
|
+
}
|
|
2388
|
+
/** Зарегистрирован ли сервис с таким именем. */
|
|
2389
|
+
has(name) {
|
|
2390
|
+
return this.#services.has(name);
|
|
2391
|
+
}
|
|
2392
|
+
/**
|
|
2393
|
+
* Определение сервиса.
|
|
2394
|
+
*
|
|
2395
|
+
* @throws {ItdConfigError} если сервис не зарегистрирован
|
|
2396
|
+
*/
|
|
2397
|
+
require(name) {
|
|
2398
|
+
const service = this.#services.get(name);
|
|
2399
|
+
if (!service) {
|
|
2400
|
+
const known = [...this.#services.keys()];
|
|
2401
|
+
throw new ItdConfigError(
|
|
2402
|
+
`\u0421\u0435\u0440\u0432\u0438\u0441 \xAB${name}\xBB \u043D\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u043D. ` + (known.length > 0 ? `\u0418\u0437\u0432\u0435\u0441\u0442\u043D\u044B: ${known.join(", ")}` : "\u0417\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0439\u0442\u0435 \u0435\u0433\u043E \u0447\u0435\u0440\u0435\u0437 itd.defineService() \u0438\u043B\u0438 \u043E\u043F\u0446\u0438\u044E services")
|
|
2403
|
+
);
|
|
2404
|
+
}
|
|
2405
|
+
return service;
|
|
2406
|
+
}
|
|
2407
|
+
/**
|
|
2408
|
+
* Базовый URL сервиса.
|
|
2409
|
+
*
|
|
2410
|
+
* @throws {ItdConfigError} если сервис не зарегистрирован
|
|
2411
|
+
*/
|
|
2412
|
+
resolveBaseUrl(name) {
|
|
2413
|
+
return this.require(name).baseUrl;
|
|
2414
|
+
}
|
|
2415
|
+
};
|
|
2416
|
+
|
|
2417
|
+
// src/core/redact.ts
|
|
2418
|
+
var SECRET_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "set-cookie", "x-api-key"]);
|
|
2419
|
+
var SECRET_FIELDS = /* @__PURE__ */ new Set([
|
|
2420
|
+
"password",
|
|
2421
|
+
"oldpassword",
|
|
2422
|
+
"newpassword",
|
|
2423
|
+
"accesstoken",
|
|
2424
|
+
"refreshtoken",
|
|
2425
|
+
"currentpassword",
|
|
2426
|
+
"flowtoken",
|
|
2427
|
+
"token",
|
|
2428
|
+
"turnstiletoken",
|
|
2429
|
+
"otp"
|
|
2430
|
+
]);
|
|
2431
|
+
function maskSecret(value) {
|
|
2432
|
+
if (value.length <= 8) return "\u2026";
|
|
2433
|
+
return `${value.slice(0, 4)}\u2026(${value.length})\u2026${value.slice(-3)}`;
|
|
2434
|
+
}
|
|
2435
|
+
function redactHeaders(headers) {
|
|
2436
|
+
const result = {};
|
|
2437
|
+
headers.forEach((value, name) => {
|
|
2438
|
+
if (SECRET_HEADERS.has(name.toLowerCase())) {
|
|
2439
|
+
const spaceAt = value.indexOf(" ");
|
|
2440
|
+
result[name] = spaceAt > 0 ? `${value.slice(0, spaceAt)} ${maskSecret(value.slice(spaceAt + 1))}` : maskSecret(value);
|
|
2441
|
+
return;
|
|
2442
|
+
}
|
|
2443
|
+
result[name] = value;
|
|
2444
|
+
});
|
|
2445
|
+
return result;
|
|
2446
|
+
}
|
|
2447
|
+
function redactBody(body) {
|
|
2448
|
+
if (body === null || body === void 0) return body;
|
|
2449
|
+
if (typeof FormData !== "undefined" && body instanceof FormData) return "[FormData]";
|
|
2450
|
+
if (isBlob(body)) return "[Blob]";
|
|
2451
|
+
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return "[binary]";
|
|
2452
|
+
if (Array.isArray(body)) return body.map(redactBody);
|
|
2453
|
+
if (typeof body === "object") {
|
|
2454
|
+
const result = {};
|
|
2455
|
+
for (const [key, value] of Object.entries(body)) {
|
|
2456
|
+
result[key] = SECRET_FIELDS.has(key.toLowerCase()) ? "[\u0441\u043A\u0440\u044B\u0442\u043E]" : redactBody(value);
|
|
2457
|
+
}
|
|
2458
|
+
return result;
|
|
2459
|
+
}
|
|
2460
|
+
return body;
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
// src/core/unwrap.ts
|
|
2464
|
+
function unwrapData(body) {
|
|
2465
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) return body;
|
|
2466
|
+
const keys = Object.keys(body);
|
|
2467
|
+
if (keys.length !== 1 || keys[0] !== "data") return body;
|
|
2468
|
+
return body.data;
|
|
2469
|
+
}
|
|
2470
|
+
function isRecord(value) {
|
|
2471
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2472
|
+
}
|
|
2473
|
+
function asString(value) {
|
|
2474
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2475
|
+
}
|
|
2476
|
+
function pickArray(source, field) {
|
|
2477
|
+
if (typeof source !== "object" || source === null) return [];
|
|
2478
|
+
const value = source[field];
|
|
2479
|
+
return Array.isArray(value) ? value : [];
|
|
2480
|
+
}
|
|
2481
|
+
function pickObject(source, field) {
|
|
2482
|
+
if (typeof source !== "object" || source === null) return void 0;
|
|
2483
|
+
const value = source[field];
|
|
2484
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
2485
|
+
return value;
|
|
2486
|
+
}
|
|
2487
|
+
function pickBoolean(source, field, fallback = false) {
|
|
2488
|
+
if (typeof source !== "object" || source === null) return fallback;
|
|
2489
|
+
const value = source[field];
|
|
2490
|
+
return typeof value === "boolean" ? value : fallback;
|
|
2491
|
+
}
|
|
2492
|
+
function pickNumber(source, field, fallback) {
|
|
2493
|
+
if (typeof source !== "object" || source === null) return fallback;
|
|
2494
|
+
const value = source[field];
|
|
2495
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
2496
|
+
}
|
|
2497
|
+
function pickString(source, field) {
|
|
2498
|
+
if (typeof source !== "object" || source === null) return void 0;
|
|
2499
|
+
const value = source[field];
|
|
2500
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
// src/core/error-factory.ts
|
|
2504
|
+
function collectFieldErrors(source) {
|
|
2505
|
+
const result = {};
|
|
2506
|
+
const errors = source.errors;
|
|
2507
|
+
if (isRecord(errors)) {
|
|
2508
|
+
for (const [field, value] of Object.entries(errors)) {
|
|
2509
|
+
if (Array.isArray(value)) {
|
|
2510
|
+
const messages = value.filter((item) => typeof item === "string");
|
|
2511
|
+
if (messages.length > 0) result[field] = messages;
|
|
2512
|
+
} else if (typeof value === "string") {
|
|
2513
|
+
result[field] = [value];
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
const violations = source.violations;
|
|
2518
|
+
if (Array.isArray(violations)) {
|
|
2519
|
+
for (const violation of violations) {
|
|
2520
|
+
if (!isRecord(violation)) continue;
|
|
2521
|
+
const field = asString(violation.field) ?? asString(violation.property);
|
|
2522
|
+
const message = asString(violation.message);
|
|
2523
|
+
if (!field || !message) continue;
|
|
2524
|
+
const existing = result[field];
|
|
2525
|
+
if (existing) existing.push(message);
|
|
2526
|
+
else result[field] = [message];
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
return result;
|
|
2530
|
+
}
|
|
2531
|
+
function parseErrorBody(body, status, statusText = "") {
|
|
2532
|
+
const fallbackMessage = statusText ? `HTTP ${status} ${statusText}` : `HTTP ${status}`;
|
|
2533
|
+
if (typeof body === "string") {
|
|
2534
|
+
return {
|
|
2535
|
+
code: "UNKNOWN_ERROR",
|
|
2536
|
+
message: asString(body.trim()) ?? fallbackMessage,
|
|
2537
|
+
detail: void 0,
|
|
2538
|
+
title: void 0,
|
|
2539
|
+
fieldErrors: {},
|
|
2540
|
+
userId: void 0
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
if (!isRecord(body)) {
|
|
2544
|
+
return {
|
|
2545
|
+
code: "UNKNOWN_ERROR",
|
|
2546
|
+
message: fallbackMessage,
|
|
2547
|
+
detail: void 0,
|
|
2548
|
+
title: void 0,
|
|
2549
|
+
fieldErrors: {},
|
|
2550
|
+
userId: void 0
|
|
2551
|
+
};
|
|
2552
|
+
}
|
|
2553
|
+
if (body.type === "validation") {
|
|
2554
|
+
const target = asString(body.on);
|
|
2555
|
+
return {
|
|
2556
|
+
code: "VALIDATION_ERROR",
|
|
2557
|
+
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",
|
|
2558
|
+
detail: void 0,
|
|
2559
|
+
title: void 0,
|
|
2560
|
+
fieldErrors: {},
|
|
2561
|
+
userId: void 0
|
|
2562
|
+
};
|
|
2563
|
+
}
|
|
2564
|
+
const inner = isRecord(body.error) ? body.error : body;
|
|
2565
|
+
const message = asString(inner.message) ?? asString(inner.detail) ?? asString(inner.title) ?? // `{ "error": "Invalid token" }` — так отвечает сервер на недействительный токен.
|
|
2566
|
+
asString(body.error) ?? fallbackMessage;
|
|
2567
|
+
return {
|
|
2568
|
+
code: asString(inner.code) ?? asString(body.code) ?? "UNKNOWN_ERROR",
|
|
2569
|
+
message,
|
|
2570
|
+
detail: asString(inner.detail),
|
|
2571
|
+
title: asString(inner.title),
|
|
2572
|
+
fieldErrors: { ...collectFieldErrors(body), ...collectFieldErrors(inner) },
|
|
2573
|
+
userId: asString(inner.userId) ?? asString(body.userId)
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
function parseRetryAfter(header, now = Date.now()) {
|
|
2577
|
+
if (!header) return void 0;
|
|
2578
|
+
const seconds = Number(header);
|
|
2579
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
|
|
2580
|
+
const date = Date.parse(header);
|
|
2581
|
+
if (Number.isFinite(date)) return Math.max(0, date - now);
|
|
2582
|
+
return void 0;
|
|
2583
|
+
}
|
|
2584
|
+
function readIntHeader(headers, name) {
|
|
2585
|
+
const raw = headers?.get(name);
|
|
2586
|
+
if (raw === null || raw === void 0) return void 0;
|
|
2066
2587
|
const value = Number.parseInt(raw, 10);
|
|
2067
2588
|
return Number.isFinite(value) ? value : void 0;
|
|
2068
2589
|
}
|
|
@@ -2139,11 +2660,7 @@ function createApiError(context) {
|
|
|
2139
2660
|
return new Ctor(init);
|
|
2140
2661
|
}
|
|
2141
2662
|
|
|
2142
|
-
// src/core/
|
|
2143
|
-
function sleep(ms) {
|
|
2144
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2145
|
-
}
|
|
2146
|
-
var EMPTY_KEYS = /* @__PURE__ */ new Set();
|
|
2663
|
+
// src/core/transport.ts
|
|
2147
2664
|
function setHeader(headers, name, value) {
|
|
2148
2665
|
try {
|
|
2149
2666
|
headers.set(name, value);
|
|
@@ -2173,6 +2690,29 @@ async function readBody(response) {
|
|
|
2173
2690
|
const text = await response.text();
|
|
2174
2691
|
return text === "" ? void 0 : text;
|
|
2175
2692
|
}
|
|
2693
|
+
function createAbortError() {
|
|
2694
|
+
const error = new Error("\u041E\u043F\u0435\u0440\u0430\u0446\u0438\u044F \u043F\u0440\u0435\u0440\u0432\u0430\u043D\u0430");
|
|
2695
|
+
error.name = "AbortError";
|
|
2696
|
+
return error;
|
|
2697
|
+
}
|
|
2698
|
+
function abortable(promise, signal) {
|
|
2699
|
+
if (signal.aborted) {
|
|
2700
|
+
return Promise.race([
|
|
2701
|
+
promise,
|
|
2702
|
+
new Promise((_resolve, reject) => {
|
|
2703
|
+
setTimeout(() => reject(createAbortError()), 0);
|
|
2704
|
+
})
|
|
2705
|
+
]);
|
|
2706
|
+
}
|
|
2707
|
+
let onAbort;
|
|
2708
|
+
const interrupted = new Promise((_resolve, reject) => {
|
|
2709
|
+
onAbort = () => reject(createAbortError());
|
|
2710
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2711
|
+
});
|
|
2712
|
+
return Promise.race([promise, interrupted]).finally(() => {
|
|
2713
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
2714
|
+
});
|
|
2715
|
+
}
|
|
2176
2716
|
function createAbortBundle(userSignal, timeout) {
|
|
2177
2717
|
const controller = new AbortController();
|
|
2178
2718
|
let timedOut = false;
|
|
@@ -2181,418 +2721,201 @@ function createAbortBundle(userSignal, timeout) {
|
|
|
2181
2721
|
if (userSignal.aborted) controller.abort(userSignal.reason);
|
|
2182
2722
|
else userSignal.addEventListener("abort", onUserAbort, { once: true });
|
|
2183
2723
|
}
|
|
2184
|
-
const timer = timeout > 0 ? setTimeout(() => {
|
|
2185
|
-
timedOut = true;
|
|
2186
|
-
controller.abort();
|
|
2187
|
-
}, timeout) : void 0;
|
|
2188
|
-
return {
|
|
2189
|
-
signal: controller.signal,
|
|
2190
|
-
timedOut: () => timedOut,
|
|
2191
|
-
cleanup: () => {
|
|
2192
|
-
if (timer !== void 0) clearTimeout(timer);
|
|
2193
|
-
userSignal?.removeEventListener("abort", onUserAbort);
|
|
2194
|
-
}
|
|
2195
|
-
};
|
|
2196
|
-
}
|
|
2197
|
-
var
|
|
2198
|
-
#config;
|
|
2199
|
-
#
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
this.#
|
|
2203
|
-
this.#collaborators = collaborators;
|
|
2204
|
-
}
|
|
2205
|
-
/** Базовый URL, к которому обращается клиент. */
|
|
2206
|
-
get baseUrl() {
|
|
2207
|
-
return this.#config.baseUrl;
|
|
2208
|
-
}
|
|
2209
|
-
/**
|
|
2210
|
-
* Имена опций запроса, заявленные плагинами.
|
|
2211
|
-
*
|
|
2212
|
-
* Читается ресурсами: они переносят в транспорт только известные поля, а чужие,
|
|
2213
|
-
* если их никто не заявил, отсеивают.
|
|
2214
|
-
*/
|
|
2215
|
-
get pluginOptionKeys() {
|
|
2216
|
-
return this.#plugins?.optionKeys ?? EMPTY_KEYS;
|
|
2217
|
-
}
|
|
2218
|
-
/** Подключает список плагинов. Реестр общий с клиентом и пополняется через `itd.use()`. */
|
|
2219
|
-
usePlugins(plugins) {
|
|
2220
|
-
this.#plugins = plugins;
|
|
2221
|
-
}
|
|
2222
|
-
/**
|
|
2223
|
-
* Подключает недостающие части конвейера.
|
|
2224
|
-
*
|
|
2225
|
-
* Нужно из-за кольцевой зависимости: слой авторизации сам выполняет запросы, поэтому
|
|
2226
|
-
* не может быть передан в конструктор до создания транспорта.
|
|
2227
|
-
*/
|
|
2228
|
-
setCollaborators(collaborators) {
|
|
2229
|
-
this.#collaborators = { ...this.#collaborators, ...collaborators };
|
|
2230
|
-
}
|
|
2231
|
-
/**
|
|
2232
|
-
* Выполняет запрос к API.
|
|
2233
|
-
*
|
|
2234
|
-
* @typeParam T ожидаемая форма ответа после снятия обёртки `{ data: … }`
|
|
2235
|
-
* @throws {ItdApiError} если сервер ответил статусом ≥ 400
|
|
2236
|
-
* @throws {ItdTimeoutError} если истёк таймаут
|
|
2237
|
-
* @throws {ItdAbortError} если запрос отменён через `signal`
|
|
2238
|
-
* @throws {ItdNetworkError} если запрос не дошёл до сервера
|
|
2239
|
-
*/
|
|
2240
|
-
async request(options) {
|
|
2241
|
-
const task = () => this.#withPlugins(options);
|
|
2242
|
-
if (!this.#collaborators.schedule || options.skipQueue) return task();
|
|
2243
|
-
return this.#collaborators.schedule(task);
|
|
2244
|
-
}
|
|
2245
|
-
/**
|
|
2246
|
-
* Прогоняет запрос через обёртки плагинов.
|
|
2247
|
-
*
|
|
2248
|
-
* Цепочка стоит **снаружи повторов и внутри очереди**: плагин должен увидеть запрос
|
|
2249
|
-
* и ответ по одному разу, независимо от того, сколько попыток понадобилось, — иначе,
|
|
2250
|
-
* например, текст поста зашифруется повторно на второй попытке.
|
|
2251
|
-
*/
|
|
2252
|
-
#withPlugins(options) {
|
|
2253
|
-
const plugins = this.#plugins;
|
|
2254
|
-
if (!plugins || plugins.size === 0) return this.#withRetries(options);
|
|
2255
|
-
return plugins.run(options, (request) => this.#withRetries(request));
|
|
2256
|
-
}
|
|
2257
|
-
async #withRetries(options) {
|
|
2258
|
-
const method = options.method.toUpperCase();
|
|
2259
|
-
for (let attempt = 1; ; attempt++) {
|
|
2260
|
-
try {
|
|
2261
|
-
return await this.#attempt(options, attempt);
|
|
2262
|
-
} catch (error) {
|
|
2263
|
-
const delay = this.#collaborators.nextRetryDelay?.(error, attempt, method);
|
|
2264
|
-
if (delay === void 0) throw error;
|
|
2265
|
-
await this.#config.hooks.onRetry?.({
|
|
2266
|
-
method,
|
|
2267
|
-
path: options.path,
|
|
2268
|
-
url: this.#buildUrl(options),
|
|
2269
|
-
headers: new Headers(),
|
|
2270
|
-
attempt,
|
|
2271
|
-
error,
|
|
2272
|
-
delay
|
|
2273
|
-
});
|
|
2274
|
-
this.#config.logger?.debug(
|
|
2275
|
-
`\u043F\u043E\u0432\u0442\u043E\u0440 ${method} ${options.path}, \u043F\u043E\u043F\u044B\u0442\u043A\u0430 ${attempt + 1} \u0447\u0435\u0440\u0435\u0437 ${delay} \u043C\u0441`
|
|
2276
|
-
);
|
|
2277
|
-
await sleep(delay);
|
|
2278
|
-
}
|
|
2279
|
-
}
|
|
2280
|
-
}
|
|
2281
|
-
#buildUrl(options) {
|
|
2282
|
-
return joinUrl(this.#config.baseUrl, options.path) + buildQuery(options.query);
|
|
2283
|
-
}
|
|
2284
|
-
async #buildHeaders(options, url) {
|
|
2285
|
-
const headers = new Headers();
|
|
2286
|
-
headers.set("Accept", "application/json");
|
|
2287
|
-
headers.set("X-Requested-With", "XMLHttpRequest");
|
|
2288
|
-
if (this.#config.userAgent) setHeader(headers, "User-Agent", this.#config.userAgent);
|
|
2289
|
-
if (this.#collaborators.getDeviceId) {
|
|
2290
|
-
setHeader(headers, "X-Device-Id", await this.#collaborators.getDeviceId());
|
|
2291
|
-
}
|
|
2292
|
-
for (const [name, value] of Object.entries(this.#config.headers))
|
|
2293
|
-
setHeader(headers, name, value);
|
|
2294
|
-
if (!options.skipAuth && this.#collaborators.getAuthHeaders) {
|
|
2295
|
-
const auth = await this.#collaborators.getAuthHeaders();
|
|
2296
|
-
for (const [name, value] of Object.entries(auth)) setHeader(headers, name, value);
|
|
2297
|
-
}
|
|
2298
|
-
if (this.#config.useCookieJar && this.#collaborators.getCookieHeader) {
|
|
2299
|
-
const cookie = this.#collaborators.getCookieHeader(url);
|
|
2300
|
-
if (cookie) setHeader(headers, "Cookie", cookie);
|
|
2301
|
-
}
|
|
2302
|
-
for (const [name, value] of Object.entries(options.headers ?? {})) {
|
|
2303
|
-
setHeader(headers, name, value);
|
|
2304
|
-
}
|
|
2305
|
-
return headers;
|
|
2306
|
-
}
|
|
2307
|
-
async #attempt(options, attempt) {
|
|
2308
|
-
const method = options.method.toUpperCase();
|
|
2309
|
-
const url = this.#buildUrl(options);
|
|
2310
|
-
const headers = await this.#buildHeaders(options, url);
|
|
2311
|
-
let body;
|
|
2312
|
-
if (options.body !== void 0 && options.body !== null) {
|
|
2313
|
-
if (isRawBody(options.body)) {
|
|
2314
|
-
body = options.body;
|
|
2315
|
-
} else {
|
|
2316
|
-
body = JSON.stringify(options.body);
|
|
2317
|
-
if (!headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
2318
|
-
}
|
|
2319
|
-
}
|
|
2320
|
-
const context = { method, path: options.path, url, headers, attempt };
|
|
2321
|
-
await this.#config.hooks.onRequest?.(context);
|
|
2322
|
-
const timeout = options.timeout ?? this.#config.timeout;
|
|
2323
|
-
const abort = createAbortBundle(options.signal, timeout);
|
|
2324
|
-
const startedAt = Date.now();
|
|
2325
|
-
this.#config.logger?.debug(`\u2192 ${method} ${options.path}`, {
|
|
2326
|
-
headers: redactHeaders(headers),
|
|
2327
|
-
body: redactBody(options.body)
|
|
2328
|
-
});
|
|
2329
|
-
let response;
|
|
2330
|
-
try {
|
|
2331
|
-
response = await this.#config.fetch(url, {
|
|
2332
|
-
method,
|
|
2333
|
-
headers,
|
|
2334
|
-
signal: abort.signal,
|
|
2335
|
-
...body !== void 0 ? { body } : {},
|
|
2336
|
-
...this.#config.sendCredentials ? { credentials: "include" } : {}
|
|
2337
|
-
});
|
|
2338
|
-
} catch (error) {
|
|
2339
|
-
const duration2 = Date.now() - startedAt;
|
|
2340
|
-
const failure = this.#toTransportError(error, abort, options, method, timeout);
|
|
2341
|
-
await this.#config.hooks.onError?.({ ...context, duration: duration2, error: failure });
|
|
2342
|
-
this.#config.logger?.warn(`\xD7 ${method} ${options.path} (${duration2} \u043C\u0441): ${failure.message}`);
|
|
2343
|
-
throw failure;
|
|
2344
|
-
} finally {
|
|
2345
|
-
abort.cleanup();
|
|
2346
|
-
}
|
|
2347
|
-
const duration = Date.now() - startedAt;
|
|
2348
|
-
if (this.#collaborators.onRateLimit) {
|
|
2349
|
-
const { limit, remaining } = readRateLimit(response.headers);
|
|
2350
|
-
this.#collaborators.onRateLimit(limit, remaining);
|
|
2351
|
-
}
|
|
2352
|
-
if (this.#config.useCookieJar) this.#collaborators.saveCookies?.(url, response);
|
|
2353
|
-
const payload = await readBody(response);
|
|
2354
|
-
if (!response.ok) {
|
|
2355
|
-
if (response.status === 401 && !options.skipAuthRefresh && this.#config.autoRefresh && this.#collaborators.onUnauthorized) {
|
|
2356
|
-
const refreshed = await this.#collaborators.onUnauthorized();
|
|
2357
|
-
if (refreshed) {
|
|
2358
|
-
this.#config.logger?.debug(`\u0442\u043E\u043A\u0435\u043D \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D, \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u044E ${method} ${options.path}`);
|
|
2359
|
-
return this.#attempt({ ...options, skipAuthRefresh: true }, attempt);
|
|
2360
|
-
}
|
|
2361
|
-
}
|
|
2362
|
-
const error = createApiError({
|
|
2363
|
-
method,
|
|
2364
|
-
path: options.path,
|
|
2365
|
-
status: response.status,
|
|
2366
|
-
statusText: response.statusText,
|
|
2367
|
-
headers: response.headers,
|
|
2368
|
-
response,
|
|
2369
|
-
body: payload
|
|
2370
|
-
});
|
|
2371
|
-
await this.#config.hooks.onError?.({ ...context, duration, error });
|
|
2372
|
-
this.#config.logger?.warn(
|
|
2373
|
-
`\u2190 ${response.status} ${method} ${options.path} (${duration} \u043C\u0441): ${error.message}`
|
|
2374
|
-
);
|
|
2375
|
-
throw error;
|
|
2376
|
-
}
|
|
2377
|
-
await this.#config.hooks.onResponse?.({
|
|
2378
|
-
...context,
|
|
2379
|
-
status: response.status,
|
|
2380
|
-
duration,
|
|
2381
|
-
response
|
|
2382
|
-
});
|
|
2383
|
-
this.#config.logger?.debug(`\u2190 ${response.status} ${method} ${options.path} (${duration} \u043C\u0441)`);
|
|
2384
|
-
return options.raw ? payload : unwrapData(payload);
|
|
2385
|
-
}
|
|
2386
|
-
/** Превращает исключение `fetch` в понятную ошибку библиотеки. */
|
|
2387
|
-
#toTransportError(error, abort, options, method, timeout) {
|
|
2388
|
-
const aborted = error instanceof Error && error.name === "AbortError";
|
|
2389
|
-
if (aborted && abort.timedOut()) {
|
|
2390
|
-
return new ItdTimeoutError({ timeout, method, path: options.path });
|
|
2391
|
-
}
|
|
2392
|
-
if (aborted) {
|
|
2393
|
-
return new ItdAbortError(`\u0417\u0430\u043F\u0440\u043E\u0441 ${method} ${options.path} \u043E\u0442\u043C\u0435\u043D\u0451\u043D`);
|
|
2394
|
-
}
|
|
2395
|
-
return new ItdNetworkError(
|
|
2396
|
-
`\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u044C ${method} ${options.path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
2397
|
-
{ method, path: options.path, cause: error }
|
|
2398
|
-
);
|
|
2399
|
-
}
|
|
2400
|
-
};
|
|
2401
|
-
|
|
2402
|
-
// src/core/plugins.ts
|
|
2403
|
-
var NO_KEYS = /* @__PURE__ */ new Set();
|
|
2404
|
-
var RESERVED_OPTION_KEYS = /* @__PURE__ */ new Set([
|
|
2405
|
-
"signal",
|
|
2406
|
-
"timeout",
|
|
2407
|
-
"headers",
|
|
2408
|
-
"retry",
|
|
2409
|
-
"method",
|
|
2410
|
-
"path",
|
|
2411
|
-
"query",
|
|
2412
|
-
"body",
|
|
2413
|
-
"skipAuth",
|
|
2414
|
-
"skipAuthRefresh",
|
|
2415
|
-
"skipQueue",
|
|
2416
|
-
"raw"
|
|
2417
|
-
]);
|
|
2418
|
-
var PluginRegistry = class {
|
|
2419
|
-
#transformers = [];
|
|
2420
|
-
#optionKeys = /* @__PURE__ */ new Set();
|
|
2421
|
-
#names = /* @__PURE__ */ new Set();
|
|
2422
|
-
/** Сколько обёрток подключено. Ноль означает, что запрос идёт прежним путём. */
|
|
2423
|
-
get size() {
|
|
2424
|
-
return this.#transformers.length;
|
|
2724
|
+
const timer = timeout > 0 ? setTimeout(() => {
|
|
2725
|
+
timedOut = true;
|
|
2726
|
+
controller.abort();
|
|
2727
|
+
}, timeout) : void 0;
|
|
2728
|
+
return {
|
|
2729
|
+
signal: controller.signal,
|
|
2730
|
+
timedOut: () => timedOut,
|
|
2731
|
+
cleanup: () => {
|
|
2732
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
2733
|
+
userSignal?.removeEventListener("abort", onUserAbort);
|
|
2734
|
+
}
|
|
2735
|
+
};
|
|
2736
|
+
}
|
|
2737
|
+
var Transport = class {
|
|
2738
|
+
#config;
|
|
2739
|
+
#deps;
|
|
2740
|
+
constructor(config, deps) {
|
|
2741
|
+
this.#config = config;
|
|
2742
|
+
this.#deps = deps;
|
|
2425
2743
|
}
|
|
2426
|
-
/**
|
|
2427
|
-
get
|
|
2428
|
-
return this.#
|
|
2744
|
+
/** Базовый URL, к которому обращается транспорт. */
|
|
2745
|
+
get baseUrl() {
|
|
2746
|
+
return this.#config.baseUrl;
|
|
2429
2747
|
}
|
|
2430
2748
|
/**
|
|
2431
|
-
*
|
|
2749
|
+
* Выполняет один сетевой запрос.
|
|
2432
2750
|
*
|
|
2433
|
-
* @throws {
|
|
2434
|
-
*
|
|
2751
|
+
* @throws {ItdApiError} если сервер ответил статусом ≥ 400
|
|
2752
|
+
* @throws {ItdTimeoutError} если истёк таймаут
|
|
2753
|
+
* @throws {ItdAbortError} если запрос отменён через `signal`
|
|
2754
|
+
* @throws {ItdNetworkError} если запрос не дошёл до сервера
|
|
2435
2755
|
*/
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
const
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2756
|
+
send = async (request) => {
|
|
2757
|
+
const method = request.method.toUpperCase();
|
|
2758
|
+
const url = this.buildUrl(request);
|
|
2759
|
+
const headers = await this.#buildHeaders(request, url);
|
|
2760
|
+
const attempt = request.attempt ?? 1;
|
|
2761
|
+
let body;
|
|
2762
|
+
if (request.body !== void 0 && request.body !== null) {
|
|
2763
|
+
if (isRawBody(request.body)) {
|
|
2764
|
+
body = request.body;
|
|
2765
|
+
} else {
|
|
2766
|
+
body = JSON.stringify(request.body);
|
|
2767
|
+
if (!headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
2768
|
+
}
|
|
2446
2769
|
}
|
|
2447
|
-
const
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2770
|
+
const context = { method, path: request.path, url, headers, attempt };
|
|
2771
|
+
await this.#config.hooks.onRequest?.(context);
|
|
2772
|
+
const timeout = request.timeout ?? this.#config.timeout;
|
|
2773
|
+
const abort = createAbortBundle(request.signal, timeout);
|
|
2774
|
+
const startedAt = Date.now();
|
|
2775
|
+
this.#config.logger?.debug(`\u2192 ${method} ${request.path}`, {
|
|
2776
|
+
headers: redactHeaders(headers),
|
|
2777
|
+
body: redactBody(request.body)
|
|
2778
|
+
});
|
|
2779
|
+
try {
|
|
2780
|
+
let response;
|
|
2781
|
+
try {
|
|
2782
|
+
response = await this.#config.fetch(url, {
|
|
2783
|
+
method,
|
|
2784
|
+
headers,
|
|
2785
|
+
signal: abort.signal,
|
|
2786
|
+
...body !== void 0 ? { body } : {},
|
|
2787
|
+
...this.#config.sendCredentials ? { credentials: "include" } : {}
|
|
2788
|
+
});
|
|
2789
|
+
} catch (error) {
|
|
2790
|
+
const duration2 = Date.now() - startedAt;
|
|
2791
|
+
const failure = this.#toTransportError(error, abort, request, method, timeout);
|
|
2792
|
+
await this.#config.hooks.onError?.({ ...context, duration: duration2, error: failure });
|
|
2793
|
+
this.#config.logger?.warn(
|
|
2794
|
+
`\xD7 ${method} ${request.path} (${duration2} \u043C\u0441): ${failure.message}`
|
|
2795
|
+
);
|
|
2796
|
+
throw failure;
|
|
2451
2797
|
}
|
|
2452
|
-
if (
|
|
2453
|
-
|
|
2454
|
-
|
|
2798
|
+
if (this.#deps.onRateLimit) {
|
|
2799
|
+
const { limit, remaining } = readRateLimit(response.headers);
|
|
2800
|
+
this.#deps.onRateLimit(limit, remaining, request);
|
|
2801
|
+
}
|
|
2802
|
+
if (this.#config.useCookieJar) this.#deps.cookies?.setFromResponse(url, response);
|
|
2803
|
+
if (response.ok) {
|
|
2804
|
+
await this.#config.hooks.onResponse?.({
|
|
2805
|
+
...context,
|
|
2806
|
+
status: response.status,
|
|
2807
|
+
duration: Date.now() - startedAt,
|
|
2808
|
+
response
|
|
2809
|
+
});
|
|
2810
|
+
}
|
|
2811
|
+
const payload = await this.#readBodyOrFail(
|
|
2812
|
+
response,
|
|
2813
|
+
context,
|
|
2814
|
+
request,
|
|
2815
|
+
method,
|
|
2816
|
+
abort,
|
|
2817
|
+
timeout
|
|
2818
|
+
);
|
|
2819
|
+
const duration = Date.now() - startedAt;
|
|
2820
|
+
if (!response.ok) {
|
|
2821
|
+
const error = createApiError({
|
|
2822
|
+
method,
|
|
2823
|
+
path: request.path,
|
|
2824
|
+
status: response.status,
|
|
2825
|
+
statusText: response.statusText,
|
|
2826
|
+
headers: response.headers,
|
|
2827
|
+
response,
|
|
2828
|
+
body: payload
|
|
2829
|
+
});
|
|
2830
|
+
await this.#config.hooks.onError?.({ ...context, duration, error });
|
|
2831
|
+
this.#config.logger?.warn(
|
|
2832
|
+
`\u2190 ${response.status} ${method} ${request.path} (${duration} \u043C\u0441): ${error.message}`
|
|
2455
2833
|
);
|
|
2834
|
+
throw error;
|
|
2456
2835
|
}
|
|
2836
|
+
this.#config.logger?.debug(`\u2190 ${response.status} ${method} ${request.path} (${duration} \u043C\u0441)`);
|
|
2837
|
+
return request.raw ? payload : unwrapData(payload);
|
|
2838
|
+
} finally {
|
|
2839
|
+
abort.cleanup();
|
|
2457
2840
|
}
|
|
2458
|
-
|
|
2841
|
+
};
|
|
2842
|
+
/** Читает тело и преобразует ошибку чтения в транспортную ошибку библиотеки. */
|
|
2843
|
+
async #readBodyOrFail(response, context, request, method, abort, timeout) {
|
|
2844
|
+
const startedAt = Date.now();
|
|
2459
2845
|
try {
|
|
2460
|
-
|
|
2846
|
+
return await abortable(readBody(response), abort.signal);
|
|
2847
|
+
} catch (error) {
|
|
2848
|
+
await response.body?.cancel().catch(() => {
|
|
2849
|
+
});
|
|
2850
|
+
const failure = this.#toTransportError(error, abort, request, method, timeout);
|
|
2851
|
+
await this.#config.hooks.onError?.({
|
|
2461
2852
|
...context,
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
throw new ItdConfigError(`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u043F\u0435\u0440\u0435\u0434\u0430\u043B \u0432 use() \u043D\u0435 \u0444\u0443\u043D\u043A\u0446\u0438\u044E`);
|
|
2465
|
-
}
|
|
2466
|
-
this.#transformers.push(transformer);
|
|
2467
|
-
}
|
|
2853
|
+
duration: Date.now() - startedAt,
|
|
2854
|
+
error: failure
|
|
2468
2855
|
});
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2856
|
+
this.#config.logger?.warn(
|
|
2857
|
+
`\xD7 ${method} ${request.path}: \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C \u0442\u0435\u043B\u043E \u043E\u0442\u0432\u0435\u0442\u0430 \u2014 ${failure.message}`
|
|
2858
|
+
);
|
|
2859
|
+
throw failure;
|
|
2472
2860
|
}
|
|
2473
|
-
this.#names.add(name);
|
|
2474
|
-
for (const key of keys) this.#optionKeys.add(key);
|
|
2475
2861
|
}
|
|
2476
2862
|
/**
|
|
2477
|
-
*
|
|
2863
|
+
* Итоговый URL со строкой запроса. Нужен и слою повторов — для хука `onRetry`.
|
|
2478
2864
|
*
|
|
2479
|
-
*
|
|
2480
|
-
* а обёрток единицы — экономить тут не на чем.
|
|
2481
|
-
*
|
|
2482
|
-
* @param execute настоящий запрос, вызывается самой внутренней обёрткой
|
|
2865
|
+
* Хост берётся из самого запроса, если он там задан: у сервисов платформы свои домены.
|
|
2483
2866
|
*/
|
|
2484
|
-
|
|
2485
|
-
const
|
|
2486
|
-
|
|
2487
|
-
execute
|
|
2488
|
-
);
|
|
2489
|
-
return chain(request);
|
|
2490
|
-
}
|
|
2491
|
-
};
|
|
2492
|
-
|
|
2493
|
-
// src/core/rate-limit.ts
|
|
2494
|
-
var RequestQueue = class {
|
|
2495
|
-
#concurrency;
|
|
2496
|
-
/** Минимальный промежуток между стартами, мс. `0` — без ограничения частоты. */
|
|
2497
|
-
#minGap;
|
|
2498
|
-
#waiting = [];
|
|
2499
|
-
#active = 0;
|
|
2500
|
-
/** Момент, раньше которого следующий запрос стартовать не должен. */
|
|
2501
|
-
#nextSlot = 0;
|
|
2502
|
-
#timer;
|
|
2503
|
-
constructor(options) {
|
|
2504
|
-
this.#concurrency = options.concurrency;
|
|
2505
|
-
this.#minGap = options.rps ? 1e3 / options.rps : 0;
|
|
2506
|
-
}
|
|
2507
|
-
/** Сколько задач выполняется прямо сейчас. */
|
|
2508
|
-
get active() {
|
|
2509
|
-
return this.#active;
|
|
2510
|
-
}
|
|
2511
|
-
/** Сколько задач ждёт очереди. */
|
|
2512
|
-
get pending() {
|
|
2513
|
-
return this.#waiting.length;
|
|
2867
|
+
buildUrl(request) {
|
|
2868
|
+
const base = request.baseUrl ?? this.#config.baseUrl;
|
|
2869
|
+
return joinUrl(base, request.path) + buildQuery(request.query);
|
|
2514
2870
|
}
|
|
2515
2871
|
/**
|
|
2516
|
-
*
|
|
2517
|
-
*
|
|
2518
|
-
* @returns результат задачи; ошибка задачи пробрасывается без изменений
|
|
2872
|
+
* Собирает общие заголовки клиента: `User-Agent`, идентификатор устройства,
|
|
2873
|
+
* заголовки конфигурации и cookie для указанного адреса.
|
|
2519
2874
|
*/
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
this.#
|
|
2531
|
-
|
|
2875
|
+
async platformHeaders(url) {
|
|
2876
|
+
const headers = new Headers();
|
|
2877
|
+
headers.set("X-Requested-With", "XMLHttpRequest");
|
|
2878
|
+
if (this.#config.userAgent) setHeader(headers, "User-Agent", this.#config.userAgent);
|
|
2879
|
+
if (this.#deps.getDeviceId) {
|
|
2880
|
+
setHeader(headers, "X-Device-Id", await this.#deps.getDeviceId());
|
|
2881
|
+
}
|
|
2882
|
+
for (const [name, value] of Object.entries(this.#config.headers))
|
|
2883
|
+
setHeader(headers, name, value);
|
|
2884
|
+
if (this.#config.useCookieJar && this.#deps.cookies) {
|
|
2885
|
+
const cookie = this.#deps.cookies.getHeader(url);
|
|
2886
|
+
if (cookie) setHeader(headers, "Cookie", cookie);
|
|
2887
|
+
}
|
|
2888
|
+
return headers;
|
|
2532
2889
|
}
|
|
2533
2890
|
/**
|
|
2534
|
-
*
|
|
2535
|
-
*
|
|
2536
|
-
* Вызывается при получении `429` с заголовком `Retry-After`: тормозить нужно все запросы,
|
|
2537
|
-
* а не только тот, который наткнулся на лимит, — иначе остальные продолжат добивать API.
|
|
2891
|
+
* Дополняет общие заголовки значением `Accept`, заголовками конвейера и вызова.
|
|
2892
|
+
* Заголовки вызова применяются последними.
|
|
2538
2893
|
*/
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
if (this.#active >= this.#concurrency) return;
|
|
2547
|
-
if (this.#timer !== void 0) return;
|
|
2548
|
-
const now = Date.now();
|
|
2549
|
-
if (this.#nextSlot > now) {
|
|
2550
|
-
this.#timer = setTimeout(() => {
|
|
2551
|
-
this.#timer = void 0;
|
|
2552
|
-
this.#drain();
|
|
2553
|
-
}, this.#nextSlot - now);
|
|
2554
|
-
return;
|
|
2894
|
+
async #buildHeaders(request, url) {
|
|
2895
|
+
const headers = await this.platformHeaders(url);
|
|
2896
|
+
if (!headers.has("Accept")) headers.set("Accept", "application/json");
|
|
2897
|
+
for (const [name, value] of Object.entries(request.layerHeaders ?? {}))
|
|
2898
|
+
setHeader(headers, name, value);
|
|
2899
|
+
for (const [name, value] of Object.entries(request.headers ?? {})) {
|
|
2900
|
+
setHeader(headers, name, value);
|
|
2555
2901
|
}
|
|
2556
|
-
|
|
2557
|
-
if (!next) return;
|
|
2558
|
-
if (this.#minGap > 0) this.#nextSlot = now + this.#minGap;
|
|
2559
|
-
next.run();
|
|
2560
|
-
this.#drain();
|
|
2561
|
-
}
|
|
2562
|
-
};
|
|
2563
|
-
|
|
2564
|
-
// src/core/retry.ts
|
|
2565
|
-
var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
2566
|
-
function isRetryable(error, method, retryWrites) {
|
|
2567
|
-
if (error instanceof ItdAbortError) return false;
|
|
2568
|
-
const safeToRepeat = retryWrites || IDEMPOTENT_METHODS.has(method);
|
|
2569
|
-
if (error instanceof ItdApiError) {
|
|
2570
|
-
if (error.status === 429) return true;
|
|
2571
|
-
if (error.status >= 500) return safeToRepeat;
|
|
2572
|
-
return false;
|
|
2902
|
+
return headers;
|
|
2573
2903
|
}
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
const capped = Math.min(exponential, options.maxDelay);
|
|
2580
|
-
const spread = capped * options.jitter * (random() * 2 - 1);
|
|
2581
|
-
return Math.max(0, Math.round(capped + spread));
|
|
2582
|
-
}
|
|
2583
|
-
function createRetryScheduler(options, random = Math.random) {
|
|
2584
|
-
return (error, attempt, method) => {
|
|
2585
|
-
if (attempt >= options.attempts) return void 0;
|
|
2586
|
-
if (options.shouldRetry) {
|
|
2587
|
-
return options.shouldRetry(error, attempt) ? backoffDelay(attempt, options, random) : void 0;
|
|
2904
|
+
/** Превращает исключение `fetch` в понятную ошибку библиотеки. */
|
|
2905
|
+
#toTransportError(error, abort, request, method, timeout) {
|
|
2906
|
+
const aborted = error instanceof Error && error.name === "AbortError";
|
|
2907
|
+
if (aborted && abort.timedOut()) {
|
|
2908
|
+
return new ItdTimeoutError({ timeout, method, path: request.path });
|
|
2588
2909
|
}
|
|
2589
|
-
if (
|
|
2590
|
-
|
|
2591
|
-
return error.retryAfter > options.maxDelay ? void 0 : error.retryAfter;
|
|
2910
|
+
if (aborted) {
|
|
2911
|
+
return new ItdAbortError(`\u0417\u0430\u043F\u0440\u043E\u0441 ${method} ${request.path} \u043E\u0442\u043C\u0435\u043D\u0451\u043D`);
|
|
2592
2912
|
}
|
|
2593
|
-
return
|
|
2594
|
-
|
|
2595
|
-
}
|
|
2913
|
+
return new ItdNetworkError(
|
|
2914
|
+
`\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)}`,
|
|
2915
|
+
{ method, path: request.path, cause: error }
|
|
2916
|
+
);
|
|
2917
|
+
}
|
|
2918
|
+
};
|
|
2596
2919
|
|
|
2597
2920
|
// src/notifications/type-map.ts
|
|
2598
2921
|
var NOTIFICATION_TYPE_ALIASES = Object.freeze({
|
|
@@ -2699,14 +3022,15 @@ var PollTransport = class {
|
|
|
2699
3022
|
while (!context.signal.aborted) {
|
|
2700
3023
|
const token = await context.getToken();
|
|
2701
3024
|
if (!token) throw new UnauthorizedStreamError();
|
|
2702
|
-
const
|
|
2703
|
-
|
|
2704
|
-
|
|
3025
|
+
const url = `${joinUrl(context.baseUrl, "/api/notifications/")}?limit=${this.#limit}&offset=0`;
|
|
3026
|
+
const headers = await context.baseHeaders(url);
|
|
3027
|
+
headers.set("Accept", "application/json");
|
|
3028
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
3029
|
+
const response = await context.fetch(url, {
|
|
3030
|
+
method: "GET",
|
|
3031
|
+
headers,
|
|
3032
|
+
signal: context.signal
|
|
2705
3033
|
});
|
|
2706
|
-
const response = await context.fetch(
|
|
2707
|
-
`${joinUrl(context.baseUrl, "/api/notifications/")}?limit=${this.#limit}&offset=0`,
|
|
2708
|
-
{ method: "GET", headers, signal: context.signal }
|
|
2709
|
-
);
|
|
2710
3034
|
if (response.status === 401) throw new UnauthorizedStreamError();
|
|
2711
3035
|
if (!response.ok) throw new Error(`\u041E\u043F\u0440\u043E\u0441 \u0443\u0432\u0435\u0434\u043E\u043C\u043B\u0435\u043D\u0438\u0439 \u0432\u0435\u0440\u043D\u0443\u043B \u0441\u0442\u0430\u0442\u0443\u0441 ${response.status}`);
|
|
2712
3036
|
const body = await response.json();
|
|
@@ -2962,13 +3286,13 @@ var SseTransport = class {
|
|
|
2962
3286
|
async connect(context) {
|
|
2963
3287
|
const token = await context.getToken();
|
|
2964
3288
|
if (!token) throw new UnauthorizedStreamError();
|
|
2965
|
-
const
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
3289
|
+
const url = joinUrl(context.baseUrl, STREAM_PATH);
|
|
3290
|
+
const headers = await context.baseHeaders(url);
|
|
3291
|
+
headers.set("Accept", "text/event-stream");
|
|
3292
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
3293
|
+
headers.set("Cache-Control", "no-cache");
|
|
2970
3294
|
if (this.#lastEventId) headers.set("Last-Event-ID", this.#lastEventId);
|
|
2971
|
-
const response = await context.fetch(
|
|
3295
|
+
const response = await context.fetch(url, {
|
|
2972
3296
|
method: "GET",
|
|
2973
3297
|
headers,
|
|
2974
3298
|
signal: context.signal
|
|
@@ -3029,10 +3353,42 @@ var RealtimeTransportKind = Object.freeze({
|
|
|
3029
3353
|
/** Периодический опрос REST. */
|
|
3030
3354
|
Poll: "poll"
|
|
3031
3355
|
});
|
|
3356
|
+
function validateRealtimeOptions(options) {
|
|
3357
|
+
const positiveInteger = (value, name) => {
|
|
3358
|
+
if (value === void 0) return;
|
|
3359
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
3360
|
+
throw new ItdConfigError(
|
|
3361
|
+
`realtime.${name} \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0446\u0435\u043B\u044B\u043C \u043D\u0435\u043E\u0442\u0440\u0438\u0446\u0430\u0442\u0435\u043B\u044C\u043D\u044B\u043C \u0447\u0438\u0441\u043B\u043E\u043C, \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${value}`
|
|
3362
|
+
);
|
|
3363
|
+
}
|
|
3364
|
+
};
|
|
3365
|
+
const duration = (value, name, min) => {
|
|
3366
|
+
if (value === void 0) return;
|
|
3367
|
+
if (!Number.isFinite(value) || value < min) {
|
|
3368
|
+
throw new ItdConfigError(
|
|
3369
|
+
`realtime.${name} \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0447\u0438\u0441\u043B\u043E\u043C \u043D\u0435 \u043C\u0435\u043D\u044C\u0448\u0435 ${min}, \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${value}`
|
|
3370
|
+
);
|
|
3371
|
+
}
|
|
3372
|
+
};
|
|
3373
|
+
positiveInteger(options.maxAttempts, "maxAttempts");
|
|
3374
|
+
duration(options.pollInterval, "pollInterval", 1);
|
|
3375
|
+
duration(options.idleTimeout, "idleTimeout", 0);
|
|
3376
|
+
if (options.jitter !== void 0 && !(options.jitter >= 0 && options.jitter <= 1)) {
|
|
3377
|
+
throw new ItdConfigError(
|
|
3378
|
+
`realtime.jitter \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0432 \u0434\u0438\u0430\u043F\u0430\u0437\u043E\u043D\u0435 0\u20261, \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${options.jitter}`
|
|
3379
|
+
);
|
|
3380
|
+
}
|
|
3381
|
+
if (options.backoff !== void 0) {
|
|
3382
|
+
if (!Array.isArray(options.backoff) || options.backoff.length === 0) {
|
|
3383
|
+
throw new ItdConfigError("realtime.backoff \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0443\u0441\u0442\u044B\u043C \u0441\u043F\u0438\u0441\u043A\u043E\u043C \u043F\u0430\u0443\u0437");
|
|
3384
|
+
}
|
|
3385
|
+
for (const delay of options.backoff) duration(delay, "backoff", 0);
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3032
3388
|
var ItdRealtime = class {
|
|
3033
3389
|
#deps;
|
|
3034
3390
|
#options;
|
|
3035
|
-
#emitter
|
|
3391
|
+
#emitter;
|
|
3036
3392
|
#transport;
|
|
3037
3393
|
#maxAttempts;
|
|
3038
3394
|
#controller;
|
|
@@ -3050,10 +3406,16 @@ var ItdRealtime = class {
|
|
|
3050
3406
|
#timer;
|
|
3051
3407
|
#detachEnvironment;
|
|
3052
3408
|
constructor(deps, options = {}) {
|
|
3409
|
+
validateRealtimeOptions(options);
|
|
3053
3410
|
this.#deps = deps;
|
|
3054
3411
|
this.#options = options;
|
|
3055
3412
|
this.#maxAttempts = options.maxAttempts ?? MAX_RECONNECT_ATTEMPTS;
|
|
3056
3413
|
this.#transport = this.#createTransport();
|
|
3414
|
+
this.#emitter = new Emitter((error) => {
|
|
3415
|
+
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";
|
|
3416
|
+
if (deps.logger) deps.logger.error(message, error);
|
|
3417
|
+
else console.error(`[itd-api] ${message}`, error);
|
|
3418
|
+
});
|
|
3057
3419
|
}
|
|
3058
3420
|
/** Текущее состояние соединения. */
|
|
3059
3421
|
get status() {
|
|
@@ -3105,6 +3467,7 @@ var ItdRealtime = class {
|
|
|
3105
3467
|
this.#controller = void 0;
|
|
3106
3468
|
this.#attempt = 0;
|
|
3107
3469
|
this.#setStatus(RealtimeStatus.Disconnected);
|
|
3470
|
+
this.#deps.onClose?.();
|
|
3108
3471
|
}
|
|
3109
3472
|
/** Снимает все подписки. Соединение при этом не закрывается. */
|
|
3110
3473
|
removeAllListeners() {
|
|
@@ -3124,6 +3487,7 @@ var ItdRealtime = class {
|
|
|
3124
3487
|
}
|
|
3125
3488
|
/** Запускает попытку подключения; повторы планирует сам. */
|
|
3126
3489
|
#run() {
|
|
3490
|
+
if (!this.#wanted) return;
|
|
3127
3491
|
this.#controller?.abort();
|
|
3128
3492
|
const controller = new AbortController();
|
|
3129
3493
|
this.#controller = controller;
|
|
@@ -3131,6 +3495,7 @@ var ItdRealtime = class {
|
|
|
3131
3495
|
void this.#transport.connect({
|
|
3132
3496
|
baseUrl: this.#deps.baseUrl,
|
|
3133
3497
|
fetch: this.#deps.fetch,
|
|
3498
|
+
baseHeaders: this.#deps.baseHeaders,
|
|
3134
3499
|
getToken: this.#deps.getToken,
|
|
3135
3500
|
signal: controller.signal,
|
|
3136
3501
|
onOpen: () => {
|
|
@@ -3181,17 +3546,17 @@ var ItdRealtime = class {
|
|
|
3181
3546
|
async #refreshAndReconnect(error) {
|
|
3182
3547
|
this.#setStatus(RealtimeStatus.Error);
|
|
3183
3548
|
const refreshed = await this.#deps.refresh().catch(() => false);
|
|
3549
|
+
if (!this.#wanted) return;
|
|
3184
3550
|
if (!refreshed) {
|
|
3185
|
-
this.#
|
|
3186
|
-
this.#emitter.emit("giveup", void 0);
|
|
3551
|
+
this.#giveUp(error);
|
|
3187
3552
|
return;
|
|
3188
3553
|
}
|
|
3189
3554
|
this.#scheduleReconnect(error);
|
|
3190
3555
|
}
|
|
3191
3556
|
#scheduleReconnect(error) {
|
|
3557
|
+
if (!this.#wanted) return;
|
|
3192
3558
|
if (this.#attempt >= this.#maxAttempts) {
|
|
3193
|
-
this.#
|
|
3194
|
-
this.#emitter.emit("giveup", void 0);
|
|
3559
|
+
this.#giveUp(error);
|
|
3195
3560
|
return;
|
|
3196
3561
|
}
|
|
3197
3562
|
const delay = reconnectDelay(this.#attempt, this.#options);
|
|
@@ -3203,6 +3568,15 @@ var ItdRealtime = class {
|
|
|
3203
3568
|
this.#run();
|
|
3204
3569
|
}, delay);
|
|
3205
3570
|
}
|
|
3571
|
+
/** Завершает автоматические попытки переподключения. */
|
|
3572
|
+
#giveUp(error) {
|
|
3573
|
+
this.#wanted = false;
|
|
3574
|
+
this.#attempt = 0;
|
|
3575
|
+
this.#detachEnvironment?.();
|
|
3576
|
+
this.#detachEnvironment = void 0;
|
|
3577
|
+
this.#emitter.emit("error", { error, willReconnect: false });
|
|
3578
|
+
this.#emitter.emit("giveup", void 0);
|
|
3579
|
+
}
|
|
3206
3580
|
/**
|
|
3207
3581
|
* Подписывается на события среды.
|
|
3208
3582
|
*
|
|
@@ -3250,6 +3624,9 @@ var PaginationMode = Object.freeze({
|
|
|
3250
3624
|
/** Следующая страница запрашивается смещением от начала списка. */
|
|
3251
3625
|
Offset: "offset"
|
|
3252
3626
|
});
|
|
3627
|
+
function mapPage(page, map) {
|
|
3628
|
+
return { ...page, items: page.items.map(map) };
|
|
3629
|
+
}
|
|
3253
3630
|
function readItems(body, fields) {
|
|
3254
3631
|
if (Array.isArray(body)) return body;
|
|
3255
3632
|
for (const field of fields) {
|
|
@@ -3403,6 +3780,14 @@ var Paginator = class {
|
|
|
3403
3780
|
}
|
|
3404
3781
|
};
|
|
3405
3782
|
|
|
3783
|
+
// src/types/options.ts
|
|
3784
|
+
var REQUEST_OPTION_KEYS = [
|
|
3785
|
+
"signal",
|
|
3786
|
+
"timeout",
|
|
3787
|
+
"headers",
|
|
3788
|
+
"retry"
|
|
3789
|
+
];
|
|
3790
|
+
|
|
3406
3791
|
// src/resources/base.ts
|
|
3407
3792
|
var BaseResource = class {
|
|
3408
3793
|
/** @internal */
|
|
@@ -3411,28 +3796,25 @@ var BaseResource = class {
|
|
|
3411
3796
|
this.http = http;
|
|
3412
3797
|
}
|
|
3413
3798
|
/**
|
|
3414
|
-
* Переносит
|
|
3799
|
+
* Переносит опции запроса в описание транспорта.
|
|
3415
3800
|
*
|
|
3416
|
-
*
|
|
3417
|
-
* {@link RequestOptions} и приносят с собой `limit`, `cursor`
|
|
3418
|
-
* запроса делать нечего.
|
|
3801
|
+
* Копируются только поля {@link REQUEST_OPTION_KEYS} и опции, заявленные плагинами:
|
|
3802
|
+
* параметры методов наследуют {@link RequestOptions} и приносят с собой `limit`, `cursor`
|
|
3803
|
+
* и прочее, чему в описании запроса делать нечего. Чужие опции плагинов библиотека
|
|
3419
3804
|
* не понимает, но обязана донести до обёрток нетронутыми.
|
|
3420
3805
|
*/
|
|
3421
3806
|
requestOptions(options) {
|
|
3422
3807
|
if (!options) return {};
|
|
3423
|
-
const result = {
|
|
3424
|
-
...options.signal !== void 0 ? { signal: options.signal } : {},
|
|
3425
|
-
...options.timeout !== void 0 ? { timeout: options.timeout } : {},
|
|
3426
|
-
...options.headers !== void 0 ? { headers: options.headers } : {},
|
|
3427
|
-
...options.retry !== void 0 ? { retry: options.retry } : {}
|
|
3428
|
-
};
|
|
3429
|
-
const pluginKeys = this.http.pluginOptionKeys;
|
|
3430
|
-
if (pluginKeys.size === 0) return result;
|
|
3431
3808
|
const source = options;
|
|
3432
|
-
const
|
|
3809
|
+
const result = {};
|
|
3810
|
+
for (const key of REQUEST_OPTION_KEYS) {
|
|
3811
|
+
const value = source[key];
|
|
3812
|
+
if (value !== void 0) result[key] = value;
|
|
3813
|
+
}
|
|
3814
|
+
const pluginKeys = this.http.pluginOptionKeys;
|
|
3433
3815
|
for (const key of pluginKeys) {
|
|
3434
3816
|
const value = source[key];
|
|
3435
|
-
if (value !== void 0)
|
|
3817
|
+
if (value !== void 0) result[key] = value;
|
|
3436
3818
|
}
|
|
3437
3819
|
return result;
|
|
3438
3820
|
}
|
|
@@ -3452,6 +3834,42 @@ var BaseResource = class {
|
|
|
3452
3834
|
...options?.start !== void 0 ? { start: options.start } : {}
|
|
3453
3835
|
});
|
|
3454
3836
|
}
|
|
3837
|
+
/**
|
|
3838
|
+
* Собирает пару «загрузка страницы + перебор» из одного описания.
|
|
3839
|
+
*
|
|
3840
|
+
* Путь, параметры запроса и разбор ответа задаются один раз; `list` и `iterate`
|
|
3841
|
+
* строятся из них.
|
|
3842
|
+
*
|
|
3843
|
+
* @example
|
|
3844
|
+
* ```ts
|
|
3845
|
+
* #feed = this.paginated<Post, FeedParams>({
|
|
3846
|
+
* path: () => '/api/posts',
|
|
3847
|
+
* query: (p) => ({ tab: p.tab, limit: p.limit }),
|
|
3848
|
+
* start: (p) => (p.cursor ? { cursor: p.cursor } : {}),
|
|
3849
|
+
* read: (body) => readCursorPage<Post>(body, 'posts'),
|
|
3850
|
+
* mode: PaginationMode.Cursor,
|
|
3851
|
+
* });
|
|
3852
|
+
* ```
|
|
3853
|
+
*/
|
|
3854
|
+
paginated(spec) {
|
|
3855
|
+
const load = async (params, state) => {
|
|
3856
|
+
const body = await this.http.request({
|
|
3857
|
+
method: "GET",
|
|
3858
|
+
path: spec.path(params),
|
|
3859
|
+
query: withPageState(spec.query(params), state),
|
|
3860
|
+
...this.requestOptions(params)
|
|
3861
|
+
});
|
|
3862
|
+
return spec.read(body, state);
|
|
3863
|
+
};
|
|
3864
|
+
return {
|
|
3865
|
+
list: (params) => load(params, spec.start(params)),
|
|
3866
|
+
iterate: (params) => this.paginate(spec.mode, (state) => load(params, state), {
|
|
3867
|
+
...params.maxPages !== void 0 ? { maxPages: params.maxPages } : {},
|
|
3868
|
+
...params.signal !== void 0 ? { signal: params.signal } : {},
|
|
3869
|
+
start: spec.start(params)
|
|
3870
|
+
})
|
|
3871
|
+
};
|
|
3872
|
+
}
|
|
3455
3873
|
};
|
|
3456
3874
|
function withPageState(query, state) {
|
|
3457
3875
|
return {
|
|
@@ -3485,7 +3903,7 @@ var AuthResource = class extends BaseResource {
|
|
|
3485
3903
|
async signUp(credentials, options = {}) {
|
|
3486
3904
|
const body = await this.http.request({
|
|
3487
3905
|
method: "POST",
|
|
3488
|
-
path:
|
|
3906
|
+
path: AUTH_PATHS.signUp,
|
|
3489
3907
|
body: credentials,
|
|
3490
3908
|
skipAuth: true,
|
|
3491
3909
|
skipAuthRefresh: true,
|
|
@@ -3510,7 +3928,7 @@ var AuthResource = class extends BaseResource {
|
|
|
3510
3928
|
async signIn(credentials, options = {}) {
|
|
3511
3929
|
const body = await this.http.request({
|
|
3512
3930
|
method: "POST",
|
|
3513
|
-
path:
|
|
3931
|
+
path: AUTH_PATHS.signIn,
|
|
3514
3932
|
body: credentials,
|
|
3515
3933
|
skipAuth: true,
|
|
3516
3934
|
skipAuthRefresh: true,
|
|
@@ -3531,7 +3949,7 @@ var AuthResource = class extends BaseResource {
|
|
|
3531
3949
|
async verifyOtp(input, options = {}) {
|
|
3532
3950
|
const body = await this.http.request({
|
|
3533
3951
|
method: "POST",
|
|
3534
|
-
path:
|
|
3952
|
+
path: AUTH_PATHS.verifyOtp,
|
|
3535
3953
|
body: input,
|
|
3536
3954
|
skipAuth: true,
|
|
3537
3955
|
skipAuthRefresh: true,
|
|
@@ -3548,7 +3966,7 @@ var AuthResource = class extends BaseResource {
|
|
|
3548
3966
|
resendOtp(input, options = {}) {
|
|
3549
3967
|
return this.http.request({
|
|
3550
3968
|
method: "POST",
|
|
3551
|
-
path:
|
|
3969
|
+
path: AUTH_PATHS.resendOtp,
|
|
3552
3970
|
body: input,
|
|
3553
3971
|
skipAuth: true,
|
|
3554
3972
|
skipAuthRefresh: true,
|
|
@@ -3625,7 +4043,7 @@ var AuthResource = class extends BaseResource {
|
|
|
3625
4043
|
async logout(options = {}) {
|
|
3626
4044
|
await this.http.request({
|
|
3627
4045
|
method: "POST",
|
|
3628
|
-
path:
|
|
4046
|
+
path: AUTH_PATHS.logout,
|
|
3629
4047
|
skipAuthRefresh: true,
|
|
3630
4048
|
...this.requestOptions(options)
|
|
3631
4049
|
});
|
|
@@ -3655,7 +4073,7 @@ var AuthResource = class extends BaseResource {
|
|
|
3655
4073
|
async forgotPassword(input, options = {}) {
|
|
3656
4074
|
const body = await this.http.request({
|
|
3657
4075
|
method: "POST",
|
|
3658
|
-
path:
|
|
4076
|
+
path: AUTH_PATHS.forgotPassword,
|
|
3659
4077
|
body: input,
|
|
3660
4078
|
skipAuth: true,
|
|
3661
4079
|
skipAuthRefresh: true,
|
|
@@ -3676,7 +4094,7 @@ var AuthResource = class extends BaseResource {
|
|
|
3676
4094
|
resetPassword(input, options = {}) {
|
|
3677
4095
|
return this.http.request({
|
|
3678
4096
|
method: "POST",
|
|
3679
|
-
path:
|
|
4097
|
+
path: AUTH_PATHS.resetPassword,
|
|
3680
4098
|
body: input,
|
|
3681
4099
|
skipAuth: true,
|
|
3682
4100
|
skipAuthRefresh: true,
|
|
@@ -3714,14 +4132,17 @@ var AuthResource = class extends BaseResource {
|
|
|
3714
4132
|
* Меняет пароль. Требует действующей сессии.
|
|
3715
4133
|
*
|
|
3716
4134
|
* При неверном текущем пароле сервер отвечает `ACCOUNT_CURRENT_PASSWORD_INCORRECT`.
|
|
4135
|
+
*
|
|
4136
|
+
* @example
|
|
4137
|
+
* ```ts
|
|
4138
|
+
* await itd.auth.changePassword({ currentPassword, newPassword });
|
|
4139
|
+
* ```
|
|
3717
4140
|
*/
|
|
3718
4141
|
changePassword(input, options = {}) {
|
|
3719
4142
|
return this.http.request({
|
|
3720
4143
|
method: "POST",
|
|
3721
|
-
path:
|
|
3722
|
-
|
|
3723
|
-
// не проверить, а лишнее поле он игнорирует.
|
|
3724
|
-
body: { ...input, currentPassword: input.oldPassword },
|
|
4144
|
+
path: AUTH_PATHS.changePassword,
|
|
4145
|
+
body: { currentPassword: input.currentPassword, newPassword: input.newPassword },
|
|
3725
4146
|
...this.requestOptions(options)
|
|
3726
4147
|
});
|
|
3727
4148
|
}
|
|
@@ -3737,13 +4158,13 @@ var AuthResource = class extends BaseResource {
|
|
|
3737
4158
|
* ```
|
|
3738
4159
|
*/
|
|
3739
4160
|
oauthUrl(provider) {
|
|
3740
|
-
return joinUrl(this.http.baseUrl,
|
|
4161
|
+
return joinUrl(this.http.baseUrl, `${AUTH_PATHS.oauthLogin}/${provider}`);
|
|
3741
4162
|
}
|
|
3742
4163
|
/** Загружает список активных сессий. У текущей поле `isCurrent` равно `true`. */
|
|
3743
4164
|
async sessions(options = {}) {
|
|
3744
4165
|
const body = await this.http.request({
|
|
3745
4166
|
method: "GET",
|
|
3746
|
-
path:
|
|
4167
|
+
path: AUTH_PATHS.sessions,
|
|
3747
4168
|
...this.requestOptions(options)
|
|
3748
4169
|
});
|
|
3749
4170
|
return pickArray(body, "sessions");
|
|
@@ -3752,7 +4173,7 @@ var AuthResource = class extends BaseResource {
|
|
|
3752
4173
|
revokeSession(sessionId, options = {}) {
|
|
3753
4174
|
return this.http.request({
|
|
3754
4175
|
method: "DELETE",
|
|
3755
|
-
path:
|
|
4176
|
+
path: `${AUTH_PATHS.sessions}/${encodeURIComponent(sessionId)}`,
|
|
3756
4177
|
...this.requestOptions(options)
|
|
3757
4178
|
});
|
|
3758
4179
|
}
|
|
@@ -3760,7 +4181,7 @@ var AuthResource = class extends BaseResource {
|
|
|
3760
4181
|
revokeOtherSessions(options = {}) {
|
|
3761
4182
|
return this.http.request({
|
|
3762
4183
|
method: "DELETE",
|
|
3763
|
-
path:
|
|
4184
|
+
path: AUTH_PATHS.sessions,
|
|
3764
4185
|
...this.requestOptions(options)
|
|
3765
4186
|
});
|
|
3766
4187
|
}
|
|
@@ -3769,6 +4190,14 @@ var AuthResource = class extends BaseResource {
|
|
|
3769
4190
|
// src/resources/comments.ts
|
|
3770
4191
|
var CommentsResource = class extends BaseResource {
|
|
3771
4192
|
#uploadFiles;
|
|
4193
|
+
/** Ответы на комментарий: `/api/comments/{id}/replies`, постраничная пагинация. */
|
|
4194
|
+
#replies = this.paginated({
|
|
4195
|
+
path: (p) => `/api/comments/${encodePathSegment(p.commentId, "commentId")}/replies`,
|
|
4196
|
+
query: (p) => ({ limit: p.limit }),
|
|
4197
|
+
start: (p) => p.page !== void 0 ? { page: p.page } : {},
|
|
4198
|
+
read: (body) => readPagedPage(body, "replies"),
|
|
4199
|
+
mode: PaginationMode.Page
|
|
4200
|
+
});
|
|
3772
4201
|
constructor(http, deps) {
|
|
3773
4202
|
super(http);
|
|
3774
4203
|
this.#uploadFiles = deps.uploadFiles;
|
|
@@ -3778,31 +4207,12 @@ var CommentsResource = class extends BaseResource {
|
|
|
3778
4207
|
*
|
|
3779
4208
|
* Здесь пагинация **постраничная**, в отличие от комментариев к посту, где курсорная.
|
|
3780
4209
|
*/
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
method: "GET",
|
|
3784
|
-
path: `/api/comments/${encodePathSegment(commentId, "commentId")}/replies`,
|
|
3785
|
-
query: { limit: params.limit, page: params.page },
|
|
3786
|
-
...this.requestOptions(params)
|
|
3787
|
-
});
|
|
3788
|
-
return readPagedPage(body, "replies");
|
|
4210
|
+
replies(commentId, params = {}) {
|
|
4211
|
+
return this.#replies.list({ ...params, commentId });
|
|
3789
4212
|
}
|
|
3790
4213
|
/** Перебирает ответы на комментарий. */
|
|
3791
4214
|
iterateReplies(commentId, params = {}) {
|
|
3792
|
-
|
|
3793
|
-
return this.paginate(
|
|
3794
|
-
PaginationMode.Page,
|
|
3795
|
-
async (state) => {
|
|
3796
|
-
const body = await this.http.request({
|
|
3797
|
-
method: "GET",
|
|
3798
|
-
path,
|
|
3799
|
-
query: withPageState({ limit: params.limit }, state),
|
|
3800
|
-
...this.requestOptions(params)
|
|
3801
|
-
});
|
|
3802
|
-
return readPagedPage(body, "replies");
|
|
3803
|
-
},
|
|
3804
|
-
{ ...params, ...params.page !== void 0 ? { start: { page: params.page } } : {} }
|
|
3805
|
-
);
|
|
4215
|
+
return this.#replies.iterate({ ...params, commentId });
|
|
3806
4216
|
}
|
|
3807
4217
|
/**
|
|
3808
4218
|
* Отвечает на комментарий.
|
|
@@ -3937,19 +4347,14 @@ function assertAllowedMime(mimeType, filename) {
|
|
|
3937
4347
|
var DEFAULT_UPLOAD_TIMEOUT = 3e5;
|
|
3938
4348
|
var FilesResource = class extends BaseResource {
|
|
3939
4349
|
#readFile;
|
|
4350
|
+
/**
|
|
4351
|
+
* @param deps.readFile чтение файлов с диска. Передаёт точка входа `itd-api/node`;
|
|
4352
|
+
* в основном бандле его нет, чтобы браузерные сборщики не пытались разрешить `node:fs`.
|
|
4353
|
+
*/
|
|
3940
4354
|
constructor(http, deps = {}) {
|
|
3941
4355
|
super(http);
|
|
3942
4356
|
this.#readFile = deps.readFile;
|
|
3943
4357
|
}
|
|
3944
|
-
/**
|
|
3945
|
-
* Подключает чтение файлов с диска.
|
|
3946
|
-
*
|
|
3947
|
-
* Вызывается точкой входа `itd-api/node`; в основном бандле работы с файловой
|
|
3948
|
-
* системой нет, чтобы браузерные сборщики не пытались разрешить `node:fs`.
|
|
3949
|
-
*/
|
|
3950
|
-
setFileReader(readFile) {
|
|
3951
|
-
this.#readFile = readFile;
|
|
3952
|
-
}
|
|
3953
4358
|
/**
|
|
3954
4359
|
* Загружает файл и возвращает его идентификатор.
|
|
3955
4360
|
*
|
|
@@ -4059,8 +4464,16 @@ var FilesResource = class extends BaseResource {
|
|
|
4059
4464
|
}
|
|
4060
4465
|
};
|
|
4061
4466
|
|
|
4062
|
-
// src/resources/
|
|
4467
|
+
// src/resources/hashtags.ts
|
|
4063
4468
|
var HashtagsResource = class extends BaseResource {
|
|
4469
|
+
/** Посты по хэштегу: `/api/hashtags/{tag}/posts`, курсорная пагинация. */
|
|
4470
|
+
#posts = this.paginated({
|
|
4471
|
+
path: (p) => `/api/hashtags/${encodePathSegment(p.tag, "tag")}/posts`,
|
|
4472
|
+
query: (p) => ({ limit: p.limit }),
|
|
4473
|
+
start: (p) => p.cursor ? { cursor: p.cursor } : {},
|
|
4474
|
+
read: (body) => readCursorPage(body, "posts"),
|
|
4475
|
+
mode: PaginationMode.Cursor
|
|
4476
|
+
});
|
|
4064
4477
|
/**
|
|
4065
4478
|
* Ищет хэштеги.
|
|
4066
4479
|
*
|
|
@@ -4091,210 +4504,48 @@ var HashtagsResource = class extends BaseResource {
|
|
|
4091
4504
|
* @param tag название без решётки; кодируется автоматически, поэтому кириллица
|
|
4092
4505
|
* и пробелы допустимы
|
|
4093
4506
|
*/
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
method: "GET",
|
|
4097
|
-
path: `/api/hashtags/${encodePathSegment(tag, "tag")}/posts`,
|
|
4098
|
-
query: { limit: params.limit, cursor: params.cursor },
|
|
4099
|
-
...this.requestOptions(params)
|
|
4100
|
-
});
|
|
4101
|
-
return readCursorPage(body, "posts");
|
|
4507
|
+
posts(tag, params = {}) {
|
|
4508
|
+
return this.#posts.list({ ...params, tag });
|
|
4102
4509
|
}
|
|
4103
4510
|
/** Перебирает посты по хэштегу. */
|
|
4104
4511
|
iteratePosts(tag, params = {}) {
|
|
4105
|
-
|
|
4106
|
-
return this.paginate(
|
|
4107
|
-
PaginationMode.Cursor,
|
|
4108
|
-
async (state) => {
|
|
4109
|
-
const body = await this.http.request({
|
|
4110
|
-
method: "GET",
|
|
4111
|
-
path,
|
|
4112
|
-
query: withPageState({ limit: params.limit }, state),
|
|
4113
|
-
...this.requestOptions(params)
|
|
4114
|
-
});
|
|
4115
|
-
return readCursorPage(body, "posts");
|
|
4116
|
-
},
|
|
4117
|
-
{ ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
|
|
4118
|
-
);
|
|
4119
|
-
}
|
|
4120
|
-
};
|
|
4121
|
-
var SearchResource = class extends BaseResource {
|
|
4122
|
-
/**
|
|
4123
|
-
* Ищет пользователей и хэштеги одним запросом.
|
|
4124
|
-
*
|
|
4125
|
-
* @example
|
|
4126
|
-
* ```ts
|
|
4127
|
-
* const { users, hashtags } = await itd.search.all('арт');
|
|
4128
|
-
* ```
|
|
4129
|
-
*/
|
|
4130
|
-
async all(query, options = {}) {
|
|
4131
|
-
const body = await this.http.request({
|
|
4132
|
-
method: "GET",
|
|
4133
|
-
path: "/api/search",
|
|
4134
|
-
query: { q: query },
|
|
4135
|
-
...this.requestOptions(options)
|
|
4136
|
-
});
|
|
4137
|
-
return {
|
|
4138
|
-
users: pickArray(body, "users"),
|
|
4139
|
-
hashtags: pickArray(body, "hashtags")
|
|
4140
|
-
};
|
|
4141
|
-
}
|
|
4142
|
-
};
|
|
4143
|
-
var ReportsResource = class extends BaseResource {
|
|
4144
|
-
/**
|
|
4145
|
-
* Отправляет жалобу.
|
|
4146
|
-
*
|
|
4147
|
-
* Повторная жалоба на тот же объект отклоняется сервером с сообщением
|
|
4148
|
-
* «Вы уже отправляли жалобу на этот контент».
|
|
4149
|
-
*
|
|
4150
|
-
* @example
|
|
4151
|
-
* ```ts
|
|
4152
|
-
* await itd.reports.create(report.post(postId).reason('spam'));
|
|
4153
|
-
* await itd.reports.create({ targetType: 'user', targetId, reason: 'fraud' });
|
|
4154
|
-
* ```
|
|
4155
|
-
*/
|
|
4156
|
-
create(input, options = {}) {
|
|
4157
|
-
const data = resolveReport(input);
|
|
4158
|
-
return this.http.request({
|
|
4159
|
-
method: "POST",
|
|
4160
|
-
path: "/api/reports",
|
|
4161
|
-
body: data,
|
|
4162
|
-
...this.requestOptions(options)
|
|
4163
|
-
});
|
|
4164
|
-
}
|
|
4165
|
-
};
|
|
4166
|
-
var VerificationResource = class extends BaseResource {
|
|
4167
|
-
/** Загружает статус заявки. Значение `none` означает, что заявка не подавалась. */
|
|
4168
|
-
status(options = {}) {
|
|
4169
|
-
return this.http.request({
|
|
4170
|
-
method: "GET",
|
|
4171
|
-
path: "/api/verification/status",
|
|
4172
|
-
...this.requestOptions(options)
|
|
4173
|
-
});
|
|
4174
|
-
}
|
|
4175
|
-
/** Подаёт заявку на верификацию с видео. */
|
|
4176
|
-
submit(videoUrl, options = {}) {
|
|
4177
|
-
return this.http.request({
|
|
4178
|
-
method: "POST",
|
|
4179
|
-
path: "/api/verification/submit",
|
|
4180
|
-
body: { videoUrl },
|
|
4181
|
-
...this.requestOptions(options)
|
|
4182
|
-
});
|
|
4183
|
-
}
|
|
4184
|
-
};
|
|
4185
|
-
var SubscriptionResource = class extends BaseResource {
|
|
4186
|
-
/** Загружает состояние подписки и её цену. */
|
|
4187
|
-
status(options = {}) {
|
|
4188
|
-
return this.http.request({
|
|
4189
|
-
method: "GET",
|
|
4190
|
-
// Завершающий слэш обязателен.
|
|
4191
|
-
path: "/api/v1/subscription/",
|
|
4192
|
-
...this.requestOptions(options)
|
|
4193
|
-
});
|
|
4194
|
-
}
|
|
4195
|
-
/**
|
|
4196
|
-
* Запускает оплату подписки.
|
|
4197
|
-
*
|
|
4198
|
-
* Форма ответа в документации API не описана, поэтому тип результата не уточняется.
|
|
4199
|
-
*/
|
|
4200
|
-
pay(options = {}) {
|
|
4201
|
-
return this.http.request({
|
|
4202
|
-
method: "POST",
|
|
4203
|
-
path: "/api/v1/subscription/pay",
|
|
4204
|
-
...this.requestOptions(options)
|
|
4205
|
-
});
|
|
4206
|
-
}
|
|
4207
|
-
/** Включает или отключает автопродление. */
|
|
4208
|
-
setAutoRenewal(enabled, options = {}) {
|
|
4209
|
-
return this.http.request({
|
|
4210
|
-
method: "POST",
|
|
4211
|
-
path: "/api/v1/subscription/auto-renewal",
|
|
4212
|
-
body: { enabled },
|
|
4213
|
-
...this.requestOptions(options)
|
|
4214
|
-
});
|
|
4215
|
-
}
|
|
4216
|
-
/** Запускает привязку карты. */
|
|
4217
|
-
bindCard(options = {}) {
|
|
4218
|
-
return this.http.request({
|
|
4219
|
-
method: "POST",
|
|
4220
|
-
path: "/api/v1/subscription/bind-card",
|
|
4221
|
-
...this.requestOptions(options)
|
|
4222
|
-
});
|
|
4223
|
-
}
|
|
4224
|
-
/** Загружает список способов оплаты. Пустой массив, если карт нет. */
|
|
4225
|
-
async methods(options = {}) {
|
|
4226
|
-
const body = await this.http.request({
|
|
4227
|
-
method: "GET",
|
|
4228
|
-
path: "/api/v1/subscription/methods",
|
|
4229
|
-
...this.requestOptions(options)
|
|
4230
|
-
});
|
|
4231
|
-
return Array.isArray(body) ? body : [];
|
|
4232
|
-
}
|
|
4233
|
-
/** Делает способ оплаты основным. */
|
|
4234
|
-
setDefaultMethod(methodId, options = {}) {
|
|
4235
|
-
return this.http.request({
|
|
4236
|
-
method: "POST",
|
|
4237
|
-
path: `/api/v1/subscription/methods/${encodePathSegment(methodId, "methodId")}/default`,
|
|
4238
|
-
...this.requestOptions(options)
|
|
4239
|
-
});
|
|
4240
|
-
}
|
|
4241
|
-
/** Удаляет способ оплаты. */
|
|
4242
|
-
removeMethod(methodId, options = {}) {
|
|
4243
|
-
return this.http.request({
|
|
4244
|
-
method: "DELETE",
|
|
4245
|
-
path: `/api/v1/subscription/methods/${encodePathSegment(methodId, "methodId")}`,
|
|
4246
|
-
...this.requestOptions(options)
|
|
4247
|
-
});
|
|
4248
|
-
}
|
|
4249
|
-
};
|
|
4250
|
-
var PlatformResource = class extends BaseResource {
|
|
4251
|
-
/** Загружает журнал изменений. */
|
|
4252
|
-
async changelog(options = {}) {
|
|
4253
|
-
const body = await this.http.request({
|
|
4254
|
-
method: "GET",
|
|
4255
|
-
path: "/api/platform/changelog",
|
|
4256
|
-
...this.requestOptions(options)
|
|
4257
|
-
});
|
|
4258
|
-
return Array.isArray(body) ? body : [];
|
|
4259
|
-
}
|
|
4260
|
-
/** Загружает анонсы платформы. */
|
|
4261
|
-
async announcements(options = {}) {
|
|
4262
|
-
const body = await this.http.request({
|
|
4263
|
-
method: "GET",
|
|
4264
|
-
path: "/api/platform/announcements",
|
|
4265
|
-
...this.requestOptions(options)
|
|
4266
|
-
});
|
|
4267
|
-
return pickArray(body, "announcements");
|
|
4268
|
-
}
|
|
4269
|
-
/** Загружает баннер текущего события — виджет «портал». */
|
|
4270
|
-
portal(options = {}) {
|
|
4271
|
-
return this.http.request({
|
|
4272
|
-
method: "GET",
|
|
4273
|
-
path: "/api/v1/portal",
|
|
4274
|
-
...this.requestOptions(options)
|
|
4275
|
-
});
|
|
4512
|
+
return this.#posts.iterate({ ...params, tag });
|
|
4276
4513
|
}
|
|
4277
4514
|
};
|
|
4278
4515
|
|
|
4279
4516
|
// src/resources/notifications.ts
|
|
4517
|
+
var NOTIFICATION_SETTING_KEYS = [
|
|
4518
|
+
"enabled",
|
|
4519
|
+
"sound",
|
|
4520
|
+
"follows",
|
|
4521
|
+
"wallPosts",
|
|
4522
|
+
"likes",
|
|
4523
|
+
"comments",
|
|
4524
|
+
"mentions"
|
|
4525
|
+
];
|
|
4280
4526
|
var READ_BATCH_SIZE = 20;
|
|
4281
4527
|
function readSettings(body) {
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
follows: pickBoolean(body, "follows", true),
|
|
4286
|
-
wallPosts: pickBoolean(body, "wallPosts", true),
|
|
4287
|
-
likes: pickBoolean(body, "likes", true),
|
|
4288
|
-
comments: pickBoolean(body, "comments", true),
|
|
4289
|
-
mentions: pickBoolean(body, "mentions", true)
|
|
4290
|
-
};
|
|
4528
|
+
const settings = {};
|
|
4529
|
+
for (const key of NOTIFICATION_SETTING_KEYS) settings[key] = pickBoolean(body, key, true);
|
|
4530
|
+
return settings;
|
|
4291
4531
|
}
|
|
4292
4532
|
var NotificationsResource = class extends BaseResource {
|
|
4533
|
+
/** Уведомления: `/api/notifications/`, пагинация по смещению. */
|
|
4534
|
+
#list = this.paginated({
|
|
4535
|
+
// Завершающий слэш обязателен: без него сервер отвечает ошибкой.
|
|
4536
|
+
path: () => "/api/notifications/",
|
|
4537
|
+
query: (p) => ({ limit: p.limit }),
|
|
4538
|
+
start: (p) => ({ offset: p.offset ?? 0 }),
|
|
4539
|
+
read: (body, state) => {
|
|
4540
|
+
const page = readOffsetPage(body, "notifications", state.offset ?? 0);
|
|
4541
|
+
return { ...page, items: page.items.map(normalizeNotification) };
|
|
4542
|
+
},
|
|
4543
|
+
mode: PaginationMode.Offset
|
|
4544
|
+
});
|
|
4293
4545
|
/**
|
|
4294
4546
|
* Загружает страницу уведомлений.
|
|
4295
4547
|
*
|
|
4296
|
-
* Пагинация здесь основана на смещении.
|
|
4297
|
-
* и притворяется, что это курсор; библиотека отдаёт честное число.
|
|
4548
|
+
* Пагинация здесь основана на смещении.
|
|
4298
4549
|
*
|
|
4299
4550
|
* @example
|
|
4300
4551
|
* ```ts
|
|
@@ -4303,19 +4554,7 @@ var NotificationsResource = class extends BaseResource {
|
|
|
4303
4554
|
* ```
|
|
4304
4555
|
*/
|
|
4305
4556
|
list(params = {}) {
|
|
4306
|
-
return this.#
|
|
4307
|
-
}
|
|
4308
|
-
/** Общая загрузка страницы для {@link list} и {@link iterate}. */
|
|
4309
|
-
async #loadPage(params, offset) {
|
|
4310
|
-
const body = await this.http.request({
|
|
4311
|
-
method: "GET",
|
|
4312
|
-
// Завершающий слэш обязателен: без него сервер отвечает ошибкой.
|
|
4313
|
-
path: "/api/notifications/",
|
|
4314
|
-
query: { limit: params.limit, offset },
|
|
4315
|
-
...this.requestOptions(params)
|
|
4316
|
-
});
|
|
4317
|
-
const page = readOffsetPage(body, "notifications", offset);
|
|
4318
|
-
return { ...page, items: page.items.map(normalizeNotification) };
|
|
4557
|
+
return this.#list.list(params);
|
|
4319
4558
|
}
|
|
4320
4559
|
/**
|
|
4321
4560
|
* Перебирает уведомления.
|
|
@@ -4328,11 +4567,7 @@ var NotificationsResource = class extends BaseResource {
|
|
|
4328
4567
|
* ```
|
|
4329
4568
|
*/
|
|
4330
4569
|
iterate(params = {}) {
|
|
4331
|
-
return this.
|
|
4332
|
-
PaginationMode.Offset,
|
|
4333
|
-
(state) => this.#loadPage(params, state.offset ?? 0),
|
|
4334
|
-
{ ...params, ...params.offset !== void 0 ? { start: { offset: params.offset } } : {} }
|
|
4335
|
-
);
|
|
4570
|
+
return this.#list.iterate(params);
|
|
4336
4571
|
}
|
|
4337
4572
|
/** Загружает число непрочитанных уведомлений. */
|
|
4338
4573
|
async count(options = {}) {
|
|
@@ -4392,43 +4627,143 @@ var NotificationsResource = class extends BaseResource {
|
|
|
4392
4627
|
async getSettings(options = {}) {
|
|
4393
4628
|
const body = await this.http.request({
|
|
4394
4629
|
method: "GET",
|
|
4395
|
-
path: "/api/notifications/settings",
|
|
4630
|
+
path: "/api/notifications/settings",
|
|
4631
|
+
...this.requestOptions(options)
|
|
4632
|
+
});
|
|
4633
|
+
return readSettings(body);
|
|
4634
|
+
}
|
|
4635
|
+
/**
|
|
4636
|
+
* Обновляет настройки уведомлений.
|
|
4637
|
+
*
|
|
4638
|
+
* Отправляются только изменяемые поля, в том же виде, в каком сервер их возвращает.
|
|
4639
|
+
*/
|
|
4640
|
+
async updateSettings(input, options = {}) {
|
|
4641
|
+
const payload = {};
|
|
4642
|
+
for (const key of NOTIFICATION_SETTING_KEYS) {
|
|
4643
|
+
const value = input[key];
|
|
4644
|
+
if (value !== void 0) payload[key] = value;
|
|
4645
|
+
}
|
|
4646
|
+
const body = await this.http.request({
|
|
4647
|
+
method: "PUT",
|
|
4648
|
+
path: "/api/notifications/settings",
|
|
4649
|
+
body: payload,
|
|
4650
|
+
...this.requestOptions(options)
|
|
4651
|
+
});
|
|
4652
|
+
return readSettings(body);
|
|
4653
|
+
}
|
|
4654
|
+
};
|
|
4655
|
+
|
|
4656
|
+
// src/core/time.ts
|
|
4657
|
+
var NAIVE_STAMP = /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}(?:\.\d+)?)$/;
|
|
4658
|
+
function utcStampToIso(value) {
|
|
4659
|
+
const match = typeof value === "string" ? NAIVE_STAMP.exec(value) : null;
|
|
4660
|
+
if (!match) return value;
|
|
4661
|
+
const iso = `${match[1]}T${match[2]}Z`;
|
|
4662
|
+
return Number.isFinite(Date.parse(iso)) ? iso : value;
|
|
4663
|
+
}
|
|
4664
|
+
|
|
4665
|
+
// src/resources/platform.ts
|
|
4666
|
+
function normalizeStatus(body) {
|
|
4667
|
+
if (!isRecord(body) || !Array.isArray(body.services)) return body;
|
|
4668
|
+
return {
|
|
4669
|
+
...body,
|
|
4670
|
+
services: body.services.map(
|
|
4671
|
+
(service) => typeof service?.last_checked === "string" ? { ...service, last_checked: utcStampToIso(service.last_checked) } : service
|
|
4672
|
+
)
|
|
4673
|
+
};
|
|
4674
|
+
}
|
|
4675
|
+
var PlatformResource = class extends BaseResource {
|
|
4676
|
+
/** Загружает журнал изменений. */
|
|
4677
|
+
async changelog(options = {}) {
|
|
4678
|
+
const body = await this.http.request({
|
|
4679
|
+
method: "GET",
|
|
4680
|
+
path: "/api/platform/changelog",
|
|
4681
|
+
...this.requestOptions(options)
|
|
4682
|
+
});
|
|
4683
|
+
return Array.isArray(body) ? body : [];
|
|
4684
|
+
}
|
|
4685
|
+
/** Загружает анонсы платформы. */
|
|
4686
|
+
async announcements(options = {}) {
|
|
4687
|
+
const body = await this.http.request({
|
|
4688
|
+
method: "GET",
|
|
4689
|
+
path: "/api/platform/announcements",
|
|
4690
|
+
...this.requestOptions(options)
|
|
4691
|
+
});
|
|
4692
|
+
return pickArray(body, "announcements");
|
|
4693
|
+
}
|
|
4694
|
+
/** Загружает баннер текущего события — виджет «портал». */
|
|
4695
|
+
portal(options = {}) {
|
|
4696
|
+
return this.http.request({
|
|
4697
|
+
method: "GET",
|
|
4698
|
+
path: "/api/v1/portal",
|
|
4396
4699
|
...this.requestOptions(options)
|
|
4397
4700
|
});
|
|
4398
|
-
return readSettings(body);
|
|
4399
4701
|
}
|
|
4400
4702
|
/**
|
|
4401
|
-
*
|
|
4703
|
+
* Загружает состояние сервисов платформы за последние 90 суток.
|
|
4402
4704
|
*
|
|
4403
|
-
*
|
|
4705
|
+
* Идёт на хост `статус.итд.com` без авторизации. Ответ кэшируется сервером на минуту.
|
|
4706
|
+
* История по суткам приходит разреженной, ровный массив даёт `statusDays`.
|
|
4707
|
+
*
|
|
4708
|
+
* @example
|
|
4709
|
+
* ```ts
|
|
4710
|
+
* const status = await itd.platform.status();
|
|
4711
|
+
*
|
|
4712
|
+
* if (status.overall_status !== 'operational') {
|
|
4713
|
+
* const broken = status.services.filter((s) => s.current_status !== 'operational');
|
|
4714
|
+
* console.log('лежит:', broken.map((s) => s.name).join(', '));
|
|
4715
|
+
* }
|
|
4716
|
+
* ```
|
|
4404
4717
|
*/
|
|
4405
|
-
async
|
|
4406
|
-
const payload = {};
|
|
4407
|
-
for (const key of [
|
|
4408
|
-
"enabled",
|
|
4409
|
-
"sound",
|
|
4410
|
-
"follows",
|
|
4411
|
-
"wallPosts",
|
|
4412
|
-
"likes",
|
|
4413
|
-
"comments",
|
|
4414
|
-
"mentions"
|
|
4415
|
-
]) {
|
|
4416
|
-
const value = input[key];
|
|
4417
|
-
if (value !== void 0) payload[key] = value;
|
|
4418
|
-
}
|
|
4718
|
+
async status(options = {}) {
|
|
4419
4719
|
const body = await this.http.request({
|
|
4420
|
-
method: "
|
|
4421
|
-
|
|
4422
|
-
|
|
4720
|
+
method: "GET",
|
|
4721
|
+
service: STATUS_SERVICE,
|
|
4722
|
+
path: "/api/status",
|
|
4423
4723
|
...this.requestOptions(options)
|
|
4424
4724
|
});
|
|
4425
|
-
return
|
|
4725
|
+
return normalizeStatus(body);
|
|
4426
4726
|
}
|
|
4427
4727
|
};
|
|
4428
4728
|
|
|
4429
4729
|
// src/resources/posts.ts
|
|
4730
|
+
function cursorStart(params) {
|
|
4731
|
+
return params.cursor ? { cursor: params.cursor } : {};
|
|
4732
|
+
}
|
|
4430
4733
|
var PostsResource = class extends BaseResource {
|
|
4431
4734
|
#uploadFiles;
|
|
4735
|
+
/** Лента: `/api/posts`, курсорная пагинация. */
|
|
4736
|
+
#feed = this.paginated({
|
|
4737
|
+
path: () => "/api/posts",
|
|
4738
|
+
query: (p) => ({ tab: p.tab, limit: p.limit }),
|
|
4739
|
+
start: cursorStart,
|
|
4740
|
+
read: (body) => readCursorPage(body, "posts"),
|
|
4741
|
+
mode: PaginationMode.Cursor
|
|
4742
|
+
});
|
|
4743
|
+
/** Стена пользователя: `/api/posts/user/{user}`. */
|
|
4744
|
+
#wall = this.paginated({
|
|
4745
|
+
path: (p) => `/api/posts/user/${encodePathSegment(p.user, "user")}`,
|
|
4746
|
+
query: (p) => ({ limit: p.limit, sort: p.sort, pinnedPostId: p.pinnedPostId }),
|
|
4747
|
+
start: cursorStart,
|
|
4748
|
+
read: (body) => readCursorPage(body, "posts"),
|
|
4749
|
+
mode: PaginationMode.Cursor
|
|
4750
|
+
});
|
|
4751
|
+
/** Понравившиеся посты пользователя: `/api/posts/user/{user}/liked`. */
|
|
4752
|
+
#liked = this.paginated({
|
|
4753
|
+
path: (p) => `/api/posts/user/${encodePathSegment(p.user, "user")}/liked`,
|
|
4754
|
+
query: (p) => ({ limit: p.limit }),
|
|
4755
|
+
start: cursorStart,
|
|
4756
|
+
read: (body) => readCursorPage(body, "posts"),
|
|
4757
|
+
mode: PaginationMode.Cursor
|
|
4758
|
+
});
|
|
4759
|
+
/** Комментарии к посту: курсор лежит рядом со списком, поэтому свой reader. */
|
|
4760
|
+
#comments = this.paginated({
|
|
4761
|
+
path: (p) => `/api/posts/${encodePathSegment(p.postId, "postId")}/comments`,
|
|
4762
|
+
query: (p) => ({ limit: p.limit, sort: p.sort }),
|
|
4763
|
+
start: cursorStart,
|
|
4764
|
+
read: (body) => readFlatCursorPage(body, "comments"),
|
|
4765
|
+
mode: PaginationMode.Cursor
|
|
4766
|
+
});
|
|
4432
4767
|
constructor(http, deps) {
|
|
4433
4768
|
super(http);
|
|
4434
4769
|
this.#uploadFiles = deps.uploadFiles;
|
|
@@ -4442,14 +4777,8 @@ var PostsResource = class extends BaseResource {
|
|
|
4442
4777
|
* const next = await itd.posts.list({ tab: FeedTab.Following, cursor: page.nextCursor ?? undefined });
|
|
4443
4778
|
* ```
|
|
4444
4779
|
*/
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
method: "GET",
|
|
4448
|
-
path: "/api/posts",
|
|
4449
|
-
query: { tab: params.tab, limit: params.limit, cursor: params.cursor },
|
|
4450
|
-
...this.requestOptions(params)
|
|
4451
|
-
});
|
|
4452
|
-
return readCursorPage(body, "posts");
|
|
4780
|
+
list(params = {}) {
|
|
4781
|
+
return this.#feed.list(params);
|
|
4453
4782
|
}
|
|
4454
4783
|
/**
|
|
4455
4784
|
* Перебирает ленту, сама подставляя курсоры.
|
|
@@ -4462,19 +4791,7 @@ var PostsResource = class extends BaseResource {
|
|
|
4462
4791
|
* ```
|
|
4463
4792
|
*/
|
|
4464
4793
|
iterate(params = {}) {
|
|
4465
|
-
return this.
|
|
4466
|
-
PaginationMode.Cursor,
|
|
4467
|
-
async (state) => {
|
|
4468
|
-
const body = await this.http.request({
|
|
4469
|
-
method: "GET",
|
|
4470
|
-
path: "/api/posts",
|
|
4471
|
-
query: withPageState({ tab: params.tab, limit: params.limit }, state),
|
|
4472
|
-
...this.requestOptions(params)
|
|
4473
|
-
});
|
|
4474
|
-
return readCursorPage(body, "posts");
|
|
4475
|
-
},
|
|
4476
|
-
{ ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
|
|
4477
|
-
);
|
|
4794
|
+
return this.#feed.iterate(params);
|
|
4478
4795
|
}
|
|
4479
4796
|
/**
|
|
4480
4797
|
* Публикует пост.
|
|
@@ -4628,66 +4945,20 @@ var PostsResource = class extends BaseResource {
|
|
|
4628
4945
|
*
|
|
4629
4946
|
* Принимает и UUID, и имя пользователя.
|
|
4630
4947
|
*/
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
method: "GET",
|
|
4634
|
-
path: `/api/posts/user/${encodePathSegment(user, "user")}`,
|
|
4635
|
-
query: {
|
|
4636
|
-
limit: params.limit,
|
|
4637
|
-
cursor: params.cursor,
|
|
4638
|
-
sort: params.sort,
|
|
4639
|
-
pinnedPostId: params.pinnedPostId
|
|
4640
|
-
},
|
|
4641
|
-
...this.requestOptions(params)
|
|
4642
|
-
});
|
|
4643
|
-
return readCursorPage(body, "posts");
|
|
4948
|
+
byUser(user, params = {}) {
|
|
4949
|
+
return this.#wall.list({ ...params, user });
|
|
4644
4950
|
}
|
|
4645
4951
|
/** Перебирает стену пользователя. Что именно в неё входит — см. {@link byUser}. */
|
|
4646
4952
|
iterateByUser(user, params = {}) {
|
|
4647
|
-
|
|
4648
|
-
return this.paginate(
|
|
4649
|
-
PaginationMode.Cursor,
|
|
4650
|
-
async (state) => {
|
|
4651
|
-
const body = await this.http.request({
|
|
4652
|
-
method: "GET",
|
|
4653
|
-
path,
|
|
4654
|
-
query: withPageState(
|
|
4655
|
-
{ limit: params.limit, sort: params.sort, pinnedPostId: params.pinnedPostId },
|
|
4656
|
-
state
|
|
4657
|
-
),
|
|
4658
|
-
...this.requestOptions(params)
|
|
4659
|
-
});
|
|
4660
|
-
return readCursorPage(body, "posts");
|
|
4661
|
-
},
|
|
4662
|
-
{ ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
|
|
4663
|
-
);
|
|
4953
|
+
return this.#wall.iterate({ ...params, user });
|
|
4664
4954
|
}
|
|
4665
4955
|
/** Загружает страницу постов, которые пользователь отметил реакцией. */
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
method: "GET",
|
|
4669
|
-
path: `/api/posts/user/${encodePathSegment(user, "user")}/liked`,
|
|
4670
|
-
query: { limit: params.limit, cursor: params.cursor },
|
|
4671
|
-
...this.requestOptions(params)
|
|
4672
|
-
});
|
|
4673
|
-
return readCursorPage(body, "posts");
|
|
4956
|
+
likedByUser(user, params = {}) {
|
|
4957
|
+
return this.#liked.list({ ...params, user });
|
|
4674
4958
|
}
|
|
4675
4959
|
/** Перебирает посты, которые пользователь отметил реакцией. */
|
|
4676
4960
|
iterateLikedByUser(user, params = {}) {
|
|
4677
|
-
|
|
4678
|
-
return this.paginate(
|
|
4679
|
-
PaginationMode.Cursor,
|
|
4680
|
-
async (state) => {
|
|
4681
|
-
const body = await this.http.request({
|
|
4682
|
-
method: "GET",
|
|
4683
|
-
path,
|
|
4684
|
-
query: withPageState({ limit: params.limit }, state),
|
|
4685
|
-
...this.requestOptions(params)
|
|
4686
|
-
});
|
|
4687
|
-
return readCursorPage(body, "posts");
|
|
4688
|
-
},
|
|
4689
|
-
{ ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
|
|
4690
|
-
);
|
|
4961
|
+
return this.#liked.iterate({ ...params, user });
|
|
4691
4962
|
}
|
|
4692
4963
|
/**
|
|
4693
4964
|
* Загружает страницу комментариев к посту.
|
|
@@ -4695,31 +4966,12 @@ var PostsResource = class extends BaseResource {
|
|
|
4695
4966
|
* У этого эндпоинта курсор и признак продолжения лежат рядом со списком, а не внутри
|
|
4696
4967
|
* объекта `pagination`, как у остальных, — разница скрыта внутри.
|
|
4697
4968
|
*/
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
method: "GET",
|
|
4701
|
-
path: `/api/posts/${encodePathSegment(postId, "postId")}/comments`,
|
|
4702
|
-
query: { limit: params.limit, cursor: params.cursor, sort: params.sort },
|
|
4703
|
-
...this.requestOptions(params)
|
|
4704
|
-
});
|
|
4705
|
-
return readFlatCursorPage(body, "comments");
|
|
4969
|
+
comments(postId, params = {}) {
|
|
4970
|
+
return this.#comments.list({ ...params, postId });
|
|
4706
4971
|
}
|
|
4707
4972
|
/** Перебирает комментарии к посту. */
|
|
4708
4973
|
iterateComments(postId, params = {}) {
|
|
4709
|
-
|
|
4710
|
-
return this.paginate(
|
|
4711
|
-
PaginationMode.Cursor,
|
|
4712
|
-
async (state) => {
|
|
4713
|
-
const body = await this.http.request({
|
|
4714
|
-
method: "GET",
|
|
4715
|
-
path,
|
|
4716
|
-
query: withPageState({ limit: params.limit, sort: params.sort }, state),
|
|
4717
|
-
...this.requestOptions(params)
|
|
4718
|
-
});
|
|
4719
|
-
return readFlatCursorPage(body, "comments");
|
|
4720
|
-
},
|
|
4721
|
-
{ ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
|
|
4722
|
-
);
|
|
4974
|
+
return this.#comments.iterate({ ...params, postId });
|
|
4723
4975
|
}
|
|
4724
4976
|
/**
|
|
4725
4977
|
* Комментирует пост.
|
|
@@ -4764,6 +5016,122 @@ var PostsResource = class extends BaseResource {
|
|
|
4764
5016
|
}
|
|
4765
5017
|
};
|
|
4766
5018
|
|
|
5019
|
+
// src/resources/reports.ts
|
|
5020
|
+
var ReportsResource = class extends BaseResource {
|
|
5021
|
+
/**
|
|
5022
|
+
* Отправляет жалобу.
|
|
5023
|
+
*
|
|
5024
|
+
* Повторная жалоба на тот же объект отклоняется сервером с сообщением
|
|
5025
|
+
* «Вы уже отправляли жалобу на этот контент».
|
|
5026
|
+
*
|
|
5027
|
+
* @example
|
|
5028
|
+
* ```ts
|
|
5029
|
+
* await itd.reports.create(report.post(postId).reason('spam'));
|
|
5030
|
+
* await itd.reports.create({ targetType: 'user', targetId, reason: 'fraud' });
|
|
5031
|
+
* ```
|
|
5032
|
+
*/
|
|
5033
|
+
create(input, options = {}) {
|
|
5034
|
+
const data = resolveReport(input);
|
|
5035
|
+
return this.http.request({
|
|
5036
|
+
method: "POST",
|
|
5037
|
+
path: "/api/reports",
|
|
5038
|
+
body: data,
|
|
5039
|
+
...this.requestOptions(options)
|
|
5040
|
+
});
|
|
5041
|
+
}
|
|
5042
|
+
};
|
|
5043
|
+
|
|
5044
|
+
// src/resources/search.ts
|
|
5045
|
+
var SearchResource = class extends BaseResource {
|
|
5046
|
+
/**
|
|
5047
|
+
* Ищет пользователей и хэштеги одним запросом.
|
|
5048
|
+
*
|
|
5049
|
+
* @example
|
|
5050
|
+
* ```ts
|
|
5051
|
+
* const { users, hashtags } = await itd.search.all('арт');
|
|
5052
|
+
* ```
|
|
5053
|
+
*/
|
|
5054
|
+
async all(query, options = {}) {
|
|
5055
|
+
const body = await this.http.request({
|
|
5056
|
+
method: "GET",
|
|
5057
|
+
path: "/api/search",
|
|
5058
|
+
query: { q: query },
|
|
5059
|
+
...this.requestOptions(options)
|
|
5060
|
+
});
|
|
5061
|
+
return {
|
|
5062
|
+
users: pickArray(body, "users"),
|
|
5063
|
+
hashtags: pickArray(body, "hashtags")
|
|
5064
|
+
};
|
|
5065
|
+
}
|
|
5066
|
+
};
|
|
5067
|
+
|
|
5068
|
+
// src/resources/subscription.ts
|
|
5069
|
+
var SubscriptionResource = class extends BaseResource {
|
|
5070
|
+
/** Загружает состояние подписки и её цену. */
|
|
5071
|
+
status(options = {}) {
|
|
5072
|
+
return this.http.request({
|
|
5073
|
+
method: "GET",
|
|
5074
|
+
// Завершающий слэш обязателен.
|
|
5075
|
+
path: "/api/v1/subscription/",
|
|
5076
|
+
...this.requestOptions(options)
|
|
5077
|
+
});
|
|
5078
|
+
}
|
|
5079
|
+
/**
|
|
5080
|
+
* Запускает оплату подписки.
|
|
5081
|
+
*
|
|
5082
|
+
* Форма ответа в документации API не описана, поэтому тип результата не уточняется.
|
|
5083
|
+
*/
|
|
5084
|
+
pay(options = {}) {
|
|
5085
|
+
return this.http.request({
|
|
5086
|
+
method: "POST",
|
|
5087
|
+
path: "/api/v1/subscription/pay",
|
|
5088
|
+
...this.requestOptions(options)
|
|
5089
|
+
});
|
|
5090
|
+
}
|
|
5091
|
+
/** Включает или отключает автопродление. */
|
|
5092
|
+
setAutoRenewal(enabled, options = {}) {
|
|
5093
|
+
return this.http.request({
|
|
5094
|
+
method: "POST",
|
|
5095
|
+
path: "/api/v1/subscription/auto-renewal",
|
|
5096
|
+
body: { enabled },
|
|
5097
|
+
...this.requestOptions(options)
|
|
5098
|
+
});
|
|
5099
|
+
}
|
|
5100
|
+
/** Запускает привязку карты. */
|
|
5101
|
+
bindCard(options = {}) {
|
|
5102
|
+
return this.http.request({
|
|
5103
|
+
method: "POST",
|
|
5104
|
+
path: "/api/v1/subscription/bind-card",
|
|
5105
|
+
...this.requestOptions(options)
|
|
5106
|
+
});
|
|
5107
|
+
}
|
|
5108
|
+
/** Загружает список способов оплаты. Пустой массив, если карт нет. */
|
|
5109
|
+
async methods(options = {}) {
|
|
5110
|
+
const body = await this.http.request({
|
|
5111
|
+
method: "GET",
|
|
5112
|
+
path: "/api/v1/subscription/methods",
|
|
5113
|
+
...this.requestOptions(options)
|
|
5114
|
+
});
|
|
5115
|
+
return Array.isArray(body) ? body : [];
|
|
5116
|
+
}
|
|
5117
|
+
/** Делает способ оплаты основным. */
|
|
5118
|
+
setDefaultMethod(methodId, options = {}) {
|
|
5119
|
+
return this.http.request({
|
|
5120
|
+
method: "POST",
|
|
5121
|
+
path: `/api/v1/subscription/methods/${encodePathSegment(methodId, "methodId")}/default`,
|
|
5122
|
+
...this.requestOptions(options)
|
|
5123
|
+
});
|
|
5124
|
+
}
|
|
5125
|
+
/** Удаляет способ оплаты. */
|
|
5126
|
+
removeMethod(methodId, options = {}) {
|
|
5127
|
+
return this.http.request({
|
|
5128
|
+
method: "DELETE",
|
|
5129
|
+
path: `/api/v1/subscription/methods/${encodePathSegment(methodId, "methodId")}`,
|
|
5130
|
+
...this.requestOptions(options)
|
|
5131
|
+
});
|
|
5132
|
+
}
|
|
5133
|
+
};
|
|
5134
|
+
|
|
4767
5135
|
// src/resources/telemetry.ts
|
|
4768
5136
|
var TelemetryResource = class extends BaseResource {
|
|
4769
5137
|
/** Идентификатор сессии телеметрии, общий для всех событий этого объекта. */
|
|
@@ -4830,6 +5198,21 @@ var TelemetryResource = class extends BaseResource {
|
|
|
4830
5198
|
|
|
4831
5199
|
// src/resources/users.ts
|
|
4832
5200
|
var UsersResource = class extends BaseResource {
|
|
5201
|
+
/**
|
|
5202
|
+
* Списки пользователей: подписчики, подписки, заблокированные.
|
|
5203
|
+
*
|
|
5204
|
+
* Путь приходит в параметрах — так один описатель обслуживает все три эндпоинта. Имена
|
|
5205
|
+
* полей перечислены с запасом: списки приходят под `users`, но альтернативное имя ничего
|
|
5206
|
+
* не стоит и спасает, если эндпоинт назовёт список по-своему. `page` уходит в запрос, хотя
|
|
5207
|
+
* сервер его сейчас не читает (см. {@link followers}): когда починят — заработает само.
|
|
5208
|
+
*/
|
|
5209
|
+
#userList = this.paginated({
|
|
5210
|
+
path: (p) => p.path,
|
|
5211
|
+
query: (p) => ({ limit: p.limit }),
|
|
5212
|
+
start: (p) => p.page !== void 0 ? { page: p.page } : {},
|
|
5213
|
+
read: (body) => readPagedPage(body, "users", "followers", "following", "blocked"),
|
|
5214
|
+
mode: PaginationMode.Page
|
|
5215
|
+
});
|
|
4833
5216
|
/** Загружает свой профиль — с подпиской и признаком подтверждённого телефона. */
|
|
4834
5217
|
me(options = {}) {
|
|
4835
5218
|
return this.http.request({
|
|
@@ -5075,48 +5458,47 @@ var UsersResource = class extends BaseResource {
|
|
|
5075
5458
|
...this.requestOptions(options)
|
|
5076
5459
|
});
|
|
5077
5460
|
}
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5461
|
+
#userPage(path, params) {
|
|
5462
|
+
return this.#userList.list({ ...params, path });
|
|
5463
|
+
}
|
|
5464
|
+
#userPaginator(path, params) {
|
|
5465
|
+
return this.#userList.iterate({ ...params, path });
|
|
5466
|
+
}
|
|
5467
|
+
};
|
|
5468
|
+
|
|
5469
|
+
// src/resources/verification.ts
|
|
5470
|
+
var VerificationResource = class extends BaseResource {
|
|
5471
|
+
/** Загружает статус заявки. Значение `none` означает, что заявка не подавалась. */
|
|
5472
|
+
status(options = {}) {
|
|
5473
|
+
return this.http.request({
|
|
5090
5474
|
method: "GET",
|
|
5091
|
-
path,
|
|
5092
|
-
|
|
5093
|
-
...this.requestOptions(params)
|
|
5475
|
+
path: "/api/verification/status",
|
|
5476
|
+
...this.requestOptions(options)
|
|
5094
5477
|
});
|
|
5095
|
-
return readPagedPage(body, "users", "followers", "following", "blocked");
|
|
5096
5478
|
}
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5479
|
+
/** Подаёт заявку на верификацию с видео. */
|
|
5480
|
+
submit(videoUrl, options = {}) {
|
|
5481
|
+
return this.http.request({
|
|
5482
|
+
method: "POST",
|
|
5483
|
+
path: "/api/verification/submit",
|
|
5484
|
+
body: { videoUrl },
|
|
5485
|
+
...this.requestOptions(options)
|
|
5100
5486
|
});
|
|
5101
5487
|
}
|
|
5102
|
-
#userPaginator(path, params) {
|
|
5103
|
-
return this.paginate(
|
|
5104
|
-
PaginationMode.Page,
|
|
5105
|
-
(state) => this.#loadUserPage(path, params, state),
|
|
5106
|
-
// Без `start` перебор начинался бы с первой страницы, молча игнорируя `page`.
|
|
5107
|
-
{ ...params, ...params.page !== void 0 ? { start: { page: params.page } } : {} }
|
|
5108
|
-
);
|
|
5109
|
-
}
|
|
5110
5488
|
};
|
|
5111
5489
|
|
|
5112
5490
|
// src/client.ts
|
|
5113
|
-
var ItdClient = class {
|
|
5491
|
+
var ItdClient = class _ItdClient {
|
|
5114
5492
|
#config;
|
|
5115
5493
|
#http;
|
|
5494
|
+
#transport;
|
|
5116
5495
|
#authManager;
|
|
5117
5496
|
#jar;
|
|
5118
|
-
#
|
|
5497
|
+
#queues;
|
|
5119
5498
|
#plugins = new PluginRegistry();
|
|
5499
|
+
#services;
|
|
5500
|
+
/** Порождённые потоки уведомлений — чтобы `close()` мог закрыть их разом. */
|
|
5501
|
+
#streams = /* @__PURE__ */ new Set();
|
|
5120
5502
|
/** Авторизация, сессии и пароли. */
|
|
5121
5503
|
auth;
|
|
5122
5504
|
/** Профили, подписки, блокировки, приватность. */
|
|
@@ -5147,26 +5529,65 @@ var ItdClient = class {
|
|
|
5147
5529
|
* @experimental Недокументированные эндпоинты. Библиотека никогда не отправляет их сама.
|
|
5148
5530
|
*/
|
|
5149
5531
|
telemetry;
|
|
5150
|
-
constructor(options = {}) {
|
|
5151
|
-
|
|
5532
|
+
constructor(options = {}, internals = {}) {
|
|
5533
|
+
const config = resolveConfig(options);
|
|
5534
|
+
this.#config = config;
|
|
5152
5535
|
this.#jar = new CookieJar();
|
|
5153
|
-
this.#
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
this.#
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5536
|
+
this.#services = new ServiceRegistry(config.baseUrl);
|
|
5537
|
+
for (const service of BUILT_IN_SERVICES) this.#services.define(service);
|
|
5538
|
+
for (const service of config.services) this.#services.define(service);
|
|
5539
|
+
const queues = config.rateLimit ? new RequestQueuePool(config.rateLimit) : void 0;
|
|
5540
|
+
this.#queues = queues;
|
|
5541
|
+
let authManager;
|
|
5542
|
+
const transport = new Transport(config, {
|
|
5543
|
+
cookies: config.useCookieJar ? this.#jar : void 0,
|
|
5544
|
+
getDeviceId: () => authManager.getDeviceId(),
|
|
5545
|
+
onRateLimit: queues && config.rateLimit?.respectHeaders ? (limit, remaining, request) => this.#throttleByHeaders(limit, remaining, request) : void 0
|
|
5546
|
+
});
|
|
5547
|
+
this.#transport = transport;
|
|
5548
|
+
const pluginsLayer = createPluginsMiddleware(this.#plugins);
|
|
5549
|
+
const retriesLayer = createRetryMiddleware({
|
|
5550
|
+
retry: config.retry,
|
|
5551
|
+
rateLimitDelays: config.rateLimit?.retryDelays ?? [],
|
|
5552
|
+
pauseQueue: queues ? (ms, request) => queues.for(request.service).pause(ms) : void 0,
|
|
5553
|
+
hooks: config.hooks,
|
|
5554
|
+
logger: config.logger,
|
|
5555
|
+
buildUrl: (request) => transport.buildUrl(request)
|
|
5168
5556
|
});
|
|
5169
|
-
|
|
5557
|
+
const authRetry = config.retry ? {
|
|
5558
|
+
attempts: config.retry.attempts,
|
|
5559
|
+
baseDelay: config.retry.baseDelay,
|
|
5560
|
+
maxDelay: config.retry.maxDelay,
|
|
5561
|
+
jitter: config.retry.jitter,
|
|
5562
|
+
retryWrites: true,
|
|
5563
|
+
...config.retry.shouldRetry ? { shouldRetry: config.retry.shouldRetry } : {}
|
|
5564
|
+
} : void 0;
|
|
5565
|
+
const authPipeline = composePipeline([pluginsLayer, retriesLayer], transport.send);
|
|
5566
|
+
const authHandler = (request) => authRetry && request.retry === void 0 ? authPipeline({ ...request, retry: authRetry }) : authPipeline(request);
|
|
5567
|
+
authManager = new AuthManager(config, authHandler, this.#jar);
|
|
5568
|
+
this.#authManager = authManager;
|
|
5569
|
+
const middlewares = [];
|
|
5570
|
+
if (queues) {
|
|
5571
|
+
middlewares.push(
|
|
5572
|
+
createQueueMiddleware((request, task) => queues.for(request.service).schedule(task))
|
|
5573
|
+
);
|
|
5574
|
+
}
|
|
5575
|
+
middlewares.push(pluginsLayer);
|
|
5576
|
+
middlewares.push(createServicesMiddleware(this.#services));
|
|
5577
|
+
middlewares.push(retriesLayer);
|
|
5578
|
+
middlewares.push(
|
|
5579
|
+
createAuthMiddleware({
|
|
5580
|
+
getAuthHeaders: () => authManager.getAuthHeaders(),
|
|
5581
|
+
onUnauthorized: () => authManager.onUnauthorized(),
|
|
5582
|
+
autoRefresh: config.autoRefresh
|
|
5583
|
+
})
|
|
5584
|
+
);
|
|
5585
|
+
const handler = composePipeline(middlewares, transport.send);
|
|
5586
|
+
this.#http = new HttpClient({ handler, plugins: this.#plugins, baseUrl: config.baseUrl });
|
|
5587
|
+
this.files = new FilesResource(
|
|
5588
|
+
this.#http,
|
|
5589
|
+
internals.fileReader ? { readFile: internals.fileReader } : {}
|
|
5590
|
+
);
|
|
5170
5591
|
const uploadFiles = (files, requestOptions) => this.files.uploadMany(files, requestOptions ?? {});
|
|
5171
5592
|
this.auth = new AuthResource(this.#http, { auth: this.#authManager });
|
|
5172
5593
|
this.users = new UsersResource(this.#http);
|
|
@@ -5210,7 +5631,7 @@ var ItdClient = class {
|
|
|
5210
5631
|
*
|
|
5211
5632
|
* @example
|
|
5212
5633
|
* ```ts
|
|
5213
|
-
* import { crypt } from 'itd-api
|
|
5634
|
+
* import { crypt } from '@itd-api/crypto';
|
|
5214
5635
|
*
|
|
5215
5636
|
* itd.use(crypt());
|
|
5216
5637
|
* await itd.posts.create({ content: 'секрет' }, { encrypt: 'invis' });
|
|
@@ -5220,6 +5641,41 @@ var ItdClient = class {
|
|
|
5220
5641
|
this.#plugins.add(plugin, { baseUrl: this.#config.baseUrl, logger: this.#config.logger });
|
|
5221
5642
|
return this;
|
|
5222
5643
|
}
|
|
5644
|
+
/**
|
|
5645
|
+
* Регистрирует сервис платформы — домен, отличный от основного.
|
|
5646
|
+
*
|
|
5647
|
+
* Запросы с `{ service: 'имя' }` уходят на его хост с его заголовками. То же самое умеет
|
|
5648
|
+
* опция `services` конструктора. Занятое имя не переопределяется — ни своё, ни встроенное:
|
|
5649
|
+
* разовому запросу хост задаётся полем `baseUrl`.
|
|
5650
|
+
*
|
|
5651
|
+
* Заголовок авторизации по умолчанию уходит только своим — домену клиента и его
|
|
5652
|
+
* поддоменам. Стороннему хосту токен нужно разрешить явно: `auth: true`.
|
|
5653
|
+
*
|
|
5654
|
+
* @throws {ItdConfigError} если определение неверно или имя уже занято
|
|
5655
|
+
*
|
|
5656
|
+
* @example Сервис платформы на поддомене — токен уходит сам
|
|
5657
|
+
* ```ts
|
|
5658
|
+
* itd.defineService({
|
|
5659
|
+
* name: 'pb',
|
|
5660
|
+
* baseUrl: 'https://pbapi.xn--d1ah4a.com',
|
|
5661
|
+
* headers: { Referer: 'https://pixel.xn--d1ah4a.com/' },
|
|
5662
|
+
* });
|
|
5663
|
+
*
|
|
5664
|
+
* await itd.request({ method: 'GET', service: 'pb', path: '/api/pixel-info' });
|
|
5665
|
+
* ```
|
|
5666
|
+
*/
|
|
5667
|
+
defineService(definition) {
|
|
5668
|
+
this.#services.define(definition);
|
|
5669
|
+
return this;
|
|
5670
|
+
}
|
|
5671
|
+
/**
|
|
5672
|
+
* Базовый URL зарегистрированного сервиса.
|
|
5673
|
+
*
|
|
5674
|
+
* @throws {ItdConfigError} если сервис не зарегистрирован
|
|
5675
|
+
*/
|
|
5676
|
+
serviceBaseUrl(name) {
|
|
5677
|
+
return this.#services.resolveBaseUrl(name);
|
|
5678
|
+
}
|
|
5223
5679
|
/**
|
|
5224
5680
|
* Подписывается на события авторизации.
|
|
5225
5681
|
*
|
|
@@ -5256,17 +5712,52 @@ var ItdClient = class {
|
|
|
5256
5712
|
* ```
|
|
5257
5713
|
*/
|
|
5258
5714
|
realtime(options = {}) {
|
|
5259
|
-
|
|
5715
|
+
let stream;
|
|
5716
|
+
stream = new ItdRealtime(
|
|
5260
5717
|
{
|
|
5261
5718
|
baseUrl: this.#config.baseUrl,
|
|
5262
5719
|
fetch: this.#config.fetch,
|
|
5720
|
+
baseHeaders: (url) => this.#transport.platformHeaders(url),
|
|
5263
5721
|
getToken: () => this.#authManager.getAccessToken(),
|
|
5264
5722
|
refresh: () => this.#authManager.onUnauthorized(),
|
|
5265
5723
|
fetchUnreadCount: () => this.notifications.count(),
|
|
5724
|
+
onClose: () => this.#streams.delete(stream),
|
|
5266
5725
|
logger: this.#config.logger
|
|
5267
5726
|
},
|
|
5268
5727
|
options
|
|
5269
5728
|
);
|
|
5729
|
+
this.#streams.add(stream);
|
|
5730
|
+
return stream;
|
|
5731
|
+
}
|
|
5732
|
+
/**
|
|
5733
|
+
* Освобождает ресурсы клиента: останавливает очередь запросов (снимает отложенные паузы)
|
|
5734
|
+
* и закрывает все потоки уведомлений, созданные через {@link realtime}.
|
|
5735
|
+
*
|
|
5736
|
+
* После вызова клиентом можно пользоваться снова — новые запросы поднимут всё заново,
|
|
5737
|
+
* но уже созданные потоки останутся закрытыми.
|
|
5738
|
+
*
|
|
5739
|
+
* @example
|
|
5740
|
+
* ```ts
|
|
5741
|
+
* await using itd = new ItdClient({ auth: token });
|
|
5742
|
+
* // …работа…
|
|
5743
|
+
* // close() вызовется сам на выходе из блока
|
|
5744
|
+
* ```
|
|
5745
|
+
*/
|
|
5746
|
+
async close() {
|
|
5747
|
+
for (const stream of this.#streams) stream.disconnect();
|
|
5748
|
+
this.#streams.clear();
|
|
5749
|
+
this.#queues?.stop();
|
|
5750
|
+
}
|
|
5751
|
+
/** Позволяет использовать клиент с `await using`. */
|
|
5752
|
+
[Symbol.asyncDispose]() {
|
|
5753
|
+
return this.close();
|
|
5754
|
+
}
|
|
5755
|
+
static {
|
|
5756
|
+
if (typeof Symbol.asyncDispose !== "symbol") {
|
|
5757
|
+
const prototype = _ItdClient.prototype;
|
|
5758
|
+
prototype[/* @__PURE__ */ Symbol.for("Symbol.asyncDispose")] = prototype.undefined;
|
|
5759
|
+
delete prototype.undefined;
|
|
5760
|
+
}
|
|
5270
5761
|
}
|
|
5271
5762
|
/** Текущая сессия целиком — чтобы сохранить её самостоятельно. */
|
|
5272
5763
|
getSession() {
|
|
@@ -5276,16 +5767,6 @@ var ItdClient = class {
|
|
|
5276
5767
|
setSession(session) {
|
|
5277
5768
|
return this.#authManager.setSession(session);
|
|
5278
5769
|
}
|
|
5279
|
-
/**
|
|
5280
|
-
* Подключает чтение файлов с диска.
|
|
5281
|
-
*
|
|
5282
|
-
* Вызывается из `itd-api/node`; напрямую обычно не нужно.
|
|
5283
|
-
*
|
|
5284
|
-
* @internal
|
|
5285
|
-
*/
|
|
5286
|
-
setFileReader(readFile) {
|
|
5287
|
-
this.files.setFileReader(readFile);
|
|
5288
|
-
}
|
|
5289
5770
|
/**
|
|
5290
5771
|
* Придерживает очередь, когда лимит сервера исчерпан.
|
|
5291
5772
|
*
|
|
@@ -5297,43 +5778,15 @@ var ItdClient = class {
|
|
|
5297
5778
|
* Смысл этой паузы прежде всего в том, чтобы при работе в несколько потоков остальные
|
|
5298
5779
|
* запросы не улетели в стену все разом.
|
|
5299
5780
|
*/
|
|
5300
|
-
#throttleByHeaders(limit, remaining) {
|
|
5781
|
+
#throttleByHeaders(limit, remaining, request) {
|
|
5301
5782
|
if (remaining === void 0 || remaining > 0) return;
|
|
5302
5783
|
const first = this.#config.rateLimit?.retryDelays[0];
|
|
5303
5784
|
if (first === void 0) return;
|
|
5304
|
-
this.#
|
|
5785
|
+
this.#queues?.for(request.service).pause(first);
|
|
5305
5786
|
this.#config.logger?.debug(
|
|
5306
5787
|
`\u043B\u0438\u043C\u0438\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D (${remaining} \u0438\u0437 ${limit ?? "?"}), \u043E\u0447\u0435\u0440\u0435\u0434\u044C \u0436\u0434\u0451\u0442 ${first} \u043C\u0441`
|
|
5307
5788
|
);
|
|
5308
5789
|
}
|
|
5309
|
-
/**
|
|
5310
|
-
* Собирает планировщик повторов и связывает его с очередью.
|
|
5311
|
-
*
|
|
5312
|
-
* Ответ `429` обрабатывается отдельно от прочих ошибок. Причина в том, что сервер
|
|
5313
|
-
* не присылает `Retry-After` и не сообщает время сброса окна: экспоненциальный откат
|
|
5314
|
-
* в сотни миллисекунд здесь бесполезен, а окно измеряется десятками секунд. Вместо
|
|
5315
|
-
* расчёта берётся лестница пауз `rateLimit.retryDelays`, и она не зависит
|
|
5316
|
-
* от `retry.attempts`, у которого совсем другая задача.
|
|
5317
|
-
*
|
|
5318
|
-
* Пауза накладывается на всю очередь: иначе остальные запросы продолжат добивать API,
|
|
5319
|
-
* пока первый ждёт.
|
|
5320
|
-
*/
|
|
5321
|
-
#createRetryScheduler() {
|
|
5322
|
-
const retry = this.#config.retry;
|
|
5323
|
-
const scheduler = retry ? createRetryScheduler(retry) : void 0;
|
|
5324
|
-
const queue = this.#queue;
|
|
5325
|
-
const delays = this.#config.rateLimit?.retryDelays ?? [];
|
|
5326
|
-
return (error, attempt, method) => {
|
|
5327
|
-
if (isItdRateLimitError(error)) {
|
|
5328
|
-
const wait = error.retryAfter ?? delays[attempt - 1];
|
|
5329
|
-
if (wait === void 0) return void 0;
|
|
5330
|
-
queue?.pause(wait);
|
|
5331
|
-
this.#config.logger?.debug(`\u043B\u0438\u043C\u0438\u0442 \u0447\u0430\u0441\u0442\u043E\u0442\u044B, \u043F\u043E\u043F\u044B\u0442\u043A\u0430 ${attempt + 1} \u0447\u0435\u0440\u0435\u0437 ${wait} \u043C\u0441`);
|
|
5332
|
-
return wait;
|
|
5333
|
-
}
|
|
5334
|
-
return scheduler?.(error, attempt, method);
|
|
5335
|
-
};
|
|
5336
|
-
}
|
|
5337
5790
|
};
|
|
5338
5791
|
function createClient(options = {}) {
|
|
5339
5792
|
return new ItdClient(options);
|
|
@@ -5440,12 +5893,19 @@ function resolveNotificationUrl(notification) {
|
|
|
5440
5893
|
function isMyProfile(profile) {
|
|
5441
5894
|
return "subscription" in profile;
|
|
5442
5895
|
}
|
|
5896
|
+
var STATUS_WINDOW_DAYS = 90;
|
|
5443
5897
|
function toDate(value) {
|
|
5444
5898
|
if (!value) return null;
|
|
5445
5899
|
const date = new Date(value);
|
|
5446
5900
|
return Number.isFinite(date.getTime()) ? date : null;
|
|
5447
5901
|
}
|
|
5902
|
+
function statusDays(service) {
|
|
5903
|
+
return Array.from(
|
|
5904
|
+
{ length: STATUS_WINDOW_DAYS },
|
|
5905
|
+
(_, index) => service.days[String(index)] ?? null
|
|
5906
|
+
);
|
|
5907
|
+
}
|
|
5448
5908
|
|
|
5449
|
-
export { ALLOWED_MIME_TYPES, AUDIO_MIME_TYPES, AUTH_FLAG_COOKIE, AUTH_PATHS, AccessType, AttachmentType, CommentSort, DEFAULT_BASE_URL, DEFAULT_TIMEOUT, DEFAULT_USER_AGENT, DEVICE_ID_HEADER, DetectedRuntime, FeedTab, IMAGE_MIME_TYPES, InteractionType, ItdAbortError, ItdApiError, ItdApiErrorKind, ItdAuthError, ItdClient, ItdConfigError, ItdConflictError, ItdError, ItdErrorCode, ItdErrorKind, ItdForbiddenError, ItdNetworkError, ItdNotFoundError, ItdPhoneVerificationError, ItdRateLimitError, ItdRealtime, ItdServerError, ItdTimeoutError, ItdValidationError, LIBRARY_VERSION, LikesVisibility, LocalStorageTokenStorage, MAX_RECONNECT_ATTEMPTS, MemoryTokenStorage, NOTIFICATION_TYPE_ALIASES, NotificationType, OAuthProvider, PaginationMode, Paginator, RECONNECT_BACKOFF, RECONNECT_JITTER, REFRESH_COOKIE, REFRESH_COOKIE_PATH, RealtimeStatus, RealtimeTransportKind, ReportReason, ReportTargetType, RuntimeMode, STREAM_PATH, SignInStatus, SpanType, TURNSTILE_SITE_KEY, UnauthorizedStreamError, VIDEO_MIME_TYPES, ViewReason, ViewSource, WallAccess, canonicalNotificationType, comment, createClient, createTokenStorage, formatNotificationText, isBuilder, isItdApiError, isItdAuthError, isItdConflictError, isItdError, isItdForbiddenError, isItdNotFoundError, isItdPhoneVerificationError, isItdRateLimitError, isItdServerError, isItdValidationError, isKnownNotificationType, isMyProfile, normalizeNotification, poll, post, readNotificationEvent, readUnreadCountEvent, report, resolveNotificationUrl, toDate };
|
|
5450
|
-
//# sourceMappingURL=chunk-
|
|
5451
|
-
//# sourceMappingURL=chunk-
|
|
5909
|
+
export { ALLOWED_MIME_TYPES, AUDIO_MIME_TYPES, AUTH_FLAG_COOKIE, AUTH_PATHS, AccessType, AttachmentType, BUILT_IN_SERVICES, CommentSort, DEFAULT_BASE_URL, DEFAULT_STATUS_BASE_URL, DEFAULT_TIMEOUT, DEFAULT_UPLOAD_TIMEOUT, DEFAULT_USER_AGENT, DEVICE_ID_HEADER, DetectedRuntime, FeedTab, IMAGE_MIME_TYPES, IncidentKind, 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, REQUEST_OPTION_KEYS, RealtimeStatus, RealtimeTransportKind, ReportReason, ReportTargetType, RuntimeMode, STATUS_SERVICE, STREAM_PATH, ServiceRegistry, ServiceState, 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, mapPage, normalizeNotification, poll, post, readNotificationEvent, readUnreadCountEvent, report, resolveNotificationUrl, statusDays, toDate, utcStampToIso };
|
|
5910
|
+
//# sourceMappingURL=chunk-MYAU2WJU.js.map
|
|
5911
|
+
//# sourceMappingURL=chunk-MYAU2WJU.js.map
|