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.
Files changed (104) hide show
  1. package/README.md +72 -27
  2. package/dist/events/index.cjs +53 -0
  3. package/dist/events/index.d.cts +3 -0
  4. package/dist/events/index.d.ts +3 -0
  5. package/dist/events/index.js +5 -0
  6. package/dist/index.cjs +234 -200
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.cts +70 -64
  9. package/dist/index.d.ts +70 -64
  10. package/dist/index.js +202 -179
  11. package/dist/index.js.map +1 -1
  12. package/dist/node/index.cjs +6 -5
  13. package/dist/node/index.cjs.map +1 -1
  14. package/dist/node/index.d.cts +2 -2
  15. package/dist/node/index.d.ts +2 -2
  16. package/dist/node/index.js +5 -4
  17. package/dist/node/index.js.map +1 -1
  18. package/dist/rest/index.cjs +32 -26
  19. package/dist/rest/index.cjs.map +1 -1
  20. package/dist/rest/index.d.cts +9 -7
  21. package/dist/rest/index.d.ts +9 -7
  22. package/dist/rest/index.js +19 -21
  23. package/dist/rest/index.js.map +1 -1
  24. package/dist/shared/{errors-DfU8M5eS.cjs → errors-BmP3TKoW.cjs} +3 -3
  25. package/dist/shared/{errors-DfU8M5eS.cjs.map → errors-BmP3TKoW.cjs.map} +1 -1
  26. package/dist/shared/{errors-Bhrd2fJd.js → errors-GI10kZxk.js} +3 -3
  27. package/dist/shared/{errors-Bhrd2fJd.js.map → errors-GI10kZxk.js.map} +1 -1
  28. package/dist/shared/{websocket-BLR8eVJV.js → events-C5wnTXPT.js} +770 -490
  29. package/dist/shared/events-C5wnTXPT.js.map +1 -0
  30. package/dist/shared/events-CqtkP65d.d.ts +698 -0
  31. package/dist/shared/events-DGAWlCaI.d.cts +698 -0
  32. package/dist/shared/{websocket-C_eI4H2o.cjs → events-FZfnEez0.cjs} +834 -518
  33. package/dist/shared/events-FZfnEez0.cjs.map +1 -0
  34. package/dist/shared/{storage-BPJR_k4-.cjs → key-value-store-B5GYVEYZ.cjs} +2 -86
  35. package/dist/shared/key-value-store-B5GYVEYZ.cjs.map +1 -0
  36. package/dist/shared/{storage-D86edNCB.js → key-value-store-Bt2PSMxY.js} +3 -69
  37. package/dist/shared/key-value-store-Bt2PSMxY.js.map +1 -0
  38. package/dist/shared/key-value-store-COZaNHZS.d.cts +69 -0
  39. package/dist/shared/key-value-store-COZaNHZS.d.ts +69 -0
  40. package/dist/shared/{multi-storage-DccjD7Ww.d.ts → multi-storage-BEL7tcKn.d.cts} +3 -2
  41. package/dist/shared/{multi-storage--yTEqiod.cjs → multi-storage-CDKLa956.cjs} +11 -10
  42. package/dist/shared/{multi-storage--yTEqiod.cjs.map → multi-storage-CDKLa956.cjs.map} +1 -1
  43. package/dist/shared/{multi-storage-CjAPB5Kq.d.cts → multi-storage-LkljOSzA.d.ts} +3 -2
  44. package/dist/shared/{multi-storage-CkvTUC5m.js → multi-storage-SoW05uzO.js} +5 -4
  45. package/dist/shared/{multi-storage-CkvTUC5m.js.map → multi-storage-SoW05uzO.js.map} +1 -1
  46. package/dist/shared/{options-Dg5N3r1V.cjs → options-DOJYtoti.cjs} +8 -6
  47. package/dist/shared/options-DOJYtoti.cjs.map +1 -0
  48. package/dist/shared/{options-DtATYdLr.js → options-DhgGg7Ms.js} +8 -6
  49. package/dist/shared/options-DhgGg7Ms.js.map +1 -0
  50. package/dist/shared/{cookies-DZwFq6kr.cjs → redact-BP00URmQ.cjs} +312 -2
  51. package/dist/shared/redact-BP00URmQ.cjs.map +1 -0
  52. package/dist/shared/{cookies-tX2sNwxb.js → redact-Ba5iSVYZ.js} +235 -3
  53. package/dist/shared/redact-Ba5iSVYZ.js.map +1 -0
  54. package/dist/shared/{render-CgwKdOzu.d.ts → render-B4qis7Sb.d.ts} +775 -313
  55. package/dist/shared/{render-DO0F5YSm.d.cts → render-DDwy2Ame.d.cts} +775 -313
  56. package/dist/shared/{render-mcuELiYi.cjs → render-Dqi8Owqk.cjs} +1464 -662
  57. package/dist/shared/render-Dqi8Owqk.cjs.map +1 -0
  58. package/dist/shared/{render-C6HRPs10.js → render-hiWFh2s4.js} +1366 -618
  59. package/dist/shared/render-hiWFh2s4.js.map +1 -0
  60. package/dist/shared/storage-BQrtKon9.d.cts +86 -0
  61. package/dist/shared/storage-CTRGT5wF.d.ts +86 -0
  62. package/dist/shared/storage-CfHKzVHf.js +71 -0
  63. package/dist/shared/storage-CfHKzVHf.js.map +1 -0
  64. package/dist/shared/storage-QNVxzpDB.cjs +88 -0
  65. package/dist/shared/storage-QNVxzpDB.cjs.map +1 -0
  66. package/dist/shared/{url-BaMCQpYH.cjs → url-Bsb3xE5U.cjs} +577 -466
  67. package/dist/shared/url-Bsb3xE5U.cjs.map +1 -0
  68. package/dist/shared/{url-IU0xN9wX.js → url-DP33mp3s.js} +508 -409
  69. package/dist/shared/url-DP33mp3s.js.map +1 -0
  70. package/dist/shared/{url-DTfZ2toq.d.ts → url-XBsMdAcv.d.cts} +1313 -1200
  71. package/dist/shared/{url-DTfZ2toq.d.cts → url-XBsMdAcv.d.ts} +1313 -1200
  72. package/dist/web/index.cjs +5 -4
  73. package/dist/web/index.cjs.map +1 -1
  74. package/dist/web/index.d.cts +1 -1
  75. package/dist/web/index.d.ts +1 -1
  76. package/dist/web/index.js +3 -2
  77. package/dist/web/index.js.map +1 -1
  78. package/package.json +16 -15
  79. package/dist/realtime/index.cjs +0 -165
  80. package/dist/realtime/index.cjs.map +0 -1
  81. package/dist/realtime/index.d.cts +0 -51
  82. package/dist/realtime/index.d.ts +0 -51
  83. package/dist/realtime/index.js +0 -120
  84. package/dist/realtime/index.js.map +0 -1
  85. package/dist/shared/auth-provider-CG8oCQ9F.cjs +0 -108
  86. package/dist/shared/auth-provider-CG8oCQ9F.cjs.map +0 -1
  87. package/dist/shared/auth-provider-mYqxsSVa.js +0 -91
  88. package/dist/shared/auth-provider-mYqxsSVa.js.map +0 -1
  89. package/dist/shared/cookies-DZwFq6kr.cjs.map +0 -1
  90. package/dist/shared/cookies-tX2sNwxb.js.map +0 -1
  91. package/dist/shared/options-Dg5N3r1V.cjs.map +0 -1
  92. package/dist/shared/options-DtATYdLr.js.map +0 -1
  93. package/dist/shared/render-C6HRPs10.js.map +0 -1
  94. package/dist/shared/render-mcuELiYi.cjs.map +0 -1
  95. package/dist/shared/storage-BPJR_k4-.cjs.map +0 -1
  96. package/dist/shared/storage-C_eICCep.d.cts +0 -152
  97. package/dist/shared/storage-C_eICCep.d.ts +0 -152
  98. package/dist/shared/storage-D86edNCB.js.map +0 -1
  99. package/dist/shared/url-BaMCQpYH.cjs.map +0 -1
  100. package/dist/shared/url-IU0xN9wX.js.map +0 -1
  101. package/dist/shared/websocket-BLR8eVJV.js.map +0 -1
  102. package/dist/shared/websocket-C_eI4H2o.cjs.map +0 -1
  103. package/dist/shared/websocket-DF7XIMiX.d.cts +0 -562
  104. package/dist/shared/websocket-DYKBr8HF.d.ts +0 -562
