itd-api 0.7.2 → 0.9.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 +72 -27
- package/dist/events/index.cjs +53 -0
- package/dist/events/index.d.cts +3 -0
- package/dist/events/index.d.ts +3 -0
- package/dist/events/index.js +5 -0
- package/dist/index.cjs +234 -200
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -64
- package/dist/index.d.ts +70 -64
- package/dist/index.js +202 -179
- package/dist/index.js.map +1 -1
- package/dist/node/index.cjs +6 -5
- package/dist/node/index.cjs.map +1 -1
- package/dist/node/index.d.cts +2 -2
- package/dist/node/index.d.ts +2 -2
- package/dist/node/index.js +5 -4
- package/dist/node/index.js.map +1 -1
- package/dist/rest/index.cjs +32 -26
- package/dist/rest/index.cjs.map +1 -1
- package/dist/rest/index.d.cts +9 -7
- package/dist/rest/index.d.ts +9 -7
- package/dist/rest/index.js +19 -21
- package/dist/rest/index.js.map +1 -1
- package/dist/shared/{errors-DfU8M5eS.cjs → errors-BmP3TKoW.cjs} +3 -3
- package/dist/shared/{errors-DfU8M5eS.cjs.map → errors-BmP3TKoW.cjs.map} +1 -1
- package/dist/shared/{errors-Bhrd2fJd.js → errors-GI10kZxk.js} +3 -3
- package/dist/shared/{errors-Bhrd2fJd.js.map → errors-GI10kZxk.js.map} +1 -1
- package/dist/shared/{websocket-BLR8eVJV.js → events-C5wnTXPT.js} +770 -490
- package/dist/shared/events-C5wnTXPT.js.map +1 -0
- package/dist/shared/events-CqtkP65d.d.ts +698 -0
- package/dist/shared/events-DGAWlCaI.d.cts +698 -0
- package/dist/shared/{websocket-C_eI4H2o.cjs → events-FZfnEez0.cjs} +834 -518
- package/dist/shared/events-FZfnEez0.cjs.map +1 -0
- package/dist/shared/{storage-BPJR_k4-.cjs → key-value-store-B5GYVEYZ.cjs} +2 -86
- package/dist/shared/key-value-store-B5GYVEYZ.cjs.map +1 -0
- package/dist/shared/{storage-D86edNCB.js → key-value-store-Bt2PSMxY.js} +3 -69
- package/dist/shared/key-value-store-Bt2PSMxY.js.map +1 -0
- package/dist/shared/key-value-store-COZaNHZS.d.cts +69 -0
- package/dist/shared/key-value-store-COZaNHZS.d.ts +69 -0
- package/dist/shared/{multi-storage-DccjD7Ww.d.ts → multi-storage-BEL7tcKn.d.cts} +3 -2
- package/dist/shared/{multi-storage--yTEqiod.cjs → multi-storage-CDKLa956.cjs} +11 -10
- package/dist/shared/{multi-storage--yTEqiod.cjs.map → multi-storage-CDKLa956.cjs.map} +1 -1
- package/dist/shared/{multi-storage-CjAPB5Kq.d.cts → multi-storage-LkljOSzA.d.ts} +3 -2
- package/dist/shared/{multi-storage-CkvTUC5m.js → multi-storage-SoW05uzO.js} +5 -4
- package/dist/shared/{multi-storage-CkvTUC5m.js.map → multi-storage-SoW05uzO.js.map} +1 -1
- package/dist/shared/{options-Dg5N3r1V.cjs → options-DOJYtoti.cjs} +8 -6
- package/dist/shared/options-DOJYtoti.cjs.map +1 -0
- package/dist/shared/{options-DtATYdLr.js → options-DhgGg7Ms.js} +8 -6
- package/dist/shared/options-DhgGg7Ms.js.map +1 -0
- package/dist/shared/{cookies-DZwFq6kr.cjs → redact-BP00URmQ.cjs} +312 -2
- package/dist/shared/redact-BP00URmQ.cjs.map +1 -0
- package/dist/shared/{cookies-tX2sNwxb.js → redact-Ba5iSVYZ.js} +235 -3
- package/dist/shared/redact-Ba5iSVYZ.js.map +1 -0
- package/dist/shared/{render-CgwKdOzu.d.ts → render-B4qis7Sb.d.ts} +775 -313
- package/dist/shared/{render-DO0F5YSm.d.cts → render-DDwy2Ame.d.cts} +775 -313
- package/dist/shared/{render-mcuELiYi.cjs → render-Dqi8Owqk.cjs} +1464 -662
- package/dist/shared/render-Dqi8Owqk.cjs.map +1 -0
- package/dist/shared/{render-C6HRPs10.js → render-hiWFh2s4.js} +1366 -618
- package/dist/shared/render-hiWFh2s4.js.map +1 -0
- package/dist/shared/storage-BQrtKon9.d.cts +86 -0
- package/dist/shared/storage-CTRGT5wF.d.ts +86 -0
- package/dist/shared/storage-CfHKzVHf.js +71 -0
- package/dist/shared/storage-CfHKzVHf.js.map +1 -0
- package/dist/shared/storage-QNVxzpDB.cjs +88 -0
- package/dist/shared/storage-QNVxzpDB.cjs.map +1 -0
- package/dist/shared/{url-BaMCQpYH.cjs → url-Bsb3xE5U.cjs} +577 -466
- package/dist/shared/url-Bsb3xE5U.cjs.map +1 -0
- package/dist/shared/{url-IU0xN9wX.js → url-DP33mp3s.js} +508 -409
- package/dist/shared/url-DP33mp3s.js.map +1 -0
- package/dist/shared/{url-DTfZ2toq.d.ts → url-XBsMdAcv.d.cts} +1313 -1200
- package/dist/shared/{url-DTfZ2toq.d.cts → url-XBsMdAcv.d.ts} +1313 -1200
- package/dist/web/index.cjs +5 -4
- package/dist/web/index.cjs.map +1 -1
- package/dist/web/index.d.cts +1 -1
- package/dist/web/index.d.ts +1 -1
- package/dist/web/index.js +3 -2
- package/dist/web/index.js.map +1 -1
- package/package.json +16 -15
- package/dist/realtime/index.cjs +0 -165
- package/dist/realtime/index.cjs.map +0 -1
- package/dist/realtime/index.d.cts +0 -51
- package/dist/realtime/index.d.ts +0 -51
- package/dist/realtime/index.js +0 -120
- package/dist/realtime/index.js.map +0 -1
- package/dist/shared/auth-provider-CG8oCQ9F.cjs +0 -108
- package/dist/shared/auth-provider-CG8oCQ9F.cjs.map +0 -1
- package/dist/shared/auth-provider-mYqxsSVa.js +0 -91
- package/dist/shared/auth-provider-mYqxsSVa.js.map +0 -1
- package/dist/shared/cookies-DZwFq6kr.cjs.map +0 -1
- package/dist/shared/cookies-tX2sNwxb.js.map +0 -1
- package/dist/shared/options-Dg5N3r1V.cjs.map +0 -1
- package/dist/shared/options-DtATYdLr.js.map +0 -1
- package/dist/shared/render-C6HRPs10.js.map +0 -1
- package/dist/shared/render-mcuELiYi.cjs.map +0 -1
- package/dist/shared/storage-BPJR_k4-.cjs.map +0 -1
- package/dist/shared/storage-C_eICCep.d.cts +0 -152
- package/dist/shared/storage-C_eICCep.d.ts +0 -152
- package/dist/shared/storage-D86edNCB.js.map +0 -1
- package/dist/shared/url-BaMCQpYH.cjs.map +0 -1
- package/dist/shared/url-IU0xN9wX.js.map +0 -1
- package/dist/shared/websocket-BLR8eVJV.js.map +0 -1
- package/dist/shared/websocket-C_eI4H2o.cjs.map +0 -1
- package/dist/shared/websocket-DF7XIMiX.d.cts +0 -562
- package/dist/shared/websocket-DYKBr8HF.d.ts +0 -562
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as ItdConfigError, x as isItdAuthError } from "./errors-
|
|
3
|
-
import { a as
|
|
1
|
+
import { A as ITD_CATALOG, G as pickString, U as pickNumber, V as pickArray, g as EventChannelStatus, i as bearerToken, l as readNotificationEvent, lt as systemClock, o as passthroughOperation, r as anonymousAuth, u as readUnreadCountEvent, ut as installAsyncDisposeFallback, z as createClientRuntime } from "./url-DP33mp3s.js";
|
|
2
|
+
import { _ as ItdStateError, a as ItdConfigError, t as ItdAbortError, x as isItdAuthError } from "./errors-GI10kZxk.js";
|
|
3
|
+
import { a as redactUrl, m as joinUrl, t as maskSecret, u as buildQuery, w as supportsStreamingBody } from "./redact-Ba5iSVYZ.js";
|
|
4
4
|
//#region src/core/emitter.ts
|
|
5
5
|
/**
|
|
6
6
|
* Минимальный типизированный источник событий.
|
|
@@ -26,7 +26,7 @@ var Emitter = class {
|
|
|
26
26
|
*
|
|
27
27
|
* @example
|
|
28
28
|
* ```ts
|
|
29
|
-
* const off =
|
|
29
|
+
* const off = events.on('notification', (event) => console.log(event));
|
|
30
30
|
* off();
|
|
31
31
|
* ```
|
|
32
32
|
*/
|
|
@@ -84,33 +84,33 @@ function reportListenerError(logger, scope, error) {
|
|
|
84
84
|
else console.error(`[itd-api] ${message}`, error);
|
|
85
85
|
}
|
|
86
86
|
//#endregion
|
|
87
|
-
//#region src/
|
|
88
|
-
const
|
|
87
|
+
//#region src/events/middleware.ts
|
|
88
|
+
const EVENT_MIDDLEWARE_SNAPSHOT = Symbol("itd-api.events.middlewareSnapshot");
|
|
89
89
|
/** @internal */
|
|
90
|
-
function
|
|
91
|
-
const capture = middleware[
|
|
90
|
+
function captureEventMiddleware(middleware) {
|
|
91
|
+
const capture = middleware[EVENT_MIDDLEWARE_SNAPSHOT];
|
|
92
92
|
if (!capture) return middleware;
|
|
93
93
|
const snapshot = capture();
|
|
94
94
|
if (typeof snapshot !== "function") throw new TypeError("Снимок промежуточного обработчика должен быть функцией");
|
|
95
95
|
return snapshot;
|
|
96
96
|
}
|
|
97
97
|
/** @internal */
|
|
98
|
-
function
|
|
99
|
-
Object.defineProperty(middleware,
|
|
98
|
+
function withEventMiddlewareSnapshot(middleware, capture) {
|
|
99
|
+
Object.defineProperty(middleware, EVENT_MIDDLEWARE_SNAPSHOT, { value: capture });
|
|
100
100
|
return middleware;
|
|
101
101
|
}
|
|
102
102
|
/** Создаёт отложенный промежуточный обработчик для объекта. @internal */
|
|
103
|
-
function
|
|
103
|
+
function deferEventMiddleware(source) {
|
|
104
104
|
const capture = () => {
|
|
105
105
|
const middleware = source.middleware();
|
|
106
|
-
if (typeof middleware !== "function") throw new ItdConfigError("
|
|
107
|
-
return
|
|
106
|
+
if (typeof middleware !== "function") throw new ItdConfigError("events middleware() должен возвращать функцию обработки");
|
|
107
|
+
return captureEventMiddleware(middleware);
|
|
108
108
|
};
|
|
109
109
|
const deferred = (context, next) => capture()(context, next);
|
|
110
|
-
return
|
|
110
|
+
return withEventMiddlewareSnapshot(deferred, capture);
|
|
111
111
|
}
|
|
112
112
|
/** Выполняет промежуточные обработчики по порядку и запрещает повторный вызов `next()`. */
|
|
113
|
-
async function
|
|
113
|
+
async function runEventMiddleware(middleware, context, terminal) {
|
|
114
114
|
let lastIndex = -1;
|
|
115
115
|
const dispatch = async (index) => {
|
|
116
116
|
if (index <= lastIndex) throw new Error("next() в промежуточном обработчике вызван повторно");
|
|
@@ -148,7 +148,7 @@ async function runRealtimeMiddleware(middleware, context, terminal) {
|
|
|
148
148
|
await dispatch(0);
|
|
149
149
|
}
|
|
150
150
|
/** Планирует нормализованные обновления и отслеживает незавершённые обработчики. */
|
|
151
|
-
var
|
|
151
|
+
var EventDispatcher = class {
|
|
152
152
|
#options;
|
|
153
153
|
#hooks;
|
|
154
154
|
#middleware = [];
|
|
@@ -189,7 +189,7 @@ var RealtimeDispatcher = class {
|
|
|
189
189
|
let middleware;
|
|
190
190
|
try {
|
|
191
191
|
keys = this.#keysFor(context);
|
|
192
|
-
middleware = this.#middleware.map(
|
|
192
|
+
middleware = this.#middleware.map(captureEventMiddleware);
|
|
193
193
|
} catch (error) {
|
|
194
194
|
this.#hooks.middlewareError(error, context);
|
|
195
195
|
return;
|
|
@@ -246,7 +246,7 @@ var RealtimeDispatcher = class {
|
|
|
246
246
|
}
|
|
247
247
|
async #run(work) {
|
|
248
248
|
try {
|
|
249
|
-
await
|
|
249
|
+
await runEventMiddleware(work.middleware, work.context, async () => {
|
|
250
250
|
for (const { predicate, handler } of work.handlers) try {
|
|
251
251
|
if (predicate(work.context)) await handler(work.context);
|
|
252
252
|
} catch (error) {
|
|
@@ -269,7 +269,7 @@ var RealtimeDispatcher = class {
|
|
|
269
269
|
}
|
|
270
270
|
};
|
|
271
271
|
//#endregion
|
|
272
|
-
//#region src/
|
|
272
|
+
//#region src/events/reconnect.ts
|
|
273
273
|
/**
|
|
274
274
|
* Паузы перед попытками переподключения, мс.
|
|
275
275
|
*
|
|
@@ -301,7 +301,7 @@ function reconnectDelay(attempt, options = {}, random = Math.random) {
|
|
|
301
301
|
return Math.max(0, Math.round(base + spread));
|
|
302
302
|
}
|
|
303
303
|
//#endregion
|
|
304
|
-
//#region src/
|
|
304
|
+
//#region src/events/transports/transport.ts
|
|
305
305
|
/** Ошибка, по которой видно, что сервер отверг авторизацию потока. */
|
|
306
306
|
var UnauthorizedStreamError = class extends Error {
|
|
307
307
|
constructor() {
|
|
@@ -310,113 +310,56 @@ var UnauthorizedStreamError = class extends Error {
|
|
|
310
310
|
}
|
|
311
311
|
};
|
|
312
312
|
//#endregion
|
|
313
|
-
//#region src/
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
Unknown: "unknown"
|
|
319
|
-
});
|
|
320
|
-
/** Источники нормализованных обновлений потока. */
|
|
321
|
-
const RealtimeUpdateOrigin = Object.freeze({
|
|
322
|
-
Stream: "stream",
|
|
323
|
-
Sync: "sync"
|
|
324
|
-
});
|
|
325
|
-
/** Проверяет форму фильтра уведомлений. */
|
|
326
|
-
function validateNotificationSelector(selector) {
|
|
327
|
-
if (typeof selector === "string") {
|
|
328
|
-
if (selector.length === 0) throw new ItdConfigError("Тип уведомления не должен быть пустым");
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
if (Array.isArray(selector)) {
|
|
332
|
-
if (selector.length === 0 || selector.some((type) => typeof type !== "string" || !type)) throw new ItdConfigError("Список типов уведомлений должен содержать непустые строки");
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
if (typeof selector !== "object" || selector === null) throw new ItdConfigError("Фильтр уведомлений должен быть строкой, списком или объектом");
|
|
336
|
-
const filter = selector;
|
|
337
|
-
if (filter.type !== void 0) validateNotificationSelector(filter.type);
|
|
338
|
-
if (filter.actorId !== void 0 && typeof filter.actorId !== "string") throw new ItdConfigError("Фильтр уведомлений: actorId должен быть строкой");
|
|
339
|
-
for (const field of ["entityId", "parentEntityId"]) {
|
|
340
|
-
const value = filter[field];
|
|
341
|
-
if (value !== void 0 && value !== null && typeof value !== "string") throw new ItdConfigError(`Фильтр уведомлений: ${field} должен быть строкой или null`);
|
|
342
|
-
}
|
|
343
|
-
if (filter.predicate !== void 0 && typeof filter.predicate !== "function") throw new ItdConfigError("Фильтр уведомлений: predicate должен быть функцией");
|
|
344
|
-
}
|
|
345
|
-
/** Преобразует транспортный кадр в одно логическое обновление. */
|
|
346
|
-
function readRealtimeUpdate(event) {
|
|
347
|
-
if (event.name === "notification") return {
|
|
348
|
-
type: RealtimeUpdateType.Notification,
|
|
349
|
-
data: readNotificationEvent(event.data)
|
|
350
|
-
};
|
|
351
|
-
if (event.name === "unread_count") {
|
|
352
|
-
const count = readUnreadCountEvent(event.data);
|
|
353
|
-
return count === void 0 ? void 0 : {
|
|
354
|
-
type: RealtimeUpdateType.UnreadCount,
|
|
355
|
-
data: count
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
return {
|
|
359
|
-
type: RealtimeUpdateType.Unknown,
|
|
360
|
-
name: event.name,
|
|
361
|
-
data: event.data
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
/** Проверяет объектный или краткий фильтр уведомления. */
|
|
365
|
-
function matchesNotification(context, selector) {
|
|
366
|
-
const notification = context.update.data.notification;
|
|
367
|
-
if (typeof selector === "string") return notification.type === selector;
|
|
368
|
-
if (Array.isArray(selector)) return selector.includes(notification.type);
|
|
369
|
-
const filter = selector;
|
|
370
|
-
const types = filter.type === void 0 ? void 0 : [filter.type].flat();
|
|
371
|
-
if (types && !types.includes(notification.type)) return false;
|
|
372
|
-
if (filter.actorId !== void 0 && !notification.actors.some(({ id }) => id === filter.actorId)) return false;
|
|
373
|
-
if (filter.entityId !== void 0 && notification.entityId !== filter.entityId) return false;
|
|
374
|
-
if (filter.parentEntityId !== void 0 && notification.parentEntityId !== filter.parentEntityId) return false;
|
|
375
|
-
return filter.predicate?.(context) ?? true;
|
|
313
|
+
//#region src/events/engine.ts
|
|
314
|
+
const EVENT_CHANNEL_GIVEUP_HOOKS = /* @__PURE__ */ new WeakMap();
|
|
315
|
+
/** Регистрирует обработчик окончательной остановки канала. @internal */
|
|
316
|
+
function setEventChannelGiveUpHook(channel, hook) {
|
|
317
|
+
EVENT_CHANNEL_GIVEUP_HOOKS.set(channel, hook);
|
|
376
318
|
}
|
|
377
|
-
/** Сужает произвольный контекст потока до контекста уведомления. */
|
|
378
|
-
function isNotificationContext(context) {
|
|
379
|
-
return context.update.type === RealtimeUpdateType.Notification;
|
|
380
|
-
}
|
|
381
|
-
//#endregion
|
|
382
|
-
//#region src/realtime/engine.ts
|
|
383
319
|
/** Проверяет настройки общей механики потока. */
|
|
384
|
-
function
|
|
320
|
+
function resolveEventChannelOptions(options = {}) {
|
|
385
321
|
const positiveInteger = (value, name) => {
|
|
386
322
|
if (value === void 0) return;
|
|
387
|
-
if (!Number.isInteger(value) || value < 0) throw new ItdConfigError(`
|
|
323
|
+
if (!Number.isInteger(value) || value < 0) throw new ItdConfigError(`events.${name} должен быть целым неотрицательным числом, получено: ${value}`);
|
|
388
324
|
};
|
|
389
325
|
const duration = (value, name) => {
|
|
390
|
-
if (!Number.isFinite(value) || value < 0) throw new ItdConfigError(`
|
|
326
|
+
if (!Number.isFinite(value) || value < 0) throw new ItdConfigError(`events.${name} должен быть числом не меньше 0, получено: ${value}`);
|
|
391
327
|
};
|
|
392
328
|
positiveInteger(options.maxAttempts, "maxAttempts");
|
|
393
329
|
positiveInteger(options.concurrency, "concurrency");
|
|
394
|
-
if (options.concurrency === 0) throw new ItdConfigError("
|
|
395
|
-
if (options.jitter !== void 0 && !(options.jitter >= 0 && options.jitter <= 1)) throw new ItdConfigError(`
|
|
330
|
+
if (options.concurrency === 0) throw new ItdConfigError("events.concurrency должен быть больше нуля");
|
|
331
|
+
if (options.jitter !== void 0 && !(options.jitter >= 0 && options.jitter <= 1)) throw new ItdConfigError(`events.jitter должен быть в диапазоне 0…1, получено: ${options.jitter}`);
|
|
396
332
|
if (options.backoff !== void 0) {
|
|
397
|
-
if (!Array.isArray(options.backoff) || options.backoff.length === 0) throw new ItdConfigError("
|
|
333
|
+
if (!Array.isArray(options.backoff) || options.backoff.length === 0) throw new ItdConfigError("events.backoff должен быть непустым списком пауз");
|
|
398
334
|
for (const delay of options.backoff) duration(delay, "backoff");
|
|
399
335
|
}
|
|
400
|
-
if (options.sequentialize !== void 0 && typeof options.sequentialize !== "function") throw new ItdConfigError("
|
|
336
|
+
if (options.sequentialize !== void 0 && typeof options.sequentialize !== "function") throw new ItdConfigError("events.sequentialize должен быть функцией");
|
|
337
|
+
return Object.freeze({
|
|
338
|
+
...options,
|
|
339
|
+
...options.backoff ? { backoff: Object.freeze([...options.backoff]) } : {}
|
|
340
|
+
});
|
|
401
341
|
}
|
|
402
342
|
/**
|
|
403
|
-
* Общая механика потока событий: соединение, переподключение с
|
|
343
|
+
* Общая механика потока событий: соединение, переподключение с задержкой, обновление
|
|
404
344
|
* токена, реакция на среду, статусы и очередь обработчиков.
|
|
405
345
|
*
|
|
406
346
|
* Ничего не знает о домене: что считать обновлением, как собрать контекст и кому его
|
|
407
|
-
* доставить, решают {@link
|
|
347
|
+
* доставить, решают {@link EventChannelDeps}. Поэтому один и тот же движок несёт
|
|
408
348
|
* и поток уведомлений, и любой другой.
|
|
409
349
|
*
|
|
410
|
-
* @internal
|
|
411
350
|
*/
|
|
412
|
-
var
|
|
351
|
+
var EventChannel = class {
|
|
413
352
|
#deps;
|
|
414
353
|
#options;
|
|
415
354
|
#dispatcher;
|
|
416
355
|
#emitter;
|
|
417
356
|
#clock;
|
|
418
357
|
#maxAttempts;
|
|
419
|
-
|
|
358
|
+
/** Остановленные сессии, чьи подготовка или транспорт ещё освобождают ресурсы. */
|
|
359
|
+
#retiredSessions = /* @__PURE__ */ new Set();
|
|
360
|
+
/** Фоновые операции канала вне сессии, например обновление авторизации. */
|
|
361
|
+
#backgroundTasks = /* @__PURE__ */ new Set();
|
|
362
|
+
#session;
|
|
420
363
|
/**
|
|
421
364
|
* Хочет ли вызывающий код, чтобы соединение было живо.
|
|
422
365
|
*
|
|
@@ -426,20 +369,20 @@ var RealtimeEngine = class {
|
|
|
426
369
|
* и соединение поднялось бы уже после отмены.
|
|
427
370
|
*/
|
|
428
371
|
#wanted = false;
|
|
429
|
-
#status =
|
|
372
|
+
#status = EventChannelStatus.Disconnected;
|
|
430
373
|
#attempt = 0;
|
|
431
374
|
#generation = 0;
|
|
432
375
|
#starting;
|
|
433
376
|
#cancelTimer;
|
|
434
377
|
#detachEnvironment;
|
|
435
378
|
constructor(deps, options = {}) {
|
|
436
|
-
|
|
379
|
+
options = resolveEventChannelOptions(options);
|
|
437
380
|
this.#deps = deps;
|
|
438
381
|
this.#options = options;
|
|
439
382
|
this.#clock = deps.clock ?? systemClock;
|
|
440
383
|
this.#maxAttempts = options.maxAttempts ?? 15;
|
|
441
|
-
this.#emitter = new Emitter((error) => reportListenerError(deps.logger, "
|
|
442
|
-
this.#dispatcher = new
|
|
384
|
+
this.#emitter = new Emitter((error) => reportListenerError(deps.logger, "событий", error));
|
|
385
|
+
this.#dispatcher = new EventDispatcher({
|
|
443
386
|
concurrency: options.concurrency ?? 1,
|
|
444
387
|
...options.sequentialize ? { sequentialize: options.sequentialize } : {}
|
|
445
388
|
}, {
|
|
@@ -476,8 +419,8 @@ var RealtimeEngine = class {
|
|
|
476
419
|
/** Добавляет промежуточный обработчик. @returns функция его удаления */
|
|
477
420
|
use(middleware) {
|
|
478
421
|
if (typeof middleware === "function") return this.#dispatcher.use(middleware);
|
|
479
|
-
if (typeof middleware !== "object" || middleware === null || typeof middleware.middleware !== "function") throw new ItdConfigError("
|
|
480
|
-
return this.#dispatcher.use(
|
|
422
|
+
if (typeof middleware !== "object" || middleware === null || typeof middleware.middleware !== "function") throw new ItdConfigError("events.use() принимает функцию обработки или объект с middleware()");
|
|
423
|
+
return this.#dispatcher.use(deferEventMiddleware(middleware));
|
|
481
424
|
}
|
|
482
425
|
/** Подписывает обработчик обновлений, подходящих под условие. */
|
|
483
426
|
onUpdate(predicate, handler) {
|
|
@@ -490,17 +433,17 @@ var RealtimeEngine = class {
|
|
|
490
433
|
* сразу после запуска: соединение живёт в фоне.
|
|
491
434
|
*/
|
|
492
435
|
async connect() {
|
|
493
|
-
|
|
436
|
+
this.#deps.connectGuard?.();
|
|
437
|
+
if (this.#wanted) return this.#starting;
|
|
494
438
|
this.#wanted = true;
|
|
495
|
-
const generation = ++this.#generation;
|
|
496
|
-
const starting = {};
|
|
497
|
-
this.#starting = starting;
|
|
498
|
-
this.#deps.onConnect?.();
|
|
499
439
|
this.#attachEnvironmentListeners();
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
440
|
+
const starting = this.#startAttempt("initial");
|
|
441
|
+
this.#starting = starting;
|
|
442
|
+
try {
|
|
443
|
+
await starting;
|
|
444
|
+
} finally {
|
|
445
|
+
if (this.#starting === starting) this.#starting = void 0;
|
|
446
|
+
}
|
|
504
447
|
}
|
|
505
448
|
/** Закрывает соединение и отменяет запланированные попытки. */
|
|
506
449
|
disconnect() {
|
|
@@ -513,16 +456,27 @@ var RealtimeEngine = class {
|
|
|
513
456
|
}
|
|
514
457
|
this.#detachEnvironment?.();
|
|
515
458
|
this.#detachEnvironment = void 0;
|
|
516
|
-
this.#
|
|
517
|
-
this.#controller = void 0;
|
|
459
|
+
this.#abortSession(new ItdAbortError("Событийное соединение остановлено"));
|
|
518
460
|
this.#attempt = 0;
|
|
519
461
|
this.#dispatcher.clearPending();
|
|
520
|
-
this.#setStatus(
|
|
521
|
-
this.#deps.onClose?.();
|
|
462
|
+
this.#setStatus(EventChannelStatus.Disconnected);
|
|
522
463
|
}
|
|
523
|
-
/**
|
|
524
|
-
|
|
525
|
-
|
|
464
|
+
/**
|
|
465
|
+
* Ждёт обработчики и полное завершение уже остановленной сессии соединения.
|
|
466
|
+
*
|
|
467
|
+
* Работающий транспорт намеренно не блокирует `drain()`: сначала владелец вызывает
|
|
468
|
+
* {@link disconnect}, который синхронно отменяет сессию. После этого `drain()` ждёт
|
|
469
|
+
* завершения подготовки, транспорта и фонового обновления авторизации, включая их
|
|
470
|
+
* асинхронное освобождение ресурсов.
|
|
471
|
+
*/
|
|
472
|
+
async drain() {
|
|
473
|
+
const sessions = [...this.#retiredSessions];
|
|
474
|
+
const background = [...this.#backgroundTasks];
|
|
475
|
+
await Promise.all([
|
|
476
|
+
this.#dispatcher.drain(),
|
|
477
|
+
...sessions,
|
|
478
|
+
...background
|
|
479
|
+
]);
|
|
526
480
|
}
|
|
527
481
|
/** Пропускает актуальное обновление через цепочку обработчиков. */
|
|
528
482
|
#dispatch(update, raw, origin) {
|
|
@@ -534,70 +488,102 @@ var RealtimeEngine = class {
|
|
|
534
488
|
* когда обработчики разберут очередь.
|
|
535
489
|
*/
|
|
536
490
|
#handleOverflow() {
|
|
537
|
-
const
|
|
538
|
-
if (!this.#wanted || !
|
|
539
|
-
const generation =
|
|
540
|
-
|
|
541
|
-
this.#
|
|
542
|
-
this.#setStatus(RealtimeStatus.Error);
|
|
491
|
+
const session = this.#session;
|
|
492
|
+
if (!this.#wanted || !session) return;
|
|
493
|
+
const generation = session.generation;
|
|
494
|
+
this.#abortSession(new ItdAbortError("Очередь событий переполнена"));
|
|
495
|
+
this.#setStatus(EventChannelStatus.Error);
|
|
543
496
|
const error = /* @__PURE__ */ new Error("Обработчики не успевают за потоком: очередь обновлений переполнена");
|
|
544
497
|
this.#dispatcher.drain().then(() => {
|
|
545
|
-
if (this.#isCurrentGeneration(generation) && !this.#
|
|
498
|
+
if (this.#isCurrentGeneration(generation) && !this.#session) this.#scheduleReconnect(error);
|
|
546
499
|
});
|
|
547
500
|
}
|
|
548
|
-
/**
|
|
549
|
-
async #
|
|
550
|
-
if (!this.#
|
|
501
|
+
/** Создаёт новую попытку соединения и выполняет её подготовку. */
|
|
502
|
+
async #startAttempt(reason) {
|
|
503
|
+
if (!this.#wanted) return;
|
|
504
|
+
this.#abortSession(new ItdAbortError("Событийное соединение заменено новой попыткой"));
|
|
505
|
+
const session = this.#createSession(++this.#generation);
|
|
506
|
+
this.#session = session;
|
|
551
507
|
try {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
508
|
+
if (this.#deps.openBeforeInitialize) {
|
|
509
|
+
this.#runTransport(session);
|
|
510
|
+
await session.handle.opened;
|
|
511
|
+
await this.#initialize(reason, session);
|
|
512
|
+
this.#openGate(session);
|
|
513
|
+
} else {
|
|
514
|
+
await this.#initialize(reason, session);
|
|
515
|
+
this.#openGate(session);
|
|
516
|
+
if (this.#isCurrentSession(session)) this.#runTransport(session);
|
|
517
|
+
}
|
|
518
|
+
} catch (error) {
|
|
519
|
+
if (!this.#isCurrentSession(session)) return;
|
|
520
|
+
this.#abortSession(error);
|
|
521
|
+
if (reason === "initial" && this.#deps.initializationRequired) {
|
|
522
|
+
this.#wanted = false;
|
|
523
|
+
this.#detachEnvironment?.();
|
|
524
|
+
this.#detachEnvironment = void 0;
|
|
525
|
+
this.#setStatus(EventChannelStatus.Disconnected);
|
|
526
|
+
throw error;
|
|
527
|
+
}
|
|
528
|
+
this.#handleFailure(error);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
async #initialize(reason, session) {
|
|
532
|
+
const initialize = this.#deps.initialize;
|
|
533
|
+
if (!initialize) return;
|
|
534
|
+
const task = this.#runSessionTask(session, () => initialize(reason, session.handle));
|
|
535
|
+
try {
|
|
536
|
+
await task;
|
|
556
537
|
} catch (error) {
|
|
538
|
+
if (this.#deps.initializationRequired) throw error;
|
|
557
539
|
this.#deps.logger?.debug(`не удалось синхронизировать поток (${reason})`, error);
|
|
558
540
|
}
|
|
559
541
|
}
|
|
560
|
-
/** Запускает
|
|
561
|
-
#
|
|
562
|
-
if (!this.#
|
|
563
|
-
this.#
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
this.#deps.transport.connect({
|
|
568
|
-
baseUrl: this.#deps.baseUrl,
|
|
569
|
-
authorize: this.#deps.authorize,
|
|
570
|
-
fetch: this.#deps.fetch,
|
|
571
|
-
request: this.#deps.request,
|
|
572
|
-
baseHeaders: this.#deps.baseHeaders,
|
|
573
|
-
getToken: this.#deps.getToken,
|
|
574
|
-
signal: controller.signal,
|
|
542
|
+
/** Запускает транспорт одной попытки. */
|
|
543
|
+
#runTransport(session) {
|
|
544
|
+
if (!this.#isCurrentSession(session)) return;
|
|
545
|
+
this.#setStatus(EventChannelStatus.Connecting);
|
|
546
|
+
this.#runSessionTask(session, () => Promise.resolve(this.#deps.transport.connect({
|
|
547
|
+
...this.#deps.connection,
|
|
548
|
+
signal: session.controller.signal,
|
|
575
549
|
onOpen: () => {
|
|
576
|
-
if (!this.#
|
|
550
|
+
if (!this.#isCurrentSession(session)) return;
|
|
577
551
|
this.#attempt = 0;
|
|
578
|
-
|
|
552
|
+
session.resolveOpened();
|
|
553
|
+
this.#setStatus(EventChannelStatus.Connected);
|
|
579
554
|
},
|
|
580
555
|
onEvent: (event) => {
|
|
581
|
-
if (this.#
|
|
556
|
+
if (this.#isCurrentSession(session)) this.#handleEvent(session, event);
|
|
582
557
|
},
|
|
583
558
|
onParseError: (error, raw) => {
|
|
584
|
-
if (this.#
|
|
559
|
+
if (this.#isCurrentSession(session)) this.#emitEngine("parseError", {
|
|
585
560
|
error,
|
|
586
561
|
raw
|
|
587
562
|
});
|
|
588
563
|
}
|
|
589
|
-
}).then(() => {
|
|
590
|
-
if (this.#
|
|
564
|
+
})).then(() => {
|
|
565
|
+
if (this.#isCurrentSession(session)) this.#handleFailure(/* @__PURE__ */ new Error("Соединение с потоком событий закрыто"));
|
|
591
566
|
}, (error) => {
|
|
592
|
-
if (this.#
|
|
593
|
-
});
|
|
567
|
+
if (this.#isCurrentSession(session)) this.#handleFailure(error);
|
|
568
|
+
}));
|
|
594
569
|
}
|
|
595
|
-
#handleEvent(event) {
|
|
570
|
+
#handleEvent(session, event) {
|
|
596
571
|
if (!this.#wanted) return;
|
|
597
572
|
this.#emitEngine("message", event);
|
|
573
|
+
if (!session.ready && (this.#deps.bufferFrame?.(event) ?? true)) {
|
|
574
|
+
if (session.buffer.length >= 256) {
|
|
575
|
+
this.#handleOverflow();
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
session.buffer.push(event);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
this.#dispatchFrame(event);
|
|
582
|
+
}
|
|
583
|
+
#dispatchFrame(event) {
|
|
598
584
|
if (this.#deps.handleFrame?.(event)) return;
|
|
599
585
|
const update = this.#deps.readUpdate(event);
|
|
600
|
-
if (update !== void 0) this.#dispatch(update, event,
|
|
586
|
+
if (update !== void 0) this.#dispatch(update, event, this.#deps.streamOrigin);
|
|
601
587
|
}
|
|
602
588
|
#reportDispatchError(event, error, context) {
|
|
603
589
|
if (this.#emitter.listenerCount(event) > 0) {
|
|
@@ -612,23 +598,20 @@ var RealtimeEngine = class {
|
|
|
612
598
|
else console.error(`[itd-api] ${message}`, error);
|
|
613
599
|
}
|
|
614
600
|
#handleFailure(error) {
|
|
615
|
-
this.#
|
|
601
|
+
this.#abortSession(error);
|
|
616
602
|
if (error instanceof UnauthorizedStreamError) {
|
|
617
|
-
this.#refreshAndReconnect(error);
|
|
603
|
+
this.#runBackgroundTask(() => this.#refreshAndReconnect(error));
|
|
618
604
|
return;
|
|
619
605
|
}
|
|
620
|
-
this.#setStatus(
|
|
606
|
+
this.#setStatus(EventChannelStatus.Error);
|
|
621
607
|
this.#scheduleReconnect(error);
|
|
622
608
|
}
|
|
623
609
|
/** Обновляет токен и переподключается; при неудаче прекращает попытки. */
|
|
624
610
|
async #refreshAndReconnect(error) {
|
|
625
611
|
const generation = this.#generation;
|
|
626
|
-
|
|
627
|
-
this.#
|
|
628
|
-
this.#
|
|
629
|
-
const refreshed = await this.#deps.refresh().catch(() => false);
|
|
630
|
-
if (this.#starting !== starting || !this.#isCurrentGeneration(generation)) return;
|
|
631
|
-
this.#starting = void 0;
|
|
612
|
+
this.#setStatus(EventChannelStatus.Error);
|
|
613
|
+
const refreshed = await this.#deps.connection.refreshAuth().catch(() => false);
|
|
614
|
+
if (!this.#isCurrentGeneration(generation)) return;
|
|
632
615
|
if (!refreshed) {
|
|
633
616
|
this.#giveUp(error);
|
|
634
617
|
return;
|
|
@@ -659,28 +642,28 @@ var RealtimeEngine = class {
|
|
|
659
642
|
/** Догон через REST идёт до сокета: он же обновляет протухший токен. */
|
|
660
643
|
async #reconnect() {
|
|
661
644
|
if (!this.#wanted || this.#starting) return;
|
|
662
|
-
const
|
|
663
|
-
const starting = {};
|
|
645
|
+
const starting = this.#startAttempt("reconnect");
|
|
664
646
|
this.#starting = starting;
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
647
|
+
try {
|
|
648
|
+
await starting;
|
|
649
|
+
} finally {
|
|
650
|
+
if (this.#starting === starting) this.#starting = void 0;
|
|
651
|
+
}
|
|
669
652
|
}
|
|
670
653
|
/**
|
|
671
654
|
* Завершает автоматические попытки переподключения.
|
|
672
655
|
*
|
|
673
|
-
* Владелец узнаёт об этом
|
|
674
|
-
* до событий: обработчик `giveup` может тут же вызвать `connect()`.
|
|
656
|
+
* Владелец узнаёт об этом через `giveup` и может тут же вызвать `connect()`.
|
|
675
657
|
*/
|
|
676
658
|
#giveUp(error) {
|
|
677
659
|
this.#wanted = false;
|
|
678
660
|
this.#generation += 1;
|
|
679
661
|
this.#starting = void 0;
|
|
680
662
|
this.#attempt = 0;
|
|
663
|
+
this.#abortSession(error);
|
|
681
664
|
this.#detachEnvironment?.();
|
|
682
665
|
this.#detachEnvironment = void 0;
|
|
683
|
-
this
|
|
666
|
+
EVENT_CHANNEL_GIVEUP_HOOKS.get(this)?.();
|
|
684
667
|
this.#emitEngine("error", {
|
|
685
668
|
error,
|
|
686
669
|
willReconnect: false
|
|
@@ -698,8 +681,8 @@ var RealtimeEngine = class {
|
|
|
698
681
|
const target = globalThis;
|
|
699
682
|
if (typeof target.addEventListener !== "function") return;
|
|
700
683
|
const wake = () => {
|
|
701
|
-
if (this.#
|
|
702
|
-
if (this.#status ===
|
|
684
|
+
if (this.#session || this.#cancelTimer || this.#starting) return;
|
|
685
|
+
if (this.#status === EventChannelStatus.Disconnected) return;
|
|
703
686
|
this.#attempt = 0;
|
|
704
687
|
this.#reconnect();
|
|
705
688
|
};
|
|
@@ -722,8 +705,103 @@ var RealtimeEngine = class {
|
|
|
722
705
|
#isCurrentGeneration(generation) {
|
|
723
706
|
return this.#wanted && this.#generation === generation;
|
|
724
707
|
}
|
|
725
|
-
#
|
|
726
|
-
return this.#isCurrentGeneration(generation) && this.#
|
|
708
|
+
#isCurrentSession(session) {
|
|
709
|
+
return this.#isCurrentGeneration(session.generation) && this.#session === session && !session.controller.signal.aborted;
|
|
710
|
+
}
|
|
711
|
+
#createSession(generation) {
|
|
712
|
+
const controller = new AbortController();
|
|
713
|
+
let resolveOpened;
|
|
714
|
+
let rejectOpened;
|
|
715
|
+
let resolveDrained;
|
|
716
|
+
const opened = new Promise((resolve, reject) => {
|
|
717
|
+
resolveOpened = resolve;
|
|
718
|
+
rejectOpened = reject;
|
|
719
|
+
});
|
|
720
|
+
const drained = new Promise((resolve) => {
|
|
721
|
+
resolveDrained = resolve;
|
|
722
|
+
});
|
|
723
|
+
opened.catch(() => {});
|
|
724
|
+
const session = {};
|
|
725
|
+
const handle = Object.freeze({
|
|
726
|
+
signal: controller.signal,
|
|
727
|
+
opened,
|
|
728
|
+
enqueue: (update, options) => {
|
|
729
|
+
if (!this.#isCurrentSession(session)) return;
|
|
730
|
+
this.#dispatch(update, options.raw, options.origin);
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
Object.assign(session, {
|
|
734
|
+
generation,
|
|
735
|
+
controller,
|
|
736
|
+
handle,
|
|
737
|
+
resolveOpened,
|
|
738
|
+
rejectOpened,
|
|
739
|
+
buffer: [],
|
|
740
|
+
tasks: /* @__PURE__ */ new Set(),
|
|
741
|
+
drained,
|
|
742
|
+
resolveDrained,
|
|
743
|
+
retired: false,
|
|
744
|
+
ready: false
|
|
745
|
+
});
|
|
746
|
+
return session;
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Учитывает работу, принадлежащую одной сессии соединения.
|
|
750
|
+
*
|
|
751
|
+
* Маркер добавляется до синхронного входа в операцию: она может повторным вызовом
|
|
752
|
+
* остановить канал из `onOpen` или подготовки, и такая сессия всё равно должна
|
|
753
|
+
* остаться в `drain()` до завершения операции.
|
|
754
|
+
*/
|
|
755
|
+
#runSessionTask(session, operation) {
|
|
756
|
+
return this.#runTrackedTask(session.tasks, operation, () => {
|
|
757
|
+
if (session.retired && session.tasks.size === 0) session.resolveDrained();
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
/** Учитывает работу канала до синхронного входа в операцию вне сессии транспорта. */
|
|
761
|
+
#runBackgroundTask(operation) {
|
|
762
|
+
this.#runTrackedTask(this.#backgroundTasks, operation);
|
|
763
|
+
}
|
|
764
|
+
/** Запускает операцию без окна между входом в неё и регистрацией маркера завершения. */
|
|
765
|
+
#runTrackedTask(tasks, operation, afterFinish) {
|
|
766
|
+
let finish;
|
|
767
|
+
const marker = new Promise((resolve) => {
|
|
768
|
+
finish = resolve;
|
|
769
|
+
});
|
|
770
|
+
tasks.add(marker);
|
|
771
|
+
let task;
|
|
772
|
+
try {
|
|
773
|
+
task = Promise.resolve(operation());
|
|
774
|
+
} catch (error) {
|
|
775
|
+
task = Promise.reject(error);
|
|
776
|
+
}
|
|
777
|
+
const settle = () => {
|
|
778
|
+
tasks.delete(marker);
|
|
779
|
+
finish();
|
|
780
|
+
afterFinish?.();
|
|
781
|
+
};
|
|
782
|
+
task.then(settle, settle);
|
|
783
|
+
return task;
|
|
784
|
+
}
|
|
785
|
+
#openGate(session) {
|
|
786
|
+
if (!this.#isCurrentSession(session) || session.ready) return;
|
|
787
|
+
session.ready = true;
|
|
788
|
+
const buffered = session.buffer.splice(0);
|
|
789
|
+
for (const event of buffered) {
|
|
790
|
+
if (!this.#isCurrentSession(session)) break;
|
|
791
|
+
this.#dispatchFrame(event);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
#abortSession(reason) {
|
|
795
|
+
const session = this.#session;
|
|
796
|
+
if (!session) return;
|
|
797
|
+
this.#session = void 0;
|
|
798
|
+
session.buffer.length = 0;
|
|
799
|
+
session.retired = true;
|
|
800
|
+
this.#retiredSessions.add(session.drained);
|
|
801
|
+
session.drained.then(() => this.#retiredSessions.delete(session.drained));
|
|
802
|
+
session.rejectOpened(reason);
|
|
803
|
+
session.controller.abort(reason);
|
|
804
|
+
if (session.tasks.size === 0) session.resolveDrained();
|
|
727
805
|
}
|
|
728
806
|
/** Рассылает событие из общей части карты, не теряя доменные события типа `E`. */
|
|
729
807
|
#emitEngine(event, payload) {
|
|
@@ -731,7 +809,7 @@ var RealtimeEngine = class {
|
|
|
731
809
|
}
|
|
732
810
|
};
|
|
733
811
|
//#endregion
|
|
734
|
-
//#region src/
|
|
812
|
+
//#region src/events/transports/poll.ts
|
|
735
813
|
/**
|
|
736
814
|
* Запасной транспорт: обычный опрос REST вместо потока.
|
|
737
815
|
*
|
|
@@ -747,19 +825,19 @@ var PollTransport = class {
|
|
|
747
825
|
#interval;
|
|
748
826
|
#limit;
|
|
749
827
|
#clock;
|
|
750
|
-
|
|
828
|
+
#request;
|
|
829
|
+
constructor(options) {
|
|
830
|
+
this.#request = options.request;
|
|
751
831
|
this.#clock = options.clock ?? systemClock;
|
|
752
832
|
this.#interval = options.interval ?? 15e3;
|
|
753
833
|
this.#limit = options.limit ?? 20;
|
|
754
834
|
}
|
|
755
835
|
async connect(context) {
|
|
756
|
-
const request = context.request;
|
|
757
|
-
if (!request) throw new ItdConfigError("опрос уведомлений выполняется через конвейер клиента; создайте поток вызовом itd.realtime()");
|
|
758
836
|
const seen = /* @__PURE__ */ new Set();
|
|
759
837
|
let firstRun = true;
|
|
760
838
|
let lastUnreadCount;
|
|
761
839
|
while (!context.signal.aborted) {
|
|
762
|
-
const payload = await this.#readUpdates(request, context.signal);
|
|
840
|
+
const payload = await this.#readUpdates(this.#request, context.signal);
|
|
763
841
|
context.onOpen();
|
|
764
842
|
const items = pickArray(payload, "notifications");
|
|
765
843
|
for (const item of [...items].reverse()) {
|
|
@@ -775,7 +853,7 @@ var PollTransport = class {
|
|
|
775
853
|
const excess = [...seen].slice(0, seen.size - this.#limit * 2);
|
|
776
854
|
for (const id of excess) seen.delete(id);
|
|
777
855
|
}
|
|
778
|
-
const count = await this.#readCount(request, context.signal);
|
|
856
|
+
const count = await this.#readCount(this.#request, context.signal);
|
|
779
857
|
if (count !== void 0 && count !== lastUnreadCount) {
|
|
780
858
|
lastUnreadCount = count;
|
|
781
859
|
context.onEvent({
|
|
@@ -790,7 +868,7 @@ var PollTransport = class {
|
|
|
790
868
|
async #readUpdates(request, signal) {
|
|
791
869
|
try {
|
|
792
870
|
return await request({
|
|
793
|
-
operationId: "
|
|
871
|
+
operationId: "events.notifications.poll.updates",
|
|
794
872
|
path: "/api/notifications/",
|
|
795
873
|
query: {
|
|
796
874
|
limit: this.#limit,
|
|
@@ -806,7 +884,7 @@ var PollTransport = class {
|
|
|
806
884
|
async #readCount(request, signal) {
|
|
807
885
|
try {
|
|
808
886
|
const payload = await request({
|
|
809
|
-
operationId: "
|
|
887
|
+
operationId: "events.notifications.poll.unread",
|
|
810
888
|
path: "/api/notifications/count",
|
|
811
889
|
signal
|
|
812
890
|
});
|
|
@@ -927,7 +1005,7 @@ ${value2}`, dataLines++;
|
|
|
927
1005
|
}
|
|
928
1006
|
if (firstCharCode === 105 && chunk.charCodeAt(start + 1) === 100 && chunk.charCodeAt(start + 2) === 58) {
|
|
929
1007
|
const value2 = chunk.slice(chunk.charCodeAt(start + 3) === SPACE ? start + 4 : start + 3, end);
|
|
930
|
-
|
|
1008
|
+
value2.includes("\0") || (id = value2);
|
|
931
1009
|
return;
|
|
932
1010
|
}
|
|
933
1011
|
if (firstCharCode === 58) {
|
|
@@ -955,7 +1033,7 @@ ${value2}`, dataLines++;
|
|
|
955
1033
|
${value}`, dataLines++;
|
|
956
1034
|
break;
|
|
957
1035
|
case "id":
|
|
958
|
-
|
|
1036
|
+
value.includes("\0") || (id = value);
|
|
959
1037
|
break;
|
|
960
1038
|
case "retry":
|
|
961
1039
|
/^\d+$/.test(value) ? onRetry(parseInt(value, 10)) : onError(new ParseError(`Invalid \`retry\` value: "${value}"`, {
|
|
@@ -998,7 +1076,7 @@ function isEventPrefix(chunk, i, firstCharCode) {
|
|
|
998
1076
|
return firstCharCode === 101 && chunk.charCodeAt(i + 1) === 118 && chunk.charCodeAt(i + 2) === 101 && chunk.charCodeAt(i + 3) === 110 && chunk.charCodeAt(i + 4) === 116 && chunk.charCodeAt(i + 5) === 58;
|
|
999
1077
|
}
|
|
1000
1078
|
//#endregion
|
|
1001
|
-
//#region src/
|
|
1079
|
+
//#region src/events/transports/sse.ts
|
|
1002
1080
|
/** Путь потока уведомлений. */
|
|
1003
1081
|
const STREAM_PATH = "/api/notifications/stream";
|
|
1004
1082
|
/**
|
|
@@ -1109,9 +1187,78 @@ var SseTransport = class {
|
|
|
1109
1187
|
}
|
|
1110
1188
|
};
|
|
1111
1189
|
//#endregion
|
|
1112
|
-
//#region src/
|
|
1190
|
+
//#region src/events/updates.ts
|
|
1191
|
+
/** Типы нормализованных обновлений потока. */
|
|
1192
|
+
const NotificationUpdateType = Object.freeze({
|
|
1193
|
+
Notification: "notification",
|
|
1194
|
+
UnreadCount: "unreadCount",
|
|
1195
|
+
Unknown: "unknown"
|
|
1196
|
+
});
|
|
1197
|
+
/** Источники нормализованных обновлений потока. */
|
|
1198
|
+
const NotificationUpdateOrigin = Object.freeze({
|
|
1199
|
+
Stream: "stream",
|
|
1200
|
+
Sync: "sync"
|
|
1201
|
+
});
|
|
1202
|
+
/** Проверяет форму фильтра уведомлений. */
|
|
1203
|
+
function validateNotificationSelector(selector) {
|
|
1204
|
+
if (typeof selector === "string") {
|
|
1205
|
+
if (selector.length === 0) throw new ItdConfigError("Тип уведомления не должен быть пустым");
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
if (Array.isArray(selector)) {
|
|
1209
|
+
if (selector.length === 0 || selector.some((type) => typeof type !== "string" || !type)) throw new ItdConfigError("Список типов уведомлений должен содержать непустые строки");
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
if (typeof selector !== "object" || selector === null) throw new ItdConfigError("Фильтр уведомлений должен быть строкой, списком или объектом");
|
|
1213
|
+
const filter = selector;
|
|
1214
|
+
if (filter.type !== void 0) validateNotificationSelector(filter.type);
|
|
1215
|
+
if (filter.actorId !== void 0 && typeof filter.actorId !== "string") throw new ItdConfigError("Фильтр уведомлений: actorId должен быть строкой");
|
|
1216
|
+
for (const field of ["entityId", "parentEntityId"]) {
|
|
1217
|
+
const value = filter[field];
|
|
1218
|
+
if (value !== void 0 && value !== null && typeof value !== "string") throw new ItdConfigError(`Фильтр уведомлений: ${field} должен быть строкой или null`);
|
|
1219
|
+
}
|
|
1220
|
+
if (filter.predicate !== void 0 && typeof filter.predicate !== "function") throw new ItdConfigError("Фильтр уведомлений: predicate должен быть функцией");
|
|
1221
|
+
}
|
|
1222
|
+
/** Преобразует транспортный кадр в одно логическое обновление. */
|
|
1223
|
+
function readNotificationEventsUpdate(event) {
|
|
1224
|
+
if (event.name === "notification") return {
|
|
1225
|
+
type: NotificationUpdateType.Notification,
|
|
1226
|
+
data: readNotificationEvent(event.data)
|
|
1227
|
+
};
|
|
1228
|
+
if (event.name === "unread_count") {
|
|
1229
|
+
const count = readUnreadCountEvent(event.data);
|
|
1230
|
+
return count === void 0 ? void 0 : {
|
|
1231
|
+
type: NotificationUpdateType.UnreadCount,
|
|
1232
|
+
data: count
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
return {
|
|
1236
|
+
type: NotificationUpdateType.Unknown,
|
|
1237
|
+
name: event.name,
|
|
1238
|
+
data: event.data
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
/** Проверяет объектный или краткий фильтр уведомления. */
|
|
1242
|
+
function matchesNotification(context, selector) {
|
|
1243
|
+
const notification = context.update.data.notification;
|
|
1244
|
+
if (typeof selector === "string") return notification.type === selector;
|
|
1245
|
+
if (Array.isArray(selector)) return selector.includes(notification.type);
|
|
1246
|
+
const filter = selector;
|
|
1247
|
+
const types = filter.type === void 0 ? void 0 : [filter.type].flat();
|
|
1248
|
+
if (types && !types.includes(notification.type)) return false;
|
|
1249
|
+
if (filter.actorId !== void 0 && !notification.actors.some(({ id }) => id === filter.actorId)) return false;
|
|
1250
|
+
if (filter.entityId !== void 0 && notification.entityId !== filter.entityId) return false;
|
|
1251
|
+
if (filter.parentEntityId !== void 0 && notification.parentEntityId !== filter.parentEntityId) return false;
|
|
1252
|
+
return filter.predicate?.(context) ?? true;
|
|
1253
|
+
}
|
|
1254
|
+
/** Сужает произвольный контекст потока до контекста уведомления. */
|
|
1255
|
+
function isNotificationContext(context) {
|
|
1256
|
+
return context.update.type === NotificationUpdateType.Notification;
|
|
1257
|
+
}
|
|
1258
|
+
//#endregion
|
|
1259
|
+
//#region src/events/stream.ts
|
|
1113
1260
|
/** Способ получения событий. */
|
|
1114
|
-
const
|
|
1261
|
+
const NotificationEventsTransport = Object.freeze({
|
|
1115
1262
|
/** Поток событий, если среда умеет читать тело по частям, иначе опрос. */
|
|
1116
1263
|
Auto: "auto",
|
|
1117
1264
|
/** Поток `text/event-stream`. */
|
|
@@ -1120,34 +1267,39 @@ const RealtimeTransportKind = Object.freeze({
|
|
|
1120
1267
|
Poll: "poll"
|
|
1121
1268
|
});
|
|
1122
1269
|
/** Проверяет настройки потока. @throws {ItdConfigError} при некорректных значениях */
|
|
1123
|
-
function
|
|
1270
|
+
function resolveNotificationEventsOptions(options = {}) {
|
|
1271
|
+
resolveEventChannelOptions(options);
|
|
1124
1272
|
const duration = (value, name, min) => {
|
|
1125
1273
|
if (value === void 0) return;
|
|
1126
|
-
if (!Number.isFinite(value) || value < min) throw new ItdConfigError(`
|
|
1274
|
+
if (!Number.isFinite(value) || value < min) throw new ItdConfigError(`events.notifications.${name} должен быть числом не меньше ${min}, получено: ${value}`);
|
|
1127
1275
|
};
|
|
1128
1276
|
duration(options.pollInterval, "pollInterval", 1);
|
|
1129
1277
|
duration(options.idleTimeout, "idleTimeout", 0);
|
|
1130
1278
|
duration(options.handshakeTimeout, "handshakeTimeout", 0);
|
|
1279
|
+
return Object.freeze({
|
|
1280
|
+
...options,
|
|
1281
|
+
...options.backoff ? { backoff: Object.freeze([...options.backoff]) } : {}
|
|
1282
|
+
});
|
|
1131
1283
|
}
|
|
1132
|
-
const
|
|
1133
|
-
/**
|
|
1134
|
-
function
|
|
1135
|
-
|
|
1284
|
+
const EVENT_CONNECT_GUARDS = /* @__PURE__ */ new WeakMap();
|
|
1285
|
+
/** Регистрирует проверку состояния создавшего поток клиента. @internal */
|
|
1286
|
+
function setNotificationEventsConnectGuard(stream, guard) {
|
|
1287
|
+
EVENT_CONNECT_GUARDS.set(stream, guard);
|
|
1136
1288
|
}
|
|
1137
1289
|
/**
|
|
1138
1290
|
* Поток уведомлений в реальном времени.
|
|
1139
1291
|
*
|
|
1140
|
-
*
|
|
1292
|
+
* Доступен как стабильное свойство `itd.notifications.events`. Соединение поднимается методом {@link connect}
|
|
1141
1293
|
* и держится само: обрывы, обновление токена и повторные попытки библиотека берёт на себя.
|
|
1142
1294
|
*
|
|
1143
1295
|
* Параметр типа задаёт форму контекста: плагин может расширить её своими полями
|
|
1144
|
-
* (`
|
|
1296
|
+
* (`NotificationEvents<NotificationEventContext & SessionFlavor<S>>`) и типизировать обработчики.
|
|
1145
1297
|
*
|
|
1146
1298
|
* @example
|
|
1147
1299
|
* ```ts
|
|
1148
1300
|
* import { NotificationType } from 'itd-api';
|
|
1149
1301
|
*
|
|
1150
|
-
* const stream = itd.
|
|
1302
|
+
* const stream = itd.notifications.events;
|
|
1151
1303
|
*
|
|
1152
1304
|
* stream.onNotification(NotificationType.PostComment, async ({ update }) => {
|
|
1153
1305
|
* await saveCommentNotification(update.data.notification);
|
|
@@ -1160,28 +1312,23 @@ function setRealtimeConnectGuard(stream, guard) {
|
|
|
1160
1312
|
* await stream.drain();
|
|
1161
1313
|
* ```
|
|
1162
1314
|
*/
|
|
1163
|
-
var
|
|
1315
|
+
var NotificationEvents = class {
|
|
1164
1316
|
#deps;
|
|
1165
1317
|
#engine;
|
|
1318
|
+
#lifecycleActive = false;
|
|
1319
|
+
/** @internal */
|
|
1166
1320
|
constructor(deps, options = {}) {
|
|
1167
|
-
|
|
1321
|
+
options = resolveNotificationEventsOptions(options);
|
|
1168
1322
|
this.#deps = deps;
|
|
1169
|
-
this.#engine = new
|
|
1170
|
-
|
|
1171
|
-
authorize: deps.authorize ?? true,
|
|
1172
|
-
fetch: deps.fetch,
|
|
1173
|
-
request: deps.request,
|
|
1323
|
+
this.#engine = new EventChannel({
|
|
1324
|
+
connection: deps.connection,
|
|
1174
1325
|
clock: deps.clock,
|
|
1175
|
-
baseHeaders: deps.baseHeaders,
|
|
1176
|
-
getToken: deps.getToken,
|
|
1177
|
-
refresh: deps.refresh,
|
|
1178
|
-
onConnect: deps.onConnect,
|
|
1179
|
-
onClose: deps.onClose,
|
|
1180
1326
|
logger: deps.logger,
|
|
1181
1327
|
transport: createTransport(deps, options),
|
|
1328
|
+
streamOrigin: NotificationUpdateOrigin.Stream,
|
|
1182
1329
|
handleFrame: (event) => this.#handleFrame(event),
|
|
1183
|
-
readUpdate:
|
|
1184
|
-
coalesceKey: (update) => update.type ===
|
|
1330
|
+
readUpdate: readNotificationEventsUpdate,
|
|
1331
|
+
coalesceKey: (update) => update.type === NotificationUpdateType.UnreadCount ? update.type : void 0,
|
|
1185
1332
|
createContext: (update, raw, origin) => ({
|
|
1186
1333
|
update,
|
|
1187
1334
|
stream: this,
|
|
@@ -1189,8 +1336,9 @@ var ItdRealtime = class {
|
|
|
1189
1336
|
origin
|
|
1190
1337
|
}),
|
|
1191
1338
|
deliver: (update) => this.#deliver(update),
|
|
1192
|
-
...options.syncCount === false ? {} : {
|
|
1339
|
+
...options.syncCount === false ? {} : { initialize: (reason, session) => this.#syncUnreadCount(reason, session.signal, (update) => session.enqueue(update, { origin: NotificationUpdateOrigin.Sync })) }
|
|
1193
1340
|
}, options);
|
|
1341
|
+
setEventChannelGiveUpHook(this.#engine, () => this.#closeLifecycle());
|
|
1194
1342
|
}
|
|
1195
1343
|
/** Текущее состояние соединения. */
|
|
1196
1344
|
get status() {
|
|
@@ -1202,7 +1350,7 @@ var ItdRealtime = class {
|
|
|
1202
1350
|
}
|
|
1203
1351
|
/** Базовый URL клиента, создавшего поток. @internal */
|
|
1204
1352
|
get baseUrl() {
|
|
1205
|
-
return this.#deps.baseUrl;
|
|
1353
|
+
return this.#deps.connection.baseUrl;
|
|
1206
1354
|
}
|
|
1207
1355
|
/** Идентификаторы аккаунта и сессии клиента, создавшего поток. @internal */
|
|
1208
1356
|
getAuthIdentity() {
|
|
@@ -1234,8 +1382,8 @@ var ItdRealtime = class {
|
|
|
1234
1382
|
onUpdate(selectorOrHandler, selectedHandler) {
|
|
1235
1383
|
const selectAll = selectedHandler === void 0;
|
|
1236
1384
|
const handler = selectAll ? selectorOrHandler : selectedHandler;
|
|
1237
|
-
if (typeof handler !== "function") throw new ItdConfigError("
|
|
1238
|
-
if (!selectAll && typeof selectorOrHandler !== "function" && !Object.values(
|
|
1385
|
+
if (typeof handler !== "function") throw new ItdConfigError("events.onUpdate() принимает функцию обработчика");
|
|
1386
|
+
if (!selectAll && typeof selectorOrHandler !== "function" && !Object.values(NotificationUpdateType).includes(selectorOrHandler)) throw new ItdConfigError(`Неизвестный тип обновления потока: ${String(selectorOrHandler)}`);
|
|
1239
1387
|
let predicate;
|
|
1240
1388
|
if (selectAll) predicate = () => true;
|
|
1241
1389
|
else {
|
|
@@ -1245,7 +1393,7 @@ var ItdRealtime = class {
|
|
|
1245
1393
|
return this.#engine.onUpdate(predicate, handler);
|
|
1246
1394
|
}
|
|
1247
1395
|
onNotification(selector, handler) {
|
|
1248
|
-
if (typeof handler !== "function") throw new ItdConfigError("
|
|
1396
|
+
if (typeof handler !== "function") throw new ItdConfigError("events.onNotification() принимает функцию обработчика");
|
|
1249
1397
|
if (typeof selector !== "function") validateNotificationSelector(selector);
|
|
1250
1398
|
const predicate = (context) => {
|
|
1251
1399
|
if (!isNotificationContext(context)) return false;
|
|
@@ -1264,14 +1412,23 @@ var ItdRealtime = class {
|
|
|
1264
1412
|
* @throws если создавший поток клиент уже освобождён
|
|
1265
1413
|
*/
|
|
1266
1414
|
async connect() {
|
|
1267
|
-
|
|
1268
|
-
return this.#engine.connect();
|
|
1415
|
+
EVENT_CONNECT_GUARDS.get(this)?.();
|
|
1416
|
+
if (this.#lifecycleActive) return this.#engine.connect();
|
|
1417
|
+
this.#lifecycleActive = true;
|
|
1418
|
+
try {
|
|
1419
|
+
this.#deps.onConnect?.();
|
|
1420
|
+
await this.#engine.connect();
|
|
1421
|
+
} catch (error) {
|
|
1422
|
+
this.#closeLifecycle();
|
|
1423
|
+
throw error;
|
|
1424
|
+
}
|
|
1269
1425
|
}
|
|
1270
1426
|
/** Закрывает соединение и отменяет запланированные попытки. */
|
|
1271
1427
|
disconnect() {
|
|
1272
1428
|
this.#engine.disconnect();
|
|
1429
|
+
this.#closeLifecycle();
|
|
1273
1430
|
}
|
|
1274
|
-
/** Ждёт завершения
|
|
1431
|
+
/** Ждёт завершения принятых обновлений и освобождения ресурсов остановленной сессии. */
|
|
1275
1432
|
drain() {
|
|
1276
1433
|
return this.#engine.drain();
|
|
1277
1434
|
}
|
|
@@ -1279,6 +1436,11 @@ var ItdRealtime = class {
|
|
|
1279
1436
|
removeAllListeners() {
|
|
1280
1437
|
this.#engine.removeAllListeners();
|
|
1281
1438
|
}
|
|
1439
|
+
#closeLifecycle() {
|
|
1440
|
+
if (!this.#lifecycleActive) return;
|
|
1441
|
+
this.#lifecycleActive = false;
|
|
1442
|
+
this.#deps.onClose?.();
|
|
1443
|
+
}
|
|
1282
1444
|
/** Кадр `connected` — не обновление, а подтверждение подключения. */
|
|
1283
1445
|
#handleFrame(event) {
|
|
1284
1446
|
if (event.name !== "connected") return false;
|
|
@@ -1291,36 +1453,40 @@ var ItdRealtime = class {
|
|
|
1291
1453
|
* Только при первом подключении: поток присылает счётчик сам, а лишний запрос
|
|
1292
1454
|
* на каждой попытке переподключения нагружал бы сервер во время недоступности сети.
|
|
1293
1455
|
*/
|
|
1294
|
-
async #syncUnreadCount(reason, dispatch) {
|
|
1456
|
+
async #syncUnreadCount(reason, signal, dispatch) {
|
|
1295
1457
|
if (reason !== "initial") return;
|
|
1296
|
-
const count = await this.#deps.fetchUnreadCount();
|
|
1458
|
+
const count = await this.#deps.fetchUnreadCount(signal);
|
|
1297
1459
|
dispatch({
|
|
1298
|
-
type:
|
|
1460
|
+
type: NotificationUpdateType.UnreadCount,
|
|
1299
1461
|
data: count
|
|
1300
1462
|
});
|
|
1301
1463
|
}
|
|
1302
1464
|
#deliver(update) {
|
|
1303
|
-
if (update.type ===
|
|
1465
|
+
if (update.type === NotificationUpdateType.Notification) {
|
|
1304
1466
|
this.#engine.emit("notification", update.data);
|
|
1305
1467
|
if (update.data.unreadCount !== void 0) this.#engine.emit("unreadCount", update.data.unreadCount);
|
|
1306
1468
|
return;
|
|
1307
1469
|
}
|
|
1308
|
-
if (update.type ===
|
|
1470
|
+
if (update.type === NotificationUpdateType.UnreadCount) {
|
|
1309
1471
|
this.#engine.emit("unreadCount", update.data);
|
|
1310
1472
|
return;
|
|
1311
1473
|
}
|
|
1312
|
-
if (update.type ===
|
|
1474
|
+
if (update.type === NotificationUpdateType.Unknown) return;
|
|
1313
1475
|
assertNeverUpdate(update);
|
|
1314
1476
|
}
|
|
1315
1477
|
};
|
|
1316
1478
|
/** Выбирает транспорт по настройкам и возможностям среды. */
|
|
1317
1479
|
function createTransport(deps, options) {
|
|
1318
|
-
const kind = options.transport ??
|
|
1480
|
+
const kind = options.transport ?? NotificationEventsTransport.Auto;
|
|
1319
1481
|
if (typeof kind === "object") return kind;
|
|
1320
|
-
if (kind ===
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1482
|
+
if (kind === NotificationEventsTransport.Poll || kind === NotificationEventsTransport.Auto && !supportsStreamingBody()) {
|
|
1483
|
+
if (!deps.request) throw new ItdConfigError("Poll transport requires a notification request adapter");
|
|
1484
|
+
return new PollTransport({
|
|
1485
|
+
request: deps.request,
|
|
1486
|
+
...deps.clock ? { clock: deps.clock } : {},
|
|
1487
|
+
...options.pollInterval !== void 0 ? { interval: options.pollInterval } : {}
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1324
1490
|
return new SseTransport({
|
|
1325
1491
|
...deps.clock ? { clock: deps.clock } : {},
|
|
1326
1492
|
...options.idleTimeout !== void 0 ? { idleTimeout: options.idleTimeout } : {},
|
|
@@ -1331,254 +1497,35 @@ function assertNeverUpdate(update) {
|
|
|
1331
1497
|
throw new TypeError(`Необработанное обновление потока: ${String(update)}`);
|
|
1332
1498
|
}
|
|
1333
1499
|
//#endregion
|
|
1334
|
-
//#region src/
|
|
1335
|
-
/**
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
if (
|
|
1363
|
-
const registration = this.#registration(middleware);
|
|
1364
|
-
const registrations = this.#routes.get(key) ?? [];
|
|
1365
|
-
registrations.push(registration);
|
|
1366
|
-
this.#routes.set(key, registrations);
|
|
1367
|
-
return () => {
|
|
1368
|
-
const current = this.#routes.get(key);
|
|
1369
|
-
if (!current) return;
|
|
1370
|
-
const index = current.indexOf(registration);
|
|
1371
|
-
if (index >= 0) current.splice(index, 1);
|
|
1372
|
-
if (current.length === 0) this.#routes.delete(key);
|
|
1373
|
-
};
|
|
1374
|
-
}
|
|
1375
|
-
/** Добавляет промежуточные обработчики для обновлений без зарегистрированного маршрута. */
|
|
1376
|
-
otherwise(...middleware) {
|
|
1377
|
-
const registration = this.#registration(middleware);
|
|
1378
|
-
this.#fallback.push(registration);
|
|
1379
|
-
return () => {
|
|
1380
|
-
const index = this.#fallback.indexOf(registration);
|
|
1381
|
-
if (index >= 0) this.#fallback.splice(index, 1);
|
|
1382
|
-
};
|
|
1383
|
-
}
|
|
1384
|
-
/** Возвращает снимок маршрутов для `stream.use(router)` или ручной композиции. */
|
|
1385
|
-
middleware() {
|
|
1386
|
-
const middleware = (context, next) => this.#captureMiddleware()(context, next);
|
|
1387
|
-
return withRealtimeMiddlewareSnapshot(middleware, () => this.#captureMiddleware());
|
|
1388
|
-
}
|
|
1389
|
-
#captureMiddleware() {
|
|
1390
|
-
const routes = /* @__PURE__ */ new Map();
|
|
1391
|
-
for (const [key, registrations] of this.#routes) routes.set(key, registrations.flatMap(({ middleware }) => middleware).map(captureRealtimeMiddleware));
|
|
1392
|
-
const fallback = this.#fallback.flatMap(({ middleware }) => middleware).map(captureRealtimeMiddleware);
|
|
1393
|
-
return async (context, next) => {
|
|
1394
|
-
const key = await this.#selector(context);
|
|
1395
|
-
if (key != null && !isPropertyKey(key)) throw new ItdConfigError("Функция выбора маршрута должна возвращать PropertyKey, null или undefined");
|
|
1396
|
-
const route = key == null ? void 0 : routes.get(key);
|
|
1397
|
-
const chain = route && route.length > 0 ? route : fallback;
|
|
1398
|
-
if (chain.length === 0) {
|
|
1399
|
-
await next();
|
|
1400
|
-
return;
|
|
1401
|
-
}
|
|
1402
|
-
await runRealtimeMiddleware(chain, context, next);
|
|
1403
|
-
};
|
|
1404
|
-
}
|
|
1405
|
-
#registration(middleware) {
|
|
1406
|
-
if (middleware.length === 0) throw new ItdConfigError("Маршрут должен содержать хотя бы один обработчик");
|
|
1407
|
-
for (const item of middleware) if (typeof item !== "function") throw new ItdConfigError("Маршрут принимает только функции обработки");
|
|
1408
|
-
return { middleware: [...middleware] };
|
|
1409
|
-
}
|
|
1410
|
-
};
|
|
1411
|
-
function isPropertyKey(value) {
|
|
1412
|
-
return typeof value === "string" || typeof value === "number" || typeof value === "symbol";
|
|
1413
|
-
}
|
|
1414
|
-
//#endregion
|
|
1415
|
-
//#region src/realtime/composer.ts
|
|
1416
|
-
function middlewareFunction(middleware) {
|
|
1417
|
-
if (typeof middleware === "function") return middleware;
|
|
1418
|
-
if (typeof middleware !== "object" || middleware === null || typeof middleware.middleware !== "function") throw new ItdConfigError("RealtimeComposer принимает функцию обработки или объект с middleware()");
|
|
1419
|
-
return deferRealtimeMiddleware(middleware);
|
|
1420
|
-
}
|
|
1421
|
-
function middlewareGroup(group) {
|
|
1422
|
-
const middleware = Array.isArray(group) ? group : [group];
|
|
1423
|
-
if (middleware.length === 0) throw new ItdConfigError("Ветка RealtimeComposer должна содержать хотя бы один middleware");
|
|
1424
|
-
return middleware.map((item) => middlewareFunction(item));
|
|
1425
|
-
}
|
|
1426
|
-
function boundaryContinuation() {
|
|
1427
|
-
let called = false;
|
|
1428
|
-
let duplicate;
|
|
1429
|
-
return {
|
|
1430
|
-
next: () => {
|
|
1431
|
-
if (!called) {
|
|
1432
|
-
called = true;
|
|
1433
|
-
return Promise.resolve();
|
|
1434
|
-
}
|
|
1435
|
-
const failure = Promise.reject(/* @__PURE__ */ new Error("next() в обработчике realtime error boundary вызван повторно"));
|
|
1436
|
-
duplicate = Promise.all(duplicate ? [duplicate, failure] : [failure]).then(() => void 0);
|
|
1437
|
-
return failure;
|
|
1438
|
-
},
|
|
1439
|
-
called: () => called,
|
|
1440
|
-
settled: async () => {
|
|
1441
|
-
await duplicate;
|
|
1442
|
-
}
|
|
1443
|
-
};
|
|
1444
|
-
}
|
|
1445
|
-
async function runBoundaryStep(run, continuation) {
|
|
1446
|
-
let failure;
|
|
1447
|
-
try {
|
|
1448
|
-
await run();
|
|
1449
|
-
} catch (error) {
|
|
1450
|
-
failure = { error };
|
|
1451
|
-
}
|
|
1452
|
-
try {
|
|
1453
|
-
await continuation.settled();
|
|
1454
|
-
} catch (error) {
|
|
1455
|
-
failure ??= { error };
|
|
1456
|
-
}
|
|
1457
|
-
if (failure) throw failure.error;
|
|
1458
|
-
}
|
|
1459
|
-
/**
|
|
1460
|
-
* Собирает переиспользуемый feature-модуль из realtime middleware.
|
|
1461
|
-
*
|
|
1462
|
-
* Composer не открывает соединение и не планирует конкурентность: готовый объект подключается
|
|
1463
|
-
* через `stream.use(composer)`, а выполнение остаётся обязанностью существующего dispatcher.
|
|
1464
|
-
* Для каждого принятого update используется снимок всей вложенной структуры composer.
|
|
1465
|
-
*
|
|
1466
|
-
* @example
|
|
1467
|
-
* ```ts
|
|
1468
|
-
* const feature = new RealtimeComposer<AppRealtimeContext>();
|
|
1469
|
-
* const safe = feature.errorBoundary(reportFeatureError);
|
|
1470
|
-
* safe.filter(isPostUpdate).use(handlePost);
|
|
1471
|
-
* stream.use(feature);
|
|
1472
|
-
* ```
|
|
1473
|
-
*/
|
|
1474
|
-
var RealtimeComposer = class RealtimeComposer {
|
|
1475
|
-
#middleware = [];
|
|
1476
|
-
constructor(...middleware) {
|
|
1477
|
-
this.use(...middleware);
|
|
1478
|
-
}
|
|
1479
|
-
/** Добавляет middleware в конец текущей onion-цепочки. */
|
|
1480
|
-
use(...middleware) {
|
|
1481
|
-
const normalized = middleware.map(middlewareFunction);
|
|
1482
|
-
this.#middleware.push(...normalized);
|
|
1483
|
-
return this;
|
|
1484
|
-
}
|
|
1485
|
-
filter(predicate, ...middleware) {
|
|
1486
|
-
if (typeof predicate !== "function") throw new ItdConfigError("RealtimeComposer.filter() принимает функцию условия");
|
|
1487
|
-
const child = new RealtimeComposer(...middleware);
|
|
1488
|
-
this.#middleware.push(deferRealtimeMiddleware({ middleware: () => {
|
|
1489
|
-
const nested = captureRealtimeMiddleware(child.middleware());
|
|
1490
|
-
return async (context, next) => {
|
|
1491
|
-
if (await predicate(context)) await nested(context, next);
|
|
1492
|
-
else await next();
|
|
1493
|
-
};
|
|
1494
|
-
} }));
|
|
1495
|
-
return child;
|
|
1496
|
-
}
|
|
1497
|
-
/**
|
|
1498
|
-
* Направляет контекст в одну именованную ветку.
|
|
1499
|
-
*
|
|
1500
|
-
* Неизвестный ключ без fallback пропускает update следующему внешнему middleware. Для
|
|
1501
|
-
* динамической регистрации и числовых ключей используйте {@link RealtimeRouter} напрямую.
|
|
1502
|
-
*/
|
|
1503
|
-
route(selector, routes, fallback) {
|
|
1504
|
-
if (typeof routes !== "object" || routes === null || Array.isArray(routes)) throw new ItdConfigError("RealtimeComposer.route() принимает объект именованных веток");
|
|
1505
|
-
const router = new RealtimeRouter(selector);
|
|
1506
|
-
let registrations = 0;
|
|
1507
|
-
for (const key of Reflect.ownKeys(routes)) {
|
|
1508
|
-
const group = routes[key];
|
|
1509
|
-
if (group === void 0) continue;
|
|
1510
|
-
router.route(key, ...middlewareGroup(group));
|
|
1511
|
-
registrations += 1;
|
|
1512
|
-
}
|
|
1513
|
-
if (fallback !== void 0) {
|
|
1514
|
-
router.otherwise(...middlewareGroup(fallback));
|
|
1515
|
-
registrations += 1;
|
|
1516
|
-
}
|
|
1517
|
-
if (registrations === 0) throw new ItdConfigError("RealtimeComposer.route() требует хотя бы одну ветку или fallback");
|
|
1518
|
-
return this.use(router);
|
|
1519
|
-
}
|
|
1520
|
-
/**
|
|
1521
|
-
* Создаёт дочернюю ветку с локальной границей ошибок.
|
|
1522
|
-
*
|
|
1523
|
-
* Граница защищает только переданные и затем добавленные в возвращённый composer middleware.
|
|
1524
|
-
* Ошибки внешней цепочки намеренно не перехватываются. Обработчик может вызвать `next()`,
|
|
1525
|
-
* чтобы после ошибки продолжить внешнюю цепочку, либо повторно выбросить исключение. Внешняя
|
|
1526
|
-
* цепочка начинается после полного завершения защищённой ветки, а не входит в её onion-вызов.
|
|
1527
|
-
*/
|
|
1528
|
-
errorBoundary(handler, ...middleware) {
|
|
1529
|
-
if (typeof handler !== "function") throw new ItdConfigError("RealtimeComposer.errorBoundary() принимает обработчик ошибки");
|
|
1530
|
-
const child = new RealtimeComposer(...middleware);
|
|
1531
|
-
this.#middleware.push(deferRealtimeMiddleware({ middleware: () => {
|
|
1532
|
-
const nested = captureRealtimeMiddleware(child.middleware());
|
|
1533
|
-
return async (context, next) => {
|
|
1534
|
-
let continuation = boundaryContinuation();
|
|
1535
|
-
try {
|
|
1536
|
-
await runBoundaryStep(() => nested(context, continuation.next), continuation);
|
|
1537
|
-
} catch (error) {
|
|
1538
|
-
continuation = boundaryContinuation();
|
|
1539
|
-
await runBoundaryStep(() => handler({
|
|
1540
|
-
error,
|
|
1541
|
-
context
|
|
1542
|
-
}, continuation.next), continuation);
|
|
1543
|
-
}
|
|
1544
|
-
if (continuation.called()) await next();
|
|
1545
|
-
};
|
|
1546
|
-
} }));
|
|
1547
|
-
return child;
|
|
1548
|
-
}
|
|
1549
|
-
/** Возвращает snapshot-aware middleware для `stream.use()` или вложенного composer. */
|
|
1550
|
-
middleware() {
|
|
1551
|
-
const middleware = (context, next) => this.#captureMiddleware()(context, next);
|
|
1552
|
-
return withRealtimeMiddlewareSnapshot(middleware, () => this.#captureMiddleware());
|
|
1553
|
-
}
|
|
1554
|
-
#captureMiddleware() {
|
|
1555
|
-
const snapshot = this.#middleware.map(captureRealtimeMiddleware);
|
|
1556
|
-
return (context, next) => runRealtimeMiddleware(snapshot, context, next);
|
|
1557
|
-
}
|
|
1558
|
-
};
|
|
1559
|
-
const CONNECTING = 0;
|
|
1560
|
-
const OPEN = 1;
|
|
1561
|
-
const NORMAL_CLOSURE = 1e3;
|
|
1562
|
-
const UNAUTHORIZED_CLOSE_CODES = /* @__PURE__ */ new Set([1008, 4401]);
|
|
1563
|
-
function validateDuration(value, name) {
|
|
1564
|
-
if (value !== void 0 && (!Number.isFinite(value) || value < 0)) throw new ItdConfigError(`WebSocketTransport.${name} должен быть числом не меньше 0, получено: ${value}`);
|
|
1565
|
-
}
|
|
1566
|
-
function isWebSocketConnection(value) {
|
|
1567
|
-
if (typeof value !== "object" || value === null) return false;
|
|
1568
|
-
const socket = value;
|
|
1569
|
-
return typeof socket.readyState === "number" && typeof socket.addEventListener === "function" && typeof socket.removeEventListener === "function" && typeof socket.send === "function" && typeof socket.close === "function";
|
|
1570
|
-
}
|
|
1571
|
-
function abortError(signal) {
|
|
1572
|
-
if (signal.reason !== void 0) return signal.reason;
|
|
1573
|
-
const error = /* @__PURE__ */ new Error("WebSocket-подключение отменено");
|
|
1574
|
-
error.name = "AbortError";
|
|
1575
|
-
return error;
|
|
1576
|
-
}
|
|
1577
|
-
function errorMessage(error) {
|
|
1578
|
-
if (error instanceof Error && error.message) return error.message;
|
|
1579
|
-
if (typeof error === "object" && error !== null && "message" in error) {
|
|
1580
|
-
const message = error.message;
|
|
1581
|
-
if (typeof message === "string" && message) return message;
|
|
1500
|
+
//#region src/operations/events.ts
|
|
1501
|
+
/** Контракты HTTP-запросов событийного канала. */
|
|
1502
|
+
const EVENT_OPERATIONS = Object.freeze({
|
|
1503
|
+
"events.notifications.poll.updates": passthroughOperation("events.notifications.poll.updates"),
|
|
1504
|
+
"events.notifications.poll.unread": passthroughOperation("events.notifications.poll.unread")
|
|
1505
|
+
});
|
|
1506
|
+
const CONNECTING = 0;
|
|
1507
|
+
const OPEN = 1;
|
|
1508
|
+
const NORMAL_CLOSURE = 1e3;
|
|
1509
|
+
const UNAUTHORIZED_CLOSE_CODES = /* @__PURE__ */ new Set([1008, 4401]);
|
|
1510
|
+
function validateDuration(value, name) {
|
|
1511
|
+
if (value !== void 0 && (!Number.isFinite(value) || value < 0)) throw new ItdConfigError(`WebSocketTransport.${name} должен быть числом не меньше 0, получено: ${value}`);
|
|
1512
|
+
}
|
|
1513
|
+
function isWebSocketConnection(value) {
|
|
1514
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1515
|
+
const socket = value;
|
|
1516
|
+
return typeof socket.readyState === "number" && typeof socket.addEventListener === "function" && typeof socket.removeEventListener === "function" && typeof socket.send === "function" && typeof socket.close === "function";
|
|
1517
|
+
}
|
|
1518
|
+
function abortError(signal) {
|
|
1519
|
+
if (signal.reason !== void 0) return signal.reason;
|
|
1520
|
+
const error = /* @__PURE__ */ new Error("WebSocket-подключение отменено");
|
|
1521
|
+
error.name = "AbortError";
|
|
1522
|
+
return error;
|
|
1523
|
+
}
|
|
1524
|
+
function errorMessage(error) {
|
|
1525
|
+
if (error instanceof Error && error.message) return error.message;
|
|
1526
|
+
if (typeof error === "object" && error !== null && "message" in error) {
|
|
1527
|
+
const message = error.message;
|
|
1528
|
+
if (typeof message === "string" && message) return message;
|
|
1582
1529
|
}
|
|
1583
1530
|
}
|
|
1584
1531
|
function safeDetail(error, url, token) {
|
|
@@ -1605,7 +1552,7 @@ function readMessage(event) {
|
|
|
1605
1552
|
function eventName(data) {
|
|
1606
1553
|
return typeof data === "object" && data !== null && "type" in data ? String(data.type) : "message";
|
|
1607
1554
|
}
|
|
1608
|
-
/** Транспорт исходных
|
|
1555
|
+
/** Транспорт исходных событий поверх стандартного WebSocket. */
|
|
1609
1556
|
var WebSocketTransport = class {
|
|
1610
1557
|
name = "ws";
|
|
1611
1558
|
#path;
|
|
@@ -1869,6 +1816,339 @@ var WebSocketTransport = class {
|
|
|
1869
1816
|
}
|
|
1870
1817
|
};
|
|
1871
1818
|
//#endregion
|
|
1872
|
-
|
|
1819
|
+
//#region src/events/client.ts
|
|
1820
|
+
/**
|
|
1821
|
+
* Поток уведомлений с собственным конвейером запросов.
|
|
1822
|
+
*
|
|
1823
|
+
* Тот же {@link NotificationEvents}, что доступен как `itd.notifications.events`, но собранный без полного клиента:
|
|
1824
|
+
* ресурсы, билдеры и сессия сюда не входят. Нужен приложениям, которым от API нужны только
|
|
1825
|
+
* события, — виджетам, счётчикам непрочитанного, фоновым слушателям.
|
|
1826
|
+
*
|
|
1827
|
+
* @example
|
|
1828
|
+
* ```ts
|
|
1829
|
+
* import { createNotificationEventsClient } from 'itd-api/events';
|
|
1830
|
+
*
|
|
1831
|
+
* await using stream = createNotificationEventsClient({ auth: token });
|
|
1832
|
+
* stream.on('notification', ({ notification }) => console.log(notification.type));
|
|
1833
|
+
* await stream.connect();
|
|
1834
|
+
* ```
|
|
1835
|
+
*/
|
|
1836
|
+
var NotificationEventsClient = class NotificationEventsClient extends NotificationEvents {
|
|
1837
|
+
#runtime;
|
|
1838
|
+
/** Общий результат терминальной очистки для идемпотентных повторных вызовов. */
|
|
1839
|
+
#disposePromise;
|
|
1840
|
+
#disposed = false;
|
|
1841
|
+
constructor(options = {}) {
|
|
1842
|
+
const runtime = createClientRuntime(options, {
|
|
1843
|
+
catalog: ITD_CATALOG,
|
|
1844
|
+
auth: () => resolveAuthProvider(options.auth)
|
|
1845
|
+
});
|
|
1846
|
+
super(notificationEventsDeps(runtime), options);
|
|
1847
|
+
this.#runtime = runtime;
|
|
1848
|
+
setNotificationEventsConnectGuard(this, () => {
|
|
1849
|
+
if (!this.#disposed) return;
|
|
1850
|
+
throw new ItdStateError("Поток уже окончательно освобождён через dispose(); нельзя подключить его снова. Создайте новый");
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Окончательно освобождает поток и его конвейер: отключает соединение, дожидается
|
|
1855
|
+
* обработчиков и останавливает очередь запросов.
|
|
1856
|
+
*
|
|
1857
|
+
* Повторные вызовы возвращают тот же результат очистки.
|
|
1858
|
+
*/
|
|
1859
|
+
dispose() {
|
|
1860
|
+
if (this.#disposePromise) return this.#disposePromise;
|
|
1861
|
+
this.#disposed = true;
|
|
1862
|
+
this.#disposePromise = this.#dispose();
|
|
1863
|
+
return this.#disposePromise;
|
|
1864
|
+
}
|
|
1865
|
+
async #dispose() {
|
|
1866
|
+
const errors = [];
|
|
1867
|
+
this.disconnect();
|
|
1868
|
+
try {
|
|
1869
|
+
await this.drain();
|
|
1870
|
+
} catch (error) {
|
|
1871
|
+
errors.push(error);
|
|
1872
|
+
}
|
|
1873
|
+
try {
|
|
1874
|
+
this.#runtime.close();
|
|
1875
|
+
await this.#runtime.dispose();
|
|
1876
|
+
} catch (error) {
|
|
1877
|
+
errors.push(error);
|
|
1878
|
+
}
|
|
1879
|
+
if (errors.length > 0) throw new AggregateError(errors, "Не удалось освободить поток");
|
|
1880
|
+
}
|
|
1881
|
+
/** Позволяет использовать поток с `await using`. */
|
|
1882
|
+
[Symbol.asyncDispose]() {
|
|
1883
|
+
return this.dispose();
|
|
1884
|
+
}
|
|
1885
|
+
static {
|
|
1886
|
+
installAsyncDisposeFallback(NotificationEventsClient);
|
|
1887
|
+
}
|
|
1888
|
+
};
|
|
1889
|
+
/** Приводит опцию `auth` к провайдеру конвейера. */
|
|
1890
|
+
function resolveAuthProvider(auth) {
|
|
1891
|
+
if (auth === void 0) return anonymousAuth();
|
|
1892
|
+
return typeof auth === "string" ? bearerToken(auth) : auth;
|
|
1893
|
+
}
|
|
1894
|
+
/**
|
|
1895
|
+
* Связывает поток с собранным конвейером.
|
|
1896
|
+
*
|
|
1897
|
+
* Начальный счётчик непрочитанных берётся той же операцией, что и при опросе
|
|
1898
|
+
* (`events.notifications.poll.unread`): ресурс уведомлений потоку для этого не нужен.
|
|
1899
|
+
*/
|
|
1900
|
+
function notificationEventsDeps(runtime) {
|
|
1901
|
+
const request = ({ operationId, path, query, signal }) => runtime.http.execute(EVENT_OPERATIONS[operationId], {
|
|
1902
|
+
path,
|
|
1903
|
+
query,
|
|
1904
|
+
signal
|
|
1905
|
+
});
|
|
1906
|
+
return {
|
|
1907
|
+
connection: runtime.connection(),
|
|
1908
|
+
clock: runtime.config.clock,
|
|
1909
|
+
logger: runtime.config.logger,
|
|
1910
|
+
request,
|
|
1911
|
+
fetchUnreadCount: async (signal) => {
|
|
1912
|
+
const payload = await request({
|
|
1913
|
+
operationId: "events.notifications.poll.unread",
|
|
1914
|
+
path: "/api/notifications/count",
|
|
1915
|
+
signal
|
|
1916
|
+
});
|
|
1917
|
+
return pickNumber(payload, "count", 0);
|
|
1918
|
+
}
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
/**
|
|
1922
|
+
* Создаёт поток уведомлений с собственным конвейером. Равнозначно конструктору
|
|
1923
|
+
* {@link NotificationEventsClient}.
|
|
1924
|
+
*/
|
|
1925
|
+
function createNotificationEventsClient(options = {}) {
|
|
1926
|
+
return new NotificationEventsClient(options);
|
|
1927
|
+
}
|
|
1928
|
+
//#endregion
|
|
1929
|
+
//#region src/events/router.ts
|
|
1930
|
+
/**
|
|
1931
|
+
* Направляет обновления потока в именованные цепочки промежуточных обработчиков.
|
|
1932
|
+
*
|
|
1933
|
+
* @example
|
|
1934
|
+
* ```ts
|
|
1935
|
+
* import { EventRouter, NotificationUpdateType } from 'itd-api/events';
|
|
1936
|
+
*
|
|
1937
|
+
* const router = new EventRouter((context) => context.update.type);
|
|
1938
|
+
* router.route(NotificationUpdateType.Notification, async (context, next) => {
|
|
1939
|
+
* if (context.update.type === NotificationUpdateType.Notification) {
|
|
1940
|
+
* await handleNotification(context.update.data.notification);
|
|
1941
|
+
* }
|
|
1942
|
+
* await next();
|
|
1943
|
+
* });
|
|
1944
|
+
* stream.use(router);
|
|
1945
|
+
* ```
|
|
1946
|
+
*/
|
|
1947
|
+
var EventRouter = class {
|
|
1948
|
+
#selector;
|
|
1949
|
+
#routes = /* @__PURE__ */ new Map();
|
|
1950
|
+
#fallback = [];
|
|
1951
|
+
constructor(selector) {
|
|
1952
|
+
if (typeof selector !== "function") throw new ItdConfigError("EventRouter принимает функцию выбора маршрута");
|
|
1953
|
+
this.#selector = selector;
|
|
1954
|
+
}
|
|
1955
|
+
/** Добавляет промежуточные обработчики к маршруту и возвращает функцию их удаления. */
|
|
1956
|
+
route(key, ...middleware) {
|
|
1957
|
+
if (!isPropertyKey(key)) throw new ItdConfigError("Ключ маршрута событий должен быть PropertyKey");
|
|
1958
|
+
const registration = this.#registration(middleware);
|
|
1959
|
+
const registrations = this.#routes.get(key) ?? [];
|
|
1960
|
+
registrations.push(registration);
|
|
1961
|
+
this.#routes.set(key, registrations);
|
|
1962
|
+
return () => {
|
|
1963
|
+
const current = this.#routes.get(key);
|
|
1964
|
+
if (!current) return;
|
|
1965
|
+
const index = current.indexOf(registration);
|
|
1966
|
+
if (index >= 0) current.splice(index, 1);
|
|
1967
|
+
if (current.length === 0) this.#routes.delete(key);
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
/** Добавляет промежуточные обработчики для обновлений без зарегистрированного маршрута. */
|
|
1971
|
+
otherwise(...middleware) {
|
|
1972
|
+
const registration = this.#registration(middleware);
|
|
1973
|
+
this.#fallback.push(registration);
|
|
1974
|
+
return () => {
|
|
1975
|
+
const index = this.#fallback.indexOf(registration);
|
|
1976
|
+
if (index >= 0) this.#fallback.splice(index, 1);
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
/** Возвращает снимок маршрутов для `stream.use(router)` или ручной композиции. */
|
|
1980
|
+
middleware() {
|
|
1981
|
+
const middleware = (context, next) => this.#captureMiddleware()(context, next);
|
|
1982
|
+
return withEventMiddlewareSnapshot(middleware, () => this.#captureMiddleware());
|
|
1983
|
+
}
|
|
1984
|
+
#captureMiddleware() {
|
|
1985
|
+
const routes = /* @__PURE__ */ new Map();
|
|
1986
|
+
for (const [key, registrations] of this.#routes) routes.set(key, registrations.flatMap(({ middleware }) => middleware).map(captureEventMiddleware));
|
|
1987
|
+
const fallback = this.#fallback.flatMap(({ middleware }) => middleware).map(captureEventMiddleware);
|
|
1988
|
+
return async (context, next) => {
|
|
1989
|
+
const key = await this.#selector(context);
|
|
1990
|
+
if (key != null && !isPropertyKey(key)) throw new ItdConfigError("Функция выбора маршрута должна возвращать PropertyKey, null или undefined");
|
|
1991
|
+
const route = key == null ? void 0 : routes.get(key);
|
|
1992
|
+
const chain = route && route.length > 0 ? route : fallback;
|
|
1993
|
+
if (chain.length === 0) {
|
|
1994
|
+
await next();
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
await runEventMiddleware(chain, context, next);
|
|
1998
|
+
};
|
|
1999
|
+
}
|
|
2000
|
+
#registration(middleware) {
|
|
2001
|
+
if (middleware.length === 0) throw new ItdConfigError("Маршрут должен содержать хотя бы один обработчик");
|
|
2002
|
+
for (const item of middleware) if (typeof item !== "function") throw new ItdConfigError("Маршрут принимает только функции обработки");
|
|
2003
|
+
return { middleware: [...middleware] };
|
|
2004
|
+
}
|
|
2005
|
+
};
|
|
2006
|
+
function isPropertyKey(value) {
|
|
2007
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "symbol";
|
|
2008
|
+
}
|
|
2009
|
+
//#endregion
|
|
2010
|
+
//#region src/events/composer.ts
|
|
2011
|
+
function middlewareFunction(middleware) {
|
|
2012
|
+
if (typeof middleware === "function") return middleware;
|
|
2013
|
+
if (typeof middleware !== "object" || middleware === null || typeof middleware.middleware !== "function") throw new ItdConfigError("EventComposer принимает функцию обработки или объект с middleware()");
|
|
2014
|
+
return deferEventMiddleware(middleware);
|
|
2015
|
+
}
|
|
2016
|
+
function middlewareGroup(group) {
|
|
2017
|
+
const middleware = Array.isArray(group) ? group : [group];
|
|
2018
|
+
if (middleware.length === 0) throw new ItdConfigError("Ветка EventComposer должна содержать хотя бы один middleware");
|
|
2019
|
+
return middleware.map((item) => middlewareFunction(item));
|
|
2020
|
+
}
|
|
2021
|
+
function boundaryContinuation() {
|
|
2022
|
+
let called = false;
|
|
2023
|
+
let duplicate;
|
|
2024
|
+
return {
|
|
2025
|
+
next: () => {
|
|
2026
|
+
if (!called) {
|
|
2027
|
+
called = true;
|
|
2028
|
+
return Promise.resolve();
|
|
2029
|
+
}
|
|
2030
|
+
const failure = Promise.reject(/* @__PURE__ */ new Error("next() в обработчике границы ошибок событий вызван повторно"));
|
|
2031
|
+
duplicate = Promise.all(duplicate ? [duplicate, failure] : [failure]).then(() => void 0);
|
|
2032
|
+
return failure;
|
|
2033
|
+
},
|
|
2034
|
+
called: () => called,
|
|
2035
|
+
settled: async () => {
|
|
2036
|
+
await duplicate;
|
|
2037
|
+
}
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
2040
|
+
async function runBoundaryStep(run, continuation) {
|
|
2041
|
+
let failure;
|
|
2042
|
+
try {
|
|
2043
|
+
await run();
|
|
2044
|
+
} catch (error) {
|
|
2045
|
+
failure = { error };
|
|
2046
|
+
}
|
|
2047
|
+
try {
|
|
2048
|
+
await continuation.settled();
|
|
2049
|
+
} catch (error) {
|
|
2050
|
+
failure ??= { error };
|
|
2051
|
+
}
|
|
2052
|
+
if (failure) throw failure.error;
|
|
2053
|
+
}
|
|
2054
|
+
/**
|
|
2055
|
+
* Собирает переиспользуемую цепочку обработчиков событий.
|
|
2056
|
+
*
|
|
2057
|
+
* Не открывает соединение. Готовый объект подключается через `stream.use(composer)`.
|
|
2058
|
+
* Для каждого обновления используется текущий снимок вложенных обработчиков.
|
|
2059
|
+
*
|
|
2060
|
+
* @example
|
|
2061
|
+
* ```ts
|
|
2062
|
+
* const feature = new EventComposer<AppEventContext>();
|
|
2063
|
+
* const safe = feature.errorBoundary(reportFeatureError);
|
|
2064
|
+
* safe.filter(isPostUpdate).use(handlePost);
|
|
2065
|
+
* stream.use(feature);
|
|
2066
|
+
* ```
|
|
2067
|
+
*/
|
|
2068
|
+
var EventComposer = class EventComposer {
|
|
2069
|
+
#middleware = [];
|
|
2070
|
+
constructor(...middleware) {
|
|
2071
|
+
this.use(...middleware);
|
|
2072
|
+
}
|
|
2073
|
+
/** Добавляет промежуточные обработчики в конец цепочки. */
|
|
2074
|
+
use(...middleware) {
|
|
2075
|
+
const normalized = middleware.map(middlewareFunction);
|
|
2076
|
+
this.#middleware.push(...normalized);
|
|
2077
|
+
return this;
|
|
2078
|
+
}
|
|
2079
|
+
filter(predicate, ...middleware) {
|
|
2080
|
+
if (typeof predicate !== "function") throw new ItdConfigError("EventComposer.filter() принимает функцию условия");
|
|
2081
|
+
const child = new EventComposer(...middleware);
|
|
2082
|
+
this.#middleware.push(deferEventMiddleware({ middleware: () => {
|
|
2083
|
+
const nested = captureEventMiddleware(child.middleware());
|
|
2084
|
+
return async (context, next) => {
|
|
2085
|
+
if (await predicate(context)) await nested(context, next);
|
|
2086
|
+
else await next();
|
|
2087
|
+
};
|
|
2088
|
+
} }));
|
|
2089
|
+
return child;
|
|
2090
|
+
}
|
|
2091
|
+
/**
|
|
2092
|
+
* Направляет контекст в одну именованную ветку.
|
|
2093
|
+
*
|
|
2094
|
+
* Неизвестный ключ без запасной ветки передаёт обновление следующему обработчику. Для
|
|
2095
|
+
* динамической регистрации и числовых ключей используйте {@link EventRouter} напрямую.
|
|
2096
|
+
*/
|
|
2097
|
+
route(selector, routes, fallback) {
|
|
2098
|
+
if (typeof routes !== "object" || routes === null || Array.isArray(routes)) throw new ItdConfigError("EventComposer.route() принимает объект именованных веток");
|
|
2099
|
+
const router = new EventRouter(selector);
|
|
2100
|
+
let registrations = 0;
|
|
2101
|
+
for (const key of Reflect.ownKeys(routes)) {
|
|
2102
|
+
const group = routes[key];
|
|
2103
|
+
if (group === void 0) continue;
|
|
2104
|
+
router.route(key, ...middlewareGroup(group));
|
|
2105
|
+
registrations += 1;
|
|
2106
|
+
}
|
|
2107
|
+
if (fallback !== void 0) {
|
|
2108
|
+
router.otherwise(...middlewareGroup(fallback));
|
|
2109
|
+
registrations += 1;
|
|
2110
|
+
}
|
|
2111
|
+
if (registrations === 0) throw new ItdConfigError("EventComposer.route() требует хотя бы одну ветку или fallback");
|
|
2112
|
+
return this.use(router);
|
|
2113
|
+
}
|
|
2114
|
+
/**
|
|
2115
|
+
* Создаёт дочернюю ветку с локальной границей ошибок.
|
|
2116
|
+
*
|
|
2117
|
+
* Перехватывает ошибки только из дочерней ветки. Вызов `next()` в обработчике ошибки
|
|
2118
|
+
* продолжает внешнюю цепочку.
|
|
2119
|
+
*/
|
|
2120
|
+
errorBoundary(handler, ...middleware) {
|
|
2121
|
+
if (typeof handler !== "function") throw new ItdConfigError("EventComposer.errorBoundary() принимает обработчик ошибки");
|
|
2122
|
+
const child = new EventComposer(...middleware);
|
|
2123
|
+
this.#middleware.push(deferEventMiddleware({ middleware: () => {
|
|
2124
|
+
const nested = captureEventMiddleware(child.middleware());
|
|
2125
|
+
return async (context, next) => {
|
|
2126
|
+
let continuation = boundaryContinuation();
|
|
2127
|
+
try {
|
|
2128
|
+
await runBoundaryStep(() => nested(context, continuation.next), continuation);
|
|
2129
|
+
} catch (error) {
|
|
2130
|
+
continuation = boundaryContinuation();
|
|
2131
|
+
await runBoundaryStep(() => handler({
|
|
2132
|
+
error,
|
|
2133
|
+
context
|
|
2134
|
+
}, continuation.next), continuation);
|
|
2135
|
+
}
|
|
2136
|
+
if (continuation.called()) await next();
|
|
2137
|
+
};
|
|
2138
|
+
} }));
|
|
2139
|
+
return child;
|
|
2140
|
+
}
|
|
2141
|
+
/** Возвращает промежуточный обработчик для `stream.use()` или вложенной цепочки. */
|
|
2142
|
+
middleware() {
|
|
2143
|
+
const middleware = (context, next) => this.#captureMiddleware()(context, next);
|
|
2144
|
+
return withEventMiddlewareSnapshot(middleware, () => this.#captureMiddleware());
|
|
2145
|
+
}
|
|
2146
|
+
#captureMiddleware() {
|
|
2147
|
+
const snapshot = this.#middleware.map(captureEventMiddleware);
|
|
2148
|
+
return (context, next) => runEventMiddleware(snapshot, context, next);
|
|
2149
|
+
}
|
|
2150
|
+
};
|
|
2151
|
+
//#endregion
|
|
2152
|
+
export { Emitter as _, WebSocketTransport as a, NotificationEventsTransport as c, NotificationUpdateOrigin as d, NotificationUpdateType as f, runEventMiddleware as g, UnauthorizedStreamError as h, createNotificationEventsClient as i, resolveNotificationEventsOptions as l, resolveEventChannelOptions as m, EventRouter as n, EVENT_OPERATIONS as o, EventChannel as p, NotificationEventsClient as r, NotificationEvents as s, EventComposer as t, setNotificationEventsConnectGuard as u, reportListenerError as v };
|
|
1873
2153
|
|
|
1874
|
-
//# sourceMappingURL=
|
|
2154
|
+
//# sourceMappingURL=events-C5wnTXPT.js.map
|