itd-api 0.7.2 → 0.8.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 (84) hide show
  1. package/README.md +12 -12
  2. package/dist/events/index.cjs +53 -0
  3. package/dist/events/index.d.cts +3 -0
  4. package/dist/events/index.d.ts +3 -0
  5. package/dist/events/index.js +5 -0
  6. package/dist/index.cjs +215 -194
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.cts +66 -63
  9. package/dist/index.d.ts +66 -63
  10. package/dist/index.js +197 -179
  11. package/dist/index.js.map +1 -1
  12. package/dist/node/index.cjs +4 -4
  13. package/dist/node/index.d.cts +1 -1
  14. package/dist/node/index.d.ts +1 -1
  15. package/dist/node/index.js +4 -4
  16. package/dist/rest/index.cjs +20 -26
  17. package/dist/rest/index.cjs.map +1 -1
  18. package/dist/rest/index.d.cts +7 -7
  19. package/dist/rest/index.d.ts +7 -7
  20. package/dist/rest/index.js +14 -20
  21. package/dist/rest/index.js.map +1 -1
  22. package/dist/shared/{errors-DfU8M5eS.cjs → errors-BmP3TKoW.cjs} +3 -3
  23. package/dist/shared/{errors-DfU8M5eS.cjs.map → errors-BmP3TKoW.cjs.map} +1 -1
  24. package/dist/shared/{errors-Bhrd2fJd.js → errors-GI10kZxk.js} +3 -3
  25. package/dist/shared/{errors-Bhrd2fJd.js.map → errors-GI10kZxk.js.map} +1 -1
  26. package/dist/shared/events-Br2r7coi.d.cts +698 -0
  27. package/dist/shared/events-QyhQZrnC.d.ts +698 -0
  28. package/dist/shared/{websocket-BLR8eVJV.js → events-TfhKh26b.js} +770 -490
  29. package/dist/shared/events-TfhKh26b.js.map +1 -0
  30. package/dist/shared/{websocket-C_eI4H2o.cjs → events-iTteVsd_.cjs} +834 -518
  31. package/dist/shared/events-iTteVsd_.cjs.map +1 -0
  32. package/dist/shared/{multi-storage-CkvTUC5m.js → multi-storage-1AEZTczk.js} +4 -4
  33. package/dist/shared/{multi-storage-CkvTUC5m.js.map → multi-storage-1AEZTczk.js.map} +1 -1
  34. package/dist/shared/{multi-storage--yTEqiod.cjs → multi-storage-CCUgkqvI.cjs} +6 -6
  35. package/dist/shared/{multi-storage--yTEqiod.cjs.map → multi-storage-CCUgkqvI.cjs.map} +1 -1
  36. package/dist/shared/{options-DtATYdLr.js → options-BNP633A5.js} +8 -6
  37. package/dist/shared/options-BNP633A5.js.map +1 -0
  38. package/dist/shared/{options-Dg5N3r1V.cjs → options-BUfoHE9G.cjs} +8 -6
  39. package/dist/shared/options-BUfoHE9G.cjs.map +1 -0
  40. package/dist/shared/{cookies-DZwFq6kr.cjs → redact-DB9EMGJx.cjs} +301 -2
  41. package/dist/shared/redact-DB9EMGJx.cjs.map +1 -0
  42. package/dist/shared/{cookies-tX2sNwxb.js → redact-FNK29Fv-.js} +224 -3
  43. package/dist/shared/redact-FNK29Fv-.js.map +1 -0
  44. package/dist/shared/{render-DO0F5YSm.d.cts → render-BszcKF7S.d.cts} +430 -313
  45. package/dist/shared/{render-C6HRPs10.js → render-CiL1VS0b.js} +925 -582
  46. package/dist/shared/render-CiL1VS0b.js.map +1 -0
  47. package/dist/shared/{render-mcuELiYi.cjs → render-DNTESBSl.cjs} +967 -618
  48. package/dist/shared/render-DNTESBSl.cjs.map +1 -0
  49. package/dist/shared/{render-CgwKdOzu.d.ts → render-DVB7T8pm.d.ts} +430 -313
  50. package/dist/shared/{storage-BPJR_k4-.cjs → storage-BLmXmzKF.cjs} +2 -2
  51. package/dist/shared/{storage-BPJR_k4-.cjs.map → storage-BLmXmzKF.cjs.map} +1 -1
  52. package/dist/shared/{storage-D86edNCB.js → storage-CCxn0Kkv.js} +2 -2
  53. package/dist/shared/{storage-D86edNCB.js.map → storage-CCxn0Kkv.js.map} +1 -1
  54. package/dist/shared/{url-IU0xN9wX.js → url-B3ocFlHj.js} +434 -408
  55. package/dist/shared/url-B3ocFlHj.js.map +1 -0
  56. package/dist/shared/{url-DTfZ2toq.d.cts → url-Be9jlWRv.d.cts} +1228 -1188
  57. package/dist/shared/{url-DTfZ2toq.d.ts → url-Be9jlWRv.d.ts} +1228 -1188
  58. package/dist/shared/{url-BaMCQpYH.cjs → url-CFN1sT9S.cjs} +497 -465
  59. package/dist/shared/url-CFN1sT9S.cjs.map +1 -0
  60. package/dist/web/index.cjs +2 -2
  61. package/dist/web/index.js +2 -2
  62. package/package.json +16 -15
  63. package/dist/realtime/index.cjs +0 -165
  64. package/dist/realtime/index.cjs.map +0 -1
  65. package/dist/realtime/index.d.cts +0 -51
  66. package/dist/realtime/index.d.ts +0 -51
  67. package/dist/realtime/index.js +0 -120
  68. package/dist/realtime/index.js.map +0 -1
  69. package/dist/shared/auth-provider-CG8oCQ9F.cjs +0 -108
  70. package/dist/shared/auth-provider-CG8oCQ9F.cjs.map +0 -1
  71. package/dist/shared/auth-provider-mYqxsSVa.js +0 -91
  72. package/dist/shared/auth-provider-mYqxsSVa.js.map +0 -1
  73. package/dist/shared/cookies-DZwFq6kr.cjs.map +0 -1
  74. package/dist/shared/cookies-tX2sNwxb.js.map +0 -1
  75. package/dist/shared/options-Dg5N3r1V.cjs.map +0 -1
  76. package/dist/shared/options-DtATYdLr.js.map +0 -1
  77. package/dist/shared/render-C6HRPs10.js.map +0 -1
  78. package/dist/shared/render-mcuELiYi.cjs.map +0 -1
  79. package/dist/shared/url-BaMCQpYH.cjs.map +0 -1
  80. package/dist/shared/url-IU0xN9wX.js.map +0 -1
  81. package/dist/shared/websocket-BLR8eVJV.js.map +0 -1
  82. package/dist/shared/websocket-C_eI4H2o.cjs.map +0 -1
  83. package/dist/shared/websocket-DF7XIMiX.d.cts +0 -562
  84. package/dist/shared/websocket-DYKBr8HF.d.ts +0 -562
@@ -1,7 +1,7 @@
1
- import { F as pickArray, H as RetrySafety, I as pickBoolean, J as DEFAULT_STATUS_BASE_URL, L as pickNumber, P as isRecord, R as pickObject, U as mergeService, Y as STATUS_SERVICE, it as isFile, nt as createDeviceId, p as InteractionType, r as normalizeNotification, rt as isBlob, v as ReportReason, x as SpanType, y as ReportTargetType, z as pickString } from "./url-IU0xN9wX.js";
2
- import { _ as ItdStateError, a as ItdConfigError, l as ItdFileError, s as ItdError, t as ItdAbortError, u as ItdFileErrorReason } from "./errors-Bhrd2fJd.js";
3
- import { o as encodePathSegment } from "./cookies-tX2sNwxb.js";
4
- import { a as fileTooLarge, c as FileTransferMode, i as isReadableByteStream, n as boundedFileStream, o as optionalBytes, r as isBoundedFileStream, s as DEFAULT_URL_FILE_MAX_BYTES, t as resolveFileStreamOptions } from "./options-DtATYdLr.js";
1
+ import { B as isRecord, C as ReportReason, E as SpanType, G as pickString, H as pickBoolean, J as identityResult, K as RetrySafety, M as defineBuiltInOperation, U as pickNumber, V as pickArray, W as pickObject, Y as mergeService, c as normalizeNotification, et as DEFAULT_STATUS_BASE_URL, o as passthroughOperation, q as defineOperation, s as voidOperation, st as createDeadline, tt as STATUS_SERVICE, w as ReportTargetType, y as InteractionType } from "./url-B3ocFlHj.js";
2
+ import { _ as ItdStateError, a as ItdConfigError, l as ItdFileError, s as ItdError, t as ItdAbortError, u as ItdFileErrorReason } from "./errors-GI10kZxk.js";
3
+ import { a as redactUrl, b as isFile, d as encodePathSegment, r as redactErrorCause, v as createDeviceId, y as isBlob } from "./redact-FNK29Fv-.js";
4
+ import { a as fileTooLarge, c as FileTransferMode, i as isReadableByteStream, n as boundedFileStream, o as optionalBytes, r as isBoundedFileStream, s as DEFAULT_URL_FILE_MAX_BYTES, t as resolveFileStreamOptions } from "./options-BNP633A5.js";
5
5
  //#region src/core/feature-catalog.ts
