itd-api 0.3.0 → 0.5.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 +3329 -1795
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1640 -1092
- package/dist/index.d.ts +1640 -1092
- package/dist/index.js +3249 -1730
- package/dist/index.js.map +1 -1
- package/dist/{multi-storage-CyMe404l.js → multi-storage-B0r0AInH.js} +151 -281
- package/dist/multi-storage-B0r0AInH.js.map +1 -0
- package/dist/{multi-storage-BhcA2Izn.d.ts → multi-storage-CLqmzq07.d.ts} +14 -54
- package/dist/{multi-storage-D1keK2Op.cjs → multi-storage-mBuCOVZY.cjs} +188 -294
- package/dist/multi-storage-mBuCOVZY.cjs.map +1 -0
- package/dist/{multi-storage-NDqzRQcD.d.cts → multi-storage-s_PcWPGH.d.cts} +14 -54
- package/dist/node.cjs +62 -53
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +19 -5
- package/dist/node.d.ts +19 -5
- package/dist/node.js +62 -54
- package/dist/node.js.map +1 -1
- package/dist/{storage-D9tfHx7Z.js → storage-DWrK3Z4M.js} +201 -18
- package/dist/storage-DWrK3Z4M.js.map +1 -0
- package/dist/storage-Doe3lpFQ.d.cts +152 -0
- package/dist/storage-Doe3lpFQ.d.ts +152 -0
- package/dist/{storage-ycBqLBRB.cjs → storage-dF8Tio5y.cjs} +254 -17
- package/dist/storage-dF8Tio5y.cjs.map +1 -0
- package/dist/web.cjs +143 -40
- package/dist/web.cjs.map +1 -1
- package/dist/web.d.cts +40 -3
- package/dist/web.d.ts +40 -3
- package/dist/web.js +141 -41
- package/dist/web.js.map +1 -1
- package/package.json +3 -3
- package/dist/multi-storage-CyMe404l.js.map +0 -1
- package/dist/multi-storage-D1keK2Op.cjs.map +0 -1
- package/dist/runtime-CFEsf-jD.cjs +0 -185
- package/dist/runtime-CFEsf-jD.cjs.map +0 -1
- package/dist/runtime-DHxDn8gf.js +0 -126
- package/dist/runtime-DHxDn8gf.js.map +0 -1
- package/dist/storage-BjNRlkbE.d.cts +0 -82
- package/dist/storage-BjNRlkbE.d.ts +0 -82
- package/dist/storage-D9tfHx7Z.js.map +0 -1
- package/dist/storage-ycBqLBRB.cjs.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as ItdConfigError,
|
|
1
|
+
import { a as collectKeyValueStoreKeys, b as ItdFileErrorReason, c as isEnumerableKeyValueStore, h as ItdConfigError, i as MemoryKeyValueStore, n as copySession, o as createKeyValueStore, y as ItdFileError } from "./storage-DWrK3Z4M.js";
|
|
2
2
|
//#region node_modules/set-cookie-parser/lib/set-cookie.js
|
|
3
3
|
var defaultParseOptions = {
|
|
4
4
|
decodeValues: true,
|
|
@@ -139,6 +139,102 @@ parseSetCookie.parse = parseSetCookie;
|
|
|
139
139
|
parseSetCookie.parseString = parseString;
|
|
140
140
|
parseSetCookie.splitCookiesString = splitCookiesString;
|
|
141
141
|
//#endregion
|
|
142
|
+
//#region src/core/url.ts
|
|
143
|
+
/**
|
|
144
|
+
* Собирает строку запроса.
|
|
145
|
+
*
|
|
146
|
+
* Правила:
|
|
147
|
+
* - `undefined` и `null` пропускаются целиком — не нужно чистить объект перед вызовом;
|
|
148
|
+
* - `boolean` превращается в `true` / `false`;
|
|
149
|
+
* - массив повторяет ключ (`ids=1&ids=2`).
|
|
150
|
+
*
|
|
151
|
+
* @returns строка вида `?a=1&b=2` либо пустая строка, если параметров нет
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```ts
|
|
155
|
+
* buildQuery({ tab: 'popular', limit: 20, cursor: undefined });
|
|
156
|
+
* // '?tab=popular&limit=20'
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
function buildQuery(params) {
|
|
160
|
+
if (!params) return "";
|
|
161
|
+
const search = new URLSearchParams();
|
|
162
|
+
for (const [key, value] of Object.entries(params)) {
|
|
163
|
+
if (value === void 0 || value === null) continue;
|
|
164
|
+
if (Array.isArray(value)) {
|
|
165
|
+
for (const item of value) {
|
|
166
|
+
if (item === void 0 || item === null) continue;
|
|
167
|
+
search.append(key, String(item));
|
|
168
|
+
}
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
search.append(key, String(value));
|
|
172
|
+
}
|
|
173
|
+
const query = search.toString();
|
|
174
|
+
return query ? `?${query}` : "";
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Кодирует значение для подстановки в путь.
|
|
178
|
+
*
|
|
179
|
+
* Нужно для сегментов, которые могут содержать что угодно, — прежде всего хэштегов
|
|
180
|
+
* в `/api/hashtags/{tag}/posts`.
|
|
181
|
+
*
|
|
182
|
+
* @throws {ItdConfigError} если значение пустое
|
|
183
|
+
*/
|
|
184
|
+
function encodePathSegment(value, name = "параметр пути") {
|
|
185
|
+
if (typeof value !== "string" || value.trim() === "") throw new ItdConfigError(`${name} должен быть непустой строкой, получено: ${JSON.stringify(value)}`);
|
|
186
|
+
return encodeURIComponent(value);
|
|
187
|
+
}
|
|
188
|
+
/** Хост из URL в нижнем регистре. Пустая строка, если разобрать не удалось. */
|
|
189
|
+
function hostOf(url) {
|
|
190
|
+
try {
|
|
191
|
+
return new URL(url).hostname.toLowerCase();
|
|
192
|
+
} catch {
|
|
193
|
+
return "";
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** Origin из URL — схема, хост и порт. Пустая строка, если разобрать не удалось. */
|
|
197
|
+
function originOf(url) {
|
|
198
|
+
try {
|
|
199
|
+
return new URL(url).origin;
|
|
200
|
+
} catch {
|
|
201
|
+
return "";
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/** Тот же хост либо его поддомен. Принимает хосты, а не адреса. */
|
|
205
|
+
function isSameSite(primaryHost, host) {
|
|
206
|
+
if (!primaryHost || !host) return false;
|
|
207
|
+
return host === primaryHost || host.endsWith(`.${primaryHost}`);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Склеивает базовый URL и путь.
|
|
211
|
+
*
|
|
212
|
+
* Завершающий слэш пути сохраняется: он значим для `/api/notifications/`
|
|
213
|
+
* и `/api/v1/subscription/`, без него сервер отвечает ошибкой.
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```ts
|
|
217
|
+
* joinUrl('https://xn--d1ah4a.com/', '/api/notifications/');
|
|
218
|
+
* // 'https://xn--d1ah4a.com/api/notifications/'
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
function joinUrl(baseUrl, path) {
|
|
222
|
+
return `${baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
|
|
223
|
+
}
|
|
224
|
+
/** Приводит базовый URL к каноничному виду и проверяет, что он вообще похож на URL. */
|
|
225
|
+
function normalizeBaseUrl(baseUrl) {
|
|
226
|
+
if (typeof baseUrl !== "string" || baseUrl.trim() === "") throw new ItdConfigError(`baseUrl должен быть непустой строкой с абсолютным URL, получено: ${JSON.stringify(baseUrl)}`);
|
|
227
|
+
let parsed;
|
|
228
|
+
try {
|
|
229
|
+
parsed = new URL(baseUrl);
|
|
230
|
+
} catch {
|
|
231
|
+
throw new ItdConfigError(`baseUrl должен быть абсолютным URL, получено: ${JSON.stringify(baseUrl)}`);
|
|
232
|
+
}
|
|
233
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new ItdConfigError(`baseUrl должен использовать http или https, получено: ${parsed.protocol}`);
|
|
234
|
+
if (parsed.username || parsed.password || parsed.search || parsed.hash) throw new ItdConfigError("baseUrl не должен содержать логин, пароль, query-параметры или fragment");
|
|
235
|
+
return parsed.origin + (parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, ""));
|
|
236
|
+
}
|
|
237
|
+
//#endregion
|
|
142
238
|
//#region src/core/cookies.ts
|
|
143
239
|
/** Имя cookie-флага «есть refresh-сессия». Ставится сайтом итд.com рядом с refresh-токеном. */
|
|
144
240
|
const AUTH_FLAG_COOKIE = "is_auth";
|
|
@@ -154,13 +250,6 @@ const REFRESH_COOKIE = "refresh_token";
|
|
|
154
250
|
const REFRESH_COOKIE_PATH = "/api/v1/auth";
|
|
155
251
|
/** Разделитель origin и содержимого cookie при сериализации. В origin пробелов не бывает. */
|
|
156
252
|
const SERIALIZED_SEPARATOR = " ";
|
|
157
|
-
function originOf(url) {
|
|
158
|
-
try {
|
|
159
|
-
return new URL(url).origin;
|
|
160
|
-
} catch {
|
|
161
|
-
return "";
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
253
|
/** Дата в миллисекунды. Некорректная дата считается отсутствующей, а не `NaN`. */
|
|
165
254
|
function toTimestamp(date) {
|
|
166
255
|
if (!date) return void 0;
|
|
@@ -332,7 +421,7 @@ var CookieJar = class {
|
|
|
332
421
|
}
|
|
333
422
|
};
|
|
334
423
|
//#endregion
|
|
335
|
-
//#region src/core/attachments.ts
|
|
424
|
+
//#region src/core/attachments/contracts.ts
|
|
336
425
|
/** Способ передачи содержимого вложения. */
|
|
337
426
|
const FileTransferMode = Object.freeze({
|
|
338
427
|
/** Сначала получить файл целиком, затем отправить его как `Blob`. */
|
|
@@ -341,112 +430,36 @@ const FileTransferMode = Object.freeze({
|
|
|
341
430
|
Stream: "stream"
|
|
342
431
|
});
|
|
343
432
|
/** Размер очереди потокового вложения по умолчанию — 4 МиБ. */
|
|
344
|
-
const DEFAULT_FILE_STREAM_BUFFER_BYTES =
|
|
433
|
+
const DEFAULT_FILE_STREAM_BUFFER_BYTES = 4194304;
|
|
345
434
|
/** Предел размера файла, скачиваемого по адресу, — 100 МиБ. */
|
|
346
|
-
const DEFAULT_URL_FILE_MAX_BYTES =
|
|
347
|
-
|
|
348
|
-
|
|
435
|
+
const DEFAULT_URL_FILE_MAX_BYTES = 104857600;
|
|
436
|
+
//#endregion
|
|
437
|
+
//#region src/core/attachments/limits.ts
|
|
349
438
|
/** Проверяет числовую границу до обращения к источнику. */
|
|
350
439
|
function optionalBytes(value, name) {
|
|
351
440
|
if (value === void 0) return void 0;
|
|
352
441
|
if (!Number.isFinite(value) || value < 0 || !Number.isInteger(value)) throw new ItdConfigError(`${name} должен быть неотрицательным целым числом, получено: ${value}`);
|
|
353
442
|
return value;
|
|
354
443
|
}
|
|
355
|
-
/**
|
|
356
|
-
function
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
if (mode === FileTransferMode.Stream && typeof ReadableStream === "undefined") throw new ItdConfigError("эта среда не поддерживает ReadableStream; используйте mode: 'buffer'");
|
|
360
|
-
const maxBytes = optionalBytes(options.maxBytes ?? defaultMaxBytes, "maxBytes");
|
|
361
|
-
const streamBufferBytes = optionalBytes(options.streamBufferBytes ?? 4194304, "streamBufferBytes") ?? 4194304;
|
|
362
|
-
if (streamBufferBytes === 0) throw new ItdConfigError("streamBufferBytes должен быть больше нуля");
|
|
363
|
-
return {
|
|
364
|
-
mode,
|
|
365
|
-
maxBytes,
|
|
366
|
-
streamBufferBytes
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
/** Убирает параметры MIME и приводит его к форме для сравнения. */
|
|
370
|
-
function normalizeMimeType(contentType) {
|
|
371
|
-
return contentType?.split(";", 1)[0]?.trim().toLowerCase() || void 0;
|
|
372
|
-
}
|
|
373
|
-
/** Достаёт имя файла из пути URL. */
|
|
374
|
-
function filenameFromUrl(url) {
|
|
375
|
-
const last = url.pathname.split("/").pop();
|
|
376
|
-
if (!last) return void 0;
|
|
377
|
-
try {
|
|
378
|
-
return decodeURIComponent(last) || void 0;
|
|
379
|
-
} catch {
|
|
380
|
-
return last;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
/** Создаёт ошибку превышения размера. */
|
|
384
|
-
function tooLarge(url, limit, actual) {
|
|
385
|
-
return new ItdFileError(`файл${url ? ` по адресу ${url}` : ""} больше предела в ${limit} байт: ${actual}`, {
|
|
444
|
+
/** Создаёт типизированную ошибку превышения размера вложения. */
|
|
445
|
+
function fileTooLarge(url, limit, actual) {
|
|
446
|
+
const source = url ? ` по адресу ${url}` : "";
|
|
447
|
+
return new ItdFileError(`файл${source} больше предела в ${limit} байт: ${actual}`, {
|
|
386
448
|
reason: ItdFileErrorReason.TooLarge,
|
|
387
449
|
...url ? { url } : {},
|
|
388
450
|
limit,
|
|
389
451
|
actual
|
|
390
452
|
});
|
|
391
453
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const size = Number(header);
|
|
397
|
-
if (!Number.isFinite(size) || size < 0 || !Number.isInteger(size)) return void 0;
|
|
398
|
-
if (maxBytes !== void 0 && size > maxBytes) throw tooLarge(url, maxBytes, size);
|
|
399
|
-
return size;
|
|
400
|
-
}
|
|
401
|
-
/** Получает HTTP-ответ источника и проверяет его статус. */
|
|
402
|
-
async function fetchFile(target, options, context) {
|
|
403
|
-
let requested;
|
|
404
|
-
try {
|
|
405
|
-
requested = new URL(target);
|
|
406
|
-
} catch {
|
|
407
|
-
throw new ItdConfigError(`«${target}» не разбирается как адрес`);
|
|
408
|
-
}
|
|
409
|
-
if (requested.protocol !== "http:" && requested.protocol !== "https:") throw new ItdConfigError(`вложение по адресу поддерживает только http и https, получено: ${requested.protocol}`);
|
|
410
|
-
let response;
|
|
411
|
-
try {
|
|
412
|
-
response = await context.fetch(requested, { ...context.signal ? { signal: context.signal } : {} });
|
|
413
|
-
} catch (error) {
|
|
414
|
-
if (context.signal?.aborted || error instanceof Error && error.name === "AbortError") throw error;
|
|
415
|
-
throw new ItdFileError(`не удалось получить файл по адресу ${requested.href}`, {
|
|
416
|
-
reason: ItdFileErrorReason.Network,
|
|
417
|
-
url: requested.href,
|
|
418
|
-
retryable: true,
|
|
419
|
-
cause: error
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
const finalUrl = response.url ? new URL(response.url) : requested;
|
|
423
|
-
if (!response.ok) {
|
|
424
|
-
await response.body?.cancel().catch(() => {});
|
|
425
|
-
throw new ItdFileError(`источник ${finalUrl.href} ответил статусом ${response.status}`, {
|
|
426
|
-
reason: ItdFileErrorReason.Http,
|
|
427
|
-
url: finalUrl.href,
|
|
428
|
-
status: response.status,
|
|
429
|
-
retryable: response.status === 408 || response.status === 429 || response.status >= 500
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
const { maxBytes } = resolveFileStreamOptions(options, DEFAULT_URL_FILE_MAX_BYTES);
|
|
433
|
-
let size;
|
|
434
|
-
try {
|
|
435
|
-
size = declaredSize(response, maxBytes, finalUrl.href);
|
|
436
|
-
} catch (error) {
|
|
437
|
-
await response.body?.cancel().catch(() => {});
|
|
438
|
-
throw error;
|
|
439
|
-
}
|
|
440
|
-
return {
|
|
441
|
-
response,
|
|
442
|
-
url: finalUrl,
|
|
443
|
-
size
|
|
444
|
-
};
|
|
445
|
-
}
|
|
454
|
+
//#endregion
|
|
455
|
+
//#region src/core/attachments/bounded-stream.ts
|
|
456
|
+
/** Потоки, на которые уже наложены счётчик размера и backpressure. */
|
|
457
|
+
const BOUNDED_FILE_STREAMS = /* @__PURE__ */ new WeakSet();
|
|
446
458
|
/** Приводит произвольный чанк потока к байтам. */
|
|
447
459
|
function asBytes(value) {
|
|
448
460
|
return value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
449
461
|
}
|
|
462
|
+
/** Проверяет значение без обращения к методам потенциально чужого объекта. */
|
|
450
463
|
function isReadableByteStream(value) {
|
|
451
464
|
return typeof ReadableStream !== "undefined" && value instanceof ReadableStream;
|
|
452
465
|
}
|
|
@@ -496,7 +509,7 @@ function boundedFileStream(source, options = {}) {
|
|
|
496
509
|
finished = true;
|
|
497
510
|
await reader.cancel().catch(() => {});
|
|
498
511
|
cleanup();
|
|
499
|
-
controller.error(
|
|
512
|
+
controller.error(fileTooLarge(options.url, maxBytes, total));
|
|
500
513
|
return;
|
|
501
514
|
}
|
|
502
515
|
controller.enqueue(chunk);
|
|
@@ -531,119 +544,22 @@ function boundedFileStream(source, options = {}) {
|
|
|
531
544
|
function isBoundedFileStream(stream) {
|
|
532
545
|
return BOUNDED_FILE_STREAMS.has(stream);
|
|
533
546
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
const
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
streamBufferBytes,
|
|
545
|
-
...signal ? { signal } : {},
|
|
546
|
-
url,
|
|
547
|
-
retryableRead: true
|
|
548
|
-
}).getReader();
|
|
549
|
-
try {
|
|
550
|
-
for (;;) {
|
|
551
|
-
const next = await reader.read();
|
|
552
|
-
if (next.done) break;
|
|
553
|
-
chunks.push(next.value);
|
|
554
|
-
}
|
|
555
|
-
} finally {
|
|
556
|
-
reader.releaseLock();
|
|
557
|
-
}
|
|
558
|
-
return new Blob(chunks.map((chunk) => Uint8Array.from(chunk).buffer));
|
|
559
|
-
}
|
|
560
|
-
/** Скачивает файл целиком с ограничением размера. @internal */
|
|
561
|
-
async function downloadFile(target, options, context) {
|
|
562
|
-
const resolved = resolveFileStreamOptions(options, DEFAULT_URL_FILE_MAX_BYTES);
|
|
563
|
-
const { response, url } = await fetchFile(target, options, context);
|
|
564
|
-
const blob = await responseBlob(response, url.href, resolved.maxBytes, resolved.streamBufferBytes, context.signal);
|
|
565
|
-
const contentType = normalizeMimeType(options.contentType ?? response.headers.get("content-type") ?? void 0);
|
|
566
|
-
const filename = options.filename ?? filenameFromUrl(url);
|
|
567
|
-
return {
|
|
568
|
-
file: new Blob([blob], { type: contentType ?? "" }),
|
|
569
|
-
...filename ? { filename } : {},
|
|
570
|
-
...contentType ? { contentType } : {}
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
/** Открывает HTTP-ответ как ограниченный поток. @internal */
|
|
574
|
-
async function openUrlFile(target, options, context) {
|
|
575
|
-
const resolved = resolveFileStreamOptions(options, DEFAULT_URL_FILE_MAX_BYTES);
|
|
576
|
-
const { response, url, size } = await fetchFile(target, options, context);
|
|
577
|
-
if (!response.body) throw new ItdFileError(`источник ${url.href} не предоставил потоковое тело`, {
|
|
578
|
-
reason: ItdFileErrorReason.StreamUnavailable,
|
|
579
|
-
url: url.href
|
|
580
|
-
});
|
|
581
|
-
const stream = boundedFileStream(response.body, {
|
|
582
|
-
...resolved.maxBytes !== void 0 ? { maxBytes: resolved.maxBytes } : {},
|
|
583
|
-
streamBufferBytes: resolved.streamBufferBytes,
|
|
584
|
-
...context.signal ? { signal: context.signal } : {},
|
|
585
|
-
url: url.href,
|
|
586
|
-
retryableRead: true
|
|
587
|
-
});
|
|
588
|
-
const filename = options.filename ?? filenameFromUrl(url);
|
|
589
|
-
const contentType = normalizeMimeType(options.contentType ?? response.headers.get("content-type") ?? void 0);
|
|
547
|
+
//#endregion
|
|
548
|
+
//#region src/core/attachments/options.ts
|
|
549
|
+
/** Проверяет и дополняет настройки потока. @internal */
|
|
550
|
+
function resolveFileStreamOptions(options, defaultMaxBytes) {
|
|
551
|
+
const mode = options.mode ?? FileTransferMode.Buffer;
|
|
552
|
+
if (mode !== FileTransferMode.Buffer && mode !== FileTransferMode.Stream) throw new ItdConfigError(`mode вложения должен быть 'buffer' или 'stream', получено: ${mode}`);
|
|
553
|
+
if (mode === FileTransferMode.Stream && typeof ReadableStream === "undefined") throw new ItdConfigError("эта среда не поддерживает ReadableStream; используйте mode: 'buffer'");
|
|
554
|
+
const maxBytes = optionalBytes(options.maxBytes ?? defaultMaxBytes, "maxBytes");
|
|
555
|
+
const streamBufferBytes = optionalBytes(options.streamBufferBytes ?? 4194304, "streamBufferBytes") ?? 4194304;
|
|
556
|
+
if (streamBufferBytes === 0) throw new ItdConfigError("streamBufferBytes должен быть больше нуля");
|
|
590
557
|
return {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
...size !== void 0 ? { size } : {},
|
|
595
|
-
close: () => stream.cancel().catch(() => {})
|
|
558
|
+
mode,
|
|
559
|
+
maxBytes,
|
|
560
|
+
streamBufferBytes
|
|
596
561
|
};
|
|
597
562
|
}
|
|
598
|
-
function fromUrl(url, options = {}) {
|
|
599
|
-
if (resolveFileStreamOptions(options, 104857600).mode === FileTransferMode.Stream) return { open: (context) => openUrlFile(url, options, context) };
|
|
600
|
-
return { load: (context) => downloadFile(url, options, context) };
|
|
601
|
-
}
|
|
602
|
-
/**
|
|
603
|
-
* Создаёт повторяемый пользовательский поток.
|
|
604
|
-
*
|
|
605
|
-
* Фабрика вызывается заново для каждой попытки; возвращать один и тот же поток нельзя.
|
|
606
|
-
*/
|
|
607
|
-
function fromStream(factory, options = {}) {
|
|
608
|
-
const resolved = resolveFileStreamOptions({
|
|
609
|
-
...options,
|
|
610
|
-
mode: FileTransferMode.Stream
|
|
611
|
-
}, void 0);
|
|
612
|
-
optionalBytes(options.size, "size");
|
|
613
|
-
return { open: async (context) => {
|
|
614
|
-
let opened;
|
|
615
|
-
try {
|
|
616
|
-
opened = await factory(context);
|
|
617
|
-
} catch (error) {
|
|
618
|
-
if (error instanceof ItdFileError || error instanceof ItdConfigError || context.signal?.aborted) throw error;
|
|
619
|
-
throw new ItdFileError("не удалось открыть поток вложения", {
|
|
620
|
-
reason: ItdFileErrorReason.Read,
|
|
621
|
-
retryable: true,
|
|
622
|
-
cause: error
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
const content = isReadableByteStream(opened) ? { stream: opened } : opened;
|
|
626
|
-
if (!content || !isReadableByteStream(content.stream)) throw new ItdConfigError("fromStream должен вернуть ReadableStream или { stream }");
|
|
627
|
-
const size = options.size ?? content.size;
|
|
628
|
-
optionalBytes(size, "size");
|
|
629
|
-
if (resolved.maxBytes !== void 0 && size !== void 0 && size > resolved.maxBytes) {
|
|
630
|
-
await content.close?.();
|
|
631
|
-
throw tooLarge(void 0, resolved.maxBytes, size);
|
|
632
|
-
}
|
|
633
|
-
return {
|
|
634
|
-
stream: boundedFileStream(content.stream, {
|
|
635
|
-
...resolved.maxBytes !== void 0 ? { maxBytes: resolved.maxBytes } : {},
|
|
636
|
-
streamBufferBytes: resolved.streamBufferBytes,
|
|
637
|
-
...context.signal ? { signal: context.signal } : {},
|
|
638
|
-
retryableRead: true
|
|
639
|
-
}),
|
|
640
|
-
...options.filename ?? content.filename ? { filename: options.filename ?? content.filename } : {},
|
|
641
|
-
...normalizeMimeType(options.contentType ?? content.contentType) ? { contentType: normalizeMimeType(options.contentType ?? content.contentType) } : {},
|
|
642
|
-
...size !== void 0 ? { size } : {},
|
|
643
|
-
...content.close ? { close: content.close } : {}
|
|
644
|
-
};
|
|
645
|
-
} };
|
|
646
|
-
}
|
|
647
563
|
//#endregion
|
|
648
564
|
//#region src/core/multi-storage.ts
|
|
649
565
|
/**
|
|
@@ -704,102 +620,56 @@ function controlledTokenStorage(storage, account) {
|
|
|
704
620
|
* из `itd-api/node` либо соберите своё через {@link createMultiTokenStorage}.
|
|
705
621
|
*/
|
|
706
622
|
var MemoryMultiTokenStorage = class {
|
|
707
|
-
#
|
|
623
|
+
#store = new MemoryKeyValueStore();
|
|
708
624
|
constructor(initial) {
|
|
709
|
-
for (const [account, session] of Object.entries(initial ?? {})) this.#
|
|
625
|
+
for (const [account, session] of Object.entries(initial ?? {})) this.#store.set(accountKey(account), copySession(session));
|
|
710
626
|
}
|
|
711
627
|
get(account) {
|
|
712
|
-
const session = this.#
|
|
628
|
+
const session = this.#store.get(accountKey(account));
|
|
713
629
|
return session ? copySession(session) : null;
|
|
714
630
|
}
|
|
715
631
|
set(account, session) {
|
|
716
|
-
this.#
|
|
632
|
+
this.#store.set(accountKey(account), copySession(session));
|
|
717
633
|
}
|
|
718
634
|
clear(account) {
|
|
719
|
-
this.#
|
|
635
|
+
this.#store.delete(accountKey(account));
|
|
720
636
|
}
|
|
721
637
|
accounts() {
|
|
722
|
-
return
|
|
638
|
+
return this.#store.keys(ACCOUNT_KEY_PREFIX).map(accountFromKey);
|
|
723
639
|
}
|
|
724
640
|
};
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
*/
|
|
729
|
-
function createMultiTokenStorage(handlers) {
|
|
730
|
-
return handlers;
|
|
641
|
+
const ACCOUNT_KEY_PREFIX = "accounts/";
|
|
642
|
+
function accountKey(account) {
|
|
643
|
+
return `${ACCOUNT_KEY_PREFIX}${encodeURIComponent(account)}`;
|
|
731
644
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
return Object.create(null);
|
|
735
|
-
}
|
|
736
|
-
/** Копирует внешний снимок в запись без унаследованных свойств. */
|
|
737
|
-
function normalizeSessionRecord(record) {
|
|
738
|
-
const normalized = emptySessionRecord();
|
|
739
|
-
for (const [account, session] of Object.entries(record ?? {})) normalized[account] = copySession(session);
|
|
740
|
-
return normalized;
|
|
645
|
+
function accountFromKey(key) {
|
|
646
|
+
return decodeURIComponent(key.slice(9));
|
|
741
647
|
}
|
|
742
648
|
/**
|
|
743
|
-
*
|
|
649
|
+
* Создаёт доменное хранилище нескольких сессий поверх enumerable key-value backend.
|
|
744
650
|
*
|
|
745
|
-
*
|
|
746
|
-
*
|
|
747
|
-
* чужие сессии. Источник читается один раз, дальше слепок живёт в памяти, а записи
|
|
748
|
-
* выстраиваются в цепочку и идут по очереди.
|
|
749
|
-
*
|
|
750
|
-
* Внутри процесса этого достаточно. Несколько процессов, пишущих в одну запись,
|
|
751
|
-
* по-прежнему затирают друг друга — как и несколько экземпляров этого адаптера,
|
|
752
|
-
* направленных на один источник в одном процессе.
|
|
651
|
+
* Отдельный индекс аккаунтов не используется: `accounts()` перечисляет ключи backend. Поэтому
|
|
652
|
+
* запись сессии не может разойтись с индексом, но backend обязан эффективно поддерживать `keys`.
|
|
753
653
|
*/
|
|
754
|
-
function
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
let writing = Promise.resolve();
|
|
761
|
-
const load = async () => {
|
|
762
|
-
if (snapshot !== void 0) return snapshot;
|
|
763
|
-
loading ??= source.read().then((value) => {
|
|
764
|
-
snapshot = normalizeSessionRecord(value);
|
|
765
|
-
return snapshot;
|
|
766
|
-
}).finally(() => {
|
|
767
|
-
loading = null;
|
|
768
|
-
});
|
|
769
|
-
return loading;
|
|
770
|
-
};
|
|
771
|
-
const flush = () => {
|
|
772
|
-
const current = normalizeSessionRecord(snapshot ?? null);
|
|
773
|
-
const operation = async () => {
|
|
774
|
-
if (source.remove && Object.keys(current).length === 0) await source.remove();
|
|
775
|
-
else await source.write(current);
|
|
776
|
-
};
|
|
777
|
-
writing = writing.then(operation, operation);
|
|
778
|
-
return writing;
|
|
779
|
-
};
|
|
654
|
+
function createMultiTokenStorage(store, options = {}) {
|
|
655
|
+
const prefix = options.prefix ?? ACCOUNT_KEY_PREFIX;
|
|
656
|
+
if (typeof prefix !== "string") throw new ItdConfigError("prefix MultiTokenStorage должен быть строкой");
|
|
657
|
+
const backend = createKeyValueStore(store);
|
|
658
|
+
if (!isEnumerableKeyValueStore(backend)) throw new ItdConfigError("MultiTokenStorage требует KeyValueStore с методом keys()");
|
|
659
|
+
const key = (account) => `${prefix}${encodeURIComponent(account)}`;
|
|
780
660
|
return {
|
|
781
661
|
async get(account) {
|
|
782
|
-
const
|
|
783
|
-
const session = Object.hasOwn(current, account) ? current[account] : void 0;
|
|
662
|
+
const session = await backend.get(key(account));
|
|
784
663
|
return session ? copySession(session) : null;
|
|
785
664
|
},
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
current[account] = copySession(session);
|
|
789
|
-
await flush();
|
|
790
|
-
},
|
|
791
|
-
async clear(account) {
|
|
792
|
-
const current = await load();
|
|
793
|
-
if (!Object.hasOwn(current, account)) return;
|
|
794
|
-
delete current[account];
|
|
795
|
-
await flush();
|
|
796
|
-
},
|
|
665
|
+
set: (account, session) => backend.set(key(account), copySession(session)),
|
|
666
|
+
clear: (account) => backend.delete(key(account)),
|
|
797
667
|
async accounts() {
|
|
798
|
-
return
|
|
668
|
+
return (await collectKeyValueStoreKeys(backend, prefix)).map((value) => decodeURIComponent(value.slice(prefix.length)));
|
|
799
669
|
}
|
|
800
670
|
};
|
|
801
671
|
}
|
|
802
672
|
//#endregion
|
|
803
|
-
export {
|
|
673
|
+
export { joinUrl as C, isSameSite as S, originOf as T, REFRESH_COOKIE as _, scopedTokenStorage as a, encodePathSegment as b, isBoundedFileStream as c, optionalBytes as d, DEFAULT_FILE_STREAM_BUFFER_BYTES as f, CookieJar as g, AUTH_FLAG_COOKIE as h, isRestorableSession as i, isReadableByteStream as l, FileTransferMode as m, controlledTokenStorage as n, resolveFileStreamOptions as o, DEFAULT_URL_FILE_MAX_BYTES as p, createMultiTokenStorage as r, boundedFileStream as s, MemoryMultiTokenStorage as t, fileTooLarge as u, REFRESH_COOKIE_PATH as v, normalizeBaseUrl as w, hostOf as x, buildQuery as y };
|
|
804
674
|
|
|
805
|
-
//# sourceMappingURL=multi-storage-
|
|
675
|
+
//# sourceMappingURL=multi-storage-B0r0AInH.js.map
|