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