@@ -1,6 +1,6 @@
1
- const require_url = require("./url-BaMCQpYH.cjs");
2
- const require_errors = require("./errors-DfU8M5eS.cjs");
3
- const require_cookies = require("./cookies-DZwFq6kr.cjs");
1
+ const require_url = require("./url-Bsb3xE5U.cjs");
2
+ const require_errors = require("./errors-BmP3TKoW.cjs");
3
+ const require_redact = require("./redact-BP00URmQ.cjs");
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 = realtime.on('notification', (event) => console.log(event));
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/realtime/middleware.ts
88
- const REALTIME_MIDDLEWARE_SNAPSHOT = Symbol("itd-api.realtime.middlewareSnapshot");
87
+ //#region src/events/middleware.ts
88
+ const EVENT_MIDDLEWARE_SNAPSHOT = Symbol("itd-api.events.middlewareSnapshot");
89
89
  /** @internal */
90
- function captureRealtimeMiddleware(middleware) {
91
- const capture = middleware[REALTIME_MIDDLEWARE_SNAPSHOT];
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 withRealtimeMiddlewareSnapshot(middleware, capture) {
99
- Object.defineProperty(middleware, REALTIME_MIDDLEWARE_SNAPSHOT, { value: capture });
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 deferRealtimeMiddleware(source) {
103
+ function deferEventMiddleware(source) {
104
104
  const capture = () => {
105
105
  const middleware = source.middleware();
106
- if (typeof middleware !== "function") throw new require_errors.ItdConfigError("realtime middleware() должен возвращать функцию обработки");
107
- return captureRealtimeMiddleware(middleware);
106
+ if (typeof middleware !== "function") throw new require_errors.ItdConfigError("events middleware() должен возвращать функцию обработки");
107
+ return captureEventMiddleware(middleware);
108
108
  };
109
109
  const deferred = (context, next) => capture()(context, next);
110
- return withRealtimeMiddlewareSnapshot(deferred, capture);
110
+ return withEventMiddlewareSnapshot(deferred, capture);
111
111
  }
112
112
  /** Выполняет промежуточные обработчики по порядку и запрещает повторный вызов `next()`. */
113
- async function runRealtimeMiddleware(middleware, context, terminal) {
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 RealtimeDispatcher = class {
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(captureRealtimeMiddleware);
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 runRealtimeMiddleware(work.middleware, work.context, async () => {
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/realtime/reconnect.ts
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/realtime/transports/transport.ts
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/realtime/updates.ts
314
- /** Типы нормализованных обновлений потока. */
315
- const RealtimeUpdateType = Object.freeze({
316
- Notification: "notification",
317
- UnreadCount: "unreadCount",
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 require_errors.ItdConfigError("Тип уведомления не должен быть пустым");
329
- return;
330
- }
331
- if (Array.isArray(selector)) {
332
- if (selector.length === 0 || selector.some((type) => typeof type !== "string" || !type)) throw new require_errors.ItdConfigError("Список типов уведомлений должен содержать непустые строки");
333
- return;
334
- }
335
- if (typeof selector !== "object" || selector === null) throw new require_errors.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 require_errors.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 require_errors.ItdConfigError(`Фильтр уведомлений: ${field} должен быть строкой или null`);
342
- }
343
- if (filter.predicate !== void 0 && typeof filter.predicate !== "function") throw new require_errors.ItdConfigError("Фильтр уведомлений: predicate должен быть функцией");
344
- }
345
- /** Преобразует транспортный кадр в одно логическое обновление. */
346
- function readRealtimeUpdate(event) {
347
- if (event.name === "notification") return {
348
- type: RealtimeUpdateType.Notification,
349
- data: require_url.readNotificationEvent(event.data)
350
- };
351
- if (event.name === "unread_count") {
352
- const count = require_url.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 validateRealtimeEngineOptions(options) {
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 require_errors.ItdConfigError(`realtime.${name} должен быть целым неотрицательным числом, получено: ${value}`);
323
+ if (!Number.isInteger(value) || value < 0) throw new require_errors.ItdConfigError(`events.${name} должен быть целым неотрицательным числом, получено: ${value}`);
388
324
  };
389
325
  const duration = (value, name) => {
390
- if (!Number.isFinite(value) || value < 0) throw new require_errors.ItdConfigError(`realtime.${name} должен быть числом не меньше 0, получено: ${value}`);
326
+ if (!Number.isFinite(value) || value < 0) throw new require_errors.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 require_errors.ItdConfigError("realtime.concurrency должен быть больше нуля");
395
- if (options.jitter !== void 0 && !(options.jitter >= 0 && options.jitter <= 1)) throw new require_errors.ItdConfigError(`realtime.jitter должен быть в диапазоне 0…1, получено: ${options.jitter}`);
330
+ if (options.concurrency === 0) throw new require_errors.ItdConfigError("events.concurrency должен быть больше нуля");
331
+ if (options.jitter !== void 0 && !(options.jitter >= 0 && options.jitter <= 1)) throw new require_errors.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 require_errors.ItdConfigError("realtime.backoff должен быть непустым списком пауз");
333
+ if (!Array.isArray(options.backoff) || options.backoff.length === 0) throw new require_errors.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 require_errors.ItdConfigError("realtime.sequentialize должен быть функцией");
336
+ if (options.sequentialize !== void 0 && typeof options.sequentialize !== "function") throw new require_errors.ItdConfigError("events.sequentialize должен быть функцией");
337
+ return Object.freeze({
338
+ ...options,
339
+ ...options.backoff ? { backoff: Object.freeze([...options.backoff]) } : {}
340
+ });
401
341
  }
402
342
  /**
403
- * Общая механика потока событий: соединение, переподключение с backoff, обновление
343
+ * Общая механика потока событий: соединение, переподключение с задержкой, обновление
404
344
  * токена, реакция на среду, статусы и очередь обработчиков.
405
345
  *
406
346
  * Ничего не знает о домене: что считать обновлением, как собрать контекст и кому его
407
- * доставить, решают {@link RealtimeEngineDeps}. Поэтому один и тот же движок несёт
347
+ * доставить, решают {@link EventChannelDeps}. Поэтому один и тот же движок несёт
408
348
  * и поток уведомлений, и любой другой.
409
349
  *
410
- * @internal
411
350
  */
412
- var RealtimeEngine = class {
351
+ var EventChannel = class {
413
352
  #deps;
414
353
  #options;
415
354
  #dispatcher;
416
355
  #emitter;
417
356
  #clock;
418
357
  #maxAttempts;
419
- #controller;
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 = require_url.RealtimeStatus.Disconnected;
372
+ #status = require_url.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
- validateRealtimeEngineOptions(options);
379
+ options = resolveEventChannelOptions(options);
437
380
  this.#deps = deps;
438
381
  this.#options = options;
439
382
  this.#clock = deps.clock ?? require_url.systemClock;
440
383
  this.#maxAttempts = options.maxAttempts ?? 15;
441
- this.#emitter = new Emitter((error) => reportListenerError(deps.logger, "realtime", error));
442
- this.#dispatcher = new RealtimeDispatcher({
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 require_errors.ItdConfigError("realtime.use() принимает функцию обработки или объект с middleware()");
480
- return this.#dispatcher.use(deferRealtimeMiddleware(middleware));
422
+ if (typeof middleware !== "object" || middleware === null || typeof middleware.middleware !== "function") throw new require_errors.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
- if (this.#wanted) return;
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
- await this.#sync("initial", generation, starting);
501
- if (this.#starting !== starting) return;
502
- this.#starting = void 0;
503
- if (this.#isCurrentGeneration(generation)) this.#run(generation);
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.#controller?.abort();
517
- this.#controller = void 0;
459
+ this.#abortSession(new require_errors.ItdAbortError("Событийное соединение остановлено"));
518
460
  this.#attempt = 0;
519
461
  this.#dispatcher.clearPending();
520
- this.#setStatus(require_url.RealtimeStatus.Disconnected);
521
- this.#deps.onClose?.();
462
+ this.#setStatus(require_url.EventChannelStatus.Disconnected);
522
463
  }
523
- /** Ждёт завершения всех принятых обновлений. */
524
- drain() {
525
- return this.#dispatcher.drain();
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 controller = this.#controller;
538
- if (!this.#wanted || !controller) return;
539
- const generation = this.#generation;
540
- controller.abort();
541
- this.#controller = void 0;
542
- this.#setStatus(require_url.RealtimeStatus.Error);
491
+ const session = this.#session;
492
+ if (!this.#wanted || !session) return;
493
+ const generation = session.generation;
494
+ this.#abortSession(new require_errors.ItdAbortError("Очередь событий переполнена"));
495
+ this.#setStatus(require_url.EventChannelStatus.Error);
543
496
  const error = /* @__PURE__ */ new Error("Обработчики не успевают за потоком: очередь обновлений переполнена");
544
497
  this.#dispatcher.drain().then(() => {
545
- if (this.#isCurrentGeneration(generation) && !this.#controller) this.#scheduleReconnect(error);
498
+ if (this.#isCurrentGeneration(generation) && !this.#session) this.#scheduleReconnect(error);
546
499
  });
547
500
  }
548
- /** Выполняет доменную синхронизацию, не роняя подключение из-за её ошибки. */
549
- async #sync(reason, generation, starting) {
550
- if (!this.#deps.sync) return;
501
+ /** Создаёт новую попытку соединения и выполняет её подготовку. */
502
+ async #startAttempt(reason) {
503
+ if (!this.#wanted) return;
504
+ this.#abortSession(new require_errors.ItdAbortError("Событийное соединение заменено новой попыткой"));
505
+ const session = this.#createSession(++this.#generation);
506
+ this.#session = session;
551
507
  try {
552
- await this.#deps.sync(reason, (update) => {
553
- if (this.#starting !== starting || !this.#isCurrentGeneration(generation)) return;
554
- this.#dispatch(update, void 0, RealtimeUpdateOrigin.Sync);
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(require_url.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
- #run(generation = this.#generation) {
562
- if (!this.#isCurrentGeneration(generation)) return;
563
- this.#controller?.abort();
564
- const controller = new AbortController();
565
- this.#controller = controller;
566
- this.#setStatus(require_url.RealtimeStatus.Connecting);
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(require_url.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.#isCurrentConnection(controller, generation)) return;
550
+ if (!this.#isCurrentSession(session)) return;
577
551
  this.#attempt = 0;
578
- this.#setStatus(require_url.RealtimeStatus.Connected);
552
+ session.resolveOpened();
553
+ this.#setStatus(require_url.EventChannelStatus.Connected);
579
554
  },
580
555
  onEvent: (event) => {
581
- if (this.#isCurrentConnection(controller, generation)) this.#handleEvent(event);
556
+ if (this.#isCurrentSession(session)) this.#handleEvent(session, event);
582
557
  },
583
558
  onParseError: (error, raw) => {
584
- if (this.#isCurrentConnection(controller, generation)) this.#emitEngine("parseError", {
559
+ if (this.#isCurrentSession(session)) this.#emitEngine("parseError", {
585
560
  error,
586
561
  raw
587
562
  });
588
563
  }
589
- }).then(() => {
590
- if (this.#isCurrentConnection(controller, generation)) this.#handleFailure(/* @__PURE__ */ new Error("Соединение с потоком событий закрыто"));
564
+ })).then(() => {
565
+ if (this.#isCurrentSession(session)) this.#handleFailure(/* @__PURE__ */ new Error("Соединение с потоком событий закрыто"));
591
566
  }, (error) => {
592
- if (this.#isCurrentConnection(controller, generation)) this.#handleFailure(error);
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, RealtimeUpdateOrigin.Stream);
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.#controller = void 0;
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(require_url.RealtimeStatus.Error);
606
+ this.#setStatus(require_url.EventChannelStatus.Error);
621
607
  this.#scheduleReconnect(error);
622
608
  }
623
609
  /** Обновляет токен и переподключается; при неудаче прекращает попытки. */
624
610
  async #refreshAndReconnect(error) {
625
611
  const generation = this.#generation;
626
- const starting = {};
627
- this.#starting = starting;
628
- this.#setStatus(require_url.RealtimeStatus.Error);
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(require_url.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 generation = this.#generation;
663
- const starting = {};
645
+ const starting = this.#startAttempt("reconnect");
664
646
  this.#starting = starting;
665
- await this.#sync("reconnect", generation, starting);
666
- if (this.#starting !== starting) return;
667
- this.#starting = void 0;
668
- if (this.#isCurrentGeneration(generation)) this.#run(generation);
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
- * Владелец узнаёт об этом так же, как при {@link disconnect}. `onClose` вызывается
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.#deps.onClose?.();
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.#controller || this.#cancelTimer || this.#starting) return;
702
- if (this.#status === require_url.RealtimeStatus.Disconnected) return;
684
+ if (this.#session || this.#cancelTimer || this.#starting) return;
685
+ if (this.#status === require_url.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
- #isCurrentConnection(controller, generation) {
726
- return this.#isCurrentGeneration(generation) && this.#controller === controller && !controller.signal.aborted;
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/realtime/transports/poll.ts
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
- constructor(options = {}) {
828
+ #request;
829
+ constructor(options) {
830
+ this.#request = options.request;
751
831
  this.#clock = options.clock ?? require_url.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 require_errors.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 = require_url.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: "realtime.poll.updates",
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: "realtime.poll.unread",
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
- id = value2.includes("\0") ? void 0 : value2;
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
- id = value.includes("\0") ? void 0 : value;
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/realtime/transports/sse.ts
1079
+ //#region src/events/transports/sse.ts
1002
1080
  /** Путь потока уведомлений. */
1003
1081
  const STREAM_PATH = "/api/notifications/stream";
1004
1082
  /**
@@ -1027,7 +1105,7 @@ var SseTransport = class {
1027
1105
  async connect(context) {
1028
1106
  const token = context.authorize ? await context.getToken() : null;
1029
1107
  if (context.authorize && !token) throw new UnauthorizedStreamError();
1030
- const url = require_cookies.joinUrl(context.baseUrl, STREAM_PATH);
1108
+ const url = require_redact.joinUrl(context.baseUrl, STREAM_PATH);
1031
1109
  const headers = await context.baseHeaders(url);
1032
1110
  headers.set("Accept", "text/event-stream");
1033
1111
  if (token) headers.set("Authorization", `Bearer ${token}`);
@@ -1109,9 +1187,78 @@ var SseTransport = class {
1109
1187
  }
1110
1188
  };
1111
1189
  //#endregion
1112
- //#region src/realtime/stream.ts
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 require_errors.ItdConfigError("Тип уведомления не должен быть пустым");
1206
+ return;
1207
+ }
1208
+ if (Array.isArray(selector)) {
1209
+ if (selector.length === 0 || selector.some((type) => typeof type !== "string" || !type)) throw new require_errors.ItdConfigError("Список типов уведомлений должен содержать непустые строки");
1210
+ return;
1211
+ }
1212
+ if (typeof selector !== "object" || selector === null) throw new require_errors.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 require_errors.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 require_errors.ItdConfigError(`Фильтр уведомлений: ${field} должен быть строкой или null`);
1219
+ }
1220
+ if (filter.predicate !== void 0 && typeof filter.predicate !== "function") throw new require_errors.ItdConfigError("Фильтр уведомлений: predicate должен быть функцией");
1221
+ }
1222
+ /** Преобразует транспортный кадр в одно логическое обновление. */
1223
+ function readNotificationEventsUpdate(event) {
1224
+ if (event.name === "notification") return {
1225
+ type: NotificationUpdateType.Notification,
1226
+ data: require_url.readNotificationEvent(event.data)
1227
+ };
1228
+ if (event.name === "unread_count") {
1229
+ const count = require_url.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 RealtimeTransportKind = Object.freeze({
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 validateRealtimeOptions(options) {
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 require_errors.ItdConfigError(`realtime.${name} должен быть числом не меньше ${min}, получено: ${value}`);
1274
+ if (!Number.isFinite(value) || value < min) throw new require_errors.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 REALTIME_CONNECT_GUARDS = /* @__PURE__ */ new WeakMap();
1133
- /** Связывает поток с lifecycle создавшего его клиента. @internal */
1134
- function setRealtimeConnectGuard(stream, guard) {
1135
- REALTIME_CONNECT_GUARDS.set(stream, guard);
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
- * Получается вызовом `itd.realtime()`. Соединение поднимается методом {@link connect}
1292
+ * Доступен как стабильное свойство `itd.notifications.events`. Соединение поднимается методом {@link connect}
1141
1293
  * и держится само: обрывы, обновление токена и повторные попытки библиотека берёт на себя.
1142
1294
  *
1143
1295
  * Параметр типа задаёт форму контекста: плагин может расширить её своими полями
1144
- * (`ItdRealtime<RealtimeContext & SessionFlavor<S>>`) и типизировать обработчики.
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.realtime();
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 ItdRealtime = class {
1315
+ var NotificationEvents = class {
1164
1316
  #deps;
1165
1317
  #engine;
1318
+ #lifecycleActive = false;
1319
+ /** @internal */
1166
1320
  constructor(deps, options = {}) {
1167
- validateRealtimeOptions(options);
1321
+ options = resolveNotificationEventsOptions(options);
1168
1322
  this.#deps = deps;
1169
- this.#engine = new RealtimeEngine({
1170
- baseUrl: deps.baseUrl,
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: readRealtimeUpdate,
1184
- coalesceKey: (update) => update.type === RealtimeUpdateType.UnreadCount ? update.type : void 0,
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 ? {} : { sync: (reason, dispatch) => this.#syncUnreadCount(reason, dispatch) }
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 require_errors.ItdConfigError("realtime.onUpdate() принимает функцию обработчика");
1238
- if (!selectAll && typeof selectorOrHandler !== "function" && !Object.values(RealtimeUpdateType).includes(selectorOrHandler)) throw new require_errors.ItdConfigError(`Неизвестный тип обновления потока: ${String(selectorOrHandler)}`);
1385
+ if (typeof handler !== "function") throw new require_errors.ItdConfigError("events.onUpdate() принимает функцию обработчика");
1386
+ if (!selectAll && typeof selectorOrHandler !== "function" && !Object.values(NotificationUpdateType).includes(selectorOrHandler)) throw new require_errors.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 require_errors.ItdConfigError("realtime.onNotification() принимает функцию обработчика");
1396
+ if (typeof handler !== "function") throw new require_errors.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
- REALTIME_CONNECT_GUARDS.get(this)?.();
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: RealtimeUpdateType.UnreadCount,
1460
+ type: NotificationUpdateType.UnreadCount,
1299
1461
  data: count
1300
1462
  });
1301
1463
  }
1302
1464
  #deliver(update) {
1303
- if (update.type === RealtimeUpdateType.Notification) {
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 === RealtimeUpdateType.UnreadCount) {
1470
+ if (update.type === NotificationUpdateType.UnreadCount) {
1309
1471
  this.#engine.emit("unreadCount", update.data);
1310
1472
  return;
1311
1473
  }
1312
- if (update.type === RealtimeUpdateType.Unknown) return;
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 ?? RealtimeTransportKind.Auto;
1480
+ const kind = options.transport ?? NotificationEventsTransport.Auto;
1319
1481
  if (typeof kind === "object") return kind;
1320
- if (kind === RealtimeTransportKind.Poll || kind === RealtimeTransportKind.Auto && !require_url.supportsStreamingBody()) return new PollTransport({
1321
- ...deps.clock ? { clock: deps.clock } : {},
1322
- ...options.pollInterval !== void 0 ? { interval: options.pollInterval } : {}
1323
- });
1482
+ if (kind === NotificationEventsTransport.Poll || kind === NotificationEventsTransport.Auto && !require_redact.supportsStreamingBody()) {
1483
+ if (!deps.request) throw new require_errors.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,261 +1497,42 @@ function assertNeverUpdate(update) {
1331
1497
  throw new TypeError(`Необработанное обновление потока: ${String(update)}`);
1332
1498
  }
1333
1499
  //#endregion
1334
- //#region src/realtime/router.ts
1335
- /**
1336
- * Направляет обновления потока в именованные цепочки промежуточных обработчиков.
1337
- *
1338
- * @example
1339
- * ```ts
1340
- * import { RealtimeRouter, RealtimeUpdateType } from 'itd-api';
1341
- *
1342
- * const router = new RealtimeRouter((context) => context.update.type);
1343
- * router.route(RealtimeUpdateType.Notification, async (context, next) => {
1344
- * if (context.update.type === RealtimeUpdateType.Notification) {
1345
- * await handleNotification(context.update.data.notification);
1346
- * }
1347
- * await next();
1348
- * });
1349
- * stream.use(router);
1350
- * ```
1351
- */
1352
- var RealtimeRouter = class {
1353
- #selector;
1354
- #routes = /* @__PURE__ */ new Map();
1355
- #fallback = [];
1356
- constructor(selector) {
1357
- if (typeof selector !== "function") throw new require_errors.ItdConfigError("RealtimeRouter принимает функцию выбора маршрута");
1358
- this.#selector = selector;
1359
- }
1360
- /** Добавляет промежуточные обработчики к маршруту и возвращает функцию их удаления. */
1361
- route(key, ...middleware) {
1362
- if (!isPropertyKey(key)) throw new require_errors.ItdConfigError("Ключ realtime route должен быть PropertyKey");
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 require_errors.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 require_errors.ItdConfigError("Маршрут должен содержать хотя бы один обработчик");
1407
- for (const item of middleware) if (typeof item !== "function") throw new require_errors.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 require_errors.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 require_errors.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 require_errors.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 require_errors.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 require_errors.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 require_errors.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 require_errors.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": require_url.passthroughOperation("events.notifications.poll.updates"),
1504
+ "events.notifications.poll.unread": require_url.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 require_errors.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) {
1585
1532
  const message = errorMessage(error);
1586
1533
  if (!message) return void 0;
1587
- const safe = message.replaceAll(url, require_url.redactUrl(url));
1588
- return token ? safe.replaceAll(token, require_url.maskSecret(token)) : safe;
1534
+ const safe = message.replaceAll(url, require_redact.redactUrl(url));
1535
+ return token ? safe.replaceAll(token, require_redact.maskSecret(token)) : safe;
1589
1536
  }
1590
1537
  function readCloseEvent(event) {
1591
1538
  const close = typeof event === "object" && event !== null ? event : void 0;
@@ -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
- /** Транспорт исходных realtime-событий поверх стандартного WebSocket. */
1555
+ /** Транспорт исходных событий поверх стандартного WebSocket. */
1609
1556
  var WebSocketTransport = class {
1610
1557
  name = "ws";
1611
1558
  #path;
@@ -1645,7 +1592,7 @@ var WebSocketTransport = class {
1645
1592
  const implementation = this.#webSocketImpl ?? this.#globalImplementation();
1646
1593
  const auth = this.#auth === "auto" ? injected ? "header" : "query" : this.#auth;
1647
1594
  const { requestUrl, socketUrl } = this.#urls(context.baseUrl);
1648
- const url = auth === "query" && token ? `${socketUrl}${require_cookies.buildQuery({ token })}` : socketUrl;
1595
+ const url = auth === "query" && token ? `${socketUrl}${require_redact.buildQuery({ token })}` : socketUrl;
1649
1596
  let headers;
1650
1597
  if (injected) {
1651
1598
  const resolved = await context.baseHeaders(requestUrl);
@@ -1661,7 +1608,7 @@ var WebSocketTransport = class {
1661
1608
  }) : new implementation(url);
1662
1609
  } catch (error) {
1663
1610
  const detail = safeDetail(error, url, token);
1664
- throw new Error(`Не удалось создать WebSocket ${require_url.redactUrl(url)}${detail ? `: ${detail}` : ""}`);
1611
+ throw new Error(`Не удалось создать WebSocket ${require_redact.redactUrl(url)}${detail ? `: ${detail}` : ""}`);
1665
1612
  }
1666
1613
  if (!isWebSocketConnection(value)) throw new require_errors.ItdConfigError("webSocketImpl вернул объект без WebSocket-методов addEventListener, send и close");
1667
1614
  try {
@@ -1675,7 +1622,7 @@ var WebSocketTransport = class {
1675
1622
  return implementation;
1676
1623
  }
1677
1624
  #urls(baseUrl) {
1678
- const request = new URL(require_cookies.joinUrl(baseUrl, this.#path));
1625
+ const request = new URL(require_redact.joinUrl(baseUrl, this.#path));
1679
1626
  const socket = new URL(request);
1680
1627
  if (request.protocol === "https:") socket.protocol = "wss:";
1681
1628
  else if (request.protocol === "http:") socket.protocol = "ws:";
@@ -1731,7 +1678,7 @@ var WebSocketTransport = class {
1731
1678
  cancelIdle?.();
1732
1679
  if (this.#idleTimeout <= 0) return;
1733
1680
  cancelIdle = this.#clock.schedule(() => {
1734
- closeWithError(/* @__PURE__ */ new Error(`WebSocket ${require_url.redactUrl(url)} молчит дольше допустимого`), 4e3, "idle timeout");
1681
+ closeWithError(/* @__PURE__ */ new Error(`WebSocket ${require_redact.redactUrl(url)} молчит дольше допустимого`), 4e3, "idle timeout");
1735
1682
  }, this.#idleTimeout);
1736
1683
  };
1737
1684
  const scheduleKeepAlive = () => {
@@ -1742,7 +1689,7 @@ var WebSocketTransport = class {
1742
1689
  socket.send("ping");
1743
1690
  } catch (error) {
1744
1691
  const detail = safeDetail(error, url, token);
1745
- closeWithError(/* @__PURE__ */ new Error(`Не удалось отправить WebSocket ping ${require_url.redactUrl(url)}` + (detail ? `: ${detail}` : "")));
1692
+ closeWithError(/* @__PURE__ */ new Error(`Не удалось отправить WebSocket ping ${require_redact.redactUrl(url)}` + (detail ? `: ${detail}` : "")));
1746
1693
  return;
1747
1694
  }
1748
1695
  scheduleKeepAlive();
@@ -1773,7 +1720,7 @@ var WebSocketTransport = class {
1773
1720
  }
1774
1721
  if (pending === void 0) return;
1775
1722
  if (pendingFrames >= 256) {
1776
- closeWithError(/* @__PURE__ */ new Error(`WebSocket ${require_url.redactUrl(url)}: очередь кадров переполнена`), 4e3, "queue overflow");
1723
+ closeWithError(/* @__PURE__ */ new Error(`WebSocket ${require_redact.redactUrl(url)}: очередь кадров переполнена`), 4e3, "queue overflow");
1777
1724
  return;
1778
1725
  }
1779
1726
  pendingFrames += 1;
@@ -1813,11 +1760,11 @@ var WebSocketTransport = class {
1813
1760
  if (context.authorize && UNAUTHORIZED_CLOSE_CODES.has(code)) outcome = new UnauthorizedStreamError();
1814
1761
  else if (socketError !== void 0) {
1815
1762
  const detail = safeDetail(socketError, url, token);
1816
- outcome = /* @__PURE__ */ new Error(`WebSocket ${require_url.redactUrl(url)} завершился ошибкой${detail ? `: ${detail}` : ""}`);
1763
+ outcome = /* @__PURE__ */ new Error(`WebSocket ${require_redact.redactUrl(url)} завершился ошибкой${detail ? `: ${detail}` : ""}`);
1817
1764
  } else if (opened && code === NORMAL_CLOSURE && wasClean) outcome = void 0;
1818
1765
  else {
1819
- const safeReason = token ? reason.replaceAll(token, require_url.maskSecret(token)) : reason;
1820
- outcome = /* @__PURE__ */ new Error(`WebSocket ${require_url.redactUrl(url)} закрыт с кодом ${code}` + (safeReason ? `: ${safeReason}` : ""));
1766
+ const safeReason = token ? reason.replaceAll(token, require_redact.maskSecret(token)) : reason;
1767
+ outcome = /* @__PURE__ */ new Error(`WebSocket ${require_redact.redactUrl(url)} закрыт с кодом ${code}` + (safeReason ? `: ${safeReason}` : ""));
1821
1768
  }
1822
1769
  if (!opened && context.authorize && !(outcome instanceof UnauthorizedStreamError) && this.#classifyOpenFailure) try {
1823
1770
  if (await this.#classifyOpenFailure(outcome, context.signal)) outcome = new UnauthorizedStreamError();
@@ -1862,53 +1809,404 @@ var WebSocketTransport = class {
1862
1809
  socket.addEventListener("close", onClose);
1863
1810
  context.signal.addEventListener("abort", onAbort, { once: true });
1864
1811
  if (this.#handshakeTimeout > 0) cancelHandshake = this.#clock.schedule(() => {
1865
- closeWithError(/* @__PURE__ */ new Error(`Истёк таймаут установки WebSocket ${require_url.redactUrl(url)}`), 4e3, "handshake timeout");
1812
+ closeWithError(/* @__PURE__ */ new Error(`Истёк таймаут установки WebSocket ${require_redact.redactUrl(url)}`), 4e3, "handshake timeout");
1866
1813
  }, this.#handshakeTimeout);
1867
1814
  if (context.signal.aborted) onAbort();
1868
1815
  });
1869
1816
  }
1870
1817
  };
1871
1818
  //#endregion
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 = require_url.createClientRuntime(options, {
1843
+ catalog: require_url.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 require_errors.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
+ require_url.installAsyncDisposeFallback(NotificationEventsClient);
1887
+ }
1888
+ };
1889
+ /** Приводит опцию `auth` к провайдеру конвейера. */
1890
+ function resolveAuthProvider(auth) {
1891
+ if (auth === void 0) return require_url.anonymousAuth();
1892
+ return typeof auth === "string" ? require_url.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 require_url.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 require_errors.ItdConfigError("EventRouter принимает функцию выбора маршрута");
1953
+ this.#selector = selector;
1954
+ }
1955
+ /** Добавляет промежуточные обработчики к маршруту и возвращает функцию их удаления. */
1956
+ route(key, ...middleware) {
1957
+ if (!isPropertyKey(key)) throw new require_errors.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 require_errors.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 require_errors.ItdConfigError("Маршрут должен содержать хотя бы один обработчик");
2002
+ for (const item of middleware) if (typeof item !== "function") throw new require_errors.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 require_errors.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 require_errors.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 require_errors.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 require_errors.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 require_errors.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 require_errors.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
+ Object.defineProperty(exports, "EVENT_OPERATIONS", {
2153
+ enumerable: true,
2154
+ get: function() {
2155
+ return EVENT_OPERATIONS;
2156
+ }
2157
+ });
1872
2158
  Object.defineProperty(exports, "Emitter", {
1873
2159
  enumerable: true,
1874
2160
  get: function() {
1875
2161
  return Emitter;
1876
2162
  }
1877
2163
  });
1878
- Object.defineProperty(exports, "ItdRealtime", {
2164
+ Object.defineProperty(exports, "EventChannel", {
2165
+ enumerable: true,
2166
+ get: function() {
2167
+ return EventChannel;
2168
+ }
2169
+ });
2170
+ Object.defineProperty(exports, "EventComposer", {
2171
+ enumerable: true,
2172
+ get: function() {
2173
+ return EventComposer;
2174
+ }
2175
+ });
2176
+ Object.defineProperty(exports, "EventRouter", {
1879
2177
  enumerable: true,
1880
2178
  get: function() {
1881
- return ItdRealtime;
2179
+ return EventRouter;
1882
2180
  }
1883
2181
  });
1884
- Object.defineProperty(exports, "RealtimeComposer", {
2182
+ Object.defineProperty(exports, "NotificationEvents", {
1885
2183
  enumerable: true,
1886
2184
  get: function() {
1887
- return RealtimeComposer;
2185
+ return NotificationEvents;
1888
2186
  }
1889
2187
  });
1890
- Object.defineProperty(exports, "RealtimeRouter", {
2188
+ Object.defineProperty(exports, "NotificationEventsClient", {
1891
2189
  enumerable: true,
1892
2190
  get: function() {
1893
- return RealtimeRouter;
2191
+ return NotificationEventsClient;
1894
2192
  }
1895
2193
  });
1896
- Object.defineProperty(exports, "RealtimeTransportKind", {
2194
+ Object.defineProperty(exports, "NotificationEventsTransport", {
1897
2195
  enumerable: true,
1898
2196
  get: function() {
1899
- return RealtimeTransportKind;
2197
+ return NotificationEventsTransport;
1900
2198
  }
1901
2199
  });
1902
- Object.defineProperty(exports, "RealtimeUpdateOrigin", {
2200
+ Object.defineProperty(exports, "NotificationUpdateOrigin", {
1903
2201
  enumerable: true,
1904
2202
  get: function() {
1905
- return RealtimeUpdateOrigin;
2203
+ return NotificationUpdateOrigin;
1906
2204
  }
1907
2205
  });
1908
- Object.defineProperty(exports, "RealtimeUpdateType", {
2206
+ Object.defineProperty(exports, "NotificationUpdateType", {
1909
2207
  enumerable: true,
1910
2208
  get: function() {
1911
- return RealtimeUpdateType;
2209
+ return NotificationUpdateType;
1912
2210
  }
1913
2211
  });
1914
2212
  Object.defineProperty(exports, "UnauthorizedStreamError", {
@@ -1923,23 +2221,41 @@ Object.defineProperty(exports, "WebSocketTransport", {
1923
2221
  return WebSocketTransport;
1924
2222
  }
1925
2223
  });
2224
+ Object.defineProperty(exports, "createNotificationEventsClient", {
2225
+ enumerable: true,
2226
+ get: function() {
2227
+ return createNotificationEventsClient;
2228
+ }
2229
+ });
1926
2230
  Object.defineProperty(exports, "reportListenerError", {
1927
2231
  enumerable: true,
1928
2232
  get: function() {
1929
2233
  return reportListenerError;
1930
2234
  }
1931
2235
  });
1932
- Object.defineProperty(exports, "runRealtimeMiddleware", {
2236
+ Object.defineProperty(exports, "resolveEventChannelOptions", {
2237
+ enumerable: true,
2238
+ get: function() {
2239
+ return resolveEventChannelOptions;
2240
+ }
2241
+ });
2242
+ Object.defineProperty(exports, "resolveNotificationEventsOptions", {
2243
+ enumerable: true,
2244
+ get: function() {
2245
+ return resolveNotificationEventsOptions;
2246
+ }
2247
+ });
2248
+ Object.defineProperty(exports, "runEventMiddleware", {
1933
2249
  enumerable: true,
1934
2250
  get: function() {
1935
- return runRealtimeMiddleware;
2251
+ return runEventMiddleware;
1936
2252
  }
1937
2253
  });
1938
- Object.defineProperty(exports, "setRealtimeConnectGuard", {
2254
+ Object.defineProperty(exports, "setNotificationEventsConnectGuard", {
1939
2255
  enumerable: true,
1940
2256
  get: function() {
1941
- return setRealtimeConnectGuard;
2257
+ return setNotificationEventsConnectGuard;
1942
2258
  }
1943
2259
  });
1944
2260
 
1945
- //# sourceMappingURL=websocket-C_eI4H2o.cjs.map
2261
+ //# sourceMappingURL=events-FZfnEez0.cjs.map