itd-api 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { _ as resolveFileStreamOptions, a as isRestorableSession, b as REFRESH_COOKIE, c as DEFAULT_URL_FILE_MAX_BYTES, d as downloadFile, f as fromStream, g as openUrlFile, h as normalizeMimeType, i as createRecordMultiStorage, l as FileTransferMode, m as isBoundedFileStream, n as controlledTokenStorage, o as scopedTokenStorage, p as fromUrl, r as createMultiTokenStorage, s as DEFAULT_FILE_STREAM_BUFFER_BYTES, t as MemoryMultiTokenStorage, u as boundedFileStream, v as AUTH_FLAG_COOKIE, x as REFRESH_COOKIE_PATH, y as CookieJar } from "./multi-storage-CyMe404l.js";
1
+ import { _ as CookieJar, a as isRestorableSession, c as boundedFileStream, d as fileTooLarge, f as optionalBytes, g as AUTH_FLAG_COOKIE, h as FileTransferMode, i as createRecordMultiStorage, l as isBoundedFileStream, m as DEFAULT_URL_FILE_MAX_BYTES, n as controlledTokenStorage, o as scopedTokenStorage, p as DEFAULT_FILE_STREAM_BUFFER_BYTES, r as createMultiTokenStorage, s as resolveFileStreamOptions, t as MemoryMultiTokenStorage, u as isReadableByteStream, v as REFRESH_COOKIE, y as REFRESH_COOKIE_PATH } from "./multi-storage-D4h6QA-w.js";
2
2
  import { A as isItdRateLimitError, C as isItdAuthError, D as isItdForbiddenError, E as isItdFileError, M as isItdValidationError, O as isItdNotFoundError, S as isItdApiError, T as isItdError, _ as ItdPhoneVerificationError, a as ItdApiError, b as ItdTimeoutError, c as ItdConfigError, d as ItdErrorKind, f as ItdFileError, g as ItdNotFoundError, h as ItdNetworkError, i as ItdAbortError, j as isItdServerError, k as isItdPhoneVerificationError, l as ItdConflictError, m as ItdForbiddenError, n as copySession, o as ItdApiErrorKind, p as ItdFileErrorReason, r as createTokenStorage, s as ItdAuthError, t as MemoryTokenStorage, u as ItdError, v as ItdRateLimitError, w as isItdConflictError, x as ItdValidationError, y as ItdServerError } from "./storage-D9tfHx7Z.js";
3
3
  import { a as isBlob, c as shouldSendCredentials, l as shouldUseCookieJar, n as RuntimeMode, o as isFile, r as createDeviceId, s as resolveFetch, t as DetectedRuntime, u as supportsStreamingBody } from "./runtime-DHxDn8gf.js";
4
4
  //#region src/core/emitter.ts
@@ -418,7 +418,7 @@ var AuthManager = class {
418
418
  await this.#saveSession({
419
419
  ...this.#session ?? {},
420
420
  accessToken,
421
- obtainedAt: Date.now()
421
+ obtainedAt: this.#config.clock.now()
422
422
  });
423
423
  this.#emitter.emit("tokens", { accessToken });
424
424
  }
@@ -503,12 +503,12 @@ var AuthManager = class {
503
503
  if (!auth) return null;
504
504
  if (typeof auth === "string") return {
505
505
  accessToken: auth,
506
- obtainedAt: Date.now()
506
+ obtainedAt: this.#config.clock.now()
507
507
  };
508
508
  if ("accessToken" in auth) return {
509
509
  accessToken: auth.accessToken,
510
510
  refreshToken: auth.refreshToken,
511
- obtainedAt: Date.now()
511
+ obtainedAt: this.#config.clock.now()
512
512
  };
513
513
  return null;
514
514
  }
@@ -556,7 +556,7 @@ var AuthManager = class {
556
556
  ...this.#session ?? {},
557
557
  accessToken,
558
558
  ...rotated ? { refreshToken: rotated } : {},
559
- obtainedAt: Date.now()
559
+ obtainedAt: this.#config.clock.now()
560
560
  });
561
561
  this.#emitter.emit("tokens", { accessToken });
562
562
  return accessToken;
@@ -631,7 +631,7 @@ var AuthManager = class {
631
631
  this.#transitionAuth(accessToken);
632
632
  await this.#saveSession({
633
633
  accessToken,
634
- obtainedAt: Date.now()
634
+ obtainedAt: this.#config.clock.now()
635
635
  });
636
636
  this.#emitter.emit("tokens", { accessToken });
637
637
  this.#emitter.emit("signIn", { accessToken });
@@ -639,6 +639,16 @@ var AuthManager = class {
639
639
  }
640
640
  };
641
641
  //#endregion
642
+ //#region src/core/clock.ts
643
+ /** Системные часы, используемые клиентом по умолчанию. */
644
+ const systemClock = Object.freeze({
645
+ now: () => Date.now(),
646
+ schedule(callback, delay) {
647
+ const timer = setTimeout(callback, delay);
648
+ return () => clearTimeout(timer);
649
+ }
650
+ });
651
+ //#endregion
642
652
  //#region src/core/url.ts
643
653
  /**
644
654
  * Собирает строку запроса.
@@ -716,7 +726,7 @@ function normalizeBaseUrl(baseUrl) {
716
726
  //#endregion
717
727
  //#region src/core/version.ts
718
728
  /** Версия библиотеки. Попадает в `User-Agent`. */
719
- const LIBRARY_VERSION = "0.2.0";
729
+ const LIBRARY_VERSION = "0.4.0";
720
730
  //#endregion
721
731
  //#region src/core/config.ts
722
732
  /** Базовый URL API итд.com. Домен записан в punycode: `итд.com`. */
