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/README.md +8 -6
- package/dist/index.cjs +1220 -588
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1307 -1079
- package/dist/index.d.ts +1307 -1079
- package/dist/index.js +1210 -582
- package/dist/index.js.map +1 -1
- package/dist/{multi-storage-D1keK2Op.cjs → multi-storage-C8R5Crwo.cjs} +33 -219
- package/dist/multi-storage-C8R5Crwo.cjs.map +1 -0
- package/dist/{multi-storage-CyMe404l.js → multi-storage-D4h6QA-w.js} +26 -200
- package/dist/multi-storage-D4h6QA-w.js.map +1 -0
- package/dist/{multi-storage-NDqzRQcD.d.cts → multi-storage-DP_ujJw1.d.cts} +3 -17
- package/dist/{multi-storage-BhcA2Izn.d.ts → multi-storage-DwaIh24p.d.ts} +3 -17
- package/dist/node.cjs +1 -1
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/node.js +1 -1
- package/dist/node.js.map +1 -1
- package/package.json +1 -1
- package/dist/multi-storage-CyMe404l.js.map +0 -1
- package/dist/multi-storage-D1keK2Op.cjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
/** Проверяет, можно ли удалить
|
|
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
|
-
|
|
1184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
#
|
|
1652
|
-
|
|
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.#
|
|
1707
|
-
|
|
1708
|
-
this.#
|
|
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,
|
|
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.#
|
|
1728
|
-
|
|
1729
|
-
this.#
|
|
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.#
|
|
1735
|
-
const now =
|
|
1758
|
+
if (this.#cancelTimer) return;
|
|
1759
|
+
const now = this.#clock.now();
|
|
1736
1760
|
if (this.#nextSlot > now) {
|
|
1737
|
-
this.#
|
|
1738
|
-
this.#
|
|
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.#
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
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:
|
|
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/
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
*/
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
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
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
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
|
|
3101
|
+
const cancel = this.#clock.schedule(finish, this.#interval);
|
|
3050
3102
|
function finish() {
|
|
3051
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
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.
|
|
3420
|
-
*
|
|
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
|
-
#
|
|
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
|
-
|
|
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.#
|
|
3512
|
-
|
|
3513
|
-
this.#
|
|
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({
|
|
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
|
|
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(
|
|
3566
|
-
this.#
|
|
3567
|
-
|
|
3568
|
-
|
|
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
|
-
|
|
3571
|
-
|
|
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 (
|
|
3575
|
-
|
|
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 (
|
|
3581
|
-
|
|
3582
|
-
|
|
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.#
|
|
3622
|
-
this.#
|
|
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.#
|
|
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
|
-
...
|
|
6601
|
+
...data,
|
|
6102
6602
|
...input.spans !== void 0 ? { spans: validateSpans(content, input.spans) } : {},
|
|
6103
|
-
...
|
|
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
|
-
|
|
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.
|
|
7773
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|