6
6
  /**
7
7
  * Изменяемый слой над каталогом конкретного API.
@@ -32,6 +32,9 @@ var ExtensibleOperationCatalog = class {
32
32
  get defaultBucket() {
33
33
  return this.#base.defaultBucket;
34
34
  }
35
+ definitionOf(id) {
36
+ return this.#operations.get(id) ?? this.#base.definitionOf(id);
37
+ }
35
38
  retrySafetyOf(id) {
36
39
  return this.#operations.get(id)?.retrySafety ?? this.#base.retrySafetyOf(id);
37
40
  }
@@ -49,7 +52,10 @@ var ExtensibleOperationCatalog = class {
49
52
  if (this.#base.isKnownBucket(name) || this.#bucketOwners.has(name)) throw new ItdConfigError(`feature «${owner}»: бакет «${name}» уже зарегистрирован`);
50
53
  this.#bucketOwners.set(name, owner);
51
54
  if (definition.limit !== void 0) this.#bucketLimits[name] = definition.limit;
52
- if (definition.concurrency !== void 0) this.#bucketOverrides[name] = Object.freeze({ concurrency: definition.concurrency });
55
+ if (definition.concurrency !== void 0 || definition.rps !== void 0) this.#bucketOverrides[name] = Object.freeze({
56
+ ...definition.concurrency === void 0 ? {} : { concurrency: definition.concurrency },
57
+ ...definition.rps === void 0 ? {} : { rps: definition.rps }
58
+ });
53
59
  return () => {
54
60
  if (this.#bucketOwners.get(name) !== owner) return;
55
61
  this.#bucketOwners.delete(name);
@@ -74,22 +80,311 @@ var ExtensibleOperationCatalog = class {
74
80
  }
75
81
  };
76
82
  //#endregion
83
+ //#region src/core/attachments/url-source.ts
84
+ /** Убирает параметры MIME и приводит его к форме для сравнения. */
85
+ function normalizeMimeType(contentType) {
86
+ return contentType?.split(";", 1)[0]?.trim().toLowerCase() || void 0;
87
+ }
88
+ /** Достаёт имя файла из пути URL. */
89
+ function filenameFromUrl(url) {
90
+ const last = url.pathname.split("/").pop();
91
+ if (!last) return void 0;
92
+ try {
93
+ return decodeURIComponent(last) || void 0;
94
+ } catch {
95
+ return last;
96
+ }
97
+ }
98
+ /** Проверяет объявленный размер и возвращает его, если заголовок корректен. */
99
+ function declaredSize(response, maxBytes, url) {
100
+ const header = response.headers.get("content-length");
101
+ if (header === null) return void 0;
102
+ const size = Number(header);
103
+ if (!Number.isFinite(size) || size < 0 || !Number.isInteger(size)) return void 0;
104
+ if (maxBytes !== void 0 && size > maxBytes) throw fileTooLarge(url, maxBytes, size);
105
+ return size;
106
+ }
107
+ /** Получает HTTP-ответ источника и проверяет его статус. */
108
+ async function fetchFile(target, options, context) {
109
+ let requested;
110
+ try {
111
+ requested = new URL(target);
112
+ } catch {
113
+ throw new ItdConfigError(`«${redactUrl(target)}» не разбирается как адрес`);
114
+ }
115
+ if (requested.protocol !== "http:" && requested.protocol !== "https:") throw new ItdConfigError(`вложение по адресу поддерживает только http и https, получено: ${requested.protocol}`);
116
+ let response;
117
+ try {
118
+ response = await context.fetch(requested, { ...context.signal ? { signal: context.signal } : {} });
119
+ } catch (error) {
120
+ if (context.signal?.aborted || error instanceof Error && error.name === "AbortError") throw error;
121
+ const safeUrl = redactUrl(requested.href);
122
+ throw new ItdFileError(`не удалось получить файл по адресу ${safeUrl}`, {
123
+ reason: ItdFileErrorReason.Network,
124
+ url: safeUrl,
125
+ retryable: true,
126
+ cause: redactErrorCause(error)
127
+ });
128
+ }
129
+ const finalUrl = response.url ? new URL(response.url) : requested;
130
+ if (!response.ok) {
131
+ await response.body?.cancel().catch(() => {});
132
+ const safeUrl = redactUrl(finalUrl.href);
133
+ throw new ItdFileError(`источник ${safeUrl} ответил статусом ${response.status}`, {
134
+ reason: ItdFileErrorReason.Http,
135
+ url: safeUrl,
136
+ status: response.status,
137
+ retryable: response.status === 408 || response.status === 429 || response.status >= 500
138
+ });
139
+ }
140
+ const { maxBytes } = resolveFileStreamOptions(options, DEFAULT_URL_FILE_MAX_BYTES);
141
+ let size;
142
+ try {
143
+ size = declaredSize(response, maxBytes, finalUrl.href);
144
+ } catch (error) {
145
+ await response.body?.cancel().catch(() => {});
146
+ throw error;
147
+ }
148
+ return {
149
+ response,
150
+ url: finalUrl,
151
+ size
152
+ };
153
+ }
154
+ /** Читает ответ с контролем размера до создания итогового `Blob`. */
155
+ async function responseBlob(response, url, maxBytes, streamBufferBytes, signal) {
156
+ if (!response.body) {
157
+ const blob = await response.blob();
158
+ if (maxBytes !== void 0 && blob.size > maxBytes) throw fileTooLarge(url, maxBytes, blob.size);
159
+ return blob;
160
+ }
161
+ const chunks = [];
162
+ const reader = boundedFileStream(response.body, {
163
+ ...maxBytes !== void 0 ? { maxBytes } : {},
164
+ streamBufferBytes,
165
+ ...signal ? { signal } : {},
166
+ url,
167
+ retryableRead: true
168
+ }).getReader();
169
+ try {
170
+ for (;;) {
171
+ const next = await reader.read();
172
+ if (next.done) break;
173
+ chunks.push(next.value);
174
+ }
175
+ } finally {
176
+ reader.releaseLock();
177
+ }
178
+ return new Blob(chunks.map((chunk) => Uint8Array.from(chunk).buffer));
179
+ }
180
+ /** Скачивает файл целиком с ограничением размера. @internal */
181
+ async function downloadFile(target, options, context) {
182
+ const resolved = resolveFileStreamOptions(options, DEFAULT_URL_FILE_MAX_BYTES);
183
+ const { response, url } = await fetchFile(target, options, context);
184
+ const blob = await responseBlob(response, url.href, resolved.maxBytes, resolved.streamBufferBytes, context.signal);
185
+ const contentType = normalizeMimeType(options.contentType ?? response.headers.get("content-type") ?? void 0);
186
+ const filename = options.filename ?? filenameFromUrl(url);
187
+ return {
188
+ file: new Blob([blob], { type: contentType ?? "" }),
189
+ ...filename ? { filename } : {},
190
+ ...contentType ? { contentType } : {}
191
+ };
192
+ }
193
+ /** Открывает HTTP-ответ как ограниченный поток. @internal */
194
+ async function openUrlFile(target, options, context) {
195
+ const resolved = resolveFileStreamOptions(options, DEFAULT_URL_FILE_MAX_BYTES);
196
+ const { response, url, size } = await fetchFile(target, options, context);
197
+ if (!response.body) {
198
+ const safeUrl = redactUrl(url.href);
199
+ throw new ItdFileError(`источник ${safeUrl} не предоставил потоковое тело`, {
200
+ reason: ItdFileErrorReason.StreamUnavailable,
201
+ url: safeUrl
202
+ });
203
+ }
204
+ const stream = boundedFileStream(response.body, {
205
+ ...resolved.maxBytes !== void 0 ? { maxBytes: resolved.maxBytes } : {},
206
+ streamBufferBytes: resolved.streamBufferBytes,
207
+ ...context.signal ? { signal: context.signal } : {},
208
+ url: url.href,
209
+ retryableRead: true
210
+ });
211
+ const filename = options.filename ?? filenameFromUrl(url);
212
+ const contentType = normalizeMimeType(options.contentType ?? response.headers.get("content-type") ?? void 0);
213
+ return {
214
+ stream,
215
+ ...filename ? { filename } : {},
216
+ ...contentType ? { contentType } : {},
217
+ ...size !== void 0 ? { size } : {},
218
+ close: () => stream.cancel().catch(() => {})
219
+ };
220
+ }
221
+ //#endregion
222
+ //#region src/core/attachments/resolver.ts
223
+ /** Возвращает функцию освобождения, которая исполняет действие не более одного раза. */
224
+ function closeOnce(action) {
225
+ let closing;
226
+ return () => closing ??= Promise.resolve().then(action);
227
+ }
228
+ /** Добавляет поле только для непустого значения, не выдумывая имя или MIME. */
229
+ function optionalText(name, value) {
230
+ const normalized = value?.trim();
231
+ return normalized ? { [name]: normalized } : {};
232
+ }
233
+ /** Единая реализация распознавания и открытия `FileInput`. @internal */
234
+ var DefaultFileResolver = class {
235
+ #fetch;
236
+ constructor(fetchImplementation) {
237
+ this.#fetch = fetchImplementation;
238
+ }
239
+ mode(input) {
240
+ if (typeof input !== "object" || input === null) return FileTransferMode.Buffer;
241
+ if ("open" in input) return FileTransferMode.Stream;
242
+ if ("url" in input) return resolveFileStreamOptions(input).mode;
243
+ return FileTransferMode.Buffer;
244
+ }
245
+ async resolve(input, options = {}, context = {}) {
246
+ const attempt = context.attempt ?? 1;
247
+ if (!Number.isInteger(attempt) || attempt < 1) throw new ItdConfigError(`attempt должен быть целым числом от 1, получено: ${attempt}`);
248
+ const maxBytes = optionalBytes(options.maxBytes, "maxBytes");
249
+ if (context.signal?.aborted) throw context.signal.reason ?? new DOMException("Aborted", "AbortError");
250
+ const fileContext = {
251
+ fetch: this.#fetch,
252
+ attempt,
253
+ ...context.signal ? { signal: context.signal } : {}
254
+ };
255
+ try {
256
+ const source = this.mode(input) === FileTransferMode.Stream ? await this.#openStream(input, options, fileContext, maxBytes) : await this.#loadBuffer(input, options, fileContext, maxBytes);
257
+ if (context.signal?.aborted) {
258
+ if (source.mode === FileTransferMode.Stream) await source.close();
259
+ throw context.signal.reason ?? new DOMException("Aborted", "AbortError");
260
+ }
261
+ return source;
262
+ } catch (error) {
263
+ if (error instanceof ItdError || context.signal?.aborted) throw error;
264
+ throw new ItdFileError("не удалось получить содержимое вложения", {
265
+ reason: ItdFileErrorReason.Read,
266
+ retryable: true,
267
+ cause: redactErrorCause(error)
268
+ });
269
+ }
270
+ }
271
+ async #loadBuffer(input, options, context, maxBytes) {
272
+ const content = await this.#resolveBuffer(input, context);
273
+ if (typeof content !== "object" || content === null || !("file" in content)) throw new ItdConfigError("буферный источник должен вернуть объект с полем file");
274
+ if (!isBlob(content.file) && !(content.file instanceof ArrayBuffer) && !ArrayBuffer.isView(content.file)) throw new ItdConfigError("поле file должно содержать Blob, ArrayBuffer или Uint8Array");
275
+ const contentType = normalizeMimeType(options.contentType ?? content.contentType) ?? normalizeMimeType(isBlob(content.file) ? content.file.type : void 0);
276
+ const blob = isBlob(content.file) && (!contentType || content.file.type === contentType) ? content.file : new Blob([content.file], { type: contentType ?? "" });
277
+ if (maxBytes !== void 0 && blob.size > maxBytes) throw fileTooLarge(void 0, maxBytes, blob.size);
278
+ return {
279
+ mode: FileTransferMode.Buffer,
280
+ blob,
281
+ size: blob.size,
282
+ ...optionalText("filename", options.filename ?? content.filename),
283
+ contentType
284
+ };
285
+ }
286
+ async #openStream(input, options, context, maxBytes) {
287
+ const opened = await this.#resolveStream(input, context);
288
+ if (!opened || !isReadableByteStream(opened.stream)) {
289
+ await opened?.close?.();
290
+ throw new ItdConfigError("потоковый источник должен вернуть { stream: ReadableStream }");
291
+ }
292
+ let handedOff = false;
293
+ const closeOpened = closeOnce(async () => {
294
+ await opened.close?.();
295
+ });
296
+ try {
297
+ optionalBytes(opened.size, "size");
298
+ if (maxBytes !== void 0 && opened.size !== void 0 && opened.size > maxBytes) throw fileTooLarge(void 0, maxBytes, opened.size);
299
+ if (options.requireKnownSize === true && opened.size === void 0) throw new ItdConfigError("размер потокового вложения должен быть известен до чтения");
300
+ const limits = resolveFileStreamOptions({
301
+ mode: FileTransferMode.Stream,
302
+ ...maxBytes !== void 0 ? { maxBytes } : {},
303
+ ...options.streamBufferBytes !== void 0 ? { streamBufferBytes: options.streamBufferBytes } : {}
304
+ });
305
+ const stream = isBoundedFileStream(opened.stream) && options.maxBytes === void 0 && options.streamBufferBytes === void 0 ? opened.stream : boundedFileStream(opened.stream, {
306
+ ...limits.maxBytes !== void 0 ? { maxBytes: limits.maxBytes } : {},
307
+ streamBufferBytes: limits.streamBufferBytes,
308
+ ...context.signal ? { signal: context.signal } : {}
309
+ });
310
+ let detachAbort;
311
+ const close = closeOnce(async () => {
312
+ detachAbort?.();
313
+ detachAbort = void 0;
314
+ await stream.cancel().catch(() => {});
315
+ await closeOpened();
316
+ });
317
+ if (context.signal) {
318
+ const onAbort = () => {
319
+ close().catch(() => {});
320
+ };
321
+ if (context.signal.aborted) {
322
+ await close();
323
+ throw context.signal.reason ?? new DOMException("Aborted", "AbortError");
324
+ } else {
325
+ context.signal.addEventListener("abort", onAbort, { once: true });
326
+ detachAbort = () => context.signal?.removeEventListener("abort", onAbort);
327
+ }
328
+ }
329
+ handedOff = true;
330
+ return {
331
+ mode: FileTransferMode.Stream,
332
+ stream,
333
+ size: opened.size,
334
+ ...optionalText("filename", options.filename ?? opened.filename),
335
+ contentType: normalizeMimeType(options.contentType ?? opened.contentType),
336
+ close
337
+ };
338
+ } finally {
339
+ if (!handedOff) await closeOpened();
340
+ }
341
+ }
342
+ #resolveBuffer(input, context) {
343
+ if (input instanceof ArrayBuffer || ArrayBuffer.isView(input) || isBlob(input)) return {
344
+ file: input,
345
+ ...isFile(input) ? { filename: input.name } : {}
346
+ };
347
+ if (typeof input !== "object" || input === null) throw new ItdConfigError(`вложение задано значением типа ${typeof input}; ожидается бинарное значение или объект { file }, { url }, { load }, { open }. Для файла на диске используйте fromPath('./photo.jpg') из itd-api/node`);
348
+ if ("load" in input) return input.load(context);
349
+ if ("url" in input) {
350
+ const { url, ...urlOptions } = input;
351
+ return downloadFile(url, urlOptions, context);
352
+ }
353
+ if ("file" in input) return input;
354
+ if ("open" in input) throw new ItdConfigError("потоковый источник нельзя использовать как буферный");
355
+ throw new ItdConfigError("вложение не распознано: ожидается { file }, { url }, { load } или { open }");
356
+ }
357
+ #resolveStream(input, context) {
358
+ if (typeof input === "object" && input !== null && "open" in input) return input.open(context);
359
+ if (typeof input === "object" && input !== null && "url" in input) {
360
+ const { url, ...urlOptions } = input;
361
+ return openUrlFile(url, urlOptions, context);
362
+ }
363
+ throw new ItdConfigError("потоковое вложение должно иметь форму { open } или { url, mode }");
364
+ }
365
+ };
366
+ /** Создаёт механизм подготовки файлов, привязанный к настроенному `fetch` клиента. @internal */
367
+ function createFileResolver(fetchImplementation) {
368
+ return new DefaultFileResolver(fetchImplementation);
369
+ }
370
+ //#endregion
77
371
  //#region src/core/features.ts
78
372
  function requireName(value, kind) {
79
373
  if (typeof value !== "string" || value.trim() === "") throw new ItdConfigError(`${kind} должен иметь непустое имя`);
80
374
  return value.trim();
81
375
  }
82
- function requireOperationName(value, featureName) {
83
- const name = requireName(value, `Операция feature «${featureName}»`);
84
- if (name.includes(".")) throw new ItdConfigError(`feature «${featureName}»: локальное имя операции «${name}» не должно содержать точку`);
376
+ function requireFeatureName(value) {
377
+ const name = requireName(value, "Feature");
378
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) throw new ItdConfigError(`имя feature «${name}» должно соответствовать [a-z][a-z0-9-]*`);
85
379
  return name;
86
380
  }
87
381
  function validateBucket(owner, localName, bucket) {
88
382
  if (typeof bucket !== "object" || bucket === null || Array.isArray(bucket)) throw new ItdConfigError(`feature «${owner}»: бакет «${localName}» должен быть объектом`);
89
383
  if (bucket.limit !== void 0 && (!Number.isFinite(bucket.limit) || bucket.limit <= 0)) throw new ItdConfigError(`feature «${owner}»: limit бакета «${localName}» должен быть положительным числом`);
90
384
  if (bucket.concurrency !== void 0 && (!Number.isInteger(bucket.concurrency) || bucket.concurrency < 1)) throw new ItdConfigError(`feature «${owner}»: concurrency бакета «${localName}» должен быть целым числом от 1`);
385
+ if (bucket.rps !== void 0 && (!Number.isFinite(bucket.rps) || bucket.rps <= 0)) throw new ItdConfigError(`feature «${owner}»: rps бакета «${localName}» должен быть положительным числом`);
91
386
  }
