itd-api 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +465 -9804
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +231 -4950
- package/dist/index.d.ts +231 -4950
- package/dist/index.js +331 -9670
- package/dist/index.js.map +1 -1
- package/dist/{node.cjs → node/index.cjs} +16 -14
- package/dist/node/index.cjs.map +1 -0
- package/dist/{node.d.cts → node/index.d.cts} +4 -3
- package/dist/{node.d.ts → node/index.d.ts} +4 -3
- package/dist/{node.js → node/index.js} +5 -3
- package/dist/node/index.js.map +1 -0
- package/dist/realtime/index.cjs +165 -0
- package/dist/realtime/index.cjs.map +1 -0
- package/dist/realtime/index.d.cts +51 -0
- package/dist/realtime/index.d.ts +51 -0
- package/dist/realtime/index.js +120 -0
- package/dist/realtime/index.js.map +1 -0
- package/dist/rest/index.cjs +334 -0
- package/dist/rest/index.cjs.map +1 -0
- package/dist/rest/index.d.cts +138 -0
- package/dist/rest/index.d.ts +138 -0
- package/dist/rest/index.js +238 -0
- package/dist/rest/index.js.map +1 -0
- package/dist/shared/auth-provider-BfogACAb.js +91 -0
- package/dist/shared/auth-provider-BfogACAb.js.map +1 -0
- package/dist/shared/auth-provider-CTJkKfgy.cjs +108 -0
- package/dist/shared/auth-provider-CTJkKfgy.cjs.map +1 -0
- package/dist/shared/contracts-BoT7msmq.d.cts +84 -0
- package/dist/shared/contracts-BoT7msmq.d.ts +84 -0
- package/dist/{multi-storage-Bf84xiO8.cjs → shared/cookies-DZwFq6kr.cjs} +98 -429
- package/dist/shared/cookies-DZwFq6kr.cjs.map +1 -0
- package/dist/{multi-storage-BUZaLAPO.js → shared/cookies-tX2sNwxb.js} +99 -352
- package/dist/shared/cookies-tX2sNwxb.js.map +1 -0
- package/dist/{storage-IHdXw52v.js → shared/errors-Bhrd2fJd.js} +2 -229
- package/dist/shared/errors-Bhrd2fJd.js.map +1 -0
- package/dist/{storage-DnzZPS_9.cjs → shared/errors-DfU8M5eS.cjs} +1 -288
- package/dist/shared/errors-DfU8M5eS.cjs.map +1 -0
- package/dist/shared/multi-storage--yTEqiod.cjs +150 -0
- package/dist/shared/multi-storage--yTEqiod.cjs.map +1 -0
- package/dist/shared/multi-storage-CjAPB5Kq.d.cts +72 -0
- package/dist/shared/multi-storage-CkvTUC5m.js +121 -0
- package/dist/shared/multi-storage-CkvTUC5m.js.map +1 -0
- package/dist/shared/multi-storage-DccjD7Ww.d.ts +72 -0
- package/dist/shared/options-Dg5N3r1V.cjs +189 -0
- package/dist/shared/options-Dg5N3r1V.cjs.map +1 -0
- package/dist/shared/options-DtATYdLr.js +142 -0
- package/dist/shared/options-DtATYdLr.js.map +1 -0
- package/dist/shared/render-DMp_3Nzk.d.cts +2250 -0
- package/dist/shared/render-DZrxhC5_.d.ts +2250 -0
- package/dist/shared/render-DyeHJNBw.cjs +4147 -0
- package/dist/shared/render-DyeHJNBw.cjs.map +1 -0
- package/dist/shared/render-vtLixIiU.js +3992 -0
- package/dist/shared/render-vtLixIiU.js.map +1 -0
- package/dist/shared/storage-BPJR_k4-.cjs +290 -0
- package/dist/shared/storage-BPJR_k4-.cjs.map +1 -0
- package/dist/{storage-BqMxs76Y.d.ts → shared/storage-C_eICCep.d.cts} +2 -2
- package/dist/{storage-BqMxs76Y.d.cts → shared/storage-C_eICCep.d.ts} +2 -2
- package/dist/shared/storage-D86edNCB.js +231 -0
- package/dist/shared/storage-D86edNCB.js.map +1 -0
- package/dist/shared/url-B6-bXHKt.d.cts +2083 -0
- package/dist/shared/url-B6-bXHKt.d.ts +2083 -0
- package/dist/shared/url-CYXgxqGx.js +3673 -0
- package/dist/shared/url-CYXgxqGx.js.map +1 -0
- package/dist/shared/url-yjl2c8Ie.cjs +4032 -0
- package/dist/shared/url-yjl2c8Ie.cjs.map +1 -0
- package/dist/shared/websocket-BMtihD56.d.ts +562 -0
- package/dist/shared/websocket-CbzB1Leq.js +1874 -0
- package/dist/shared/websocket-CbzB1Leq.js.map +1 -0
- package/dist/shared/websocket-D1p32SB2.d.cts +562 -0
- package/dist/shared/websocket-sYlynrr0.cjs +1945 -0
- package/dist/shared/websocket-sYlynrr0.cjs.map +1 -0
- package/dist/{web.cjs → web/index.cjs} +4 -3
- package/dist/web/index.cjs.map +1 -0
- package/dist/{web.d.cts → web/index.d.cts} +2 -2
- package/dist/{web.d.ts → web/index.d.ts} +2 -2
- package/dist/{web.js → web/index.js} +3 -2
- package/dist/web/index.js.map +1 -0
- package/package.json +40 -12
- package/dist/multi-storage-BUZaLAPO.js.map +0 -1
- package/dist/multi-storage-BUoEoW8f.d.cts +0 -154
- package/dist/multi-storage-Bf84xiO8.cjs.map +0 -1
- package/dist/multi-storage-CQSlI_kn.d.ts +0 -154
- package/dist/node.cjs.map +0 -1
- package/dist/node.js.map +0 -1
- package/dist/storage-DnzZPS_9.cjs.map +0 -1
- package/dist/storage-IHdXw52v.js.map +0 -1
- package/dist/web.cjs.map +0 -1
- package/dist/web.js.map +0 -1
|
@@ -0,0 +1,1945 @@
|
|
|
1
|
+
const require_url = require("./url-yjl2c8Ie.cjs");
|
|
2
|
+
const require_errors = require("./errors-DfU8M5eS.cjs");
|
|
3
|
+
const require_cookies = require("./cookies-DZwFq6kr.cjs");
|
|
4
|
+
//#region src/core/emitter.ts
|
|
5
|
+
/**
|
|
6
|
+
* Минимальный типизированный источник событий.
|
|
7
|
+
*
|
|
8
|
+
* Своя реализация вместо `EventTarget` и `EventEmitter`: первый есть не везде и требует
|
|
9
|
+
* обёрток `CustomEvent`, второй существует только в Node. Нужны ровно подписка и рассылка.
|
|
10
|
+
*
|
|
11
|
+
* Исключение в обработчике не прерывает рассылку остальным и не роняет библиотеку.
|
|
12
|
+
*
|
|
13
|
+
* @typeParam Events карта «имя события → тип полезной нагрузки». Задаётся интерфейсом,
|
|
14
|
+
* поэтому ограничение на индексную сигнатуру намеренно не накладывается.
|
|
15
|
+
*/
|
|
16
|
+
var Emitter = class {
|
|
17
|
+
#listeners = /* @__PURE__ */ new Map();
|
|
18
|
+
#onError;
|
|
19
|
+
constructor(onListenerError) {
|
|
20
|
+
this.#onError = onListenerError;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Подписывается на событие.
|
|
24
|
+
*
|
|
25
|
+
* @returns функция отписки
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* const off = realtime.on('notification', (event) => console.log(event));
|
|
30
|
+
* off();
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
on(event, listener) {
|
|
34
|
+
const set = this.#listeners.get(event) ?? /* @__PURE__ */ new Set();
|
|
35
|
+
set.add(listener);
|
|
36
|
+
this.#listeners.set(event, set);
|
|
37
|
+
return () => this.off(event, listener);
|
|
38
|
+
}
|
|
39
|
+
/** Подписывается на одно срабатывание. */
|
|
40
|
+
once(event, listener) {
|
|
41
|
+
const off = this.on(event, (payload) => {
|
|
42
|
+
off();
|
|
43
|
+
listener(payload);
|
|
44
|
+
});
|
|
45
|
+
return off;
|
|
46
|
+
}
|
|
47
|
+
/** Отписывается от события. */
|
|
48
|
+
off(event, listener) {
|
|
49
|
+
this.#listeners.get(event)?.delete(listener);
|
|
50
|
+
}
|
|
51
|
+
/** Рассылает событие подписчикам. */
|
|
52
|
+
emit(event, payload) {
|
|
53
|
+
const set = this.#listeners.get(event);
|
|
54
|
+
if (!set) return;
|
|
55
|
+
for (const listener of [...set]) try {
|
|
56
|
+
listener(payload);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
this.#onError?.(error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Сколько подписчиков у события. */
|
|
62
|
+
listenerCount(event) {
|
|
63
|
+
return this.#listeners.get(event)?.size ?? 0;
|
|
64
|
+
}
|
|
65
|
+
/** Снимает все подписки. */
|
|
66
|
+
removeAllListeners() {
|
|
67
|
+
this.#listeners.clear();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Сообщает об исключении из пользовательского обработчика события: пишет в логгер,
|
|
72
|
+
* при его отсутствии — в консоль.
|
|
73
|
+
*
|
|
74
|
+
* Ошибка чужого обработчика не должна ронять библиотеку, но и пропадать молча ей нельзя:
|
|
75
|
+
* иначе подписка, падающая на каждом событии, выглядит как её отсутствие.
|
|
76
|
+
*
|
|
77
|
+
* @param scope о чьих событиях речь — попадает в текст сообщения
|
|
78
|
+
*
|
|
79
|
+
* @internal
|
|
80
|
+
*/
|
|
81
|
+
function reportListenerError(logger, scope, error) {
|
|
82
|
+
const message = `Ошибка в обработчике события ${scope}`;
|
|
83
|
+
if (logger) logger.error(message, error);
|
|
84
|
+
else console.error(`[itd-api] ${message}`, error);
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/realtime/middleware.ts
|
|
88
|
+
const REALTIME_MIDDLEWARE_SNAPSHOT = Symbol("itd-api.realtime.middlewareSnapshot");
|
|
89
|
+
/** @internal */
|
|
90
|
+
function captureRealtimeMiddleware(middleware) {
|
|
91
|
+
const capture = middleware[REALTIME_MIDDLEWARE_SNAPSHOT];
|
|
92
|
+
if (!capture) return middleware;
|
|
93
|
+
const snapshot = capture();
|
|
94
|
+
if (typeof snapshot !== "function") throw new TypeError("Снимок промежуточного обработчика должен быть функцией");
|
|
95
|
+
return snapshot;
|
|
96
|
+
}
|
|
97
|
+
/** @internal */
|
|
98
|
+
function withRealtimeMiddlewareSnapshot(middleware, capture) {
|
|
99
|
+
Object.defineProperty(middleware, REALTIME_MIDDLEWARE_SNAPSHOT, { value: capture });
|
|
100
|
+
return middleware;
|
|
101
|
+
}
|
|
102
|
+
/** Создаёт отложенный промежуточный обработчик для объекта. @internal */
|
|
103
|
+
function deferRealtimeMiddleware(source) {
|
|
104
|
+
const capture = () => {
|
|
105
|
+
const middleware = source.middleware();
|
|
106
|
+
if (typeof middleware !== "function") throw new require_errors.ItdConfigError("realtime middleware() должен возвращать функцию обработки");
|
|
107
|
+
return captureRealtimeMiddleware(middleware);
|
|
108
|
+
};
|
|
109
|
+
const deferred = (context, next) => capture()(context, next);
|
|
110
|
+
return withRealtimeMiddlewareSnapshot(deferred, capture);
|
|
111
|
+
}
|
|
112
|
+
/** Выполняет промежуточные обработчики по порядку и запрещает повторный вызов `next()`. */
|
|
113
|
+
async function runRealtimeMiddleware(middleware, context, terminal) {
|
|
114
|
+
let lastIndex = -1;
|
|
115
|
+
const dispatch = async (index) => {
|
|
116
|
+
if (index <= lastIndex) throw new Error("next() в промежуточном обработчике вызван повторно");
|
|
117
|
+
lastIndex = index;
|
|
118
|
+
const current = middleware[index];
|
|
119
|
+
if (!current) {
|
|
120
|
+
await terminal();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
let downstream;
|
|
124
|
+
let duplicateCalls;
|
|
125
|
+
let failure;
|
|
126
|
+
const next = () => {
|
|
127
|
+
if (!downstream) {
|
|
128
|
+
downstream = dispatch(index + 1);
|
|
129
|
+
return downstream;
|
|
130
|
+
}
|
|
131
|
+
const duplicate = Promise.reject(/* @__PURE__ */ new Error("next() в одном промежуточном обработчике вызван повторно"));
|
|
132
|
+
duplicateCalls = Promise.all(duplicateCalls ? [duplicateCalls, duplicate] : [duplicate]).then(() => void 0);
|
|
133
|
+
return duplicate;
|
|
134
|
+
};
|
|
135
|
+
try {
|
|
136
|
+
await current(context, next);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
failure = { error };
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
await downstream;
|
|
142
|
+
await duplicateCalls;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
failure ??= { error };
|
|
145
|
+
}
|
|
146
|
+
if (failure) throw failure.error;
|
|
147
|
+
};
|
|
148
|
+
await dispatch(0);
|
|
149
|
+
}
|
|
150
|
+
/** Планирует нормализованные обновления и отслеживает незавершённые обработчики. */
|
|
151
|
+
var RealtimeDispatcher = class {
|
|
152
|
+
#options;
|
|
153
|
+
#hooks;
|
|
154
|
+
#middleware = [];
|
|
155
|
+
#handlers = [];
|
|
156
|
+
#queue = [];
|
|
157
|
+
#activeKeys = /* @__PURE__ */ new Set();
|
|
158
|
+
#drainWaiters = /* @__PURE__ */ new Set();
|
|
159
|
+
#active = 0;
|
|
160
|
+
constructor(options, hooks) {
|
|
161
|
+
this.#options = options;
|
|
162
|
+
this.#hooks = hooks;
|
|
163
|
+
}
|
|
164
|
+
use(middleware) {
|
|
165
|
+
this.#middleware.push(middleware);
|
|
166
|
+
return () => {
|
|
167
|
+
const index = this.#middleware.indexOf(middleware);
|
|
168
|
+
if (index >= 0) this.#middleware.splice(index, 1);
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
on(predicate, handler) {
|
|
172
|
+
const registration = {
|
|
173
|
+
predicate,
|
|
174
|
+
handler
|
|
175
|
+
};
|
|
176
|
+
this.#handlers.push(registration);
|
|
177
|
+
return () => {
|
|
178
|
+
const index = this.#handlers.indexOf(registration);
|
|
179
|
+
if (index >= 0) this.#handlers.splice(index, 1);
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Принимает обновление к обработке.
|
|
184
|
+
*
|
|
185
|
+
* @param coalesceKey ожидающая работа с тем же ключом заменяется новой
|
|
186
|
+
*/
|
|
187
|
+
dispatch(context, coalesceKey) {
|
|
188
|
+
let keys;
|
|
189
|
+
let middleware;
|
|
190
|
+
try {
|
|
191
|
+
keys = this.#keysFor(context);
|
|
192
|
+
middleware = this.#middleware.map(captureRealtimeMiddleware);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
this.#hooks.middlewareError(error, context);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const replaced = coalesceKey === void 0 ? -1 : this.#queue.findIndex((pending) => pending.coalesceKey === coalesceKey);
|
|
198
|
+
if (replaced < 0 && this.#queue.length >= 256) {
|
|
199
|
+
this.#hooks.overflow();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const work = {
|
|
203
|
+
context,
|
|
204
|
+
middleware,
|
|
205
|
+
handlers: [...this.#handlers],
|
|
206
|
+
keys,
|
|
207
|
+
coalesceKey
|
|
208
|
+
};
|
|
209
|
+
if (replaced >= 0) this.#queue[replaced] = work;
|
|
210
|
+
else this.#queue.push(work);
|
|
211
|
+
this.#pump();
|
|
212
|
+
}
|
|
213
|
+
/** Отбрасывает обновления, обработка которых ещё не началась. */
|
|
214
|
+
clearPending() {
|
|
215
|
+
this.#queue.length = 0;
|
|
216
|
+
this.#resolveDrain();
|
|
217
|
+
}
|
|
218
|
+
/** Ждёт завершения активных и поставленных в очередь обновлений. */
|
|
219
|
+
drain() {
|
|
220
|
+
if (this.#active === 0 && this.#queue.length === 0) return Promise.resolve();
|
|
221
|
+
return new Promise((resolve) => this.#drainWaiters.add(resolve));
|
|
222
|
+
}
|
|
223
|
+
#keysFor(context) {
|
|
224
|
+
const value = this.#options.sequentialize?.(context);
|
|
225
|
+
if (value === void 0) return [];
|
|
226
|
+
const values = Array.isArray(value) ? value : [value];
|
|
227
|
+
for (const key of values) if (typeof key !== "string" && typeof key !== "number" && typeof key !== "symbol") throw new TypeError("sequentialize() должен возвращать PropertyKey или их список");
|
|
228
|
+
return [...new Set(values)];
|
|
229
|
+
}
|
|
230
|
+
#pump() {
|
|
231
|
+
while (this.#active < this.#options.concurrency) {
|
|
232
|
+
const blockedKeys = new Set(this.#activeKeys);
|
|
233
|
+
const index = this.#queue.findIndex(({ keys }) => {
|
|
234
|
+
const runnable = keys.every((key) => !blockedKeys.has(key));
|
|
235
|
+
if (!runnable) for (const key of keys) blockedKeys.add(key);
|
|
236
|
+
return runnable;
|
|
237
|
+
});
|
|
238
|
+
if (index < 0) break;
|
|
239
|
+
const [work] = this.#queue.splice(index, 1);
|
|
240
|
+
if (!work) break;
|
|
241
|
+
this.#active += 1;
|
|
242
|
+
for (const key of work.keys) this.#activeKeys.add(key);
|
|
243
|
+
this.#run(work);
|
|
244
|
+
}
|
|
245
|
+
this.#resolveDrain();
|
|
246
|
+
}
|
|
247
|
+
async #run(work) {
|
|
248
|
+
try {
|
|
249
|
+
await runRealtimeMiddleware(work.middleware, work.context, async () => {
|
|
250
|
+
for (const { predicate, handler } of work.handlers) try {
|
|
251
|
+
if (predicate(work.context)) await handler(work.context);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
this.#hooks.handlerError(error, work.context);
|
|
254
|
+
}
|
|
255
|
+
this.#hooks.deliver(work.context);
|
|
256
|
+
});
|
|
257
|
+
} catch (error) {
|
|
258
|
+
this.#hooks.middlewareError(error, work.context);
|
|
259
|
+
} finally {
|
|
260
|
+
this.#active -= 1;
|
|
261
|
+
for (const key of work.keys) this.#activeKeys.delete(key);
|
|
262
|
+
this.#pump();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
#resolveDrain() {
|
|
266
|
+
if (this.#active !== 0 || this.#queue.length !== 0) return;
|
|
267
|
+
for (const resolve of this.#drainWaiters) resolve();
|
|
268
|
+
this.#drainWaiters.clear();
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
//#endregion
|
|
272
|
+
//#region src/realtime/reconnect.ts
|
|
273
|
+
/**
|
|
274
|
+
* Паузы перед попытками переподключения, мс.
|
|
275
|
+
*
|
|
276
|
+
* Значения совпадают с теми, что использует сайт итд.com, — поведение библиотеки
|
|
277
|
+
* не отличается от привычного пользователю.
|
|
278
|
+
*/
|
|
279
|
+
const RECONNECT_BACKOFF = Object.freeze([
|
|
280
|
+
1e3,
|
|
281
|
+
2e3,
|
|
282
|
+
4e3,
|
|
283
|
+
8e3,
|
|
284
|
+
16e3,
|
|
285
|
+
3e4
|
|
286
|
+
]);
|
|
287
|
+
/**
|
|
288
|
+
* Вычисляет паузу перед попыткой переподключения.
|
|
289
|
+
*
|
|
290
|
+
* Разброс нужен, чтобы клиенты, отключившиеся одновременно из-за перезапуска сервера,
|
|
291
|
+
* не вернулись тоже одновременно.
|
|
292
|
+
*
|
|
293
|
+
* @param attempt номер попытки, начиная с нуля
|
|
294
|
+
* @param random источник случайности; подменяется в тестах
|
|
295
|
+
*/
|
|
296
|
+
function reconnectDelay(attempt, options = {}, random = Math.random) {
|
|
297
|
+
const backoff = options.backoff ?? RECONNECT_BACKOFF;
|
|
298
|
+
const jitter = options.jitter ?? .3;
|
|
299
|
+
const base = backoff[Math.min(attempt, backoff.length - 1)] ?? 3e4;
|
|
300
|
+
const spread = base * jitter * (random() * 2 - 1);
|
|
301
|
+
return Math.max(0, Math.round(base + spread));
|
|
302
|
+
}
|
|
303
|
+
//#endregion
|
|
304
|
+
//#region src/realtime/transports/transport.ts
|
|
305
|
+
/** Ошибка, по которой видно, что сервер отверг авторизацию потока. */
|
|
306
|
+
var UnauthorizedStreamError = class extends Error {
|
|
307
|
+
constructor() {
|
|
308
|
+
super("Поток событий отверг токен доступа");
|
|
309
|
+
this.name = "UnauthorizedStreamError";
|
|
310
|
+
}
|
|
311
|
+
};
|
|
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;
|
|
376
|
+
}
|
|
377
|
+
/** Сужает произвольный контекст потока до контекста уведомления. */
|
|
378
|
+
function isNotificationContext(context) {
|
|
379
|
+
return context.update.type === RealtimeUpdateType.Notification;
|
|
380
|
+
}
|
|
381
|
+
//#endregion
|
|
382
|
+
//#region src/realtime/engine.ts
|
|
383
|
+
/** Проверяет настройки общей механики потока. */
|
|
384
|
+
function validateRealtimeEngineOptions(options) {
|
|
385
|
+
const positiveInteger = (value, name) => {
|
|
386
|
+
if (value === void 0) return;
|
|
387
|
+
if (!Number.isInteger(value) || value < 0) throw new require_errors.ItdConfigError(`realtime.${name} должен быть целым неотрицательным числом, получено: ${value}`);
|
|
388
|
+
};
|
|
389
|
+
const duration = (value, name) => {
|
|
390
|
+
if (!Number.isFinite(value) || value < 0) throw new require_errors.ItdConfigError(`realtime.${name} должен быть числом не меньше 0, получено: ${value}`);
|
|
391
|
+
};
|
|
392
|
+
positiveInteger(options.maxAttempts, "maxAttempts");
|
|
393
|
+
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}`);
|
|
396
|
+
if (options.backoff !== void 0) {
|
|
397
|
+
if (!Array.isArray(options.backoff) || options.backoff.length === 0) throw new require_errors.ItdConfigError("realtime.backoff должен быть непустым списком пауз");
|
|
398
|
+
for (const delay of options.backoff) duration(delay, "backoff");
|
|
399
|
+
}
|
|
400
|
+
if (options.sequentialize !== void 0 && typeof options.sequentialize !== "function") throw new require_errors.ItdConfigError("realtime.sequentialize должен быть функцией");
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Общая механика потока событий: соединение, переподключение с backoff, обновление
|
|
404
|
+
* токена, реакция на среду, статусы и очередь обработчиков.
|
|
405
|
+
*
|
|
406
|
+
* Ничего не знает о домене: что считать обновлением, как собрать контекст и кому его
|
|
407
|
+
* доставить, решают {@link RealtimeEngineDeps}. Поэтому один и тот же движок несёт
|
|
408
|
+
* и поток уведомлений, и любой другой.
|
|
409
|
+
*
|
|
410
|
+
* @internal
|
|
411
|
+
*/
|
|
412
|
+
var RealtimeEngine = class {
|
|
413
|
+
#deps;
|
|
414
|
+
#options;
|
|
415
|
+
#dispatcher;
|
|
416
|
+
#emitter;
|
|
417
|
+
#clock;
|
|
418
|
+
#maxAttempts;
|
|
419
|
+
#controller;
|
|
420
|
+
/**
|
|
421
|
+
* Хочет ли вызывающий код, чтобы соединение было живо.
|
|
422
|
+
*
|
|
423
|
+
* Отдельно от `#controller`, потому что тот появляется только после `await` внутри
|
|
424
|
+
* {@link connect}. Без этого флага два вызова подряд проскочили бы проверку оба
|
|
425
|
+
* и подняли два соединения, а `disconnect()` во время синхронизации не был бы замечен
|
|
426
|
+
* и соединение поднялось бы уже после отмены.
|
|
427
|
+
*/
|
|
428
|
+
#wanted = false;
|
|
429
|
+
#status = require_url.RealtimeStatus.Disconnected;
|
|
430
|
+
#attempt = 0;
|
|
431
|
+
#generation = 0;
|
|
432
|
+
#starting;
|
|
433
|
+
#cancelTimer;
|
|
434
|
+
#detachEnvironment;
|
|
435
|
+
constructor(deps, options = {}) {
|
|
436
|
+
validateRealtimeEngineOptions(options);
|
|
437
|
+
this.#deps = deps;
|
|
438
|
+
this.#options = options;
|
|
439
|
+
this.#clock = deps.clock ?? require_url.systemClock;
|
|
440
|
+
this.#maxAttempts = options.maxAttempts ?? 15;
|
|
441
|
+
this.#emitter = new Emitter((error) => reportListenerError(deps.logger, "realtime", error));
|
|
442
|
+
this.#dispatcher = new RealtimeDispatcher({
|
|
443
|
+
concurrency: options.concurrency ?? 1,
|
|
444
|
+
...options.sequentialize ? { sequentialize: options.sequentialize } : {}
|
|
445
|
+
}, {
|
|
446
|
+
deliver: (context) => this.#deps.deliver(context.update),
|
|
447
|
+
middlewareError: (error, context) => this.#reportDispatchError("middlewareError", error, context),
|
|
448
|
+
handlerError: (error, context) => this.#reportDispatchError("handlerError", error, context),
|
|
449
|
+
overflow: () => this.#handleOverflow()
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
/** Текущее состояние соединения. */
|
|
453
|
+
get status() {
|
|
454
|
+
return this.#status;
|
|
455
|
+
}
|
|
456
|
+
/** Имя используемого транспорта. */
|
|
457
|
+
get transport() {
|
|
458
|
+
return this.#deps.transport.name;
|
|
459
|
+
}
|
|
460
|
+
/** Подписывается на событие потока. @returns функция отписки */
|
|
461
|
+
on(event, listener) {
|
|
462
|
+
return this.#emitter.on(event, listener);
|
|
463
|
+
}
|
|
464
|
+
/** Подписывается на одно срабатывание события потока. */
|
|
465
|
+
once(event, listener) {
|
|
466
|
+
return this.#emitter.once(event, listener);
|
|
467
|
+
}
|
|
468
|
+
/** Рассылает доменное событие через общий эмиттер потока. */
|
|
469
|
+
emit(event, payload) {
|
|
470
|
+
this.#emitter.emit(event, payload);
|
|
471
|
+
}
|
|
472
|
+
/** Снимает подписки на события. Обработчики обновлений и middleware остаются. */
|
|
473
|
+
removeAllListeners() {
|
|
474
|
+
this.#emitter.removeAllListeners();
|
|
475
|
+
}
|
|
476
|
+
/** Добавляет промежуточный обработчик. @returns функция его удаления */
|
|
477
|
+
use(middleware) {
|
|
478
|
+
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));
|
|
481
|
+
}
|
|
482
|
+
/** Подписывает обработчик обновлений, подходящих под условие. */
|
|
483
|
+
onUpdate(predicate, handler) {
|
|
484
|
+
return this.#dispatcher.on(predicate, handler);
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Поднимает соединение.
|
|
488
|
+
*
|
|
489
|
+
* Повторный вызов при уже живом соединении ничего не делает. Возвращает управление
|
|
490
|
+
* сразу после запуска: соединение живёт в фоне.
|
|
491
|
+
*/
|
|
492
|
+
async connect() {
|
|
493
|
+
if (this.#wanted) return;
|
|
494
|
+
this.#wanted = true;
|
|
495
|
+
const generation = ++this.#generation;
|
|
496
|
+
const starting = {};
|
|
497
|
+
this.#starting = starting;
|
|
498
|
+
this.#deps.onConnect?.();
|
|
499
|
+
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);
|
|
504
|
+
}
|
|
505
|
+
/** Закрывает соединение и отменяет запланированные попытки. */
|
|
506
|
+
disconnect() {
|
|
507
|
+
this.#wanted = false;
|
|
508
|
+
this.#generation += 1;
|
|
509
|
+
this.#starting = void 0;
|
|
510
|
+
if (this.#cancelTimer) {
|
|
511
|
+
this.#cancelTimer();
|
|
512
|
+
this.#cancelTimer = void 0;
|
|
513
|
+
}
|
|
514
|
+
this.#detachEnvironment?.();
|
|
515
|
+
this.#detachEnvironment = void 0;
|
|
516
|
+
this.#controller?.abort();
|
|
517
|
+
this.#controller = void 0;
|
|
518
|
+
this.#attempt = 0;
|
|
519
|
+
this.#dispatcher.clearPending();
|
|
520
|
+
this.#setStatus(require_url.RealtimeStatus.Disconnected);
|
|
521
|
+
this.#deps.onClose?.();
|
|
522
|
+
}
|
|
523
|
+
/** Ждёт завершения всех принятых обновлений. */
|
|
524
|
+
drain() {
|
|
525
|
+
return this.#dispatcher.drain();
|
|
526
|
+
}
|
|
527
|
+
/** Пропускает актуальное обновление через цепочку обработчиков. */
|
|
528
|
+
#dispatch(update, raw, origin) {
|
|
529
|
+
if (!this.#wanted) return;
|
|
530
|
+
this.#dispatcher.dispatch(this.#deps.createContext(update, raw, origin), this.#deps.coalesceKey?.(update));
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Очередь обновлений достигла предела: закрывает соединение и переподключается,
|
|
534
|
+
* когда обработчики разберут очередь.
|
|
535
|
+
*/
|
|
536
|
+
#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);
|
|
543
|
+
const error = /* @__PURE__ */ new Error("Обработчики не успевают за потоком: очередь обновлений переполнена");
|
|
544
|
+
this.#dispatcher.drain().then(() => {
|
|
545
|
+
if (this.#isCurrentGeneration(generation) && !this.#controller) this.#scheduleReconnect(error);
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
/** Выполняет доменную синхронизацию, не роняя подключение из-за её ошибки. */
|
|
549
|
+
async #sync(reason, generation, starting) {
|
|
550
|
+
if (!this.#deps.sync) return;
|
|
551
|
+
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
|
+
});
|
|
556
|
+
} catch (error) {
|
|
557
|
+
this.#deps.logger?.debug(`не удалось синхронизировать поток (${reason})`, error);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
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,
|
|
575
|
+
onOpen: () => {
|
|
576
|
+
if (!this.#isCurrentConnection(controller, generation)) return;
|
|
577
|
+
this.#attempt = 0;
|
|
578
|
+
this.#setStatus(require_url.RealtimeStatus.Connected);
|
|
579
|
+
},
|
|
580
|
+
onEvent: (event) => {
|
|
581
|
+
if (this.#isCurrentConnection(controller, generation)) this.#handleEvent(event);
|
|
582
|
+
},
|
|
583
|
+
onParseError: (error, raw) => {
|
|
584
|
+
if (this.#isCurrentConnection(controller, generation)) this.#emitEngine("parseError", {
|
|
585
|
+
error,
|
|
586
|
+
raw
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}).then(() => {
|
|
590
|
+
if (this.#isCurrentConnection(controller, generation)) this.#handleFailure(/* @__PURE__ */ new Error("Соединение с потоком событий закрыто"));
|
|
591
|
+
}, (error) => {
|
|
592
|
+
if (this.#isCurrentConnection(controller, generation)) this.#handleFailure(error);
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
#handleEvent(event) {
|
|
596
|
+
if (!this.#wanted) return;
|
|
597
|
+
this.#emitEngine("message", event);
|
|
598
|
+
if (this.#deps.handleFrame?.(event)) return;
|
|
599
|
+
const update = this.#deps.readUpdate(event);
|
|
600
|
+
if (update !== void 0) this.#dispatch(update, event, RealtimeUpdateOrigin.Stream);
|
|
601
|
+
}
|
|
602
|
+
#reportDispatchError(event, error, context) {
|
|
603
|
+
if (this.#emitter.listenerCount(event) > 0) {
|
|
604
|
+
this.#emitEngine(event, {
|
|
605
|
+
error,
|
|
606
|
+
context
|
|
607
|
+
});
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const message = event === "middlewareError" ? "Ошибка в промежуточном обработчике потока" : "Ошибка в обработчике обновления потока";
|
|
611
|
+
if (this.#deps.logger) this.#deps.logger.error(message, error);
|
|
612
|
+
else console.error(`[itd-api] ${message}`, error);
|
|
613
|
+
}
|
|
614
|
+
#handleFailure(error) {
|
|
615
|
+
this.#controller = void 0;
|
|
616
|
+
if (error instanceof UnauthorizedStreamError) {
|
|
617
|
+
this.#refreshAndReconnect(error);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
this.#setStatus(require_url.RealtimeStatus.Error);
|
|
621
|
+
this.#scheduleReconnect(error);
|
|
622
|
+
}
|
|
623
|
+
/** Обновляет токен и переподключается; при неудаче прекращает попытки. */
|
|
624
|
+
async #refreshAndReconnect(error) {
|
|
625
|
+
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;
|
|
632
|
+
if (!refreshed) {
|
|
633
|
+
this.#giveUp(error);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
this.#scheduleReconnect(error);
|
|
637
|
+
}
|
|
638
|
+
#scheduleReconnect(error) {
|
|
639
|
+
if (!this.#wanted) return;
|
|
640
|
+
if (this.#attempt >= this.#maxAttempts) {
|
|
641
|
+
this.#giveUp(error);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const delay = reconnectDelay(this.#attempt, this.#options);
|
|
645
|
+
this.#attempt += 1;
|
|
646
|
+
this.#emitEngine("error", {
|
|
647
|
+
error,
|
|
648
|
+
willReconnect: true
|
|
649
|
+
});
|
|
650
|
+
this.#emitEngine("reconnect", {
|
|
651
|
+
attempt: this.#attempt,
|
|
652
|
+
delay
|
|
653
|
+
});
|
|
654
|
+
this.#cancelTimer = this.#clock.schedule(() => {
|
|
655
|
+
this.#cancelTimer = void 0;
|
|
656
|
+
this.#reconnect();
|
|
657
|
+
}, delay);
|
|
658
|
+
}
|
|
659
|
+
/** Догон через REST идёт до сокета: он же обновляет протухший токен. */
|
|
660
|
+
async #reconnect() {
|
|
661
|
+
if (!this.#wanted || this.#starting) return;
|
|
662
|
+
const generation = this.#generation;
|
|
663
|
+
const starting = {};
|
|
664
|
+
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);
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Завершает автоматические попытки переподключения.
|
|
672
|
+
*
|
|
673
|
+
* Владелец узнаёт об этом так же, как при {@link disconnect}. `onClose` вызывается
|
|
674
|
+
* до событий: обработчик `giveup` может тут же вызвать `connect()`.
|
|
675
|
+
*/
|
|
676
|
+
#giveUp(error) {
|
|
677
|
+
this.#wanted = false;
|
|
678
|
+
this.#generation += 1;
|
|
679
|
+
this.#starting = void 0;
|
|
680
|
+
this.#attempt = 0;
|
|
681
|
+
this.#detachEnvironment?.();
|
|
682
|
+
this.#detachEnvironment = void 0;
|
|
683
|
+
this.#deps.onClose?.();
|
|
684
|
+
this.#emitEngine("error", {
|
|
685
|
+
error,
|
|
686
|
+
willReconnect: false
|
|
687
|
+
});
|
|
688
|
+
this.#emitEngine("giveup", void 0);
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Подписывается на события среды.
|
|
692
|
+
*
|
|
693
|
+
* Возврат вкладки из фона и восстановление сети — самые частые причины «мёртвого»
|
|
694
|
+
* соединения. У сайта итд.com такой обработки нет.
|
|
695
|
+
*/
|
|
696
|
+
#attachEnvironmentListeners() {
|
|
697
|
+
if (this.#detachEnvironment) return;
|
|
698
|
+
const target = globalThis;
|
|
699
|
+
if (typeof target.addEventListener !== "function") return;
|
|
700
|
+
const wake = () => {
|
|
701
|
+
if (this.#controller || this.#cancelTimer || this.#starting) return;
|
|
702
|
+
if (this.#status === require_url.RealtimeStatus.Disconnected) return;
|
|
703
|
+
this.#attempt = 0;
|
|
704
|
+
this.#reconnect();
|
|
705
|
+
};
|
|
706
|
+
const onVisibility = () => {
|
|
707
|
+
if (target.document?.visibilityState === "visible") wake();
|
|
708
|
+
};
|
|
709
|
+
const listeners = [];
|
|
710
|
+
if (this.#options.reconnectOnVisible !== false && target.document) listeners.push(["visibilitychange", onVisibility]);
|
|
711
|
+
if (this.#options.reconnectOnOnline !== false) listeners.push(["online", wake]);
|
|
712
|
+
for (const [type, listener] of listeners) target.addEventListener(type, listener);
|
|
713
|
+
this.#detachEnvironment = () => {
|
|
714
|
+
for (const [type, listener] of listeners) target.removeEventListener?.(type, listener);
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
#setStatus(status) {
|
|
718
|
+
if (this.#status === status) return;
|
|
719
|
+
this.#status = status;
|
|
720
|
+
this.#emitEngine("status", status);
|
|
721
|
+
}
|
|
722
|
+
#isCurrentGeneration(generation) {
|
|
723
|
+
return this.#wanted && this.#generation === generation;
|
|
724
|
+
}
|
|
725
|
+
#isCurrentConnection(controller, generation) {
|
|
726
|
+
return this.#isCurrentGeneration(generation) && this.#controller === controller && !controller.signal.aborted;
|
|
727
|
+
}
|
|
728
|
+
/** Рассылает событие из общей части карты, не теряя доменные события типа `E`. */
|
|
729
|
+
#emitEngine(event, payload) {
|
|
730
|
+
this.#emitter.emit(event, payload);
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
//#endregion
|
|
734
|
+
//#region src/realtime/transports/poll.ts
|
|
735
|
+
/**
|
|
736
|
+
* Запасной транспорт: обычный опрос REST вместо потока.
|
|
737
|
+
*
|
|
738
|
+
* Нужен там, где `fetch` не умеет отдавать тело по частям, — например в части сборок
|
|
739
|
+
* React Native. Наружу выдаёт те же события, что и {@link SseTransport}, поэтому
|
|
740
|
+
* вызывающий код разницы не замечает.
|
|
741
|
+
*
|
|
742
|
+
* Новыми считаются уведомления, которых не было в предыдущем ответе; чтобы список
|
|
743
|
+
* известных не рос бесконечно, он ограничен последними двумя страницами.
|
|
744
|
+
*/
|
|
745
|
+
var PollTransport = class {
|
|
746
|
+
name = "poll";
|
|
747
|
+
#interval;
|
|
748
|
+
#limit;
|
|
749
|
+
#clock;
|
|
750
|
+
constructor(options = {}) {
|
|
751
|
+
this.#clock = options.clock ?? require_url.systemClock;
|
|
752
|
+
this.#interval = options.interval ?? 15e3;
|
|
753
|
+
this.#limit = options.limit ?? 20;
|
|
754
|
+
}
|
|
755
|
+
async connect(context) {
|
|
756
|
+
const request = context.request;
|
|
757
|
+
if (!request) throw new require_errors.ItdConfigError("опрос уведомлений выполняется через конвейер клиента; создайте поток вызовом itd.realtime()");
|
|
758
|
+
const seen = /* @__PURE__ */ new Set();
|
|
759
|
+
let firstRun = true;
|
|
760
|
+
let lastUnreadCount;
|
|
761
|
+
while (!context.signal.aborted) {
|
|
762
|
+
const payload = await this.#readUpdates(request, context.signal);
|
|
763
|
+
context.onOpen();
|
|
764
|
+
const items = require_url.pickArray(payload, "notifications");
|
|
765
|
+
for (const item of [...items].reverse()) {
|
|
766
|
+
const id = typeof item.id === "string" ? item.id : void 0;
|
|
767
|
+
if (!id || seen.has(id)) continue;
|
|
768
|
+
seen.add(id);
|
|
769
|
+
if (!firstRun) context.onEvent({
|
|
770
|
+
name: "notification",
|
|
771
|
+
data: { payload: item }
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
if (seen.size > this.#limit * 2) {
|
|
775
|
+
const excess = [...seen].slice(0, seen.size - this.#limit * 2);
|
|
776
|
+
for (const id of excess) seen.delete(id);
|
|
777
|
+
}
|
|
778
|
+
const count = await this.#readCount(request, context.signal);
|
|
779
|
+
if (count !== void 0 && count !== lastUnreadCount) {
|
|
780
|
+
lastUnreadCount = count;
|
|
781
|
+
context.onEvent({
|
|
782
|
+
name: "unread_count",
|
|
783
|
+
data: { payload: { count } }
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
firstRun = false;
|
|
787
|
+
await this.#wait(context.signal);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
async #readUpdates(request, signal) {
|
|
791
|
+
try {
|
|
792
|
+
return await request({
|
|
793
|
+
operationId: "realtime.poll.updates",
|
|
794
|
+
path: "/api/notifications/",
|
|
795
|
+
query: {
|
|
796
|
+
limit: this.#limit,
|
|
797
|
+
offset: 0
|
|
798
|
+
},
|
|
799
|
+
signal
|
|
800
|
+
});
|
|
801
|
+
} catch (error) {
|
|
802
|
+
if (require_errors.isItdAuthError(error)) throw new UnauthorizedStreamError();
|
|
803
|
+
throw error;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
async #readCount(request, signal) {
|
|
807
|
+
try {
|
|
808
|
+
const payload = await request({
|
|
809
|
+
operationId: "realtime.poll.unread",
|
|
810
|
+
path: "/api/notifications/count",
|
|
811
|
+
signal
|
|
812
|
+
});
|
|
813
|
+
return require_url.pickNumber(payload, "count", 0);
|
|
814
|
+
} catch {
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
/** Ждёт следующего опроса, прерываясь при отмене. */
|
|
819
|
+
#wait(signal) {
|
|
820
|
+
if (signal.aborted) return Promise.resolve();
|
|
821
|
+
return new Promise((resolve) => {
|
|
822
|
+
const cancel = this.#clock.schedule(finish, this.#interval);
|
|
823
|
+
function finish() {
|
|
824
|
+
cancel();
|
|
825
|
+
signal.removeEventListener("abort", finish);
|
|
826
|
+
resolve();
|
|
827
|
+
}
|
|
828
|
+
signal.addEventListener("abort", finish, { once: true });
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
//#endregion
|
|
833
|
+
//#region node_modules/eventsource-parser/dist/index.js
|
|
834
|
+
var ParseError = class extends Error {
|
|
835
|
+
constructor(message, options) {
|
|
836
|
+
super(message), this.name = "ParseError", this.type = options.type, this.field = options.field, this.value = options.value, this.line = options.line;
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
const LF = 10;
|
|
840
|
+
const CR = 13;
|
|
841
|
+
const SPACE = 32;
|
|
842
|
+
function noop(_arg) {}
|
|
843
|
+
function createParser(config) {
|
|
844
|
+
if (typeof config == "function") throw new TypeError("`config` must be an object, got a function instead. Did you mean `createParser({onEvent: fn})`?");
|
|
845
|
+
const { onEvent = noop, onError = noop, onRetry = noop, onComment, maxBufferSize } = config, pendingFragments = [];
|
|
846
|
+
let pendingFragmentsLength = 0, isFirstChunk = !0, id, data = "", dataLines = 0, eventType, terminated = !1;
|
|
847
|
+
function feed(chunk) {
|
|
848
|
+
if (terminated) throw new Error("Cannot feed parser: it was terminated after exceeding the configured max buffer size. Call `reset()` to resume parsing.");
|
|
849
|
+
if (isFirstChunk && (isFirstChunk = !1, chunk.charCodeAt(0) === 239 && chunk.charCodeAt(1) === 187 && chunk.charCodeAt(2) === 191 && (chunk = chunk.slice(3))), pendingFragments.length === 0) {
|
|
850
|
+
const trailing2 = processLines(chunk);
|
|
851
|
+
trailing2 !== "" && (pendingFragments.push(trailing2), pendingFragmentsLength = trailing2.length), checkBufferSize();
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
if (chunk.indexOf(`
|
|
855
|
+
`) === -1 && chunk.indexOf("\r") === -1) {
|
|
856
|
+
pendingFragments.push(chunk), pendingFragmentsLength += chunk.length, checkBufferSize();
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
pendingFragments.push(chunk);
|
|
860
|
+
const input = pendingFragments.join("");
|
|
861
|
+
pendingFragments.length = 0, pendingFragmentsLength = 0;
|
|
862
|
+
const trailing = processLines(input);
|
|
863
|
+
trailing !== "" && (pendingFragments.push(trailing), pendingFragmentsLength = trailing.length), checkBufferSize();
|
|
864
|
+
}
|
|
865
|
+
function checkBufferSize() {
|
|
866
|
+
maxBufferSize !== void 0 && (pendingFragmentsLength + data.length <= maxBufferSize || (terminated = !0, pendingFragments.length = 0, pendingFragmentsLength = 0, id = void 0, data = "", dataLines = 0, eventType = void 0, onError(new ParseError(`Buffered data exceeded max buffer size of ${maxBufferSize} characters`, { type: "max-buffer-size-exceeded" }))));
|
|
867
|
+
}
|
|
868
|
+
function processLines(chunk) {
|
|
869
|
+
let searchIndex = 0;
|
|
870
|
+
if (chunk.indexOf("\r") === -1) {
|
|
871
|
+
let lfIndex = chunk.indexOf(`
|
|
872
|
+
`, searchIndex);
|
|
873
|
+
for (; lfIndex !== -1;) {
|
|
874
|
+
if (searchIndex === lfIndex) {
|
|
875
|
+
dataLines > 0 && onEvent({
|
|
876
|
+
id,
|
|
877
|
+
event: eventType,
|
|
878
|
+
data
|
|
879
|
+
}), id = void 0, data = "", dataLines = 0, eventType = void 0, searchIndex = lfIndex + 1, lfIndex = chunk.indexOf(`
|
|
880
|
+
`, searchIndex);
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
const firstCharCode = chunk.charCodeAt(searchIndex);
|
|
884
|
+
if (isDataPrefix(chunk, searchIndex, firstCharCode)) {
|
|
885
|
+
const valueStart = chunk.charCodeAt(searchIndex + 5) === SPACE ? searchIndex + 6 : searchIndex + 5, value = chunk.slice(valueStart, lfIndex);
|
|
886
|
+
if (dataLines === 0 && chunk.charCodeAt(lfIndex + 1) === LF) {
|
|
887
|
+
onEvent({
|
|
888
|
+
id,
|
|
889
|
+
event: eventType,
|
|
890
|
+
data: value
|
|
891
|
+
}), id = void 0, data = "", eventType = void 0, searchIndex = lfIndex + 2, lfIndex = chunk.indexOf(`
|
|
892
|
+
`, searchIndex);
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
data = dataLines === 0 ? value : `${data}
|
|
896
|
+
${value}`, dataLines++;
|
|
897
|
+
} else isEventPrefix(chunk, searchIndex, firstCharCode) ? eventType = chunk.slice(chunk.charCodeAt(searchIndex + 6) === SPACE ? searchIndex + 7 : searchIndex + 6, lfIndex) || void 0 : parseLine(chunk, searchIndex, lfIndex);
|
|
898
|
+
searchIndex = lfIndex + 1, lfIndex = chunk.indexOf(`
|
|
899
|
+
`, searchIndex);
|
|
900
|
+
}
|
|
901
|
+
return chunk.slice(searchIndex);
|
|
902
|
+
}
|
|
903
|
+
for (; searchIndex < chunk.length;) {
|
|
904
|
+
const crIndex = chunk.indexOf("\r", searchIndex), lfIndex = chunk.indexOf(`
|
|
905
|
+
`, searchIndex);
|
|
906
|
+
let lineEnd = -1;
|
|
907
|
+
if (crIndex !== -1 && lfIndex !== -1 ? lineEnd = crIndex < lfIndex ? crIndex : lfIndex : crIndex !== -1 ? crIndex === chunk.length - 1 ? lineEnd = -1 : lineEnd = crIndex : lfIndex !== -1 && (lineEnd = lfIndex), lineEnd === -1) break;
|
|
908
|
+
parseLine(chunk, searchIndex, lineEnd), searchIndex = lineEnd + 1, chunk.charCodeAt(searchIndex - 1) === CR && chunk.charCodeAt(searchIndex) === LF && searchIndex++;
|
|
909
|
+
}
|
|
910
|
+
return chunk.slice(searchIndex);
|
|
911
|
+
}
|
|
912
|
+
function parseLine(chunk, start, end) {
|
|
913
|
+
if (start === end) {
|
|
914
|
+
dispatchEvent();
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
const firstCharCode = chunk.charCodeAt(start);
|
|
918
|
+
if (isDataPrefix(chunk, start, firstCharCode)) {
|
|
919
|
+
const valueStart = chunk.charCodeAt(start + 5) === SPACE ? start + 6 : start + 5, value2 = chunk.slice(valueStart, end);
|
|
920
|
+
data = dataLines === 0 ? value2 : `${data}
|
|
921
|
+
${value2}`, dataLines++;
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
if (isEventPrefix(chunk, start, firstCharCode)) {
|
|
925
|
+
eventType = chunk.slice(chunk.charCodeAt(start + 6) === SPACE ? start + 7 : start + 6, end) || void 0;
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
if (firstCharCode === 105 && chunk.charCodeAt(start + 1) === 100 && chunk.charCodeAt(start + 2) === 58) {
|
|
929
|
+
const value2 = chunk.slice(chunk.charCodeAt(start + 3) === SPACE ? start + 4 : start + 3, end);
|
|
930
|
+
id = value2.includes("\0") ? void 0 : value2;
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
if (firstCharCode === 58) {
|
|
934
|
+
if (onComment) {
|
|
935
|
+
const line2 = chunk.slice(start, end);
|
|
936
|
+
onComment(line2.slice(chunk.charCodeAt(start + 1) === SPACE ? 2 : 1));
|
|
937
|
+
}
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
const line = chunk.slice(start, end), fieldSeparatorIndex = line.indexOf(":");
|
|
941
|
+
if (fieldSeparatorIndex === -1) {
|
|
942
|
+
processField(line, "", line);
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
const field = line.slice(0, fieldSeparatorIndex), offset = line.charCodeAt(fieldSeparatorIndex + 1) === SPACE ? 2 : 1;
|
|
946
|
+
processField(field, line.slice(fieldSeparatorIndex + offset), line);
|
|
947
|
+
}
|
|
948
|
+
function processField(field, value, line) {
|
|
949
|
+
switch (field) {
|
|
950
|
+
case "event":
|
|
951
|
+
eventType = value || void 0;
|
|
952
|
+
break;
|
|
953
|
+
case "data":
|
|
954
|
+
data = dataLines === 0 ? value : `${data}
|
|
955
|
+
${value}`, dataLines++;
|
|
956
|
+
break;
|
|
957
|
+
case "id":
|
|
958
|
+
id = value.includes("\0") ? void 0 : value;
|
|
959
|
+
break;
|
|
960
|
+
case "retry":
|
|
961
|
+
/^\d+$/.test(value) ? onRetry(parseInt(value, 10)) : onError(new ParseError(`Invalid \`retry\` value: "${value}"`, {
|
|
962
|
+
type: "invalid-retry",
|
|
963
|
+
value,
|
|
964
|
+
line
|
|
965
|
+
}));
|
|
966
|
+
break;
|
|
967
|
+
default: onError(new ParseError(`Unknown field "${field.length > 20 ? `${field.slice(0, 20)}\u2026` : field}"`, {
|
|
968
|
+
type: "unknown-field",
|
|
969
|
+
field,
|
|
970
|
+
value,
|
|
971
|
+
line
|
|
972
|
+
}));
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
function dispatchEvent() {
|
|
976
|
+
dataLines > 0 && onEvent({
|
|
977
|
+
id,
|
|
978
|
+
event: eventType,
|
|
979
|
+
data
|
|
980
|
+
}), id = void 0, data = "", dataLines = 0, eventType = void 0;
|
|
981
|
+
}
|
|
982
|
+
function reset(options = {}) {
|
|
983
|
+
if (options.consume && pendingFragments.length > 0) {
|
|
984
|
+
const incompleteLine = pendingFragments.join("");
|
|
985
|
+
parseLine(incompleteLine, 0, incompleteLine.length);
|
|
986
|
+
}
|
|
987
|
+
isFirstChunk = !0, id = void 0, data = "", dataLines = 0, eventType = void 0, pendingFragments.length = 0, pendingFragmentsLength = 0, terminated = !1;
|
|
988
|
+
}
|
|
989
|
+
return {
|
|
990
|
+
feed,
|
|
991
|
+
reset
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
function isDataPrefix(chunk, i, firstCharCode) {
|
|
995
|
+
return firstCharCode === 100 && chunk.charCodeAt(i + 1) === 97 && chunk.charCodeAt(i + 2) === 116 && chunk.charCodeAt(i + 3) === 97 && chunk.charCodeAt(i + 4) === 58;
|
|
996
|
+
}
|
|
997
|
+
function isEventPrefix(chunk, i, firstCharCode) {
|
|
998
|
+
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
|
+
}
|
|
1000
|
+
//#endregion
|
|
1001
|
+
//#region src/realtime/transports/sse.ts
|
|
1002
|
+
/** Путь потока уведомлений. */
|
|
1003
|
+
const STREAM_PATH = "/api/notifications/stream";
|
|
1004
|
+
/**
|
|
1005
|
+
* Транспорт поверх Server-Sent Events.
|
|
1006
|
+
*
|
|
1007
|
+
* Нативный `EventSource` не подходит: он не умеет отправлять заголовок `Authorization`,
|
|
1008
|
+
* а поток требует Bearer-токен. Поэтому используется `fetch` с чтением тела по частям.
|
|
1009
|
+
*
|
|
1010
|
+
* Разбор кадров делегирован `eventsource-parser`, который корректно обрабатывает
|
|
1011
|
+
* многострочный `data:`, перевод строки `\r\n`, `data:` без пробела и кадр, разорванный
|
|
1012
|
+
* между сетевыми чанками. В разборе на сайте итд.com каждый из этих случаев обрабатывается
|
|
1013
|
+
* неверно.
|
|
1014
|
+
*/
|
|
1015
|
+
var SseTransport = class {
|
|
1016
|
+
name = "sse";
|
|
1017
|
+
#idleTimeout;
|
|
1018
|
+
#handshakeTimeout;
|
|
1019
|
+
#clock;
|
|
1020
|
+
/** Идентификатор последнего события — отправляется при переподключении. */
|
|
1021
|
+
#lastEventId;
|
|
1022
|
+
constructor(options = {}) {
|
|
1023
|
+
this.#clock = options.clock ?? require_url.systemClock;
|
|
1024
|
+
this.#idleTimeout = options.idleTimeout ?? 9e4;
|
|
1025
|
+
this.#handshakeTimeout = options.handshakeTimeout ?? 2e4;
|
|
1026
|
+
}
|
|
1027
|
+
async connect(context) {
|
|
1028
|
+
const token = context.authorize ? await context.getToken() : null;
|
|
1029
|
+
if (context.authorize && !token) throw new UnauthorizedStreamError();
|
|
1030
|
+
const url = require_cookies.joinUrl(context.baseUrl, STREAM_PATH);
|
|
1031
|
+
const headers = await context.baseHeaders(url);
|
|
1032
|
+
headers.set("Accept", "text/event-stream");
|
|
1033
|
+
if (token) headers.set("Authorization", `Bearer ${token}`);
|
|
1034
|
+
headers.set("Cache-Control", "no-cache");
|
|
1035
|
+
if (this.#lastEventId) headers.set("Last-Event-ID", this.#lastEventId);
|
|
1036
|
+
const controller = new AbortController();
|
|
1037
|
+
const relayAbort = () => controller.abort(context.signal.reason);
|
|
1038
|
+
if (context.signal.aborted) controller.abort(context.signal.reason);
|
|
1039
|
+
else context.signal.addEventListener("abort", relayAbort, { once: true });
|
|
1040
|
+
try {
|
|
1041
|
+
const response = await this.#handshake(url, headers, context, controller);
|
|
1042
|
+
if (context.authorize && response.status === 401) throw new UnauthorizedStreamError();
|
|
1043
|
+
if (!response.ok) throw new Error(`Поток уведомлений вернул статус ${response.status}`);
|
|
1044
|
+
if (!response.body) throw new Error("Ответ потока уведомлений пуст");
|
|
1045
|
+
context.onOpen();
|
|
1046
|
+
await this.#read(response.body, context);
|
|
1047
|
+
} finally {
|
|
1048
|
+
context.signal.removeEventListener("abort", relayAbort);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
/** Выполняет запрос потока, обрывая его, если ответ не пришёл за отведённое время. */
|
|
1052
|
+
async #handshake(url, headers, context, controller) {
|
|
1053
|
+
let expired = false;
|
|
1054
|
+
const cancelTimer = this.#handshakeTimeout > 0 ? this.#clock.schedule(() => {
|
|
1055
|
+
expired = true;
|
|
1056
|
+
controller.abort(/* @__PURE__ */ new Error("Истёк таймаут рукопожатия SSE"));
|
|
1057
|
+
}, this.#handshakeTimeout) : void 0;
|
|
1058
|
+
try {
|
|
1059
|
+
return await context.fetch(url, {
|
|
1060
|
+
method: "GET",
|
|
1061
|
+
headers,
|
|
1062
|
+
signal: controller.signal
|
|
1063
|
+
});
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
if (expired) throw new Error("Поток уведомлений не ответил: истёк таймаут рукопожатия");
|
|
1066
|
+
throw error;
|
|
1067
|
+
} finally {
|
|
1068
|
+
cancelTimer?.();
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
async #read(body, context) {
|
|
1072
|
+
const reader = body.getReader();
|
|
1073
|
+
const decoder = new TextDecoder();
|
|
1074
|
+
const parser = createParser({ onEvent: (message) => {
|
|
1075
|
+
if (message.id) this.#lastEventId = message.id;
|
|
1076
|
+
let data;
|
|
1077
|
+
try {
|
|
1078
|
+
data = JSON.parse(message.data);
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
context.onParseError(error, message.data);
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const name = message.event ?? (typeof data === "object" && data !== null && "type" in data ? String(data.type) : "message");
|
|
1084
|
+
context.onEvent({
|
|
1085
|
+
name,
|
|
1086
|
+
data
|
|
1087
|
+
});
|
|
1088
|
+
} });
|
|
1089
|
+
let cancelIdleTimer;
|
|
1090
|
+
const armIdleTimer = () => {
|
|
1091
|
+
if (this.#idleTimeout <= 0) return;
|
|
1092
|
+
cancelIdleTimer?.();
|
|
1093
|
+
cancelIdleTimer = this.#clock.schedule(() => {
|
|
1094
|
+
reader.cancel(/* @__PURE__ */ new Error("Поток молчит дольше допустимого")).catch(() => {});
|
|
1095
|
+
}, this.#idleTimeout);
|
|
1096
|
+
};
|
|
1097
|
+
armIdleTimer();
|
|
1098
|
+
try {
|
|
1099
|
+
for (;;) {
|
|
1100
|
+
const { done, value } = await reader.read();
|
|
1101
|
+
if (done) break;
|
|
1102
|
+
armIdleTimer();
|
|
1103
|
+
parser.feed(decoder.decode(value, { stream: true }));
|
|
1104
|
+
}
|
|
1105
|
+
} finally {
|
|
1106
|
+
cancelIdleTimer?.();
|
|
1107
|
+
reader.releaseLock?.();
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
//#endregion
|
|
1112
|
+
//#region src/realtime/stream.ts
|
|
1113
|
+
/** Способ получения событий. */
|
|
1114
|
+
const RealtimeTransportKind = Object.freeze({
|
|
1115
|
+
/** Поток событий, если среда умеет читать тело по частям, иначе опрос. */
|
|
1116
|
+
Auto: "auto",
|
|
1117
|
+
/** Поток `text/event-stream`. */
|
|
1118
|
+
Sse: "sse",
|
|
1119
|
+
/** Периодический опрос REST. */
|
|
1120
|
+
Poll: "poll"
|
|
1121
|
+
});
|
|
1122
|
+
/** Проверяет настройки потока. @throws {ItdConfigError} при некорректных значениях */
|
|
1123
|
+
function validateRealtimeOptions(options) {
|
|
1124
|
+
const duration = (value, name, min) => {
|
|
1125
|
+
if (value === void 0) return;
|
|
1126
|
+
if (!Number.isFinite(value) || value < min) throw new require_errors.ItdConfigError(`realtime.${name} должен быть числом не меньше ${min}, получено: ${value}`);
|
|
1127
|
+
};
|
|
1128
|
+
duration(options.pollInterval, "pollInterval", 1);
|
|
1129
|
+
duration(options.idleTimeout, "idleTimeout", 0);
|
|
1130
|
+
duration(options.handshakeTimeout, "handshakeTimeout", 0);
|
|
1131
|
+
}
|
|
1132
|
+
const REALTIME_CONNECT_GUARDS = /* @__PURE__ */ new WeakMap();
|
|
1133
|
+
/** Связывает поток с lifecycle создавшего его клиента. @internal */
|
|
1134
|
+
function setRealtimeConnectGuard(stream, guard) {
|
|
1135
|
+
REALTIME_CONNECT_GUARDS.set(stream, guard);
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Поток уведомлений в реальном времени.
|
|
1139
|
+
*
|
|
1140
|
+
* Получается вызовом `itd.realtime()`. Соединение поднимается методом {@link connect}
|
|
1141
|
+
* и держится само: обрывы, обновление токена и повторные попытки библиотека берёт на себя.
|
|
1142
|
+
*
|
|
1143
|
+
* Параметр типа задаёт форму контекста: плагин может расширить её своими полями
|
|
1144
|
+
* (`ItdRealtime<RealtimeContext & SessionFlavor<S>>`) и типизировать обработчики.
|
|
1145
|
+
*
|
|
1146
|
+
* @example
|
|
1147
|
+
* ```ts
|
|
1148
|
+
* import { NotificationType } from 'itd-api';
|
|
1149
|
+
*
|
|
1150
|
+
* const stream = itd.realtime();
|
|
1151
|
+
*
|
|
1152
|
+
* stream.onNotification(NotificationType.PostComment, async ({ update }) => {
|
|
1153
|
+
* await saveCommentNotification(update.data.notification);
|
|
1154
|
+
* });
|
|
1155
|
+
* stream.on('status', (status) => console.log('соединение:', status));
|
|
1156
|
+
*
|
|
1157
|
+
* await stream.connect();
|
|
1158
|
+
* // …позже
|
|
1159
|
+
* stream.disconnect();
|
|
1160
|
+
* await stream.drain();
|
|
1161
|
+
* ```
|
|
1162
|
+
*/
|
|
1163
|
+
var ItdRealtime = class {
|
|
1164
|
+
#deps;
|
|
1165
|
+
#engine;
|
|
1166
|
+
constructor(deps, options = {}) {
|
|
1167
|
+
validateRealtimeOptions(options);
|
|
1168
|
+
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,
|
|
1174
|
+
clock: deps.clock,
|
|
1175
|
+
baseHeaders: deps.baseHeaders,
|
|
1176
|
+
getToken: deps.getToken,
|
|
1177
|
+
refresh: deps.refresh,
|
|
1178
|
+
onConnect: deps.onConnect,
|
|
1179
|
+
onClose: deps.onClose,
|
|
1180
|
+
logger: deps.logger,
|
|
1181
|
+
transport: createTransport(deps, options),
|
|
1182
|
+
handleFrame: (event) => this.#handleFrame(event),
|
|
1183
|
+
readUpdate: readRealtimeUpdate,
|
|
1184
|
+
coalesceKey: (update) => update.type === RealtimeUpdateType.UnreadCount ? update.type : void 0,
|
|
1185
|
+
createContext: (update, raw, origin) => ({
|
|
1186
|
+
update,
|
|
1187
|
+
stream: this,
|
|
1188
|
+
raw,
|
|
1189
|
+
origin
|
|
1190
|
+
}),
|
|
1191
|
+
deliver: (update) => this.#deliver(update),
|
|
1192
|
+
...options.syncCount === false ? {} : { sync: (reason, dispatch) => this.#syncUnreadCount(reason, dispatch) }
|
|
1193
|
+
}, options);
|
|
1194
|
+
}
|
|
1195
|
+
/** Текущее состояние соединения. */
|
|
1196
|
+
get status() {
|
|
1197
|
+
return this.#engine.status;
|
|
1198
|
+
}
|
|
1199
|
+
/** Имя используемого транспорта. */
|
|
1200
|
+
get transport() {
|
|
1201
|
+
return this.#engine.transport;
|
|
1202
|
+
}
|
|
1203
|
+
/** Базовый URL клиента, создавшего поток. @internal */
|
|
1204
|
+
get baseUrl() {
|
|
1205
|
+
return this.#deps.baseUrl;
|
|
1206
|
+
}
|
|
1207
|
+
/** Идентификаторы аккаунта и сессии клиента, создавшего поток. @internal */
|
|
1208
|
+
getAuthIdentity() {
|
|
1209
|
+
return this.#deps.getAuthIdentity?.();
|
|
1210
|
+
}
|
|
1211
|
+
/** Непрозрачная область авторизации создавшего поток клиента. @internal */
|
|
1212
|
+
getAuthScope() {
|
|
1213
|
+
return this.#deps.getAuthScope?.();
|
|
1214
|
+
}
|
|
1215
|
+
/** Подписывается на событие потока. @returns функция отписки */
|
|
1216
|
+
on(event, listener) {
|
|
1217
|
+
return this.#engine.on(event, listener);
|
|
1218
|
+
}
|
|
1219
|
+
/** Подписывается на одно срабатывание. */
|
|
1220
|
+
once(event, listener) {
|
|
1221
|
+
return this.#engine.once(event, listener);
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Добавляет промежуточный обработчик или объект, предоставляющий его через `middleware()`.
|
|
1225
|
+
*
|
|
1226
|
+
* Обработчики выполняются в порядке регистрации. Если `next()` не вызван, обновление не
|
|
1227
|
+
* передаётся дальше по цепочке, асинхронным обработчикам и слушателям событий.
|
|
1228
|
+
*
|
|
1229
|
+
* @returns функция удаления обработчика
|
|
1230
|
+
*/
|
|
1231
|
+
use(middleware) {
|
|
1232
|
+
return this.#engine.use(middleware);
|
|
1233
|
+
}
|
|
1234
|
+
onUpdate(selectorOrHandler, selectedHandler) {
|
|
1235
|
+
const selectAll = selectedHandler === void 0;
|
|
1236
|
+
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)}`);
|
|
1239
|
+
let predicate;
|
|
1240
|
+
if (selectAll) predicate = () => true;
|
|
1241
|
+
else {
|
|
1242
|
+
const selector = selectorOrHandler;
|
|
1243
|
+
predicate = typeof selector === "function" ? selector : (context) => context.update.type === selector;
|
|
1244
|
+
}
|
|
1245
|
+
return this.#engine.onUpdate(predicate, handler);
|
|
1246
|
+
}
|
|
1247
|
+
onNotification(selector, handler) {
|
|
1248
|
+
if (typeof handler !== "function") throw new require_errors.ItdConfigError("realtime.onNotification() принимает функцию обработчика");
|
|
1249
|
+
if (typeof selector !== "function") validateNotificationSelector(selector);
|
|
1250
|
+
const predicate = (context) => {
|
|
1251
|
+
if (!isNotificationContext(context)) return false;
|
|
1252
|
+
return typeof selector === "function" ? selector(context) : matchesNotification(context, selector);
|
|
1253
|
+
};
|
|
1254
|
+
return this.#engine.onUpdate(predicate, handler);
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Поднимает соединение.
|
|
1258
|
+
*
|
|
1259
|
+
* Повторный вызов при уже живом соединении ничего не делает — это защита от двойного
|
|
1260
|
+
* подключения при перерисовке интерфейса.
|
|
1261
|
+
*
|
|
1262
|
+
* Возвращает управление сразу после запуска: соединение живёт в фоне.
|
|
1263
|
+
*
|
|
1264
|
+
* @throws если создавший поток клиент уже освобождён
|
|
1265
|
+
*/
|
|
1266
|
+
async connect() {
|
|
1267
|
+
REALTIME_CONNECT_GUARDS.get(this)?.();
|
|
1268
|
+
return this.#engine.connect();
|
|
1269
|
+
}
|
|
1270
|
+
/** Закрывает соединение и отменяет запланированные попытки. */
|
|
1271
|
+
disconnect() {
|
|
1272
|
+
this.#engine.disconnect();
|
|
1273
|
+
}
|
|
1274
|
+
/** Ждёт завершения всех принятых обновлений. */
|
|
1275
|
+
drain() {
|
|
1276
|
+
return this.#engine.drain();
|
|
1277
|
+
}
|
|
1278
|
+
/** Снимает подписки `on()` и `once()`. Остальные обработчики остаются. */
|
|
1279
|
+
removeAllListeners() {
|
|
1280
|
+
this.#engine.removeAllListeners();
|
|
1281
|
+
}
|
|
1282
|
+
/** Кадр `connected` — не обновление, а подтверждение подключения. */
|
|
1283
|
+
#handleFrame(event) {
|
|
1284
|
+
if (event.name !== "connected") return false;
|
|
1285
|
+
this.#engine.emit("ready", { userId: require_url.pickString(event.data, "userId") });
|
|
1286
|
+
return true;
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Загружает начальное число непрочитанных.
|
|
1290
|
+
*
|
|
1291
|
+
* Только при первом подключении: поток присылает счётчик сам, а лишний запрос
|
|
1292
|
+
* на каждой попытке переподключения нагружал бы сервер во время недоступности сети.
|
|
1293
|
+
*/
|
|
1294
|
+
async #syncUnreadCount(reason, dispatch) {
|
|
1295
|
+
if (reason !== "initial") return;
|
|
1296
|
+
const count = await this.#deps.fetchUnreadCount();
|
|
1297
|
+
dispatch({
|
|
1298
|
+
type: RealtimeUpdateType.UnreadCount,
|
|
1299
|
+
data: count
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
#deliver(update) {
|
|
1303
|
+
if (update.type === RealtimeUpdateType.Notification) {
|
|
1304
|
+
this.#engine.emit("notification", update.data);
|
|
1305
|
+
if (update.data.unreadCount !== void 0) this.#engine.emit("unreadCount", update.data.unreadCount);
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
if (update.type === RealtimeUpdateType.UnreadCount) {
|
|
1309
|
+
this.#engine.emit("unreadCount", update.data);
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
if (update.type === RealtimeUpdateType.Unknown) return;
|
|
1313
|
+
assertNeverUpdate(update);
|
|
1314
|
+
}
|
|
1315
|
+
};
|
|
1316
|
+
/** Выбирает транспорт по настройкам и возможностям среды. */
|
|
1317
|
+
function createTransport(deps, options) {
|
|
1318
|
+
const kind = options.transport ?? RealtimeTransportKind.Auto;
|
|
1319
|
+
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
|
+
});
|
|
1324
|
+
return new SseTransport({
|
|
1325
|
+
...deps.clock ? { clock: deps.clock } : {},
|
|
1326
|
+
...options.idleTimeout !== void 0 ? { idleTimeout: options.idleTimeout } : {},
|
|
1327
|
+
...options.handshakeTimeout !== void 0 ? { handshakeTimeout: options.handshakeTimeout } : {}
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
function assertNeverUpdate(update) {
|
|
1331
|
+
throw new TypeError(`Необработанное обновление потока: ${String(update)}`);
|
|
1332
|
+
}
|
|
1333
|
+
//#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;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
function safeDetail(error, url, token) {
|
|
1585
|
+
const message = errorMessage(error);
|
|
1586
|
+
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;
|
|
1589
|
+
}
|
|
1590
|
+
function readCloseEvent(event) {
|
|
1591
|
+
const close = typeof event === "object" && event !== null ? event : void 0;
|
|
1592
|
+
return {
|
|
1593
|
+
code: typeof close?.code === "number" ? close.code : 1006,
|
|
1594
|
+
reason: typeof close?.reason === "string" ? close.reason : "",
|
|
1595
|
+
wasClean: close?.wasClean === true
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
function readMessage(event) {
|
|
1599
|
+
const data = typeof event === "object" && event !== null && "data" in event ? event.data : void 0;
|
|
1600
|
+
if (typeof data === "string") return data;
|
|
1601
|
+
if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
|
|
1602
|
+
if (ArrayBuffer.isView(data)) return new TextDecoder().decode(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
|
1603
|
+
if (typeof Blob !== "undefined" && data instanceof Blob) return data.text();
|
|
1604
|
+
}
|
|
1605
|
+
function eventName(data) {
|
|
1606
|
+
return typeof data === "object" && data !== null && "type" in data ? String(data.type) : "message";
|
|
1607
|
+
}
|
|
1608
|
+
/** Транспорт исходных realtime-событий поверх стандартного WebSocket. */
|
|
1609
|
+
var WebSocketTransport = class {
|
|
1610
|
+
name = "ws";
|
|
1611
|
+
#path;
|
|
1612
|
+
#webSocketImpl;
|
|
1613
|
+
#auth;
|
|
1614
|
+
#idleTimeout;
|
|
1615
|
+
#keepAlive;
|
|
1616
|
+
#handshakeTimeout;
|
|
1617
|
+
#classifyOpenFailure;
|
|
1618
|
+
#clock;
|
|
1619
|
+
constructor(options = {}) {
|
|
1620
|
+
const path = options.path ?? "/api/ws";
|
|
1621
|
+
if (typeof path !== "string" || path.trim() === "" || path.includes("?") || path.includes("#") || /^[a-z][a-z\d+.-]*:/i.test(path)) throw new require_errors.ItdConfigError("WebSocketTransport.path должен быть путём без query и fragment");
|
|
1622
|
+
if (options.webSocketImpl !== void 0 && typeof options.webSocketImpl !== "function") throw new require_errors.ItdConfigError("WebSocketTransport.webSocketImpl должен быть конструктором");
|
|
1623
|
+
if (options.classifyOpenFailure !== void 0 && typeof options.classifyOpenFailure !== "function") throw new require_errors.ItdConfigError("WebSocketTransport.classifyOpenFailure должен быть функцией");
|
|
1624
|
+
const auth = options.auth ?? "auto";
|
|
1625
|
+
if (auth !== "auto" && auth !== "query" && auth !== "header") throw new require_errors.ItdConfigError(`Неизвестный способ авторизации WebSocket: ${String(auth)}`);
|
|
1626
|
+
if (auth === "header" && !options.webSocketImpl) throw new require_errors.ItdConfigError("WebSocketTransport.auth=\"header\" требует webSocketImpl с поддержкой заголовков");
|
|
1627
|
+
validateDuration(options.idleTimeout, "idleTimeout");
|
|
1628
|
+
validateDuration(options.keepAlive, "keepAlive");
|
|
1629
|
+
validateDuration(options.handshakeTimeout, "handshakeTimeout");
|
|
1630
|
+
this.#path = path;
|
|
1631
|
+
this.#webSocketImpl = options.webSocketImpl;
|
|
1632
|
+
this.#auth = auth;
|
|
1633
|
+
this.#idleTimeout = options.idleTimeout ?? 9e4;
|
|
1634
|
+
this.#keepAlive = options.keepAlive ?? 3e4;
|
|
1635
|
+
this.#handshakeTimeout = options.handshakeTimeout ?? 2e4;
|
|
1636
|
+
this.#classifyOpenFailure = options.classifyOpenFailure;
|
|
1637
|
+
this.#clock = options.clock ?? require_url.systemClock;
|
|
1638
|
+
}
|
|
1639
|
+
async connect(context) {
|
|
1640
|
+
if (context.signal.aborted) throw abortError(context.signal);
|
|
1641
|
+
const token = context.authorize ? await context.getToken() : null;
|
|
1642
|
+
if (context.signal.aborted) throw abortError(context.signal);
|
|
1643
|
+
if (context.authorize && !token) throw new UnauthorizedStreamError();
|
|
1644
|
+
const injected = this.#webSocketImpl !== void 0;
|
|
1645
|
+
const implementation = this.#webSocketImpl ?? this.#globalImplementation();
|
|
1646
|
+
const auth = this.#auth === "auto" ? injected ? "header" : "query" : this.#auth;
|
|
1647
|
+
const { requestUrl, socketUrl } = this.#urls(context.baseUrl);
|
|
1648
|
+
const url = auth === "query" && token ? `${socketUrl}${require_cookies.buildQuery({ token })}` : socketUrl;
|
|
1649
|
+
let headers;
|
|
1650
|
+
if (injected) {
|
|
1651
|
+
const resolved = await context.baseHeaders(requestUrl);
|
|
1652
|
+
if (context.signal.aborted) throw abortError(context.signal);
|
|
1653
|
+
if (auth === "header" && token) resolved.set("Authorization", `Bearer ${token}`);
|
|
1654
|
+
headers = Object.fromEntries(resolved.entries());
|
|
1655
|
+
}
|
|
1656
|
+
let value;
|
|
1657
|
+
try {
|
|
1658
|
+
value = injected ? new implementation(url, [], {
|
|
1659
|
+
...headers ? { headers } : {},
|
|
1660
|
+
...this.#handshakeTimeout > 0 ? { handshakeTimeout: this.#handshakeTimeout } : {}
|
|
1661
|
+
}) : new implementation(url);
|
|
1662
|
+
} catch (error) {
|
|
1663
|
+
const detail = safeDetail(error, url, token);
|
|
1664
|
+
throw new Error(`Не удалось создать WebSocket ${require_url.redactUrl(url)}${detail ? `: ${detail}` : ""}`);
|
|
1665
|
+
}
|
|
1666
|
+
if (!isWebSocketConnection(value)) throw new require_errors.ItdConfigError("webSocketImpl вернул объект без WebSocket-методов addEventListener, send и close");
|
|
1667
|
+
try {
|
|
1668
|
+
value.binaryType = "arraybuffer";
|
|
1669
|
+
} catch {}
|
|
1670
|
+
return this.#listen(value, url, token, context);
|
|
1671
|
+
}
|
|
1672
|
+
#globalImplementation() {
|
|
1673
|
+
const implementation = Reflect.get(globalThis, "WebSocket");
|
|
1674
|
+
if (typeof implementation !== "function") throw new require_errors.ItdConfigError("В этой среде нет WebSocket: передайте реализацию через webSocketImpl");
|
|
1675
|
+
return implementation;
|
|
1676
|
+
}
|
|
1677
|
+
#urls(baseUrl) {
|
|
1678
|
+
const request = new URL(require_cookies.joinUrl(baseUrl, this.#path));
|
|
1679
|
+
const socket = new URL(request);
|
|
1680
|
+
if (request.protocol === "https:") socket.protocol = "wss:";
|
|
1681
|
+
else if (request.protocol === "http:") socket.protocol = "ws:";
|
|
1682
|
+
else throw new require_errors.ItdConfigError(`WebSocketTransport требует baseUrl с http или https, получено: ${request.protocol}`);
|
|
1683
|
+
return {
|
|
1684
|
+
requestUrl: request.toString(),
|
|
1685
|
+
socketUrl: socket.toString()
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
#listen(socket, url, token, context) {
|
|
1689
|
+
return new Promise((resolve, reject) => {
|
|
1690
|
+
let settled = false;
|
|
1691
|
+
let finishing = false;
|
|
1692
|
+
let opened = false;
|
|
1693
|
+
let discardMessages = false;
|
|
1694
|
+
let socketError;
|
|
1695
|
+
let messageQueue = Promise.resolve();
|
|
1696
|
+
let pendingFrames = 0;
|
|
1697
|
+
let cancelHandshake;
|
|
1698
|
+
let cancelIdle;
|
|
1699
|
+
let cancelKeepAlive;
|
|
1700
|
+
const stopSocketListeners = () => {
|
|
1701
|
+
cancelHandshake?.();
|
|
1702
|
+
cancelIdle?.();
|
|
1703
|
+
cancelKeepAlive?.();
|
|
1704
|
+
cancelHandshake = void 0;
|
|
1705
|
+
cancelIdle = void 0;
|
|
1706
|
+
cancelKeepAlive = void 0;
|
|
1707
|
+
socket.removeEventListener("open", onOpen);
|
|
1708
|
+
socket.removeEventListener("message", onMessage);
|
|
1709
|
+
socket.removeEventListener("error", onError);
|
|
1710
|
+
socket.removeEventListener("close", onClose);
|
|
1711
|
+
};
|
|
1712
|
+
const finish = (error) => {
|
|
1713
|
+
if (settled) return;
|
|
1714
|
+
settled = true;
|
|
1715
|
+
stopSocketListeners();
|
|
1716
|
+
context.signal.removeEventListener("abort", onAbort);
|
|
1717
|
+
if (error !== void 0) reject(error);
|
|
1718
|
+
else resolve();
|
|
1719
|
+
};
|
|
1720
|
+
const closeWithError = (error, code = 4e3, reason = "transport error") => {
|
|
1721
|
+
if (settled || finishing) return;
|
|
1722
|
+
socketError = error;
|
|
1723
|
+
finishing = true;
|
|
1724
|
+
stopSocketListeners();
|
|
1725
|
+
try {
|
|
1726
|
+
if (socket.readyState === CONNECTING || socket.readyState === OPEN) socket.close(code, reason);
|
|
1727
|
+
} catch {}
|
|
1728
|
+
messageQueue.then(() => finish(error), (queueError) => finish(queueError));
|
|
1729
|
+
};
|
|
1730
|
+
const armIdleTimer = () => {
|
|
1731
|
+
cancelIdle?.();
|
|
1732
|
+
if (this.#idleTimeout <= 0) return;
|
|
1733
|
+
cancelIdle = this.#clock.schedule(() => {
|
|
1734
|
+
closeWithError(/* @__PURE__ */ new Error(`WebSocket ${require_url.redactUrl(url)} молчит дольше допустимого`), 4e3, "idle timeout");
|
|
1735
|
+
}, this.#idleTimeout);
|
|
1736
|
+
};
|
|
1737
|
+
const scheduleKeepAlive = () => {
|
|
1738
|
+
if (this.#keepAlive <= 0 || settled || finishing) return;
|
|
1739
|
+
cancelKeepAlive = this.#clock.schedule(() => {
|
|
1740
|
+
if (settled || finishing || socket.readyState !== OPEN) return;
|
|
1741
|
+
try {
|
|
1742
|
+
socket.send("ping");
|
|
1743
|
+
} catch (error) {
|
|
1744
|
+
const detail = safeDetail(error, url, token);
|
|
1745
|
+
closeWithError(/* @__PURE__ */ new Error(`Не удалось отправить WebSocket ping ${require_url.redactUrl(url)}` + (detail ? `: ${detail}` : "")));
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
scheduleKeepAlive();
|
|
1749
|
+
}, this.#keepAlive);
|
|
1750
|
+
};
|
|
1751
|
+
const onOpen = () => {
|
|
1752
|
+
if (settled || finishing || opened) return;
|
|
1753
|
+
opened = true;
|
|
1754
|
+
cancelHandshake?.();
|
|
1755
|
+
cancelHandshake = void 0;
|
|
1756
|
+
armIdleTimer();
|
|
1757
|
+
scheduleKeepAlive();
|
|
1758
|
+
try {
|
|
1759
|
+
context.onOpen();
|
|
1760
|
+
} catch (error) {
|
|
1761
|
+
closeWithError(error);
|
|
1762
|
+
}
|
|
1763
|
+
};
|
|
1764
|
+
const onMessage = (event) => {
|
|
1765
|
+
if (settled || finishing) return;
|
|
1766
|
+
armIdleTimer();
|
|
1767
|
+
let pending;
|
|
1768
|
+
try {
|
|
1769
|
+
const raw = readMessage(event);
|
|
1770
|
+
if (raw !== void 0) pending = Promise.resolve(raw).then((value) => ({ raw: value }), (error) => ({ error }));
|
|
1771
|
+
} catch (error) {
|
|
1772
|
+
pending = Promise.resolve({ error });
|
|
1773
|
+
}
|
|
1774
|
+
if (pending === void 0) return;
|
|
1775
|
+
if (pendingFrames >= 256) {
|
|
1776
|
+
closeWithError(/* @__PURE__ */ new Error(`WebSocket ${require_url.redactUrl(url)}: очередь кадров переполнена`), 4e3, "queue overflow");
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
pendingFrames += 1;
|
|
1780
|
+
messageQueue = messageQueue.then(async () => {
|
|
1781
|
+
pendingFrames -= 1;
|
|
1782
|
+
if (settled || discardMessages) return;
|
|
1783
|
+
const decoded = await pending;
|
|
1784
|
+
if ("error" in decoded) {
|
|
1785
|
+
context.onParseError(decoded.error, "[binary WebSocket frame]");
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
const { raw } = decoded;
|
|
1789
|
+
if (settled || discardMessages || raw === "pong") return;
|
|
1790
|
+
let data;
|
|
1791
|
+
try {
|
|
1792
|
+
data = JSON.parse(raw);
|
|
1793
|
+
} catch (error) {
|
|
1794
|
+
context.onParseError(error, raw);
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
context.onEvent({
|
|
1798
|
+
name: eventName(data),
|
|
1799
|
+
data
|
|
1800
|
+
});
|
|
1801
|
+
}).catch((error) => {
|
|
1802
|
+
if (finishing) throw error;
|
|
1803
|
+
closeWithError(error);
|
|
1804
|
+
});
|
|
1805
|
+
};
|
|
1806
|
+
const onError = (event) => {
|
|
1807
|
+
if (settled || finishing) return;
|
|
1808
|
+
socketError = typeof event === "object" && event !== null && "error" in event ? event.error : event;
|
|
1809
|
+
};
|
|
1810
|
+
const finishClose = async (event) => {
|
|
1811
|
+
const { code, reason, wasClean } = readCloseEvent(event);
|
|
1812
|
+
let outcome;
|
|
1813
|
+
if (context.authorize && UNAUTHORIZED_CLOSE_CODES.has(code)) outcome = new UnauthorizedStreamError();
|
|
1814
|
+
else if (socketError !== void 0) {
|
|
1815
|
+
const detail = safeDetail(socketError, url, token);
|
|
1816
|
+
outcome = /* @__PURE__ */ new Error(`WebSocket ${require_url.redactUrl(url)} завершился ошибкой${detail ? `: ${detail}` : ""}`);
|
|
1817
|
+
} else if (opened && code === NORMAL_CLOSURE && wasClean) outcome = void 0;
|
|
1818
|
+
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}` : ""));
|
|
1821
|
+
}
|
|
1822
|
+
if (!opened && context.authorize && !(outcome instanceof UnauthorizedStreamError) && this.#classifyOpenFailure) try {
|
|
1823
|
+
if (await this.#classifyOpenFailure(outcome, context.signal)) outcome = new UnauthorizedStreamError();
|
|
1824
|
+
} catch {}
|
|
1825
|
+
if (context.signal.aborted) {
|
|
1826
|
+
discardMessages = true;
|
|
1827
|
+
finish(abortError(context.signal));
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
try {
|
|
1831
|
+
await messageQueue;
|
|
1832
|
+
finish(outcome);
|
|
1833
|
+
} catch (error) {
|
|
1834
|
+
finish(error);
|
|
1835
|
+
}
|
|
1836
|
+
};
|
|
1837
|
+
const onClose = (event) => {
|
|
1838
|
+
if (settled || finishing) return;
|
|
1839
|
+
if (context.signal.aborted) {
|
|
1840
|
+
discardMessages = true;
|
|
1841
|
+
finish(abortError(context.signal));
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
finishing = true;
|
|
1845
|
+
stopSocketListeners();
|
|
1846
|
+
finishClose(event);
|
|
1847
|
+
};
|
|
1848
|
+
const onAbort = () => {
|
|
1849
|
+
if (settled) return;
|
|
1850
|
+
const error = abortError(context.signal);
|
|
1851
|
+
discardMessages = true;
|
|
1852
|
+
finishing = true;
|
|
1853
|
+
stopSocketListeners();
|
|
1854
|
+
try {
|
|
1855
|
+
if (socket.readyState === CONNECTING || socket.readyState === OPEN) socket.close(NORMAL_CLOSURE, "aborted");
|
|
1856
|
+
} catch {}
|
|
1857
|
+
finish(error);
|
|
1858
|
+
};
|
|
1859
|
+
socket.addEventListener("open", onOpen);
|
|
1860
|
+
socket.addEventListener("message", onMessage);
|
|
1861
|
+
socket.addEventListener("error", onError);
|
|
1862
|
+
socket.addEventListener("close", onClose);
|
|
1863
|
+
context.signal.addEventListener("abort", onAbort, { once: true });
|
|
1864
|
+
if (this.#handshakeTimeout > 0) cancelHandshake = this.#clock.schedule(() => {
|
|
1865
|
+
closeWithError(/* @__PURE__ */ new Error(`Истёк таймаут установки WebSocket ${require_url.redactUrl(url)}`), 4e3, "handshake timeout");
|
|
1866
|
+
}, this.#handshakeTimeout);
|
|
1867
|
+
if (context.signal.aborted) onAbort();
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1871
|
+
//#endregion
|
|
1872
|
+
Object.defineProperty(exports, "Emitter", {
|
|
1873
|
+
enumerable: true,
|
|
1874
|
+
get: function() {
|
|
1875
|
+
return Emitter;
|
|
1876
|
+
}
|
|
1877
|
+
});
|
|
1878
|
+
Object.defineProperty(exports, "ItdRealtime", {
|
|
1879
|
+
enumerable: true,
|
|
1880
|
+
get: function() {
|
|
1881
|
+
return ItdRealtime;
|
|
1882
|
+
}
|
|
1883
|
+
});
|
|
1884
|
+
Object.defineProperty(exports, "RealtimeComposer", {
|
|
1885
|
+
enumerable: true,
|
|
1886
|
+
get: function() {
|
|
1887
|
+
return RealtimeComposer;
|
|
1888
|
+
}
|
|
1889
|
+
});
|
|
1890
|
+
Object.defineProperty(exports, "RealtimeRouter", {
|
|
1891
|
+
enumerable: true,
|
|
1892
|
+
get: function() {
|
|
1893
|
+
return RealtimeRouter;
|
|
1894
|
+
}
|
|
1895
|
+
});
|
|
1896
|
+
Object.defineProperty(exports, "RealtimeTransportKind", {
|
|
1897
|
+
enumerable: true,
|
|
1898
|
+
get: function() {
|
|
1899
|
+
return RealtimeTransportKind;
|
|
1900
|
+
}
|
|
1901
|
+
});
|
|
1902
|
+
Object.defineProperty(exports, "RealtimeUpdateOrigin", {
|
|
1903
|
+
enumerable: true,
|
|
1904
|
+
get: function() {
|
|
1905
|
+
return RealtimeUpdateOrigin;
|
|
1906
|
+
}
|
|
1907
|
+
});
|
|
1908
|
+
Object.defineProperty(exports, "RealtimeUpdateType", {
|
|
1909
|
+
enumerable: true,
|
|
1910
|
+
get: function() {
|
|
1911
|
+
return RealtimeUpdateType;
|
|
1912
|
+
}
|
|
1913
|
+
});
|
|
1914
|
+
Object.defineProperty(exports, "UnauthorizedStreamError", {
|
|
1915
|
+
enumerable: true,
|
|
1916
|
+
get: function() {
|
|
1917
|
+
return UnauthorizedStreamError;
|
|
1918
|
+
}
|
|
1919
|
+
});
|
|
1920
|
+
Object.defineProperty(exports, "WebSocketTransport", {
|
|
1921
|
+
enumerable: true,
|
|
1922
|
+
get: function() {
|
|
1923
|
+
return WebSocketTransport;
|
|
1924
|
+
}
|
|
1925
|
+
});
|
|
1926
|
+
Object.defineProperty(exports, "reportListenerError", {
|
|
1927
|
+
enumerable: true,
|
|
1928
|
+
get: function() {
|
|
1929
|
+
return reportListenerError;
|
|
1930
|
+
}
|
|
1931
|
+
});
|
|
1932
|
+
Object.defineProperty(exports, "runRealtimeMiddleware", {
|
|
1933
|
+
enumerable: true,
|
|
1934
|
+
get: function() {
|
|
1935
|
+
return runRealtimeMiddleware;
|
|
1936
|
+
}
|
|
1937
|
+
});
|
|
1938
|
+
Object.defineProperty(exports, "setRealtimeConnectGuard", {
|
|
1939
|
+
enumerable: true,
|
|
1940
|
+
get: function() {
|
|
1941
|
+
return setRealtimeConnectGuard;
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1944
|
+
|
|
1945
|
+
//# sourceMappingURL=websocket-sYlynrr0.cjs.map
|