itd-api 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { _ as resolveFileStreamOptions, a as isRestorableSession, b as REFRESH_COOKIE, c as DEFAULT_URL_FILE_MAX_BYTES, d as downloadFile, f as fromStream, g as openUrlFile, h as normalizeMimeType, i as createRecordMultiStorage, l as FileTransferMode, m as isBoundedFileStream, n as controlledTokenStorage, o as scopedTokenStorage, p as fromUrl, r as createMultiTokenStorage, s as DEFAULT_FILE_STREAM_BUFFER_BYTES, t as MemoryMultiTokenStorage, u as boundedFileStream, v as AUTH_FLAG_COOKIE, x as REFRESH_COOKIE_PATH, y as CookieJar } from "./multi-storage-CyMe404l.js";
1
+ import { _ as CookieJar, a as isRestorableSession, c as boundedFileStream, d as fileTooLarge, f as optionalBytes, g as AUTH_FLAG_COOKIE, h as FileTransferMode, i as createRecordMultiStorage, l as isBoundedFileStream, m as DEFAULT_URL_FILE_MAX_BYTES, n as controlledTokenStorage, o as scopedTokenStorage, p as DEFAULT_FILE_STREAM_BUFFER_BYTES, r as createMultiTokenStorage, s as resolveFileStreamOptions, t as MemoryMultiTokenStorage, u as isReadableByteStream, v as REFRESH_COOKIE, y as REFRESH_COOKIE_PATH } from "./multi-storage-D4h6QA-w.js";
2
2
  import { A as isItdRateLimitError, C as isItdAuthError, D as isItdForbiddenError, E as isItdFileError, M as isItdValidationError, O as isItdNotFoundError, S as isItdApiError, T as isItdError, _ as ItdPhoneVerificationError, a as ItdApiError, b as ItdTimeoutError, c as ItdConfigError, d as ItdErrorKind, f as ItdFileError, g as ItdNotFoundError, h as ItdNetworkError, i as ItdAbortError, j as isItdServerError, k as isItdPhoneVerificationError, l as ItdConflictError, m as ItdForbiddenError, n as copySession, o as ItdApiErrorKind, p as ItdFileErrorReason, r as createTokenStorage, s as ItdAuthError, t as MemoryTokenStorage, u as ItdError, v as ItdRateLimitError, w as isItdConflictError, x as ItdValidationError, y as ItdServerError } from "./storage-D9tfHx7Z.js";
3
3
  import { a as isBlob, c as shouldSendCredentials, l as shouldUseCookieJar, n as RuntimeMode, o as isFile, r as createDeviceId, s as resolveFetch, t as DetectedRuntime, u as supportsStreamingBody } from "./runtime-DHxDn8gf.js";
4
4
  //#region src/core/emitter.ts
@@ -726,7 +726,7 @@ function normalizeBaseUrl(baseUrl) {
726
726
  //#endregion
727
727
  //#region src/core/version.ts
728
728
  /** Версия библиотеки. Попадает в `User-Agent`. */
729
- const LIBRARY_VERSION = "0.3.0";
729
+ const LIBRARY_VERSION = "0.4.0";
730
730
  //#endregion
731
731
  //#region src/core/config.ts
732
732
  /** Базовый URL API итд.com. Домен записан в punycode: `итд.com`. */
@@ -1051,15 +1051,271 @@ function withLayerHeaders(request, headers) {
1051
1051
  };
1052
1052
  }
1053
1053
  //#endregion