@@ -944,6 +954,7 @@ function resolveConfig(options = {}) {
944
954
  const mode = options.mode ?? RuntimeMode.Auto;
945
955
  if (!Object.values(RuntimeMode).includes(mode)) throw new ItdConfigError(`mode должен быть одним из ${Object.values(RuntimeMode).join(", ")}, получено: ${mode}`);
946
956
  const timeout = requirePositive(options.timeout ?? 3e4, "timeout");
957
+ if (options.clock !== void 0 && (typeof options.clock !== "object" || options.clock === null || typeof options.clock.now !== "function" || typeof options.clock.schedule !== "function")) throw new ItdConfigError("clock должен предоставлять методы now() и schedule()");
947
958
  requireOptionalBoolean(options.autoRefresh, "autoRefresh");
948
959
  requireOptionalBoolean(options.reloginOnRefreshFailure, "reloginOnRefreshFailure");
949
960
  if (options.userAgent !== void 0 && options.userAgent !== false && typeof options.userAgent !== "string") throw new ItdConfigError("userAgent должен быть строкой или false");
@@ -956,6 +967,7 @@ function resolveConfig(options = {}) {
956
967
  autoRefresh: options.autoRefresh ?? true,
957
968
  reloginOnRefreshFailure: options.reloginOnRefreshFailure ?? true,
958
969
  fetch: resolveFetch(options.fetch),
970
+ clock: options.clock ?? systemClock,
959
971
  timeout,
960
972
  retry: resolveRetry(options.retry),
961
973
  rateLimit: resolveRateLimit(options.rateLimit),
@@ -1039,15 +1051,271 @@ function withLayerHeaders(request, headers) {
1039
1051
  };
1040
1052
  }
1041
1053
  //#endregion
1042
- //#region src/core/plugins.ts
1043
- /** Пустой набор отдаётся, пока плагинов нет, чтобы не заводить объект на каждый запрос. */
1044
- const NO_KEYS = /* @__PURE__ */ new Set();
1054
+ //#region src/core/plugins/hooks.ts
1055
+ const REQUEST_HOOK_DISPATCHERS = /* @__PURE__ */ new WeakMap();
1056
+ const PLUGIN_HOOK_SCOPE = Symbol("itd-api.plugin-hooks");
1057
+ /** Создаёт динамический набор hooks, связанный с диспетчером registry. @internal */
1058
+ function createRequestHooks(dispatcher) {
1059
+ const hooks = {};
1060
+ REQUEST_HOOK_DISPATCHERS.set(hooks, dispatcher);
1061
+ return hooks;
1062
+ }
1063
+ /** Привязывает к запросу неизменяемый снимок plugin hooks. @internal */
1064
+ function withRequestHookScope(request, hooks) {
1065
+ const scoped = request;
1066
+ return scoped[PLUGIN_HOOK_SCOPE] === hooks ? scoped : {
1067
+ ...request,
1068
+ [PLUGIN_HOOK_SCOPE]: hooks
1069
+ };
1070
+ }
1071
+ /** Читает снимок plugin hooks, привязанный к логическому запросу. @internal */
1072
+ function requestHookScope(request) {
1073
+ return request[PLUGIN_HOOK_SCOPE] ?? [];
1074
+ }
1075
+ /**
1076
+ * Вызывает публичный хук, сохраняя привязанный к логическому запросу снимок плагинов.
1077
+ *
1078
+ * Обычные наборы хуков по-прежнему получают только публичный контекст. Дополнительный
1079
+ * аргумент используется исключительно внутренним составным набором PluginRegistry.
1080
+ *
1081
+ * @internal
1082
+ */
1083
+ async function dispatchRequestHook(hooks, field, context, request) {
1084
+ const dispatcher = REQUEST_HOOK_DISPATCHERS.get(hooks);
1085
+ if (dispatcher) {
1086
+ await dispatcher(field, context, request);
1087
+ return;
1088
+ }
1089
+ const hook = hooks[field];
1090
+ await hook?.(context);
1091
+ }
1092
+ //#endregion
1093
+ //#region src/core/retry.ts
1094
+ /**
1095
+ * Методы, повтор которых безопасен по определению.
1096
+ *
1097
+ * `DELETE` формально тоже идемпотентен, но его повтор после успеха вернёт `404`
1098
+ * и собьёт с толку — в список он не входит.
1099
+ */
1100
+ const IDEMPOTENT_METHODS = /* @__PURE__ */ new Set([
1101
+ "GET",
1102
+ "HEAD",
1103
+ "OPTIONS"
1104
+ ]);
1105
+ /**
1106
+ * Стоит ли повторять запрос после этой ошибки.
1107
+ *
1108
+ * Отдельно разобран `429`: он повторяется даже для запросов на запись, потому что
1109
+ * гарантирует, что запрос **не был обработан**. Обрыв сети и `5xx` такой гарантии не дают —
1110
+ * сервер мог успеть создать пост, — поэтому запись по умолчанию не повторяется.
1111
+ */
1112
+ function isRetryable(error, method, retryWrites, retryNetworkWrite) {
1113
+ if (error instanceof ItdAbortError) return false;
1114
+ const safeToRepeat = retryWrites || IDEMPOTENT_METHODS.has(method);
1115
+ if (error instanceof ItdApiError) {
1116
+ if (error.status === 429) return true;
1117
+ if (error.status >= 500) return safeToRepeat;
1118
+ return false;
1119
+ }
1120
+ if (error instanceof ItdNetworkError || error instanceof ItdTimeoutError) return safeToRepeat || retryNetworkWrite;
1121
+ if (error instanceof ItdFileError) return error.retryable && (safeToRepeat || retryNetworkWrite);
1122
+ return false;
1123
+ }
1124
+ /** Экспоненциальная пауза со случайным разбросом. */
1125
+ function backoffDelay(attempt, options, random) {
1126
+ const exponential = options.baseDelay * 2 ** (attempt - 1);
1127
+ const capped = Math.min(exponential, options.maxDelay);
1128
+ const spread = capped * options.jitter * (random() * 2 - 1);
1129
+ return Math.max(0, Math.round(capped + spread));
1130
+ }
1131
+ /**
1132
+ * Собирает планировщик повторов для транспорта.
1133
+ *
1134
+ * Поведение при `Retry-After`: пауза, названная сервером, соблюдается точно — она
1135
+ * авторитетнее нашего расчёта. Но если сервер просит ждать дольше, чем `maxDelay`,
1136
+ * повтор **не выполняется вовсе**: молча спать десять минут внутри вызова библиотека
1137
+ * не должна, лучше отдать {@link ItdRateLimitError} и дать решить вызывающему коду.
1138
+ *
1139
+ * @param options настройки повторов после подстановки значений по умолчанию
1140
+ * @param random источник случайности; подменяется в тестах ради предсказуемости
1141
+ *
1142
+ * @example
1143
+ * ```ts
1144
+ * const scheduler = createRetryScheduler(config.retry);
1145
+ * const delay = scheduler(error, 1, 'GET'); // 500 мс ± 30%
1146
+ * ```
1147
+ */
1148
+ function createRetryScheduler(options, random = Math.random) {
1149
+ return (error, attempt, method, retryNetworkWrite = false) => {
1150
+ if (attempt >= options.attempts) return void 0;
1151
+ if (options.shouldRetry) return options.shouldRetry(error, attempt) ? backoffDelay(attempt, options, random) : void 0;
1152
+ if (!isRetryable(error, method, options.retryWrites, retryNetworkWrite)) return void 0;
1153
+ if (error instanceof ItdApiError && error.retryAfter !== void 0) return error.retryAfter > options.maxDelay ? void 0 : error.retryAfter;
1154
+ return backoffDelay(attempt, options, random);
1155
+ };
1156
+ }
1157
+ //#endregion
1158
+ //#region src/core/middleware.ts
1159
+ /** Ожидание повтора, которое уважает отмену запроса. */
1160
+ function sleep(clock, ms, signal) {
1161
+ if (!signal) return new Promise((resolve) => clock.schedule(resolve, ms));
1162
+ if (signal.aborted) return Promise.reject(new ItdAbortError("Запрос отменён во время ожидания повтора"));
1163
+ return new Promise((resolve, reject) => {
1164
+ const cancel = clock.schedule(() => {
1165
+ signal.removeEventListener("abort", onAbort);
1166
+ resolve();
1167
+ }, ms);
1168
+ const onAbort = () => {
1169
+ cancel();
1170
+ reject(new ItdAbortError("Запрос отменён во время ожидания повтора"));
1171
+ };
1172
+ signal.addEventListener("abort", onAbort, { once: true });
1173
+ });
1174
+ }
1175
+ /**
1176
+ * Слой очереди: ограничение конкурентности и частоты.
1177
+ *
1178
+ * `skipQueue` пропускает запрос мимо очереди — так поступают служебные запросы, которые
1179
+ * порождены изнутри другого запроса и не могут ждать освободившегося слота.
1180
+ *
1181
+ * Очередь выбирается по запросу: у каждого сервиса платформы свой хост и свой лимит.
1182
+ */
1183
+ function createQueueMiddleware(schedule) {
1184
+ return (request, next) => request.skipQueue ? next(request) : schedule(request, () => next(request));
1185
+ }
1186
+ /**
1187
+ * Слой плагинов.
1188
+ *
1189
+ * Стоит снаружи повторов и внутри очереди: плагин должен увидеть запрос и ответ по одному
1190
+ * разу, независимо от числа попыток, — иначе, например, текст поста зашифруется дважды.
1191
+ */
1192
+ function createPluginsMiddleware(plugins) {
1193
+ return (request, next) => plugins.run(request, next);
1194
+ }
1195
+ /**
1196
+ * Слой сервисов.
1197
+ *
1198
+ * Запросу с полем `service` подставляет хост сервиса, его заголовки и `skipAuth`, если
1199
+ * сервис объявлен публичным. Заданный у запроса `baseUrl` не трогает.
1200
+ *
1201
+ * Стоит снаружи повторов и авторизации, чтобы выставленный здесь `skipAuth` был ей виден.
1202
+ */
1203
+ function createServicesMiddleware(registry) {
1204
+ return async (request, next) => {
1205
+ const service = request.service === void 0 ? void 0 : registry.require(request.service);
1206
+ let prepared = request;
1207
+ if (request.baseUrl !== void 0) {
1208
+ const baseUrl = normalizeBaseUrl(request.baseUrl);
1209
+ if (baseUrl !== request.baseUrl) prepared = {
1210
+ ...prepared,
1211
+ baseUrl
1212
+ };
1213
+ if (!(service?.baseUrl === baseUrl ? service.auth !== false : registry.isPrimarySite(baseUrl)) && prepared.skipAuth === void 0) prepared = {
1214
+ ...prepared,
1215
+ skipAuth: true
1216
+ };
1217
+ }
1218
+ if (!service) return next(prepared);
1219
+ if (prepared.baseUrl === void 0) prepared = {
1220
+ ...prepared,
1221
+ baseUrl: service.baseUrl
1222
+ };
1223
+ if (service.headers) prepared = withLayerHeaders(prepared, service.headers);
1224
+ if (service.auth === false && prepared.skipAuth === void 0) prepared = {
1225
+ ...prepared,
1226
+ skipAuth: true
1227
+ };
1228
+ return next(prepared);
1229
+ };
1230
+ }
1231
+ async function applyAuth(request, deps) {
1232
+ if (request.skipAuth) return request;
1233
+ const headers = await deps.getAuthHeaders();
1234
+ return Object.keys(headers).length > 0 ? withLayerHeaders(request, headers) : request;
1235
+ }
1236
+ /**
1237
+ * Слой авторизации.
1238
+ *
1239
+ * Подставляет заголовок `Authorization` и обрабатывает `401`: обновляет токен и повторяет
1240
+ * запрос ровно один раз. Стоит внутри повторов, поэтому обычным попыткам он не виден —
1241
+ * они уже работают со свежим токеном.
1242
+ */
1243
+ function createAuthMiddleware(deps) {
1244
+ return async (request, next) => {
1245
+ const authorized = await applyAuth(request, deps);
1246
+ try {
1247
+ return await next(authorized);
1248
+ } catch (error) {
1249
+ if (request.skipAuthRefresh || !deps.autoRefresh || !isItdApiError(error) || error.status !== 401) throw error;
1250
+ if (!await deps.onUnauthorized()) throw error;
1251
+ return next(await applyAuth({
1252
+ ...request,
1253
+ skipAuthRefresh: true
1254
+ }, deps));
1255
+ }
1256
+ };
1257
+ }
1258
+ /**
1259
+ * Выбирает планировщик отката для конкретного запроса.
1260
+ *
1261
+ * `retry` у запроса переопределяет глобальную настройку: `false` выключает повторы,
1262
+ * объект задаёт свои. Обработка `429` от этого не зависит — она общая.
1263
+ */
1264
+ function resolveBackoff(retry, global) {
1265
+ if (retry === void 0) return global;
1266
+ if (retry === false) return void 0;
1267
+ const resolved = resolveRetry(retry);
1268
+ return resolved ? createRetryScheduler(resolved) : void 0;
1269
+ }
1045
1270
  /**
1046
- * Имена, которые плагин заявить не может.
1271
+ * Слой повторов.
1047
1272
  *
1048
- * Заявленные опции ресурсы переносят в описание запроса поверх собранных полей, поэтому
1049
- * имя из {@link RawRequestOptions} подменило бы путь, тело или заголовки любого вызова.
1273
+ * Ответ `429` обрабатывается отдельно от прочих ошибок лестницей пауз и с придержанием
1274
+ * всей очереди; сетевые сбои и `5xx` экспоненциальным откатом. Настройка `retry`
1275
+ * у отдельного запроса имеет приоритет над глобальной.
1050
1276
  */
1277
+ function createRetryMiddleware(deps) {
1278
+ const globalScheduler = deps.retry ? createRetryScheduler(deps.retry) : void 0;
1279
+ const nextDelay = (error, attempt, request, method, backoff) => {
1280
+ if (isItdRateLimitError(error)) {
1281
+ const wait = error.retryAfter ?? deps.rateLimitDelays[attempt - 1];
1282
+ if (wait === void 0) return void 0;
1283
+ deps.pauseQueue?.(wait, request);
1284
+ deps.logger?.debug(`лимит частоты, попытка ${attempt + 1} через ${wait} мс`);
1285
+ return wait;
1286
+ }
1287
+ return backoff?.(error, attempt, method, request.retryNetworkWrite ?? false);
1288
+ };
1289
+ return async (request, next) => {
1290
+ const method = request.method.toUpperCase();
1291
+ const backoff = resolveBackoff(request.retry, globalScheduler);
1292
+ for (let attempt = 1;; attempt++) try {
1293
+ return await next({
1294
+ ...request,
1295
+ attempt
1296
+ });
1297
+ } catch (error) {
1298
+ const delay = nextDelay(error, attempt, request, method, backoff);
1299
+ if (delay === void 0) throw error;
1300
+ await dispatchRequestHook(deps.hooks, "onRetry", {
1301
+ method,
1302
+ path: request.path,
1303
+ url: deps.buildUrl(request),
1304
+ headers: new Headers({
1305
+ ...request.layerHeaders,
1306
+ ...request.headers
1307
+ }),
1308
+ attempt,
1309
+ error,
1310
+ delay
1311
+ }, request);
1312
+ deps.logger?.debug(`повтор ${method} ${request.path}, попытка ${attempt + 1} через ${delay} мс`);
1313
+ await sleep(deps.clock ?? systemClock, delay, request.signal);
1314
+ }
1315
+ };
1316
+ }
1317
+ //#endregion
1318
+ //#region src/core/plugins/order.ts
1051
1319
  const RESERVED_OPTION_KEYS = /* @__PURE__ */ new Set([
1052
1320
  "signal",
1053
1321
  "timeout",
@@ -1088,7 +1356,8 @@ function validateNameList(plugin, field) {
1088
1356
  seen.add(value);
1089
1357
  }
1090
1358
  }
1091
- function validateHooks(plugin, hooks) {
1359
+ /** Проверяет набор hooks, переданный плагином. @internal */
1360
+ function validatePluginHooks(plugin, hooks) {
1092
1361
  if (typeof hooks !== "object" || hooks === null) throw new ItdConfigError(`плагин «${plugin}» передал в useHooks() не объект`);
1093
1362
  for (const field of HOOK_FIELDS) if (hooks[field] !== void 0 && typeof hooks[field] !== "function") throw new ItdConfigError(`плагин «${plugin}»: useHooks().${field} должен быть функцией`);
1094
1363
  }
@@ -1175,32 +1444,16 @@ function orderPluginDefinitions(plugins) {
1175
1444
  if (ordered.length !== entries.length) throw new ItdConfigError(`циклический порядок плагинов: ${entries.filter(({ plugin }) => (indegree.get(plugin.name) ?? 0) > 0).map(({ plugin }) => plugin.name).join(" → ")}`);
1176
1445
  return ordered;
1177
1446
  }
1178
- /** Проверяет, можно ли удалить плагин, не нарушив обязательные зависимости. @internal */
1447
+ /** Проверяет, можно ли удалить плагин без нарушения обязательных зависимостей. @internal */
1179
1448
  function assertPluginRemovable(plugins, name) {
1180
1449
  const dependent = plugins.find((plugin) => plugin.requires?.includes(name));
1181
1450
  if (dependent) throw new ItdConfigError(`нельзя отключить плагин «${name}»: от него зависит «${dependent.name}»`);
1182
1451
  }
1183
- const REQUEST_HOOK_DISPATCHERS = /* @__PURE__ */ new WeakMap();
1184
- const PLUGIN_HOOK_SCOPE = Symbol("itd-api.plugin-hooks");
1452
+ //#endregion
1453
+ //#region src/core/plugins/registry.ts
1454
+ const NO_KEYS = /* @__PURE__ */ new Set();
1185
1455
  /**
1186
- * Вызывает публичный хук, сохраняя привязанный к логическому запросу снимок плагинов.
1187
- *
1188
- * Обычные наборы хуков по-прежнему получают только публичный контекст. Дополнительный
1189
- * аргумент используется исключительно внутренним составным набором PluginRegistry.
1190
- *
1191
- * @internal
1192
- */
1193
- async function dispatchRequestHook(hooks, field, context, request) {
1194
- const dispatcher = REQUEST_HOOK_DISPATCHERS.get(hooks);
1195
- if (dispatcher) {
1196
- await dispatcher(field, context, request);
1197
- return;
1198
- }
1199
- const hook = hooks[field];
1200
- await hook?.(context);
1201
- }
1202
- /**
1203
- * Список подключённых плагинов и собранная из них цепочка обёрток.
1456
+ * Список подключённых плагинов и собранная из них цепочка обёрток.
1204
1457
  *
1205
1458
  * Живёт в клиенте, а работает в транспорте: {@link HttpClient} прогоняет через `run`
1206
1459
  * каждый запрос, если плагины есть.
@@ -1256,7 +1509,7 @@ var PluginRegistry = class {
1256
1509
  transformers.push(transformer);
1257
1510
  },
1258
1511
  useHooks: (value) => {
1259
- validateHooks(plugin.name, value);
1512
+ validatePluginHooks(plugin.name, value);
1260
1513
  hooks.push({ ...value });
1261
1514
  }
1262
1515
  });
@@ -1338,9 +1591,7 @@ var PluginRegistry = class {
1338
1591
  * со следующего логического запроса без пересоздания транспорта.
1339
1592
  */
1340
1593
  hooks(base) {
1341
- const hooks = {};
1342
- REQUEST_HOOK_DISPATCHERS.set(hooks, ((field, context, request) => this.#runHook(field, context, request, base)));
1343
- return hooks;
1594
+ return createRequestHooks((field, context, request) => this.#runHook(field, context, request, base));
1344
1595
  }
1345
1596
  /**
1346
1597
  * Прогоняет запрос через цепочку обёрток.
@@ -1354,10 +1605,7 @@ var PluginRegistry = class {
1354
1605
  const entries = [...this.#ordered];
1355
1606
  for (const entry of entries) entry.activeRequests += 1;
1356
1607
  const hookScope = entries.flatMap((entry) => entry.hooks);
1357
- const scoped = (current) => current[PLUGIN_HOOK_SCOPE] === hookScope ? current : {
1358
- ...current,
1359
- [PLUGIN_HOOK_SCOPE]: hookScope
1360
- };
1608
+ const scoped = (current) => withRequestHookScope(current, hookScope);
1361
1609
  const chain = entries.flatMap((entry) => entry.transformers).reduceRight((next, transformer) => (current) => transformer(scoped(current), (prepared) => next(scoped(prepared))), (current) => execute(scoped(current)));
1362
1610
  try {
1363
1611
  return await chain(scoped(request));
@@ -1391,239 +1639,13 @@ var PluginRegistry = class {
1391
1639
  async #runHook(field, context, request, base) {
1392
1640
  const baseHook = base[field];
1393
1641
  await baseHook?.(context);
1394
- const scope = request[PLUGIN_HOOK_SCOPE] ?? [];
1395
- for (const hooks of scope) {
1642
+ for (const hooks of requestHookScope(request)) {
1396
1643
  const hook = hooks[field];
1397
1644
  await hook?.(context);
1398
1645
  }
1399
1646
  }
1400
1647
  };
1401
1648
  //#endregion
1402
- //#region src/core/retry.ts
1403
- /**
1404
- * Методы, повтор которых безопасен по определению.
1405
- *
1406
- * `DELETE` формально тоже идемпотентен, но его повтор после успеха вернёт `404`
1407
- * и собьёт с толку — в список он не входит.
1408
- */
1409
- const IDEMPOTENT_METHODS = /* @__PURE__ */ new Set([
1410
- "GET",
1411
- "HEAD",
1412
- "OPTIONS"
1413
- ]);
1414
- /**
1415
- * Стоит ли повторять запрос после этой ошибки.
1416
- *
1417
- * Отдельно разобран `429`: он повторяется даже для запросов на запись, потому что
1418
- * гарантирует, что запрос **не был обработан**. Обрыв сети и `5xx` такой гарантии не дают —
1419
- * сервер мог успеть создать пост, — поэтому запись по умолчанию не повторяется.
1420
- */
1421
- function isRetryable(error, method, retryWrites, retryNetworkWrite) {
1422
- if (error instanceof ItdAbortError) return false;
1423
- const safeToRepeat = retryWrites || IDEMPOTENT_METHODS.has(method);
1424
- if (error instanceof ItdApiError) {
1425
- if (error.status === 429) return true;
1426
- if (error.status >= 500) return safeToRepeat;
1427
- return false;
1428
- }
1429
- if (error instanceof ItdNetworkError || error instanceof ItdTimeoutError) return safeToRepeat || retryNetworkWrite;
1430
- if (error instanceof ItdFileError) return error.retryable && (safeToRepeat || retryNetworkWrite);
1431
- return false;
1432
- }
1433
- /** Экспоненциальная пауза со случайным разбросом. */
1434
- function backoffDelay(attempt, options, random) {
1435
- const exponential = options.baseDelay * 2 ** (attempt - 1);
1436
- const capped = Math.min(exponential, options.maxDelay);
1437
- const spread = capped * options.jitter * (random() * 2 - 1);
1438
- return Math.max(0, Math.round(capped + spread));
1439
- }
1440
- /**
1441
- * Собирает планировщик повторов для транспорта.
1442
- *
1443
- * Поведение при `Retry-After`: пауза, названная сервером, соблюдается точно — она
1444
- * авторитетнее нашего расчёта. Но если сервер просит ждать дольше, чем `maxDelay`,
1445
- * повтор **не выполняется вовсе**: молча спать десять минут внутри вызова библиотека
1446
- * не должна, лучше отдать {@link ItdRateLimitError} и дать решить вызывающему коду.
1447
- *
1448
- * @param options настройки повторов после подстановки значений по умолчанию
1449
- * @param random источник случайности; подменяется в тестах ради предсказуемости
1450
- *
1451
- * @example
1452
- * ```ts
1453
- * const scheduler = createRetryScheduler(config.retry);
1454
- * const delay = scheduler(error, 1, 'GET'); // 500 мс ± 30%
1455
- * ```
1456
- */
1457
- function createRetryScheduler(options, random = Math.random) {
1458
- return (error, attempt, method, retryNetworkWrite = false) => {
1459
- if (attempt >= options.attempts) return void 0;
1460
- if (options.shouldRetry) return options.shouldRetry(error, attempt) ? backoffDelay(attempt, options, random) : void 0;
1461
- if (!isRetryable(error, method, options.retryWrites, retryNetworkWrite)) return void 0;
1462
- if (error instanceof ItdApiError && error.retryAfter !== void 0) return error.retryAfter > options.maxDelay ? void 0 : error.retryAfter;
1463
- return backoffDelay(attempt, options, random);
1464
- };
1465
- }
1466
- //#endregion
1467
- //#region src/core/middleware.ts
1468
- /** Ожидание повтора, которое уважает отмену запроса. */
1469
- function sleep(ms, signal) {
1470
- if (!signal) return new Promise((resolve) => setTimeout(resolve, ms));
1471
- if (signal.aborted) return Promise.reject(new ItdAbortError("Запрос отменён во время ожидания повтора"));
1472
- return new Promise((resolve, reject) => {
1473
- const timer = setTimeout(() => {
1474
- signal.removeEventListener("abort", onAbort);
1475
- resolve();
1476
- }, ms);
1477
- const onAbort = () => {
1478
- clearTimeout(timer);
1479
- reject(new ItdAbortError("Запрос отменён во время ожидания повтора"));
1480
- };
1481
- signal.addEventListener("abort", onAbort, { once: true });
1482
- });
1483
- }
1484
- /**
1485
- * Слой очереди: ограничение конкурентности и частоты.
1486
- *
1487
- * `skipQueue` пропускает запрос мимо очереди — так поступают служебные запросы, которые
1488
- * порождены изнутри другого запроса и не могут ждать освободившегося слота.
1489
- *
1490
- * Очередь выбирается по запросу: у каждого сервиса платформы свой хост и свой лимит.
1491
- */
1492
- function createQueueMiddleware(schedule) {
1493
- return (request, next) => request.skipQueue ? next(request) : schedule(request, () => next(request));
1494
- }
1495
- /**
1496
- * Слой плагинов.
1497
- *
1498
- * Стоит снаружи повторов и внутри очереди: плагин должен увидеть запрос и ответ по одному
1499
- * разу, независимо от числа попыток, — иначе, например, текст поста зашифруется дважды.
1500
- */
1501
- function createPluginsMiddleware(plugins) {
1502
- return (request, next) => plugins.run(request, next);
1503
- }
1504
- /**
1505
- * Слой сервисов.
1506
- *
1507
- * Запросу с полем `service` подставляет хост сервиса, его заголовки и `skipAuth`, если
1508
- * сервис объявлен публичным. Заданный у запроса `baseUrl` не трогает.
1509
- *
1510
- * Стоит снаружи повторов и авторизации, чтобы выставленный здесь `skipAuth` был ей виден.
1511
- */
1512
- function createServicesMiddleware(registry) {
1513
- return async (request, next) => {
1514
- const service = request.service === void 0 ? void 0 : registry.require(request.service);
1515
- let prepared = request;
1516
- if (request.baseUrl !== void 0) {
1517
- const baseUrl = normalizeBaseUrl(request.baseUrl);
1518
- if (baseUrl !== request.baseUrl) prepared = {
1519
- ...prepared,
1520
- baseUrl
1521
- };
1522
- if (!(service?.baseUrl === baseUrl ? service.auth !== false : registry.isPrimarySite(baseUrl)) && prepared.skipAuth === void 0) prepared = {
1523
- ...prepared,
1524
- skipAuth: true
1525
- };
1526
- }
1527
- if (!service) return next(prepared);
1528
- if (prepared.baseUrl === void 0) prepared = {
1529
- ...prepared,
1530
- baseUrl: service.baseUrl
1531
- };
1532
- if (service.headers) prepared = withLayerHeaders(prepared, service.headers);
1533
- if (service.auth === false && prepared.skipAuth === void 0) prepared = {
1534
- ...prepared,
1535
- skipAuth: true
1536
- };
1537
- return next(prepared);
1538
- };
1539
- }
1540
- async function applyAuth(request, deps) {
1541
- if (request.skipAuth) return request;
1542
- const headers = await deps.getAuthHeaders();
1543
- return Object.keys(headers).length > 0 ? withLayerHeaders(request, headers) : request;
1544
- }
1545
- /**
1546
- * Слой авторизации.
1547
- *
1548
- * Подставляет заголовок `Authorization` и обрабатывает `401`: обновляет токен и повторяет
1549
- * запрос ровно один раз. Стоит внутри повторов, поэтому обычным попыткам он не виден —
1550
- * они уже работают со свежим токеном.
1551
- */
1552
- function createAuthMiddleware(deps) {
1553
- return async (request, next) => {
1554
- const authorized = await applyAuth(request, deps);
1555
- try {
1556
- return await next(authorized);
1557
- } catch (error) {
1558
- if (request.skipAuthRefresh || !deps.autoRefresh || !isItdApiError(error) || error.status !== 401) throw error;
1559
- if (!await deps.onUnauthorized()) throw error;
1560
- return next(await applyAuth({
1561
- ...request,
1562
- skipAuthRefresh: true
1563
- }, deps));
1564
- }
1565
- };
1566
- }
1567
- /**
1568
- * Выбирает планировщик отката для конкретного запроса.
1569
- *
1570
- * `retry` у запроса переопределяет глобальную настройку: `false` выключает повторы,
1571
- * объект задаёт свои. Обработка `429` от этого не зависит — она общая.
1572
- */
1573
- function resolveBackoff(retry, global) {
1574
- if (retry === void 0) return global;
1575
- if (retry === false) return void 0;
1576
- const resolved = resolveRetry(retry);
1577
- return resolved ? createRetryScheduler(resolved) : void 0;
1578
- }
1579
- /**
1580
- * Слой повторов.
1581
- *
1582
- * Ответ `429` обрабатывается отдельно от прочих ошибок лестницей пауз и с придержанием
1583
- * всей очереди; сетевые сбои и `5xx` — экспоненциальным откатом. Настройка `retry`
1584
- * у отдельного запроса имеет приоритет над глобальной.
1585
- */
1586
- function createRetryMiddleware(deps) {
1587
- const globalScheduler = deps.retry ? createRetryScheduler(deps.retry) : void 0;
1588
- const nextDelay = (error, attempt, request, method, backoff) => {
1589
- if (isItdRateLimitError(error)) {
1590
- const wait = error.retryAfter ?? deps.rateLimitDelays[attempt - 1];
1591
- if (wait === void 0) return void 0;
1592
- deps.pauseQueue?.(wait, request);
1593
- deps.logger?.debug(`лимит частоты, попытка ${attempt + 1} через ${wait} мс`);
1594
- return wait;
1595
- }
1596
- return backoff?.(error, attempt, method, request.retryNetworkWrite ?? false);
1597
- };
1598
- return async (request, next) => {
1599
- const method = request.method.toUpperCase();
1600
- const backoff = resolveBackoff(request.retry, globalScheduler);
1601
- for (let attempt = 1;; attempt++) try {
1602
- return await next({
1603
- ...request,
1604
- attempt
1605
- });
1606
- } catch (error) {
1607
- const delay = nextDelay(error, attempt, request, method, backoff);
1608
- if (delay === void 0) throw error;
1609
- await dispatchRequestHook(deps.hooks, "onRetry", {
1610
- method,
1611
- path: request.path,
1612
- url: deps.buildUrl(request),
1613
- headers: new Headers({
1614
- ...request.layerHeaders,
1615
- ...request.headers
1616
- }),
1617
- attempt,
1618
- error,
1619
- delay
1620
- }, request);
1621
- deps.logger?.debug(`повтор ${method} ${request.path}, попытка ${attempt + 1} через ${delay} мс`);
1622
- await sleep(delay, request.signal);
1623
- }
1624
- };
1625
- }
1626
- //#endregion
1627
1649
  //#region src/core/rate-limit.ts
1628
1650
  /** Ошибка отмены запроса, который ещё не дошёл до транспорта. */
1629
1651
  function queueAbortError() {
@@ -1648,10 +1670,12 @@ var RequestQueue = class {
1648
1670
  #active = 0;
1649
1671
  /** Момент, раньше которого следующий запрос стартовать не должен. */
1650
1672
  #nextSlot = 0;
1651
- #timer;
1652
- constructor(options) {
1673
+ #clock;
1674
+ #cancelTimer;
1675
+ constructor(options, clock = systemClock) {
1653
1676
  this.#concurrency = options.concurrency;
1654
1677
  this.#minGap = options.rps ? 1e3 / options.rps : 0;
1678
+ this.#clock = clock;
1655
1679
  }
1656
1680
  /** Сколько задач выполняется прямо сейчас. */
1657
1681
  get active() {
@@ -1703,9 +1727,9 @@ var RequestQueue = class {
1703
1727
  * ошибкой `ItdAbortError`. Уже выполняющиеся задачи доводятся до конца.
1704
1728
  */
1705
1729
  stop() {
1706
- if (this.#timer !== void 0) {
1707
- clearTimeout(this.#timer);
1708
- this.#timer = void 0;
1730
+ if (this.#cancelTimer) {
1731
+ this.#cancelTimer();
1732
+ this.#cancelTimer = void 0;
1709
1733
  }
1710
1734
  this.#nextSlot = 0;
1711
1735
  const pending = this.#waiting.splice(0, this.#waiting.length);
@@ -1719,23 +1743,23 @@ var RequestQueue = class {
1719
1743
  */
1720
1744
  pause(ms) {
1721
1745
  if (ms <= 0) return;
1722
- this.#nextSlot = Math.max(this.#nextSlot, Date.now() + ms);
1746
+ this.#nextSlot = Math.max(this.#nextSlot, this.#clock.now() + ms);
1723
1747
  }
1724
1748
  /** Запускает столько ожидающих задач, сколько позволяют ограничения. */
1725
1749
  #drain() {
1726
1750
  if (this.#waiting.length === 0) {
1727
- if (this.#timer !== void 0) {
1728
- clearTimeout(this.#timer);
1729
- this.#timer = void 0;
1751
+ if (this.#cancelTimer) {
1752
+ this.#cancelTimer();
1753
+ this.#cancelTimer = void 0;
1730
1754
  }
1731
1755
  return;
1732
1756
  }
1733
1757
  if (this.#active >= this.#concurrency) return;
1734
- if (this.#timer !== void 0) return;
1735
- const now = Date.now();
1758
+ if (this.#cancelTimer) return;
1759
+ const now = this.#clock.now();
1736
1760
  if (this.#nextSlot > now) {
1737
- this.#timer = setTimeout(() => {
1738
- this.#timer = void 0;
1761
+ this.#cancelTimer = this.#clock.schedule(() => {
1762
+ this.#cancelTimer = void 0;
1739
1763
  this.#drain();
1740
1764
  }, this.#nextSlot - now);
1741
1765
  return;
@@ -1754,19 +1778,21 @@ var RequestQueue = class {
1754
1778
  */
1755
1779
  var RequestQueuePool = class {
1756
1780
  #options;
1781
+ #clock;
1757
1782
  #main;
1758
1783
  /** Очереди сервисов заводятся при первом запросе — обычно не нужна ни одна. */
1759
1784
  #byService = /* @__PURE__ */ new Map();
1760
- constructor(options) {
1785
+ constructor(options, clock = systemClock) {
1761
1786
  this.#options = options;
1762
- this.#main = new RequestQueue(options);
1787
+ this.#clock = clock;
1788
+ this.#main = new RequestQueue(options, clock);
1763
1789
  }
1764
1790
  /** Очередь хоста. */
1765
1791
  for(service) {
1766
1792
  if (service === void 0) return this.#main;
1767
1793
  let queue = this.#byService.get(service);
1768
1794
  if (!queue) {
1769
- queue = new RequestQueue(this.#options);
1795
+ queue = new RequestQueue(this.#options, this.#clock);
1770
1796
  this.#byService.set(service, queue);
1771
1797
  }
1772
1798
  return queue;
@@ -2214,7 +2240,7 @@ function createApiError(context) {
2214
2240
  path: context.path,
2215
2241
  raw: safeRawBody(context.body),
2216
2242
  response: context.response,
2217
- retryAfter: parseRetryAfter(context.headers?.get("retry-after"))
2243
+ retryAfter: parseRetryAfter(context.headers?.get("retry-after"), context.now)
2218
2244
  };
2219
2245
  if (parsed.code === "PHONE_VERIFICATION_REQUIRED") return new ItdPhoneVerificationError({
2220
2246
  ...init,
@@ -2290,13 +2316,13 @@ function abortable(promise, signal) {
2290
2316
  * Реализовано вручную, а не через `AbortSignal.any`: последний появился только в Node 20,
2291
2317
  * а библиотека поддерживает Node 18.
2292
2318
  */
2293
- function createAbortBundle(userSignal, timeout) {
2319
+ function createAbortBundle(userSignal, timeout, clock) {
2294
2320
  const controller = new AbortController();
2295
2321
  let timedOut = false;
2296
2322
  const onUserAbort = () => controller.abort(userSignal?.reason);
2297
2323
  if (userSignal) if (userSignal.aborted) controller.abort(userSignal.reason);
2298
2324
  else userSignal.addEventListener("abort", onUserAbort, { once: true });
2299
- const timer = timeout > 0 ? setTimeout(() => {
2325
+ const cancelTimer = timeout > 0 ? clock.schedule(() => {
2300
2326
  timedOut = true;
2301
2327
  controller.abort();
2302
2328
  }, timeout) : void 0;
@@ -2304,7 +2330,7 @@ function createAbortBundle(userSignal, timeout) {
2304
2330
  signal: controller.signal,
2305
2331
  timedOut: () => timedOut,
2306
2332
  cleanup: () => {
2307
- if (timer !== void 0) clearTimeout(timer);
2333
+ cancelTimer?.();
2308
2334
  userSignal?.removeEventListener("abort", onUserAbort);
2309
2335
  }
2310
2336
  };
@@ -2341,8 +2367,8 @@ var Transport = class {
2341
2367
  const headers = await this.#buildHeaders(request, url);
2342
2368
  const attempt = request.attempt ?? 1;
2343
2369
  const timeout = request.timeout ?? this.#config.timeout;
2344
- const abort = createAbortBundle(request.signal, timeout);
2345
- const startedAt = Date.now();
2370
+ const abort = createAbortBundle(request.signal, timeout, this.#config.clock);
2371
+ const startedAt = this.#config.clock.now();
2346
2372
  let cleanupBody;
2347
2373
  try {
2348
2374
  let body;
@@ -2361,7 +2387,7 @@ var Transport = class {
2361
2387
  };
2362
2388
  await dispatchRequestHook(this.#config.hooks, "onError", {
2363
2389
  ...context,
2364
- duration: Date.now() - startedAt,
2390
+ duration: this.#config.clock.now() - startedAt,
2365
2391
  error: failure
2366
2392
  }, request);
2367
2393
  throw failure;
@@ -2390,7 +2416,7 @@ var Transport = class {
2390
2416
  if (typeof ReadableStream !== "undefined" && body instanceof ReadableStream) init.duplex = "half";
2391
2417
  response = await this.#config.fetch(url, init);
2392
2418
  } catch (error) {
2393
- const duration = Date.now() - startedAt;
2419
+ const duration = this.#config.clock.now() - startedAt;
2394
2420
  const failure = this.#toTransportError(error, abort, request, method, timeout);
2395
2421
  await dispatchRequestHook(this.#config.hooks, "onError", {
2396
2422
  ...context,
@@ -2408,16 +2434,17 @@ var Transport = class {
2408
2434
  if (response.ok) await dispatchRequestHook(this.#config.hooks, "onResponse", {
2409
2435
  ...context,
2410
2436
  status: response.status,
2411
- duration: Date.now() - startedAt,
2437
+ duration: this.#config.clock.now() - startedAt,
2412
2438
  response
2413
2439
  }, request);
2414
2440
  const payload = await this.#readBodyOrFail(response, context, request, method, abort, timeout);
2415
- const duration = Date.now() - startedAt;
2441
+ const duration = this.#config.clock.now() - startedAt;
2416
2442
  if (!response.ok) {
2417
2443
  const error = createApiError({
2418
2444
  method,
2419
2445
  path: request.path,
2420
2446
  status: response.status,
2447
+ now: this.#config.clock.now(),
2421
2448
  statusText: response.statusText,
2422
2449
  headers: response.headers,
2423
2450
  response,
@@ -2486,7 +2513,7 @@ var Transport = class {
2486
2513
  }
2487
2514
  /** Читает тело и преобразует ошибку чтения в транспортную ошибку библиотеки. */
2488
2515
  async #readBodyOrFail(response, context, request, method, abort, timeout) {
2489
- const startedAt = Date.now();
2516
+ const startedAt = this.#config.clock.now();
2490
2517
  try {
2491
2518
  return await abortable(readBody(response), abort.signal);
2492
2519
  } catch (error) {
@@ -2494,7 +2521,7 @@ var Transport = class {
2494
2521
  const failure = this.#toTransportError(error, abort, request, method, timeout);
2495
2522
  await dispatchRequestHook(this.#config.hooks, "onError", {
2496
2523
  ...context,
2497
- duration: Date.now() - startedAt,
2524
+ duration: this.#config.clock.now() - startedAt,
2498
2525
  error: failure
2499
2526
  }, request);
2500
2527
  this.#config.logger?.warn(`× ${method} ${request.path}: не удалось прочитать тело ответа — ${failure.message}`);
@@ -2815,144 +2842,167 @@ const ItdErrorCode = Object.freeze({
2815
2842
  WRITE_ACCESS_RESTRICTED: "WRITE_ACCESS_RESTRICTED"
2816
2843
  });
2817
2844
  //#endregion
2818
- //#region src/notifications/type-map.ts
2819
- /**
2820
- * Соответствие коротких имён типов уведомлений развёрнутым.
2821
- *
2822
- * Сервер и в списке, и в потоке событий — присылает короткие имена: `like`, `comment`,
2823
- * `reply`, `repost`, `comment_like`. Развёрнутые (`post_reaction`, `post_comment`)
2824
- * встречаются в оформлении интерфейса, поэтому библиотека приводит типы к ним:
2825
- * они однозначно называют и объект, и действие.
2826
- *
2827
- * Пришедшее значение всегда остаётся в поле `rawType`.
2828
- */
2829
- const NOTIFICATION_TYPE_ALIASES = Object.freeze({
2830
- like: NotificationType.PostReaction,
2831
- comment: NotificationType.PostComment,
2832
- comment_like: NotificationType.CommentReaction,
2833
- reply: NotificationType.CommentReply,
2834
- repost: NotificationType.PostRepost,
2835
- mention: NotificationType.PostMention
2836
- });
2837
- const KNOWN_TYPES = new Set(Object.values(NotificationType));
2838
- /**
2839
- * Приводит имя типа к каноническому.
2840
- *
2841
- * Неизвестное значение возвращается без изменений, чтобы не менять смысл нового типа
2842
- * уведомления на другой.
2843
- *
2844
- * @example
2845
- * ```ts
2846
- * canonicalNotificationType('like'); // 'post_reaction'
2847
- * canonicalNotificationType('post_reaction'); // 'post_reaction'
2848
- * canonicalNotificationType('новое_событие'); // 'новое_событие'
2849
- * ```
2850
- */
2851
- function canonicalNotificationType(rawType) {
2852
- return NOTIFICATION_TYPE_ALIASES[rawType] ?? rawType;
2853
- }
2854
- /**
2855
- * Известен ли библиотеке этот тип уведомления.
2856
- *
2857
- * Полезно, чтобы решить, показывать ли уведомление, для которого нет своего оформления.
2858
- */
2859
- function isKnownNotificationType(type) {
2860
- return KNOWN_TYPES.has(canonicalNotificationType(type));
2861
- }
2862
- //#endregion
2863
- //#region src/notifications/normalize.ts
2864
- function asActor(value) {
2865
- if (!isRecord(value)) return void 0;
2866
- const id = asString(value.id);
2867
- if (!id) return void 0;
2868
- return {
2869
- id,
2870
- username: asString(value.username) ?? "",
2871
- displayName: asString(value.displayName) ?? "",
2872
- avatar: asString(value.avatar) ?? "",
2873
- ...typeof value.isFollowing === "boolean" ? { isFollowing: value.isFollowing } : {},
2874
- ...typeof value.isFollowedBy === "boolean" ? { isFollowedBy: value.isFollowedBy } : {}
2875
- };
2876
- }
2877
- /** Собирает участников: сервер присылает либо одного `actor`, либо массив `actors`. */
2878
- function readActors(source) {
2879
- if (Array.isArray(source.actors)) return source.actors.map(asActor).filter((actor) => actor !== void 0);
2880
- const single = asActor(source.actor);
2881
- return single ? [single] : [];
2882
- }
2883
- /**
2884
- * Приводит уведомление к единой форме.
2885
- *
2886
- * Нужна потому, что REST-список и поток событий описывают одно и то же событие по-разному:
2887
- * различаются имена типов (`like` против `post_reaction`), имена полей
2888
- * (`targetId`/`entityId`, `read`/`isRead`, `preview`/`entityPreview`) и число участников
2889
- * (`actor` против массива `actors`). После приведения объекты из обоих источников
2890
- * можно складывать в один список.
2891
- *
2892
- * Исходные данные не теряются: имя типа с сервера остаётся в `rawType`,
2893
- * весь объект целиком — в `raw`.
2894
- *
2895
- * @param input уведомление из REST-ответа либо полезная нагрузка события потока
2896
- *
2897
- * @example
2898
- * ```ts
2899
- * const fromRest = normalizeNotification(restItem);
2900
- * const fromStream = normalizeNotification(event.payload);
2901
- * // одинаковая форма — можно объединять
2902
- * ```
2903
- */
2904
- function normalizeNotification(input) {
2905
- const source = isRecord(input) ? input : {};
2906
- const payload = isRecord(source.payload) ? source.payload : source;
2907
- const rawType = asString(payload.type) ?? asString(source.type) ?? "";
2908
- const createdAt = asString(payload.createdAt) ?? asString(source.createdAt) ?? "";
2909
- const readAt = asString(payload.readAt) ?? asString(source.readAt);
2910
- const isRead = typeof payload.isRead === "boolean" ? payload.isRead : typeof payload.read === "boolean" ? payload.read : Boolean(readAt);
2911
- const subjectId = asString(payload.subjectId);
2912
- const targetId = asString(payload.targetId);
2913
- const subjectIsComment = payload.subjectType === "comment";
2914
- const clickUrl = asString(payload.clickUrl);
2915
- return {
2916
- id: asString(payload.id) ?? asString(source.id) ?? "",
2917
- type: canonicalNotificationType(rawType),
2918
- rawType,
2919
- entityId: asString(payload.entityId) ?? (subjectIsComment ? subjectId ?? targetId : targetId) ?? null,
2920
- parentEntityId: asString(payload.parentEntityId) ?? (subjectIsComment ? targetId ?? null : null),
2921
- isRead,
2922
- actors: readActors(payload),
2923
- count: typeof payload.count === "number" && payload.count > 0 ? payload.count : 1,
2924
- preview: asString(payload.entityPreview) ?? asString(payload.preview) ?? null,
2925
- ...clickUrl ? { clickUrl } : {},
2926
- createdAt,
2927
- updatedAt: asString(payload.updatedAt) ?? readAt ?? createdAt,
2928
- raw: input
2929
- };
2930
- }
2931
- /**
2932
- * Разбирает событие `notification` из потока.
2933
- *
2934
- * Кроме самого уведомления событие несёт служебные поля уровня конверта: актуальный
2935
- * счётчик непрочитанных и признак звука.
2936
- */
2937
- function readNotificationEvent(data) {
2938
- const source = isRecord(data) ? data : {};
2939
- return {
2940
- notification: normalizeNotification(data),
2941
- unreadCount: typeof source.unreadCount === "number" ? source.unreadCount : void 0,
2942
- sound: source.sound === true
2845
+ //#region src/realtime/middleware.ts
2846
+ const REALTIME_MIDDLEWARE_SNAPSHOT = Symbol("itd-api.realtime.middlewareSnapshot");
2847
+ /** @internal */
2848
+ function captureRealtimeMiddleware(middleware) {
2849
+ const capture = middleware[REALTIME_MIDDLEWARE_SNAPSHOT];
2850
+ if (!capture) return middleware;
2851
+ const snapshot = capture();
2852
+ if (typeof snapshot !== "function") throw new TypeError("Снимок промежуточного обработчика должен быть функцией");
2853
+ return snapshot;
2854
+ }
2855
+ /** @internal */
2856
+ function withRealtimeMiddlewareSnapshot(middleware, capture) {
2857
+ Object.defineProperty(middleware, REALTIME_MIDDLEWARE_SNAPSHOT, { value: capture });
2858
+ return middleware;
2859
+ }
2860
+ /** Выполняет промежуточные обработчики по порядку и запрещает повторный вызов `next()`. */
2861
+ async function runRealtimeMiddleware(middleware, context, terminal) {
2862
+ let lastIndex = -1;
2863
+ const dispatch = async (index) => {
2864
+ if (index <= lastIndex) throw new Error("next() в промежуточном обработчике вызван повторно");
2865
+ lastIndex = index;
2866
+ const current = middleware[index];
2867
+ if (!current) {
2868
+ await terminal();
2869
+ return;
2870
+ }
2871
+ let downstream;
2872
+ let duplicateCalls;
2873
+ let failure;
2874
+ const next = () => {
2875
+ if (!downstream) {
2876
+ downstream = dispatch(index + 1);
2877
+ return downstream;
2878
+ }
2879
+ const duplicate = Promise.reject(/* @__PURE__ */ new Error("next() в одном промежуточном обработчике вызван повторно"));
2880
+ duplicateCalls = Promise.all(duplicateCalls ? [duplicateCalls, duplicate] : [duplicate]).then(() => void 0);
2881
+ return duplicate;
2882
+ };
2883
+ try {
2884
+ await current(context, next);
2885
+ } catch (error) {
2886
+ failure = { error };
2887
+ }
2888
+ try {
2889
+ await downstream;
2890
+ await duplicateCalls;
2891
+ } catch (error) {
2892
+ failure ??= { error };
2893
+ }
2894
+ if (failure) throw failure.error;
2943
2895
  };
2896
+ await dispatch(0);
2944
2897
  }
2945
- /**
2946
- * Разбирает событие `unread_count` из потока.
2947
- *
2948
- * Возвращает `undefined`, если сервер прислал событие без вложенного `payload`.
2949
- */
2950
- function readUnreadCountEvent(data) {
2951
- if (!isRecord(data)) return void 0;
2952
- const payload = isRecord(data.payload) ? data.payload : void 0;
2953
- if (!payload) return void 0;
2954
- return typeof payload.count === "number" ? payload.count : void 0;
2955
- }
2898
+ /** Планирует нормализованные обновления и отслеживает незавершённые обработчики. */
2899
+ var RealtimeDispatcher = class {
2900
+ #options;
2901
+ #hooks;
2902
+ #middleware = [];
2903
+ #handlers = [];
2904
+ #queue = [];
2905
+ #activeKeys = /* @__PURE__ */ new Set();
2906
+ #drainWaiters = /* @__PURE__ */ new Set();
2907
+ #active = 0;
2908
+ constructor(options, hooks) {
2909
+ this.#options = options;
2910
+ this.#hooks = hooks;
2911
+ }
2912
+ use(middleware) {
2913
+ this.#middleware.push(middleware);
2914
+ return () => {
2915
+ const index = this.#middleware.indexOf(middleware);
2916
+ if (index >= 0) this.#middleware.splice(index, 1);
2917
+ };
2918
+ }
2919
+ on(predicate, handler) {
2920
+ const registration = {
2921
+ predicate,
2922
+ handler
2923
+ };
2924
+ this.#handlers.push(registration);
2925
+ return () => {
2926
+ const index = this.#handlers.indexOf(registration);
2927
+ if (index >= 0) this.#handlers.splice(index, 1);
2928
+ };
2929
+ }
2930
+ dispatch(context) {
2931
+ let keys;
2932
+ let middleware;
2933
+ try {
2934
+ keys = this.#keysFor(context);
2935
+ middleware = this.#middleware.map(captureRealtimeMiddleware);
2936
+ } catch (error) {
2937
+ this.#hooks.middlewareError(error, context);
2938
+ return;
2939
+ }
2940
+ this.#queue.push({
2941
+ context,
2942
+ middleware,
2943
+ handlers: [...this.#handlers],
2944
+ keys
2945
+ });
2946
+ this.#pump();
2947
+ }
2948
+ /** Отбрасывает обновления, обработка которых ещё не началась. */
2949
+ clearPending() {
2950
+ this.#queue.length = 0;
2951
+ this.#resolveDrain();
2952
+ }
2953
+ /** Ждёт завершения активных и поставленных в очередь обновлений. */
2954
+ drain() {
2955
+ if (this.#active === 0 && this.#queue.length === 0) return Promise.resolve();
2956
+ return new Promise((resolve) => this.#drainWaiters.add(resolve));
2957
+ }
2958
+ #keysFor(context) {
2959
+ const value = this.#options.sequentialize?.(context);
2960
+ if (value === void 0) return [];
2961
+ const values = Array.isArray(value) ? value : [value];
2962
+ for (const key of values) if (typeof key !== "string" && typeof key !== "number" && typeof key !== "symbol") throw new TypeError("sequentialize() должен возвращать PropertyKey или их список");
2963
+ return [...new Set(values)];
2964
+ }
2965
+ #pump() {
2966
+ while (this.#active < this.#options.concurrency) {
2967
+ const blockedKeys = new Set(this.#activeKeys);
2968
+ const index = this.#queue.findIndex(({ keys }) => {
2969
+ const runnable = keys.every((key) => !blockedKeys.has(key));
2970
+ if (!runnable) for (const key of keys) blockedKeys.add(key);
2971
+ return runnable;
2972
+ });
2973
+ if (index < 0) break;
2974
+ const [work] = this.#queue.splice(index, 1);
2975
+ if (!work) break;
2976
+ this.#active += 1;
2977
+ for (const key of work.keys) this.#activeKeys.add(key);
2978
+ this.#run(work);
2979
+ }
2980
+ this.#resolveDrain();
2981
+ }
2982
+ async #run(work) {
2983
+ try {
2984
+ await runRealtimeMiddleware(work.middleware, work.context, async () => {
2985
+ for (const { predicate, handler } of work.handlers) try {
2986
+ if (predicate(work.context)) await handler(work.context);
2987
+ } catch (error) {
2988
+ this.#hooks.handlerError(error, work.context);
2989
+ }
2990
+ this.#hooks.deliver(work.context);
2991
+ });
2992
+ } catch (error) {
2993
+ this.#hooks.middlewareError(error, work.context);
2994
+ } finally {
2995
+ this.#active -= 1;
2996
+ for (const key of work.keys) this.#activeKeys.delete(key);
2997
+ this.#pump();
2998
+ }
2999
+ }
3000
+ #resolveDrain() {
3001
+ if (this.#active !== 0 || this.#queue.length !== 0) return;
3002
+ for (const resolve of this.#drainWaiters) resolve();
3003
+ this.#drainWaiters.clear();
3004
+ }
3005
+ };
2956
3006
  //#endregion
2957
3007
  //#region src/realtime/transport.ts
2958
3008
  /** Ошибка, по которой видно, что сервер отверг авторизацию потока. */
@@ -2978,7 +3028,9 @@ var PollTransport = class {
2978
3028
  name = "poll";
2979
3029
  #interval;
2980
3030
  #limit;
3031
+ #clock;
2981
3032
  constructor(options = {}) {
3033
+ this.#clock = options.clock ?? systemClock;
2982
3034
  this.#interval = options.interval ?? 15e3;
2983
3035
  this.#limit = options.limit ?? 20;
2984
3036
  }
@@ -3046,9 +3098,9 @@ var PollTransport = class {
3046
3098
  #wait(signal) {
3047
3099
  if (signal.aborted) return Promise.resolve();
3048
3100
  return new Promise((resolve) => {
3049
- const timer = setTimeout(finish, this.#interval);
3101
+ const cancel = this.#clock.schedule(finish, this.#interval);
3050
3102
  function finish() {
3051
- clearTimeout(timer);
3103
+ cancel();
3052
3104
  signal.removeEventListener("abort", finish);
3053
3105
  resolve();
3054
3106
  }
@@ -3285,9 +3337,11 @@ var SseTransport = class {
3285
3337
  name = "sse";
3286
3338
  #idleTimeout;
3287
3339
  #handshakeTimeout;
3340
+ #clock;
3288
3341
  /** Идентификатор последнего события — отправляется при переподключении. */
3289
3342
  #lastEventId;
3290
3343
  constructor(options = {}) {
3344
+ this.#clock = options.clock ?? systemClock;
3291
3345
  this.#idleTimeout = options.idleTimeout ?? 9e4;
3292
3346
  this.#handshakeTimeout = options.handshakeTimeout ?? 2e4;
3293
3347
  }
@@ -3315,66 +3369,274 @@ var SseTransport = class {
3315
3369
  context.signal.removeEventListener("abort", relayAbort);
3316
3370
  }
3317
3371
  }
3318
- /** Выполняет запрос потока, обрывая его, если ответ не пришёл за отведённое время. */
3319
- async #handshake(url, headers, context, controller) {
3320
- let expired = false;
3321
- const timer = this.#handshakeTimeout > 0 ? setTimeout(() => {
3322
- expired = true;
3323
- controller.abort(/* @__PURE__ */ new Error("Истёк таймаут рукопожатия SSE"));
3324
- }, this.#handshakeTimeout) : void 0;
3325
- try {
3326
- return await context.fetch(url, {
3327
- method: "GET",
3328
- headers,
3329
- signal: controller.signal
3330
- });
3331
- } catch (error) {
3332
- if (expired) throw new Error("Поток уведомлений не ответил: истёк таймаут рукопожатия");
3333
- throw error;
3334
- } finally {
3335
- if (timer !== void 0) clearTimeout(timer);
3336
- }
3372
+ /** Выполняет запрос потока, обрывая его, если ответ не пришёл за отведённое время. */
3373
+ async #handshake(url, headers, context, controller) {
3374
+ let expired = false;
3375
+ const cancelTimer = this.#handshakeTimeout > 0 ? this.#clock.schedule(() => {
3376
+ expired = true;
3377
+ controller.abort(/* @__PURE__ */ new Error("Истёк таймаут рукопожатия SSE"));
3378
+ }, this.#handshakeTimeout) : void 0;
3379
+ try {
3380
+ return await context.fetch(url, {
3381
+ method: "GET",
3382
+ headers,
3383
+ signal: controller.signal
3384
+ });
3385
+ } catch (error) {
3386
+ if (expired) throw new Error("Поток уведомлений не ответил: истёк таймаут рукопожатия");
3387
+ throw error;
3388
+ } finally {
3389
+ cancelTimer?.();
3390
+ }
3391
+ }
3392
+ async #read(body, context) {
3393
+ const reader = body.getReader();
3394
+ const decoder = new TextDecoder();
3395
+ const parser = createParser({ onEvent: (message) => {
3396
+ if (message.id) this.#lastEventId = message.id;
3397
+ let data;
3398
+ try {
3399
+ data = JSON.parse(message.data);
3400
+ } catch (error) {
3401
+ context.onParseError(error, message.data);
3402
+ return;
3403
+ }
3404
+ const name = message.event ?? (typeof data === "object" && data !== null && "type" in data ? String(data.type) : "message");
3405
+ context.onEvent({
3406
+ name,
3407
+ data
3408
+ });
3409
+ } });
3410
+ let cancelIdleTimer;
3411
+ const armIdleTimer = () => {
3412
+ if (this.#idleTimeout <= 0) return;
3413
+ cancelIdleTimer?.();
3414
+ cancelIdleTimer = this.#clock.schedule(() => {
3415
+ reader.cancel(/* @__PURE__ */ new Error("Поток молчит дольше допустимого")).catch(() => {});
3416
+ }, this.#idleTimeout);
3417
+ };
3418
+ armIdleTimer();
3419
+ try {
3420
+ for (;;) {
3421
+ const { done, value } = await reader.read();
3422
+ if (done) break;
3423
+ armIdleTimer();
3424
+ parser.feed(decoder.decode(value, { stream: true }));
3425
+ }
3426
+ } finally {
3427
+ cancelIdleTimer?.();
3428
+ reader.releaseLock?.();
3429
+ }
3430
+ }
3431
+ };
3432
+ //#endregion
3433
+ //#region src/notifications/type-map.ts
3434
+ /**
3435
+ * Соответствие коротких имён типов уведомлений развёрнутым.
3436
+ *
3437
+ * Сервер — и в списке, и в потоке событий — присылает короткие имена: `like`, `comment`,
3438
+ * `reply`, `repost`, `comment_like`. Развёрнутые (`post_reaction`, `post_comment`)
3439
+ * встречаются в оформлении интерфейса, поэтому библиотека приводит типы к ним:
3440
+ * они однозначно называют и объект, и действие.
3441
+ *
3442
+ * Пришедшее значение всегда остаётся в поле `rawType`.
3443
+ */
3444
+ const NOTIFICATION_TYPE_ALIASES = Object.freeze({
3445
+ like: NotificationType.PostReaction,
3446
+ comment: NotificationType.PostComment,
3447
+ comment_like: NotificationType.CommentReaction,
3448
+ reply: NotificationType.CommentReply,
3449
+ repost: NotificationType.PostRepost,
3450
+ mention: NotificationType.PostMention
3451
+ });
3452
+ const KNOWN_TYPES = new Set(Object.values(NotificationType));
3453
+ /**
3454
+ * Приводит имя типа к каноническому.
3455
+ *
3456
+ * Неизвестное значение возвращается без изменений, чтобы не менять смысл нового типа
3457
+ * уведомления на другой.
3458
+ *
3459
+ * @example
3460
+ * ```ts
3461
+ * canonicalNotificationType('like'); // 'post_reaction'
3462
+ * canonicalNotificationType('post_reaction'); // 'post_reaction'
3463
+ * canonicalNotificationType('новое_событие'); // 'новое_событие'
3464
+ * ```
3465
+ */
3466
+ function canonicalNotificationType(rawType) {
3467
+ return NOTIFICATION_TYPE_ALIASES[rawType] ?? rawType;
3468
+ }
3469
+ /**
3470
+ * Известен ли библиотеке этот тип уведомления.
3471
+ *
3472
+ * Полезно, чтобы решить, показывать ли уведомление, для которого нет своего оформления.
3473
+ */
3474
+ function isKnownNotificationType(type) {
3475
+ return KNOWN_TYPES.has(canonicalNotificationType(type));
3476
+ }
3477
+ //#endregion
3478
+ //#region src/notifications/normalize.ts
3479
+ function asActor(value) {
3480
+ if (!isRecord(value)) return void 0;
3481
+ const id = asString(value.id);
3482
+ if (!id) return void 0;
3483
+ return {
3484
+ id,
3485
+ username: asString(value.username) ?? "",
3486
+ displayName: asString(value.displayName) ?? "",
3487
+ avatar: asString(value.avatar) ?? "",
3488
+ ...typeof value.isFollowing === "boolean" ? { isFollowing: value.isFollowing } : {},
3489
+ ...typeof value.isFollowedBy === "boolean" ? { isFollowedBy: value.isFollowedBy } : {}
3490
+ };
3491
+ }
3492
+ /** Собирает участников: сервер присылает либо одного `actor`, либо массив `actors`. */
3493
+ function readActors(source) {
3494
+ if (Array.isArray(source.actors)) return source.actors.map(asActor).filter((actor) => actor !== void 0);
3495
+ const single = asActor(source.actor);
3496
+ return single ? [single] : [];
3497
+ }
3498
+ /**
3499
+ * Приводит уведомление к единой форме.
3500
+ *
3501
+ * Нужна потому, что REST-список и поток событий описывают одно и то же событие по-разному:
3502
+ * различаются имена типов (`like` против `post_reaction`), имена полей
3503
+ * (`targetId`/`entityId`, `read`/`isRead`, `preview`/`entityPreview`) и число участников
3504
+ * (`actor` против массива `actors`). После приведения объекты из обоих источников
3505
+ * можно складывать в один список.
3506
+ *
3507
+ * Исходные данные не теряются: имя типа с сервера остаётся в `rawType`,
3508
+ * весь объект целиком — в `raw`.
3509
+ *
3510
+ * @param input уведомление из REST-ответа либо полезная нагрузка события потока
3511
+ *
3512
+ * @example
3513
+ * ```ts
3514
+ * const fromRest = normalizeNotification(restItem);
3515
+ * const fromStream = normalizeNotification(event.payload);
3516
+ * // одинаковая форма — можно объединять
3517
+ * ```
3518
+ */
3519
+ function normalizeNotification(input) {
3520
+ const source = isRecord(input) ? input : {};
3521
+ const payload = isRecord(source.payload) ? source.payload : source;
3522
+ const rawType = asString(payload.type) ?? asString(source.type) ?? "";
3523
+ const createdAt = asString(payload.createdAt) ?? asString(source.createdAt) ?? "";
3524
+ const readAt = asString(payload.readAt) ?? asString(source.readAt);
3525
+ const isRead = typeof payload.isRead === "boolean" ? payload.isRead : typeof payload.read === "boolean" ? payload.read : Boolean(readAt);
3526
+ const subjectId = asString(payload.subjectId);
3527
+ const targetId = asString(payload.targetId);
3528
+ const subjectIsComment = payload.subjectType === "comment";
3529
+ const clickUrl = asString(payload.clickUrl);
3530
+ return {
3531
+ id: asString(payload.id) ?? asString(source.id) ?? "",
3532
+ type: canonicalNotificationType(rawType),
3533
+ rawType,
3534
+ entityId: asString(payload.entityId) ?? (subjectIsComment ? subjectId ?? targetId : targetId) ?? null,
3535
+ parentEntityId: asString(payload.parentEntityId) ?? (subjectIsComment ? targetId ?? null : null),
3536
+ isRead,
3537
+ actors: readActors(payload),
3538
+ count: typeof payload.count === "number" && payload.count > 0 ? payload.count : 1,
3539
+ preview: asString(payload.entityPreview) ?? asString(payload.preview) ?? null,
3540
+ ...clickUrl ? { clickUrl } : {},
3541
+ createdAt,
3542
+ updatedAt: asString(payload.updatedAt) ?? readAt ?? createdAt,
3543
+ raw: input
3544
+ };
3545
+ }
3546
+ /**
3547
+ * Разбирает событие `notification` из потока.
3548
+ *
3549
+ * Кроме самого уведомления событие несёт служебные поля уровня конверта: актуальный
3550
+ * счётчик непрочитанных и признак звука.
3551
+ */
3552
+ function readNotificationEvent(data) {
3553
+ const source = isRecord(data) ? data : {};
3554
+ return {
3555
+ notification: normalizeNotification(data),
3556
+ unreadCount: typeof source.unreadCount === "number" ? source.unreadCount : void 0,
3557
+ sound: source.sound === true
3558
+ };
3559
+ }
3560
+ /**
3561
+ * Разбирает событие `unread_count` из потока.
3562
+ *
3563
+ * Возвращает `undefined`, если сервер прислал событие без вложенного `payload`.
3564
+ */
3565
+ function readUnreadCountEvent(data) {
3566
+ if (!isRecord(data)) return void 0;
3567
+ const payload = isRecord(data.payload) ? data.payload : void 0;
3568
+ if (!payload) return void 0;
3569
+ return typeof payload.count === "number" ? payload.count : void 0;
3570
+ }
3571
+ //#endregion
3572
+ //#region src/realtime/updates.ts
3573
+ /** Типы нормализованных обновлений потока. */
3574
+ const RealtimeUpdateType = Object.freeze({
3575
+ Notification: "notification",
3576
+ UnreadCount: "unreadCount",
3577
+ Unknown: "unknown"
3578
+ });
3579
+ /** Источники нормализованных обновлений потока. */
3580
+ const RealtimeUpdateOrigin = Object.freeze({
3581
+ Stream: "stream",
3582
+ Sync: "sync"
3583
+ });
3584
+ /** Проверяет форму фильтра уведомлений. */
3585
+ function validateNotificationSelector(selector) {
3586
+ if (typeof selector === "string") {
3587
+ if (selector.length === 0) throw new ItdConfigError("Тип уведомления не должен быть пустым");
3588
+ return;
3589
+ }
3590
+ if (Array.isArray(selector)) {
3591
+ if (selector.length === 0 || selector.some((type) => typeof type !== "string" || !type)) throw new ItdConfigError("Список типов уведомлений должен содержать непустые строки");
3592
+ return;
3337
3593
  }
3338
- async #read(body, context) {
3339
- const reader = body.getReader();
3340
- const decoder = new TextDecoder();
3341
- const parser = createParser({ onEvent: (message) => {
3342
- if (message.id) this.#lastEventId = message.id;
3343
- let data;
3344
- try {
3345
- data = JSON.parse(message.data);
3346
- } catch (error) {
3347
- context.onParseError(error, message.data);
3348
- return;
3349
- }
3350
- const name = message.event ?? (typeof data === "object" && data !== null && "type" in data ? String(data.type) : "message");
3351
- context.onEvent({
3352
- name,
3353
- data
3354
- });
3355
- } });
3356
- let idleTimer;
3357
- const armIdleTimer = () => {
3358
- if (this.#idleTimeout <= 0) return;
3359
- if (idleTimer !== void 0) clearTimeout(idleTimer);
3360
- idleTimer = setTimeout(() => {
3361
- reader.cancel(/* @__PURE__ */ new Error("Поток молчит дольше допустимого")).catch(() => {});
3362
- }, this.#idleTimeout);
3594
+ if (typeof selector !== "object" || selector === null) throw new ItdConfigError("Фильтр уведомлений должен быть строкой, списком или объектом");
3595
+ const filter = selector;
3596
+ if (filter.type !== void 0) validateNotificationSelector(filter.type);
3597
+ if (filter.actorId !== void 0 && typeof filter.actorId !== "string") throw new ItdConfigError("Фильтр уведомлений: actorId должен быть строкой");
3598
+ for (const field of ["entityId", "parentEntityId"]) {
3599
+ const value = filter[field];
3600
+ if (value !== void 0 && value !== null && typeof value !== "string") throw new ItdConfigError(`Фильтр уведомлений: ${field} должен быть строкой или null`);
3601
+ }
3602
+ if (filter.predicate !== void 0 && typeof filter.predicate !== "function") throw new ItdConfigError("Фильтр уведомлений: predicate должен быть функцией");
3603
+ }
3604
+ /** Преобразует транспортный кадр в одно логическое обновление. */
3605
+ function readRealtimeUpdate(event) {
3606
+ if (event.name === "notification") return {
3607
+ type: RealtimeUpdateType.Notification,
3608
+ data: readNotificationEvent(event.data)
3609
+ };
3610
+ if (event.name === "unread_count") {
3611
+ const count = readUnreadCountEvent(event.data);
3612
+ return count === void 0 ? void 0 : {
3613
+ type: RealtimeUpdateType.UnreadCount,
3614
+ data: count
3363
3615
  };
3364
- armIdleTimer();
3365
- try {
3366
- for (;;) {
3367
- const { done, value } = await reader.read();
3368
- if (done) break;
3369
- armIdleTimer();
3370
- parser.feed(decoder.decode(value, { stream: true }));
3371
- }
3372
- } finally {
3373
- if (idleTimer !== void 0) clearTimeout(idleTimer);
3374
- reader.releaseLock?.();
3375
- }
3376
3616
  }
3377
- };
3617
+ return {
3618
+ type: RealtimeUpdateType.Unknown,
3619
+ name: event.name,
3620
+ data: event.data
3621
+ };
3622
+ }
3623
+ /** Проверяет объектный или краткий фильтр уведомления. */
3624
+ function matchesNotification(context, selector) {
3625
+ const notification = context.update.data.notification;
3626
+ if (typeof selector === "string") return notification.type === selector;
3627
+ if (Array.isArray(selector)) return selector.includes(notification.type);
3628
+ const filter = selector;
3629
+ const types = filter.type === void 0 ? void 0 : [filter.type].flat();
3630
+ if (types && !types.includes(notification.type)) return false;
3631
+ if (filter.actorId !== void 0 && !notification.actors.some(({ id }) => id === filter.actorId)) return false;
3632
+ if (filter.entityId !== void 0 && notification.entityId !== filter.entityId) return false;
3633
+ if (filter.parentEntityId !== void 0 && notification.parentEntityId !== filter.parentEntityId) return false;
3634
+ return filter.predicate?.(context) ?? true;
3635
+ }
3636
+ /** Сужает произвольный контекст потока до контекста уведомления. */
3637
+ function isNotificationContext(context) {
3638
+ return context.update.type === RealtimeUpdateType.Notification;
3639
+ }
3378
3640
  //#endregion
3379
3641
  //#region src/realtime/stream.ts
3380
3642
  /** Способ получения событий. */
@@ -3397,6 +3659,8 @@ function validateRealtimeOptions(options) {
3397
3659
  if (!Number.isFinite(value) || value < min) throw new ItdConfigError(`realtime.${name} должен быть числом не меньше ${min}, получено: ${value}`);
3398
3660
  };
3399
3661
  positiveInteger(options.maxAttempts, "maxAttempts");
3662
+ positiveInteger(options.concurrency, "concurrency");
3663
+ if (options.concurrency === 0) throw new ItdConfigError("realtime.concurrency должен быть больше нуля");
3400
3664
  duration(options.pollInterval, "pollInterval", 1);
3401
3665
  duration(options.idleTimeout, "idleTimeout", 0);
3402
3666
  duration(options.handshakeTimeout, "handshakeTimeout", 0);
@@ -3405,6 +3669,7 @@ function validateRealtimeOptions(options) {
3405
3669
  if (!Array.isArray(options.backoff) || options.backoff.length === 0) throw new ItdConfigError("realtime.backoff должен быть непустым списком пауз");
3406
3670
  for (const delay of options.backoff) duration(delay, "backoff", 0);
3407
3671
  }
3672
+ if (options.sequentialize !== void 0 && typeof options.sequentialize !== "function") throw new ItdConfigError("realtime.sequentialize должен быть функцией");
3408
3673
  }
3409
3674
  /**
3410
3675
  * Поток уведомлений в реальном времени.
@@ -3414,22 +3679,26 @@ function validateRealtimeOptions(options) {
3414
3679
  *
3415
3680
  * @example
3416
3681
  * ```ts
3682
+ * import { NotificationType } from 'itd-api';
3683
+ *
3417
3684
  * const stream = itd.realtime();
3418
3685
  *
3419
- * stream.on('notification', ({ notification, unreadCount }) => {
3420
- * console.log(formatNotificationText(notification), unreadCount);
3686
+ * stream.onNotification(NotificationType.PostComment, async ({ update }) => {
3687
+ * await saveCommentNotification(update.data.notification);
3421
3688
  * });
3422
3689
  * stream.on('status', (status) => console.log('соединение:', status));
3423
3690
  *
3424
3691
  * await stream.connect();
3425
3692
  * // …позже
3426
3693
  * stream.disconnect();
3694
+ * await stream.drain();
3427
3695
  * ```
3428
3696
  */
3429
3697
  var ItdRealtime = class {
3430
3698
  #deps;
3431
3699
  #options;
3432
3700
  #emitter;
3701
+ #dispatcher;
3433
3702
  #transport;
3434
3703
  #maxAttempts;
3435
3704
  #controller;
@@ -3444,7 +3713,7 @@ var ItdRealtime = class {
3444
3713
  #wanted = false;
3445
3714
  #status = RealtimeStatus.Disconnected;
3446
3715
  #attempt = 0;
3447
- #timer;
3716
+ #cancelTimer;
3448
3717
  #detachEnvironment;
3449
3718
  constructor(deps, options = {}) {
3450
3719
  validateRealtimeOptions(options);
@@ -3457,6 +3726,14 @@ var ItdRealtime = class {
3457
3726
  if (deps.logger) deps.logger.error(message, error);
3458
3727
  else console.error(`[itd-api] ${message}`, error);
3459
3728
  });
3729
+ this.#dispatcher = new RealtimeDispatcher({
3730
+ concurrency: options.concurrency ?? 1,
3731
+ ...options.sequentialize ? { sequentialize: options.sequentialize } : {}
3732
+ }, {
3733
+ deliver: (context) => this.#deliver(context.update),
3734
+ middlewareError: (error, context) => this.#reportDispatchError("middlewareError", error, context),
3735
+ handlerError: (error, context) => this.#reportDispatchError("handlerError", error, context)
3736
+ });
3460
3737
  }
3461
3738
  /** Текущее состояние соединения. */
3462
3739
  get status() {
@@ -3487,6 +3764,40 @@ var ItdRealtime = class {
3487
3764
  return this.#emitter.once(event, listener);
3488
3765
  }
3489
3766
  /**
3767
+ * Добавляет промежуточный обработчик нормализованных обновлений.
3768
+ *
3769
+ * Обработчики выполняются в порядке регистрации. Если `next()` не вызван, обновление не
3770
+ * передаётся дальше по цепочке, асинхронным обработчикам и слушателям событий.
3771
+ *
3772
+ * @returns функция удаления обработчика
3773
+ */
3774
+ use(middleware) {
3775
+ if (typeof middleware !== "function") throw new ItdConfigError("realtime.use() принимает функцию обработки");
3776
+ return this.#dispatcher.use(middleware);
3777
+ }
3778
+ onUpdate(selectorOrHandler, selectedHandler) {
3779
+ const selectAll = selectedHandler === void 0;
3780
+ const handler = selectAll ? selectorOrHandler : selectedHandler;
3781
+ if (typeof handler !== "function") throw new ItdConfigError("realtime.onUpdate() принимает функцию обработчика");
3782
+ if (!selectAll && typeof selectorOrHandler !== "function" && !Object.values(RealtimeUpdateType).includes(selectorOrHandler)) throw new ItdConfigError(`Неизвестный тип обновления потока: ${String(selectorOrHandler)}`);
3783
+ let predicate;
3784
+ if (selectAll) predicate = () => true;
3785
+ else {
3786
+ const selector = selectorOrHandler;
3787
+ predicate = typeof selector === "function" ? selector : (context) => context.update.type === selector;
3788
+ }
3789
+ return this.#dispatcher.on(predicate, handler);
3790
+ }
3791
+ onNotification(selector, handler) {
3792
+ if (typeof handler !== "function") throw new ItdConfigError("realtime.onNotification() принимает функцию обработчика");
3793
+ if (typeof selector !== "function") validateNotificationSelector(selector);
3794
+ const predicate = (context) => {
3795
+ if (!isNotificationContext(context)) return false;
3796
+ return typeof selector === "function" ? selector(context) : matchesNotification(context, selector);
3797
+ };
3798
+ return this.#dispatcher.on(predicate, handler);
3799
+ }
3800
+ /**
3490
3801
  * Поднимает соединение.
3491
3802
  *
3492
3803
  * Повторный вызов при уже живом соединении ничего не делает — это защита от двойного
@@ -3497,9 +3808,14 @@ var ItdRealtime = class {
3497
3808
  async connect() {
3498
3809
  if (this.#wanted) return;
3499
3810
  this.#wanted = true;
3811
+ this.#deps.onConnect?.();
3500
3812
  this.#attachEnvironmentListeners();
3501
3813
  if (this.#options.syncCount !== false) try {
3502
- this.#emitter.emit("unreadCount", await this.#deps.fetchUnreadCount());
3814
+ const count = await this.#deps.fetchUnreadCount();
3815
+ if (this.#wanted) this.#dispatch({
3816
+ type: RealtimeUpdateType.UnreadCount,
3817
+ data: count
3818
+ }, void 0, RealtimeUpdateOrigin.Sync);
3503
3819
  } catch (error) {
3504
3820
  this.#deps.logger?.debug("не удалось получить число непрочитанных", error);
3505
3821
  }
@@ -3508,27 +3824,36 @@ var ItdRealtime = class {
3508
3824
  /** Закрывает соединение и отменяет запланированные попытки. */
3509
3825
  disconnect() {
3510
3826
  this.#wanted = false;
3511
- if (this.#timer !== void 0) {
3512
- clearTimeout(this.#timer);
3513
- this.#timer = void 0;
3827
+ if (this.#cancelTimer) {
3828
+ this.#cancelTimer();
3829
+ this.#cancelTimer = void 0;
3514
3830
  }
3515
3831
  this.#detachEnvironment?.();
3516
3832
  this.#detachEnvironment = void 0;
3517
3833
  this.#controller?.abort();
3518
3834
  this.#controller = void 0;
3519
3835
  this.#attempt = 0;
3836
+ this.#dispatcher.clearPending();
3520
3837
  this.#setStatus(RealtimeStatus.Disconnected);
3521
3838
  this.#deps.onClose?.();
3522
3839
  }
3523
- /** Снимает все подписки. Соединение при этом не закрывается. */
3840
+ /** Ждёт завершения всех принятых обновлений. */
3841
+ drain() {
3842
+ return this.#dispatcher.drain();
3843
+ }
3844
+ /** Снимает подписки `on()` и `once()`. Остальные обработчики остаются. */
3524
3845
  removeAllListeners() {
3525
3846
  this.#emitter.removeAllListeners();
3526
3847
  }
3527
3848
  #createTransport() {
3528
3849
  const kind = this.#options.transport ?? RealtimeTransportKind.Auto;
3529
3850
  if (typeof kind === "object") return kind;
3530
- if (kind === RealtimeTransportKind.Poll || kind === RealtimeTransportKind.Auto && !supportsStreamingBody()) return new PollTransport({ ...this.#options.pollInterval !== void 0 ? { interval: this.#options.pollInterval } : {} });
3851
+ if (kind === RealtimeTransportKind.Poll || kind === RealtimeTransportKind.Auto && !supportsStreamingBody()) return new PollTransport({
3852
+ clock: this.#deps.clock ?? systemClock,
3853
+ ...this.#options.pollInterval !== void 0 ? { interval: this.#options.pollInterval } : {}
3854
+ });
3531
3855
  return new SseTransport({
3856
+ clock: this.#deps.clock ?? systemClock,
3532
3857
  ...this.#options.idleTimeout !== void 0 ? { idleTimeout: this.#options.idleTimeout } : {},
3533
3858
  ...this.#options.handshakeTimeout !== void 0 ? { handshakeTimeout: this.#options.handshakeTimeout } : {}
3534
3859
  });
@@ -3550,7 +3875,7 @@ var ItdRealtime = class {
3550
3875
  this.#attempt = 0;
3551
3876
  this.#setStatus(RealtimeStatus.Connected);
3552
3877
  },
3553
- onEvent: (event) => this.#handleEvent(event.name, event.data),
3878
+ onEvent: (event) => this.#handleEvent(event),
3554
3879
  onParseError: (error, raw) => this.#emitter.emit("parseError", {
3555
3880
  error,
3556
3881
  raw
@@ -3562,25 +3887,48 @@ var ItdRealtime = class {
3562
3887
  this.#handleFailure(error);
3563
3888
  });
3564
3889
  }
3565
- #handleEvent(name, data) {
3566
- this.#emitter.emit("message", {
3567
- name,
3568
- data
3890
+ #handleEvent(event) {
3891
+ if (!this.#wanted) return;
3892
+ this.#emitter.emit("message", event);
3893
+ if (event.name === "connected") {
3894
+ this.#emitter.emit("ready", { userId: pickString(event.data, "userId") });
3895
+ return;
3896
+ }
3897
+ const update = readRealtimeUpdate(event);
3898
+ if (update) this.#dispatch(update, event, RealtimeUpdateOrigin.Stream);
3899
+ }
3900
+ #dispatch(update, raw, origin) {
3901
+ this.#dispatcher.dispatch({
3902
+ update,
3903
+ stream: this,
3904
+ raw,
3905
+ origin
3569
3906
  });
3570
- if (name === "connected") {
3571
- this.#emitter.emit("ready", { userId: pickString(data, "userId") });
3907
+ }
3908
+ #deliver(update) {
3909
+ if (update.type === RealtimeUpdateType.Notification) {
3910
+ this.#emitter.emit("notification", update.data);
3911
+ if (update.data.unreadCount !== void 0) this.#emitter.emit("unreadCount", update.data.unreadCount);
3572
3912
  return;
3573
3913
  }
3574
- if (name === "notification") {
3575
- const event = readNotificationEvent(data);
3576
- this.#emitter.emit("notification", event);
3577
- if (event.unreadCount !== void 0) this.#emitter.emit("unreadCount", event.unreadCount);
3914
+ if (update.type === RealtimeUpdateType.UnreadCount) {
3915
+ this.#emitter.emit("unreadCount", update.data);
3578
3916
  return;
3579
3917
  }
3580
- if (name === "unread_count") {
3581
- const count = readUnreadCountEvent(data);
3582
- if (count !== void 0) this.#emitter.emit("unreadCount", count);
3918
+ if (update.type === RealtimeUpdateType.Unknown) return;
3919
+ assertNeverUpdate(update);
3920
+ }
3921
+ #reportDispatchError(event, error, context) {
3922
+ if (this.#emitter.listenerCount(event) > 0) {
3923
+ this.#emitter.emit(event, {
3924
+ error,
3925
+ context
3926
+ });
3927
+ return;
3583
3928
  }
3929
+ const message = event === "middlewareError" ? "Ошибка в промежуточном обработчике потока" : "Ошибка в обработчике обновления потока";
3930
+ if (this.#deps.logger) this.#deps.logger.error(message, error);
3931
+ else console.error(`[itd-api] ${message}`, error);
3584
3932
  }
3585
3933
  #handleFailure(error) {
3586
3934
  this.#controller = void 0;
@@ -3618,8 +3966,8 @@ var ItdRealtime = class {
3618
3966
  attempt: this.#attempt,
3619
3967
  delay
3620
3968
  });
3621
- this.#timer = setTimeout(() => {
3622
- this.#timer = void 0;
3969
+ this.#cancelTimer = (this.#deps.clock ?? systemClock).schedule(() => {
3970
+ this.#cancelTimer = void 0;
3623
3971
  this.#run();
3624
3972
  }, delay);
3625
3973
  }
@@ -3646,7 +3994,7 @@ var ItdRealtime = class {
3646
3994
  const target = globalThis;
3647
3995
  if (typeof target.addEventListener !== "function") return;
3648
3996
  const wake = () => {
3649
- if (this.#controller || this.#timer !== void 0) return;
3997
+ if (this.#controller || this.#cancelTimer) return;
3650
3998
  if (this.#status === RealtimeStatus.Disconnected) return;
3651
3999
  this.#attempt = 0;
3652
4000
  this.#run();
@@ -3668,6 +4016,9 @@ var ItdRealtime = class {
3668
4016
  this.#emitter.emit("status", status);
3669
4017
  }
3670
4018
  };
4019
+ function assertNeverUpdate(update) {
4020
+ throw new TypeError(`Необработанное обновление потока: ${String(update)}`);
4021
+ }
3671
4022
  //#endregion
3672
4023
  //#region src/core/pagination.ts
3673
4024
  /** Схема пагинации эндпоинта. */
@@ -4583,6 +4934,140 @@ var CommentsResource = class extends BaseResource {
4583
4934
  }
4584
4935
  };
4585
4936
  //#endregion
4937
+ //#region src/core/attachments/url-source.ts
4938
+ /** Убирает параметры MIME и приводит его к форме для сравнения. */
4939
+ function normalizeMimeType(contentType) {
4940
+ return contentType?.split(";", 1)[0]?.trim().toLowerCase() || void 0;
4941
+ }
4942
+ /** Достаёт имя файла из пути URL. */
4943
+ function filenameFromUrl(url) {
4944
+ const last = url.pathname.split("/").pop();
4945
+ if (!last) return void 0;
4946
+ try {
4947
+ return decodeURIComponent(last) || void 0;
4948
+ } catch {
4949
+ return last;
4950
+ }
4951
+ }
4952
+ /** Проверяет объявленный размер и возвращает его, если заголовок корректен. */
4953
+ function declaredSize(response, maxBytes, url) {
4954
+ const header = response.headers.get("content-length");
4955
+ if (header === null) return void 0;
4956
+ const size = Number(header);
4957
+ if (!Number.isFinite(size) || size < 0 || !Number.isInteger(size)) return void 0;
4958
+ if (maxBytes !== void 0 && size > maxBytes) throw fileTooLarge(url, maxBytes, size);
4959
+ return size;
4960
+ }
4961
+ /** Получает HTTP-ответ источника и проверяет его статус. */
4962
+ async function fetchFile(target, options, context) {
4963
+ let requested;
4964
+ try {
4965
+ requested = new URL(target);
4966
+ } catch {
4967
+ throw new ItdConfigError(`«${target}» не разбирается как адрес`);
4968
+ }
4969
+ if (requested.protocol !== "http:" && requested.protocol !== "https:") throw new ItdConfigError(`вложение по адресу поддерживает только http и https, получено: ${requested.protocol}`);
4970
+ let response;
4971
+ try {
4972
+ response = await context.fetch(requested, { ...context.signal ? { signal: context.signal } : {} });
4973
+ } catch (error) {
4974
+ if (context.signal?.aborted || error instanceof Error && error.name === "AbortError") throw error;
4975
+ throw new ItdFileError(`не удалось получить файл по адресу ${requested.href}`, {
4976
+ reason: ItdFileErrorReason.Network,
4977
+ url: requested.href,
4978
+ retryable: true,
4979
+ cause: error
4980
+ });
4981
+ }
4982
+ const finalUrl = response.url ? new URL(response.url) : requested;
4983
+ if (!response.ok) {
4984
+ await response.body?.cancel().catch(() => {});
4985
+ throw new ItdFileError(`источник ${finalUrl.href} ответил статусом ${response.status}`, {
4986
+ reason: ItdFileErrorReason.Http,
4987
+ url: finalUrl.href,
4988
+ status: response.status,
4989
+ retryable: response.status === 408 || response.status === 429 || response.status >= 500
4990
+ });
4991
+ }
4992
+ const { maxBytes } = resolveFileStreamOptions(options, DEFAULT_URL_FILE_MAX_BYTES);
4993
+ let size;
4994
+ try {
4995
+ size = declaredSize(response, maxBytes, finalUrl.href);
4996
+ } catch (error) {
4997
+ await response.body?.cancel().catch(() => {});
4998
+ throw error;
4999
+ }
5000
+ return {
5001
+ response,
5002
+ url: finalUrl,
5003
+ size
5004
+ };
5005
+ }
5006
+ /** Читает ответ с контролем размера до создания итогового `Blob`. */
5007
+ async function responseBlob(response, url, maxBytes, streamBufferBytes, signal) {
5008
+ if (!response.body) {
5009
+ const blob = await response.blob();
5010
+ if (maxBytes !== void 0 && blob.size > maxBytes) throw fileTooLarge(url, maxBytes, blob.size);
5011
+ return blob;
5012
+ }
5013
+ const chunks = [];
5014
+ const reader = boundedFileStream(response.body, {
5015
+ ...maxBytes !== void 0 ? { maxBytes } : {},
5016
+ streamBufferBytes,
5017
+ ...signal ? { signal } : {},
5018
+ url,
5019
+ retryableRead: true
5020
+ }).getReader();
5021
+ try {
5022
+ for (;;) {
5023
+ const next = await reader.read();
5024
+ if (next.done) break;
5025
+ chunks.push(next.value);
5026
+ }
5027
+ } finally {
5028
+ reader.releaseLock();
5029
+ }
5030
+ return new Blob(chunks.map((chunk) => Uint8Array.from(chunk).buffer));
5031
+ }
5032
+ /** Скачивает файл целиком с ограничением размера. @internal */
5033
+ async function downloadFile(target, options, context) {
5034
+ const resolved = resolveFileStreamOptions(options, DEFAULT_URL_FILE_MAX_BYTES);
5035
+ const { response, url } = await fetchFile(target, options, context);
5036
+ const blob = await responseBlob(response, url.href, resolved.maxBytes, resolved.streamBufferBytes, context.signal);
5037
+ const contentType = normalizeMimeType(options.contentType ?? response.headers.get("content-type") ?? void 0);
5038
+ const filename = options.filename ?? filenameFromUrl(url);
5039
+ return {
5040
+ file: new Blob([blob], { type: contentType ?? "" }),
5041
+ ...filename ? { filename } : {},
5042
+ ...contentType ? { contentType } : {}
5043
+ };
5044
+ }
5045
+ /** Открывает HTTP-ответ как ограниченный поток. @internal */
5046
+ async function openUrlFile(target, options, context) {
5047
+ const resolved = resolveFileStreamOptions(options, DEFAULT_URL_FILE_MAX_BYTES);
5048
+ const { response, url, size } = await fetchFile(target, options, context);
5049
+ if (!response.body) throw new ItdFileError(`источник ${url.href} не предоставил потоковое тело`, {
5050
+ reason: ItdFileErrorReason.StreamUnavailable,
5051
+ url: url.href
5052
+ });
5053
+ const stream = boundedFileStream(response.body, {
5054
+ ...resolved.maxBytes !== void 0 ? { maxBytes: resolved.maxBytes } : {},
5055
+ streamBufferBytes: resolved.streamBufferBytes,
5056
+ ...context.signal ? { signal: context.signal } : {},
5057
+ url: url.href,
5058
+ retryableRead: true
5059
+ });
5060
+ const filename = options.filename ?? filenameFromUrl(url);
5061
+ const contentType = normalizeMimeType(options.contentType ?? response.headers.get("content-type") ?? void 0);
5062
+ return {
5063
+ stream,
5064
+ ...filename ? { filename } : {},
5065
+ ...contentType ? { contentType } : {},
5066
+ ...size !== void 0 ? { size } : {},
5067
+ close: () => stream.cancel().catch(() => {})
5068
+ };
5069
+ }
5070
+ //#endregion
4586
5071
  //#region src/core/mime.ts
4587
5072
  /** Изображения, которые принимает `POST /api/files/upload`. */
4588
5073
  const IMAGE_MIME_TYPES = Object.freeze([
@@ -5128,7 +5613,6 @@ var NotificationsResource = class extends BaseResource {
5128
5613
  };
5129
5614
  //#endregion
5130
5615
  //#region src/core/time.ts
5131
- /** Отметка времени без часового пояса: `2026-07-23 23:14:25`, возможно с долями секунды. */
5132
5616
  const NAIVE_STAMP = /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}(?:\.\d+)?)$/;
5133
5617
  /**
5134
5618
  * Приводит отметку времени без часового пояса к ISO-8601, считая её временем UTC.
@@ -5147,6 +5631,21 @@ function utcStampToIso(value) {
5147
5631
  const iso = `${match[1]}T${match[2]}Z`;
5148
5632
  return Number.isFinite(Date.parse(iso)) ? iso : value;
5149
5633
  }
5634
+ /**
5635
+ * Разбирает дату API в объект `Date`.
5636
+ *
5637
+ * @returns `null`, если строки нет или она не разбирается
5638
+ *
5639
+ * @example
5640
+ * ```ts
5641
+ * const created = toDate(post.createdAt);
5642
+ * ```
5643
+ */
5644
+ function toDate(value) {
5645
+ if (!value) return null;
5646
+ const date = new Date(value);
5647
+ return Number.isFinite(date.getTime()) ? date : null;
5648
+ }
5150
5649
  //#endregion
5151
5650
  //#region src/resources/platform.ts
5152
5651
  /** Приводит `last_checked` каждого сервиса к ISO. Остальное остаётся как прислал сервер. */
@@ -6077,7 +6576,7 @@ function resolvePoll(input) {
6077
6576
  const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6078
6577
  const BUILD_UPDATE = Symbol.for("itd.postBuilder.update");
6079
6578
  /**
6080
- * Проверяет данные поста.
6579
+ * Проверяет и нормализует данные поста.
6081
6580
  *
6082
6581
  * Отдельного внимания заслуживает `wallRecipientId`: API принимает там **только UUID**,
6083
6582
  * а имя пользователя молча приводит к ошибке на сервере. Проверка здесь превращает
@@ -6097,10 +6596,11 @@ function validatePost(input) {
6097
6596
  if (wallRecipientId !== void 0 && wallRecipientId !== null) {
6098
6597
  if (!UUID_PATTERN.test(wallRecipientId)) throw new ItdConfigError(`wallRecipientId должен быть UUID, а не именем пользователя (получено: «${wallRecipientId}»). Идентификатор можно взять из профиля: (await itd.users.get(username)).id`);
6099
6598
  }
6599
+ const { poll: inputPoll, ...data } = input;
6100
6600
  return {
6101
- ...input,
6601
+ ...data,
6102
6602
  ...input.spans !== void 0 ? { spans: validateSpans(content, input.spans) } : {},
6103
- ...input.poll !== void 0 ? { poll: resolvePoll(input.poll) } : {}
6603
+ ...inputPoll !== void 0 ? { poll: resolvePoll(inputPoll) } : {}
6104
6604
  };
6105
6605
  }
6106
6606
  /**
@@ -6297,7 +6797,8 @@ function post(content) {
6297
6797
  }
6298
6798
  /** Приводит любую форму входа к готовым данным поста. */
6299
6799
  function resolvePost(input) {
6300
- return resolveInput(input, () => post(), validatePost);
6800
+ const resolved = typeof input === "function" ? input(post()) : input;
6801
+ return isBuilder(resolved) ? validatePost(resolved.build()) : validatePost(resolved);
6301
6802
  }
6302
6803
  function validatePostUpdate(input) {
6303
6804
  if (!input || typeof input !== "object") throw new ItdConfigError("Для обновления поста нужен объект с явно заданным content");
@@ -7572,7 +8073,7 @@ var ItdClient = class ItdClient {
7572
8073
  for (const service of BUILT_IN_SERVICES) this.#services.define(service);
7573
8074
  for (const service of config.services) this.#services.define(service);
7574
8075
  const shared = config.rateLimit ? internals.queues : void 0;
7575
- const queues = shared ?? (config.rateLimit ? new RequestQueuePool(config.rateLimit) : void 0);
8076
+ const queues = shared ?? (config.rateLimit ? new RequestQueuePool(config.rateLimit, config.clock) : void 0);
7576
8077
  this.#queues = queues;
7577
8078
  this.#ownsQueues = shared === void 0;
7578
8079
  let authManager;
@@ -7588,6 +8089,7 @@ var ItdClient = class ItdClient {
7588
8089
  this.#transport = transport;
7589
8090
  const pluginsLayer = createPluginsMiddleware(this.#plugins);
7590
8091
  const retriesLayer = createRetryMiddleware({
8092
+ clock: config.clock,
7591
8093
  retry: config.retry,
7592
8094
  rateLimitDelays: config.rateLimit?.retryDelays ?? [],
7593
8095
  pauseQueue: queues ? (ms, request) => queues.for(request.service).pause(ms) : void 0,
@@ -7767,10 +8269,12 @@ var ItdClient = class ItdClient {
7767
8269
  *
7768
8270
  * @example
7769
8271
  * ```ts
8272
+ * import { NotificationType } from 'itd-api';
8273
+ *
7770
8274
  * const stream = itd.realtime();
7771
8275
  *
7772
- * stream.on('notification', ({ notification }) => {
7773
- * console.log(formatNotificationText(notification));
8276
+ * stream.onNotification(NotificationType.PostComment, async ({ update }) => {
8277
+ * await handleComment(update.data.notification);
7774
8278
  * });
7775
8279
  * stream.on('unreadCount', (count) => setBadge(count));
7776
8280
  *
@@ -7788,8 +8292,10 @@ var ItdClient = class ItdClient {
7788
8292
  getToken: () => this.#authManager.getAccessToken(),
7789
8293
  refresh: () => this.#authManager.onUnauthorized(),
7790
8294
  fetchUnreadCount: () => this.notifications.count(),
8295
+ onConnect: () => this.#streams.add(stream),
7791
8296
  onClose: () => this.#streams.delete(stream),
7792
- logger: this.#config.logger
8297
+ logger: this.#config.logger,
8298
+ clock: this.#config.clock
7793
8299
  }, options);
7794
8300
  this.#streams.add(stream);
7795
8301
  return stream;
@@ -7798,8 +8304,8 @@ var ItdClient = class ItdClient {
7798
8304
  * Освобождает ресурсы клиента: закрывает все потоки уведомлений, отправляет открытые
7799
8305
  * накопители {@link telemetry}, затем останавливает очередь запросов.
7800
8306
  *
7801
- * После вызова клиентом можно пользоваться снова — новые запросы поднимут всё заново,
7802
- * но уже созданные потоки и успешно закрытые накопители останутся закрытыми.
8307
+ * Метод дожидается активных обработчиков потока. После вызова клиентом можно пользоваться
8308
+ * снова; ранее созданный поток можно запустить повторным `connect()`.
7803
8309
  *
7804
8310
  * Общая очередь, полученная от {@link ItdAccounts}, не останавливается: её гасит сам
7805
8311
  * контейнер, когда закрывает все аккаунты разом.
@@ -7812,8 +8318,9 @@ var ItdClient = class ItdClient {
7812
8318
  * ```
7813
8319
  */
7814
8320
  async close() {
7815
- this.#disconnectStreams();
8321
+ const streams = this.#disconnectStreams();
7816
8322
  try {
8323
+ await Promise.all(streams.map((stream) => stream.drain()));
7817
8324
  await this.telemetry.close();
7818
8325
  } finally {
7819
8326
  if (this.#ownsQueues) this.#queues?.stop();
@@ -7832,8 +8339,10 @@ var ItdClient = class ItdClient {
7832
8339
  }
7833
8340
  /** Завершает потоки до того, как запросы начнут использовать другой аккаунт. */
7834
8341
  #disconnectStreams() {
7835
- for (const stream of [...this.#streams]) stream.disconnect();
8342
+ const streams = [...this.#streams];
8343
+ for (const stream of streams) stream.disconnect();
7836
8344
  this.#streams.clear();
8345
+ return streams;
7837
8346
  }
7838
8347
  /** Позволяет использовать клиент с `await using`. */
7839
8348
  [Symbol.asyncDispose]() {
@@ -7972,7 +8481,7 @@ var ItdAccounts = class ItdAccounts {
7972
8481
  this.#plugins = orderPluginDefinitions(plugins ?? []);
7973
8482
  this.#rateLimitScope = rateLimitScope ?? "account";
7974
8483
  const rateLimit = this.#rateLimitScope === "shared" ? resolveRateLimit(base.rateLimit) : void 0;
7975
- this.#queues = rateLimit ? new RequestQueuePool(rateLimit) : void 0;
8484
+ this.#queues = rateLimit ? new RequestQueuePool(rateLimit, base.clock ?? systemClock) : void 0;
7976
8485
  const logger = typeof base.logger === "object" ? base.logger : void 0;
7977
8486
  this.#logger = logger;
7978
8487
  this.#emitter = new Emitter((error) => reportListenerError(logger, "аккаунтов", error));
@@ -8301,6 +8810,92 @@ function createAccounts(options = {}) {
8301
8810
  return new ItdAccounts(options);
8302
8811
  }
8303
8812
  //#endregion
8813
+ //#region src/core/attachments/factories.ts
8814
+ function fromUrl(url, options = {}) {
8815
+ if (resolveFileStreamOptions(options, 104857600).mode === FileTransferMode.Stream) return { open: (context) => openUrlFile(url, options, context) };
8816
+ return { load: (context) => downloadFile(url, options, context) };
8817
+ }
8818
+ /**
8819
+ * Создаёт повторяемый пользовательский поток.
8820
+ *
8821
+ * Фабрика вызывается заново для каждой попытки; возвращать один и тот же поток нельзя.
8822
+ */
8823
+ function fromStream(factory, options = {}) {
8824
+ const resolved = resolveFileStreamOptions({
8825
+ ...options,
8826
+ mode: FileTransferMode.Stream
8827
+ }, void 0);
8828
+ optionalBytes(options.size, "size");
8829
+ return { open: async (context) => {
8830
+ let opened;
8831
+ try {
8832
+ opened = await factory(context);
8833
+ } catch (error) {
8834
+ if (error instanceof ItdFileError || error instanceof ItdConfigError || context.signal?.aborted) throw error;
8835
+ throw new ItdFileError("не удалось открыть поток вложения", {
8836
+ reason: ItdFileErrorReason.Read,
8837
+ retryable: true,
8838
+ cause: error
8839
+ });
8840
+ }
8841
+ const content = isReadableByteStream(opened) ? { stream: opened } : opened;
8842
+ if (!content || !isReadableByteStream(content.stream)) throw new ItdConfigError("fromStream должен вернуть ReadableStream или { stream }");
8843
+ const size = options.size ?? content.size;
8844
+ optionalBytes(size, "size");
8845
+ if (resolved.maxBytes !== void 0 && size !== void 0 && size > resolved.maxBytes) {
8846
+ await content.close?.();
8847
+ throw fileTooLarge(void 0, resolved.maxBytes, size);
8848
+ }
8849
+ const contentType = normalizeMimeType(options.contentType ?? content.contentType);
8850
+ return {
8851
+ stream: boundedFileStream(content.stream, {
8852
+ ...resolved.maxBytes !== void 0 ? { maxBytes: resolved.maxBytes } : {},
8853
+ streamBufferBytes: resolved.streamBufferBytes,
8854
+ ...context.signal ? { signal: context.signal } : {},
8855
+ retryableRead: true
8856
+ }),
8857
+ ...options.filename ?? content.filename ? { filename: options.filename ?? content.filename } : {},
8858
+ ...contentType ? { contentType } : {},
8859
+ ...size !== void 0 ? { size } : {},
8860
+ ...content.close ? { close: content.close } : {}
8861
+ };
8862
+ } };
8863
+ }
8864
+ //#endregion
8865
+ //#region src/models/guards.ts
8866
+ /**
8867
+ * Свой ли это профиль.
8868
+ *
8869
+ * @example
8870
+ * ```ts
8871
+ * if (isMyProfile(profile)) console.log(profile.subscription.isActive);
8872
+ * ```
8873
+ */
8874
+ function isMyProfile(profile) {
8875
+ return "subscription" in profile;
8876
+ }
8877
+ //#endregion
8878
+ //#region src/models/status-helpers.ts
8879
+ const STATUS_WINDOW_DAYS = 90;
8880
+ /**
8881
+ * Разворачивает историю сервиса в массив на 90 суток.
8882
+ * Сутки без данных становятся `null`.
8883
+ *
8884
+ * @returns массив, где индекс — сколько суток назад: `[0]` — сегодня
8885
+ *
8886
+ * @example
8887
+ * ```ts
8888
+ * const status = await itd.platform.status();
8889
+ * const days = statusDays(status.services[0]);
8890
+ *
8891
+ * days[0]?.uptime; // доступность за сегодня
8892
+ * days.filter((day) => day === null).length; // за сколько суток данных нет
8893
+ * ```
8894
+ */
8895
+ function statusDays(service) {
8896
+ return Array.from({ length: STATUS_WINDOW_DAYS }, (_, index) => service.days[String(index)] ?? null);
8897
+ }
8898
+ //#endregion
8304
8899
  //#region src/notifications/text.ts
8305
8900
  /** Имя, которое подставляется, если участник неизвестен. */
8306
8901
  const UNKNOWN_ACTOR = "Пользователь";
@@ -8432,6 +9027,87 @@ function resolveNotificationUrl(notification) {
8432
9027
  return clickUrl || "/notifications";
8433
9028
  }
8434
9029
  //#endregion
9030
+ //#region src/realtime/router.ts
9031
+ /**
9032
+ * Направляет обновления потока в именованные цепочки промежуточных обработчиков.
9033
+ *
9034
+ * @example
9035
+ * ```ts
9036
+ * import { RealtimeRouter, RealtimeUpdateType } from 'itd-api';
9037
+ *
9038
+ * const router = new RealtimeRouter((context) => context.update.type);
9039
+ * router.route(RealtimeUpdateType.Notification, async (context, next) => {
9040
+ * if (context.update.type === RealtimeUpdateType.Notification) {
9041
+ * await handleNotification(context.update.data.notification);
9042
+ * }
9043
+ * await next();
9044
+ * });
9045
+ * stream.use(router.middleware());
9046
+ * ```
9047
+ */
9048
+ var RealtimeRouter = class {
9049
+ #selector;
9050
+ #routes = /* @__PURE__ */ new Map();
9051
+ #fallback = [];
9052
+ constructor(selector) {
9053
+ if (typeof selector !== "function") throw new ItdConfigError("RealtimeRouter принимает функцию выбора маршрута");
9054
+ this.#selector = selector;
9055
+ }
9056
+ /** Добавляет промежуточные обработчики к маршруту и возвращает функцию их удаления. */
9057
+ route(key, ...middleware) {
9058
+ if (!isPropertyKey(key)) throw new ItdConfigError("Ключ realtime route должен быть PropertyKey");
9059
+ const registration = this.#registration(middleware);
9060
+ const registrations = this.#routes.get(key) ?? [];
9061
+ registrations.push(registration);
9062
+ this.#routes.set(key, registrations);
9063
+ return () => {
9064
+ const current = this.#routes.get(key);
9065
+ if (!current) return;
9066
+ const index = current.indexOf(registration);
9067
+ if (index >= 0) current.splice(index, 1);
9068
+ if (current.length === 0) this.#routes.delete(key);
9069
+ };
9070
+ }
9071
+ /** Добавляет промежуточные обработчики для обновлений без зарегистрированного маршрута. */
9072
+ otherwise(...middleware) {
9073
+ const registration = this.#registration(middleware);
9074
+ this.#fallback.push(registration);
9075
+ return () => {
9076
+ const index = this.#fallback.indexOf(registration);
9077
+ if (index >= 0) this.#fallback.splice(index, 1);
9078
+ };
9079
+ }
9080
+ /** Возвращает промежуточный обработчик для `stream.use()`. */
9081
+ middleware() {
9082
+ const middleware = (context, next) => this.#captureMiddleware()(context, next);
9083
+ return withRealtimeMiddlewareSnapshot(middleware, () => this.#captureMiddleware());
9084
+ }
9085
+ #captureMiddleware() {
9086
+ const routes = /* @__PURE__ */ new Map();
9087
+ for (const [key, registrations] of this.#routes) routes.set(key, registrations.flatMap(({ middleware }) => middleware).map(captureRealtimeMiddleware));
9088
+ const fallback = this.#fallback.flatMap(({ middleware }) => middleware).map(captureRealtimeMiddleware);
9089
+ return async (context, next) => {
9090
+ const key = await this.#selector(context);
9091
+ if (key != null && !isPropertyKey(key)) throw new ItdConfigError("Функция выбора маршрута должна возвращать PropertyKey, null или undefined");
9092
+ const route = key == null ? void 0 : routes.get(key);
9093
+ const chain = route && route.length > 0 ? route : fallback;
9094
+ if (chain.length === 0) {
9095
+ await next();
9096
+ return;
9097
+ }
9098
+ await runRealtimeMiddleware(chain, context, next);
9099
+ };
9100
+ }
9101
+ #registration(middleware) {
9102
+ if (middleware.length === 0) throw new ItdConfigError("Маршрут должен содержать хотя бы один обработчик");
9103
+ for (const item of middleware) if (typeof item !== "function") throw new ItdConfigError("Маршрут принимает только функции обработки");
9104
+ return { middleware: [...middleware] };
9105
+ }
9106
+ };
9107
+ function isPropertyKey(value) {
9108
+ return typeof value === "string" || typeof value === "number" || typeof value === "symbol";
9109
+ }
9110
+ //#endregion
8435
9111
  //#region src/spans/render.ts
8436
9112
  /** Формат результата {@link renderSpans}. */
8437
9113
  const SpanRenderFormat = Object.freeze({
@@ -8636,54 +9312,6 @@ function renderSpans(content, spans = [], options = {}) {
8636
9312
  return result;
8637
9313
  }
8638
9314
  //#endregion
8639
- //#region src/types/models.ts
8640
- /**
8641
- * Свой ли это профиль.
8642
- *
8643
- * @example
8644
- * ```ts
8645
- * if (isMyProfile(profile)) console.log(profile.subscription.isActive);
8646
- * ```
8647
- */
8648
- function isMyProfile(profile) {
8649
- return "subscription" in profile;
8650
- }
8651
- /** Глубина истории статуса в сутках. Столько элементов отдаёт {@link statusDays}. */
8652
- const STATUS_WINDOW_DAYS = 90;
8653
- /**
8654
- * Разбирает дату API в объект `Date`.
8655
- *
8656
- * @returns `null`, если строки нет или она не разбирается
8657
- *
8658
- * @example
8659
- * ```ts
8660
- * const created = toDate(post.createdAt);
8661
- * ```
8662
- */
8663
- function toDate(value) {
8664
- if (!value) return null;
8665
- const date = new Date(value);
8666
- return Number.isFinite(date.getTime()) ? date : null;
8667
- }
8668
- /**
8669
- * Разворачивает историю сервиса в массив на 90 суток.
8670
- * Сутки без данных становятся `null`.
8671
- *
8672
- * @returns массив, где индекс — сколько суток назад: `[0]` — сегодня
8673
- *
8674
- * @example
8675
- * ```ts
8676
- * const status = await itd.platform.status();
8677
- * const days = statusDays(status.services[0]);
8678
- *
8679
- * days[0]?.uptime; // доступность за сегодня
8680
- * days.filter((day) => day === null).length; // за сколько суток данных нет
8681
- * ```
8682
- */
8683
- function statusDays(service) {
8684
- return Array.from({ length: STATUS_WINDOW_DAYS }, (_, index) => service.days[String(index)] ?? null);
8685
- }
8686
- //#endregion
8687
- export { ALLOWED_MIME_TYPES, AUDIO_MIME_TYPES, AUTH_FLAG_COOKIE, AUTH_PATHS, AccessType, AttachmentType, BUILT_IN_SERVICES, CommentSort, DEFAULT_BASE_URL, DEFAULT_FILE_STREAM_BUFFER_BYTES, DEFAULT_STATUS_BASE_URL, DEFAULT_TIMEOUT, DEFAULT_UPLOAD_TIMEOUT, DEFAULT_URL_FILE_MAX_BYTES, DEFAULT_USER_AGENT, DEVICE_ID_HEADER, DetectedRuntime, FeedTab, FileTransferMode, IMAGE_MIME_TYPES, IncidentKind, InteractionType, ItdAbortError, ItdAccounts, ItdApiError, ItdApiErrorKind, ItdAuthError, ItdClient, ItdConfigError, ItdConflictError, ItdError, ItdErrorCode, ItdErrorKind, ItdFileError, ItdFileErrorReason, ItdForbiddenError, ItdNetworkError, ItdNotFoundError, ItdPhoneVerificationError, ItdRateLimitError, ItdRealtime, ItdServerError, ItdTimeoutError, ItdValidationError, LIBRARY_VERSION, LikesVisibility, MAX_RECONNECT_ATTEMPTS, MemoryMultiTokenStorage, MemoryTokenStorage, NOTIFICATION_TYPE_ALIASES, NotificationType, PaginationMode, Paginator, RECONNECT_BACKOFF, RECONNECT_JITTER, REFRESH_COOKIE, REFRESH_COOKIE_PATH, REQUEST_OPTION_KEYS, RealtimeStatus, RealtimeTransportKind, ReportReason, ReportTargetType, RuntimeMode, STATUS_SERVICE, STREAM_PATH, ServiceRegistry, ServiceState, SignInStatus, SpanRenderFormat, SpanType, TURNSTILE_SITE_KEY, UnauthorizedStreamError, VIDEO_MIME_TYPES, ViewReason, ViewSource, WallAccess, autoSpans, canonicalNotificationType, comment, createAccounts, createClient, createMultiTokenStorage, createRecordMultiStorage, createTokenStorage, formatNotificationText, fromStream, fromUrl, isBuilder, isItdApiError, isItdAuthError, isItdConflictError, isItdError, isItdFileError, isItdForbiddenError, isItdNotFoundError, isItdPhoneVerificationError, isItdRateLimitError, isItdServerError, isItdValidationError, isKnownNotificationType, isMyProfile, mapPage, markup, normalizeNotification, parseHtml, parseMarkdown, poll, post, readNotificationEvent, readUnreadCountEvent, renderSpans, report, resolveNotificationUrl, scopedTokenStorage, statusDays, toDate, utcStampToIso };
9315
+ export { ALLOWED_MIME_TYPES, AUDIO_MIME_TYPES, AUTH_FLAG_COOKIE, AUTH_PATHS, AccessType, AttachmentType, BUILT_IN_SERVICES, CommentSort, DEFAULT_BASE_URL, DEFAULT_FILE_STREAM_BUFFER_BYTES, DEFAULT_STATUS_BASE_URL, DEFAULT_TIMEOUT, DEFAULT_UPLOAD_TIMEOUT, DEFAULT_URL_FILE_MAX_BYTES, DEFAULT_USER_AGENT, DEVICE_ID_HEADER, DetectedRuntime, FeedTab, FileTransferMode, IMAGE_MIME_TYPES, IncidentKind, InteractionType, ItdAbortError, ItdAccounts, ItdApiError, ItdApiErrorKind, ItdAuthError, ItdClient, ItdConfigError, ItdConflictError, ItdError, ItdErrorCode, ItdErrorKind, ItdFileError, ItdFileErrorReason, ItdForbiddenError, ItdNetworkError, ItdNotFoundError, ItdPhoneVerificationError, ItdRateLimitError, ItdRealtime, ItdServerError, ItdTimeoutError, ItdValidationError, LIBRARY_VERSION, LikesVisibility, MAX_RECONNECT_ATTEMPTS, MemoryMultiTokenStorage, MemoryTokenStorage, NOTIFICATION_TYPE_ALIASES, NotificationType, PaginationMode, Paginator, RECONNECT_BACKOFF, RECONNECT_JITTER, REFRESH_COOKIE, REFRESH_COOKIE_PATH, REQUEST_OPTION_KEYS, RealtimeRouter, RealtimeStatus, RealtimeTransportKind, RealtimeUpdateOrigin, RealtimeUpdateType, ReportReason, ReportTargetType, RuntimeMode, STATUS_SERVICE, STREAM_PATH, ServiceRegistry, ServiceState, SignInStatus, SpanRenderFormat, SpanType, TURNSTILE_SITE_KEY, UnauthorizedStreamError, VIDEO_MIME_TYPES, ViewReason, ViewSource, WallAccess, autoSpans, canonicalNotificationType, comment, createAccounts, createClient, createMultiTokenStorage, createRecordMultiStorage, createTokenStorage, formatNotificationText, fromStream, fromUrl, isBuilder, isItdApiError, isItdAuthError, isItdConflictError, isItdError, isItdFileError, isItdForbiddenError, isItdNotFoundError, isItdPhoneVerificationError, isItdRateLimitError, isItdServerError, isItdValidationError, isKnownNotificationType, isMyProfile, mapPage, markup, normalizeNotification, parseHtml, parseMarkdown, poll, post, readNotificationEvent, readUnreadCountEvent, renderSpans, report, resolveNotificationUrl, scopedTokenStorage, statusDays, systemClock, toDate, utcStampToIso };
8688
9316
 
8689
9317
  //# sourceMappingURL=index.js.map