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