1054
- //#region src/core/plugins.ts
1055
- /** Пустой набор отдаётся, пока плагинов нет, чтобы не заводить объект на каждый запрос. */
1056
- const NO_KEYS = /* @__PURE__ */ new Set();
1054
+ //#region src/core/plugins/hooks.ts
1055
+ const REQUEST_HOOK_DISPATCHERS = /* @__PURE__ */ new WeakMap();
1056
+ const PLUGIN_HOOK_SCOPE = Symbol("itd-api.plugin-hooks");
1057
+ /** Создаёт динамический набор hooks, связанный с диспетчером registry. @internal */
1058
+ function createRequestHooks(dispatcher) {
1059
+ const hooks = {};
1060
+ REQUEST_HOOK_DISPATCHERS.set(hooks, dispatcher);
1061
+ return hooks;
1062
+ }
1063
+ /** Привязывает к запросу неизменяемый снимок plugin hooks. @internal */
1064
+ function withRequestHookScope(request, hooks) {
1065
+ const scoped = request;
1066
+ return scoped[PLUGIN_HOOK_SCOPE] === hooks ? scoped : {
1067
+ ...request,
1068
+ [PLUGIN_HOOK_SCOPE]: hooks
1069
+ };
1070
+ }
1071
+ /** Читает снимок plugin hooks, привязанный к логическому запросу. @internal */
1072
+ function requestHookScope(request) {
1073
+ return request[PLUGIN_HOOK_SCOPE] ?? [];
1074
+ }
1075
+ /**
1076
+ * Вызывает публичный хук, сохраняя привязанный к логическому запросу снимок плагинов.
1077
+ *
1078
+ * Обычные наборы хуков по-прежнему получают только публичный контекст. Дополнительный
1079
+ * аргумент используется исключительно внутренним составным набором PluginRegistry.
1080
+ *
1081
+ * @internal
1082
+ */
1083
+ async function dispatchRequestHook(hooks, field, context, request) {
1084
+ const dispatcher = REQUEST_HOOK_DISPATCHERS.get(hooks);
1085
+ if (dispatcher) {
1086
+ await dispatcher(field, context, request);
1087
+ return;
1088
+ }
1089
+ const hook = hooks[field];
1090
+ await hook?.(context);
1091
+ }
1092
+ //#endregion
1093
+ //#region src/core/retry.ts
1094
+ /**
1095
+ * Методы, повтор которых безопасен по определению.
1096
+ *
1097
+ * `DELETE` формально тоже идемпотентен, но его повтор после успеха вернёт `404`
1098
+ * и собьёт с толку — в список он не входит.
1099
+ */
1100
+ const IDEMPOTENT_METHODS = /* @__PURE__ */ new Set([
1101
+ "GET",
1102
+ "HEAD",
1103
+ "OPTIONS"
1104
+ ]);
1105
+ /**
1106
+ * Стоит ли повторять запрос после этой ошибки.
1107
+ *
1108
+ * Отдельно разобран `429`: он повторяется даже для запросов на запись, потому что
1109
+ * гарантирует, что запрос **не был обработан**. Обрыв сети и `5xx` такой гарантии не дают —
1110
+ * сервер мог успеть создать пост, — поэтому запись по умолчанию не повторяется.
1111
+ */
1112
+ function isRetryable(error, method, retryWrites, retryNetworkWrite) {
1113
+ if (error instanceof ItdAbortError) return false;
1114
+ const safeToRepeat = retryWrites || IDEMPOTENT_METHODS.has(method);
1115
+ if (error instanceof ItdApiError) {
1116
+ if (error.status === 429) return true;
1117
+ if (error.status >= 500) return safeToRepeat;
1118
+ return false;
1119
+ }
1120
+ if (error instanceof ItdNetworkError || error instanceof ItdTimeoutError) return safeToRepeat || retryNetworkWrite;
1121
+ if (error instanceof ItdFileError) return error.retryable && (safeToRepeat || retryNetworkWrite);
1122
+ return false;
1123
+ }
1124
+ /** Экспоненциальная пауза со случайным разбросом. */
1125
+ function backoffDelay(attempt, options, random) {
1126
+ const exponential = options.baseDelay * 2 ** (attempt - 1);
1127
+ const capped = Math.min(exponential, options.maxDelay);
1128
+ const spread = capped * options.jitter * (random() * 2 - 1);
1129
+ return Math.max(0, Math.round(capped + spread));
1130
+ }
1131
+ /**
1132
+ * Собирает планировщик повторов для транспорта.
1133
+ *
1134
+ * Поведение при `Retry-After`: пауза, названная сервером, соблюдается точно — она
1135
+ * авторитетнее нашего расчёта. Но если сервер просит ждать дольше, чем `maxDelay`,
1136
+ * повтор **не выполняется вовсе**: молча спать десять минут внутри вызова библиотека
1137
+ * не должна, лучше отдать {@link ItdRateLimitError} и дать решить вызывающему коду.
1138
+ *
1139
+ * @param options настройки повторов после подстановки значений по умолчанию
1140
+ * @param random источник случайности; подменяется в тестах ради предсказуемости
1141
+ *
1142
+ * @example
1143
+ * ```ts
1144
+ * const scheduler = createRetryScheduler(config.retry);
1145
+ * const delay = scheduler(error, 1, 'GET'); // 500 мс ± 30%
1146
+ * ```
1147
+ */
1148
+ function createRetryScheduler(options, random = Math.random) {
1149
+ return (error, attempt, method, retryNetworkWrite = false) => {
1150
+ if (attempt >= options.attempts) return void 0;
1151
+ if (options.shouldRetry) return options.shouldRetry(error, attempt) ? backoffDelay(attempt, options, random) : void 0;
1152
+ if (!isRetryable(error, method, options.retryWrites, retryNetworkWrite)) return void 0;
1153
+ if (error instanceof ItdApiError && error.retryAfter !== void 0) return error.retryAfter > options.maxDelay ? void 0 : error.retryAfter;
1154
+ return backoffDelay(attempt, options, random);
1155
+ };
1156
+ }
1157
+ //#endregion
1158
+ //#region src/core/middleware.ts
1159
+ /** Ожидание повтора, которое уважает отмену запроса. */
1160
+ function sleep(clock, ms, signal) {
1161
+ if (!signal) return new Promise((resolve) => clock.schedule(resolve, ms));
1162
+ if (signal.aborted) return Promise.reject(new ItdAbortError("Запрос отменён во время ожидания повтора"));
1163
+ return new Promise((resolve, reject) => {
1164
+ const cancel = clock.schedule(() => {
1165
+ signal.removeEventListener("abort", onAbort);
1166
+ resolve();
1167
+ }, ms);
1168
+ const onAbort = () => {
1169
+ cancel();
1170
+ reject(new ItdAbortError("Запрос отменён во время ожидания повтора"));
1171
+ };
1172
+ signal.addEventListener("abort", onAbort, { once: true });
1173
+ });
1174
+ }
1175
+ /**
1176
+ * Слой очереди: ограничение конкурентности и частоты.
1177
+ *
1178
+ * `skipQueue` пропускает запрос мимо очереди — так поступают служебные запросы, которые
1179
+ * порождены изнутри другого запроса и не могут ждать освободившегося слота.
1180
+ *
1181
+ * Очередь выбирается по запросу: у каждого сервиса платформы свой хост и свой лимит.
1182
+ */
1183
+ function createQueueMiddleware(schedule) {
1184
+ return (request, next) => request.skipQueue ? next(request) : schedule(request, () => next(request));
1185
+ }
1186
+ /**
1187
+ * Слой плагинов.
1188
+ *
1189
+ * Стоит снаружи повторов и внутри очереди: плагин должен увидеть запрос и ответ по одному
1190
+ * разу, независимо от числа попыток, — иначе, например, текст поста зашифруется дважды.
1191
+ */
1192
+ function createPluginsMiddleware(plugins) {
1193
+ return (request, next) => plugins.run(request, next);
1194
+ }
1195
+ /**
1196
+ * Слой сервисов.
1197
+ *
1198
+ * Запросу с полем `service` подставляет хост сервиса, его заголовки и `skipAuth`, если
1199
+ * сервис объявлен публичным. Заданный у запроса `baseUrl` не трогает.
1200
+ *
1201
+ * Стоит снаружи повторов и авторизации, чтобы выставленный здесь `skipAuth` был ей виден.
1202
+ */
1203
+ function createServicesMiddleware(registry) {
1204
+ return async (request, next) => {
1205
+ const service = request.service === void 0 ? void 0 : registry.require(request.service);
1206
+ let prepared = request;
1207
+ if (request.baseUrl !== void 0) {
1208
+ const baseUrl = normalizeBaseUrl(request.baseUrl);
1209
+ if (baseUrl !== request.baseUrl) prepared = {
1210
+ ...prepared,
1211
+ baseUrl
1212
+ };
1213
+ if (!(service?.baseUrl === baseUrl ? service.auth !== false : registry.isPrimarySite(baseUrl)) && prepared.skipAuth === void 0) prepared = {
1214
+ ...prepared,
1215
+ skipAuth: true
1216
+ };
1217
+ }
1218
+ if (!service) return next(prepared);
1219
+ if (prepared.baseUrl === void 0) prepared = {
1220
+ ...prepared,
1221
+ baseUrl: service.baseUrl
1222
+ };
1223
+ if (service.headers) prepared = withLayerHeaders(prepared, service.headers);
1224
+ if (service.auth === false && prepared.skipAuth === void 0) prepared = {
1225
+ ...prepared,
1226
+ skipAuth: true
1227
+ };
1228
+ return next(prepared);
1229
+ };
1230
+ }
1231
+ async function applyAuth(request, deps) {
1232
+ if (request.skipAuth) return request;
1233
+ const headers = await deps.getAuthHeaders();
1234
+ return Object.keys(headers).length > 0 ? withLayerHeaders(request, headers) : request;
1235
+ }
1057
1236
  /**
1058
- * Имена, которые плагин заявить не может.
1237
+ * Слой авторизации.
1059
1238
  *
1060
- * Заявленные опции ресурсы переносят в описание запроса поверх собранных полей, поэтому
1061
- * имя из {@link RawRequestOptions} подменило бы путь, тело или заголовки любого вызова.
1239
+ * Подставляет заголовок `Authorization` и обрабатывает `401`: обновляет токен и повторяет
1240
+ * запрос ровно один раз. Стоит внутри повторов, поэтому обычным попыткам он не виден —
1241
+ * они уже работают со свежим токеном.
1062
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
+ }
1270
+ /**
1271
+ * Слой повторов.
1272
+ *
1273
+ * Ответ `429` обрабатывается отдельно от прочих ошибок лестницей пауз и с придержанием
1274
+ * всей очереди; сетевые сбои и `5xx` — экспоненциальным откатом. Настройка `retry`
1275
+ * у отдельного запроса имеет приоритет над глобальной.
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
1063
1319
  const RESERVED_OPTION_KEYS = /* @__PURE__ */ new Set([
1064
1320
  "signal",
1065
1321
  "timeout",
@@ -1100,7 +1356,8 @@ function validateNameList(plugin, field) {
1100
1356
  seen.add(value);
1101
1357
  }
1102
1358
  }
1103
- function validateHooks(plugin, hooks) {
1359
+ /** Проверяет набор hooks, переданный плагином. @internal */
1360
+ function validatePluginHooks(plugin, hooks) {
1104
1361
  if (typeof hooks !== "object" || hooks === null) throw new ItdConfigError(`плагин «${plugin}» передал в useHooks() не объект`);
1105
1362
  for (const field of HOOK_FIELDS) if (hooks[field] !== void 0 && typeof hooks[field] !== "function") throw new ItdConfigError(`плагин «${plugin}»: useHooks().${field} должен быть функцией`);
1106
1363
  }
@@ -1187,30 +1444,14 @@ function orderPluginDefinitions(plugins) {
1187
1444
  if (ordered.length !== entries.length) throw new ItdConfigError(`циклический порядок плагинов: ${entries.filter(({ plugin }) => (indegree.get(plugin.name) ?? 0) > 0).map(({ plugin }) => plugin.name).join(" → ")}`);
1188
1445
  return ordered;
1189
1446
  }
1190
- /** Проверяет, можно ли удалить плагин, не нарушив обязательные зависимости. @internal */
1447
+ /** Проверяет, можно ли удалить плагин без нарушения обязательных зависимостей. @internal */
1191
1448
  function assertPluginRemovable(plugins, name) {
1192
1449
  const dependent = plugins.find((plugin) => plugin.requires?.includes(name));
1193
1450
  if (dependent) throw new ItdConfigError(`нельзя отключить плагин «${name}»: от него зависит «${dependent.name}»`);
1194
1451
  }
1195
- const REQUEST_HOOK_DISPATCHERS = /* @__PURE__ */ new WeakMap();
1196
- const PLUGIN_HOOK_SCOPE = Symbol("itd-api.plugin-hooks");
1197
- /**
1198
- * Вызывает публичный хук, сохраняя привязанный к логическому запросу снимок плагинов.
1199
- *
1200
- * Обычные наборы хуков по-прежнему получают только публичный контекст. Дополнительный
1201
- * аргумент используется исключительно внутренним составным набором PluginRegistry.
1202
- *
1203
- * @internal
1204
- */
1205
- async function dispatchRequestHook(hooks, field, context, request) {
1206
- const dispatcher = REQUEST_HOOK_DISPATCHERS.get(hooks);
1207
- if (dispatcher) {
1208
- await dispatcher(field, context, request);
1209
- return;
1210
- }
1211
- const hook = hooks[field];
1212
- await hook?.(context);
1213
- }
1452
+ //#endregion
1453
+ //#region src/core/plugins/registry.ts
1454
+ const NO_KEYS = /* @__PURE__ */ new Set();
1214
1455
  /**
1215
1456
  * Список подключённых плагинов и собранная из них цепочка обёрток.
1216
1457
  *
@@ -1268,7 +1509,7 @@ var PluginRegistry = class {
1268
1509
  transformers.push(transformer);
1269
1510
  },
1270
1511
  useHooks: (value) => {
1271
- validateHooks(plugin.name, value);
1512
+ validatePluginHooks(plugin.name, value);
1272
1513
  hooks.push({ ...value });
1273
1514
  }
1274
1515
  });
@@ -1348,293 +1589,62 @@ var PluginRegistry = class {
1348
1589
  *
1349
1590
  * Возвращённый объект динамический: подключение и отключение плагина начинает действовать
1350
1591
  * со следующего логического запроса без пересоздания транспорта.
1351
- */
1352
- hooks(base) {
1353
- const hooks = {};
1354
- REQUEST_HOOK_DISPATCHERS.set(hooks, ((field, context, request) => this.#runHook(field, context, request, base)));
1355
- return hooks;
1356
- }
1357
- /**
1358
- * Прогоняет запрос через цепочку обёрток.
1359
- *
1360
- * Снимок цепочки берётся в начале: `unuse()` влияет на новые запросы, но не обрывает
1361
- * уже выполняющийся посередине.
1362
- *
1363
- * @param execute настоящий запрос, вызывается самой внутренней обёрткой
1364
- */
1365
- async run(request, execute) {
1366
- const entries = [...this.#ordered];
1367
- for (const entry of entries) entry.activeRequests += 1;
1368
- const hookScope = entries.flatMap((entry) => entry.hooks);
1369
- const scoped = (current) => current[PLUGIN_HOOK_SCOPE] === hookScope ? current : {
1370
- ...current,
1371
- [PLUGIN_HOOK_SCOPE]: hookScope
1372
- };
1373
- const chain = entries.flatMap((entry) => entry.transformers).reduceRight((next, transformer) => (current) => transformer(scoped(current), (prepared) => next(scoped(prepared))), (current) => execute(scoped(current)));
1374
- try {
1375
- return await chain(scoped(request));
1376
- } finally {
1377
- for (const entry of entries) {
1378
- entry.activeRequests -= 1;
1379
- if (entry.activeRequests === 0) {
1380
- entry.finishDrain?.();
1381
- entry.finishDrain = void 0;
1382
- entry.drain = void 0;
1383
- }
1384
- }
1385
- }
1386
- }
1387
- #rebuildOptionKeys() {
1388
- this.#optionKeys.clear();
1389
- for (const { plugin } of this.#ordered) for (const key of plugin.optionKeys ?? []) this.#optionKeys.add(key);
1390
- }
1391
- #waitForDrain(entry) {
1392
- if (entry.activeRequests === 0) return Promise.resolve();
1393
- entry.drain ??= new Promise((resolve) => {
1394
- entry.finishDrain = resolve;
1395
- });
1396
- return entry.drain;
1397
- }
1398
- #trackCleanup(cleanup) {
1399
- this.#cleanups.add(cleanup);
1400
- cleanup.then(() => this.#cleanups.delete(cleanup), () => this.#cleanups.delete(cleanup));
1401
- return cleanup;
1402
- }
1403
- async #runHook(field, context, request, base) {
1404
- const baseHook = base[field];
1405
- await baseHook?.(context);
1406
- const scope = request[PLUGIN_HOOK_SCOPE] ?? [];
1407
- for (const hooks of scope) {
1408
- const hook = hooks[field];
1409
- await hook?.(context);
1410
- }
1411
- }
1412
- };
1413
- //#endregion
1414
- //#region src/core/retry.ts
1415
- /**
1416
- * Методы, повтор которых безопасен по определению.
1417
- *
1418
- * `DELETE` формально тоже идемпотентен, но его повтор после успеха вернёт `404`
1419
- * и собьёт с толку — в список он не входит.
1420
- */
1421
- const IDEMPOTENT_METHODS = /* @__PURE__ */ new Set([
1422
- "GET",
1423
- "HEAD",
1424
- "OPTIONS"
1425
- ]);
1426
- /**
1427
- * Стоит ли повторять запрос после этой ошибки.
1428
- *
1429
- * Отдельно разобран `429`: он повторяется даже для запросов на запись, потому что
1430
- * гарантирует, что запрос **не был обработан**. Обрыв сети и `5xx` такой гарантии не дают —
1431
- * сервер мог успеть создать пост, — поэтому запись по умолчанию не повторяется.
1432
- */
1433
- function isRetryable(error, method, retryWrites, retryNetworkWrite) {
1434
- if (error instanceof ItdAbortError) return false;
1435
- const safeToRepeat = retryWrites || IDEMPOTENT_METHODS.has(method);
1436
- if (error instanceof ItdApiError) {
1437
- if (error.status === 429) return true;
1438
- if (error.status >= 500) return safeToRepeat;
1439
- return false;
1440
- }
1441
- if (error instanceof ItdNetworkError || error instanceof ItdTimeoutError) return safeToRepeat || retryNetworkWrite;
1442
- if (error instanceof ItdFileError) return error.retryable && (safeToRepeat || retryNetworkWrite);
1443
- return false;
1444
- }
1445
- /** Экспоненциальная пауза со случайным разбросом. */
1446
- function backoffDelay(attempt, options, random) {
1447
- const exponential = options.baseDelay * 2 ** (attempt - 1);
1448
- const capped = Math.min(exponential, options.maxDelay);
1449
- const spread = capped * options.jitter * (random() * 2 - 1);
1450
- return Math.max(0, Math.round(capped + spread));
1451
- }
1452
- /**
1453
- * Собирает планировщик повторов для транспорта.
1454
- *
1455
- * Поведение при `Retry-After`: пауза, названная сервером, соблюдается точно — она
1456
- * авторитетнее нашего расчёта. Но если сервер просит ждать дольше, чем `maxDelay`,
1457
- * повтор **не выполняется вовсе**: молча спать десять минут внутри вызова библиотека
1458
- * не должна, лучше отдать {@link ItdRateLimitError} и дать решить вызывающему коду.
1459
- *
1460
- * @param options настройки повторов после подстановки значений по умолчанию
1461
- * @param random источник случайности; подменяется в тестах ради предсказуемости
1462
- *
1463
- * @example
1464
- * ```ts
1465
- * const scheduler = createRetryScheduler(config.retry);
1466
- * const delay = scheduler(error, 1, 'GET'); // 500 мс ± 30%
1467
- * ```
1468
- */
1469
- function createRetryScheduler(options, random = Math.random) {
1470
- return (error, attempt, method, retryNetworkWrite = false) => {
1471
- if (attempt >= options.attempts) return void 0;
1472
- if (options.shouldRetry) return options.shouldRetry(error, attempt) ? backoffDelay(attempt, options, random) : void 0;
1473
- if (!isRetryable(error, method, options.retryWrites, retryNetworkWrite)) return void 0;
1474
- if (error instanceof ItdApiError && error.retryAfter !== void 0) return error.retryAfter > options.maxDelay ? void 0 : error.retryAfter;
1475
- return backoffDelay(attempt, options, random);
1476
- };
1477
- }
1478
- //#endregion
1479
- //#region src/core/middleware.ts
1480
- /** Ожидание повтора, которое уважает отмену запроса. */
1481
- function sleep(clock, ms, signal) {
1482
- if (!signal) return new Promise((resolve) => clock.schedule(resolve, ms));
1483
- if (signal.aborted) return Promise.reject(new ItdAbortError("Запрос отменён во время ожидания повтора"));
1484
- return new Promise((resolve, reject) => {
1485
- const cancel = clock.schedule(() => {
1486
- signal.removeEventListener("abort", onAbort);
1487
- resolve();
1488
- }, ms);
1489
- const onAbort = () => {
1490
- cancel();
1491
- reject(new ItdAbortError("Запрос отменён во время ожидания повтора"));
1492
- };
1493
- signal.addEventListener("abort", onAbort, { once: true });
1494
- });
1495
- }
1496
- /**
1497
- * Слой очереди: ограничение конкурентности и частоты.
1498
- *
1499
- * `skipQueue` пропускает запрос мимо очереди — так поступают служебные запросы, которые
1500
- * порождены изнутри другого запроса и не могут ждать освободившегося слота.
1501
- *
1502
- * Очередь выбирается по запросу: у каждого сервиса платформы свой хост и свой лимит.
1503
- */
1504
- function createQueueMiddleware(schedule) {
1505
- return (request, next) => request.skipQueue ? next(request) : schedule(request, () => next(request));
1506
- }
1507
- /**
1508
- * Слой плагинов.
1509
- *
1510
- * Стоит снаружи повторов и внутри очереди: плагин должен увидеть запрос и ответ по одному
1511
- * разу, независимо от числа попыток, — иначе, например, текст поста зашифруется дважды.
1512
- */
1513
- function createPluginsMiddleware(plugins) {
1514
- return (request, next) => plugins.run(request, next);
1515
- }
1516
- /**
1517
- * Слой сервисов.
1518
- *
1519
- * Запросу с полем `service` подставляет хост сервиса, его заголовки и `skipAuth`, если
1520
- * сервис объявлен публичным. Заданный у запроса `baseUrl` не трогает.
1521
- *
1522
- * Стоит снаружи повторов и авторизации, чтобы выставленный здесь `skipAuth` был ей виден.
1523
- */
1524
- function createServicesMiddleware(registry) {
1525
- return async (request, next) => {
1526
- const service = request.service === void 0 ? void 0 : registry.require(request.service);
1527
- let prepared = request;
1528
- if (request.baseUrl !== void 0) {
1529
- const baseUrl = normalizeBaseUrl(request.baseUrl);
1530
- if (baseUrl !== request.baseUrl) prepared = {
1531
- ...prepared,
1532
- baseUrl
1533
- };
1534
- if (!(service?.baseUrl === baseUrl ? service.auth !== false : registry.isPrimarySite(baseUrl)) && prepared.skipAuth === void 0) prepared = {
1535
- ...prepared,
1536
- skipAuth: true
1537
- };
1538
- }
1539
- if (!service) return next(prepared);
1540
- if (prepared.baseUrl === void 0) prepared = {
1541
- ...prepared,
1542
- baseUrl: service.baseUrl
1543
- };
1544
- if (service.headers) prepared = withLayerHeaders(prepared, service.headers);
1545
- if (service.auth === false && prepared.skipAuth === void 0) prepared = {
1546
- ...prepared,
1547
- skipAuth: true
1548
- };
1549
- return next(prepared);
1550
- };
1551
- }
1552
- async function applyAuth(request, deps) {
1553
- if (request.skipAuth) return request;
1554
- const headers = await deps.getAuthHeaders();
1555
- return Object.keys(headers).length > 0 ? withLayerHeaders(request, headers) : request;
1556
- }
1557
- /**
1558
- * Слой авторизации.
1559
- *
1560
- * Подставляет заголовок `Authorization` и обрабатывает `401`: обновляет токен и повторяет
1561
- * запрос ровно один раз. Стоит внутри повторов, поэтому обычным попыткам он не виден —
1562
- * они уже работают со свежим токеном.
1563
- */
1564
- function createAuthMiddleware(deps) {
1565
- return async (request, next) => {
1566
- const authorized = await applyAuth(request, deps);
1592
+ */
1593
+ hooks(base) {
1594
+ return createRequestHooks((field, context, request) => this.#runHook(field, context, request, base));
1595
+ }
1596
+ /**
1597
+ * Прогоняет запрос через цепочку обёрток.
1598
+ *
1599
+ * Снимок цепочки берётся в начале: `unuse()` влияет на новые запросы, но не обрывает
1600
+ * уже выполняющийся посередине.
1601
+ *
1602
+ * @param execute настоящий запрос, вызывается самой внутренней обёрткой
1603
+ */
1604
+ async run(request, execute) {
1605
+ const entries = [...this.#ordered];
1606
+ for (const entry of entries) entry.activeRequests += 1;
1607
+ const hookScope = entries.flatMap((entry) => entry.hooks);
1608
+ const scoped = (current) => withRequestHookScope(current, hookScope);
1609
+ const chain = entries.flatMap((entry) => entry.transformers).reduceRight((next, transformer) => (current) => transformer(scoped(current), (prepared) => next(scoped(prepared))), (current) => execute(scoped(current)));
1567
1610
  try {
1568
- return await next(authorized);
1569
- } catch (error) {
1570
- if (request.skipAuthRefresh || !deps.autoRefresh || !isItdApiError(error) || error.status !== 401) throw error;
1571
- if (!await deps.onUnauthorized()) throw error;
1572
- return next(await applyAuth({
1573
- ...request,
1574
- skipAuthRefresh: true
1575
- }, deps));
1576
- }
1577
- };
1578
- }
1579
- /**
1580
- * Выбирает планировщик отката для конкретного запроса.
1581
- *
1582
- * `retry` у запроса переопределяет глобальную настройку: `false` выключает повторы,
1583
- * объект задаёт свои. Обработка `429` от этого не зависит — она общая.
1584
- */
1585
- function resolveBackoff(retry, global) {
1586
- if (retry === void 0) return global;
1587
- if (retry === false) return void 0;
1588
- const resolved = resolveRetry(retry);
1589
- return resolved ? createRetryScheduler(resolved) : void 0;
1590
- }
1591
- /**
1592
- * Слой повторов.
1593
- *
1594
- * Ответ `429` обрабатывается отдельно от прочих ошибок лестницей пауз и с придержанием
1595
- * всей очереди; сетевые сбои и `5xx` — экспоненциальным откатом. Настройка `retry`
1596
- * у отдельного запроса имеет приоритет над глобальной.
1597
- */
1598
- function createRetryMiddleware(deps) {
1599
- const globalScheduler = deps.retry ? createRetryScheduler(deps.retry) : void 0;
1600
- const nextDelay = (error, attempt, request, method, backoff) => {
1601
- if (isItdRateLimitError(error)) {
1602
- const wait = error.retryAfter ?? deps.rateLimitDelays[attempt - 1];
1603
- if (wait === void 0) return void 0;
1604
- deps.pauseQueue?.(wait, request);
1605
- deps.logger?.debug(`лимит частоты, попытка ${attempt + 1} через ${wait} мс`);
1606
- return wait;
1611
+ return await chain(scoped(request));
1612
+ } finally {
1613
+ for (const entry of entries) {
1614
+ entry.activeRequests -= 1;
1615
+ if (entry.activeRequests === 0) {
1616
+ entry.finishDrain?.();
1617
+ entry.finishDrain = void 0;
1618
+ entry.drain = void 0;
1619
+ }
1620
+ }
1607
1621
  }
1608
- return backoff?.(error, attempt, method, request.retryNetworkWrite ?? false);
1609
- };
1610
- return async (request, next) => {
1611
- const method = request.method.toUpperCase();
1612
- const backoff = resolveBackoff(request.retry, globalScheduler);
1613
- for (let attempt = 1;; attempt++) try {
1614
- return await next({
1615
- ...request,
1616
- attempt
1617
- });
1618
- } catch (error) {
1619
- const delay = nextDelay(error, attempt, request, method, backoff);
1620
- if (delay === void 0) throw error;
1621
- await dispatchRequestHook(deps.hooks, "onRetry", {
1622
- method,
1623
- path: request.path,
1624
- url: deps.buildUrl(request),
1625
- headers: new Headers({
1626
- ...request.layerHeaders,
1627
- ...request.headers
1628
- }),
1629
- attempt,
1630
- error,
1631
- delay
1632
- }, request);
1633
- deps.logger?.debug(`повтор ${method} ${request.path}, попытка ${attempt + 1} через ${delay} мс`);
1634
- await sleep(deps.clock ?? systemClock, delay, request.signal);
1622
+ }
1623
+ #rebuildOptionKeys() {
1624
+ this.#optionKeys.clear();
1625
+ for (const { plugin } of this.#ordered) for (const key of plugin.optionKeys ?? []) this.#optionKeys.add(key);
1626
+ }
1627
+ #waitForDrain(entry) {
1628
+ if (entry.activeRequests === 0) return Promise.resolve();
1629
+ entry.drain ??= new Promise((resolve) => {
1630
+ entry.finishDrain = resolve;
1631
+ });
1632
+ return entry.drain;
1633
+ }
1634
+ #trackCleanup(cleanup) {
1635
+ this.#cleanups.add(cleanup);
1636
+ cleanup.then(() => this.#cleanups.delete(cleanup), () => this.#cleanups.delete(cleanup));
1637
+ return cleanup;
1638
+ }
1639
+ async #runHook(field, context, request, base) {
1640
+ const baseHook = base[field];
1641
+ await baseHook?.(context);
1642
+ for (const hooks of requestHookScope(request)) {
1643
+ const hook = hooks[field];
1644
+ await hook?.(context);
1635
1645
  }
1636
- };
1637
- }
1646
+ }
1647
+ };
1638
1648
  //#endregion
1639
1649
  //#region src/core/rate-limit.ts
1640
1650
  /** Ошибка отмены запроса, который ещё не дошёл до транспорта. */
@@ -4924,6 +4934,140 @@ var CommentsResource = class extends BaseResource {
4924
4934
  }
4925
4935
  };
4926
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
4927
5071
  //#region src/core/mime.ts
4928
5072
  /** Изображения, которые принимает `POST /api/files/upload`. */
4929
5073
  const IMAGE_MIME_TYPES = Object.freeze([
@@ -5469,7 +5613,6 @@ var NotificationsResource = class extends BaseResource {
5469
5613
  };
5470
5614
  //#endregion
5471
5615
  //#region src/core/time.ts
5472
- /** Отметка времени без часового пояса: `2026-07-23 23:14:25`, возможно с долями секунды. */
5473
5616
  const NAIVE_STAMP = /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}(?:\.\d+)?)$/;
5474
5617
  /**
5475
5618
  * Приводит отметку времени без часового пояса к ISO-8601, считая её временем UTC.
@@ -5488,6 +5631,21 @@ function utcStampToIso(value) {
5488
5631
  const iso = `${match[1]}T${match[2]}Z`;
5489
5632
  return Number.isFinite(Date.parse(iso)) ? iso : value;
5490
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
+ }
5491
5649
  //#endregion
5492
5650
  //#region src/resources/platform.ts
5493
5651
  /** Приводит `last_checked` каждого сервиса к ISO. Остальное остаётся как прислал сервер. */
@@ -6418,7 +6576,7 @@ function resolvePoll(input) {
6418
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;
6419
6577
  const BUILD_UPDATE = Symbol.for("itd.postBuilder.update");
6420
6578
  /**
6421
- * Проверяет данные поста.
6579
+ * Проверяет и нормализует данные поста.
6422
6580
  *
6423
6581
  * Отдельного внимания заслуживает `wallRecipientId`: API принимает там **только UUID**,
6424
6582
  * а имя пользователя молча приводит к ошибке на сервере. Проверка здесь превращает
@@ -6438,10 +6596,11 @@ function validatePost(input) {
6438
6596
  if (wallRecipientId !== void 0 && wallRecipientId !== null) {
6439
6597
  if (!UUID_PATTERN.test(wallRecipientId)) throw new ItdConfigError(`wallRecipientId должен быть UUID, а не именем пользователя (получено: «${wallRecipientId}»). Идентификатор можно взять из профиля: (await itd.users.get(username)).id`);
6440
6598
  }
6599
+ const { poll: inputPoll, ...data } = input;
6441
6600
  return {
6442
- ...input,
6601
+ ...data,
6443
6602
  ...input.spans !== void 0 ? { spans: validateSpans(content, input.spans) } : {},
6444
- ...input.poll !== void 0 ? { poll: resolvePoll(input.poll) } : {}
6603
+ ...inputPoll !== void 0 ? { poll: resolvePoll(inputPoll) } : {}
6445
6604
  };
6446
6605
  }
6447
6606
  /**
@@ -6638,7 +6797,8 @@ function post(content) {
6638
6797
  }
6639
6798
  /** Приводит любую форму входа к готовым данным поста. */
6640
6799
  function resolvePost(input) {
6641
- return resolveInput(input, () => post(), validatePost);
6800
+ const resolved = typeof input === "function" ? input(post()) : input;
6801
+ return isBuilder(resolved) ? validatePost(resolved.build()) : validatePost(resolved);
6642
6802
  }
6643
6803
  function validatePostUpdate(input) {
6644
6804
  if (!input || typeof input !== "object") throw new ItdConfigError("Для обновления поста нужен объект с явно заданным content");
@@ -8650,6 +8810,92 @@ function createAccounts(options = {}) {
8650
8810
  return new ItdAccounts(options);
8651
8811
  }
8652
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
8653
8899
  //#region src/notifications/text.ts
8654
8900
  /** Имя, которое подставляется, если участник неизвестен. */
8655
8901
  const UNKNOWN_ACTOR = "Пользователь";
@@ -9066,54 +9312,6 @@ function renderSpans(content, spans = [], options = {}) {
9066
9312
  return result;
9067
9313
  }
9068
9314
  //#endregion
9069
- //#region src/types/models.ts
9070
- /**
9071
- * Свой ли это профиль.
9072
- *
9073
- * @example
9074
- * ```ts
9075
- * if (isMyProfile(profile)) console.log(profile.subscription.isActive);
9076
- * ```
9077
- */
9078
- function isMyProfile(profile) {
9079
- return "subscription" in profile;
9080
- }
9081
- /** Глубина истории статуса в сутках. Столько элементов отдаёт {@link statusDays}. */
9082
- const STATUS_WINDOW_DAYS = 90;
9083
- /**
9084
- * Разбирает дату API в объект `Date`.
9085
- *
9086
- * @returns `null`, если строки нет или она не разбирается
9087
- *
9088
- * @example
9089
- * ```ts
9090
- * const created = toDate(post.createdAt);
9091
- * ```
9092
- */
9093
- function toDate(value) {
9094
- if (!value) return null;
9095
- const date = new Date(value);
9096
- return Number.isFinite(date.getTime()) ? date : null;
9097
- }
9098
- /**
9099
- * Разворачивает историю сервиса в массив на 90 суток.
9100
- * Сутки без данных становятся `null`.
9101
- *
9102
- * @returns массив, где индекс — сколько суток назад: `[0]` — сегодня
9103
- *
9104
- * @example
9105
- * ```ts
9106
- * const status = await itd.platform.status();
9107
- * const days = statusDays(status.services[0]);
9108
- *
9109
- * days[0]?.uptime; // доступность за сегодня
9110
- * days.filter((day) => day === null).length; // за сколько суток данных нет
9111
- * ```
9112
- */
9113
- function statusDays(service) {
9114
- return Array.from({ length: STATUS_WINDOW_DAYS }, (_, index) => service.days[String(index)] ?? null);
9115
- }
9116
- //#endregion
9117
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 };
9118
9316
 
9119
9317
  //# sourceMappingURL=index.js.map