itd-api 0.2.0 → 0.3.0
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 +5 -4
- package/dist/index.cjs +652 -218
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +228 -42
- package/dist/index.d.ts +228 -42
- package/dist/index.js +649 -219
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -418,7 +418,7 @@ var AuthManager = class {
|
|
|
418
418
|
await this.#saveSession({
|
|
419
419
|
...this.#session ?? {},
|
|
420
420
|
accessToken,
|
|
421
|
-
obtainedAt:
|
|
421
|
+
obtainedAt: this.#config.clock.now()
|
|
422
422
|
});
|
|
423
423
|
this.#emitter.emit("tokens", { accessToken });
|
|
424
424
|
}
|
|
@@ -503,12 +503,12 @@ var AuthManager = class {
|
|
|
503
503
|
if (!auth) return null;
|
|
504
504
|
if (typeof auth === "string") return {
|
|
505
505
|
accessToken: auth,
|
|
506
|
-
obtainedAt:
|
|
506
|
+
obtainedAt: this.#config.clock.now()
|
|
507
507
|
};
|
|
508
508
|
if ("accessToken" in auth) return {
|
|
509
509
|
accessToken: auth.accessToken,
|
|
510
510
|
refreshToken: auth.refreshToken,
|
|
511
|
-
obtainedAt:
|
|
511
|
+
obtainedAt: this.#config.clock.now()
|
|
512
512
|
};
|
|
513
513
|
return null;
|
|
514
514
|
}
|
|
@@ -556,7 +556,7 @@ var AuthManager = class {
|
|
|
556
556
|
...this.#session ?? {},
|
|
557
557
|
accessToken,
|
|
558
558
|
...rotated ? { refreshToken: rotated } : {},
|
|
559
|
-
obtainedAt:
|
|
559
|
+
obtainedAt: this.#config.clock.now()
|
|
560
560
|
});
|
|
561
561
|
this.#emitter.emit("tokens", { accessToken });
|
|
562
562
|
return accessToken;
|
|
@@ -631,7 +631,7 @@ var AuthManager = class {
|
|
|
631
631
|
this.#transitionAuth(accessToken);
|
|
632
632
|
await this.#saveSession({
|
|
633
633
|
accessToken,
|
|
634
|
-
obtainedAt:
|
|
634
|
+
obtainedAt: this.#config.clock.now()
|
|
635
635
|
});
|
|
636
636
|
this.#emitter.emit("tokens", { accessToken });
|
|
637
637
|
this.#emitter.emit("signIn", { accessToken });
|
|
@@ -639,6 +639,16 @@ var AuthManager = class {
|
|
|
639
639
|
}
|
|
640
640
|
};
|
|
641
641
|
//#endregion
|
|
642
|
+
//#region src/core/clock.ts
|
|
643
|
+
/** Системные часы, используемые клиентом по умолчанию. */
|
|
644
|
+
const systemClock = Object.freeze({
|
|
645
|
+
now: () => Date.now(),
|
|
646
|
+
schedule(callback, delay) {
|
|
647
|
+
const timer = setTimeout(callback, delay);
|
|
648
|
+
return () => clearTimeout(timer);
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
//#endregion
|
|
642
652
|
//#region src/core/url.ts
|
|
643
653
|
/**
|
|
644
654
|
* Собирает строку запроса.
|
|
@@ -716,7 +726,7 @@ function normalizeBaseUrl(baseUrl) {
|
|
|
716
726
|
//#endregion
|
|
717
727
|
//#region src/core/version.ts
|
|
718
728
|
/** Версия библиотеки. Попадает в `User-Agent`. */
|
|
719
|
-
const LIBRARY_VERSION = "0.
|
|
729
|
+
const LIBRARY_VERSION = "0.3.0";
|
|
720
730
|
//#endregion
|
|
721
731
|
//#region src/core/config.ts
|
|
722
732
|
/** Базовый URL API итд.com. Домен записан в punycode: `итд.com`. */
|
|
@@ -944,6 +954,7 @@ function resolveConfig(options = {}) {
|
|
|
944
954
|
const mode = options.mode ?? RuntimeMode.Auto;
|
|
945
955
|
if (!Object.values(RuntimeMode).includes(mode)) throw new ItdConfigError(`mode должен быть одним из ${Object.values(RuntimeMode).join(", ")}, получено: ${mode}`);
|
|
946
956
|
const timeout = requirePositive(options.timeout ?? 3e4, "timeout");
|
|
957
|
+
if (options.clock !== void 0 && (typeof options.clock !== "object" || options.clock === null || typeof options.clock.now !== "function" || typeof options.clock.schedule !== "function")) throw new ItdConfigError("clock должен предоставлять методы now() и schedule()");
|
|
947
958
|
requireOptionalBoolean(options.autoRefresh, "autoRefresh");
|
|
948
959
|
requireOptionalBoolean(options.reloginOnRefreshFailure, "reloginOnRefreshFailure");
|
|
949
960
|
if (options.userAgent !== void 0 && options.userAgent !== false && typeof options.userAgent !== "string") throw new ItdConfigError("userAgent должен быть строкой или false");
|
|
@@ -956,6 +967,7 @@ function resolveConfig(options = {}) {
|
|
|
956
967
|
autoRefresh: options.autoRefresh ?? true,
|
|
957
968
|
reloginOnRefreshFailure: options.reloginOnRefreshFailure ?? true,
|
|
958
969
|
fetch: resolveFetch(options.fetch),
|
|
970
|
+
clock: options.clock ?? systemClock,
|
|
959
971
|
timeout,
|
|
960
972
|
retry: resolveRetry(options.retry),
|
|
961
973
|
rateLimit: resolveRateLimit(options.rateLimit),
|
|
@@ -1466,16 +1478,16 @@ function createRetryScheduler(options, random = Math.random) {
|
|
|
1466
1478
|
//#endregion
|
|
1467
1479
|
//#region src/core/middleware.ts
|
|
1468
1480
|
/** Ожидание повтора, которое уважает отмену запроса. */
|
|
1469
|
-
function sleep(ms, signal) {
|
|
1470
|
-
if (!signal) return new Promise((resolve) =>
|
|
1481
|
+
function sleep(clock, ms, signal) {
|
|
1482
|
+
if (!signal) return new Promise((resolve) => clock.schedule(resolve, ms));
|
|
1471
1483
|
if (signal.aborted) return Promise.reject(new ItdAbortError("Запрос отменён во время ожидания повтора"));
|
|
1472
1484
|
return new Promise((resolve, reject) => {
|
|
1473
|
-
const
|
|
1485
|
+
const cancel = clock.schedule(() => {
|
|
1474
1486
|
signal.removeEventListener("abort", onAbort);
|
|
1475
1487
|
resolve();
|
|
1476
1488
|
}, ms);
|
|
1477
1489
|
const onAbort = () => {
|
|
1478
|
-
|
|
1490
|
+
cancel();
|
|
1479
1491
|
reject(new ItdAbortError("Запрос отменён во время ожидания повтора"));
|
|
1480
1492
|
};
|
|
1481
1493
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
@@ -1619,7 +1631,7 @@ function createRetryMiddleware(deps) {
|
|
|
1619
1631
|
delay
|
|
1620
1632
|
}, request);
|
|
1621
1633
|
deps.logger?.debug(`повтор ${method} ${request.path}, попытка ${attempt + 1} через ${delay} мс`);
|
|
1622
|
-
await sleep(delay, request.signal);
|
|
1634
|
+
await sleep(deps.clock ?? systemClock, delay, request.signal);
|
|
1623
1635
|
}
|
|
1624
1636
|
};
|
|
1625
1637
|
}
|
|
@@ -1648,10 +1660,12 @@ var RequestQueue = class {
|
|
|
1648
1660
|
#active = 0;
|
|
1649
1661
|
/** Момент, раньше которого следующий запрос стартовать не должен. */
|
|
1650
1662
|
#nextSlot = 0;
|
|
1651
|
-
#
|
|
1652
|
-
|
|
1663
|
+
#clock;
|
|
1664
|
+
#cancelTimer;
|
|
1665
|
+
constructor(options, clock = systemClock) {
|
|
1653
1666
|
this.#concurrency = options.concurrency;
|
|
1654
1667
|
this.#minGap = options.rps ? 1e3 / options.rps : 0;
|
|
1668
|
+
this.#clock = clock;
|
|
1655
1669
|
}
|
|
1656
1670
|
/** Сколько задач выполняется прямо сейчас. */
|
|
1657
1671
|
get active() {
|
|
@@ -1703,9 +1717,9 @@ var RequestQueue = class {
|
|
|
1703
1717
|
* ошибкой `ItdAbortError`. Уже выполняющиеся задачи доводятся до конца.
|
|
1704
1718
|
*/
|
|
1705
1719
|
stop() {
|
|
1706
|
-
if (this.#
|
|
1707
|
-
|
|
1708
|
-
this.#
|
|
1720
|
+
if (this.#cancelTimer) {
|
|
1721
|
+
this.#cancelTimer();
|
|
1722
|
+
this.#cancelTimer = void 0;
|
|
1709
1723
|
}
|
|
1710
1724
|
this.#nextSlot = 0;
|
|
1711
1725
|
const pending = this.#waiting.splice(0, this.#waiting.length);
|
|
@@ -1719,23 +1733,23 @@ var RequestQueue = class {
|
|
|
1719
1733
|
*/
|
|
1720
1734
|
pause(ms) {
|
|
1721
1735
|
if (ms <= 0) return;
|
|
1722
|
-
this.#nextSlot = Math.max(this.#nextSlot,
|
|
1736
|
+
this.#nextSlot = Math.max(this.#nextSlot, this.#clock.now() + ms);
|
|
1723
1737
|
}
|
|
1724
1738
|
/** Запускает столько ожидающих задач, сколько позволяют ограничения. */
|
|
1725
1739
|
#drain() {
|
|
1726
1740
|
if (this.#waiting.length === 0) {
|
|
1727
|
-
if (this.#
|
|
1728
|
-
|
|
1729
|
-
this.#
|
|
1741
|
+
if (this.#cancelTimer) {
|
|
1742
|
+
this.#cancelTimer();
|
|
1743
|
+
this.#cancelTimer = void 0;
|
|
1730
1744
|
}
|
|
1731
1745
|
return;
|
|
1732
1746
|
}
|
|
1733
1747
|
if (this.#active >= this.#concurrency) return;
|
|
1734
|
-
if (this.#
|
|
1735
|
-
const now =
|
|
1748
|
+
if (this.#cancelTimer) return;
|
|
1749
|
+
const now = this.#clock.now();
|
|
1736
1750
|
if (this.#nextSlot > now) {
|
|
1737
|
-
this.#
|
|
1738
|
-
this.#
|
|
1751
|
+
this.#cancelTimer = this.#clock.schedule(() => {
|
|
1752
|
+
this.#cancelTimer = void 0;
|
|
1739
1753
|
this.#drain();
|
|
1740
1754
|
}, this.#nextSlot - now);
|
|
1741
1755
|
return;
|
|
@@ -1754,19 +1768,21 @@ var RequestQueue = class {
|
|
|
1754
1768
|
*/
|
|
1755
1769
|
var RequestQueuePool = class {
|
|
1756
1770
|
#options;
|
|
1771
|
+
#clock;
|
|
1757
1772
|
#main;
|
|
1758
1773
|
/** Очереди сервисов заводятся при первом запросе — обычно не нужна ни одна. */
|
|
1759
1774
|
#byService = /* @__PURE__ */ new Map();
|
|
1760
|
-
constructor(options) {
|
|
1775
|
+
constructor(options, clock = systemClock) {
|
|
1761
1776
|
this.#options = options;
|
|
1762
|
-
this.#
|
|
1777
|
+
this.#clock = clock;
|
|
1778
|
+
this.#main = new RequestQueue(options, clock);
|
|
1763
1779
|
}
|
|
1764
1780
|
/** Очередь хоста. */
|
|
1765
1781
|
for(service) {
|
|
1766
1782
|
if (service === void 0) return this.#main;
|
|
1767
1783
|
let queue = this.#byService.get(service);
|
|
1768
1784
|
if (!queue) {
|
|
1769
|
-
queue = new RequestQueue(this.#options);
|
|
1785
|
+
queue = new RequestQueue(this.#options, this.#clock);
|
|
1770
1786
|
this.#byService.set(service, queue);
|
|
1771
1787
|
}
|
|
1772
1788
|
return queue;
|
|
@@ -2214,7 +2230,7 @@ function createApiError(context) {
|
|
|
2214
2230
|
path: context.path,
|
|
2215
2231
|
raw: safeRawBody(context.body),
|
|
2216
2232
|
response: context.response,
|
|
2217
|
-
retryAfter: parseRetryAfter(context.headers?.get("retry-after"))
|
|
2233
|
+
retryAfter: parseRetryAfter(context.headers?.get("retry-after"), context.now)
|
|
2218
2234
|
};
|
|
2219
2235
|
if (parsed.code === "PHONE_VERIFICATION_REQUIRED") return new ItdPhoneVerificationError({
|
|
2220
2236
|
...init,
|
|
@@ -2290,13 +2306,13 @@ function abortable(promise, signal) {
|
|
|
2290
2306
|
* Реализовано вручную, а не через `AbortSignal.any`: последний появился только в Node 20,
|
|
2291
2307
|
* а библиотека поддерживает Node 18.
|
|
2292
2308
|
*/
|
|
2293
|
-
function createAbortBundle(userSignal, timeout) {
|
|
2309
|
+
function createAbortBundle(userSignal, timeout, clock) {
|
|
2294
2310
|
const controller = new AbortController();
|
|
2295
2311
|
let timedOut = false;
|
|
2296
2312
|
const onUserAbort = () => controller.abort(userSignal?.reason);
|
|
2297
2313
|
if (userSignal) if (userSignal.aborted) controller.abort(userSignal.reason);
|
|
2298
2314
|
else userSignal.addEventListener("abort", onUserAbort, { once: true });
|
|
2299
|
-
const
|
|
2315
|
+
const cancelTimer = timeout > 0 ? clock.schedule(() => {
|
|
2300
2316
|
timedOut = true;
|
|
2301
2317
|
controller.abort();
|
|
2302
2318
|
}, timeout) : void 0;
|
|
@@ -2304,7 +2320,7 @@ function createAbortBundle(userSignal, timeout) {
|
|
|
2304
2320
|
signal: controller.signal,
|
|
2305
2321
|
timedOut: () => timedOut,
|
|
2306
2322
|
cleanup: () => {
|
|
2307
|
-
|
|
2323
|
+
cancelTimer?.();
|
|
2308
2324
|
userSignal?.removeEventListener("abort", onUserAbort);
|
|
2309
2325
|
}
|
|
2310
2326
|
};
|
|
@@ -2341,8 +2357,8 @@ var Transport = class {
|
|
|
2341
2357
|
const headers = await this.#buildHeaders(request, url);
|
|
2342
2358
|
const attempt = request.attempt ?? 1;
|
|
2343
2359
|
const timeout = request.timeout ?? this.#config.timeout;
|
|
2344
|
-
const abort = createAbortBundle(request.signal, timeout);
|
|
2345
|
-
const startedAt =
|
|
2360
|
+
const abort = createAbortBundle(request.signal, timeout, this.#config.clock);
|
|
2361
|
+
const startedAt = this.#config.clock.now();
|
|
2346
2362
|
let cleanupBody;
|
|
2347
2363
|
try {
|
|
2348
2364
|
let body;
|
|
@@ -2361,7 +2377,7 @@ var Transport = class {
|
|
|
2361
2377
|
};
|
|
2362
2378
|
await dispatchRequestHook(this.#config.hooks, "onError", {
|
|
2363
2379
|
...context,
|
|
2364
|
-
duration:
|
|
2380
|
+
duration: this.#config.clock.now() - startedAt,
|
|
2365
2381
|
error: failure
|
|
2366
2382
|
}, request);
|
|
2367
2383
|
throw failure;
|
|
@@ -2390,7 +2406,7 @@ var Transport = class {
|
|
|
2390
2406
|
if (typeof ReadableStream !== "undefined" && body instanceof ReadableStream) init.duplex = "half";
|
|
2391
2407
|
response = await this.#config.fetch(url, init);
|
|
2392
2408
|
} catch (error) {
|
|
2393
|
-
const duration =
|
|
2409
|
+
const duration = this.#config.clock.now() - startedAt;
|
|
2394
2410
|
const failure = this.#toTransportError(error, abort, request, method, timeout);
|
|
2395
2411
|
await dispatchRequestHook(this.#config.hooks, "onError", {
|
|
2396
2412
|
...context,
|
|
@@ -2408,16 +2424,17 @@ var Transport = class {
|
|
|
2408
2424
|
if (response.ok) await dispatchRequestHook(this.#config.hooks, "onResponse", {
|
|
2409
2425
|
...context,
|
|
2410
2426
|
status: response.status,
|
|
2411
|
-
duration:
|
|
2427
|
+
duration: this.#config.clock.now() - startedAt,
|
|
2412
2428
|
response
|
|
2413
2429
|
}, request);
|
|
2414
2430
|
const payload = await this.#readBodyOrFail(response, context, request, method, abort, timeout);
|
|
2415
|
-
const duration =
|
|
2431
|
+
const duration = this.#config.clock.now() - startedAt;
|
|
2416
2432
|
if (!response.ok) {
|
|
2417
2433
|
const error = createApiError({
|
|
2418
2434
|
method,
|
|
2419
2435
|
path: request.path,
|
|
2420
2436
|
status: response.status,
|
|
2437
|
+
now: this.#config.clock.now(),
|
|
2421
2438
|
statusText: response.statusText,
|
|
2422
2439
|
headers: response.headers,
|
|
2423
2440
|
response,
|
|
@@ -2486,7 +2503,7 @@ var Transport = class {
|
|
|
2486
2503
|
}
|
|
2487
2504
|
/** Читает тело и преобразует ошибку чтения в транспортную ошибку библиотеки. */
|
|
2488
2505
|
async #readBodyOrFail(response, context, request, method, abort, timeout) {
|
|
2489
|
-
const startedAt =
|
|
2506
|
+
const startedAt = this.#config.clock.now();
|
|
2490
2507
|
try {
|
|
2491
2508
|
return await abortable(readBody(response), abort.signal);
|
|
2492
2509
|
} catch (error) {
|
|
@@ -2494,7 +2511,7 @@ var Transport = class {
|
|
|
2494
2511
|
const failure = this.#toTransportError(error, abort, request, method, timeout);
|
|
2495
2512
|
await dispatchRequestHook(this.#config.hooks, "onError", {
|
|
2496
2513
|
...context,
|
|
2497
|
-
duration:
|
|
2514
|
+
duration: this.#config.clock.now() - startedAt,
|
|
2498
2515
|
error: failure
|
|
2499
2516
|
}, request);
|
|
2500
2517
|
this.#config.logger?.warn(`× ${method} ${request.path}: не удалось прочитать тело ответа — ${failure.message}`);
|
|
@@ -2815,144 +2832,167 @@ const ItdErrorCode = Object.freeze({
|
|
|
2815
2832
|
WRITE_ACCESS_RESTRICTED: "WRITE_ACCESS_RESTRICTED"
|
|
2816
2833
|
});
|
|
2817
2834
|
//#endregion
|
|
2818
|
-
//#region src/
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
*/
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
return {
|
|
2869
|
-
id,
|
|
2870
|
-
username: asString(value.username) ?? "",
|
|
2871
|
-
displayName: asString(value.displayName) ?? "",
|
|
2872
|
-
avatar: asString(value.avatar) ?? "",
|
|
2873
|
-
...typeof value.isFollowing === "boolean" ? { isFollowing: value.isFollowing } : {},
|
|
2874
|
-
...typeof value.isFollowedBy === "boolean" ? { isFollowedBy: value.isFollowedBy } : {}
|
|
2875
|
-
};
|
|
2876
|
-
}
|
|
2877
|
-
/** Собирает участников: сервер присылает либо одного `actor`, либо массив `actors`. */
|
|
2878
|
-
function readActors(source) {
|
|
2879
|
-
if (Array.isArray(source.actors)) return source.actors.map(asActor).filter((actor) => actor !== void 0);
|
|
2880
|
-
const single = asActor(source.actor);
|
|
2881
|
-
return single ? [single] : [];
|
|
2882
|
-
}
|
|
2883
|
-
/**
|
|
2884
|
-
* Приводит уведомление к единой форме.
|
|
2885
|
-
*
|
|
2886
|
-
* Нужна потому, что REST-список и поток событий описывают одно и то же событие по-разному:
|
|
2887
|
-
* различаются имена типов (`like` против `post_reaction`), имена полей
|
|
2888
|
-
* (`targetId`/`entityId`, `read`/`isRead`, `preview`/`entityPreview`) и число участников
|
|
2889
|
-
* (`actor` против массива `actors`). После приведения объекты из обоих источников
|
|
2890
|
-
* можно складывать в один список.
|
|
2891
|
-
*
|
|
2892
|
-
* Исходные данные не теряются: имя типа с сервера остаётся в `rawType`,
|
|
2893
|
-
* весь объект целиком — в `raw`.
|
|
2894
|
-
*
|
|
2895
|
-
* @param input уведомление из REST-ответа либо полезная нагрузка события потока
|
|
2896
|
-
*
|
|
2897
|
-
* @example
|
|
2898
|
-
* ```ts
|
|
2899
|
-
* const fromRest = normalizeNotification(restItem);
|
|
2900
|
-
* const fromStream = normalizeNotification(event.payload);
|
|
2901
|
-
* // одинаковая форма — можно объединять
|
|
2902
|
-
* ```
|
|
2903
|
-
*/
|
|
2904
|
-
function normalizeNotification(input) {
|
|
2905
|
-
const source = isRecord(input) ? input : {};
|
|
2906
|
-
const payload = isRecord(source.payload) ? source.payload : source;
|
|
2907
|
-
const rawType = asString(payload.type) ?? asString(source.type) ?? "";
|
|
2908
|
-
const createdAt = asString(payload.createdAt) ?? asString(source.createdAt) ?? "";
|
|
2909
|
-
const readAt = asString(payload.readAt) ?? asString(source.readAt);
|
|
2910
|
-
const isRead = typeof payload.isRead === "boolean" ? payload.isRead : typeof payload.read === "boolean" ? payload.read : Boolean(readAt);
|
|
2911
|
-
const subjectId = asString(payload.subjectId);
|
|
2912
|
-
const targetId = asString(payload.targetId);
|
|
2913
|
-
const subjectIsComment = payload.subjectType === "comment";
|
|
2914
|
-
const clickUrl = asString(payload.clickUrl);
|
|
2915
|
-
return {
|
|
2916
|
-
id: asString(payload.id) ?? asString(source.id) ?? "",
|
|
2917
|
-
type: canonicalNotificationType(rawType),
|
|
2918
|
-
rawType,
|
|
2919
|
-
entityId: asString(payload.entityId) ?? (subjectIsComment ? subjectId ?? targetId : targetId) ?? null,
|
|
2920
|
-
parentEntityId: asString(payload.parentEntityId) ?? (subjectIsComment ? targetId ?? null : null),
|
|
2921
|
-
isRead,
|
|
2922
|
-
actors: readActors(payload),
|
|
2923
|
-
count: typeof payload.count === "number" && payload.count > 0 ? payload.count : 1,
|
|
2924
|
-
preview: asString(payload.entityPreview) ?? asString(payload.preview) ?? null,
|
|
2925
|
-
...clickUrl ? { clickUrl } : {},
|
|
2926
|
-
createdAt,
|
|
2927
|
-
updatedAt: asString(payload.updatedAt) ?? readAt ?? createdAt,
|
|
2928
|
-
raw: input
|
|
2929
|
-
};
|
|
2930
|
-
}
|
|
2931
|
-
/**
|
|
2932
|
-
* Разбирает событие `notification` из потока.
|
|
2933
|
-
*
|
|
2934
|
-
* Кроме самого уведомления событие несёт служебные поля уровня конверта: актуальный
|
|
2935
|
-
* счётчик непрочитанных и признак звука.
|
|
2936
|
-
*/
|
|
2937
|
-
function readNotificationEvent(data) {
|
|
2938
|
-
const source = isRecord(data) ? data : {};
|
|
2939
|
-
return {
|
|
2940
|
-
notification: normalizeNotification(data),
|
|
2941
|
-
unreadCount: typeof source.unreadCount === "number" ? source.unreadCount : void 0,
|
|
2942
|
-
sound: source.sound === true
|
|
2835
|
+
//#region src/realtime/middleware.ts
|
|
2836
|
+
const REALTIME_MIDDLEWARE_SNAPSHOT = Symbol("itd-api.realtime.middlewareSnapshot");
|
|
2837
|
+
/** @internal */
|
|
2838
|
+
function captureRealtimeMiddleware(middleware) {
|
|
2839
|
+
const capture = middleware[REALTIME_MIDDLEWARE_SNAPSHOT];
|
|
2840
|
+
if (!capture) return middleware;
|
|
2841
|
+
const snapshot = capture();
|
|
2842
|
+
if (typeof snapshot !== "function") throw new TypeError("Снимок промежуточного обработчика должен быть функцией");
|
|
2843
|
+
return snapshot;
|
|
2844
|
+
}
|
|
2845
|
+
/** @internal */
|
|
2846
|
+
function withRealtimeMiddlewareSnapshot(middleware, capture) {
|
|
2847
|
+
Object.defineProperty(middleware, REALTIME_MIDDLEWARE_SNAPSHOT, { value: capture });
|
|
2848
|
+
return middleware;
|
|
2849
|
+
}
|
|
2850
|
+
/** Выполняет промежуточные обработчики по порядку и запрещает повторный вызов `next()`. */
|
|
2851
|
+
async function runRealtimeMiddleware(middleware, context, terminal) {
|
|
2852
|
+
let lastIndex = -1;
|
|
2853
|
+
const dispatch = async (index) => {
|
|
2854
|
+
if (index <= lastIndex) throw new Error("next() в промежуточном обработчике вызван повторно");
|
|
2855
|
+
lastIndex = index;
|
|
2856
|
+
const current = middleware[index];
|
|
2857
|
+
if (!current) {
|
|
2858
|
+
await terminal();
|
|
2859
|
+
return;
|
|
2860
|
+
}
|
|
2861
|
+
let downstream;
|
|
2862
|
+
let duplicateCalls;
|
|
2863
|
+
let failure;
|
|
2864
|
+
const next = () => {
|
|
2865
|
+
if (!downstream) {
|
|
2866
|
+
downstream = dispatch(index + 1);
|
|
2867
|
+
return downstream;
|
|
2868
|
+
}
|
|
2869
|
+
const duplicate = Promise.reject(/* @__PURE__ */ new Error("next() в одном промежуточном обработчике вызван повторно"));
|
|
2870
|
+
duplicateCalls = Promise.all(duplicateCalls ? [duplicateCalls, duplicate] : [duplicate]).then(() => void 0);
|
|
2871
|
+
return duplicate;
|
|
2872
|
+
};
|
|
2873
|
+
try {
|
|
2874
|
+
await current(context, next);
|
|
2875
|
+
} catch (error) {
|
|
2876
|
+
failure = { error };
|
|
2877
|
+
}
|
|
2878
|
+
try {
|
|
2879
|
+
await downstream;
|
|
2880
|
+
await duplicateCalls;
|
|
2881
|
+
} catch (error) {
|
|
2882
|
+
failure ??= { error };
|
|
2883
|
+
}
|
|
2884
|
+
if (failure) throw failure.error;
|
|
2943
2885
|
};
|
|
2886
|
+
await dispatch(0);
|
|
2944
2887
|
}
|
|
2945
|
-
/**
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2888
|
+
/** Планирует нормализованные обновления и отслеживает незавершённые обработчики. */
|
|
2889
|
+
var RealtimeDispatcher = class {
|
|
2890
|
+
#options;
|
|
2891
|
+
#hooks;
|
|
2892
|
+
#middleware = [];
|
|
2893
|
+
#handlers = [];
|
|
2894
|
+
#queue = [];
|
|
2895
|
+
#activeKeys = /* @__PURE__ */ new Set();
|
|
2896
|
+
#drainWaiters = /* @__PURE__ */ new Set();
|
|
2897
|
+
#active = 0;
|
|
2898
|
+
constructor(options, hooks) {
|
|
2899
|
+
this.#options = options;
|
|
2900
|
+
this.#hooks = hooks;
|
|
2901
|
+
}
|
|
2902
|
+
use(middleware) {
|
|
2903
|
+
this.#middleware.push(middleware);
|
|
2904
|
+
return () => {
|
|
2905
|
+
const index = this.#middleware.indexOf(middleware);
|
|
2906
|
+
if (index >= 0) this.#middleware.splice(index, 1);
|
|
2907
|
+
};
|
|
2908
|
+
}
|
|
2909
|
+
on(predicate, handler) {
|
|
2910
|
+
const registration = {
|
|
2911
|
+
predicate,
|
|
2912
|
+
handler
|
|
2913
|
+
};
|
|
2914
|
+
this.#handlers.push(registration);
|
|
2915
|
+
return () => {
|
|
2916
|
+
const index = this.#handlers.indexOf(registration);
|
|
2917
|
+
if (index >= 0) this.#handlers.splice(index, 1);
|
|
2918
|
+
};
|
|
2919
|
+
}
|
|
2920
|
+
dispatch(context) {
|
|
2921
|
+
let keys;
|
|
2922
|
+
let middleware;
|
|
2923
|
+
try {
|
|
2924
|
+
keys = this.#keysFor(context);
|
|
2925
|
+
middleware = this.#middleware.map(captureRealtimeMiddleware);
|
|
2926
|
+
} catch (error) {
|
|
2927
|
+
this.#hooks.middlewareError(error, context);
|
|
2928
|
+
return;
|
|
2929
|
+
}
|
|
2930
|
+
this.#queue.push({
|
|
2931
|
+
context,
|
|
2932
|
+
middleware,
|
|
2933
|
+
handlers: [...this.#handlers],
|
|
2934
|
+
keys
|
|
2935
|
+
});
|
|
2936
|
+
this.#pump();
|
|
2937
|
+
}
|
|
2938
|
+
/** Отбрасывает обновления, обработка которых ещё не началась. */
|
|
2939
|
+
clearPending() {
|
|
2940
|
+
this.#queue.length = 0;
|
|
2941
|
+
this.#resolveDrain();
|
|
2942
|
+
}
|
|
2943
|
+
/** Ждёт завершения активных и поставленных в очередь обновлений. */
|
|
2944
|
+
drain() {
|
|
2945
|
+
if (this.#active === 0 && this.#queue.length === 0) return Promise.resolve();
|
|
2946
|
+
return new Promise((resolve) => this.#drainWaiters.add(resolve));
|
|
2947
|
+
}
|
|
2948
|
+
#keysFor(context) {
|
|
2949
|
+
const value = this.#options.sequentialize?.(context);
|
|
2950
|
+
if (value === void 0) return [];
|
|
2951
|
+
const values = Array.isArray(value) ? value : [value];
|
|
2952
|
+
for (const key of values) if (typeof key !== "string" && typeof key !== "number" && typeof key !== "symbol") throw new TypeError("sequentialize() должен возвращать PropertyKey или их список");
|
|
2953
|
+
return [...new Set(values)];
|
|
2954
|
+
}
|
|
2955
|
+
#pump() {
|
|
2956
|
+
while (this.#active < this.#options.concurrency) {
|
|
2957
|
+
const blockedKeys = new Set(this.#activeKeys);
|
|
2958
|
+
const index = this.#queue.findIndex(({ keys }) => {
|
|
2959
|
+
const runnable = keys.every((key) => !blockedKeys.has(key));
|
|
2960
|
+
if (!runnable) for (const key of keys) blockedKeys.add(key);
|
|
2961
|
+
return runnable;
|
|
2962
|
+
});
|
|
2963
|
+
if (index < 0) break;
|
|
2964
|
+
const [work] = this.#queue.splice(index, 1);
|
|
2965
|
+
if (!work) break;
|
|
2966
|
+
this.#active += 1;
|
|
2967
|
+
for (const key of work.keys) this.#activeKeys.add(key);
|
|
2968
|
+
this.#run(work);
|
|
2969
|
+
}
|
|
2970
|
+
this.#resolveDrain();
|
|
2971
|
+
}
|
|
2972
|
+
async #run(work) {
|
|
2973
|
+
try {
|
|
2974
|
+
await runRealtimeMiddleware(work.middleware, work.context, async () => {
|
|
2975
|
+
for (const { predicate, handler } of work.handlers) try {
|
|
2976
|
+
if (predicate(work.context)) await handler(work.context);
|
|
2977
|
+
} catch (error) {
|
|
2978
|
+
this.#hooks.handlerError(error, work.context);
|
|
2979
|
+
}
|
|
2980
|
+
this.#hooks.deliver(work.context);
|
|
2981
|
+
});
|
|
2982
|
+
} catch (error) {
|
|
2983
|
+
this.#hooks.middlewareError(error, work.context);
|
|
2984
|
+
} finally {
|
|
2985
|
+
this.#active -= 1;
|
|
2986
|
+
for (const key of work.keys) this.#activeKeys.delete(key);
|
|
2987
|
+
this.#pump();
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
#resolveDrain() {
|
|
2991
|
+
if (this.#active !== 0 || this.#queue.length !== 0) return;
|
|
2992
|
+
for (const resolve of this.#drainWaiters) resolve();
|
|
2993
|
+
this.#drainWaiters.clear();
|
|
2994
|
+
}
|
|
2995
|
+
};
|
|
2956
2996
|
//#endregion
|
|
2957
2997
|
//#region src/realtime/transport.ts
|
|
2958
2998
|
/** Ошибка, по которой видно, что сервер отверг авторизацию потока. */
|
|
@@ -2978,7 +3018,9 @@ var PollTransport = class {
|
|
|
2978
3018
|
name = "poll";
|
|
2979
3019
|
#interval;
|
|
2980
3020
|
#limit;
|
|
3021
|
+
#clock;
|
|
2981
3022
|
constructor(options = {}) {
|
|
3023
|
+
this.#clock = options.clock ?? systemClock;
|
|
2982
3024
|
this.#interval = options.interval ?? 15e3;
|
|
2983
3025
|
this.#limit = options.limit ?? 20;
|
|
2984
3026
|
}
|
|
@@ -3046,9 +3088,9 @@ var PollTransport = class {
|
|
|
3046
3088
|
#wait(signal) {
|
|
3047
3089
|
if (signal.aborted) return Promise.resolve();
|
|
3048
3090
|
return new Promise((resolve) => {
|
|
3049
|
-
const
|
|
3091
|
+
const cancel = this.#clock.schedule(finish, this.#interval);
|
|
3050
3092
|
function finish() {
|
|
3051
|
-
|
|
3093
|
+
cancel();
|
|
3052
3094
|
signal.removeEventListener("abort", finish);
|
|
3053
3095
|
resolve();
|
|
3054
3096
|
}
|
|
@@ -3285,9 +3327,11 @@ var SseTransport = class {
|
|
|
3285
3327
|
name = "sse";
|
|
3286
3328
|
#idleTimeout;
|
|
3287
3329
|
#handshakeTimeout;
|
|
3330
|
+
#clock;
|
|
3288
3331
|
/** Идентификатор последнего события — отправляется при переподключении. */
|
|
3289
3332
|
#lastEventId;
|
|
3290
3333
|
constructor(options = {}) {
|
|
3334
|
+
this.#clock = options.clock ?? systemClock;
|
|
3291
3335
|
this.#idleTimeout = options.idleTimeout ?? 9e4;
|
|
3292
3336
|
this.#handshakeTimeout = options.handshakeTimeout ?? 2e4;
|
|
3293
3337
|
}
|
|
@@ -3318,7 +3362,7 @@ var SseTransport = class {
|
|
|
3318
3362
|
/** Выполняет запрос потока, обрывая его, если ответ не пришёл за отведённое время. */
|
|
3319
3363
|
async #handshake(url, headers, context, controller) {
|
|
3320
3364
|
let expired = false;
|
|
3321
|
-
const
|
|
3365
|
+
const cancelTimer = this.#handshakeTimeout > 0 ? this.#clock.schedule(() => {
|
|
3322
3366
|
expired = true;
|
|
3323
3367
|
controller.abort(/* @__PURE__ */ new Error("Истёк таймаут рукопожатия SSE"));
|
|
3324
3368
|
}, this.#handshakeTimeout) : void 0;
|
|
@@ -3332,7 +3376,7 @@ var SseTransport = class {
|
|
|
3332
3376
|
if (expired) throw new Error("Поток уведомлений не ответил: истёк таймаут рукопожатия");
|
|
3333
3377
|
throw error;
|
|
3334
3378
|
} finally {
|
|
3335
|
-
|
|
3379
|
+
cancelTimer?.();
|
|
3336
3380
|
}
|
|
3337
3381
|
}
|
|
3338
3382
|
async #read(body, context) {
|
|
@@ -3353,11 +3397,11 @@ var SseTransport = class {
|
|
|
3353
3397
|
data
|
|
3354
3398
|
});
|
|
3355
3399
|
} });
|
|
3356
|
-
let
|
|
3400
|
+
let cancelIdleTimer;
|
|
3357
3401
|
const armIdleTimer = () => {
|
|
3358
3402
|
if (this.#idleTimeout <= 0) return;
|
|
3359
|
-
|
|
3360
|
-
|
|
3403
|
+
cancelIdleTimer?.();
|
|
3404
|
+
cancelIdleTimer = this.#clock.schedule(() => {
|
|
3361
3405
|
reader.cancel(/* @__PURE__ */ new Error("Поток молчит дольше допустимого")).catch(() => {});
|
|
3362
3406
|
}, this.#idleTimeout);
|
|
3363
3407
|
};
|
|
@@ -3370,12 +3414,220 @@ var SseTransport = class {
|
|
|
3370
3414
|
parser.feed(decoder.decode(value, { stream: true }));
|
|
3371
3415
|
}
|
|
3372
3416
|
} finally {
|
|
3373
|
-
|
|
3417
|
+
cancelIdleTimer?.();
|
|
3374
3418
|
reader.releaseLock?.();
|
|
3375
3419
|
}
|
|
3376
3420
|
}
|
|
3377
3421
|
};
|
|
3378
3422
|
//#endregion
|
|
3423
|
+
//#region src/notifications/type-map.ts
|
|
3424
|
+
/**
|
|
3425
|
+
* Соответствие коротких имён типов уведомлений развёрнутым.
|
|
3426
|
+
*
|
|
3427
|
+
* Сервер — и в списке, и в потоке событий — присылает короткие имена: `like`, `comment`,
|
|
3428
|
+
* `reply`, `repost`, `comment_like`. Развёрнутые (`post_reaction`, `post_comment`)
|
|
3429
|
+
* встречаются в оформлении интерфейса, поэтому библиотека приводит типы к ним:
|
|
3430
|
+
* они однозначно называют и объект, и действие.
|
|
3431
|
+
*
|
|
3432
|
+
* Пришедшее значение всегда остаётся в поле `rawType`.
|
|
3433
|
+
*/
|
|
3434
|
+
const NOTIFICATION_TYPE_ALIASES = Object.freeze({
|
|
3435
|
+
like: NotificationType.PostReaction,
|
|
3436
|
+
comment: NotificationType.PostComment,
|
|
3437
|
+
comment_like: NotificationType.CommentReaction,
|
|
3438
|
+
reply: NotificationType.CommentReply,
|
|
3439
|
+
repost: NotificationType.PostRepost,
|
|
3440
|
+
mention: NotificationType.PostMention
|
|
3441
|
+
});
|
|
3442
|
+
const KNOWN_TYPES = new Set(Object.values(NotificationType));
|
|
3443
|
+
/**
|
|
3444
|
+
* Приводит имя типа к каноническому.
|
|
3445
|
+
*
|
|
3446
|
+
* Неизвестное значение возвращается без изменений, чтобы не менять смысл нового типа
|
|
3447
|
+
* уведомления на другой.
|
|
3448
|
+
*
|
|
3449
|
+
* @example
|
|
3450
|
+
* ```ts
|
|
3451
|
+
* canonicalNotificationType('like'); // 'post_reaction'
|
|
3452
|
+
* canonicalNotificationType('post_reaction'); // 'post_reaction'
|
|
3453
|
+
* canonicalNotificationType('новое_событие'); // 'новое_событие'
|
|
3454
|
+
* ```
|
|
3455
|
+
*/
|
|
3456
|
+
function canonicalNotificationType(rawType) {
|
|
3457
|
+
return NOTIFICATION_TYPE_ALIASES[rawType] ?? rawType;
|
|
3458
|
+
}
|
|
3459
|
+
/**
|
|
3460
|
+
* Известен ли библиотеке этот тип уведомления.
|
|
3461
|
+
*
|
|
3462
|
+
* Полезно, чтобы решить, показывать ли уведомление, для которого нет своего оформления.
|
|
3463
|
+
*/
|
|
3464
|
+
function isKnownNotificationType(type) {
|
|
3465
|
+
return KNOWN_TYPES.has(canonicalNotificationType(type));
|
|
3466
|
+
}
|
|
3467
|
+
//#endregion
|
|
3468
|
+
//#region src/notifications/normalize.ts
|
|
3469
|
+
function asActor(value) {
|
|
3470
|
+
if (!isRecord(value)) return void 0;
|
|
3471
|
+
const id = asString(value.id);
|
|
3472
|
+
if (!id) return void 0;
|
|
3473
|
+
return {
|
|
3474
|
+
id,
|
|
3475
|
+
username: asString(value.username) ?? "",
|
|
3476
|
+
displayName: asString(value.displayName) ?? "",
|
|
3477
|
+
avatar: asString(value.avatar) ?? "",
|
|
3478
|
+
...typeof value.isFollowing === "boolean" ? { isFollowing: value.isFollowing } : {},
|
|
3479
|
+
...typeof value.isFollowedBy === "boolean" ? { isFollowedBy: value.isFollowedBy } : {}
|
|
3480
|
+
};
|
|
3481
|
+
}
|
|
3482
|
+
/** Собирает участников: сервер присылает либо одного `actor`, либо массив `actors`. */
|
|
3483
|
+
function readActors(source) {
|
|
3484
|
+
if (Array.isArray(source.actors)) return source.actors.map(asActor).filter((actor) => actor !== void 0);
|
|
3485
|
+
const single = asActor(source.actor);
|
|
3486
|
+
return single ? [single] : [];
|
|
3487
|
+
}
|
|
3488
|
+
/**
|
|
3489
|
+
* Приводит уведомление к единой форме.
|
|
3490
|
+
*
|
|
3491
|
+
* Нужна потому, что REST-список и поток событий описывают одно и то же событие по-разному:
|
|
3492
|
+
* различаются имена типов (`like` против `post_reaction`), имена полей
|
|
3493
|
+
* (`targetId`/`entityId`, `read`/`isRead`, `preview`/`entityPreview`) и число участников
|
|
3494
|
+
* (`actor` против массива `actors`). После приведения объекты из обоих источников
|
|
3495
|
+
* можно складывать в один список.
|
|
3496
|
+
*
|
|
3497
|
+
* Исходные данные не теряются: имя типа с сервера остаётся в `rawType`,
|
|
3498
|
+
* весь объект целиком — в `raw`.
|
|
3499
|
+
*
|
|
3500
|
+
* @param input уведомление из REST-ответа либо полезная нагрузка события потока
|
|
3501
|
+
*
|
|
3502
|
+
* @example
|
|
3503
|
+
* ```ts
|
|
3504
|
+
* const fromRest = normalizeNotification(restItem);
|
|
3505
|
+
* const fromStream = normalizeNotification(event.payload);
|
|
3506
|
+
* // одинаковая форма — можно объединять
|
|
3507
|
+
* ```
|
|
3508
|
+
*/
|
|
3509
|
+
function normalizeNotification(input) {
|
|
3510
|
+
const source = isRecord(input) ? input : {};
|
|
3511
|
+
const payload = isRecord(source.payload) ? source.payload : source;
|
|
3512
|
+
const rawType = asString(payload.type) ?? asString(source.type) ?? "";
|
|
3513
|
+
const createdAt = asString(payload.createdAt) ?? asString(source.createdAt) ?? "";
|
|
3514
|
+
const readAt = asString(payload.readAt) ?? asString(source.readAt);
|
|
3515
|
+
const isRead = typeof payload.isRead === "boolean" ? payload.isRead : typeof payload.read === "boolean" ? payload.read : Boolean(readAt);
|
|
3516
|
+
const subjectId = asString(payload.subjectId);
|
|
3517
|
+
const targetId = asString(payload.targetId);
|
|
3518
|
+
const subjectIsComment = payload.subjectType === "comment";
|
|
3519
|
+
const clickUrl = asString(payload.clickUrl);
|
|
3520
|
+
return {
|
|
3521
|
+
id: asString(payload.id) ?? asString(source.id) ?? "",
|
|
3522
|
+
type: canonicalNotificationType(rawType),
|
|
3523
|
+
rawType,
|
|
3524
|
+
entityId: asString(payload.entityId) ?? (subjectIsComment ? subjectId ?? targetId : targetId) ?? null,
|
|
3525
|
+
parentEntityId: asString(payload.parentEntityId) ?? (subjectIsComment ? targetId ?? null : null),
|
|
3526
|
+
isRead,
|
|
3527
|
+
actors: readActors(payload),
|
|
3528
|
+
count: typeof payload.count === "number" && payload.count > 0 ? payload.count : 1,
|
|
3529
|
+
preview: asString(payload.entityPreview) ?? asString(payload.preview) ?? null,
|
|
3530
|
+
...clickUrl ? { clickUrl } : {},
|
|
3531
|
+
createdAt,
|
|
3532
|
+
updatedAt: asString(payload.updatedAt) ?? readAt ?? createdAt,
|
|
3533
|
+
raw: input
|
|
3534
|
+
};
|
|
3535
|
+
}
|
|
3536
|
+
/**
|
|
3537
|
+
* Разбирает событие `notification` из потока.
|
|
3538
|
+
*
|
|
3539
|
+
* Кроме самого уведомления событие несёт служебные поля уровня конверта: актуальный
|
|
3540
|
+
* счётчик непрочитанных и признак звука.
|
|
3541
|
+
*/
|
|
3542
|
+
function readNotificationEvent(data) {
|
|
3543
|
+
const source = isRecord(data) ? data : {};
|
|
3544
|
+
return {
|
|
3545
|
+
notification: normalizeNotification(data),
|
|
3546
|
+
unreadCount: typeof source.unreadCount === "number" ? source.unreadCount : void 0,
|
|
3547
|
+
sound: source.sound === true
|
|
3548
|
+
};
|
|
3549
|
+
}
|
|
3550
|
+
/**
|
|
3551
|
+
* Разбирает событие `unread_count` из потока.
|
|
3552
|
+
*
|
|
3553
|
+
* Возвращает `undefined`, если сервер прислал событие без вложенного `payload`.
|
|
3554
|
+
*/
|
|
3555
|
+
function readUnreadCountEvent(data) {
|
|
3556
|
+
if (!isRecord(data)) return void 0;
|
|
3557
|
+
const payload = isRecord(data.payload) ? data.payload : void 0;
|
|
3558
|
+
if (!payload) return void 0;
|
|
3559
|
+
return typeof payload.count === "number" ? payload.count : void 0;
|
|
3560
|
+
}
|
|
3561
|
+
//#endregion
|
|
3562
|
+
//#region src/realtime/updates.ts
|
|
3563
|
+
/** Типы нормализованных обновлений потока. */
|
|
3564
|
+
const RealtimeUpdateType = Object.freeze({
|
|
3565
|
+
Notification: "notification",
|
|
3566
|
+
UnreadCount: "unreadCount",
|
|
3567
|
+
Unknown: "unknown"
|
|
3568
|
+
});
|
|
3569
|
+
/** Источники нормализованных обновлений потока. */
|
|
3570
|
+
const RealtimeUpdateOrigin = Object.freeze({
|
|
3571
|
+
Stream: "stream",
|
|
3572
|
+
Sync: "sync"
|
|
3573
|
+
});
|
|
3574
|
+
/** Проверяет форму фильтра уведомлений. */
|
|
3575
|
+
function validateNotificationSelector(selector) {
|
|
3576
|
+
if (typeof selector === "string") {
|
|
3577
|
+
if (selector.length === 0) throw new ItdConfigError("Тип уведомления не должен быть пустым");
|
|
3578
|
+
return;
|
|
3579
|
+
}
|
|
3580
|
+
if (Array.isArray(selector)) {
|
|
3581
|
+
if (selector.length === 0 || selector.some((type) => typeof type !== "string" || !type)) throw new ItdConfigError("Список типов уведомлений должен содержать непустые строки");
|
|
3582
|
+
return;
|
|
3583
|
+
}
|
|
3584
|
+
if (typeof selector !== "object" || selector === null) throw new ItdConfigError("Фильтр уведомлений должен быть строкой, списком или объектом");
|
|
3585
|
+
const filter = selector;
|
|
3586
|
+
if (filter.type !== void 0) validateNotificationSelector(filter.type);
|
|
3587
|
+
if (filter.actorId !== void 0 && typeof filter.actorId !== "string") throw new ItdConfigError("Фильтр уведомлений: actorId должен быть строкой");
|
|
3588
|
+
for (const field of ["entityId", "parentEntityId"]) {
|
|
3589
|
+
const value = filter[field];
|
|
3590
|
+
if (value !== void 0 && value !== null && typeof value !== "string") throw new ItdConfigError(`Фильтр уведомлений: ${field} должен быть строкой или null`);
|
|
3591
|
+
}
|
|
3592
|
+
if (filter.predicate !== void 0 && typeof filter.predicate !== "function") throw new ItdConfigError("Фильтр уведомлений: predicate должен быть функцией");
|
|
3593
|
+
}
|
|
3594
|
+
/** Преобразует транспортный кадр в одно логическое обновление. */
|
|
3595
|
+
function readRealtimeUpdate(event) {
|
|
3596
|
+
if (event.name === "notification") return {
|
|
3597
|
+
type: RealtimeUpdateType.Notification,
|
|
3598
|
+
data: readNotificationEvent(event.data)
|
|
3599
|
+
};
|
|
3600
|
+
if (event.name === "unread_count") {
|
|
3601
|
+
const count = readUnreadCountEvent(event.data);
|
|
3602
|
+
return count === void 0 ? void 0 : {
|
|
3603
|
+
type: RealtimeUpdateType.UnreadCount,
|
|
3604
|
+
data: count
|
|
3605
|
+
};
|
|
3606
|
+
}
|
|
3607
|
+
return {
|
|
3608
|
+
type: RealtimeUpdateType.Unknown,
|
|
3609
|
+
name: event.name,
|
|
3610
|
+
data: event.data
|
|
3611
|
+
};
|
|
3612
|
+
}
|
|
3613
|
+
/** Проверяет объектный или краткий фильтр уведомления. */
|
|
3614
|
+
function matchesNotification(context, selector) {
|
|
3615
|
+
const notification = context.update.data.notification;
|
|
3616
|
+
if (typeof selector === "string") return notification.type === selector;
|
|
3617
|
+
if (Array.isArray(selector)) return selector.includes(notification.type);
|
|
3618
|
+
const filter = selector;
|
|
3619
|
+
const types = filter.type === void 0 ? void 0 : [filter.type].flat();
|
|
3620
|
+
if (types && !types.includes(notification.type)) return false;
|
|
3621
|
+
if (filter.actorId !== void 0 && !notification.actors.some(({ id }) => id === filter.actorId)) return false;
|
|
3622
|
+
if (filter.entityId !== void 0 && notification.entityId !== filter.entityId) return false;
|
|
3623
|
+
if (filter.parentEntityId !== void 0 && notification.parentEntityId !== filter.parentEntityId) return false;
|
|
3624
|
+
return filter.predicate?.(context) ?? true;
|
|
3625
|
+
}
|
|
3626
|
+
/** Сужает произвольный контекст потока до контекста уведомления. */
|
|
3627
|
+
function isNotificationContext(context) {
|
|
3628
|
+
return context.update.type === RealtimeUpdateType.Notification;
|
|
3629
|
+
}
|
|
3630
|
+
//#endregion
|
|
3379
3631
|
//#region src/realtime/stream.ts
|
|
3380
3632
|
/** Способ получения событий. */
|
|
3381
3633
|
const RealtimeTransportKind = Object.freeze({
|
|
@@ -3397,6 +3649,8 @@ function validateRealtimeOptions(options) {
|
|
|
3397
3649
|
if (!Number.isFinite(value) || value < min) throw new ItdConfigError(`realtime.${name} должен быть числом не меньше ${min}, получено: ${value}`);
|
|
3398
3650
|
};
|
|
3399
3651
|
positiveInteger(options.maxAttempts, "maxAttempts");
|
|
3652
|
+
positiveInteger(options.concurrency, "concurrency");
|
|
3653
|
+
if (options.concurrency === 0) throw new ItdConfigError("realtime.concurrency должен быть больше нуля");
|
|
3400
3654
|
duration(options.pollInterval, "pollInterval", 1);
|
|
3401
3655
|
duration(options.idleTimeout, "idleTimeout", 0);
|
|
3402
3656
|
duration(options.handshakeTimeout, "handshakeTimeout", 0);
|
|
@@ -3405,6 +3659,7 @@ function validateRealtimeOptions(options) {
|
|
|
3405
3659
|
if (!Array.isArray(options.backoff) || options.backoff.length === 0) throw new ItdConfigError("realtime.backoff должен быть непустым списком пауз");
|
|
3406
3660
|
for (const delay of options.backoff) duration(delay, "backoff", 0);
|
|
3407
3661
|
}
|
|
3662
|
+
if (options.sequentialize !== void 0 && typeof options.sequentialize !== "function") throw new ItdConfigError("realtime.sequentialize должен быть функцией");
|
|
3408
3663
|
}
|
|
3409
3664
|
/**
|
|
3410
3665
|
* Поток уведомлений в реальном времени.
|
|
@@ -3414,22 +3669,26 @@ function validateRealtimeOptions(options) {
|
|
|
3414
3669
|
*
|
|
3415
3670
|
* @example
|
|
3416
3671
|
* ```ts
|
|
3672
|
+
* import { NotificationType } from 'itd-api';
|
|
3673
|
+
*
|
|
3417
3674
|
* const stream = itd.realtime();
|
|
3418
3675
|
*
|
|
3419
|
-
* stream.
|
|
3420
|
-
*
|
|
3676
|
+
* stream.onNotification(NotificationType.PostComment, async ({ update }) => {
|
|
3677
|
+
* await saveCommentNotification(update.data.notification);
|
|
3421
3678
|
* });
|
|
3422
3679
|
* stream.on('status', (status) => console.log('соединение:', status));
|
|
3423
3680
|
*
|
|
3424
3681
|
* await stream.connect();
|
|
3425
3682
|
* // …позже
|
|
3426
3683
|
* stream.disconnect();
|
|
3684
|
+
* await stream.drain();
|
|
3427
3685
|
* ```
|
|
3428
3686
|
*/
|
|
3429
3687
|
var ItdRealtime = class {
|
|
3430
3688
|
#deps;
|
|
3431
3689
|
#options;
|
|
3432
3690
|
#emitter;
|
|
3691
|
+
#dispatcher;
|
|
3433
3692
|
#transport;
|
|
3434
3693
|
#maxAttempts;
|
|
3435
3694
|
#controller;
|
|
@@ -3444,7 +3703,7 @@ var ItdRealtime = class {
|
|
|
3444
3703
|
#wanted = false;
|
|
3445
3704
|
#status = RealtimeStatus.Disconnected;
|
|
3446
3705
|
#attempt = 0;
|
|
3447
|
-
#
|
|
3706
|
+
#cancelTimer;
|
|
3448
3707
|
#detachEnvironment;
|
|
3449
3708
|
constructor(deps, options = {}) {
|
|
3450
3709
|
validateRealtimeOptions(options);
|
|
@@ -3457,6 +3716,14 @@ var ItdRealtime = class {
|
|
|
3457
3716
|
if (deps.logger) deps.logger.error(message, error);
|
|
3458
3717
|
else console.error(`[itd-api] ${message}`, error);
|
|
3459
3718
|
});
|
|
3719
|
+
this.#dispatcher = new RealtimeDispatcher({
|
|
3720
|
+
concurrency: options.concurrency ?? 1,
|
|
3721
|
+
...options.sequentialize ? { sequentialize: options.sequentialize } : {}
|
|
3722
|
+
}, {
|
|
3723
|
+
deliver: (context) => this.#deliver(context.update),
|
|
3724
|
+
middlewareError: (error, context) => this.#reportDispatchError("middlewareError", error, context),
|
|
3725
|
+
handlerError: (error, context) => this.#reportDispatchError("handlerError", error, context)
|
|
3726
|
+
});
|
|
3460
3727
|
}
|
|
3461
3728
|
/** Текущее состояние соединения. */
|
|
3462
3729
|
get status() {
|
|
@@ -3487,6 +3754,40 @@ var ItdRealtime = class {
|
|
|
3487
3754
|
return this.#emitter.once(event, listener);
|
|
3488
3755
|
}
|
|
3489
3756
|
/**
|
|
3757
|
+
* Добавляет промежуточный обработчик нормализованных обновлений.
|
|
3758
|
+
*
|
|
3759
|
+
* Обработчики выполняются в порядке регистрации. Если `next()` не вызван, обновление не
|
|
3760
|
+
* передаётся дальше по цепочке, асинхронным обработчикам и слушателям событий.
|
|
3761
|
+
*
|
|
3762
|
+
* @returns функция удаления обработчика
|
|
3763
|
+
*/
|
|
3764
|
+
use(middleware) {
|
|
3765
|
+
if (typeof middleware !== "function") throw new ItdConfigError("realtime.use() принимает функцию обработки");
|
|
3766
|
+
return this.#dispatcher.use(middleware);
|
|
3767
|
+
}
|
|
3768
|
+
onUpdate(selectorOrHandler, selectedHandler) {
|
|
3769
|
+
const selectAll = selectedHandler === void 0;
|
|
3770
|
+
const handler = selectAll ? selectorOrHandler : selectedHandler;
|
|
3771
|
+
if (typeof handler !== "function") throw new ItdConfigError("realtime.onUpdate() принимает функцию обработчика");
|
|
3772
|
+
if (!selectAll && typeof selectorOrHandler !== "function" && !Object.values(RealtimeUpdateType).includes(selectorOrHandler)) throw new ItdConfigError(`Неизвестный тип обновления потока: ${String(selectorOrHandler)}`);
|
|
3773
|
+
let predicate;
|
|
3774
|
+
if (selectAll) predicate = () => true;
|
|
3775
|
+
else {
|
|
3776
|
+
const selector = selectorOrHandler;
|
|
3777
|
+
predicate = typeof selector === "function" ? selector : (context) => context.update.type === selector;
|
|
3778
|
+
}
|
|
3779
|
+
return this.#dispatcher.on(predicate, handler);
|
|
3780
|
+
}
|
|
3781
|
+
onNotification(selector, handler) {
|
|
3782
|
+
if (typeof handler !== "function") throw new ItdConfigError("realtime.onNotification() принимает функцию обработчика");
|
|
3783
|
+
if (typeof selector !== "function") validateNotificationSelector(selector);
|
|
3784
|
+
const predicate = (context) => {
|
|
3785
|
+
if (!isNotificationContext(context)) return false;
|
|
3786
|
+
return typeof selector === "function" ? selector(context) : matchesNotification(context, selector);
|
|
3787
|
+
};
|
|
3788
|
+
return this.#dispatcher.on(predicate, handler);
|
|
3789
|
+
}
|
|
3790
|
+
/**
|
|
3490
3791
|
* Поднимает соединение.
|
|
3491
3792
|
*
|
|
3492
3793
|
* Повторный вызов при уже живом соединении ничего не делает — это защита от двойного
|
|
@@ -3497,9 +3798,14 @@ var ItdRealtime = class {
|
|
|
3497
3798
|
async connect() {
|
|
3498
3799
|
if (this.#wanted) return;
|
|
3499
3800
|
this.#wanted = true;
|
|
3801
|
+
this.#deps.onConnect?.();
|
|
3500
3802
|
this.#attachEnvironmentListeners();
|
|
3501
3803
|
if (this.#options.syncCount !== false) try {
|
|
3502
|
-
|
|
3804
|
+
const count = await this.#deps.fetchUnreadCount();
|
|
3805
|
+
if (this.#wanted) this.#dispatch({
|
|
3806
|
+
type: RealtimeUpdateType.UnreadCount,
|
|
3807
|
+
data: count
|
|
3808
|
+
}, void 0, RealtimeUpdateOrigin.Sync);
|
|
3503
3809
|
} catch (error) {
|
|
3504
3810
|
this.#deps.logger?.debug("не удалось получить число непрочитанных", error);
|
|
3505
3811
|
}
|
|
@@ -3508,27 +3814,36 @@ var ItdRealtime = class {
|
|
|
3508
3814
|
/** Закрывает соединение и отменяет запланированные попытки. */
|
|
3509
3815
|
disconnect() {
|
|
3510
3816
|
this.#wanted = false;
|
|
3511
|
-
if (this.#
|
|
3512
|
-
|
|
3513
|
-
this.#
|
|
3817
|
+
if (this.#cancelTimer) {
|
|
3818
|
+
this.#cancelTimer();
|
|
3819
|
+
this.#cancelTimer = void 0;
|
|
3514
3820
|
}
|
|
3515
3821
|
this.#detachEnvironment?.();
|
|
3516
3822
|
this.#detachEnvironment = void 0;
|
|
3517
3823
|
this.#controller?.abort();
|
|
3518
3824
|
this.#controller = void 0;
|
|
3519
3825
|
this.#attempt = 0;
|
|
3826
|
+
this.#dispatcher.clearPending();
|
|
3520
3827
|
this.#setStatus(RealtimeStatus.Disconnected);
|
|
3521
3828
|
this.#deps.onClose?.();
|
|
3522
3829
|
}
|
|
3523
|
-
/**
|
|
3830
|
+
/** Ждёт завершения всех принятых обновлений. */
|
|
3831
|
+
drain() {
|
|
3832
|
+
return this.#dispatcher.drain();
|
|
3833
|
+
}
|
|
3834
|
+
/** Снимает подписки `on()` и `once()`. Остальные обработчики остаются. */
|
|
3524
3835
|
removeAllListeners() {
|
|
3525
3836
|
this.#emitter.removeAllListeners();
|
|
3526
3837
|
}
|
|
3527
3838
|
#createTransport() {
|
|
3528
3839
|
const kind = this.#options.transport ?? RealtimeTransportKind.Auto;
|
|
3529
3840
|
if (typeof kind === "object") return kind;
|
|
3530
|
-
if (kind === RealtimeTransportKind.Poll || kind === RealtimeTransportKind.Auto && !supportsStreamingBody()) return new PollTransport({
|
|
3841
|
+
if (kind === RealtimeTransportKind.Poll || kind === RealtimeTransportKind.Auto && !supportsStreamingBody()) return new PollTransport({
|
|
3842
|
+
clock: this.#deps.clock ?? systemClock,
|
|
3843
|
+
...this.#options.pollInterval !== void 0 ? { interval: this.#options.pollInterval } : {}
|
|
3844
|
+
});
|
|
3531
3845
|
return new SseTransport({
|
|
3846
|
+
clock: this.#deps.clock ?? systemClock,
|
|
3532
3847
|
...this.#options.idleTimeout !== void 0 ? { idleTimeout: this.#options.idleTimeout } : {},
|
|
3533
3848
|
...this.#options.handshakeTimeout !== void 0 ? { handshakeTimeout: this.#options.handshakeTimeout } : {}
|
|
3534
3849
|
});
|
|
@@ -3550,7 +3865,7 @@ var ItdRealtime = class {
|
|
|
3550
3865
|
this.#attempt = 0;
|
|
3551
3866
|
this.#setStatus(RealtimeStatus.Connected);
|
|
3552
3867
|
},
|
|
3553
|
-
onEvent: (event) => this.#handleEvent(event
|
|
3868
|
+
onEvent: (event) => this.#handleEvent(event),
|
|
3554
3869
|
onParseError: (error, raw) => this.#emitter.emit("parseError", {
|
|
3555
3870
|
error,
|
|
3556
3871
|
raw
|
|
@@ -3562,25 +3877,48 @@ var ItdRealtime = class {
|
|
|
3562
3877
|
this.#handleFailure(error);
|
|
3563
3878
|
});
|
|
3564
3879
|
}
|
|
3565
|
-
#handleEvent(
|
|
3566
|
-
this.#
|
|
3567
|
-
|
|
3568
|
-
|
|
3880
|
+
#handleEvent(event) {
|
|
3881
|
+
if (!this.#wanted) return;
|
|
3882
|
+
this.#emitter.emit("message", event);
|
|
3883
|
+
if (event.name === "connected") {
|
|
3884
|
+
this.#emitter.emit("ready", { userId: pickString(event.data, "userId") });
|
|
3885
|
+
return;
|
|
3886
|
+
}
|
|
3887
|
+
const update = readRealtimeUpdate(event);
|
|
3888
|
+
if (update) this.#dispatch(update, event, RealtimeUpdateOrigin.Stream);
|
|
3889
|
+
}
|
|
3890
|
+
#dispatch(update, raw, origin) {
|
|
3891
|
+
this.#dispatcher.dispatch({
|
|
3892
|
+
update,
|
|
3893
|
+
stream: this,
|
|
3894
|
+
raw,
|
|
3895
|
+
origin
|
|
3569
3896
|
});
|
|
3570
|
-
|
|
3571
|
-
|
|
3897
|
+
}
|
|
3898
|
+
#deliver(update) {
|
|
3899
|
+
if (update.type === RealtimeUpdateType.Notification) {
|
|
3900
|
+
this.#emitter.emit("notification", update.data);
|
|
3901
|
+
if (update.data.unreadCount !== void 0) this.#emitter.emit("unreadCount", update.data.unreadCount);
|
|
3572
3902
|
return;
|
|
3573
3903
|
}
|
|
3574
|
-
if (
|
|
3575
|
-
|
|
3576
|
-
this.#emitter.emit("notification", event);
|
|
3577
|
-
if (event.unreadCount !== void 0) this.#emitter.emit("unreadCount", event.unreadCount);
|
|
3904
|
+
if (update.type === RealtimeUpdateType.UnreadCount) {
|
|
3905
|
+
this.#emitter.emit("unreadCount", update.data);
|
|
3578
3906
|
return;
|
|
3579
3907
|
}
|
|
3580
|
-
if (
|
|
3581
|
-
|
|
3582
|
-
|
|
3908
|
+
if (update.type === RealtimeUpdateType.Unknown) return;
|
|
3909
|
+
assertNeverUpdate(update);
|
|
3910
|
+
}
|
|
3911
|
+
#reportDispatchError(event, error, context) {
|
|
3912
|
+
if (this.#emitter.listenerCount(event) > 0) {
|
|
3913
|
+
this.#emitter.emit(event, {
|
|
3914
|
+
error,
|
|
3915
|
+
context
|
|
3916
|
+
});
|
|
3917
|
+
return;
|
|
3583
3918
|
}
|
|
3919
|
+
const message = event === "middlewareError" ? "Ошибка в промежуточном обработчике потока" : "Ошибка в обработчике обновления потока";
|
|
3920
|
+
if (this.#deps.logger) this.#deps.logger.error(message, error);
|
|
3921
|
+
else console.error(`[itd-api] ${message}`, error);
|
|
3584
3922
|
}
|
|
3585
3923
|
#handleFailure(error) {
|
|
3586
3924
|
this.#controller = void 0;
|
|
@@ -3618,8 +3956,8 @@ var ItdRealtime = class {
|
|
|
3618
3956
|
attempt: this.#attempt,
|
|
3619
3957
|
delay
|
|
3620
3958
|
});
|
|
3621
|
-
this.#
|
|
3622
|
-
this.#
|
|
3959
|
+
this.#cancelTimer = (this.#deps.clock ?? systemClock).schedule(() => {
|
|
3960
|
+
this.#cancelTimer = void 0;
|
|
3623
3961
|
this.#run();
|
|
3624
3962
|
}, delay);
|
|
3625
3963
|
}
|
|
@@ -3646,7 +3984,7 @@ var ItdRealtime = class {
|
|
|
3646
3984
|
const target = globalThis;
|
|
3647
3985
|
if (typeof target.addEventListener !== "function") return;
|
|
3648
3986
|
const wake = () => {
|
|
3649
|
-
if (this.#controller || this.#
|
|
3987
|
+
if (this.#controller || this.#cancelTimer) return;
|
|
3650
3988
|
if (this.#status === RealtimeStatus.Disconnected) return;
|
|
3651
3989
|
this.#attempt = 0;
|
|
3652
3990
|
this.#run();
|
|
@@ -3668,6 +4006,9 @@ var ItdRealtime = class {
|
|
|
3668
4006
|
this.#emitter.emit("status", status);
|
|
3669
4007
|
}
|
|
3670
4008
|
};
|
|
4009
|
+
function assertNeverUpdate(update) {
|
|
4010
|
+
throw new TypeError(`Необработанное обновление потока: ${String(update)}`);
|
|
4011
|
+
}
|
|
3671
4012
|
//#endregion
|
|
3672
4013
|
//#region src/core/pagination.ts
|
|
3673
4014
|
/** Схема пагинации эндпоинта. */
|
|
@@ -7572,7 +7913,7 @@ var ItdClient = class ItdClient {
|
|
|
7572
7913
|
for (const service of BUILT_IN_SERVICES) this.#services.define(service);
|
|
7573
7914
|
for (const service of config.services) this.#services.define(service);
|
|
7574
7915
|
const shared = config.rateLimit ? internals.queues : void 0;
|
|
7575
|
-
const queues = shared ?? (config.rateLimit ? new RequestQueuePool(config.rateLimit) : void 0);
|
|
7916
|
+
const queues = shared ?? (config.rateLimit ? new RequestQueuePool(config.rateLimit, config.clock) : void 0);
|
|
7576
7917
|
this.#queues = queues;
|
|
7577
7918
|
this.#ownsQueues = shared === void 0;
|
|
7578
7919
|
let authManager;
|
|
@@ -7588,6 +7929,7 @@ var ItdClient = class ItdClient {
|
|
|
7588
7929
|
this.#transport = transport;
|
|
7589
7930
|
const pluginsLayer = createPluginsMiddleware(this.#plugins);
|
|
7590
7931
|
const retriesLayer = createRetryMiddleware({
|
|
7932
|
+
clock: config.clock,
|
|
7591
7933
|
retry: config.retry,
|
|
7592
7934
|
rateLimitDelays: config.rateLimit?.retryDelays ?? [],
|
|
7593
7935
|
pauseQueue: queues ? (ms, request) => queues.for(request.service).pause(ms) : void 0,
|
|
@@ -7767,10 +8109,12 @@ var ItdClient = class ItdClient {
|
|
|
7767
8109
|
*
|
|
7768
8110
|
* @example
|
|
7769
8111
|
* ```ts
|
|
8112
|
+
* import { NotificationType } from 'itd-api';
|
|
8113
|
+
*
|
|
7770
8114
|
* const stream = itd.realtime();
|
|
7771
8115
|
*
|
|
7772
|
-
* stream.
|
|
7773
|
-
*
|
|
8116
|
+
* stream.onNotification(NotificationType.PostComment, async ({ update }) => {
|
|
8117
|
+
* await handleComment(update.data.notification);
|
|
7774
8118
|
* });
|
|
7775
8119
|
* stream.on('unreadCount', (count) => setBadge(count));
|
|
7776
8120
|
*
|
|
@@ -7788,8 +8132,10 @@ var ItdClient = class ItdClient {
|
|
|
7788
8132
|
getToken: () => this.#authManager.getAccessToken(),
|
|
7789
8133
|
refresh: () => this.#authManager.onUnauthorized(),
|
|
7790
8134
|
fetchUnreadCount: () => this.notifications.count(),
|
|
8135
|
+
onConnect: () => this.#streams.add(stream),
|
|
7791
8136
|
onClose: () => this.#streams.delete(stream),
|
|
7792
|
-
logger: this.#config.logger
|
|
8137
|
+
logger: this.#config.logger,
|
|
8138
|
+
clock: this.#config.clock
|
|
7793
8139
|
}, options);
|
|
7794
8140
|
this.#streams.add(stream);
|
|
7795
8141
|
return stream;
|
|
@@ -7798,8 +8144,8 @@ var ItdClient = class ItdClient {
|
|
|
7798
8144
|
* Освобождает ресурсы клиента: закрывает все потоки уведомлений, отправляет открытые
|
|
7799
8145
|
* накопители {@link telemetry}, затем останавливает очередь запросов.
|
|
7800
8146
|
*
|
|
7801
|
-
* После вызова клиентом можно пользоваться
|
|
7802
|
-
*
|
|
8147
|
+
* Метод дожидается активных обработчиков потока. После вызова клиентом можно пользоваться
|
|
8148
|
+
* снова; ранее созданный поток можно запустить повторным `connect()`.
|
|
7803
8149
|
*
|
|
7804
8150
|
* Общая очередь, полученная от {@link ItdAccounts}, не останавливается: её гасит сам
|
|
7805
8151
|
* контейнер, когда закрывает все аккаунты разом.
|
|
@@ -7812,8 +8158,9 @@ var ItdClient = class ItdClient {
|
|
|
7812
8158
|
* ```
|
|
7813
8159
|
*/
|
|
7814
8160
|
async close() {
|
|
7815
|
-
this.#disconnectStreams();
|
|
8161
|
+
const streams = this.#disconnectStreams();
|
|
7816
8162
|
try {
|
|
8163
|
+
await Promise.all(streams.map((stream) => stream.drain()));
|
|
7817
8164
|
await this.telemetry.close();
|
|
7818
8165
|
} finally {
|
|
7819
8166
|
if (this.#ownsQueues) this.#queues?.stop();
|
|
@@ -7832,8 +8179,10 @@ var ItdClient = class ItdClient {
|
|
|
7832
8179
|
}
|
|
7833
8180
|
/** Завершает потоки до того, как запросы начнут использовать другой аккаунт. */
|
|
7834
8181
|
#disconnectStreams() {
|
|
7835
|
-
|
|
8182
|
+
const streams = [...this.#streams];
|
|
8183
|
+
for (const stream of streams) stream.disconnect();
|
|
7836
8184
|
this.#streams.clear();
|
|
8185
|
+
return streams;
|
|
7837
8186
|
}
|
|
7838
8187
|
/** Позволяет использовать клиент с `await using`. */
|
|
7839
8188
|
[Symbol.asyncDispose]() {
|
|
@@ -7972,7 +8321,7 @@ var ItdAccounts = class ItdAccounts {
|
|
|
7972
8321
|
this.#plugins = orderPluginDefinitions(plugins ?? []);
|
|
7973
8322
|
this.#rateLimitScope = rateLimitScope ?? "account";
|
|
7974
8323
|
const rateLimit = this.#rateLimitScope === "shared" ? resolveRateLimit(base.rateLimit) : void 0;
|
|
7975
|
-
this.#queues = rateLimit ? new RequestQueuePool(rateLimit) : void 0;
|
|
8324
|
+
this.#queues = rateLimit ? new RequestQueuePool(rateLimit, base.clock ?? systemClock) : void 0;
|
|
7976
8325
|
const logger = typeof base.logger === "object" ? base.logger : void 0;
|
|
7977
8326
|
this.#logger = logger;
|
|
7978
8327
|
this.#emitter = new Emitter((error) => reportListenerError(logger, "аккаунтов", error));
|
|
@@ -8432,6 +8781,87 @@ function resolveNotificationUrl(notification) {
|
|
|
8432
8781
|
return clickUrl || "/notifications";
|
|
8433
8782
|
}
|
|
8434
8783
|
//#endregion
|
|
8784
|
+
//#region src/realtime/router.ts
|
|
8785
|
+
/**
|
|
8786
|
+
* Направляет обновления потока в именованные цепочки промежуточных обработчиков.
|
|
8787
|
+
*
|
|
8788
|
+
* @example
|
|
8789
|
+
* ```ts
|
|
8790
|
+
* import { RealtimeRouter, RealtimeUpdateType } from 'itd-api';
|
|
8791
|
+
*
|
|
8792
|
+
* const router = new RealtimeRouter((context) => context.update.type);
|
|
8793
|
+
* router.route(RealtimeUpdateType.Notification, async (context, next) => {
|
|
8794
|
+
* if (context.update.type === RealtimeUpdateType.Notification) {
|
|
8795
|
+
* await handleNotification(context.update.data.notification);
|
|
8796
|
+
* }
|
|
8797
|
+
* await next();
|
|
8798
|
+
* });
|
|
8799
|
+
* stream.use(router.middleware());
|
|
8800
|
+
* ```
|
|
8801
|
+
*/
|
|
8802
|
+
var RealtimeRouter = class {
|
|
8803
|
+
#selector;
|
|
8804
|
+
#routes = /* @__PURE__ */ new Map();
|
|
8805
|
+
#fallback = [];
|
|
8806
|
+
constructor(selector) {
|
|
8807
|
+
if (typeof selector !== "function") throw new ItdConfigError("RealtimeRouter принимает функцию выбора маршрута");
|
|
8808
|
+
this.#selector = selector;
|
|
8809
|
+
}
|
|
8810
|
+
/** Добавляет промежуточные обработчики к маршруту и возвращает функцию их удаления. */
|
|
8811
|
+
route(key, ...middleware) {
|
|
8812
|
+
if (!isPropertyKey(key)) throw new ItdConfigError("Ключ realtime route должен быть PropertyKey");
|
|
8813
|
+
const registration = this.#registration(middleware);
|
|
8814
|
+
const registrations = this.#routes.get(key) ?? [];
|
|
8815
|
+
registrations.push(registration);
|
|
8816
|
+
this.#routes.set(key, registrations);
|
|
8817
|
+
return () => {
|
|
8818
|
+
const current = this.#routes.get(key);
|
|
8819
|
+
if (!current) return;
|
|
8820
|
+
const index = current.indexOf(registration);
|
|
8821
|
+
if (index >= 0) current.splice(index, 1);
|
|
8822
|
+
if (current.length === 0) this.#routes.delete(key);
|
|
8823
|
+
};
|
|
8824
|
+
}
|
|
8825
|
+
/** Добавляет промежуточные обработчики для обновлений без зарегистрированного маршрута. */
|
|
8826
|
+
otherwise(...middleware) {
|
|
8827
|
+
const registration = this.#registration(middleware);
|
|
8828
|
+
this.#fallback.push(registration);
|
|
8829
|
+
return () => {
|
|
8830
|
+
const index = this.#fallback.indexOf(registration);
|
|
8831
|
+
if (index >= 0) this.#fallback.splice(index, 1);
|
|
8832
|
+
};
|
|
8833
|
+
}
|
|
8834
|
+
/** Возвращает промежуточный обработчик для `stream.use()`. */
|
|
8835
|
+
middleware() {
|
|
8836
|
+
const middleware = (context, next) => this.#captureMiddleware()(context, next);
|
|
8837
|
+
return withRealtimeMiddlewareSnapshot(middleware, () => this.#captureMiddleware());
|
|
8838
|
+
}
|
|
8839
|
+
#captureMiddleware() {
|
|
8840
|
+
const routes = /* @__PURE__ */ new Map();
|
|
8841
|
+
for (const [key, registrations] of this.#routes) routes.set(key, registrations.flatMap(({ middleware }) => middleware).map(captureRealtimeMiddleware));
|
|
8842
|
+
const fallback = this.#fallback.flatMap(({ middleware }) => middleware).map(captureRealtimeMiddleware);
|
|
8843
|
+
return async (context, next) => {
|
|
8844
|
+
const key = await this.#selector(context);
|
|
8845
|
+
if (key != null && !isPropertyKey(key)) throw new ItdConfigError("Функция выбора маршрута должна возвращать PropertyKey, null или undefined");
|
|
8846
|
+
const route = key == null ? void 0 : routes.get(key);
|
|
8847
|
+
const chain = route && route.length > 0 ? route : fallback;
|
|
8848
|
+
if (chain.length === 0) {
|
|
8849
|
+
await next();
|
|
8850
|
+
return;
|
|
8851
|
+
}
|
|
8852
|
+
await runRealtimeMiddleware(chain, context, next);
|
|
8853
|
+
};
|
|
8854
|
+
}
|
|
8855
|
+
#registration(middleware) {
|
|
8856
|
+
if (middleware.length === 0) throw new ItdConfigError("Маршрут должен содержать хотя бы один обработчик");
|
|
8857
|
+
for (const item of middleware) if (typeof item !== "function") throw new ItdConfigError("Маршрут принимает только функции обработки");
|
|
8858
|
+
return { middleware: [...middleware] };
|
|
8859
|
+
}
|
|
8860
|
+
};
|
|
8861
|
+
function isPropertyKey(value) {
|
|
8862
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "symbol";
|
|
8863
|
+
}
|
|
8864
|
+
//#endregion
|
|
8435
8865
|
//#region src/spans/render.ts
|
|
8436
8866
|
/** Формат результата {@link renderSpans}. */
|
|
8437
8867
|
const SpanRenderFormat = Object.freeze({
|
|
@@ -8684,6 +9114,6 @@ function statusDays(service) {
|
|
|
8684
9114
|
return Array.from({ length: STATUS_WINDOW_DAYS }, (_, index) => service.days[String(index)] ?? null);
|
|
8685
9115
|
}
|
|
8686
9116
|
//#endregion
|
|
8687
|
-
export { ALLOWED_MIME_TYPES, AUDIO_MIME_TYPES, AUTH_FLAG_COOKIE, AUTH_PATHS, AccessType, AttachmentType, BUILT_IN_SERVICES, CommentSort, DEFAULT_BASE_URL, DEFAULT_FILE_STREAM_BUFFER_BYTES, DEFAULT_STATUS_BASE_URL, DEFAULT_TIMEOUT, DEFAULT_UPLOAD_TIMEOUT, DEFAULT_URL_FILE_MAX_BYTES, DEFAULT_USER_AGENT, DEVICE_ID_HEADER, DetectedRuntime, FeedTab, FileTransferMode, IMAGE_MIME_TYPES, IncidentKind, InteractionType, ItdAbortError, ItdAccounts, ItdApiError, ItdApiErrorKind, ItdAuthError, ItdClient, ItdConfigError, ItdConflictError, ItdError, ItdErrorCode, ItdErrorKind, ItdFileError, ItdFileErrorReason, ItdForbiddenError, ItdNetworkError, ItdNotFoundError, ItdPhoneVerificationError, ItdRateLimitError, ItdRealtime, ItdServerError, ItdTimeoutError, ItdValidationError, LIBRARY_VERSION, LikesVisibility, MAX_RECONNECT_ATTEMPTS, MemoryMultiTokenStorage, MemoryTokenStorage, NOTIFICATION_TYPE_ALIASES, NotificationType, PaginationMode, Paginator, RECONNECT_BACKOFF, RECONNECT_JITTER, REFRESH_COOKIE, REFRESH_COOKIE_PATH, REQUEST_OPTION_KEYS, RealtimeStatus, RealtimeTransportKind, ReportReason, ReportTargetType, RuntimeMode, STATUS_SERVICE, STREAM_PATH, ServiceRegistry, ServiceState, SignInStatus, SpanRenderFormat, SpanType, TURNSTILE_SITE_KEY, UnauthorizedStreamError, VIDEO_MIME_TYPES, ViewReason, ViewSource, WallAccess, autoSpans, canonicalNotificationType, comment, createAccounts, createClient, createMultiTokenStorage, createRecordMultiStorage, createTokenStorage, formatNotificationText, fromStream, fromUrl, isBuilder, isItdApiError, isItdAuthError, isItdConflictError, isItdError, isItdFileError, isItdForbiddenError, isItdNotFoundError, isItdPhoneVerificationError, isItdRateLimitError, isItdServerError, isItdValidationError, isKnownNotificationType, isMyProfile, mapPage, markup, normalizeNotification, parseHtml, parseMarkdown, poll, post, readNotificationEvent, readUnreadCountEvent, renderSpans, report, resolveNotificationUrl, scopedTokenStorage, statusDays, toDate, utcStampToIso };
|
|
9117
|
+
export { ALLOWED_MIME_TYPES, AUDIO_MIME_TYPES, AUTH_FLAG_COOKIE, AUTH_PATHS, AccessType, AttachmentType, BUILT_IN_SERVICES, CommentSort, DEFAULT_BASE_URL, DEFAULT_FILE_STREAM_BUFFER_BYTES, DEFAULT_STATUS_BASE_URL, DEFAULT_TIMEOUT, DEFAULT_UPLOAD_TIMEOUT, DEFAULT_URL_FILE_MAX_BYTES, DEFAULT_USER_AGENT, DEVICE_ID_HEADER, DetectedRuntime, FeedTab, FileTransferMode, IMAGE_MIME_TYPES, IncidentKind, InteractionType, ItdAbortError, ItdAccounts, ItdApiError, ItdApiErrorKind, ItdAuthError, ItdClient, ItdConfigError, ItdConflictError, ItdError, ItdErrorCode, ItdErrorKind, ItdFileError, ItdFileErrorReason, ItdForbiddenError, ItdNetworkError, ItdNotFoundError, ItdPhoneVerificationError, ItdRateLimitError, ItdRealtime, ItdServerError, ItdTimeoutError, ItdValidationError, LIBRARY_VERSION, LikesVisibility, MAX_RECONNECT_ATTEMPTS, MemoryMultiTokenStorage, MemoryTokenStorage, NOTIFICATION_TYPE_ALIASES, NotificationType, PaginationMode, Paginator, RECONNECT_BACKOFF, RECONNECT_JITTER, REFRESH_COOKIE, REFRESH_COOKIE_PATH, REQUEST_OPTION_KEYS, RealtimeRouter, RealtimeStatus, RealtimeTransportKind, RealtimeUpdateOrigin, RealtimeUpdateType, ReportReason, ReportTargetType, RuntimeMode, STATUS_SERVICE, STREAM_PATH, ServiceRegistry, ServiceState, SignInStatus, SpanRenderFormat, SpanType, TURNSTILE_SITE_KEY, UnauthorizedStreamError, VIDEO_MIME_TYPES, ViewReason, ViewSource, WallAccess, autoSpans, canonicalNotificationType, comment, createAccounts, createClient, createMultiTokenStorage, createRecordMultiStorage, createTokenStorage, formatNotificationText, fromStream, fromUrl, isBuilder, isItdApiError, isItdAuthError, isItdConflictError, isItdError, isItdFileError, isItdForbiddenError, isItdNotFoundError, isItdPhoneVerificationError, isItdRateLimitError, isItdServerError, isItdValidationError, isKnownNotificationType, isMyProfile, mapPage, markup, normalizeNotification, parseHtml, parseMarkdown, poll, post, readNotificationEvent, readUnreadCountEvent, renderSpans, report, resolveNotificationUrl, scopedTokenStorage, statusDays, systemClock, toDate, utcStampToIso };
|
|
8688
9118
|
|
|
8689
9119
|
//# sourceMappingURL=index.js.map
|