92
- /** Реестр feature одного facade. @internal */
387
+ /** Реестр подключаемых модулей клиента. @internal */
93
388
  var FeatureRegistry = class {
94
389
  #deps;
95
390
  #features = /* @__PURE__ */ new Map();
@@ -102,7 +397,7 @@ var FeatureRegistry = class {
102
397
  this.#deps.assertActive("установить feature");
103
398
  if (this.#disposed) throw new ItdStateError("Реестр feature уже освобождён");
104
399
  if (typeof feature !== "object" || feature === null) throw new ItdConfigError("feature должен быть объектом");
105
- const name = requireName(feature.name, "Feature");
400
+ const name = requireFeatureName(feature.name);
106
401
  if (this.#features.has(name)) throw new ItdConfigError(`feature «${name}» уже установлен`);
107
402
  if (typeof feature.setup !== "function") throw new ItdConfigError(`feature «${name}» должен предоставлять setup()`);
108
403
  if (typeof feature.operations !== "object" || feature.operations === null || Array.isArray(feature.operations)) throw new ItdConfigError(`feature «${name}» должен объявлять operations`);
@@ -114,6 +409,11 @@ var FeatureRegistry = class {
114
409
  const operations = /* @__PURE__ */ new Map();
115
410
  const controller = new AbortController();
116
411
  let committed = false;
412
+ const reportRollbackError = (message, error) => {
413
+ try {
414
+ this.#deps.logger?.error(message, error);
415
+ } catch {}
416
+ };
117
417
  try {
118
418
  for (const service of feature.services ?? []) {
119
419
  const serviceName = requireName(service?.name, `Сервис feature «${name}»`);
@@ -147,7 +447,7 @@ var FeatureRegistry = class {
147
447
  bucketNames.set(localName, globalName);
148
448
  }
149
449
  for (const [rawLocalName, definition] of Object.entries(feature.operations)) {
150
- const localName = requireOperationName(rawLocalName, name);
450
+ const localName = requireName(rawLocalName, `Операция feature «${name}»`);
151
451
  if (typeof definition !== "object" || definition === null) throw new ItdConfigError(`feature «${name}»: операция «${localName}» должна быть объектом`);
152
452
  if (![
153
453
  "GET",
@@ -159,19 +459,27 @@ var FeatureRegistry = class {
159
459
  if (!Object.values(RetrySafety).includes(definition.retrySafety)) throw new ItdConfigError(`feature «${name}»: операция «${localName}» содержит неизвестную retrySafety`);
160
460
  if (definition.service !== void 0 && typeof definition.service !== "string") throw new ItdConfigError(`feature «${name}»: service операции «${localName}» должен быть строкой`);
161
461
  if (definition.bucket !== void 0 && typeof definition.bucket !== "string") throw new ItdConfigError(`feature «${name}»: bucket операции «${localName}» должен быть строкой`);
462
+ if (definition.read !== void 0 && typeof definition.read !== "function") throw new ItdConfigError(`feature «${name}»: read операции «${localName}» должна быть функцией`);
463
+ if (definition.annotations !== void 0 && (typeof definition.annotations !== "object" || definition.annotations === null || Array.isArray(definition.annotations))) throw new ItdConfigError(`feature «${name}»: annotations операции «${localName}» должны быть объектом`);
162
464
  if (definition.service !== void 0 && !services.has(definition.service)) throw new ItdConfigError(`feature «${name}»: операция «${localName}» ссылается на необъявленный сервис «${definition.service}»`);
163
465
  const bucket = definition.bucket === void 0 ? this.#deps.catalog.defaultBucket : bucketNames.get(definition.bucket);
164
466
  if (bucket === void 0) throw new ItdConfigError(`feature «${name}»: операция «${localName}» ссылается на необъявленный бакет «${definition.bucket}»`);
165
467
  const operationId = `${name}.${localName}`;
166
- const unregister = this.#deps.catalog.registerOperation(name, operationId, {
468
+ const contract = defineOperation(operationId, {
167
469
  method: definition.method,
168
470
  retrySafety: definition.retrySafety,
169
- bucket
471
+ bucket,
472
+ ...definition.annotations === void 0 ? {} : { annotations: definition.annotations }
473
+ }, definition.read ?? identityResult);
474
+ const unregister = this.#deps.catalog.registerOperation(name, operationId, {
475
+ method: contract.method,
476
+ retrySafety: contract.retrySafety,
477
+ bucket,
478
+ ...contract.annotations === void 0 ? {} : { annotations: contract.annotations }
170
479
  });
171
480
  rollback.push(unregister);
172
481
  operations.set(localName, {
173
- operationId,
174
- method: definition.method,
482
+ contract,
175
483
  service: definition.service
176
484
  });
177
485
  }
@@ -181,6 +489,15 @@ var FeatureRegistry = class {
181
489
  signal: controller.signal,
182
490
  clock: this.#deps.clock,
183
491
  logger: this.#deps.logger,
492
+ files: Object.freeze({ resolve: (input, options, context) => {
493
+ try {
494
+ this.#deps.assertActive(`открыть файловый источник feature «${name}»`);
495
+ if (!committed || controller.signal.aborted) throw new ItdStateError(`feature «${name}» не активен`);
496
+ return this.#deps.files.resolve(input, options, context);
497
+ } catch (error) {
498
+ return Promise.reject(error);
499
+ }
500
+ } }),
184
501
  request: (operation, options) => {
185
502
  try {
186
503
  this.#deps.assertActive(`выполнить операцию feature «${name}»`);
@@ -194,10 +511,8 @@ var FeatureRegistry = class {
194
511
  delete scoped.baseUrl;
195
512
  delete scoped.retrySafety;
196
513
  delete scoped.rateLimitBucket;
197
- return this.#deps.http.request({
514
+ return this.#deps.http.execute(resolved.contract, {
198
515
  ...scoped,
199
- operationId: resolved.operationId,
200
- method: resolved.method,
201
516
  ...resolved.service === void 0 ? {} : { service: resolved.service }
202
517
  });
203
518
  } catch (error) {
@@ -207,6 +522,43 @@ var FeatureRegistry = class {
207
522
  serviceBaseUrl: (serviceName) => {
208
523
  if (!services.has(serviceName)) throw new ItdConfigError(`feature «${name}» не объявляет сервис «${serviceName}»`);
209
524
  return this.#deps.services.resolveBaseUrl(serviceName);
525
+ },
526
+ connection: (serviceName) => {
527
+ if (!services.has(serviceName)) throw new ItdConfigError(`feature «${name}» не объявляет сервис «${serviceName}»`);
528
+ return this.#deps.connection(serviceName);
529
+ },
530
+ manage: (resource) => {
531
+ this.#deps.assertActive(`зарегистрировать ресурс feature «${name}»`);
532
+ if (controller.signal.aborted) throw new ItdStateError(`feature «${name}» не активен`);
533
+ const unregister = this.#deps.manage(resource);
534
+ let registered = true;
535
+ const release = () => {
536
+ if (!registered) return;
537
+ registered = false;
538
+ unregister();
539
+ };
540
+ if (!committed) rollback.push(() => {
541
+ if (!registered) return;
542
+ if (committed) {
543
+ release();
544
+ return;
545
+ }
546
+ try {
547
+ resource.stop();
548
+ } catch (error) {
549
+ reportRollbackError(`Не удалось остановить ресурс «${resource.kind}» при откате feature «${name}»`, error);
550
+ } finally {
551
+ release();
552
+ }
553
+ Promise.resolve().then(() => resource.drain()).catch((error) => {
554
+ reportRollbackError(`Не удалось освободить ресурс «${resource.kind}» при откате feature «${name}»`, error);
555
+ });
556
+ });
557
+ return release;
558
+ },
559
+ assertActive: (action) => {
560
+ this.#deps.assertActive(action);
561
+ if (!committed || controller.signal.aborted) throw new ItdStateError(`feature «${name}» не активен`);
210
562
  }
211
563
  });
212
564
  const installation = feature.setup(context);
@@ -235,36 +587,166 @@ var FeatureRegistry = class {
235
587
  return this.#features.has(name);
236
588
  }
237
589
  async close() {
238
- const errors = [];
239
- for (const feature of [...this.#features.values()].reverse()) try {
240
- await feature.close?.();
241
- } catch (error) {
242
- errors.push(error);
243
- }
590
+ const errors = (await Promise.allSettled([...this.#features.values()].reverse().map(async (feature) => feature.close?.()))).filter((result) => result.status === "rejected").map((result) => result.reason);
244
591
  if (errors.length > 0) throw new AggregateError(errors, "Не удалось закрыть feature");
245
592
  }
246
593
  async dispose() {
247
594
  if (this.#disposed) return;
248
595
  this.#disposed = true;
249
596
  const errors = [];
250
- for (const feature of [...this.#features.values()].reverse()) {
597
+ const disposals = [];
598
+ const features = [...this.#features.values()].reverse();
599
+ for (const feature of features) {
251
600
  feature.controller.abort(new ItdAbortError(`Feature «${feature.name}» освобождён`));
252
601
  try {
253
- await feature.dispose?.();
254
- } catch (error) {
255
- errors.push(error);
256
- }
257
- for (const cleanup of [...feature.cleanup].reverse()) try {
258
- cleanup();
602
+ disposals.push(Promise.resolve(feature.dispose?.()).then(() => void 0));
259
603
  } catch (error) {
260
604
  errors.push(error);
261
605
  }
262
606
  }
607
+ for (const feature of features) for (const cleanup of [...feature.cleanup].reverse()) try {
608
+ cleanup();
609
+ } catch (error) {
610
+ errors.push(error);
611
+ }
263
612
  this.#features.clear();
613
+ const results = await Promise.allSettled(disposals);
614
+ for (const result of results) if (result.status === "rejected") errors.push(result.reason);
264
615
  if (errors.length > 0) throw new AggregateError(errors, "Не удалось освободить feature");
265
616
  }
266
617
  };
267
618
  //#endregion
619
+ //#region src/core/managed-resources.ts
620
+ /** Реестр активных ресурсов клиента. @internal */
621
+ var ManagedResourceRegistry = class {
622
+ #resources = /* @__PURE__ */ new Set();
623
+ #disposed = false;
624
+ register(resource) {
625
+ if (this.#disposed) throw new ItdStateError("Lifecycle клиента уже освобождён");
626
+ if (typeof resource !== "object" || resource === null || typeof resource.kind !== "string" || resource.kind.trim() === "" || typeof resource.stop !== "function" || typeof resource.drain !== "function") throw new ItdConfigError("Managed resource должен предоставлять kind, stop() и drain()");
627
+ this.#resources.add(resource);
628
+ let registered = true;
629
+ return () => {
630
+ if (!registered) return;
631
+ registered = false;
632
+ this.#resources.delete(resource);
633
+ };
634
+ }
635
+ /** Останавливает snapshot синхронно, чтобы новые auth-данные ещё не были опубликованы. */
636
+ stopAll() {
637
+ const resources = [...this.#resources];
638
+ const errors = [];
639
+ for (const resource of resources) try {
640
+ resource.stop();
641
+ } catch (error) {
642
+ errors.push(error);
643
+ }
644
+ return {
645
+ resources,
646
+ errors
647
+ };
648
+ }
649
+ dispose() {
650
+ this.#disposed = true;
651
+ this.#resources.clear();
652
+ }
653
+ };
654
+ //#endregion
655
+ //#region src/core/feature-host.ts
656
+ /**
657
+ * Управляет подключаемыми модулями и их ресурсами.
658
+ *
659
+ * @internal
660
+ */
661
+ var ClientFeatureHost = class {
662
+ #runtime;
663
+ #features;
664
+ #resources = new ManagedResourceRegistry();
665
+ /** Общий файловый порт ресурсов и подключаемых модулей этого клиента. @internal */
666
+ files;
667
+ constructor(runtime, options) {
668
+ this.#runtime = runtime;
669
+ this.files = createFileResolver(runtime.config.fetch);
670
+ this.#features = new FeatureRegistry({
671
+ http: runtime.http,
672
+ services: runtime.services,
673
+ serviceOverrides: runtime.config.services,
674
+ catalog: options.catalog,
675
+ baseUrl: runtime.config.baseUrl,
676
+ clock: runtime.config.clock,
677
+ logger: runtime.config.logger,
678
+ files: this.files,
679
+ assertActive: options.assertActive,
680
+ connection: (serviceName) => runtime.connection(serviceName),
681
+ manage: (resource) => this.#resources.register(resource),
682
+ registerBucket: (name, definition) => runtime.registerRateLimitBucket(name, definition)
683
+ });
684
+ }
685
+ install(feature) {
686
+ return this.#features.install(feature);
687
+ }
688
+ names() {
689
+ return this.#features.names();
690
+ }
691
+ has(name) {
692
+ return this.#features.has(name);
693
+ }
694
+ /** Регистрирует ресурс самого клиента. @internal */
695
+ manage(resource) {
696
+ return this.#resources.register(resource);
697
+ }
698
+ /** Останавливает ресурсы перед сменой авторизации. */
699
+ stop() {
700
+ const { errors } = this.#resources.stopAll();
701
+ if (errors.length > 0) this.#runtime.config.logger?.error("Не удалось остановить ресурсы клиента", errors);
702
+ }
703
+ /** Останавливает ресурсы и ждёт функции закрытия модулей. */
704
+ async close() {
705
+ const snapshot = this.#resources.stopAll();
706
+ const errors = [...snapshot.errors];
707
+ const deadline = createDeadline(this.#runtime.config.shutdownTimeout, this.#runtime.config.clock);
708
+ const tasks = [...snapshot.resources.map((resource) => ({
709
+ kind: resource.kind,
710
+ promise: Promise.resolve().then(() => resource.drain())
711
+ })), {
712
+ kind: "feature lifecycle",
713
+ promise: Promise.resolve().then(() => this.#features.close())
714
+ }];
715
+ try {
716
+ const stuck = (await Promise.all(tasks.map(async ({ kind, promise }) => {
717
+ const result = promise.catch((error) => {
718
+ errors.push(error);
719
+ });
720
+ return await deadline.wait(result) ? void 0 : kind;
721
+ }))).filter((kind) => kind !== void 0);
722
+ if (stuck.length > 0) errors.push(new ItdStateError(`ресурсы (${stuck.join(", ")}) не завершились за ${this.#runtime.config.shutdownTimeout} мс`));
723
+ } finally {
724
+ deadline.cancel();
725
+ }
726
+ if (errors.length === 1) throw errors[0];
727
+ if (errors.length > 1) throw new AggregateError(errors, "Не удалось закрыть feature-host");
728
+ }
729
+ async dispose() {
730
+ this.#resources.dispose();
731
+ const deadline = createDeadline(this.#runtime.config.shutdownTimeout, this.#runtime.config.clock);
732
+ let failed = false;
733
+ let failure;
734
+ const settled = this.#features.dispose().catch((error) => {
735
+ failed = true;
736
+ failure = error;
737
+ });
738
+ try {
739
+ if (!await deadline.wait(settled)) throw new ItdStateError(`освобождение подключаемых модулей не завершилось за ${this.#runtime.config.shutdownTimeout} мс`);
740
+ } finally {
741
+ deadline.cancel();
742
+ }
743
+ if (failed) throw failure;
744
+ }
745
+ };
746
+ function createFeatureHost(runtime, options) {
747
+ return new ClientFeatureHost(runtime, options);
748
+ }
749
+ //#endregion
268
750
  //#region src/resources/pagination.ts
269
751
  /** Схема пагинации эндпоинта. */
270
752
  const PaginationMode = Object.freeze({
@@ -382,6 +864,10 @@ function readOffsetPage(body, field, offset) {
382
864
  raw: body
383
865
  };
384
866
  }
867
+ /** Создаёт единый контракт пагинированной операции. */
868
+ function pageOperation(id, read) {
869
+ return defineBuiltInOperation(id, read);
870
+ }
385
871
  /**
386
872
  * Перебор страниц списка.
387
873
  *
@@ -507,6 +993,10 @@ var BaseResource = class {
507
993
  constructor(http) {
508
994
  this.http = http;
509
995
  }
996
+ /** Выполняет операцию без возвращаемого значения и отбрасывает служебное тело ответа. */
997
+ voidOperation(operation, options) {
998
+ return this.http.execute(operation, options);
999
+ }
510
1000
  /**
511
1001
  * Собирает перебор страниц.
512
1002
  *
@@ -532,24 +1022,22 @@ var BaseResource = class {
532
1022
  * @example
533
1023
  * ```ts
534
1024
  * #feed = this.paginated<Post, FeedParams>({
535
- * operationId: 'posts.list',
1025
+ * operation: POSTS_LIST,
536
1026
  * path: () => '/api/posts',
537
1027
  * query: (p) => ({ tab: p.tab, limit: p.limit }),
538
1028
  * start: (p) => (p.cursor ? { cursor: p.cursor } : {}),
539
- * read: (body) => readCursorPage<Post>(body, 'posts'),
540
1029
  * mode: PaginationMode.Cursor,
541
1030
  * });
542
1031
  * ```
543
1032
  */
544
1033
  paginated(spec) {
545
1034
  const load = async (params, state, options = {}) => {
546
- const operationId = typeof spec.operationId === "function" ? spec.operationId(params) : spec.operationId;
547
- const body = await this.http.operation(operationId, {
1035
+ const operation = typeof spec.operation === "function" ? spec.operation(params) : spec.operation;
1036
+ return this.http.execute(operation, {
548
1037
  path: spec.path(params),
549
1038
  query: withPageState(spec.query(params), state),
550
1039
  ...options
551
1040
  });
552
- return spec.read(body, state);
553
1041
  };
554
1042
  return {
555
1043
  list: (params, options) => load(params, spec.start(params), options),
@@ -574,6 +1062,163 @@ function withPageState(query, state) {
574
1062
  };
575
1063
  }
576
1064
  //#endregion
1065
+ //#region src/resources/notifications.ts
1066
+ const NOTIFICATIONS_LIST = pageOperation("notifications.list", (body, request) => {
1067
+ const offset = Number(request.query?.offset ?? 0);
1068
+ const page = readOffsetPage(body, "notifications", Number.isFinite(offset) ? offset : 0);
1069
+ return {
1070
+ ...page,
1071
+ items: page.items.map(normalizeNotification)
1072
+ };
1073
+ });
1074
+ const NOTIFICATIONS_COUNT = defineBuiltInOperation("notifications.count", (body) => pickNumber(body, "count", 0));
1075
+ const markedCountOperation = (id) => defineBuiltInOperation(id, (body) => pickNumber(body, "markedCount", 0));
1076
+ const NOTIFICATIONS_MARK_READ = markedCountOperation("notifications.markRead");
1077
+ const NOTIFICATIONS_MARK_READ_BATCH = markedCountOperation("notifications.markReadBatch");
1078
+ const NOTIFICATIONS_MARK_ALL_READ = markedCountOperation("notifications.markAllRead");
1079
+ const NOTIFICATIONS_GET_SETTINGS = defineBuiltInOperation("notifications.getSettings", readSettings);
1080
+ const NOTIFICATIONS_UPDATE_SETTINGS = defineBuiltInOperation("notifications.updateSettings", readSettings);
1081
+ const NOTIFICATION_SETTING_KEYS = [
1082
+ "enabled",
1083
+ "sound",
1084
+ "follows",
1085
+ "wallPosts",
1086
+ "likes",
1087
+ "comments",
1088
+ "mentions"
1089
+ ];
1090
+ /**
1091
+ * Сколько идентификаторов уходит в одном запросе на отметку прочтения.
1092
+ *
1093
+ * Столько же отправляет сайт итд.com — значит на сервере, скорее всего, есть ограничение.
1094
+ */
1095
+ const READ_BATCH_SIZE = 20;
1096
+ /**
1097
+ * Читает настройки уведомлений.
1098
+ *
1099
+ * Сервер отдаёт плоский объект: `enabled`, `sound`, `follows`, `wallPosts`, `likes`,
1100
+ * `comments`, `mentions`. Отсутствующая настройка считается включённой — так же
1101
+ * ведёт себя сайт итд.com.
1102
+ */
1103
+ function readSettings(body) {
1104
+ const settings = {};
1105
+ for (const key of NOTIFICATION_SETTING_KEYS) settings[key] = pickBoolean(body, key, true);
1106
+ return settings;
1107
+ }
1108
+ /**
1109
+ * Уведомления: список, счётчик, отметки о прочтении, настройки.
1110
+ *
1111
+ * Доступна как `itd.notifications`. Все уведомления приведены к единой форме, поэтому
1112
+ * объекты отсюда и из потока событий можно складывать в один список.
1113
+ */
1114
+ var NotificationsResource = class extends BaseResource {
1115
+ /** Уведомления: `/api/notifications/`, пагинация по смещению. */
1116
+ #list = this.paginated({
1117
+ operation: NOTIFICATIONS_LIST,
1118
+ path: () => "/api/notifications/",
1119
+ query: (p) => ({ limit: p.limit }),
1120
+ start: (p) => ({ offset: p.offset ?? 0 }),
1121
+ mode: PaginationMode.Offset
1122
+ });
1123
+ /**
1124
+ * Загружает страницу уведомлений.
1125
+ *
1126
+ * Пагинация здесь основана на смещении.
1127
+ *
1128
+ * @example
1129
+ * ```ts
1130
+ * const page = await itd.notifications.list({ limit: 20 });
1131
+ * const next = await itd.notifications.list({ limit: 20, offset: page.nextOffset });
1132
+ * ```
1133
+ */
1134
+ list(params = {}, options = {}) {
1135
+ return this.#list.list(params, options);
1136
+ }
1137
+ /**
1138
+ * Перебирает уведомления.
1139
+ *
1140
+ * @example
1141
+ * ```ts
1142
+ * for await (const notification of itd.notifications.iterate()) {
1143
+ * console.log(formatNotificationText(notification));
1144
+ * }
1145
+ * ```
1146
+ */
1147
+ iterate(params = {}, options = {}) {
1148
+ return this.#list.iterate(params, options);
1149
+ }
1150
+ /** Загружает число непрочитанных уведомлений. */
1151
+ count(options = {}) {
1152
+ return this.http.execute(NOTIFICATIONS_COUNT, {
1153
+ path: "/api/notifications/count",
1154
+ ...options
1155
+ });
1156
+ }
1157
+ /**
1158
+ * Отмечает уведомление прочитанным.
1159
+ *
1160
+ * @returns сколько записей отметил сервер
1161
+ */
1162
+ markRead(notificationId, options = {}) {
1163
+ return this.http.execute(NOTIFICATIONS_MARK_READ, {
1164
+ path: `/api/notifications/${encodePathSegment(notificationId, "notificationId")}/read`,
1165
+ ...options
1166
+ });
1167
+ }
1168
+ /**
1169
+ * Отмечает прочитанными сразу несколько уведомлений.
1170
+ *
1171
+ * Список автоматически режется на части по 20 идентификаторов — столько же отправляет
1172
+ * сайт итд.com, поэтому на сервере вероятен предел. Части уходят последовательно,
1173
+ * результат суммируется.
1174
+ *
1175
+ * @returns сколько записей отметил сервер суммарно
1176
+ */
1177
+ async markReadBatch(ids, options = {}) {
1178
+ let marked = 0;
1179
+ for (let index = 0; index < ids.length; index += READ_BATCH_SIZE) {
1180
+ const chunk = ids.slice(index, index + READ_BATCH_SIZE);
1181
+ marked += await this.http.execute(NOTIFICATIONS_MARK_READ_BATCH, {
1182
+ path: "/api/notifications/read-batch",
1183
+ body: { ids: chunk },
1184
+ ...options
1185
+ });
1186
+ }
1187
+ return marked;
1188
+ }
1189
+ /** Отмечает прочитанными все уведомления. */
1190
+ markAllRead(options = {}) {
1191
+ return this.http.execute(NOTIFICATIONS_MARK_ALL_READ, {
1192
+ path: "/api/notifications/read-all",
1193
+ ...options
1194
+ });
1195
+ }
1196
+ /** Загружает настройки уведомлений. */
1197
+ getSettings(options = {}) {
1198
+ return this.http.execute(NOTIFICATIONS_GET_SETTINGS, {
1199
+ path: "/api/notifications/settings",
1200
+ ...options
1201
+ });
1202
+ }
1203
+ /**
1204
+ * Обновляет настройки уведомлений.
1205
+ *
1206
+ * Отправляются только изменяемые поля, в том же виде, в каком сервер их возвращает.
1207
+ */
1208
+ async updateSettings(input, options = {}) {
1209
+ const payload = {};
1210
+ for (const key of NOTIFICATION_SETTING_KEYS) {
1211
+ const value = input[key];
1212
+ if (value !== void 0) payload[key] = value;
1213
+ }
1214
+ return this.http.execute(NOTIFICATIONS_UPDATE_SETTINGS, {
1215
+ path: "/api/notifications/settings",
1216
+ body: payload,
1217
+ ...options
1218
+ });
1219
+ }
1220
+ };
1221
+ //#endregion
577
1222
  //#region src/domain/time.ts
578
1223
  const NAIVE_STAMP = /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}(?:\.\d+)?)$/;
579
1224
  /**
@@ -627,11 +1272,11 @@ var StatusResource = class {
627
1272
  constructor(context) {
628
1273
  this.#context = context;
629
1274
  }
630
- async get(options = {}) {
631
- return normalizeStatus(await this.#context.request("get", {
1275
+ get(options = {}) {
1276
+ return this.#context.request("get", {
632
1277
  path: "/api/status",
633
1278
  ...options
634
- }));
1279
+ });
635
1280
  }
636
1281
  };
637
1282
  /** Встроенный модуль страницы состояния платформы. @internal */
@@ -646,7 +1291,8 @@ function createStatusFeature() {
646
1291
  operations: { get: {
647
1292
  method: "GET",
648
1293
  retrySafety: RetrySafety.Safe,
649
- service: STATUS_SERVICE
1294
+ service: STATUS_SERVICE,
1295
+ read: (body) => normalizeStatus(body)
650
1296
  } },
651
1297
  setup: (context) => ({ api: new StatusResource(context) })
652
1298
  };
@@ -811,6 +1457,13 @@ function resolveComment(input, allowReplyTo = false) {
811
1457
  }
812
1458
  //#endregion
813
1459
  //#region src/resources/comments.ts
1460
+ const COMMENTS_REPLIES = pageOperation("comments.replies", (body) => readPagedPage(body, "replies"));
1461
+ const COMMENTS_REPLY = passthroughOperation("comments.reply");
1462
+ const COMMENTS_UPDATE = passthroughOperation("comments.update");
1463
+ const COMMENTS_RESTORE = passthroughOperation("comments.restore");
1464
+ const COMMENTS_LIKE = passthroughOperation("comments.like");
1465
+ const COMMENTS_UNLIKE = passthroughOperation("comments.unlike");
1466
+ const COMMENTS_REMOVE = voidOperation("comments.remove");
814
1467
  /**
815
1468
  * Комментарии и ответы на них.
816
1469
  *
@@ -821,11 +1474,10 @@ var CommentsResource = class extends BaseResource {
821
1474
  #uploadFiles;
822
1475
  /** Ответы на комментарий: `/api/comments/{id}/replies`, постраничная пагинация. */
823
1476
  #replies = this.paginated({
824
- operationId: "comments.replies",
1477
+ operation: COMMENTS_REPLIES,
825
1478
  path: (p) => `/api/comments/${encodePathSegment(p.commentId, "commentId")}/replies`,
826
1479
  query: (p) => ({ limit: p.limit }),
827
1480
  start: (p) => p.page !== void 0 ? { page: p.page } : {},
828
- read: (body) => readPagedPage(body, "replies"),
829
1481
  mode: PaginationMode.Page
830
1482
  });
831
1483
  constructor(http, deps) {
@@ -864,7 +1516,7 @@ var CommentsResource = class extends BaseResource {
864
1516
  const existing = data.attachmentIds ?? [];
865
1517
  const files = data.files ?? [];
866
1518
  const attachmentIds = files.length > 0 ? [...existing, ...await this.#uploadFiles(files, options)] : existing;
867
- return this.http.operation("comments.reply", {
1519
+ return this.http.execute(COMMENTS_REPLY, {
868
1520
  path: `/api/comments/${encodePathSegment(commentId, "commentId")}/replies`,
869
1521
  body: {
870
1522
  content: data.content ?? "",
@@ -876,7 +1528,7 @@ var CommentsResource = class extends BaseResource {
876
1528
  }
877
1529
  /** Редактирует текст комментария. */
878
1530
  update(commentId, content, options = {}) {
879
- return this.http.operation("comments.update", {
1531
+ return this.http.execute(COMMENTS_UPDATE, {
880
1532
  path: `/api/comments/${encodePathSegment(commentId, "commentId")}`,
881
1533
  body: { content },
882
1534
  ...options
@@ -884,167 +1536,33 @@ var CommentsResource = class extends BaseResource {
884
1536
  }
885
1537
  /** Удаляет комментарий. Восстановить его можно через {@link restore}. */
886
1538
  remove(commentId, options = {}) {
887
- return this.http.operation("comments.remove", {
1539
+ return this.voidOperation(COMMENTS_REMOVE, {
888
1540
  path: `/api/comments/${encodePathSegment(commentId, "commentId")}`,
889
1541
  ...options
890
1542
  });
891
1543
  }
892
- /** Восстанавливает удалённый комментарий. */
893
- restore(commentId, options = {}) {
894
- return this.http.operation("comments.restore", {
895
- path: `/api/comments/${encodePathSegment(commentId, "commentId")}/restore`,
896
- ...options
897
- });
898
- }
899
- /** Ставит реакцию на комментарий. */
900
- like(commentId, options = {}) {
901
- return this.http.operation("comments.like", {
902
- path: `/api/comments/${encodePathSegment(commentId, "commentId")}/like`,
903
- ...options
904
- });
905
- }
906
- /** Убирает реакцию с комментария. */
907
- unlike(commentId, options = {}) {
908
- return this.http.operation("comments.unlike", {
909
- path: `/api/comments/${encodePathSegment(commentId, "commentId")}/like`,
910
- ...options
911
- });
912
- }
913
- };
914
- //#endregion
915
- //#region src/core/attachments/url-source.ts
916
- /** Убирает параметры MIME и приводит его к форме для сравнения. */
917
- function normalizeMimeType(contentType) {
918
- return contentType?.split(";", 1)[0]?.trim().toLowerCase() || void 0;
919
- }
920
- /** Достаёт имя файла из пути URL. */
921
- function filenameFromUrl(url) {
922
- const last = url.pathname.split("/").pop();
923
- if (!last) return void 0;
924
- try {
925
- return decodeURIComponent(last) || void 0;
926
- } catch {
927
- return last;
928
- }
929
- }
930
- /** Проверяет объявленный размер и возвращает его, если заголовок корректен. */
931
- function declaredSize(response, maxBytes, url) {
932
- const header = response.headers.get("content-length");
933
- if (header === null) return void 0;
934
- const size = Number(header);
935
- if (!Number.isFinite(size) || size < 0 || !Number.isInteger(size)) return void 0;
936
- if (maxBytes !== void 0 && size > maxBytes) throw fileTooLarge(url, maxBytes, size);
937
- return size;
938
- }
939
- /** Получает HTTP-ответ источника и проверяет его статус. */
940
- async function fetchFile(target, options, context) {
941
- let requested;
942
- try {
943
- requested = new URL(target);
944
- } catch {
945
- throw new ItdConfigError(`«${target}» не разбирается как адрес`);
946
- }
947
- if (requested.protocol !== "http:" && requested.protocol !== "https:") throw new ItdConfigError(`вложение по адресу поддерживает только http и https, получено: ${requested.protocol}`);
948
- let response;
949
- try {
950
- response = await context.fetch(requested, { ...context.signal ? { signal: context.signal } : {} });
951
- } catch (error) {
952
- if (context.signal?.aborted || error instanceof Error && error.name === "AbortError") throw error;
953
- throw new ItdFileError(`не удалось получить файл по адресу ${requested.href}`, {
954
- reason: ItdFileErrorReason.Network,
955
- url: requested.href,
956
- retryable: true,
957
- cause: error
958
- });
959
- }
960
- const finalUrl = response.url ? new URL(response.url) : requested;
961
- if (!response.ok) {
962
- await response.body?.cancel().catch(() => {});
963
- throw new ItdFileError(`источник ${finalUrl.href} ответил статусом ${response.status}`, {
964
- reason: ItdFileErrorReason.Http,
965
- url: finalUrl.href,
966
- status: response.status,
967
- retryable: response.status === 408 || response.status === 429 || response.status >= 500
968
- });
969
- }
970
- const { maxBytes } = resolveFileStreamOptions(options, DEFAULT_URL_FILE_MAX_BYTES);
971
- let size;
972
- try {
973
- size = declaredSize(response, maxBytes, finalUrl.href);
974
- } catch (error) {
975
- await response.body?.cancel().catch(() => {});
976
- throw error;
977
- }
978
- return {
979
- response,
980
- url: finalUrl,
981
- size
982
- };
983
- }
984
- /** Читает ответ с контролем размера до создания итогового `Blob`. */
985
- async function responseBlob(response, url, maxBytes, streamBufferBytes, signal) {
986
- if (!response.body) {
987
- const blob = await response.blob();
988
- if (maxBytes !== void 0 && blob.size > maxBytes) throw fileTooLarge(url, maxBytes, blob.size);
989
- return blob;
990
- }
991
- const chunks = [];
992
- const reader = boundedFileStream(response.body, {
993
- ...maxBytes !== void 0 ? { maxBytes } : {},
994
- streamBufferBytes,
995
- ...signal ? { signal } : {},
996
- url,
997
- retryableRead: true
998
- }).getReader();
999
- try {
1000
- for (;;) {
1001
- const next = await reader.read();
1002
- if (next.done) break;
1003
- chunks.push(next.value);
1004
- }
1005
- } finally {
1006
- reader.releaseLock();
1007
- }
1008
- return new Blob(chunks.map((chunk) => Uint8Array.from(chunk).buffer));
1009
- }
1010
- /** Скачивает файл целиком с ограничением размера. @internal */
1011
- async function downloadFile(target, options, context) {
1012
- const resolved = resolveFileStreamOptions(options, DEFAULT_URL_FILE_MAX_BYTES);
1013
- const { response, url } = await fetchFile(target, options, context);
1014
- const blob = await responseBlob(response, url.href, resolved.maxBytes, resolved.streamBufferBytes, context.signal);
1015
- const contentType = normalizeMimeType(options.contentType ?? response.headers.get("content-type") ?? void 0);
1016
- const filename = options.filename ?? filenameFromUrl(url);
1017
- return {
1018
- file: new Blob([blob], { type: contentType ?? "" }),
1019
- ...filename ? { filename } : {},
1020
- ...contentType ? { contentType } : {}
1021
- };
1022
- }
1023
- /** Открывает HTTP-ответ как ограниченный поток. @internal */
1024
- async function openUrlFile(target, options, context) {
1025
- const resolved = resolveFileStreamOptions(options, DEFAULT_URL_FILE_MAX_BYTES);
1026
- const { response, url, size } = await fetchFile(target, options, context);
1027
- if (!response.body) throw new ItdFileError(`источник ${url.href} не предоставил потоковое тело`, {
1028
- reason: ItdFileErrorReason.StreamUnavailable,
1029
- url: url.href
1030
- });
1031
- const stream = boundedFileStream(response.body, {
1032
- ...resolved.maxBytes !== void 0 ? { maxBytes: resolved.maxBytes } : {},
1033
- streamBufferBytes: resolved.streamBufferBytes,
1034
- ...context.signal ? { signal: context.signal } : {},
1035
- url: url.href,
1036
- retryableRead: true
1037
- });
1038
- const filename = options.filename ?? filenameFromUrl(url);
1039
- const contentType = normalizeMimeType(options.contentType ?? response.headers.get("content-type") ?? void 0);
1040
- return {
1041
- stream,
1042
- ...filename ? { filename } : {},
1043
- ...contentType ? { contentType } : {},
1044
- ...size !== void 0 ? { size } : {},
1045
- close: () => stream.cancel().catch(() => {})
1046
- };
1047
- }
1544
+ /** Восстанавливает удалённый комментарий. */
1545
+ restore(commentId, options = {}) {
1546
+ return this.http.execute(COMMENTS_RESTORE, {
1547
+ path: `/api/comments/${encodePathSegment(commentId, "commentId")}/restore`,
1548
+ ...options
1549
+ });
1550
+ }
1551
+ /** Ставит реакцию на комментарий. */
1552
+ like(commentId, options = {}) {
1553
+ return this.http.execute(COMMENTS_LIKE, {
1554
+ path: `/api/comments/${encodePathSegment(commentId, "commentId")}/like`,
1555
+ ...options
1556
+ });
1557
+ }
1558
+ /** Убирает реакцию с комментария. */
1559
+ unlike(commentId, options = {}) {
1560
+ return this.http.execute(COMMENTS_UNLIKE, {
1561
+ path: `/api/comments/${encodePathSegment(commentId, "commentId")}/like`,
1562
+ ...options
1563
+ });
1564
+ }
1565
+ };
1048
1566
  //#endregion
1049
1567
  //#region src/core/multipart.ts
1050
1568
  const encoder = new TextEncoder();
@@ -1182,12 +1700,17 @@ function assertAllowedMime(mimeType, filename) {
1182
1700
  if (!mimeType) throw new ItdConfigError(`Не удалось определить тип файла${filename ? ` «${filename}»` : ""}. Укажите его явно: { file, filename, contentType }.`);
1183
1701
  if (!isAllowedMime(mimeType)) throw new ItdConfigError(`Тип «${mimeType}» не поддерживается загрузкой. Допустимые: ${ALLOWED_MIME_TYPES.join(", ")}.`);
1184
1702
  }
1703
+ //#endregion
1704
+ //#region src/resources/files.ts
1705
+ const FILES_UPLOAD = passthroughOperation("files.upload");
1706
+ const FILES_GET = passthroughOperation("files.get");
1707
+ const FILES_REMOVE = voidOperation("files.remove");
1185
1708
  /** Файлы и медиа. */
1186
1709
  var FilesResource = class extends BaseResource {
1187
- #fetch;
1710
+ #files;
1188
1711
  constructor(http, deps) {
1189
1712
  super(http);
1190
- this.#fetch = deps.fetch;
1713
+ this.#files = deps.files;
1191
1714
  }
1192
1715
  /**
1193
1716
  * Загружает файл и возвращает его идентификатор.
@@ -1197,7 +1720,7 @@ var FilesResource = class extends BaseResource {
1197
1720
  */
1198
1721
  upload(input, uploadOptions = {}, requestOptions = {}) {
1199
1722
  const bodyFactory = this.#createBodyFactory(input, uploadOptions);
1200
- return this.http.operation("files.upload", {
1723
+ return this.http.execute(FILES_UPLOAD, {
1201
1724
  path: "/api/files/upload",
1202
1725
  bodyFactory,
1203
1726
  ...requestOptions,
@@ -1216,14 +1739,14 @@ var FilesResource = class extends BaseResource {
1216
1739
  * Для ещё не прикреплённого файла сервер может ответить `404`.
1217
1740
  */
1218
1741
  get(fileId, options = {}) {
1219
- return this.http.operation("files.get", {
1742
+ return this.http.execute(FILES_GET, {
1220
1743
  path: `/api/files/${encodePathSegment(fileId, "fileId")}`,
1221
1744
  ...options
1222
1745
  });
1223
1746
  }
1224
1747
  /** Удаляет загруженный файл. */
1225
1748
  remove(fileId, options = {}) {
1226
- return this.http.operation("files.remove", {
1749
+ return this.voidOperation(FILES_REMOVE, {
1227
1750
  path: `/api/files/${encodePathSegment(fileId, "fileId")}`,
1228
1751
  ...options
1229
1752
  });
@@ -1233,140 +1756,70 @@ var FilesResource = class extends BaseResource {
1233
1756
  * заново, поэтому каждая транспортная попытка получает непрочитанное тело.
1234
1757
  */
1235
1758
  #createBodyFactory(input, options) {
1236
- const streamMode = this.#isStreamInput(input);
1759
+ const streamMode = this.#files.mode(input) === FileTransferMode.Stream;
1237
1760
  let buffered;
1238
1761
  return async ({ signal, attempt }) => {
1239
- const context = {
1240
- fetch: this.#fetch,
1762
+ let prepared;
1763
+ if (streamMode) prepared = await this.#files.resolve(input, options, {
1241
1764
  signal,
1242
1765
  attempt
1243
- };
1244
- let prepared;
1245
- try {
1246
- if (streamMode) prepared = await this.#prepareStream(input, options, context);
1247
- else {
1248
- buffered ??= this.#prepareBuffer(input, options, context).catch((error) => {
1249
- buffered = void 0;
1250
- throw error;
1251
- });
1252
- prepared = await buffered;
1253
- }
1254
- } catch (error) {
1255
- if (error instanceof ItdError || signal.aborted) throw error;
1256
- throw new ItdFileError("не удалось получить содержимое вложения", {
1257
- reason: ItdFileErrorReason.Read,
1258
- retryable: true,
1259
- cause: error
1766
+ });
1767
+ else {
1768
+ buffered ??= this.#files.resolve(input, options, {
1769
+ signal,
1770
+ attempt
1771
+ }).then((source) => source).catch((error) => {
1772
+ buffered = void 0;
1773
+ throw error;
1260
1774
  });
1775
+ prepared = await buffered;
1261
1776
  }
1262
- if (prepared.mode === "buffer") {
1777
+ if (prepared.mode === FileTransferMode.Buffer) {
1778
+ const filename = this.#filename(prepared, options);
1779
+ const contentType = prepared.contentType ?? mimeFromFilename(filename);
1780
+ if (options.validateMime !== false) assertAllowedMime(contentType, filename);
1781
+ const blob = !contentType || prepared.blob.type === contentType ? prepared.blob : new Blob([prepared.blob], { type: contentType });
1263
1782
  const form = new FormData();
1264
- form.set("file", prepared.blob, prepared.filename);
1783
+ form.set("file", blob, filename);
1265
1784
  return { body: form };
1266
1785
  }
1267
- const multipart = createMultipartFileBody(prepared);
1268
- return {
1269
- body: multipart.body,
1270
- headers: { "Content-Type": multipart.contentType },
1271
- cleanup: async () => {
1272
- await multipart.cancel();
1273
- await prepared.close?.();
1274
- }
1275
- };
1276
- };
1277
- }
1278
- /** Определяет режим без чтения источника. */
1279
- #isStreamInput(input) {
1280
- if (typeof input !== "object" || input === null) return false;
1281
- if ("open" in input) return true;
1282
- if ("url" in input) return resolveFileStreamOptions(input).mode === FileTransferMode.Stream;
1283
- return false;
1284
- }
1285
- /** Получает и проверяет буферный источник. */
1286
- async #prepareBuffer(input, options, context) {
1287
- const content = await this.#resolveBuffer(input, context);
1288
- const filename = options.filename ?? content.filename ?? this.#nameFromMime(options.contentType ?? content.contentType);
1289
- const contentType = normalizeMimeType(options.contentType ?? content.contentType) ?? normalizeMimeType(isBlob(content.file) ? content.file.type : void 0) ?? mimeFromFilename(filename);
1290
- if (options.validateMime !== false) assertAllowedMime(contentType, filename);
1291
- const blob = isBlob(content.file) && (!contentType || content.file.type === contentType) ? content.file : new Blob([content.file], { type: contentType ?? "" });
1292
- const limits = resolveFileStreamOptions(options);
1293
- if (limits.maxBytes !== void 0 && blob.size > limits.maxBytes) throw new ItdFileError(`файл больше предела в ${limits.maxBytes} байт: ${blob.size}`, {
1294
- reason: ItdFileErrorReason.TooLarge,
1295
- limit: limits.maxBytes,
1296
- actual: blob.size
1297
- });
1298
- return {
1299
- mode: "buffer",
1300
- blob,
1301
- filename
1302
- };
1303
- }
1304
- /** Открывает и проверяет потоковый источник. */
1305
- async #prepareStream(input, options, context) {
1306
- let opened;
1307
- if (typeof input === "object" && input !== null && "open" in input) opened = await input.open(context);
1308
- else if (typeof input === "object" && input !== null && "url" in input) {
1309
- const { url, ...urlOptions } = input;
1310
- opened = await openUrlFile(url, urlOptions, context);
1311
- } else throw new ItdConfigError("потоковое вложение должно иметь форму { open } или { url, mode }");
1312
- if (!opened || typeof ReadableStream === "undefined" || !(opened.stream instanceof ReadableStream)) {
1313
- await opened?.close?.();
1314
- throw new ItdConfigError("потоковый источник должен вернуть { stream: ReadableStream }");
1315
- }
1316
- try {
1317
- const filename = options.filename ?? opened.filename ?? this.#nameFromMime(options.contentType ?? opened.contentType);
1318
- const contentType = normalizeMimeType(options.contentType ?? opened.contentType) ?? mimeFromFilename(filename);
1319
- if (options.validateMime !== false) assertAllowedMime(contentType, filename);
1320
- const limits = resolveFileStreamOptions({
1321
- mode: FileTransferMode.Stream,
1322
- ...options.maxBytes !== void 0 ? { maxBytes: options.maxBytes } : {},
1323
- ...options.streamBufferBytes !== void 0 ? { streamBufferBytes: options.streamBufferBytes } : {}
1324
- });
1325
- if (limits.maxBytes !== void 0 && opened.size !== void 0 && opened.size > limits.maxBytes) throw new ItdFileError(`файл больше предела в ${limits.maxBytes} байт: ${opened.size}`, {
1326
- reason: ItdFileErrorReason.TooLarge,
1327
- limit: limits.maxBytes,
1328
- actual: opened.size
1329
- });
1330
- return {
1331
- mode: "stream",
1332
- stream: isBoundedFileStream(opened.stream) && options.maxBytes === void 0 && options.streamBufferBytes === void 0 ? opened.stream : boundedFileStream(opened.stream, {
1333
- ...limits.maxBytes !== void 0 ? { maxBytes: limits.maxBytes } : {},
1334
- streamBufferBytes: limits.streamBufferBytes,
1335
- signal: context.signal
1336
- }),
1337
- filename,
1338
- contentType: contentType ?? "",
1339
- ...opened.close ? { close: opened.close } : {}
1340
- };
1341
- } catch (error) {
1342
- await opened.close?.();
1343
- throw error;
1344
- }
1345
- }
1346
- /** Разрешает только буферные формы. */
1347
- #resolveBuffer(input, context) {
1348
- if (input instanceof ArrayBuffer || ArrayBuffer.isView(input) || isBlob(input)) return {
1349
- file: input,
1350
- ...isFile(input) ? { filename: input.name } : {}
1786
+ let multipart;
1787
+ try {
1788
+ const filename = this.#filename(prepared, options);
1789
+ const contentType = prepared.contentType ?? mimeFromFilename(filename);
1790
+ if (options.validateMime !== false) assertAllowedMime(contentType, filename);
1791
+ multipart = createMultipartFileBody({
1792
+ stream: prepared.stream,
1793
+ filename,
1794
+ contentType: contentType ?? ""
1795
+ });
1796
+ return {
1797
+ body: multipart.body,
1798
+ headers: { "Content-Type": multipart.contentType },
1799
+ cleanup: async () => {
1800
+ await multipart?.cancel();
1801
+ await prepared.close();
1802
+ }
1803
+ };
1804
+ } catch (error) {
1805
+ await prepared.close();
1806
+ throw error;
1807
+ }
1351
1808
  };
1352
- if (typeof input !== "object" || input === null) throw new ItdConfigError(`вложение задано значением типа ${typeof input}; ожидается бинарное значение или объект { file }, { url }, { load }, { open }. Для файла на диске используйте fromPath('./photo.jpg') из itd-api/node`);
1353
- if ("load" in input) return input.load(context);
1354
- if ("url" in input) {
1355
- const { url, ...urlOptions } = input;
1356
- return downloadFile(url, urlOptions, context);
1357
- }
1358
- if ("file" in input) return input;
1359
- if ("open" in input) throw new ItdConfigError("потоковый источник нельзя использовать как буферный");
1360
- throw new ItdConfigError("вложение не распознано: ожидается { file }, { url }, { load } или { open }");
1361
1809
  }
1362
1810
  /** Подбирает непустое имя, обязательное для multipart. */
1363
- #nameFromMime(contentType) {
1364
- const extension = normalizeMimeType(contentType)?.split("/")[1];
1811
+ #filename(source, options) {
1812
+ if (options.filename) return options.filename;
1813
+ if (source.filename) return source.filename;
1814
+ const extension = options.contentType?.split(";", 1)[0]?.trim().split("/")[1];
1365
1815
  return extension ? `file.${extension}` : "file";
1366
1816
  }
1367
1817
  };
1368
1818
  //#endregion
1369
1819
  //#region src/resources/hashtags.ts
1820
+ const HASHTAG_POSTS = pageOperation("hashtags.posts", (body) => readCursorPage(body, "posts"));
1821
+ const HASHTAGS_SEARCH = defineBuiltInOperation("hashtags.search", (body) => pickArray(body, "hashtags"));
1822
+ const HASHTAGS_TRENDING = defineBuiltInOperation("hashtags.trending", (body) => pickArray(body, "hashtags"));
1370
1823
  /**
1371
1824
  * Хэштеги.
1372
1825
  *
@@ -1375,11 +1828,10 @@ var FilesResource = class extends BaseResource {
1375
1828
  var HashtagsResource = class extends BaseResource {
1376
1829
  /** Посты по хэштегу: `/api/hashtags/{tag}/posts`, курсорная пагинация. */
1377
1830
  #posts = this.paginated({
1378
- operationId: "hashtags.posts",
1831
+ operation: HASHTAG_POSTS,
1379
1832
  path: (p) => `/api/hashtags/${encodePathSegment(p.tag, "tag")}/posts`,
1380
1833
  query: (p) => ({ limit: p.limit }),
1381
1834
  start: (p) => p.cursor ? { cursor: p.cursor } : {},
1382
- read: (body) => readCursorPage(body, "posts"),
1383
1835
  mode: PaginationMode.Cursor
1384
1836
  });
1385
1837
  /**
@@ -1387,8 +1839,8 @@ var HashtagsResource = class extends BaseResource {
1387
1839
  *
1388
1840
  * Без строки запроса возвращает общий список.
1389
1841
  */
1390
- async search(query, params = {}, options = {}) {
1391
- const body = await this.http.operation("hashtags.search", {
1842
+ search(query, params = {}, options = {}) {
1843
+ return this.http.execute(HASHTAGS_SEARCH, {
1392
1844
  path: "/api/hashtags",
1393
1845
  query: {
1394
1846
  q: query,
@@ -1396,16 +1848,14 @@ var HashtagsResource = class extends BaseResource {
1396
1848
  },
1397
1849
  ...options
1398
1850
  });
1399
- return pickArray(body, "hashtags");
1400
1851
  }
1401
1852
  /** Загружает трендовые хэштеги. */
1402
- async trending(params = {}, options = {}) {
1403
- const body = await this.http.operation("hashtags.trending", {
1853
+ trending(params = {}, options = {}) {
1854
+ return this.http.execute(HASHTAGS_TRENDING, {
1404
1855
  path: "/api/hashtags/trending",
1405
1856
  query: { limit: params.limit },
1406
1857
  ...options
1407
1858
  });
1408
- return pickArray(body, "hashtags");
1409
1859
  }
1410
1860
  /**
1411
1861
  * Загружает страницу постов по хэштегу.
@@ -1428,160 +1878,11 @@ var HashtagsResource = class extends BaseResource {
1428
1878
  }
1429
1879
  };
1430
1880
  //#endregion
1431
- //#region src/resources/notifications.ts
1432
- const NOTIFICATION_SETTING_KEYS = [
1433
- "enabled",
1434
- "sound",
1435
- "follows",
1436
- "wallPosts",
1437
- "likes",
1438
- "comments",
1439
- "mentions"
1440
- ];
1441
- /**
1442
- * Сколько идентификаторов уходит в одном запросе на отметку прочтения.
1443
- *
1444
- * Столько же отправляет сайт итд.com — значит на сервере, скорее всего, есть ограничение.
1445
- */
1446
- const READ_BATCH_SIZE = 20;
1447
- /**
1448
- * Читает настройки уведомлений.
1449
- *
1450
- * Сервер отдаёт плоский объект: `enabled`, `sound`, `follows`, `wallPosts`, `likes`,
1451
- * `comments`, `mentions`. Отсутствующая настройка считается включённой — так же
1452
- * ведёт себя сайт итд.com.
1453
- */
1454
- function readSettings(body) {
1455
- const settings = {};
1456
- for (const key of NOTIFICATION_SETTING_KEYS) settings[key] = pickBoolean(body, key, true);
1457
- return settings;
1458
- }
1459
- /**
1460
- * Уведомления: список, счётчик, отметки о прочтении, настройки.
1461
- *
1462
- * Доступна как `itd.notifications`. Все уведомления приведены к единой форме, поэтому
1463
- * объекты отсюда и из потока событий можно складывать в один список.
1464
- */
1465
- var NotificationsResource = class extends BaseResource {
1466
- /** Уведомления: `/api/notifications/`, пагинация по смещению. */
1467
- #list = this.paginated({
1468
- operationId: "notifications.list",
1469
- path: () => "/api/notifications/",
1470
- query: (p) => ({ limit: p.limit }),
1471
- start: (p) => ({ offset: p.offset ?? 0 }),
1472
- read: (body, state) => {
1473
- const page = readOffsetPage(body, "notifications", state.offset ?? 0);
1474
- return {
1475
- ...page,
1476
- items: page.items.map(normalizeNotification)
1477
- };
1478
- },
1479
- mode: PaginationMode.Offset
1480
- });
1481
- /**
1482
- * Загружает страницу уведомлений.
1483
- *
1484
- * Пагинация здесь основана на смещении.
1485
- *
1486
- * @example
1487
- * ```ts
1488
- * const page = await itd.notifications.list({ limit: 20 });
1489
- * const next = await itd.notifications.list({ limit: 20, offset: page.nextOffset });
1490
- * ```
1491
- */
1492
- list(params = {}, options = {}) {
1493
- return this.#list.list(params, options);
1494
- }
1495
- /**
1496
- * Перебирает уведомления.
1497
- *
1498
- * @example
1499
- * ```ts
1500
- * for await (const notification of itd.notifications.iterate()) {
1501
- * console.log(formatNotificationText(notification));
1502
- * }
1503
- * ```
1504
- */
1505
- iterate(params = {}, options = {}) {
1506
- return this.#list.iterate(params, options);
1507
- }
1508
- /** Загружает число непрочитанных уведомлений. */
1509
- async count(options = {}) {
1510
- const body = await this.http.operation("notifications.count", {
1511
- path: "/api/notifications/count",
1512
- ...options
1513
- });
1514
- return pickNumber(body, "count", 0);
1515
- }
1516
- /**
1517
- * Отмечает уведомление прочитанным.
1518
- *
1519
- * @returns сколько записей отметил сервер
1520
- */
1521
- async markRead(notificationId, options = {}) {
1522
- const body = await this.http.operation("notifications.markRead", {
1523
- path: `/api/notifications/${encodePathSegment(notificationId, "notificationId")}/read`,
1524
- ...options
1525
- });
1526
- return pickNumber(body, "markedCount", 0);
1527
- }
1528
- /**
1529
- * Отмечает прочитанными сразу несколько уведомлений.
1530
- *
1531
- * Список автоматически режется на части по 20 идентификаторов — столько же отправляет
1532
- * сайт итд.com, поэтому на сервере вероятен предел. Части уходят последовательно,
1533
- * результат суммируется.
1534
- *
1535
- * @returns сколько записей отметил сервер суммарно
1536
- */
1537
- async markReadBatch(ids, options = {}) {
1538
- let marked = 0;
1539
- for (let index = 0; index < ids.length; index += READ_BATCH_SIZE) {
1540
- const chunk = ids.slice(index, index + READ_BATCH_SIZE);
1541
- const body = await this.http.operation("notifications.markReadBatch", {
1542
- path: "/api/notifications/read-batch",
1543
- body: { ids: chunk },
1544
- ...options
1545
- });
1546
- marked += pickNumber(body, "markedCount", 0);
1547
- }
1548
- return marked;
1549
- }
1550
- /** Отмечает прочитанными все уведомления. */
1551
- async markAllRead(options = {}) {
1552
- const body = await this.http.operation("notifications.markAllRead", {
1553
- path: "/api/notifications/read-all",
1554
- ...options
1555
- });
1556
- return pickNumber(body, "markedCount", 0);
1557
- }
1558
- /** Загружает настройки уведомлений. */
1559
- async getSettings(options = {}) {
1560
- return readSettings(await this.http.operation("notifications.getSettings", {
1561
- path: "/api/notifications/settings",
1562
- ...options
1563
- }));
1564
- }
1565
- /**
1566
- * Обновляет настройки уведомлений.
1567
- *
1568
- * Отправляются только изменяемые поля, в том же виде, в каком сервер их возвращает.
1569
- */
1570
- async updateSettings(input, options = {}) {
1571
- const payload = {};
1572
- for (const key of NOTIFICATION_SETTING_KEYS) {
1573
- const value = input[key];
1574
- if (value !== void 0) payload[key] = value;
1575
- }
1576
- return readSettings(await this.http.operation("notifications.updateSettings", {
1577
- path: "/api/notifications/settings",
1578
- body: payload,
1579
- ...options
1580
- }));
1581
- }
1582
- };
1583
- //#endregion
1584
1881
  //#region src/resources/platform.ts
1882
+ const PLATFORM_CHANGELOG = defineBuiltInOperation("platform.changelog", (body) => Array.isArray(body) ? body : []);
1883
+ const PLATFORM_ANNOUNCEMENTS = defineBuiltInOperation("platform.announcements", (body) => pickArray(body, "announcements"));
1884
+ const PLATFORM_VERSION = passthroughOperation("platform.version");
1885
+ const PLATFORM_PORTAL = passthroughOperation("platform.portal");
1585
1886
  /**
1586
1887
  * Сведения о платформе: версии приложений, изменения, анонсы, баннер события.
1587
1888
  *
@@ -1606,31 +1907,29 @@ var PlatformResource = class extends BaseResource {
1606
1907
  * ```
1607
1908
  */
1608
1909
  version(options = {}) {
1609
- return this.http.operation("platform.version", {
1910
+ return this.http.execute(PLATFORM_VERSION, {
1610
1911
  path: "/api/platform/version",
1611
1912
  skipAuth: true,
1612
1913
  ...options
1613
1914
  });
1614
1915
  }
1615
1916
  /** Загружает журнал изменений. */
1616
- async changelog(options = {}) {
1617
- const body = await this.http.operation("platform.changelog", {
1917
+ changelog(options = {}) {
1918
+ return this.http.execute(PLATFORM_CHANGELOG, {
1618
1919
  path: "/api/platform/changelog",
1619
1920
  ...options
1620
1921
  });
1621
- return Array.isArray(body) ? body : [];
1622
1922
  }
1623
1923
  /** Загружает анонсы платформы. */
1624
- async announcements(options = {}) {
1625
- const body = await this.http.operation("platform.announcements", {
1924
+ announcements(options = {}) {
1925
+ return this.http.execute(PLATFORM_ANNOUNCEMENTS, {
1626
1926
  path: "/api/platform/announcements",
1627
1927
  ...options
1628
1928
  });
1629
- return pickArray(body, "announcements");
1630
1929
  }
1631
1930
  /** Загружает баннер текущего события — виджет «портал». */
1632
1931
  portal(options = {}) {
1633
- return this.http.operation("platform.portal", {
1932
+ return this.http.execute(PLATFORM_PORTAL, {
1634
1933
  path: "/api/v1/portal",
1635
1934
  ...options
1636
1935
  });
@@ -2347,7 +2646,7 @@ function autoSpans(text, options = {}) {
2347
2646
  });
2348
2647
  }
2349
2648
  if (includeLinks) spans.push(...links);
2350
- if (includeHashtags || includeMentions) for (const match of text.matchAll(/#([\p{L}\p{M}\p{N}_]+)|@([a-z\d][a-z\d._-]*)/giu)) {
2649
+ if (includeHashtags || includeMentions) for (const match of text.matchAll(/#([\p{L}\p{M}\p{N}_]+)|@([a-z\d](?:[a-z\d_]*[a-z\d])?)/giu)) {
2351
2650
  if (match.index === void 0) continue;
2352
2651
  const offset = match.index;
2353
2652
  const previous = previousCodePoint(text, offset);
@@ -2355,7 +2654,7 @@ function autoSpans(text, options = {}) {
2355
2654
  const end = offset + match[0].length;
2356
2655
  if (links.some((span) => overlaps(span, offset, end))) continue;
2357
2656
  const hashtag = match[1];
2358
- const username = match[2]?.replace(/[._-]+$/u, "");
2657
+ const username = match[2];
2359
2658
  if (hashtag !== void 0 && includeHashtags) spans.push({
2360
2659
  type: SpanType.Hashtag,
2361
2660
  offset,
@@ -2748,6 +3047,24 @@ function resolvePostUpdate(input) {
2748
3047
  }
2749
3048
  //#endregion
2750
3049
  //#region src/resources/posts.ts
3050
+ const POSTS_LIST = pageOperation("posts.list", (body) => readCursorPage(body, "posts"));
3051
+ const POSTS_BY_USER = pageOperation("posts.byUser", (body) => readCursorPage(body, "posts"));
3052
+ const POSTS_LIKED_BY_USER = pageOperation("posts.likedByUser", (body) => readCursorPage(body, "posts"));
3053
+ const POSTS_COMMENTS = pageOperation("posts.comments", (body) => readFlatCursorPage(body, "comments"));
3054
+ const POSTS_STATS = defineBuiltInOperation("posts.stats", (body) => pickArray(body, "posts"));
3055
+ const POSTS_CREATE = passthroughOperation("posts.create");
3056
+ const POSTS_GET = passthroughOperation("posts.get");
3057
+ const POSTS_UPDATE = passthroughOperation("posts.update");
3058
+ const POSTS_RESTORE = passthroughOperation("posts.restore");
3059
+ const POSTS_LIKE = passthroughOperation("posts.like");
3060
+ const POSTS_UNLIKE = passthroughOperation("posts.unlike");
3061
+ const POSTS_REPOST = passthroughOperation("posts.repost");
3062
+ const POSTS_PIN = passthroughOperation("posts.pin");
3063
+ const POSTS_UNPIN = passthroughOperation("posts.unpin");
3064
+ const POSTS_VOTE = passthroughOperation("posts.vote");
3065
+ const POSTS_COMMENT = passthroughOperation("posts.comment");
3066
+ const POSTS_REMOVE = voidOperation("posts.remove");
3067
+ const POSTS_UNREPOST = voidOperation("posts.unrepost");
2751
3068
  /** Курсорная позиция из параметров: если курсор задан — с него, иначе с начала. */
2752
3069
  function cursorStart(params) {
2753
3070
  return params.cursor ? { cursor: params.cursor } : {};
@@ -2761,19 +3078,18 @@ var PostsResource = class extends BaseResource {
2761
3078
  #uploadFiles;
2762
3079
  /** Лента: `/api/posts`, курсорная пагинация. */
2763
3080
  #feed = this.paginated({
2764
- operationId: "posts.list",
3081
+ operation: POSTS_LIST,
2765
3082
  path: () => "/api/posts",
2766
3083
  query: (p) => ({
2767
3084
  tab: p.tab,
2768
3085
  limit: p.limit
2769
3086
  }),
2770
3087
  start: cursorStart,
2771
- read: (body) => readCursorPage(body, "posts"),
2772
3088
  mode: PaginationMode.Cursor
2773
3089
  });
2774
3090
  /** Стена пользователя: `/api/posts/user/{user}`. */
2775
3091
  #wall = this.paginated({
2776
- operationId: "posts.byUser",
3092
+ operation: POSTS_BY_USER,
2777
3093
  path: (p) => `/api/posts/user/${encodePathSegment(p.user, "user")}`,
2778
3094
  query: (p) => ({
2779
3095
  limit: p.limit,
@@ -2781,28 +3097,25 @@ var PostsResource = class extends BaseResource {
2781
3097
  pinnedPostId: p.pinnedPostId
2782
3098
  }),
2783
3099
  start: cursorStart,
2784
- read: (body) => readCursorPage(body, "posts"),
2785
3100
  mode: PaginationMode.Cursor
2786
3101
  });
2787
3102
  /** Понравившиеся посты пользователя: `/api/posts/user/{user}/liked`. */
2788
3103
  #liked = this.paginated({
2789
- operationId: "posts.likedByUser",
3104
+ operation: POSTS_LIKED_BY_USER,
2790
3105
  path: (p) => `/api/posts/user/${encodePathSegment(p.user, "user")}/liked`,
2791
3106
  query: (p) => ({ limit: p.limit }),
2792
3107
  start: cursorStart,
2793
- read: (body) => readCursorPage(body, "posts"),
2794
3108
  mode: PaginationMode.Cursor
2795
3109
  });
2796
3110
  /** Комментарии к посту: курсор лежит рядом со списком, поэтому свой reader. */
2797
3111
  #comments = this.paginated({
2798
- operationId: "posts.comments",
3112
+ operation: POSTS_COMMENTS,
2799
3113
  path: (p) => `/api/posts/${encodePathSegment(p.postId, "postId")}/comments`,
2800
3114
  query: (p) => ({
2801
3115
  limit: p.limit,
2802
3116
  sort: p.sort
2803
3117
  }),
2804
3118
  start: cursorStart,
2805
- read: (body) => readFlatCursorPage(body, "comments"),
2806
3119
  mode: PaginationMode.Cursor
2807
3120
  });
2808
3121
  constructor(http, deps) {
@@ -2849,7 +3162,7 @@ var PostsResource = class extends BaseResource {
2849
3162
  async create(input, options = {}) {
2850
3163
  const data = resolvePost(input);
2851
3164
  const attachmentIds = await this.#collectAttachments(data, options);
2852
- return this.http.operation("posts.create", {
3165
+ return this.http.execute(POSTS_CREATE, {
2853
3166
  path: "/api/posts",
2854
3167
  body: {
2855
3168
  content: data.content ?? "",
@@ -2867,7 +3180,7 @@ var PostsResource = class extends BaseResource {
2867
3180
  * В отличие от списков, здесь у поста заполнено поле `comments`.
2868
3181
  */
2869
3182
  get(postId, options = {}) {
2870
- return this.http.operation("posts.get", {
3183
+ return this.http.execute(POSTS_GET, {
2871
3184
  path: `/api/posts/${encodePathSegment(postId, "postId")}`,
2872
3185
  ...options
2873
3186
  });
@@ -2881,7 +3194,7 @@ var PostsResource = class extends BaseResource {
2881
3194
  */
2882
3195
  update(postId, input, options = {}) {
2883
3196
  const data = resolvePostUpdate(input);
2884
- return this.http.operation("posts.update", {
3197
+ return this.http.execute(POSTS_UPDATE, {
2885
3198
  path: `/api/posts/${encodePathSegment(postId, "postId")}`,
2886
3199
  body: {
2887
3200
  content: data.content,
@@ -2892,28 +3205,28 @@ var PostsResource = class extends BaseResource {
2892
3205
  }
2893
3206
  /** Удаляет пост. Восстановить его можно через {@link restore}. */
2894
3207
  remove(postId, options = {}) {
2895
- return this.http.operation("posts.remove", {
3208
+ return this.voidOperation(POSTS_REMOVE, {
2896
3209
  path: `/api/posts/${encodePathSegment(postId, "postId")}`,
2897
3210
  ...options
2898
3211
  });
2899
3212
  }
2900
3213
  /** Восстанавливает удалённый пост. */
2901
3214
  restore(postId, options = {}) {
2902
- return this.http.operation("posts.restore", {
3215
+ return this.http.execute(POSTS_RESTORE, {
2903
3216
  path: `/api/posts/${encodePathSegment(postId, "postId")}/restore`,
2904
3217
  ...options
2905
3218
  });
2906
3219
  }
2907
3220
  /** Ставит реакцию на пост. */
2908
3221
  like(postId, options = {}) {
2909
- return this.http.operation("posts.like", {
3222
+ return this.http.execute(POSTS_LIKE, {
2910
3223
  path: `/api/posts/${encodePathSegment(postId, "postId")}/like`,
2911
3224
  ...options
2912
3225
  });
2913
3226
  }
2914
3227
  /** Убирает реакцию с поста. */
2915
3228
  unlike(postId, options = {}) {
2916
- return this.http.operation("posts.unlike", {
3229
+ return this.http.execute(POSTS_UNLIKE, {
2917
3230
  path: `/api/posts/${encodePathSegment(postId, "postId")}/like`,
2918
3231
  ...options
2919
3232
  });
@@ -2925,7 +3238,7 @@ var PostsResource = class extends BaseResource {
2925
3238
  * для файлов здесь нет.
2926
3239
  */
2927
3240
  repost(postId, content = "", options = {}) {
2928
- return this.http.operation("posts.repost", {
3241
+ return this.http.execute(POSTS_REPOST, {
2929
3242
  path: `/api/posts/${encodePathSegment(postId, "postId")}/repost`,
2930
3243
  body: { content },
2931
3244
  ...options
@@ -2933,21 +3246,21 @@ var PostsResource = class extends BaseResource {
2933
3246
  }
2934
3247
  /** Отменяет репост. */
2935
3248
  unrepost(postId, options = {}) {
2936
- return this.http.operation("posts.unrepost", {
3249
+ return this.voidOperation(POSTS_UNREPOST, {
2937
3250
  path: `/api/posts/${encodePathSegment(postId, "postId")}/repost`,
2938
3251
  ...options
2939
3252
  });
2940
3253
  }
2941
3254
  /** Закрепляет пост в профиле. */
2942
3255
  pin(postId, options = {}) {
2943
- return this.http.operation("posts.pin", {
3256
+ return this.http.execute(POSTS_PIN, {
2944
3257
  path: `/api/posts/${encodePathSegment(postId, "postId")}/pin`,
2945
3258
  ...options
2946
3259
  });
2947
3260
  }
2948
3261
  /** Открепляет пост. */
2949
3262
  unpin(postId, options = {}) {
2950
- return this.http.operation("posts.unpin", {
3263
+ return this.http.execute(POSTS_UNPIN, {
2951
3264
  path: `/api/posts/${encodePathSegment(postId, "postId")}/pin`,
2952
3265
  ...options
2953
3266
  });
@@ -2958,20 +3271,19 @@ var PostsResource = class extends BaseResource {
2958
3271
  * @param optionIds выбранные варианты; несколько допустимы только при `multipleChoice`
2959
3272
  */
2960
3273
  vote(postId, optionIds, options = {}) {
2961
- return this.http.operation("posts.vote", {
3274
+ return this.http.execute(POSTS_VOTE, {
2962
3275
  path: `/api/posts/${encodePathSegment(postId, "postId")}/poll/vote`,
2963
3276
  body: { optionIds },
2964
3277
  ...options
2965
3278
  });
2966
3279
  }
2967
3280
  /** Запрашивает счётчики сразу для нескольких постов. */
2968
- async stats(ids, options = {}) {
2969
- const body = await this.http.operation("posts.stats", {
3281
+ stats(ids, options = {}) {
3282
+ return this.http.execute(POSTS_STATS, {
2970
3283
  path: "/api/posts/stats",
2971
3284
  body: { ids },
2972
3285
  ...options
2973
3286
  });
2974
- return pickArray(body, "posts");
2975
3287
  }
2976
3288
  /**
2977
3289
  * Загружает страницу стены пользователя.
@@ -3041,7 +3353,7 @@ var PostsResource = class extends BaseResource {
3041
3353
  async comment(postId, input, options = {}) {
3042
3354
  const data = resolveComment(typeof input === "string" ? { content: input } : input);
3043
3355
  const attachmentIds = await this.#collectAttachments(data, options);
3044
- return this.http.operation("posts.comment", {
3356
+ return this.http.execute(POSTS_COMMENT, {
3045
3357
  path: `/api/posts/${encodePathSegment(postId, "postId")}/comments`,
3046
3358
  body: {
3047
3359
  content: data.content ?? "",
@@ -3162,6 +3474,7 @@ function resolveReport(input) {
3162
3474
  }
3163
3475
  //#endregion
3164
3476
  //#region src/resources/reports.ts
3477
+ const REPORTS_CREATE = passthroughOperation("reports.create");
3165
3478
  /**
3166
3479
  * Жалобы на контент и пользователей.
3167
3480
  *
@@ -3182,7 +3495,7 @@ var ReportsResource = class extends BaseResource {
3182
3495
  */
3183
3496
  create(input, options = {}) {
3184
3497
  const data = resolveReport(input);
3185
- return this.http.operation("reports.create", {
3498
+ return this.http.execute(REPORTS_CREATE, {
3186
3499
  path: "/api/reports",
3187
3500
  body: data,
3188
3501
  ...options
@@ -3191,6 +3504,10 @@ var ReportsResource = class extends BaseResource {
3191
3504
  };
3192
3505
  //#endregion
3193
3506
  //#region src/resources/search.ts
3507
+ const SEARCH_ALL = defineBuiltInOperation("search.all", (body) => ({
3508
+ users: pickArray(body, "users"),
3509
+ hashtags: pickArray(body, "hashtags")
3510
+ }));
3194
3511
  /**
3195
3512
  * Глобальный поиск.
3196
3513
  *
@@ -3205,20 +3522,23 @@ var SearchResource = class extends BaseResource {
3205
3522
  * const { users, hashtags } = await itd.search.all('арт');
3206
3523
  * ```
3207
3524
  */
3208
- async all(query, options = {}) {
3209
- const body = await this.http.operation("search.all", {
3525
+ all(query, options = {}) {
3526
+ return this.http.execute(SEARCH_ALL, {
3210
3527
  path: "/api/search",
3211
3528
  query: { q: query },
3212
3529
  ...options
3213
3530
  });
3214
- return {
3215
- users: pickArray(body, "users"),
3216
- hashtags: pickArray(body, "hashtags")
3217
- };
3218
3531
  }
3219
3532
  };
3220
3533
  //#endregion
3221
3534
  //#region src/resources/subscription.ts
3535
+ const SUBSCRIPTION_METHODS = defineBuiltInOperation("subscription.methods", (body) => Array.isArray(body) ? body : []);
3536
+ const SUBSCRIPTION_STATUS = passthroughOperation("subscription.status");
3537
+ const SUBSCRIPTION_PAY = passthroughOperation("subscription.pay");
3538
+ const SUBSCRIPTION_SET_AUTO_RENEWAL = passthroughOperation("subscription.setAutoRenewal");
3539
+ const SUBSCRIPTION_BIND_CARD = passthroughOperation("subscription.bindCard");
3540
+ const SUBSCRIPTION_SET_DEFAULT_METHOD = passthroughOperation("subscription.setDefaultMethod");
3541
+ const SUBSCRIPTION_REMOVE_METHOD = voidOperation("subscription.removeMethod");
3222
3542
  /**
3223
3543
  * Подписка и способы оплаты.
3224
3544
  *
@@ -3227,7 +3547,7 @@ var SearchResource = class extends BaseResource {
3227
3547
  var SubscriptionResource = class extends BaseResource {
3228
3548
  /** Загружает состояние подписки и её цену. */
3229
3549
  status(options = {}) {
3230
- return this.http.operation("subscription.status", {
3550
+ return this.http.execute(SUBSCRIPTION_STATUS, {
3231
3551
  path: "/api/v1/subscription/",
3232
3552
  ...options
3233
3553
  });
@@ -3238,14 +3558,14 @@ var SubscriptionResource = class extends BaseResource {
3238
3558
  * Форма ответа в документации API не описана, поэтому тип результата не уточняется.
3239
3559
  */
3240
3560
  pay(options = {}) {
3241
- return this.http.operation("subscription.pay", {
3561
+ return this.http.execute(SUBSCRIPTION_PAY, {
3242
3562
  path: "/api/v1/subscription/pay",
3243
3563
  ...options
3244
3564
  });
3245
3565
  }
3246
3566
  /** Включает или отключает автопродление. */
3247
3567
  setAutoRenewal(enabled, options = {}) {
3248
- return this.http.operation("subscription.setAutoRenewal", {
3568
+ return this.http.execute(SUBSCRIPTION_SET_AUTO_RENEWAL, {
3249
3569
  path: "/api/v1/subscription/auto-renewal",
3250
3570
  body: { enabled },
3251
3571
  ...options
@@ -3253,29 +3573,28 @@ var SubscriptionResource = class extends BaseResource {
3253
3573
  }
3254
3574
  /** Запускает привязку карты. */
3255
3575
  bindCard(options = {}) {
3256
- return this.http.operation("subscription.bindCard", {
3576
+ return this.http.execute(SUBSCRIPTION_BIND_CARD, {
3257
3577
  path: "/api/v1/subscription/bind-card",
3258
3578
  ...options
3259
3579
  });
3260
3580
  }
3261
3581
  /** Загружает список способов оплаты. Пустой массив, если карт нет. */
3262
- async methods(options = {}) {
3263
- const body = await this.http.operation("subscription.methods", {
3582
+ methods(options = {}) {
3583
+ return this.http.execute(SUBSCRIPTION_METHODS, {
3264
3584
  path: "/api/v1/subscription/methods",
3265
3585
  ...options
3266
3586
  });
3267
- return Array.isArray(body) ? body : [];
3268
3587
  }
3269
3588
  /** Делает способ оплаты основным. */
3270
3589
  setDefaultMethod(methodId, options = {}) {
3271
- return this.http.operation("subscription.setDefaultMethod", {
3590
+ return this.http.execute(SUBSCRIPTION_SET_DEFAULT_METHOD, {
3272
3591
  path: `/api/v1/subscription/methods/${encodePathSegment(methodId, "methodId")}/default`,
3273
3592
  ...options
3274
3593
  });
3275
3594
  }
3276
3595
  /** Удаляет способ оплаты. */
3277
3596
  removeMethod(methodId, options = {}) {
3278
- return this.http.operation("subscription.removeMethod", {
3597
+ return this.voidOperation(SUBSCRIPTION_REMOVE_METHOD, {
3279
3598
  path: `/api/v1/subscription/methods/${encodePathSegment(methodId, "methodId")}`,
3280
3599
  ...options
3281
3600
  });
@@ -3434,7 +3753,7 @@ var TelemetryBatchImpl = class {
3434
3753
  close() {
3435
3754
  return this.#close(false);
3436
3755
  }
3437
- /** Внутренняя финализация после перехода клиента в terminal state. */
3756
+ /** Внутренняя отправка данных после вызова `dispose()`. */
3438
3757
  closeForDispose() {
3439
3758
  this.prepareForDispose();
3440
3759
  return this.#close(true);
@@ -3530,7 +3849,7 @@ var TelemetryResource = class extends BaseResource {
3530
3849
  },
3531
3850
  ...requestOptions
3532
3851
  };
3533
- return disposeCleanup ? this.http.cleanupOperation("telemetry.dwell", options) : this.http.operation("telemetry.dwell", options);
3852
+ return disposeCleanup ? this.http.cleanupOperation(TELEMETRY_DWELL, options) : this.http.execute(TELEMETRY_DWELL, options);
3534
3853
  }
3535
3854
  /** Отправляет события взаимодействия с контентом (`POST /api/v1/x`). */
3536
3855
  interaction(entries, telemetryOptions = {}, requestOptions = {}) {
@@ -3554,7 +3873,7 @@ var TelemetryResource = class extends BaseResource {
3554
3873
  },
3555
3874
  ...requestOptions
3556
3875
  };
3557
- return disposeCleanup ? this.http.cleanupOperation("telemetry.interaction", options) : this.http.operation("telemetry.interaction", options);
3876
+ return disposeCleanup ? this.http.cleanupOperation(TELEMETRY_INTERACTION, options) : this.http.execute(TELEMETRY_INTERACTION, options);
3558
3877
  }
3559
3878
  /** Начинает измерять время просмотра и отправляет результат после `finish()`. */
3560
3879
  startView(input, options = {}, requestOptions = {}) {
@@ -3597,12 +3916,43 @@ var TelemetryResource = class extends BaseResource {
3597
3916
  function prepareTelemetryForDispose(resource) {
3598
3917
  if (resource) TELEMETRY_DISPOSE_PREPARERS.get(resource)?.();
3599
3918
  }
3600
- /** Финализирует созданные накопители после перехода клиента в terminal state. @internal */
3919
+ /** Отправляет и закрывает созданные накопители после вызова `dispose()`. @internal */
3601
3920
  function closeTelemetryForDispose(resource) {
3602
3921
  return resource ? TELEMETRY_DISPOSE_CLOSERS.get(resource)?.() ?? Promise.resolve() : Promise.resolve();
3603
3922
  }
3923
+ const TELEMETRY_DWELL = defineBuiltInOperation("telemetry.dwell");
3924
+ const TELEMETRY_INTERACTION = defineBuiltInOperation("telemetry.interaction");
3604
3925
  //#endregion
3605
3926
  //#region src/resources/users.ts
3927
+ const userListOperation = (id) => pageOperation(id, (body) => readPagedPage(body, "users", "followers", "following", "blocked"));
3928
+ const USER_LIST_OPERATIONS = {
3929
+ "users.followers": userListOperation("users.followers"),
3930
+ "users.following": userListOperation("users.following"),
3931
+ "users.blocked": userListOperation("users.blocked")
3932
+ };
3933
+ const USERS_CHECK_USERNAME = defineBuiltInOperation("users.checkUsername", (body) => pickBoolean(body, "available"));
3934
+ const USERS_SEARCH = defineBuiltInOperation("users.search", (body) => pickArray(body, "users"));
3935
+ const USERS_WHO_TO_FOLLOW = defineBuiltInOperation("users.whoToFollow", (body) => pickArray(body, "users"));
3936
+ const USERS_TOP_CLANS = defineBuiltInOperation("users.topClans", (body) => pickArray(body, "clans"));
3937
+ const USERS_PINS = defineBuiltInOperation("users.pins", (body) => ({
3938
+ pins: pickArray(body, "pins"),
3939
+ activePin: pickString(body, "activePin") ?? null
3940
+ }));
3941
+ const USERS_ME = passthroughOperation("users.me");
3942
+ const USERS_UPDATE_ME = passthroughOperation("users.updateMe");
3943
+ const USERS_CREATE_PROFILE = passthroughOperation("users.createProfile");
3944
+ const USERS_GET = passthroughOperation("users.get");
3945
+ const USERS_FOLLOW = passthroughOperation("users.follow");
3946
+ const USERS_FOLLOW_STATUS = passthroughOperation("users.followStatus");
3947
+ const USERS_GET_PRIVACY = passthroughOperation("users.getPrivacy");
3948
+ const USERS_UPDATE_PRIVACY = passthroughOperation("users.updatePrivacy");
3949
+ const USERS_DEACTIVATE = voidOperation("users.deactivate");
3950
+ const USERS_RESTORE = voidOperation("users.restore");
3951
+ const USERS_UNFOLLOW = voidOperation("users.unfollow");
3952
+ const USERS_BLOCK = voidOperation("users.block");
3953
+ const USERS_UNBLOCK = voidOperation("users.unblock");
3954
+ const USERS_SET_PIN = voidOperation("users.setPin");
3955
+ const USERS_REMOVE_PIN = voidOperation("users.removePin");
3606
3956
  /**
3607
3957
  * Пользователи: профили, подписки, блокировки, приватность.
3608
3958
  *
@@ -3623,23 +3973,22 @@ var UsersResource = class extends BaseResource {
3623
3973
  * сервер его сейчас не читает (см. {@link followers}): когда починят — заработает само.
3624
3974
  */
3625
3975
  #userList = this.paginated({
3626
- operationId: (p) => p.operationId,
3976
+ operation: (p) => p.operation,
3627
3977
  path: (p) => p.path,
3628
3978
  query: (p) => ({ limit: p.limit }),
3629
3979
  start: (p) => p.page !== void 0 ? { page: p.page } : {},
3630
- read: (body) => readPagedPage(body, "users", "followers", "following", "blocked"),
3631
3980
  mode: PaginationMode.Page
3632
3981
  });
3633
3982
  /** Загружает свой профиль — с подпиской и признаком подтверждённого телефона. */
3634
3983
  me(options = {}) {
3635
- return this.http.operation("users.me", {
3984
+ return this.http.execute(USERS_ME, {
3636
3985
  path: "/api/users/me",
3637
3986
  ...options
3638
3987
  });
3639
3988
  }
3640
3989
  /** Обновляет свой профиль. Передавайте только изменяемые поля. */
3641
3990
  updateMe(input, options = {}) {
3642
- return this.http.operation("users.updateMe", {
3991
+ return this.http.execute(USERS_UPDATE_ME, {
3643
3992
  path: "/api/users/me",
3644
3993
  body: input,
3645
3994
  ...options
@@ -3666,21 +4015,21 @@ var UsersResource = class extends BaseResource {
3666
4015
  }
3667
4016
  /** Деактивирует аккаунт. Вернуть его можно через {@link restore}. */
3668
4017
  deactivate(options = {}) {
3669
- return this.http.operation("users.deactivate", {
4018
+ return this.voidOperation(USERS_DEACTIVATE, {
3670
4019
  path: "/api/users/me",
3671
4020
  ...options
3672
4021
  });
3673
4022
  }
3674
4023
  /** Восстанавливает деактивированный аккаунт. */
3675
4024
  restore(options = {}) {
3676
- return this.http.operation("users.restore", {
4025
+ return this.voidOperation(USERS_RESTORE, {
3677
4026
  path: "/api/users/me/restore",
3678
4027
  ...options
3679
4028
  });
3680
4029
  }
3681
4030
  /** Создаёт профиль после регистрации. */
3682
4031
  createProfile(input, options = {}) {
3683
- return this.http.operation("users.createProfile", {
4032
+ return this.http.execute(USERS_CREATE_PROFILE, {
3684
4033
  path: "/api/users/profile",
3685
4034
  body: input,
3686
4035
  ...options
@@ -3698,23 +4047,22 @@ var UsersResource = class extends BaseResource {
3698
4047
  * ```
3699
4048
  */
3700
4049
  get(user, options = {}) {
3701
- return this.http.operation("users.get", {
4050
+ return this.http.execute(USERS_GET, {
3702
4051
  path: `/api/users/${encodePathSegment(user, "user")}`,
3703
4052
  ...options
3704
4053
  });
3705
4054
  }
3706
4055
  /** Проверяет, свободно ли имя пользователя. */
3707
- async checkUsername(username, options = {}) {
3708
- const body = await this.http.operation("users.checkUsername", {
4056
+ checkUsername(username, options = {}) {
4057
+ return this.http.execute(USERS_CHECK_USERNAME, {
3709
4058
  path: "/api/users/check-username",
3710
4059
  query: { username },
3711
4060
  ...options
3712
4061
  });
3713
- return pickBoolean(body, "available");
3714
4062
  }
3715
4063
  /** Ищет пользователей по строке запроса. */
3716
- async search(query, params = {}, options = {}) {
3717
- const body = await this.http.operation("users.search", {
4064
+ search(query, params = {}, options = {}) {
4065
+ return this.http.execute(USERS_SEARCH, {
3718
4066
  path: "/api/users/search",
3719
4067
  query: {
3720
4068
  q: query,
@@ -3722,23 +4070,20 @@ var UsersResource = class extends BaseResource {
3722
4070
  },
3723
4071
  ...options
3724
4072
  });
3725
- return pickArray(body, "users");
3726
4073
  }
3727
4074
  /** Загружает рекомендации, на кого подписаться. */
3728
- async whoToFollow(options = {}) {
3729
- const body = await this.http.operation("users.whoToFollow", {
4075
+ whoToFollow(options = {}) {
4076
+ return this.http.execute(USERS_WHO_TO_FOLLOW, {
3730
4077
  path: "/api/users/suggestions/who-to-follow",
3731
4078
  ...options
3732
4079
  });
3733
- return pickArray(body, "users");
3734
4080
  }
3735
4081
  /** Загружает рейтинг кланов. */
3736
- async topClans(options = {}) {
3737
- const body = await this.http.operation("users.topClans", {
4082
+ topClans(options = {}) {
4083
+ return this.http.execute(USERS_TOP_CLANS, {
3738
4084
  path: "/api/users/stats/top-clans",
3739
4085
  ...options
3740
4086
  });
3741
- return pickArray(body, "clans");
3742
4087
  }
3743
4088
  /**
3744
4089
  * Подписывается на пользователя.
@@ -3746,7 +4091,7 @@ var UsersResource = class extends BaseResource {
3746
4091
  * У закрытого профиля вместо подписки отправляется заявка — это видно по полю `status`.
3747
4092
  */
3748
4093
  follow(user, options = {}) {
3749
- return this.http.operation("users.follow", {
4094
+ return this.http.execute(USERS_FOLLOW, {
3750
4095
  path: `/api/users/${encodePathSegment(user, "user")}/follow`,
3751
4096
  body: {},
3752
4097
  ...options
@@ -3754,7 +4099,7 @@ var UsersResource = class extends BaseResource {
3754
4099
  }
3755
4100
  /** Отписывается от пользователя. */
3756
4101
  unfollow(user, options = {}) {
3757
- return this.http.operation("users.unfollow", {
4102
+ return this.voidOperation(USERS_UNFOLLOW, {
3758
4103
  path: `/api/users/${encodePathSegment(user, "user")}/follow`,
3759
4104
  ...options
3760
4105
  });
@@ -3802,7 +4147,7 @@ var UsersResource = class extends BaseResource {
3802
4147
  * ```
3803
4148
  */
3804
4149
  followStatus(userIds, options = {}) {
3805
- return this.http.operation("users.followStatus", {
4150
+ return this.http.execute(USERS_FOLLOW_STATUS, {
3806
4151
  path: "/api/users/follow-status",
3807
4152
  body: { userIds },
3808
4153
  ...options
@@ -3810,7 +4155,7 @@ var UsersResource = class extends BaseResource {
3810
4155
  }
3811
4156
  /** Блокирует пользователя. */
3812
4157
  block(user, options = {}) {
3813
- return this.http.operation("users.block", {
4158
+ return this.voidOperation(USERS_BLOCK, {
3814
4159
  path: `/api/users/${encodePathSegment(user, "user")}/block`,
3815
4160
  body: {},
3816
4161
  ...options
@@ -3818,7 +4163,7 @@ var UsersResource = class extends BaseResource {
3818
4163
  }
3819
4164
  /** Снимает блокировку. */
3820
4165
  unblock(user, options = {}) {
3821
- return this.http.operation("users.unblock", {
4166
+ return this.voidOperation(USERS_UNBLOCK, {
3822
4167
  path: `/api/users/${encodePathSegment(user, "user")}/block`,
3823
4168
  ...options
3824
4169
  });
@@ -3833,14 +4178,14 @@ var UsersResource = class extends BaseResource {
3833
4178
  }
3834
4179
  /** Загружает настройки приватности. */
3835
4180
  getPrivacy(options = {}) {
3836
- return this.http.operation("users.getPrivacy", {
4181
+ return this.http.execute(USERS_GET_PRIVACY, {
3837
4182
  path: "/api/users/me/privacy",
3838
4183
  ...options
3839
4184
  });
3840
4185
  }
3841
4186
  /** Обновляет настройки приватности. Передавайте только изменяемые поля. */
3842
4187
  updatePrivacy(input, options = {}) {
3843
- return this.http.operation("users.updatePrivacy", {
4188
+ return this.http.execute(USERS_UPDATE_PRIVACY, {
3844
4189
  path: "/api/users/me/privacy",
3845
4190
  body: input,
3846
4191
  ...options
@@ -3851,19 +4196,15 @@ var UsersResource = class extends BaseResource {
3851
4196
  *
3852
4197
  * `activePin` — строка-идентификатор, а не объект.
3853
4198
  */
3854
- async pins(options = {}) {
3855
- const body = await this.http.operation("users.pins", {
4199
+ pins(options = {}) {
4200
+ return this.http.execute(USERS_PINS, {
3856
4201
  path: "/api/users/me/pins",
3857
4202
  ...options
3858
4203
  });
3859
- return {
3860
- pins: pickArray(body, "pins"),
3861
- activePin: pickString(body, "activePin") ?? null
3862
- };
3863
4204
  }
3864
4205
  /** Выбирает активный значок профиля. */
3865
4206
  setPin(slug, options = {}) {
3866
- return this.http.operation("users.setPin", {
4207
+ return this.voidOperation(USERS_SET_PIN, {
3867
4208
  path: "/api/users/me/pin",
3868
4209
  body: { slug },
3869
4210
  ...options
@@ -3871,7 +4212,7 @@ var UsersResource = class extends BaseResource {
3871
4212
  }
3872
4213
  /** Снимает активный значок. */
3873
4214
  removePin(options = {}) {
3874
- return this.http.operation("users.removePin", {
4215
+ return this.voidOperation(USERS_REMOVE_PIN, {
3875
4216
  path: "/api/users/me/pin",
3876
4217
  ...options
3877
4218
  });
@@ -3879,20 +4220,22 @@ var UsersResource = class extends BaseResource {
3879
4220
  #userPage(operationId, path, params, options) {
3880
4221
  return this.#userList.list({
3881
4222
  ...params,
3882
- operationId,
4223
+ operation: USER_LIST_OPERATIONS[operationId],
3883
4224
  path
3884
4225
  }, options);
3885
4226
  }
3886
4227
  #userPaginator(operationId, path, params, options) {
3887
4228
  return this.#userList.iterate({
3888
4229
  ...params,
3889
- operationId,
4230
+ operation: USER_LIST_OPERATIONS[operationId],
3890
4231
  path
3891
4232
  }, options);
3892
4233
  }
3893
4234
  };
3894
4235
  //#endregion
3895
4236
  //#region src/resources/verification.ts
4237
+ const VERIFICATION_STATUS = passthroughOperation("verification.status");
4238
+ const VERIFICATION_SUBMIT = passthroughOperation("verification.submit");
3896
4239
  /**
3897
4240
  * Верификация профиля.
3898
4241
  *
@@ -3901,14 +4244,14 @@ var UsersResource = class extends BaseResource {
3901
4244
  var VerificationResource = class extends BaseResource {
3902
4245
  /** Загружает статус заявки. Значение `none` означает, что заявка не подавалась. */
3903
4246
  status(options = {}) {
3904
- return this.http.operation("verification.status", {
4247
+ return this.http.execute(VERIFICATION_STATUS, {
3905
4248
  path: "/api/verification/status",
3906
4249
  ...options
3907
4250
  });
3908
4251
  }
3909
4252
  /** Подаёт заявку на верификацию с видео. */
3910
4253
  submit(videoUrl, options = {}) {
3911
- return this.http.operation("verification.submit", {
4254
+ return this.http.execute(VERIFICATION_SUBMIT, {
3912
4255
  path: "/api/verification/submit",
3913
4256
  body: { videoUrl },
3914
4257
  ...options
@@ -3953,11 +4296,11 @@ function createResources(deps) {
3953
4296
  return comments;
3954
4297
  },
3955
4298
  get files() {
3956
- files ??= new FilesResource(deps.http, { fetch: deps.fetch });
4299
+ files ??= new FilesResource(deps.http, { files: deps.files });
3957
4300
  return files;
3958
4301
  },
3959
4302
  get notifications() {
3960
- notifications ??= new NotificationsResource(deps.http);
4303
+ notifications ??= deps.createNotifications(deps.http);
3961
4304
  return notifications;
3962
4305
  },
3963
4306
  get hashtags() {
@@ -4021,7 +4364,7 @@ function fromStream(factory, options = {}) {
4021
4364
  throw new ItdFileError("не удалось открыть поток вложения", {
4022
4365
  reason: ItdFileErrorReason.Read,
4023
4366
  retryable: true,
4024
- cause: error
4367
+ cause: redactErrorCause(error)
4025
4368
  });
4026
4369
  }
4027
4370
  const content = isReadableByteStream(opened) ? { stream: opened } : opened;
@@ -4284,6 +4627,6 @@ function renderSpans(content, spans = [], options = {}) {
4284
4627
  return result;
4285
4628
  }
4286
4629
  //#endregion
4287
- export { utcStampToIso as C, mapPage as D, Paginator as E, FeatureRegistry as O, toDate as S, PaginationMode as T, IMAGE_MIME_TYPES as _, fromStream as a, isBuilder as b, report as c, autoSpans as d, markup as f, AUDIO_MIME_TYPES as g, ALLOWED_MIME_TYPES as h, isMyProfile as i, ExtensibleOperationCatalog as k, post as l, parseMarkdown as m, renderSpans as n, fromUrl as o, parseHtml as p, statusDays as r, createResources as s, SpanRenderFormat as t, poll as u, VIDEO_MIME_TYPES as v, BaseResource as w, createStatusFeature as x, comment as y };
4630
+ export { ExtensibleOperationCatalog as A, utcStampToIso as C, Paginator as D, PaginationMode as E, mapPage as O, toDate as S, BaseResource as T, IMAGE_MIME_TYPES as _, fromStream as a, isBuilder as b, report as c, autoSpans as d, markup as f, AUDIO_MIME_TYPES as g, ALLOWED_MIME_TYPES as h, isMyProfile as i, createFeatureHost as k, post as l, parseMarkdown as m, renderSpans as n, fromUrl as o, parseHtml as p, statusDays as r, createResources as s, SpanRenderFormat as t, poll as u, VIDEO_MIME_TYPES as v, NotificationsResource as w, createStatusFeature as x, comment as y };
4288
4631
 
4289
- //# sourceMappingURL=render-C6HRPs10.js.map
4632
+ //# sourceMappingURL=render-CiL1VS0b.js.map