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.
Files changed (42) hide show
  1. package/README.md +3 -2
  2. package/dist/index.cjs +3329 -1795
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +1640 -1092
  5. package/dist/index.d.ts +1640 -1092
  6. package/dist/index.js +3249 -1730
  7. package/dist/index.js.map +1 -1
  8. package/dist/{multi-storage-CyMe404l.js → multi-storage-B0r0AInH.js} +151 -281
  9. package/dist/multi-storage-B0r0AInH.js.map +1 -0
  10. package/dist/{multi-storage-BhcA2Izn.d.ts → multi-storage-CLqmzq07.d.ts} +14 -54
  11. package/dist/{multi-storage-D1keK2Op.cjs → multi-storage-mBuCOVZY.cjs} +188 -294
  12. package/dist/multi-storage-mBuCOVZY.cjs.map +1 -0
  13. package/dist/{multi-storage-NDqzRQcD.d.cts → multi-storage-s_PcWPGH.d.cts} +14 -54
  14. package/dist/node.cjs +62 -53
  15. package/dist/node.cjs.map +1 -1
  16. package/dist/node.d.cts +19 -5
  17. package/dist/node.d.ts +19 -5
  18. package/dist/node.js +62 -54
  19. package/dist/node.js.map +1 -1
  20. package/dist/{storage-D9tfHx7Z.js → storage-DWrK3Z4M.js} +201 -18
  21. package/dist/storage-DWrK3Z4M.js.map +1 -0
  22. package/dist/storage-Doe3lpFQ.d.cts +152 -0
  23. package/dist/storage-Doe3lpFQ.d.ts +152 -0
  24. package/dist/{storage-ycBqLBRB.cjs → storage-dF8Tio5y.cjs} +254 -17
  25. package/dist/storage-dF8Tio5y.cjs.map +1 -0
  26. package/dist/web.cjs +143 -40
  27. package/dist/web.cjs.map +1 -1
  28. package/dist/web.d.cts +40 -3
  29. package/dist/web.d.ts +40 -3
  30. package/dist/web.js +141 -41
  31. package/dist/web.js.map +1 -1
  32. package/package.json +3 -3
  33. package/dist/multi-storage-CyMe404l.js.map +0 -1
  34. package/dist/multi-storage-D1keK2Op.cjs.map +0 -1
  35. package/dist/runtime-CFEsf-jD.cjs +0 -185
  36. package/dist/runtime-CFEsf-jD.cjs.map +0 -1
  37. package/dist/runtime-DHxDn8gf.js +0 -126
  38. package/dist/runtime-DHxDn8gf.js.map +0 -1
  39. package/dist/storage-BjNRlkbE.d.cts +0 -82
  40. package/dist/storage-BjNRlkbE.d.ts +0 -82
  41. package/dist/storage-D9tfHx7Z.js.map +0 -1
  42. package/dist/storage-ycBqLBRB.cjs.map +0 -1
@@ -1,4 +1,4 @@
1
- const require_storage = require("./storage-ycBqLBRB.cjs");
1
+ const require_storage = require("./storage-dF8Tio5y.cjs");
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 require_storage.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 require_storage.ItdConfigError(`baseUrl должен быть непустой строкой с абсолютным URL, получено: ${JSON.stringify(baseUrl)}`);
227
+ let parsed;
228
+ try {
229
+ parsed = new URL(baseUrl);
230
+ } catch {
231
+ throw new require_storage.ItdConfigError(`baseUrl должен быть абсолютным URL, получено: ${JSON.stringify(baseUrl)}`);
232
+ }
233
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new require_storage.ItdConfigError(`baseUrl должен использовать http или https, получено: ${parsed.protocol}`);
234
+ if (parsed.username || parsed.password || parsed.search || parsed.hash) throw new require_storage.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 = 4 * 1024 * 1024;
433
+ const DEFAULT_FILE_STREAM_BUFFER_BYTES = 4194304;
345
434
  /** Предел размера файла, скачиваемого по адресу, — 100 МиБ. */
