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/README.md +3 -2
- package/dist/index.cjs +572 -374
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +833 -791
- package/dist/index.d.ts +833 -791
- package/dist/index.js +565 -367
- 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
|
|
@@ -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.
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
/** Проверяет, можно ли удалить
|
|
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
|
-
|
|
1196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
*
|
|
1359
|
-
*
|
|
1360
|
-
*
|
|
1361
|
-
*
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
const
|
|
1367
|
-
|
|
1368
|
-
const
|
|
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
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
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
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
const
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
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
|
-
...
|
|
6601
|
+
...data,
|
|
6443
6602
|
...input.spans !== void 0 ? { spans: validateSpans(content, input.spans) } : {},
|
|
6444
|
-
...
|
|
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
|
-
|
|
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
|