346
- const DEFAULT_URL_FILE_MAX_BYTES = 100 * 1024 * 1024;
347
- /** Потоки, на которые уже наложены счётчик размера и backpressure. */
348
- const BOUNDED_FILE_STREAMS = /* @__PURE__ */ new WeakSet();
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 require_storage.ItdConfigError(`${name} должен быть неотрицательным целым числом, получено: ${value}`);
353
442
  return value;
354
443
  }
355
- /** Проверяет и дополняет настройки потока. @internal */
356
- function resolveFileStreamOptions(options, defaultMaxBytes) {
357
- const mode = options.mode ?? FileTransferMode.Buffer;
358
- if (mode !== FileTransferMode.Buffer && mode !== FileTransferMode.Stream) throw new require_storage.ItdConfigError(`mode вложения должен быть 'buffer' или 'stream', получено: ${mode}`);
359
- if (mode === FileTransferMode.Stream && typeof ReadableStream === "undefined") throw new require_storage.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 require_storage.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 require_storage.ItdFileError(`файл${url ? ` по адресу ${url}` : ""} больше предела в ${limit} байт: ${actual}`, {
444
+ /** Создаёт типизированную ошибку превышения размера вложения. */
445
+ function fileTooLarge(url, limit, actual) {
446
+ const source = url ? ` по адресу ${url}` : "";
447
+ return new require_storage.ItdFileError(`файл${source} больше предела в ${limit} байт: ${actual}`, {
386
448
  reason: require_storage.ItdFileErrorReason.TooLarge,
387
449
  ...url ? { url } : {},
388
450
  limit,
389
451
  actual
390
452
  });
391
453
  }
392
- /** Проверяет объявленный размер и возвращает его, если заголовок корректен. */
393
- function declaredSize(response, maxBytes, url) {
394
- const header = response.headers.get("content-length");
395
- if (header === null) return void 0;
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 require_storage.ItdConfigError(`«${target}» не разбирается как адрес`);
408
- }
409
- if (requested.protocol !== "http:" && requested.protocol !== "https:") throw new require_storage.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 require_storage.ItdFileError(`не удалось получить файл по адресу ${requested.href}`, {
416
- reason: require_storage.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 require_storage.ItdFileError(`источник ${finalUrl.href} ответил статусом ${response.status}`, {
426
- reason: require_storage.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(tooLarge(options.url, maxBytes, total));
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
- /** Читает ответ с контролем размера до создания итогового `Blob`. */
535
- async function responseBlob(response, url, maxBytes, streamBufferBytes, signal) {
536
- if (!response.body) {
537
- const blob = await response.blob();
538
- if (maxBytes !== void 0 && blob.size > maxBytes) throw tooLarge(url, maxBytes, blob.size);
539
- return blob;
540
- }
541
- const chunks = [];
542
- const reader = boundedFileStream(response.body, {
543
- ...maxBytes !== void 0 ? { maxBytes } : {},
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 require_storage.ItdFileError(`источник ${url.href} не предоставил потоковое тело`, {
578
- reason: require_storage.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 require_storage.ItdConfigError(`mode вложения должен быть 'buffer' или 'stream', получено: ${mode}`);
553
+ if (mode === FileTransferMode.Stream && typeof ReadableStream === "undefined") throw new require_storage.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 require_storage.ItdConfigError("streamBufferBytes должен быть больше нуля");
590
557
  return {
591
- stream,
592
- ...filename ? { filename } : {},
593
- ...contentType ? { contentType } : {},
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 require_storage.ItdFileError || error instanceof require_storage.ItdConfigError || context.signal?.aborted) throw error;
619
- throw new require_storage.ItdFileError("не удалось открыть поток вложения", {
620
- reason: require_storage.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 require_storage.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,98 +620,52 @@ function controlledTokenStorage(storage, account) {
704
620
  * из `itd-api/node` либо соберите своё через {@link createMultiTokenStorage}.
705
621
  */
706
622
  var MemoryMultiTokenStorage = class {
707
- #sessions = /* @__PURE__ */ new Map();
623
+ #store = new require_storage.MemoryKeyValueStore();
708
624
  constructor(initial) {
709
- for (const [account, session] of Object.entries(initial ?? {})) this.#sessions.set(account, require_storage.copySession(session));
625
+ for (const [account, session] of Object.entries(initial ?? {})) this.#store.set(accountKey(account), require_storage.copySession(session));
710
626
  }
711
627
  get(account) {
712
- const session = this.#sessions.get(account);
628
+ const session = this.#store.get(accountKey(account));
713
629
  return session ? require_storage.copySession(session) : null;
714
630
  }
715
631
  set(account, session) {
716
- this.#sessions.set(account, require_storage.copySession(session));
632
+ this.#store.set(accountKey(account), require_storage.copySession(session));
717
633
  }
718
634
  clear(account) {
719
- this.#sessions.delete(account);
635
+ this.#store.delete(accountKey(account));
720
636
  }
721
637
  accounts() {
722
- return [...this.#sessions.keys()];
638
+ return this.#store.keys(ACCOUNT_KEY_PREFIX).map(accountFromKey);
723
639
  }
724
640
  };
725
- /**
726
- * Собирает {@link MultiTokenStorage} из четырёх функций — когда заводить класс избыточно.
727
- * Аналог `createTokenStorage` для нескольких аккаунтов.
728
- */
729
- function createMultiTokenStorage(handlers) {
730
- return handlers;
731
- }
732
- /** Создаёт запись без прототипа: имена `__proto__` и `constructor` остаются обычными ключами. */
733
- function emptySessionRecord() {
734
- return Object.create(null);
641
+ const ACCOUNT_KEY_PREFIX = "accounts/";
642
+ function accountKey(account) {
643
+ return `${ACCOUNT_KEY_PREFIX}${encodeURIComponent(account)}`;
735
644
  }
736
- /** Копирует внешний снимок в запись без унаследованных свойств. */
737
- function normalizeSessionRecord(record) {
738
- const normalized = emptySessionRecord();
739
- for (const [account, session] of Object.entries(record ?? {})) normalized[account] = require_storage.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 createRecordMultiStorage(source) {
755
- /** Слепок записи. `undefined` источник ещё не читался. */
756
- let snapshot;
757
- /** Общий промис чтения: параллельные вызовы на холодном старте читают источник один раз. */
758
- let loading = null;
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 require_storage.ItdConfigError("prefix MultiTokenStorage должен быть строкой");
657
+ const backend = require_storage.createKeyValueStore(store);
658
+ if (!require_storage.isEnumerableKeyValueStore(backend)) throw new require_storage.ItdConfigError("MultiTokenStorage требует KeyValueStore с методом keys()");
659
+ const key = (account) => `${prefix}${encodeURIComponent(account)}`;
780
660
  return {
781
661
  async get(account) {
782
- const current = await load();
783
- const session = Object.hasOwn(current, account) ? current[account] : void 0;
662
+ const session = await backend.get(key(account));
784
663
  return session ? require_storage.copySession(session) : null;
785
664
  },
786
- async set(account, session) {
787
- const current = await load();
788
- current[account] = require_storage.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), require_storage.copySession(session)),
666
+ clear: (account) => backend.delete(key(account)),
797
667
  async accounts() {
798
- return Object.keys(await load());
668
+ return (await require_storage.collectKeyValueStoreKeys(backend, prefix)).map((value) => decodeURIComponent(value.slice(prefix.length)));
799
669
  }
800
670
  };
801
671
  }
@@ -854,6 +724,12 @@ Object.defineProperty(exports, "boundedFileStream", {
854
724
  return boundedFileStream;
855
725
  }
856
726
  });
727
+ Object.defineProperty(exports, "buildQuery", {
728
+ enumerable: true,
729
+ get: function() {
730
+ return buildQuery;
731
+ }
732
+ });
857
733
  Object.defineProperty(exports, "controlledTokenStorage", {
858
734
  enumerable: true,
859
735
  get: function() {
@@ -866,34 +742,34 @@ Object.defineProperty(exports, "createMultiTokenStorage", {
866
742
  return createMultiTokenStorage;
867
743
  }
868
744
  });
869
- Object.defineProperty(exports, "createRecordMultiStorage", {
745
+ Object.defineProperty(exports, "encodePathSegment", {
870
746
  enumerable: true,
871
747
  get: function() {
872
- return createRecordMultiStorage;
748
+ return encodePathSegment;
873
749
  }
874
750
  });
875
- Object.defineProperty(exports, "downloadFile", {
751
+ Object.defineProperty(exports, "fileTooLarge", {
876
752
  enumerable: true,
877
753
  get: function() {
878
- return downloadFile;
754
+ return fileTooLarge;
879
755
  }
880
756
  });
881
- Object.defineProperty(exports, "fromStream", {
757
+ Object.defineProperty(exports, "hostOf", {
882
758
  enumerable: true,
883
759
  get: function() {
884
- return fromStream;
760
+ return hostOf;
885
761
  }
886
762
  });
887
- Object.defineProperty(exports, "fromUrl", {
763
+ Object.defineProperty(exports, "isBoundedFileStream", {
888
764
  enumerable: true,
889
765
  get: function() {
890
- return fromUrl;
766
+ return isBoundedFileStream;
891
767
  }
892
768
  });
893
- Object.defineProperty(exports, "isBoundedFileStream", {
769
+ Object.defineProperty(exports, "isReadableByteStream", {
894
770
  enumerable: true,
895
771
  get: function() {
896
- return isBoundedFileStream;
772
+ return isReadableByteStream;
897
773
  }
898
774
  });
899
775
  Object.defineProperty(exports, "isRestorableSession", {
@@ -902,16 +778,34 @@ Object.defineProperty(exports, "isRestorableSession", {
902
778
  return isRestorableSession;
903
779
  }
904
780
  });
905
- Object.defineProperty(exports, "normalizeMimeType", {
781
+ Object.defineProperty(exports, "isSameSite", {
782
+ enumerable: true,
783
+ get: function() {
784
+ return isSameSite;
785
+ }
786
+ });
787
+ Object.defineProperty(exports, "joinUrl", {
788
+ enumerable: true,
789
+ get: function() {
790
+ return joinUrl;
791
+ }
792
+ });
793
+ Object.defineProperty(exports, "normalizeBaseUrl", {
794
+ enumerable: true,
795
+ get: function() {
796
+ return normalizeBaseUrl;
797
+ }
798
+ });
799
+ Object.defineProperty(exports, "optionalBytes", {
906
800
  enumerable: true,
907
801
  get: function() {
908
- return normalizeMimeType;
802
+ return optionalBytes;
909
803
  }
910
804
  });
911
- Object.defineProperty(exports, "openUrlFile", {
805
+ Object.defineProperty(exports, "originOf", {
912
806
  enumerable: true,
913
807
  get: function() {
914
- return openUrlFile;
808
+ return originOf;
915
809
  }
916
810
  });
917
811
  Object.defineProperty(exports, "resolveFileStreamOptions", {
@@ -927,4 +821,4 @@ Object.defineProperty(exports, "scopedTokenStorage", {
927
821
  }
928
822
  });
929
823
 
930
- //# sourceMappingURL=multi-storage-D1keK2Op.cjs.map
824
+ //# sourceMappingURL=multi-storage-mBuCOVZY.cjs.map