itd-api 0.7.1 → 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.
- package/README.md +14 -14
- package/dist/events/index.cjs +53 -0
- package/dist/events/index.d.cts +3 -0
- package/dist/events/index.d.ts +3 -0
- package/dist/events/index.js +5 -0
- package/dist/index.cjs +269 -175
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -56
- package/dist/index.d.ts +77 -56
- package/dist/index.js +251 -160
- package/dist/index.js.map +1 -1
- package/dist/node/index.cjs +4 -4
- package/dist/node/index.d.cts +1 -1
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.js +4 -4
- package/dist/rest/index.cjs +72 -14
- package/dist/rest/index.cjs.map +1 -1
- package/dist/rest/index.d.cts +11 -3
- package/dist/rest/index.d.ts +11 -3
- package/dist/rest/index.js +66 -8
- package/dist/rest/index.js.map +1 -1
- package/dist/shared/{errors-DfU8M5eS.cjs → errors-BmP3TKoW.cjs} +3 -3
- package/dist/shared/{errors-DfU8M5eS.cjs.map → errors-BmP3TKoW.cjs.map} +1 -1
- package/dist/shared/{errors-Bhrd2fJd.js → errors-GI10kZxk.js} +3 -3
- package/dist/shared/{errors-Bhrd2fJd.js.map → errors-GI10kZxk.js.map} +1 -1
- package/dist/shared/events-Br2r7coi.d.cts +698 -0
- package/dist/shared/events-QyhQZrnC.d.ts +698 -0
- package/dist/shared/{websocket-CbzB1Leq.js → events-TfhKh26b.js} +770 -490
- package/dist/shared/events-TfhKh26b.js.map +1 -0
- package/dist/shared/{websocket-sYlynrr0.cjs → events-iTteVsd_.cjs} +834 -518
- package/dist/shared/events-iTteVsd_.cjs.map +1 -0
- package/dist/shared/{multi-storage-CkvTUC5m.js → multi-storage-1AEZTczk.js} +4 -4
- package/dist/shared/{multi-storage-CkvTUC5m.js.map → multi-storage-1AEZTczk.js.map} +1 -1
- package/dist/shared/{multi-storage--yTEqiod.cjs → multi-storage-CCUgkqvI.cjs} +6 -6
- package/dist/shared/{multi-storage--yTEqiod.cjs.map → multi-storage-CCUgkqvI.cjs.map} +1 -1
- package/dist/shared/{options-DtATYdLr.js → options-BNP633A5.js} +8 -6
- package/dist/shared/options-BNP633A5.js.map +1 -0
- package/dist/shared/{options-Dg5N3r1V.cjs → options-BUfoHE9G.cjs} +8 -6
- package/dist/shared/options-BUfoHE9G.cjs.map +1 -0
- package/dist/shared/{cookies-DZwFq6kr.cjs → redact-DB9EMGJx.cjs} +301 -2
- package/dist/shared/redact-DB9EMGJx.cjs.map +1 -0
- package/dist/shared/{cookies-tX2sNwxb.js → redact-FNK29Fv-.js} +224 -3
- package/dist/shared/redact-FNK29Fv-.js.map +1 -0
- package/dist/shared/{render-DMp_3Nzk.d.cts → render-BszcKF7S.d.cts} +432 -254
- package/dist/shared/{render-vtLixIiU.js → render-CiL1VS0b.js} +1261 -621
- package/dist/shared/render-CiL1VS0b.js.map +1 -0
- package/dist/shared/{render-DyeHJNBw.cjs → render-DNTESBSl.cjs} +1335 -671
- package/dist/shared/render-DNTESBSl.cjs.map +1 -0
- package/dist/shared/{render-DZrxhC5_.d.ts → render-DVB7T8pm.d.ts} +432 -254
- package/dist/shared/{storage-BPJR_k4-.cjs → storage-BLmXmzKF.cjs} +2 -2
- package/dist/shared/{storage-BPJR_k4-.cjs.map → storage-BLmXmzKF.cjs.map} +1 -1
- package/dist/shared/{storage-D86edNCB.js → storage-CCxn0Kkv.js} +2 -2
- package/dist/shared/{storage-D86edNCB.js.map → storage-CCxn0Kkv.js.map} +1 -1
- package/dist/shared/{url-CYXgxqGx.js → url-B3ocFlHj.js} +491 -425
- package/dist/shared/url-B3ocFlHj.js.map +1 -0
- package/dist/shared/{url-B6-bXHKt.d.ts → url-Be9jlWRv.d.cts} +1233 -1195
- package/dist/shared/{url-B6-bXHKt.d.cts → url-Be9jlWRv.d.ts} +1233 -1195
- package/dist/shared/{url-yjl2c8Ie.cjs → url-CFN1sT9S.cjs} +562 -478
- package/dist/shared/url-CFN1sT9S.cjs.map +1 -0
- package/dist/web/index.cjs +2 -2
- package/dist/web/index.js +2 -2
- package/package.json +16 -15
- package/dist/realtime/index.cjs +0 -165
- package/dist/realtime/index.cjs.map +0 -1
- package/dist/realtime/index.d.cts +0 -51
- package/dist/realtime/index.d.ts +0 -51
- package/dist/realtime/index.js +0 -120
- package/dist/realtime/index.js.map +0 -1
- package/dist/shared/auth-provider-BfogACAb.js +0 -91
- package/dist/shared/auth-provider-BfogACAb.js.map +0 -1
- package/dist/shared/auth-provider-CTJkKfgy.cjs +0 -108
- package/dist/shared/auth-provider-CTJkKfgy.cjs.map +0 -1
- package/dist/shared/cookies-DZwFq6kr.cjs.map +0 -1
- package/dist/shared/cookies-tX2sNwxb.js.map +0 -1
- package/dist/shared/options-Dg5N3r1V.cjs.map +0 -1
- package/dist/shared/options-DtATYdLr.js.map +0 -1
- package/dist/shared/render-DyeHJNBw.cjs.map +0 -1
- package/dist/shared/render-vtLixIiU.js.map +0 -1
- package/dist/shared/url-CYXgxqGx.js.map +0 -1
- package/dist/shared/url-yjl2c8Ie.cjs.map +0 -1
- package/dist/shared/websocket-BMtihD56.d.ts +0 -562
- package/dist/shared/websocket-CbzB1Leq.js.map +0 -1
- package/dist/shared/websocket-D1p32SB2.d.cts +0 -562
- package/dist/shared/websocket-sYlynrr0.cjs.map +0 -1
|
@@ -1,7 +1,752 @@
|
|
|
1
|
-
const require_url = require("./url-
|
|
2
|
-
const require_errors = require("./errors-
|
|
3
|
-
const
|
|
4
|
-
const require_options = require("./options-
|
|
1
|
+
const require_url = require("./url-CFN1sT9S.cjs");
|
|
2
|
+
const require_errors = require("./errors-BmP3TKoW.cjs");
|
|
3
|
+
const require_redact = require("./redact-DB9EMGJx.cjs");
|
|
4
|
+
const require_options = require("./options-BUfoHE9G.cjs");
|
|
5
|
+
//#region src/core/feature-catalog.ts
|
|
6
|
+
/**
|
|
7
|
+
* Изменяемый слой над каталогом конкретного API.
|
|
8
|
+
*
|
|
9
|
+
* Базовый каталог остаётся замороженным и общим для всех клиентов, а регистрации feature
|
|
10
|
+
* принадлежат одному facade. Поэтому установка модуля в одном клиенте не меняет остальные.
|
|
11
|
+
*
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
var ExtensibleOperationCatalog = class {
|
|
15
|
+
#base;
|
|
16
|
+
#operations = /* @__PURE__ */ new Map();
|
|
17
|
+
#operationOwners = /* @__PURE__ */ new Map();
|
|
18
|
+
#bucketOwners = /* @__PURE__ */ new Map();
|
|
19
|
+
#bucketLimits;
|
|
20
|
+
#bucketOverrides;
|
|
21
|
+
constructor(base) {
|
|
22
|
+
this.#base = base;
|
|
23
|
+
this.#bucketLimits = { ...base.bucketLimits };
|
|
24
|
+
this.#bucketOverrides = { ...base.bucketOverrides };
|
|
25
|
+
}
|
|
26
|
+
get bucketLimits() {
|
|
27
|
+
return this.#bucketLimits;
|
|
28
|
+
}
|
|
29
|
+
get bucketOverrides() {
|
|
30
|
+
return this.#bucketOverrides;
|
|
31
|
+
}
|
|
32
|
+
get defaultBucket() {
|
|
33
|
+
return this.#base.defaultBucket;
|
|
34
|
+
}
|
|
35
|
+
definitionOf(id) {
|
|
36
|
+
return this.#operations.get(id) ?? this.#base.definitionOf(id);
|
|
37
|
+
}
|
|
38
|
+
retrySafetyOf(id) {
|
|
39
|
+
return this.#operations.get(id)?.retrySafety ?? this.#base.retrySafetyOf(id);
|
|
40
|
+
}
|
|
41
|
+
methodOf(id) {
|
|
42
|
+
return this.#operations.get(id)?.method ?? this.#base.methodOf(id);
|
|
43
|
+
}
|
|
44
|
+
bucketOf(id) {
|
|
45
|
+
return this.#operations.get(id)?.bucket ?? this.#base.bucketOf(id);
|
|
46
|
+
}
|
|
47
|
+
isKnownBucket(name) {
|
|
48
|
+
return this.#bucketOwners.has(name) || this.#base.isKnownBucket(name);
|
|
49
|
+
}
|
|
50
|
+
/** Регистрирует принадлежащий feature бакет и возвращает откат регистрации. */
|
|
51
|
+
registerBucket(owner, name, definition) {
|
|
52
|
+
if (this.#base.isKnownBucket(name) || this.#bucketOwners.has(name)) throw new require_errors.ItdConfigError(`feature «${owner}»: бакет «${name}» уже зарегистрирован`);
|
|
53
|
+
this.#bucketOwners.set(name, owner);
|
|
54
|
+
if (definition.limit !== void 0) this.#bucketLimits[name] = definition.limit;
|
|
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
|
+
});
|
|
59
|
+
return () => {
|
|
60
|
+
if (this.#bucketOwners.get(name) !== owner) return;
|
|
61
|
+
this.#bucketOwners.delete(name);
|
|
62
|
+
delete this.#bucketLimits[name];
|
|
63
|
+
delete this.#bucketOverrides[name];
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Регистрирует операцию и возвращает откат регистрации. */
|
|
67
|
+
registerOperation(owner, id, definition) {
|
|
68
|
+
const ownerPrefix = `${owner}.`;
|
|
69
|
+
if (!id.startsWith(ownerPrefix) || id.length === ownerPrefix.length) throw new require_errors.ItdConfigError(`feature «${owner}»: операция «${id}» должна находиться в пространстве имён «${owner}»`);
|
|
70
|
+
const occupiedBy = this.#operationOwners.get(id);
|
|
71
|
+
if (occupiedBy !== void 0) throw new require_errors.ItdConfigError(`feature «${owner}»: операция «${id}» уже зарегистрирована feature «${occupiedBy}»`);
|
|
72
|
+
if (this.#base.methodOf(id) !== void 0) throw new require_errors.ItdConfigError(`feature «${owner}»: операция «${id}» уже существует в базовом каталоге`);
|
|
73
|
+
this.#operationOwners.set(id, owner);
|
|
74
|
+
this.#operations.set(id, Object.freeze({ ...definition }));
|
|
75
|
+
return () => {
|
|
76
|
+
if (this.#operationOwners.get(id) !== owner) return;
|
|
77
|
+
this.#operationOwners.delete(id);
|
|
78
|
+
this.#operations.delete(id);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
};
|
|
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 require_options.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 require_errors.ItdConfigError(`«${require_redact.redactUrl(target)}» не разбирается как адрес`);
|
|
114
|
+
}
|
|
115
|
+
if (requested.protocol !== "http:" && requested.protocol !== "https:") throw new require_errors.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 = require_redact.redactUrl(requested.href);
|
|
122
|
+
throw new require_errors.ItdFileError(`не удалось получить файл по адресу ${safeUrl}`, {
|
|
123
|
+
reason: require_errors.ItdFileErrorReason.Network,
|
|
124
|
+
url: safeUrl,
|
|
125
|
+
retryable: true,
|
|
126
|
+
cause: require_redact.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 = require_redact.redactUrl(finalUrl.href);
|
|
133
|
+
throw new require_errors.ItdFileError(`источник ${safeUrl} ответил статусом ${response.status}`, {
|
|
134
|
+
reason: require_errors.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 } = require_options.resolveFileStreamOptions(options, require_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 require_options.fileTooLarge(url, maxBytes, blob.size);
|
|
159
|
+
return blob;
|
|
160
|
+
}
|
|
161
|
+
const chunks = [];
|
|
162
|
+
const reader = require_options.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 = require_options.resolveFileStreamOptions(options, require_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 = require_options.resolveFileStreamOptions(options, require_options.DEFAULT_URL_FILE_MAX_BYTES);
|
|
196
|
+
const { response, url, size } = await fetchFile(target, options, context);
|
|
197
|
+
if (!response.body) {
|
|
198
|
+
const safeUrl = require_redact.redactUrl(url.href);
|
|
199
|
+
throw new require_errors.ItdFileError(`источник ${safeUrl} не предоставил потоковое тело`, {
|
|
200
|
+
reason: require_errors.ItdFileErrorReason.StreamUnavailable,
|
|
201
|
+
url: safeUrl
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
const stream = require_options.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 require_options.FileTransferMode.Buffer;
|
|
241
|
+
if ("open" in input) return require_options.FileTransferMode.Stream;
|
|
242
|
+
if ("url" in input) return require_options.resolveFileStreamOptions(input).mode;
|
|
243
|
+
return require_options.FileTransferMode.Buffer;
|
|
244
|
+
}
|
|
245
|
+
async resolve(input, options = {}, context = {}) {
|
|
246
|
+
const attempt = context.attempt ?? 1;
|
|
247
|
+
if (!Number.isInteger(attempt) || attempt < 1) throw new require_errors.ItdConfigError(`attempt должен быть целым числом от 1, получено: ${attempt}`);
|
|
248
|
+
const maxBytes = require_options.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) === require_options.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 === require_options.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 require_errors.ItdError || context.signal?.aborted) throw error;
|
|
264
|
+
throw new require_errors.ItdFileError("не удалось получить содержимое вложения", {
|
|
265
|
+
reason: require_errors.ItdFileErrorReason.Read,
|
|
266
|
+
retryable: true,
|
|
267
|
+
cause: require_redact.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 require_errors.ItdConfigError("буферный источник должен вернуть объект с полем file");
|
|
274
|
+
if (!require_redact.isBlob(content.file) && !(content.file instanceof ArrayBuffer) && !ArrayBuffer.isView(content.file)) throw new require_errors.ItdConfigError("поле file должно содержать Blob, ArrayBuffer или Uint8Array");
|
|
275
|
+
const contentType = normalizeMimeType(options.contentType ?? content.contentType) ?? normalizeMimeType(require_redact.isBlob(content.file) ? content.file.type : void 0);
|
|
276
|
+
const blob = require_redact.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 require_options.fileTooLarge(void 0, maxBytes, blob.size);
|
|
278
|
+
return {
|
|
279
|
+
mode: require_options.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 || !require_options.isReadableByteStream(opened.stream)) {
|
|
289
|
+
await opened?.close?.();
|
|
290
|
+
throw new require_errors.ItdConfigError("потоковый источник должен вернуть { stream: ReadableStream }");
|
|
291
|
+
}
|
|
292
|
+
let handedOff = false;
|
|
293
|
+
const closeOpened = closeOnce(async () => {
|
|
294
|
+
await opened.close?.();
|
|
295
|
+
});
|
|
296
|
+
try {
|
|
297
|
+
require_options.optionalBytes(opened.size, "size");
|
|
298
|
+
if (maxBytes !== void 0 && opened.size !== void 0 && opened.size > maxBytes) throw require_options.fileTooLarge(void 0, maxBytes, opened.size);
|
|
299
|
+
if (options.requireKnownSize === true && opened.size === void 0) throw new require_errors.ItdConfigError("размер потокового вложения должен быть известен до чтения");
|
|
300
|
+
const limits = require_options.resolveFileStreamOptions({
|
|
301
|
+
mode: require_options.FileTransferMode.Stream,
|
|
302
|
+
...maxBytes !== void 0 ? { maxBytes } : {},
|
|
303
|
+
...options.streamBufferBytes !== void 0 ? { streamBufferBytes: options.streamBufferBytes } : {}
|
|
304
|
+
});
|
|
305
|
+
const stream = require_options.isBoundedFileStream(opened.stream) && options.maxBytes === void 0 && options.streamBufferBytes === void 0 ? opened.stream : require_options.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: require_options.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) || require_redact.isBlob(input)) return {
|
|
344
|
+
file: input,
|
|
345
|
+
...require_redact.isFile(input) ? { filename: input.name } : {}
|
|
346
|
+
};
|
|
347
|
+
if (typeof input !== "object" || input === null) throw new require_errors.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 require_errors.ItdConfigError("потоковый источник нельзя использовать как буферный");
|
|
355
|
+
throw new require_errors.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 require_errors.ItdConfigError("потоковое вложение должно иметь форму { open } или { url, mode }");
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
/** Создаёт механизм подготовки файлов, привязанный к настроенному `fetch` клиента. @internal */
|
|
367
|
+
function createFileResolver(fetchImplementation) {
|
|
368
|
+
return new DefaultFileResolver(fetchImplementation);
|
|
369
|
+
}
|
|
370
|
+
//#endregion
|
|
371
|
+
//#region src/core/features.ts
|
|
372
|
+
function requireName(value, kind) {
|
|
373
|
+
if (typeof value !== "string" || value.trim() === "") throw new require_errors.ItdConfigError(`${kind} должен иметь непустое имя`);
|
|
374
|
+
return value.trim();
|
|
375
|
+
}
|
|
376
|
+
function requireFeatureName(value) {
|
|
377
|
+
const name = requireName(value, "Feature");
|
|
378
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name)) throw new require_errors.ItdConfigError(`имя feature «${name}» должно соответствовать [a-z][a-z0-9-]*`);
|
|
379
|
+
return name;
|
|
380
|
+
}
|
|
381
|
+
function validateBucket(owner, localName, bucket) {
|
|
382
|
+
if (typeof bucket !== "object" || bucket === null || Array.isArray(bucket)) throw new require_errors.ItdConfigError(`feature «${owner}»: бакет «${localName}» должен быть объектом`);
|
|
383
|
+
if (bucket.limit !== void 0 && (!Number.isFinite(bucket.limit) || bucket.limit <= 0)) throw new require_errors.ItdConfigError(`feature «${owner}»: limit бакета «${localName}» должен быть положительным числом`);
|
|
384
|
+
if (bucket.concurrency !== void 0 && (!Number.isInteger(bucket.concurrency) || bucket.concurrency < 1)) throw new require_errors.ItdConfigError(`feature «${owner}»: concurrency бакета «${localName}» должен быть целым числом от 1`);
|
|
385
|
+
if (bucket.rps !== void 0 && (!Number.isFinite(bucket.rps) || bucket.rps <= 0)) throw new require_errors.ItdConfigError(`feature «${owner}»: rps бакета «${localName}» должен быть положительным числом`);
|
|
386
|
+
}
|
|
387
|
+
/** Реестр подключаемых модулей клиента. @internal */
|
|
388
|
+
var FeatureRegistry = class {
|
|
389
|
+
#deps;
|
|
390
|
+
#features = /* @__PURE__ */ new Map();
|
|
391
|
+
#serviceOwners = /* @__PURE__ */ new Map();
|
|
392
|
+
#disposed = false;
|
|
393
|
+
constructor(deps) {
|
|
394
|
+
this.#deps = deps;
|
|
395
|
+
}
|
|
396
|
+
install(feature) {
|
|
397
|
+
this.#deps.assertActive("установить feature");
|
|
398
|
+
if (this.#disposed) throw new require_errors.ItdStateError("Реестр feature уже освобождён");
|
|
399
|
+
if (typeof feature !== "object" || feature === null) throw new require_errors.ItdConfigError("feature должен быть объектом");
|
|
400
|
+
const name = requireFeatureName(feature.name);
|
|
401
|
+
if (this.#features.has(name)) throw new require_errors.ItdConfigError(`feature «${name}» уже установлен`);
|
|
402
|
+
if (typeof feature.setup !== "function") throw new require_errors.ItdConfigError(`feature «${name}» должен предоставлять setup()`);
|
|
403
|
+
if (typeof feature.operations !== "object" || feature.operations === null || Array.isArray(feature.operations)) throw new require_errors.ItdConfigError(`feature «${name}» должен объявлять operations`);
|
|
404
|
+
if (feature.services !== void 0 && !Array.isArray(feature.services)) throw new require_errors.ItdConfigError(`feature «${name}»: services должен быть массивом`);
|
|
405
|
+
if (feature.buckets !== void 0 && (typeof feature.buckets !== "object" || feature.buckets === null || Array.isArray(feature.buckets))) throw new require_errors.ItdConfigError(`feature «${name}»: buckets должен быть объектом`);
|
|
406
|
+
const rollback = [];
|
|
407
|
+
const services = /* @__PURE__ */ new Set();
|
|
408
|
+
const bucketNames = /* @__PURE__ */ new Map();
|
|
409
|
+
const operations = /* @__PURE__ */ new Map();
|
|
410
|
+
const controller = new AbortController();
|
|
411
|
+
let committed = false;
|
|
412
|
+
const reportRollbackError = (message, error) => {
|
|
413
|
+
try {
|
|
414
|
+
this.#deps.logger?.error(message, error);
|
|
415
|
+
} catch {}
|
|
416
|
+
};
|
|
417
|
+
try {
|
|
418
|
+
for (const service of feature.services ?? []) {
|
|
419
|
+
const serviceName = requireName(service?.name, `Сервис feature «${name}»`);
|
|
420
|
+
const owner = this.#serviceOwners.get(serviceName);
|
|
421
|
+
if (owner !== void 0) throw new require_errors.ItdConfigError(`feature «${name}»: сервис «${serviceName}» уже принадлежит feature «${owner}»`);
|
|
422
|
+
if (services.has(serviceName)) throw new require_errors.ItdConfigError(`feature «${name}»: сервис «${serviceName}» объявлен повторно`);
|
|
423
|
+
if (!this.#deps.services.has(serviceName)) {
|
|
424
|
+
this.#deps.services.define(service);
|
|
425
|
+
rollback.push(() => void this.#deps.services.delete(serviceName));
|
|
426
|
+
} else {
|
|
427
|
+
const override = this.#deps.serviceOverrides.find((candidate) => candidate.name.trim() === serviceName);
|
|
428
|
+
if (override) {
|
|
429
|
+
const previous = this.#deps.services.replace(require_url.mergeService(service, override));
|
|
430
|
+
rollback.push(() => void this.#deps.services.replace(previous));
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
this.#serviceOwners.set(serviceName, name);
|
|
434
|
+
rollback.push(() => {
|
|
435
|
+
if (this.#serviceOwners.get(serviceName) === name) this.#serviceOwners.delete(serviceName);
|
|
436
|
+
});
|
|
437
|
+
services.add(serviceName);
|
|
438
|
+
}
|
|
439
|
+
for (const [rawLocalName, bucket] of Object.entries(feature.buckets ?? {})) {
|
|
440
|
+
const localName = requireName(rawLocalName, `Бакет feature «${name}»`);
|
|
441
|
+
validateBucket(name, localName, bucket);
|
|
442
|
+
const globalName = `feature:${name}/${localName}`;
|
|
443
|
+
const unregisterCatalog = this.#deps.catalog.registerBucket(name, globalName, bucket);
|
|
444
|
+
rollback.push(unregisterCatalog);
|
|
445
|
+
const unregisterQueue = this.#deps.registerBucket(globalName, bucket);
|
|
446
|
+
if (unregisterQueue) rollback.push(unregisterQueue);
|
|
447
|
+
bucketNames.set(localName, globalName);
|
|
448
|
+
}
|
|
449
|
+
for (const [rawLocalName, definition] of Object.entries(feature.operations)) {
|
|
450
|
+
const localName = requireName(rawLocalName, `Операция feature «${name}»`);
|
|
451
|
+
if (typeof definition !== "object" || definition === null) throw new require_errors.ItdConfigError(`feature «${name}»: операция «${localName}» должна быть объектом`);
|
|
452
|
+
if (![
|
|
453
|
+
"GET",
|
|
454
|
+
"POST",
|
|
455
|
+
"PUT",
|
|
456
|
+
"PATCH",
|
|
457
|
+
"DELETE"
|
|
458
|
+
].includes(definition.method)) throw new require_errors.ItdConfigError(`feature «${name}»: операция «${localName}» содержит неизвестный HTTP-метод`);
|
|
459
|
+
if (!Object.values(require_url.RetrySafety).includes(definition.retrySafety)) throw new require_errors.ItdConfigError(`feature «${name}»: операция «${localName}» содержит неизвестную retrySafety`);
|
|
460
|
+
if (definition.service !== void 0 && typeof definition.service !== "string") throw new require_errors.ItdConfigError(`feature «${name}»: service операции «${localName}» должен быть строкой`);
|
|
461
|
+
if (definition.bucket !== void 0 && typeof definition.bucket !== "string") throw new require_errors.ItdConfigError(`feature «${name}»: bucket операции «${localName}» должен быть строкой`);
|
|
462
|
+
if (definition.read !== void 0 && typeof definition.read !== "function") throw new require_errors.ItdConfigError(`feature «${name}»: read операции «${localName}» должна быть функцией`);
|
|
463
|
+
if (definition.annotations !== void 0 && (typeof definition.annotations !== "object" || definition.annotations === null || Array.isArray(definition.annotations))) throw new require_errors.ItdConfigError(`feature «${name}»: annotations операции «${localName}» должны быть объектом`);
|
|
464
|
+
if (definition.service !== void 0 && !services.has(definition.service)) throw new require_errors.ItdConfigError(`feature «${name}»: операция «${localName}» ссылается на необъявленный сервис «${definition.service}»`);
|
|
465
|
+
const bucket = definition.bucket === void 0 ? this.#deps.catalog.defaultBucket : bucketNames.get(definition.bucket);
|
|
466
|
+
if (bucket === void 0) throw new require_errors.ItdConfigError(`feature «${name}»: операция «${localName}» ссылается на необъявленный бакет «${definition.bucket}»`);
|
|
467
|
+
const operationId = `${name}.${localName}`;
|
|
468
|
+
const contract = require_url.defineOperation(operationId, {
|
|
469
|
+
method: definition.method,
|
|
470
|
+
retrySafety: definition.retrySafety,
|
|
471
|
+
bucket,
|
|
472
|
+
...definition.annotations === void 0 ? {} : { annotations: definition.annotations }
|
|
473
|
+
}, definition.read ?? require_url.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 }
|
|
479
|
+
});
|
|
480
|
+
rollback.push(unregister);
|
|
481
|
+
operations.set(localName, {
|
|
482
|
+
contract,
|
|
483
|
+
service: definition.service
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
const context = Object.freeze({
|
|
487
|
+
featureName: name,
|
|
488
|
+
baseUrl: this.#deps.baseUrl,
|
|
489
|
+
signal: controller.signal,
|
|
490
|
+
clock: this.#deps.clock,
|
|
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 require_errors.ItdStateError(`feature «${name}» не активен`);
|
|
496
|
+
return this.#deps.files.resolve(input, options, context);
|
|
497
|
+
} catch (error) {
|
|
498
|
+
return Promise.reject(error);
|
|
499
|
+
}
|
|
500
|
+
} }),
|
|
501
|
+
request: (operation, options) => {
|
|
502
|
+
try {
|
|
503
|
+
this.#deps.assertActive(`выполнить операцию feature «${name}»`);
|
|
504
|
+
if (!committed || controller.signal.aborted) throw new require_errors.ItdStateError(`feature «${name}» не активен`);
|
|
505
|
+
const resolved = operations.get(operation);
|
|
506
|
+
if (!resolved) throw new require_errors.ItdConfigError(`feature «${name}» не объявляет операцию «${operation}»`);
|
|
507
|
+
const scoped = { ...options };
|
|
508
|
+
delete scoped.operationId;
|
|
509
|
+
delete scoped.method;
|
|
510
|
+
delete scoped.service;
|
|
511
|
+
delete scoped.baseUrl;
|
|
512
|
+
delete scoped.retrySafety;
|
|
513
|
+
delete scoped.rateLimitBucket;
|
|
514
|
+
return this.#deps.http.execute(resolved.contract, {
|
|
515
|
+
...scoped,
|
|
516
|
+
...resolved.service === void 0 ? {} : { service: resolved.service }
|
|
517
|
+
});
|
|
518
|
+
} catch (error) {
|
|
519
|
+
return Promise.reject(error);
|
|
520
|
+
}
|
|
521
|
+
},
|
|
522
|
+
serviceBaseUrl: (serviceName) => {
|
|
523
|
+
if (!services.has(serviceName)) throw new require_errors.ItdConfigError(`feature «${name}» не объявляет сервис «${serviceName}»`);
|
|
524
|
+
return this.#deps.services.resolveBaseUrl(serviceName);
|
|
525
|
+
},
|
|
526
|
+
connection: (serviceName) => {
|
|
527
|
+
if (!services.has(serviceName)) throw new require_errors.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 require_errors.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 require_errors.ItdStateError(`feature «${name}» не активен`);
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
const installation = feature.setup(context);
|
|
565
|
+
if (typeof installation !== "object" || installation === null || !("api" in installation)) throw new require_errors.ItdConfigError(`setup() feature «${name}» должен вернуть объект с полем api`);
|
|
566
|
+
if (installation.close !== void 0 && typeof installation.close !== "function") throw new require_errors.ItdConfigError(`feature «${name}»: close должен быть функцией`);
|
|
567
|
+
if (installation.dispose !== void 0 && typeof installation.dispose !== "function") throw new require_errors.ItdConfigError(`feature «${name}»: dispose должен быть функцией`);
|
|
568
|
+
this.#features.set(name, {
|
|
569
|
+
name,
|
|
570
|
+
controller,
|
|
571
|
+
close: installation.close,
|
|
572
|
+
dispose: installation.dispose,
|
|
573
|
+
cleanup: [...rollback]
|
|
574
|
+
});
|
|
575
|
+
committed = true;
|
|
576
|
+
return installation.api;
|
|
577
|
+
} catch (error) {
|
|
578
|
+
controller.abort(new require_errors.ItdAbortError(`Установка feature «${name}» отменена`));
|
|
579
|
+
for (const undo of rollback.reverse()) undo();
|
|
580
|
+
throw error;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
names() {
|
|
584
|
+
return [...this.#features.keys()];
|
|
585
|
+
}
|
|
586
|
+
has(name) {
|
|
587
|
+
return this.#features.has(name);
|
|
588
|
+
}
|
|
589
|
+
async close() {
|
|
590
|
+
const errors = (await Promise.allSettled([...this.#features.values()].reverse().map(async (feature) => feature.close?.()))).filter((result) => result.status === "rejected").map((result) => result.reason);
|
|
591
|
+
if (errors.length > 0) throw new AggregateError(errors, "Не удалось закрыть feature");
|
|
592
|
+
}
|
|
593
|
+
async dispose() {
|
|
594
|
+
if (this.#disposed) return;
|
|
595
|
+
this.#disposed = true;
|
|
596
|
+
const errors = [];
|
|
597
|
+
const disposals = [];
|
|
598
|
+
const features = [...this.#features.values()].reverse();
|
|
599
|
+
for (const feature of features) {
|
|
600
|
+
feature.controller.abort(new require_errors.ItdAbortError(`Feature «${feature.name}» освобождён`));
|
|
601
|
+
try {
|
|
602
|
+
disposals.push(Promise.resolve(feature.dispose?.()).then(() => void 0));
|
|
603
|
+
} catch (error) {
|
|
604
|
+
errors.push(error);
|
|
605
|
+
}
|
|
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
|
+
}
|
|
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);
|
|
615
|
+
if (errors.length > 0) throw new AggregateError(errors, "Не удалось освободить feature");
|
|
616
|
+
}
|
|
617
|
+
};
|
|
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 require_errors.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 require_errors.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 = require_url.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 require_errors.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 = require_url.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 require_errors.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
|
|
5
750
|
//#region src/resources/pagination.ts
|
|
6
751
|
/** Схема пагинации эндпоинта. */
|
|
7
752
|
const PaginationMode = Object.freeze({
|
|
@@ -119,6 +864,10 @@ function readOffsetPage(body, field, offset) {
|
|
|
119
864
|
raw: body
|
|
120
865
|
};
|
|
121
866
|
}
|
|
867
|
+
/** Создаёт единый контракт пагинированной операции. */
|
|
868
|
+
function pageOperation(id, read) {
|
|
869
|
+
return require_url.defineBuiltInOperation(id, read);
|
|
870
|
+
}
|
|
122
871
|
/**
|
|
123
872
|
* Перебор страниц списка.
|
|
124
873
|
*
|
|
@@ -244,70 +993,308 @@ var BaseResource = class {
|
|
|
244
993
|
constructor(http) {
|
|
245
994
|
this.http = http;
|
|
246
995
|
}
|
|
247
|
-
/**
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
*
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
996
|
+
/** Выполняет операцию без возвращаемого значения и отбрасывает служебное тело ответа. */
|
|
997
|
+
voidOperation(operation, options) {
|
|
998
|
+
return this.http.execute(operation, options);
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Собирает перебор страниц.
|
|
1002
|
+
*
|
|
1003
|
+
* @param mode схема пагинации эндпоинта
|
|
1004
|
+
* @param load загружает одну страницу для указанной позиции
|
|
1005
|
+
* @param options только управление самим перебором: предел, отмена и начальная позиция
|
|
1006
|
+
*/
|
|
1007
|
+
paginate(mode, load, options) {
|
|
1008
|
+
return new Paginator({
|
|
1009
|
+
mode,
|
|
1010
|
+
load,
|
|
1011
|
+
...options?.maxPages !== void 0 ? { maxPages: options.maxPages } : {},
|
|
1012
|
+
...options?.signal !== void 0 ? { signal: options.signal } : {},
|
|
1013
|
+
...options?.start !== void 0 ? { start: options.start } : {}
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Собирает пару «загрузка страницы + перебор» из одного описания.
|
|
1018
|
+
*
|
|
1019
|
+
* Путь, параметры запроса и разбор ответа задаются один раз; `list` и `iterate`
|
|
1020
|
+
* строятся из них.
|
|
1021
|
+
*
|
|
1022
|
+
* @example
|
|
1023
|
+
* ```ts
|
|
1024
|
+
* #feed = this.paginated<Post, FeedParams>({
|
|
1025
|
+
* operation: POSTS_LIST,
|
|
1026
|
+
* path: () => '/api/posts',
|
|
1027
|
+
* query: (p) => ({ tab: p.tab, limit: p.limit }),
|
|
1028
|
+
* start: (p) => (p.cursor ? { cursor: p.cursor } : {}),
|
|
1029
|
+
* mode: PaginationMode.Cursor,
|
|
1030
|
+
* });
|
|
1031
|
+
* ```
|
|
1032
|
+
*/
|
|
1033
|
+
paginated(spec) {
|
|
1034
|
+
const load = async (params, state, options = {}) => {
|
|
1035
|
+
const operation = typeof spec.operation === "function" ? spec.operation(params) : spec.operation;
|
|
1036
|
+
return this.http.execute(operation, {
|
|
1037
|
+
path: spec.path(params),
|
|
1038
|
+
query: withPageState(spec.query(params), state),
|
|
1039
|
+
...options
|
|
1040
|
+
});
|
|
1041
|
+
};
|
|
1042
|
+
return {
|
|
1043
|
+
list: (params, options) => load(params, spec.start(params), options),
|
|
1044
|
+
iterate: (params, options = {}) => {
|
|
1045
|
+
const { maxPages, ...requestOptions } = options;
|
|
1046
|
+
return this.paginate(spec.mode, (state) => load(params, state, requestOptions), {
|
|
1047
|
+
...maxPages !== void 0 ? { maxPages } : {},
|
|
1048
|
+
...requestOptions.signal !== void 0 ? { signal: requestOptions.signal } : {},
|
|
1049
|
+
start: spec.start(params)
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
/** Добавляет позицию страницы в параметры запроса. */
|
|
1056
|
+
function withPageState(query, state) {
|
|
1057
|
+
return {
|
|
1058
|
+
...query,
|
|
1059
|
+
...state.cursor !== void 0 ? { cursor: state.cursor } : {},
|
|
1060
|
+
...state.page !== void 0 ? { page: state.page } : {},
|
|
1061
|
+
...state.offset !== void 0 ? { offset: state.offset } : {}
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
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(require_url.normalizeNotification)
|
|
1072
|
+
};
|
|
1073
|
+
});
|
|
1074
|
+
const NOTIFICATIONS_COUNT = require_url.defineBuiltInOperation("notifications.count", (body) => require_url.pickNumber(body, "count", 0));
|
|
1075
|
+
const markedCountOperation = (id) => require_url.defineBuiltInOperation(id, (body) => require_url.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 = require_url.defineBuiltInOperation("notifications.getSettings", readSettings);
|
|
1080
|
+
const NOTIFICATIONS_UPDATE_SETTINGS = require_url.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] = require_url.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/${require_redact.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
|
|
261
1201
|
});
|
|
262
1202
|
}
|
|
263
1203
|
/**
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
* Путь, параметры запроса и разбор ответа задаются один раз; `list` и `iterate`
|
|
267
|
-
* строятся из них.
|
|
1204
|
+
* Обновляет настройки уведомлений.
|
|
268
1205
|
*
|
|
269
|
-
*
|
|
270
|
-
* ```ts
|
|
271
|
-
* #feed = this.paginated<Post, FeedParams>({
|
|
272
|
-
* operationId: 'posts.list',
|
|
273
|
-
* path: () => '/api/posts',
|
|
274
|
-
* query: (p) => ({ tab: p.tab, limit: p.limit }),
|
|
275
|
-
* start: (p) => (p.cursor ? { cursor: p.cursor } : {}),
|
|
276
|
-
* read: (body) => readCursorPage<Post>(body, 'posts'),
|
|
277
|
-
* mode: PaginationMode.Cursor,
|
|
278
|
-
* });
|
|
279
|
-
* ```
|
|
1206
|
+
* Отправляются только изменяемые поля, в том же виде, в каком сервер их возвращает.
|
|
280
1207
|
*/
|
|
281
|
-
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
list: (params, options) => load(params, spec.start(params), options),
|
|
293
|
-
iterate: (params, options = {}) => {
|
|
294
|
-
const { maxPages, ...requestOptions } = options;
|
|
295
|
-
return this.paginate(spec.mode, (state) => load(params, state, requestOptions), {
|
|
296
|
-
...maxPages !== void 0 ? { maxPages } : {},
|
|
297
|
-
...requestOptions.signal !== void 0 ? { signal: requestOptions.signal } : {},
|
|
298
|
-
start: spec.start(params)
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
};
|
|
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
|
+
});
|
|
302
1219
|
}
|
|
303
1220
|
};
|
|
304
|
-
|
|
305
|
-
|
|
1221
|
+
//#endregion
|
|
1222
|
+
//#region src/domain/time.ts
|
|
1223
|
+
const NAIVE_STAMP = /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}(?:\.\d+)?)$/;
|
|
1224
|
+
/**
|
|
1225
|
+
* Приводит отметку времени без часового пояса к ISO-8601, считая её временем UTC.
|
|
1226
|
+
*
|
|
1227
|
+
* Строку другого вида возвращает нетронутой.
|
|
1228
|
+
*
|
|
1229
|
+
* @example
|
|
1230
|
+
* ```ts
|
|
1231
|
+
* utcStampToIso('2026-07-23 23:14:25'); // '2026-07-23T23:14:25Z'
|
|
1232
|
+
* utcStampToIso('2026-07-23T23:14:25Z'); // без изменений
|
|
1233
|
+
* ```
|
|
1234
|
+
*/
|
|
1235
|
+
function utcStampToIso(value) {
|
|
1236
|
+
const match = typeof value === "string" ? NAIVE_STAMP.exec(value) : null;
|
|
1237
|
+
if (!match) return value;
|
|
1238
|
+
const iso = `${match[1]}T${match[2]}Z`;
|
|
1239
|
+
return Number.isFinite(Date.parse(iso)) ? iso : value;
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Разбирает дату API в объект `Date`.
|
|
1243
|
+
*
|
|
1244
|
+
* @returns `null`, если строки нет или она не разбирается
|
|
1245
|
+
*
|
|
1246
|
+
* @example
|
|
1247
|
+
* ```ts
|
|
1248
|
+
* const created = toDate(post.createdAt);
|
|
1249
|
+
* ```
|
|
1250
|
+
*/
|
|
1251
|
+
function toDate(value) {
|
|
1252
|
+
if (!value) return null;
|
|
1253
|
+
const date = new Date(value);
|
|
1254
|
+
return Number.isFinite(date.getTime()) ? date : null;
|
|
1255
|
+
}
|
|
1256
|
+
//#endregion
|
|
1257
|
+
//#region src/resources/status.ts
|
|
1258
|
+
/** Приводит `last_checked` каждого сервиса к ISO. Остальное остаётся как прислал сервер. */
|
|
1259
|
+
function normalizeStatus(body) {
|
|
1260
|
+
if (!require_url.isRecord(body) || !Array.isArray(body.services)) return body;
|
|
306
1261
|
return {
|
|
307
|
-
...
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
1262
|
+
...body,
|
|
1263
|
+
services: body.services.map((service) => typeof service?.last_checked === "string" ? {
|
|
1264
|
+
...service,
|
|
1265
|
+
last_checked: utcStampToIso(service.last_checked)
|
|
1266
|
+
} : service)
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
/** API встроенного status feature. @internal */
|
|
1270
|
+
var StatusResource = class {
|
|
1271
|
+
#context;
|
|
1272
|
+
constructor(context) {
|
|
1273
|
+
this.#context = context;
|
|
1274
|
+
}
|
|
1275
|
+
get(options = {}) {
|
|
1276
|
+
return this.#context.request("get", {
|
|
1277
|
+
path: "/api/status",
|
|
1278
|
+
...options
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
/** Встроенный модуль страницы состояния платформы. @internal */
|
|
1283
|
+
function createStatusFeature() {
|
|
1284
|
+
return {
|
|
1285
|
+
name: "status",
|
|
1286
|
+
services: [{
|
|
1287
|
+
name: require_url.STATUS_SERVICE,
|
|
1288
|
+
baseUrl: require_url.DEFAULT_STATUS_BASE_URL,
|
|
1289
|
+
auth: false
|
|
1290
|
+
}],
|
|
1291
|
+
operations: { get: {
|
|
1292
|
+
method: "GET",
|
|
1293
|
+
retrySafety: require_url.RetrySafety.Safe,
|
|
1294
|
+
service: require_url.STATUS_SERVICE,
|
|
1295
|
+
read: (body) => normalizeStatus(body)
|
|
1296
|
+
} },
|
|
1297
|
+
setup: (context) => ({ api: new StatusResource(context) })
|
|
311
1298
|
};
|
|
312
1299
|
}
|
|
313
1300
|
//#endregion
|
|
@@ -470,6 +1457,13 @@ function resolveComment(input, allowReplyTo = false) {
|
|
|
470
1457
|
}
|
|
471
1458
|
//#endregion
|
|
472
1459
|
//#region src/resources/comments.ts
|
|
1460
|
+
const COMMENTS_REPLIES = pageOperation("comments.replies", (body) => readPagedPage(body, "replies"));
|
|
1461
|
+
const COMMENTS_REPLY = require_url.passthroughOperation("comments.reply");
|
|
1462
|
+
const COMMENTS_UPDATE = require_url.passthroughOperation("comments.update");
|
|
1463
|
+
const COMMENTS_RESTORE = require_url.passthroughOperation("comments.restore");
|
|
1464
|
+
const COMMENTS_LIKE = require_url.passthroughOperation("comments.like");
|
|
1465
|
+
const COMMENTS_UNLIKE = require_url.passthroughOperation("comments.unlike");
|
|
1466
|
+
const COMMENTS_REMOVE = require_url.voidOperation("comments.remove");
|
|
473
1467
|
/**
|
|
474
1468
|
* Комментарии и ответы на них.
|
|
475
1469
|
*
|
|
@@ -480,11 +1474,10 @@ var CommentsResource = class extends BaseResource {
|
|
|
480
1474
|
#uploadFiles;
|
|
481
1475
|
/** Ответы на комментарий: `/api/comments/{id}/replies`, постраничная пагинация. */
|
|
482
1476
|
#replies = this.paginated({
|
|
483
|
-
|
|
484
|
-
path: (p) => `/api/comments/${
|
|
1477
|
+
operation: COMMENTS_REPLIES,
|
|
1478
|
+
path: (p) => `/api/comments/${require_redact.encodePathSegment(p.commentId, "commentId")}/replies`,
|
|
485
1479
|
query: (p) => ({ limit: p.limit }),
|
|
486
1480
|
start: (p) => p.page !== void 0 ? { page: p.page } : {},
|
|
487
|
-
read: (body) => readPagedPage(body, "replies"),
|
|
488
1481
|
mode: PaginationMode.Page
|
|
489
1482
|
});
|
|
490
1483
|
constructor(http, deps) {
|
|
@@ -523,8 +1516,8 @@ var CommentsResource = class extends BaseResource {
|
|
|
523
1516
|
const existing = data.attachmentIds ?? [];
|
|
524
1517
|
const files = data.files ?? [];
|
|
525
1518
|
const attachmentIds = files.length > 0 ? [...existing, ...await this.#uploadFiles(files, options)] : existing;
|
|
526
|
-
return this.http.
|
|
527
|
-
path: `/api/comments/${
|
|
1519
|
+
return this.http.execute(COMMENTS_REPLY, {
|
|
1520
|
+
path: `/api/comments/${require_redact.encodePathSegment(commentId, "commentId")}/replies`,
|
|
528
1521
|
body: {
|
|
529
1522
|
content: data.content ?? "",
|
|
530
1523
|
attachmentIds,
|
|
@@ -535,176 +1528,42 @@ var CommentsResource = class extends BaseResource {
|
|
|
535
1528
|
}
|
|
536
1529
|
/** Редактирует текст комментария. */
|
|
537
1530
|
update(commentId, content, options = {}) {
|
|
538
|
-
return this.http.
|
|
539
|
-
path: `/api/comments/${
|
|
1531
|
+
return this.http.execute(COMMENTS_UPDATE, {
|
|
1532
|
+
path: `/api/comments/${require_redact.encodePathSegment(commentId, "commentId")}`,
|
|
540
1533
|
body: { content },
|
|
541
1534
|
...options
|
|
542
1535
|
});
|
|
543
1536
|
}
|
|
544
1537
|
/** Удаляет комментарий. Восстановить его можно через {@link restore}. */
|
|
545
1538
|
remove(commentId, options = {}) {
|
|
546
|
-
return this.
|
|
547
|
-
path: `/api/comments/${
|
|
1539
|
+
return this.voidOperation(COMMENTS_REMOVE, {
|
|
1540
|
+
path: `/api/comments/${require_redact.encodePathSegment(commentId, "commentId")}`,
|
|
548
1541
|
...options
|
|
549
1542
|
});
|
|
550
1543
|
}
|
|
551
1544
|
/** Восстанавливает удалённый комментарий. */
|
|
552
1545
|
restore(commentId, options = {}) {
|
|
553
|
-
return this.http.
|
|
554
|
-
path: `/api/comments/${
|
|
1546
|
+
return this.http.execute(COMMENTS_RESTORE, {
|
|
1547
|
+
path: `/api/comments/${require_redact.encodePathSegment(commentId, "commentId")}/restore`,
|
|
555
1548
|
...options
|
|
556
1549
|
});
|
|
557
1550
|
}
|
|
558
1551
|
/** Ставит реакцию на комментарий. */
|
|
559
1552
|
like(commentId, options = {}) {
|
|
560
|
-
return this.http.
|
|
561
|
-
path: `/api/comments/${
|
|
1553
|
+
return this.http.execute(COMMENTS_LIKE, {
|
|
1554
|
+
path: `/api/comments/${require_redact.encodePathSegment(commentId, "commentId")}/like`,
|
|
562
1555
|
...options
|
|
563
1556
|
});
|
|
564
1557
|
}
|
|
565
1558
|
/** Убирает реакцию с комментария. */
|
|
566
1559
|
unlike(commentId, options = {}) {
|
|
567
|
-
return this.http.
|
|
568
|
-
path: `/api/comments/${
|
|
1560
|
+
return this.http.execute(COMMENTS_UNLIKE, {
|
|
1561
|
+
path: `/api/comments/${require_redact.encodePathSegment(commentId, "commentId")}/like`,
|
|
569
1562
|
...options
|
|
570
1563
|
});
|
|
571
1564
|
}
|
|
572
1565
|
};
|
|
573
1566
|
//#endregion
|
|
574
|
-
//#region src/core/attachments/url-source.ts
|
|
575
|
-
/** Убирает параметры MIME и приводит его к форме для сравнения. */
|
|
576
|
-
function normalizeMimeType(contentType) {
|
|
577
|
-
return contentType?.split(";", 1)[0]?.trim().toLowerCase() || void 0;
|
|
578
|
-
}
|
|
579
|
-
/** Достаёт имя файла из пути URL. */
|
|
580
|
-
function filenameFromUrl(url) {
|
|
581
|
-
const last = url.pathname.split("/").pop();
|
|
582
|
-
if (!last) return void 0;
|
|
583
|
-
try {
|
|
584
|
-
return decodeURIComponent(last) || void 0;
|
|
585
|
-
} catch {
|
|
586
|
-
return last;
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
/** Проверяет объявленный размер и возвращает его, если заголовок корректен. */
|
|
590
|
-
function declaredSize(response, maxBytes, url) {
|
|
591
|
-
const header = response.headers.get("content-length");
|
|
592
|
-
if (header === null) return void 0;
|
|
593
|
-
const size = Number(header);
|
|
594
|
-
if (!Number.isFinite(size) || size < 0 || !Number.isInteger(size)) return void 0;
|
|
595
|
-
if (maxBytes !== void 0 && size > maxBytes) throw require_options.fileTooLarge(url, maxBytes, size);
|
|
596
|
-
return size;
|
|
597
|
-
}
|
|
598
|
-
/** Получает HTTP-ответ источника и проверяет его статус. */
|
|
599
|
-
async function fetchFile(target, options, context) {
|
|
600
|
-
let requested;
|
|
601
|
-
try {
|
|
602
|
-
requested = new URL(target);
|
|
603
|
-
} catch {
|
|
604
|
-
throw new require_errors.ItdConfigError(`«${target}» не разбирается как адрес`);
|
|
605
|
-
}
|
|
606
|
-
if (requested.protocol !== "http:" && requested.protocol !== "https:") throw new require_errors.ItdConfigError(`вложение по адресу поддерживает только http и https, получено: ${requested.protocol}`);
|
|
607
|
-
let response;
|
|
608
|
-
try {
|
|
609
|
-
response = await context.fetch(requested, { ...context.signal ? { signal: context.signal } : {} });
|
|
610
|
-
} catch (error) {
|
|
611
|
-
if (context.signal?.aborted || error instanceof Error && error.name === "AbortError") throw error;
|
|
612
|
-
throw new require_errors.ItdFileError(`не удалось получить файл по адресу ${requested.href}`, {
|
|
613
|
-
reason: require_errors.ItdFileErrorReason.Network,
|
|
614
|
-
url: requested.href,
|
|
615
|
-
retryable: true,
|
|
616
|
-
cause: error
|
|
617
|
-
});
|
|
618
|
-
}
|
|
619
|
-
const finalUrl = response.url ? new URL(response.url) : requested;
|
|
620
|
-
if (!response.ok) {
|
|
621
|
-
await response.body?.cancel().catch(() => {});
|
|
622
|
-
throw new require_errors.ItdFileError(`источник ${finalUrl.href} ответил статусом ${response.status}`, {
|
|
623
|
-
reason: require_errors.ItdFileErrorReason.Http,
|
|
624
|
-
url: finalUrl.href,
|
|
625
|
-
status: response.status,
|
|
626
|
-
retryable: response.status === 408 || response.status === 429 || response.status >= 500
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
const { maxBytes } = require_options.resolveFileStreamOptions(options, require_options.DEFAULT_URL_FILE_MAX_BYTES);
|
|
630
|
-
let size;
|
|
631
|
-
try {
|
|
632
|
-
size = declaredSize(response, maxBytes, finalUrl.href);
|
|
633
|
-
} catch (error) {
|
|
634
|
-
await response.body?.cancel().catch(() => {});
|
|
635
|
-
throw error;
|
|
636
|
-
}
|
|
637
|
-
return {
|
|
638
|
-
response,
|
|
639
|
-
url: finalUrl,
|
|
640
|
-
size
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
/** Читает ответ с контролем размера до создания итогового `Blob`. */
|
|
644
|
-
async function responseBlob(response, url, maxBytes, streamBufferBytes, signal) {
|
|
645
|
-
if (!response.body) {
|
|
646
|
-
const blob = await response.blob();
|
|
647
|
-
if (maxBytes !== void 0 && blob.size > maxBytes) throw require_options.fileTooLarge(url, maxBytes, blob.size);
|
|
648
|
-
return blob;
|
|
649
|
-
}
|
|
650
|
-
const chunks = [];
|
|
651
|
-
const reader = require_options.boundedFileStream(response.body, {
|
|
652
|
-
...maxBytes !== void 0 ? { maxBytes } : {},
|
|
653
|
-
streamBufferBytes,
|
|
654
|
-
...signal ? { signal } : {},
|
|
655
|
-
url,
|
|
656
|
-
retryableRead: true
|
|
657
|
-
}).getReader();
|
|
658
|
-
try {
|
|
659
|
-
for (;;) {
|
|
660
|
-
const next = await reader.read();
|
|
661
|
-
if (next.done) break;
|
|
662
|
-
chunks.push(next.value);
|
|
663
|
-
}
|
|
664
|
-
} finally {
|
|
665
|
-
reader.releaseLock();
|
|
666
|
-
}
|
|
667
|
-
return new Blob(chunks.map((chunk) => Uint8Array.from(chunk).buffer));
|
|
668
|
-
}
|
|
669
|
-
/** Скачивает файл целиком с ограничением размера. @internal */
|
|
670
|
-
async function downloadFile(target, options, context) {
|
|
671
|
-
const resolved = require_options.resolveFileStreamOptions(options, require_options.DEFAULT_URL_FILE_MAX_BYTES);
|
|
672
|
-
const { response, url } = await fetchFile(target, options, context);
|
|
673
|
-
const blob = await responseBlob(response, url.href, resolved.maxBytes, resolved.streamBufferBytes, context.signal);
|
|
674
|
-
const contentType = normalizeMimeType(options.contentType ?? response.headers.get("content-type") ?? void 0);
|
|
675
|
-
const filename = options.filename ?? filenameFromUrl(url);
|
|
676
|
-
return {
|
|
677
|
-
file: new Blob([blob], { type: contentType ?? "" }),
|
|
678
|
-
...filename ? { filename } : {},
|
|
679
|
-
...contentType ? { contentType } : {}
|
|
680
|
-
};
|
|
681
|
-
}
|
|
682
|
-
/** Открывает HTTP-ответ как ограниченный поток. @internal */
|
|
683
|
-
async function openUrlFile(target, options, context) {
|
|
684
|
-
const resolved = require_options.resolveFileStreamOptions(options, require_options.DEFAULT_URL_FILE_MAX_BYTES);
|
|
685
|
-
const { response, url, size } = await fetchFile(target, options, context);
|
|
686
|
-
if (!response.body) throw new require_errors.ItdFileError(`источник ${url.href} не предоставил потоковое тело`, {
|
|
687
|
-
reason: require_errors.ItdFileErrorReason.StreamUnavailable,
|
|
688
|
-
url: url.href
|
|
689
|
-
});
|
|
690
|
-
const stream = require_options.boundedFileStream(response.body, {
|
|
691
|
-
...resolved.maxBytes !== void 0 ? { maxBytes: resolved.maxBytes } : {},
|
|
692
|
-
streamBufferBytes: resolved.streamBufferBytes,
|
|
693
|
-
...context.signal ? { signal: context.signal } : {},
|
|
694
|
-
url: url.href,
|
|
695
|
-
retryableRead: true
|
|
696
|
-
});
|
|
697
|
-
const filename = options.filename ?? filenameFromUrl(url);
|
|
698
|
-
const contentType = normalizeMimeType(options.contentType ?? response.headers.get("content-type") ?? void 0);
|
|
699
|
-
return {
|
|
700
|
-
stream,
|
|
701
|
-
...filename ? { filename } : {},
|
|
702
|
-
...contentType ? { contentType } : {},
|
|
703
|
-
...size !== void 0 ? { size } : {},
|
|
704
|
-
close: () => stream.cancel().catch(() => {})
|
|
705
|
-
};
|
|
706
|
-
}
|
|
707
|
-
//#endregion
|
|
708
1567
|
//#region src/core/multipart.ts
|
|
709
1568
|
const encoder = new TextEncoder();
|
|
710
1569
|
/** Убирает возможность внедрить дополнительный multipart-заголовок через имя файла. */
|
|
@@ -841,12 +1700,17 @@ function assertAllowedMime(mimeType, filename) {
|
|
|
841
1700
|
if (!mimeType) throw new require_errors.ItdConfigError(`Не удалось определить тип файла${filename ? ` «${filename}»` : ""}. Укажите его явно: { file, filename, contentType }.`);
|
|
842
1701
|
if (!isAllowedMime(mimeType)) throw new require_errors.ItdConfigError(`Тип «${mimeType}» не поддерживается загрузкой. Допустимые: ${ALLOWED_MIME_TYPES.join(", ")}.`);
|
|
843
1702
|
}
|
|
1703
|
+
//#endregion
|
|
1704
|
+
//#region src/resources/files.ts
|
|
1705
|
+
const FILES_UPLOAD = require_url.passthroughOperation("files.upload");
|
|
1706
|
+
const FILES_GET = require_url.passthroughOperation("files.get");
|
|
1707
|
+
const FILES_REMOVE = require_url.voidOperation("files.remove");
|
|
844
1708
|
/** Файлы и медиа. */
|
|
845
1709
|
var FilesResource = class extends BaseResource {
|
|
846
|
-
#
|
|
1710
|
+
#files;
|
|
847
1711
|
constructor(http, deps) {
|
|
848
1712
|
super(http);
|
|
849
|
-
this.#
|
|
1713
|
+
this.#files = deps.files;
|
|
850
1714
|
}
|
|
851
1715
|
/**
|
|
852
1716
|
* Загружает файл и возвращает его идентификатор.
|
|
@@ -856,7 +1720,7 @@ var FilesResource = class extends BaseResource {
|
|
|
856
1720
|
*/
|
|
857
1721
|
upload(input, uploadOptions = {}, requestOptions = {}) {
|
|
858
1722
|
const bodyFactory = this.#createBodyFactory(input, uploadOptions);
|
|
859
|
-
return this.http.
|
|
1723
|
+
return this.http.execute(FILES_UPLOAD, {
|
|
860
1724
|
path: "/api/files/upload",
|
|
861
1725
|
bodyFactory,
|
|
862
1726
|
...requestOptions,
|
|
@@ -875,15 +1739,15 @@ var FilesResource = class extends BaseResource {
|
|
|
875
1739
|
* Для ещё не прикреплённого файла сервер может ответить `404`.
|
|
876
1740
|
*/
|
|
877
1741
|
get(fileId, options = {}) {
|
|
878
|
-
return this.http.
|
|
879
|
-
path: `/api/files/${
|
|
1742
|
+
return this.http.execute(FILES_GET, {
|
|
1743
|
+
path: `/api/files/${require_redact.encodePathSegment(fileId, "fileId")}`,
|
|
880
1744
|
...options
|
|
881
1745
|
});
|
|
882
1746
|
}
|
|
883
1747
|
/** Удаляет загруженный файл. */
|
|
884
1748
|
remove(fileId, options = {}) {
|
|
885
|
-
return this.
|
|
886
|
-
path: `/api/files/${
|
|
1749
|
+
return this.voidOperation(FILES_REMOVE, {
|
|
1750
|
+
path: `/api/files/${require_redact.encodePathSegment(fileId, "fileId")}`,
|
|
887
1751
|
...options
|
|
888
1752
|
});
|
|
889
1753
|
}
|
|
@@ -892,140 +1756,70 @@ var FilesResource = class extends BaseResource {
|
|
|
892
1756
|
* заново, поэтому каждая транспортная попытка получает непрочитанное тело.
|
|
893
1757
|
*/
|
|
894
1758
|
#createBodyFactory(input, options) {
|
|
895
|
-
const streamMode = this.#
|
|
1759
|
+
const streamMode = this.#files.mode(input) === require_options.FileTransferMode.Stream;
|
|
896
1760
|
let buffered;
|
|
897
1761
|
return async ({ signal, attempt }) => {
|
|
898
|
-
|
|
899
|
-
|
|
1762
|
+
let prepared;
|
|
1763
|
+
if (streamMode) prepared = await this.#files.resolve(input, options, {
|
|
900
1764
|
signal,
|
|
901
1765
|
attempt
|
|
902
|
-
};
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
});
|
|
911
|
-
prepared = await buffered;
|
|
912
|
-
}
|
|
913
|
-
} catch (error) {
|
|
914
|
-
if (error instanceof require_errors.ItdError || signal.aborted) throw error;
|
|
915
|
-
throw new require_errors.ItdFileError("не удалось получить содержимое вложения", {
|
|
916
|
-
reason: require_errors.ItdFileErrorReason.Read,
|
|
917
|
-
retryable: true,
|
|
918
|
-
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;
|
|
919
1774
|
});
|
|
1775
|
+
prepared = await buffered;
|
|
920
1776
|
}
|
|
921
|
-
if (prepared.mode ===
|
|
1777
|
+
if (prepared.mode === require_options.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 });
|
|
922
1782
|
const form = new FormData();
|
|
923
|
-
form.set("file",
|
|
1783
|
+
form.set("file", blob, filename);
|
|
924
1784
|
return { body: form };
|
|
925
1785
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
const contentType = normalizeMimeType(options.contentType ?? content.contentType) ?? normalizeMimeType(require_url.isBlob(content.file) ? content.file.type : void 0) ?? mimeFromFilename(filename);
|
|
949
|
-
if (options.validateMime !== false) assertAllowedMime(contentType, filename);
|
|
950
|
-
const blob = require_url.isBlob(content.file) && (!contentType || content.file.type === contentType) ? content.file : new Blob([content.file], { type: contentType ?? "" });
|
|
951
|
-
const limits = require_options.resolveFileStreamOptions(options);
|
|
952
|
-
if (limits.maxBytes !== void 0 && blob.size > limits.maxBytes) throw new require_errors.ItdFileError(`файл больше предела в ${limits.maxBytes} байт: ${blob.size}`, {
|
|
953
|
-
reason: require_errors.ItdFileErrorReason.TooLarge,
|
|
954
|
-
limit: limits.maxBytes,
|
|
955
|
-
actual: blob.size
|
|
956
|
-
});
|
|
957
|
-
return {
|
|
958
|
-
mode: "buffer",
|
|
959
|
-
blob,
|
|
960
|
-
filename
|
|
961
|
-
};
|
|
962
|
-
}
|
|
963
|
-
/** Открывает и проверяет потоковый источник. */
|
|
964
|
-
async #prepareStream(input, options, context) {
|
|
965
|
-
let opened;
|
|
966
|
-
if (typeof input === "object" && input !== null && "open" in input) opened = await input.open(context);
|
|
967
|
-
else if (typeof input === "object" && input !== null && "url" in input) {
|
|
968
|
-
const { url, ...urlOptions } = input;
|
|
969
|
-
opened = await openUrlFile(url, urlOptions, context);
|
|
970
|
-
} else throw new require_errors.ItdConfigError("потоковое вложение должно иметь форму { open } или { url, mode }");
|
|
971
|
-
if (!opened || typeof ReadableStream === "undefined" || !(opened.stream instanceof ReadableStream)) {
|
|
972
|
-
await opened?.close?.();
|
|
973
|
-
throw new require_errors.ItdConfigError("потоковый источник должен вернуть { stream: ReadableStream }");
|
|
974
|
-
}
|
|
975
|
-
try {
|
|
976
|
-
const filename = options.filename ?? opened.filename ?? this.#nameFromMime(options.contentType ?? opened.contentType);
|
|
977
|
-
const contentType = normalizeMimeType(options.contentType ?? opened.contentType) ?? mimeFromFilename(filename);
|
|
978
|
-
if (options.validateMime !== false) assertAllowedMime(contentType, filename);
|
|
979
|
-
const limits = require_options.resolveFileStreamOptions({
|
|
980
|
-
mode: require_options.FileTransferMode.Stream,
|
|
981
|
-
...options.maxBytes !== void 0 ? { maxBytes: options.maxBytes } : {},
|
|
982
|
-
...options.streamBufferBytes !== void 0 ? { streamBufferBytes: options.streamBufferBytes } : {}
|
|
983
|
-
});
|
|
984
|
-
if (limits.maxBytes !== void 0 && opened.size !== void 0 && opened.size > limits.maxBytes) throw new require_errors.ItdFileError(`файл больше предела в ${limits.maxBytes} байт: ${opened.size}`, {
|
|
985
|
-
reason: require_errors.ItdFileErrorReason.TooLarge,
|
|
986
|
-
limit: limits.maxBytes,
|
|
987
|
-
actual: opened.size
|
|
988
|
-
});
|
|
989
|
-
return {
|
|
990
|
-
mode: "stream",
|
|
991
|
-
stream: require_options.isBoundedFileStream(opened.stream) && options.maxBytes === void 0 && options.streamBufferBytes === void 0 ? opened.stream : require_options.boundedFileStream(opened.stream, {
|
|
992
|
-
...limits.maxBytes !== void 0 ? { maxBytes: limits.maxBytes } : {},
|
|
993
|
-
streamBufferBytes: limits.streamBufferBytes,
|
|
994
|
-
signal: context.signal
|
|
995
|
-
}),
|
|
996
|
-
filename,
|
|
997
|
-
contentType: contentType ?? "",
|
|
998
|
-
...opened.close ? { close: opened.close } : {}
|
|
999
|
-
};
|
|
1000
|
-
} catch (error) {
|
|
1001
|
-
await opened.close?.();
|
|
1002
|
-
throw error;
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
/** Разрешает только буферные формы. */
|
|
1006
|
-
#resolveBuffer(input, context) {
|
|
1007
|
-
if (input instanceof ArrayBuffer || ArrayBuffer.isView(input) || require_url.isBlob(input)) return {
|
|
1008
|
-
file: input,
|
|
1009
|
-
...require_url.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
|
+
}
|
|
1010
1808
|
};
|
|
1011
|
-
if (typeof input !== "object" || input === null) throw new require_errors.ItdConfigError(`вложение задано значением типа ${typeof input}; ожидается бинарное значение или объект { file }, { url }, { load }, { open }. Для файла на диске используйте fromPath('./photo.jpg') из itd-api/node`);
|
|
1012
|
-
if ("load" in input) return input.load(context);
|
|
1013
|
-
if ("url" in input) {
|
|
1014
|
-
const { url, ...urlOptions } = input;
|
|
1015
|
-
return downloadFile(url, urlOptions, context);
|
|
1016
|
-
}
|
|
1017
|
-
if ("file" in input) return input;
|
|
1018
|
-
if ("open" in input) throw new require_errors.ItdConfigError("потоковый источник нельзя использовать как буферный");
|
|
1019
|
-
throw new require_errors.ItdConfigError("вложение не распознано: ожидается { file }, { url }, { load } или { open }");
|
|
1020
1809
|
}
|
|
1021
1810
|
/** Подбирает непустое имя, обязательное для multipart. */
|
|
1022
|
-
#
|
|
1023
|
-
|
|
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];
|
|
1024
1815
|
return extension ? `file.${extension}` : "file";
|
|
1025
1816
|
}
|
|
1026
1817
|
};
|
|
1027
1818
|
//#endregion
|
|
1028
1819
|
//#region src/resources/hashtags.ts
|
|
1820
|
+
const HASHTAG_POSTS = pageOperation("hashtags.posts", (body) => readCursorPage(body, "posts"));
|
|
1821
|
+
const HASHTAGS_SEARCH = require_url.defineBuiltInOperation("hashtags.search", (body) => require_url.pickArray(body, "hashtags"));
|
|
1822
|
+
const HASHTAGS_TRENDING = require_url.defineBuiltInOperation("hashtags.trending", (body) => require_url.pickArray(body, "hashtags"));
|
|
1029
1823
|
/**
|
|
1030
1824
|
* Хэштеги.
|
|
1031
1825
|
*
|
|
@@ -1034,11 +1828,10 @@ var FilesResource = class extends BaseResource {
|
|
|
1034
1828
|
var HashtagsResource = class extends BaseResource {
|
|
1035
1829
|
/** Посты по хэштегу: `/api/hashtags/{tag}/posts`, курсорная пагинация. */
|
|
1036
1830
|
#posts = this.paginated({
|
|
1037
|
-
|
|
1038
|
-
path: (p) => `/api/hashtags/${
|
|
1831
|
+
operation: HASHTAG_POSTS,
|
|
1832
|
+
path: (p) => `/api/hashtags/${require_redact.encodePathSegment(p.tag, "tag")}/posts`,
|
|
1039
1833
|
query: (p) => ({ limit: p.limit }),
|
|
1040
1834
|
start: (p) => p.cursor ? { cursor: p.cursor } : {},
|
|
1041
|
-
read: (body) => readCursorPage(body, "posts"),
|
|
1042
1835
|
mode: PaginationMode.Cursor
|
|
1043
1836
|
});
|
|
1044
1837
|
/**
|
|
@@ -1046,8 +1839,8 @@ var HashtagsResource = class extends BaseResource {
|
|
|
1046
1839
|
*
|
|
1047
1840
|
* Без строки запроса возвращает общий список.
|
|
1048
1841
|
*/
|
|
1049
|
-
|
|
1050
|
-
|
|
1842
|
+
search(query, params = {}, options = {}) {
|
|
1843
|
+
return this.http.execute(HASHTAGS_SEARCH, {
|
|
1051
1844
|
path: "/api/hashtags",
|
|
1052
1845
|
query: {
|
|
1053
1846
|
q: query,
|
|
@@ -1055,16 +1848,14 @@ var HashtagsResource = class extends BaseResource {
|
|
|
1055
1848
|
},
|
|
1056
1849
|
...options
|
|
1057
1850
|
});
|
|
1058
|
-
return require_url.pickArray(body, "hashtags");
|
|
1059
1851
|
}
|
|
1060
1852
|
/** Загружает трендовые хэштеги. */
|
|
1061
|
-
|
|
1062
|
-
|
|
1853
|
+
trending(params = {}, options = {}) {
|
|
1854
|
+
return this.http.execute(HASHTAGS_TRENDING, {
|
|
1063
1855
|
path: "/api/hashtags/trending",
|
|
1064
1856
|
query: { limit: params.limit },
|
|
1065
1857
|
...options
|
|
1066
1858
|
});
|
|
1067
|
-
return require_url.pickArray(body, "hashtags");
|
|
1068
1859
|
}
|
|
1069
1860
|
/**
|
|
1070
1861
|
* Загружает страницу постов по хэштегу.
|
|
@@ -1087,212 +1878,23 @@ var HashtagsResource = class extends BaseResource {
|
|
|
1087
1878
|
}
|
|
1088
1879
|
};
|
|
1089
1880
|
//#endregion
|
|
1090
|
-
//#region src/resources/notifications.ts
|
|
1091
|
-
const NOTIFICATION_SETTING_KEYS = [
|
|
1092
|
-
"enabled",
|
|
1093
|
-
"sound",
|
|
1094
|
-
"follows",
|
|
1095
|
-
"wallPosts",
|
|
1096
|
-
"likes",
|
|
1097
|
-
"comments",
|
|
1098
|
-
"mentions"
|
|
1099
|
-
];
|
|
1100
|
-
/**
|
|
1101
|
-
* Сколько идентификаторов уходит в одном запросе на отметку прочтения.
|
|
1102
|
-
*
|
|
1103
|
-
* Столько же отправляет сайт итд.com — значит на сервере, скорее всего, есть ограничение.
|
|
1104
|
-
*/
|
|
1105
|
-
const READ_BATCH_SIZE = 20;
|
|
1106
|
-
/**
|
|
1107
|
-
* Читает настройки уведомлений.
|
|
1108
|
-
*
|
|
1109
|
-
* Сервер отдаёт плоский объект: `enabled`, `sound`, `follows`, `wallPosts`, `likes`,
|
|
1110
|
-
* `comments`, `mentions`. Отсутствующая настройка считается включённой — так же
|
|
1111
|
-
* ведёт себя сайт итд.com.
|
|
1112
|
-
*/
|
|
1113
|
-
function readSettings(body) {
|
|
1114
|
-
const settings = {};
|
|
1115
|
-
for (const key of NOTIFICATION_SETTING_KEYS) settings[key] = require_url.pickBoolean(body, key, true);
|
|
1116
|
-
return settings;
|
|
1117
|
-
}
|
|
1118
|
-
/**
|
|
1119
|
-
* Уведомления: список, счётчик, отметки о прочтении, настройки.
|
|
1120
|
-
*
|
|
1121
|
-
* Доступна как `itd.notifications`. Все уведомления приведены к единой форме, поэтому
|
|
1122
|
-
* объекты отсюда и из потока событий можно складывать в один список.
|
|
1123
|
-
*/
|
|
1124
|
-
var NotificationsResource = class extends BaseResource {
|
|
1125
|
-
/** Уведомления: `/api/notifications/`, пагинация по смещению. */
|
|
1126
|
-
#list = this.paginated({
|
|
1127
|
-
operationId: "notifications.list",
|
|
1128
|
-
path: () => "/api/notifications/",
|
|
1129
|
-
query: (p) => ({ limit: p.limit }),
|
|
1130
|
-
start: (p) => ({ offset: p.offset ?? 0 }),
|
|
1131
|
-
read: (body, state) => {
|
|
1132
|
-
const page = readOffsetPage(body, "notifications", state.offset ?? 0);
|
|
1133
|
-
return {
|
|
1134
|
-
...page,
|
|
1135
|
-
items: page.items.map(require_url.normalizeNotification)
|
|
1136
|
-
};
|
|
1137
|
-
},
|
|
1138
|
-
mode: PaginationMode.Offset
|
|
1139
|
-
});
|
|
1140
|
-
/**
|
|
1141
|
-
* Загружает страницу уведомлений.
|
|
1142
|
-
*
|
|
1143
|
-
* Пагинация здесь основана на смещении.
|
|
1144
|
-
*
|
|
1145
|
-
* @example
|
|
1146
|
-
* ```ts
|
|
1147
|
-
* const page = await itd.notifications.list({ limit: 20 });
|
|
1148
|
-
* const next = await itd.notifications.list({ limit: 20, offset: page.nextOffset });
|
|
1149
|
-
* ```
|
|
1150
|
-
*/
|
|
1151
|
-
list(params = {}, options = {}) {
|
|
1152
|
-
return this.#list.list(params, options);
|
|
1153
|
-
}
|
|
1154
|
-
/**
|
|
1155
|
-
* Перебирает уведомления.
|
|
1156
|
-
*
|
|
1157
|
-
* @example
|
|
1158
|
-
* ```ts
|
|
1159
|
-
* for await (const notification of itd.notifications.iterate()) {
|
|
1160
|
-
* console.log(formatNotificationText(notification));
|
|
1161
|
-
* }
|
|
1162
|
-
* ```
|
|
1163
|
-
*/
|
|
1164
|
-
iterate(params = {}, options = {}) {
|
|
1165
|
-
return this.#list.iterate(params, options);
|
|
1166
|
-
}
|
|
1167
|
-
/** Загружает число непрочитанных уведомлений. */
|
|
1168
|
-
async count(options = {}) {
|
|
1169
|
-
const body = await this.http.operation("notifications.count", {
|
|
1170
|
-
path: "/api/notifications/count",
|
|
1171
|
-
...options
|
|
1172
|
-
});
|
|
1173
|
-
return require_url.pickNumber(body, "count", 0);
|
|
1174
|
-
}
|
|
1175
|
-
/**
|
|
1176
|
-
* Отмечает уведомление прочитанным.
|
|
1177
|
-
*
|
|
1178
|
-
* @returns сколько записей отметил сервер
|
|
1179
|
-
*/
|
|
1180
|
-
async markRead(notificationId, options = {}) {
|
|
1181
|
-
const body = await this.http.operation("notifications.markRead", {
|
|
1182
|
-
path: `/api/notifications/${require_cookies.encodePathSegment(notificationId, "notificationId")}/read`,
|
|
1183
|
-
...options
|
|
1184
|
-
});
|
|
1185
|
-
return require_url.pickNumber(body, "markedCount", 0);
|
|
1186
|
-
}
|
|
1187
|
-
/**
|
|
1188
|
-
* Отмечает прочитанными сразу несколько уведомлений.
|
|
1189
|
-
*
|
|
1190
|
-
* Список автоматически режется на части по 20 идентификаторов — столько же отправляет
|
|
1191
|
-
* сайт итд.com, поэтому на сервере вероятен предел. Части уходят последовательно,
|
|
1192
|
-
* результат суммируется.
|
|
1193
|
-
*
|
|
1194
|
-
* @returns сколько записей отметил сервер суммарно
|
|
1195
|
-
*/
|
|
1196
|
-
async markReadBatch(ids, options = {}) {
|
|
1197
|
-
let marked = 0;
|
|
1198
|
-
for (let index = 0; index < ids.length; index += READ_BATCH_SIZE) {
|
|
1199
|
-
const chunk = ids.slice(index, index + READ_BATCH_SIZE);
|
|
1200
|
-
const body = await this.http.operation("notifications.markReadBatch", {
|
|
1201
|
-
path: "/api/notifications/read-batch",
|
|
1202
|
-
body: { ids: chunk },
|
|
1203
|
-
...options
|
|
1204
|
-
});
|
|
1205
|
-
marked += require_url.pickNumber(body, "markedCount", 0);
|
|
1206
|
-
}
|
|
1207
|
-
return marked;
|
|
1208
|
-
}
|
|
1209
|
-
/** Отмечает прочитанными все уведомления. */
|
|
1210
|
-
async markAllRead(options = {}) {
|
|
1211
|
-
const body = await this.http.operation("notifications.markAllRead", {
|
|
1212
|
-
path: "/api/notifications/read-all",
|
|
1213
|
-
...options
|
|
1214
|
-
});
|
|
1215
|
-
return require_url.pickNumber(body, "markedCount", 0);
|
|
1216
|
-
}
|
|
1217
|
-
/** Загружает настройки уведомлений. */
|
|
1218
|
-
async getSettings(options = {}) {
|
|
1219
|
-
return readSettings(await this.http.operation("notifications.getSettings", {
|
|
1220
|
-
path: "/api/notifications/settings",
|
|
1221
|
-
...options
|
|
1222
|
-
}));
|
|
1223
|
-
}
|
|
1224
|
-
/**
|
|
1225
|
-
* Обновляет настройки уведомлений.
|
|
1226
|
-
*
|
|
1227
|
-
* Отправляются только изменяемые поля, в том же виде, в каком сервер их возвращает.
|
|
1228
|
-
*/
|
|
1229
|
-
async updateSettings(input, options = {}) {
|
|
1230
|
-
const payload = {};
|
|
1231
|
-
for (const key of NOTIFICATION_SETTING_KEYS) {
|
|
1232
|
-
const value = input[key];
|
|
1233
|
-
if (value !== void 0) payload[key] = value;
|
|
1234
|
-
}
|
|
1235
|
-
return readSettings(await this.http.operation("notifications.updateSettings", {
|
|
1236
|
-
path: "/api/notifications/settings",
|
|
1237
|
-
body: payload,
|
|
1238
|
-
...options
|
|
1239
|
-
}));
|
|
1240
|
-
}
|
|
1241
|
-
};
|
|
1242
|
-
//#endregion
|
|
1243
|
-
//#region src/domain/time.ts
|
|
1244
|
-
const NAIVE_STAMP = /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}(?:\.\d+)?)$/;
|
|
1245
|
-
/**
|
|
1246
|
-
* Приводит отметку времени без часового пояса к ISO-8601, считая её временем UTC.
|
|
1247
|
-
*
|
|
1248
|
-
* Строку другого вида возвращает нетронутой.
|
|
1249
|
-
*
|
|
1250
|
-
* @example
|
|
1251
|
-
* ```ts
|
|
1252
|
-
* utcStampToIso('2026-07-23 23:14:25'); // '2026-07-23T23:14:25Z'
|
|
1253
|
-
* utcStampToIso('2026-07-23T23:14:25Z'); // без изменений
|
|
1254
|
-
* ```
|
|
1255
|
-
*/
|
|
1256
|
-
function utcStampToIso(value) {
|
|
1257
|
-
const match = typeof value === "string" ? NAIVE_STAMP.exec(value) : null;
|
|
1258
|
-
if (!match) return value;
|
|
1259
|
-
const iso = `${match[1]}T${match[2]}Z`;
|
|
1260
|
-
return Number.isFinite(Date.parse(iso)) ? iso : value;
|
|
1261
|
-
}
|
|
1262
|
-
/**
|
|
1263
|
-
* Разбирает дату API в объект `Date`.
|
|
1264
|
-
*
|
|
1265
|
-
* @returns `null`, если строки нет или она не разбирается
|
|
1266
|
-
*
|
|
1267
|
-
* @example
|
|
1268
|
-
* ```ts
|
|
1269
|
-
* const created = toDate(post.createdAt);
|
|
1270
|
-
* ```
|
|
1271
|
-
*/
|
|
1272
|
-
function toDate(value) {
|
|
1273
|
-
if (!value) return null;
|
|
1274
|
-
const date = new Date(value);
|
|
1275
|
-
return Number.isFinite(date.getTime()) ? date : null;
|
|
1276
|
-
}
|
|
1277
|
-
//#endregion
|
|
1278
1881
|
//#region src/resources/platform.ts
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
...body,
|
|
1284
|
-
services: body.services.map((service) => typeof service?.last_checked === "string" ? {
|
|
1285
|
-
...service,
|
|
1286
|
-
last_checked: utcStampToIso(service.last_checked)
|
|
1287
|
-
} : service)
|
|
1288
|
-
};
|
|
1289
|
-
}
|
|
1882
|
+
const PLATFORM_CHANGELOG = require_url.defineBuiltInOperation("platform.changelog", (body) => Array.isArray(body) ? body : []);
|
|
1883
|
+
const PLATFORM_ANNOUNCEMENTS = require_url.defineBuiltInOperation("platform.announcements", (body) => require_url.pickArray(body, "announcements"));
|
|
1884
|
+
const PLATFORM_VERSION = require_url.passthroughOperation("platform.version");
|
|
1885
|
+
const PLATFORM_PORTAL = require_url.passthroughOperation("platform.portal");
|
|
1290
1886
|
/**
|
|
1291
1887
|
* Сведения о платформе: версии приложений, изменения, анонсы, баннер события.
|
|
1292
1888
|
*
|
|
1293
1889
|
* Доступна как `itd.platform`.
|
|
1294
1890
|
*/
|
|
1295
1891
|
var PlatformResource = class extends BaseResource {
|
|
1892
|
+
#status;
|
|
1893
|
+
/** @internal */
|
|
1894
|
+
constructor(http, status) {
|
|
1895
|
+
super(http);
|
|
1896
|
+
this.#status = status;
|
|
1897
|
+
}
|
|
1296
1898
|
/**
|
|
1297
1899
|
* Загружает минимальные и актуальные версии клиентских приложений.
|
|
1298
1900
|
*
|
|
@@ -1305,31 +1907,29 @@ var PlatformResource = class extends BaseResource {
|
|
|
1305
1907
|
* ```
|
|
1306
1908
|
*/
|
|
1307
1909
|
version(options = {}) {
|
|
1308
|
-
return this.http.
|
|
1910
|
+
return this.http.execute(PLATFORM_VERSION, {
|
|
1309
1911
|
path: "/api/platform/version",
|
|
1310
1912
|
skipAuth: true,
|
|
1311
1913
|
...options
|
|
1312
1914
|
});
|
|
1313
1915
|
}
|
|
1314
1916
|
/** Загружает журнал изменений. */
|
|
1315
|
-
|
|
1316
|
-
|
|
1917
|
+
changelog(options = {}) {
|
|
1918
|
+
return this.http.execute(PLATFORM_CHANGELOG, {
|
|
1317
1919
|
path: "/api/platform/changelog",
|
|
1318
1920
|
...options
|
|
1319
1921
|
});
|
|
1320
|
-
return Array.isArray(body) ? body : [];
|
|
1321
1922
|
}
|
|
1322
1923
|
/** Загружает анонсы платформы. */
|
|
1323
|
-
|
|
1324
|
-
|
|
1924
|
+
announcements(options = {}) {
|
|
1925
|
+
return this.http.execute(PLATFORM_ANNOUNCEMENTS, {
|
|
1325
1926
|
path: "/api/platform/announcements",
|
|
1326
1927
|
...options
|
|
1327
1928
|
});
|
|
1328
|
-
return require_url.pickArray(body, "announcements");
|
|
1329
1929
|
}
|
|
1330
1930
|
/** Загружает баннер текущего события — виджет «портал». */
|
|
1331
1931
|
portal(options = {}) {
|
|
1332
|
-
return this.http.
|
|
1932
|
+
return this.http.execute(PLATFORM_PORTAL, {
|
|
1333
1933
|
path: "/api/v1/portal",
|
|
1334
1934
|
...options
|
|
1335
1935
|
});
|
|
@@ -1351,11 +1951,7 @@ var PlatformResource = class extends BaseResource {
|
|
|
1351
1951
|
* ```
|
|
1352
1952
|
*/
|
|
1353
1953
|
async status(options = {}) {
|
|
1354
|
-
return
|
|
1355
|
-
service: require_url.STATUS_SERVICE,
|
|
1356
|
-
path: "/api/status",
|
|
1357
|
-
...options
|
|
1358
|
-
}));
|
|
1954
|
+
return this.#status.get(options);
|
|
1359
1955
|
}
|
|
1360
1956
|
};
|
|
1361
1957
|
//#endregion
|
|
@@ -2050,7 +2646,7 @@ function autoSpans(text, options = {}) {
|
|
|
2050
2646
|
});
|
|
2051
2647
|
}
|
|
2052
2648
|
if (includeLinks) spans.push(...links);
|
|
2053
|
-
if (includeHashtags || includeMentions) for (const match of text.matchAll(/#([\p{L}\p{M}\p{N}_]+)|@([a-z\d][a-z\d
|
|
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)) {
|
|
2054
2650
|
if (match.index === void 0) continue;
|
|
2055
2651
|
const offset = match.index;
|
|
2056
2652
|
const previous = previousCodePoint(text, offset);
|
|
@@ -2058,7 +2654,7 @@ function autoSpans(text, options = {}) {
|
|
|
2058
2654
|
const end = offset + match[0].length;
|
|
2059
2655
|
if (links.some((span) => overlaps(span, offset, end))) continue;
|
|
2060
2656
|
const hashtag = match[1];
|
|
2061
|
-
const username = match[2]
|
|
2657
|
+
const username = match[2];
|
|
2062
2658
|
if (hashtag !== void 0 && includeHashtags) spans.push({
|
|
2063
2659
|
type: require_url.SpanType.Hashtag,
|
|
2064
2660
|
offset,
|
|
@@ -2451,6 +3047,24 @@ function resolvePostUpdate(input) {
|
|
|
2451
3047
|
}
|
|
2452
3048
|
//#endregion
|
|
2453
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 = require_url.defineBuiltInOperation("posts.stats", (body) => require_url.pickArray(body, "posts"));
|
|
3055
|
+
const POSTS_CREATE = require_url.passthroughOperation("posts.create");
|
|
3056
|
+
const POSTS_GET = require_url.passthroughOperation("posts.get");
|
|
3057
|
+
const POSTS_UPDATE = require_url.passthroughOperation("posts.update");
|
|
3058
|
+
const POSTS_RESTORE = require_url.passthroughOperation("posts.restore");
|
|
3059
|
+
const POSTS_LIKE = require_url.passthroughOperation("posts.like");
|
|
3060
|
+
const POSTS_UNLIKE = require_url.passthroughOperation("posts.unlike");
|
|
3061
|
+
const POSTS_REPOST = require_url.passthroughOperation("posts.repost");
|
|
3062
|
+
const POSTS_PIN = require_url.passthroughOperation("posts.pin");
|
|
3063
|
+
const POSTS_UNPIN = require_url.passthroughOperation("posts.unpin");
|
|
3064
|
+
const POSTS_VOTE = require_url.passthroughOperation("posts.vote");
|
|
3065
|
+
const POSTS_COMMENT = require_url.passthroughOperation("posts.comment");
|
|
3066
|
+
const POSTS_REMOVE = require_url.voidOperation("posts.remove");
|
|
3067
|
+
const POSTS_UNREPOST = require_url.voidOperation("posts.unrepost");
|
|
2454
3068
|
/** Курсорная позиция из параметров: если курсор задан — с него, иначе с начала. */
|
|
2455
3069
|
function cursorStart(params) {
|
|
2456
3070
|
return params.cursor ? { cursor: params.cursor } : {};
|
|
@@ -2464,48 +3078,44 @@ var PostsResource = class extends BaseResource {
|
|
|
2464
3078
|
#uploadFiles;
|
|
2465
3079
|
/** Лента: `/api/posts`, курсорная пагинация. */
|
|
2466
3080
|
#feed = this.paginated({
|
|
2467
|
-
|
|
3081
|
+
operation: POSTS_LIST,
|
|
2468
3082
|
path: () => "/api/posts",
|
|
2469
3083
|
query: (p) => ({
|
|
2470
3084
|
tab: p.tab,
|
|
2471
3085
|
limit: p.limit
|
|
2472
3086
|
}),
|
|
2473
3087
|
start: cursorStart,
|
|
2474
|
-
read: (body) => readCursorPage(body, "posts"),
|
|
2475
3088
|
mode: PaginationMode.Cursor
|
|
2476
3089
|
});
|
|
2477
3090
|
/** Стена пользователя: `/api/posts/user/{user}`. */
|
|
2478
3091
|
#wall = this.paginated({
|
|
2479
|
-
|
|
2480
|
-
path: (p) => `/api/posts/user/${
|
|
3092
|
+
operation: POSTS_BY_USER,
|
|
3093
|
+
path: (p) => `/api/posts/user/${require_redact.encodePathSegment(p.user, "user")}`,
|
|
2481
3094
|
query: (p) => ({
|
|
2482
3095
|
limit: p.limit,
|
|
2483
3096
|
sort: p.sort,
|
|
2484
3097
|
pinnedPostId: p.pinnedPostId
|
|
2485
3098
|
}),
|
|
2486
3099
|
start: cursorStart,
|
|
2487
|
-
read: (body) => readCursorPage(body, "posts"),
|
|
2488
3100
|
mode: PaginationMode.Cursor
|
|
2489
3101
|
});
|
|
2490
3102
|
/** Понравившиеся посты пользователя: `/api/posts/user/{user}/liked`. */
|
|
2491
3103
|
#liked = this.paginated({
|
|
2492
|
-
|
|
2493
|
-
path: (p) => `/api/posts/user/${
|
|
3104
|
+
operation: POSTS_LIKED_BY_USER,
|
|
3105
|
+
path: (p) => `/api/posts/user/${require_redact.encodePathSegment(p.user, "user")}/liked`,
|
|
2494
3106
|
query: (p) => ({ limit: p.limit }),
|
|
2495
3107
|
start: cursorStart,
|
|
2496
|
-
read: (body) => readCursorPage(body, "posts"),
|
|
2497
3108
|
mode: PaginationMode.Cursor
|
|
2498
3109
|
});
|
|
2499
3110
|
/** Комментарии к посту: курсор лежит рядом со списком, поэтому свой reader. */
|
|
2500
3111
|
#comments = this.paginated({
|
|
2501
|
-
|
|
2502
|
-
path: (p) => `/api/posts/${
|
|
3112
|
+
operation: POSTS_COMMENTS,
|
|
3113
|
+
path: (p) => `/api/posts/${require_redact.encodePathSegment(p.postId, "postId")}/comments`,
|
|
2503
3114
|
query: (p) => ({
|
|
2504
3115
|
limit: p.limit,
|
|
2505
3116
|
sort: p.sort
|
|
2506
3117
|
}),
|
|
2507
3118
|
start: cursorStart,
|
|
2508
|
-
read: (body) => readFlatCursorPage(body, "comments"),
|
|
2509
3119
|
mode: PaginationMode.Cursor
|
|
2510
3120
|
});
|
|
2511
3121
|
constructor(http, deps) {
|
|
@@ -2552,7 +3162,7 @@ var PostsResource = class extends BaseResource {
|
|
|
2552
3162
|
async create(input, options = {}) {
|
|
2553
3163
|
const data = resolvePost(input);
|
|
2554
3164
|
const attachmentIds = await this.#collectAttachments(data, options);
|
|
2555
|
-
return this.http.
|
|
3165
|
+
return this.http.execute(POSTS_CREATE, {
|
|
2556
3166
|
path: "/api/posts",
|
|
2557
3167
|
body: {
|
|
2558
3168
|
content: data.content ?? "",
|
|
@@ -2570,8 +3180,8 @@ var PostsResource = class extends BaseResource {
|
|
|
2570
3180
|
* В отличие от списков, здесь у поста заполнено поле `comments`.
|
|
2571
3181
|
*/
|
|
2572
3182
|
get(postId, options = {}) {
|
|
2573
|
-
return this.http.
|
|
2574
|
-
path: `/api/posts/${
|
|
3183
|
+
return this.http.execute(POSTS_GET, {
|
|
3184
|
+
path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}`,
|
|
2575
3185
|
...options
|
|
2576
3186
|
});
|
|
2577
3187
|
}
|
|
@@ -2584,8 +3194,8 @@ var PostsResource = class extends BaseResource {
|
|
|
2584
3194
|
*/
|
|
2585
3195
|
update(postId, input, options = {}) {
|
|
2586
3196
|
const data = resolvePostUpdate(input);
|
|
2587
|
-
return this.http.
|
|
2588
|
-
path: `/api/posts/${
|
|
3197
|
+
return this.http.execute(POSTS_UPDATE, {
|
|
3198
|
+
path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}`,
|
|
2589
3199
|
body: {
|
|
2590
3200
|
content: data.content,
|
|
2591
3201
|
...data.spans ? { spans: data.spans } : {}
|
|
@@ -2595,29 +3205,29 @@ var PostsResource = class extends BaseResource {
|
|
|
2595
3205
|
}
|
|
2596
3206
|
/** Удаляет пост. Восстановить его можно через {@link restore}. */
|
|
2597
3207
|
remove(postId, options = {}) {
|
|
2598
|
-
return this.
|
|
2599
|
-
path: `/api/posts/${
|
|
3208
|
+
return this.voidOperation(POSTS_REMOVE, {
|
|
3209
|
+
path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}`,
|
|
2600
3210
|
...options
|
|
2601
3211
|
});
|
|
2602
3212
|
}
|
|
2603
3213
|
/** Восстанавливает удалённый пост. */
|
|
2604
3214
|
restore(postId, options = {}) {
|
|
2605
|
-
return this.http.
|
|
2606
|
-
path: `/api/posts/${
|
|
3215
|
+
return this.http.execute(POSTS_RESTORE, {
|
|
3216
|
+
path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}/restore`,
|
|
2607
3217
|
...options
|
|
2608
3218
|
});
|
|
2609
3219
|
}
|
|
2610
3220
|
/** Ставит реакцию на пост. */
|
|
2611
3221
|
like(postId, options = {}) {
|
|
2612
|
-
return this.http.
|
|
2613
|
-
path: `/api/posts/${
|
|
3222
|
+
return this.http.execute(POSTS_LIKE, {
|
|
3223
|
+
path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}/like`,
|
|
2614
3224
|
...options
|
|
2615
3225
|
});
|
|
2616
3226
|
}
|
|
2617
3227
|
/** Убирает реакцию с поста. */
|
|
2618
3228
|
unlike(postId, options = {}) {
|
|
2619
|
-
return this.http.
|
|
2620
|
-
path: `/api/posts/${
|
|
3229
|
+
return this.http.execute(POSTS_UNLIKE, {
|
|
3230
|
+
path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}/like`,
|
|
2621
3231
|
...options
|
|
2622
3232
|
});
|
|
2623
3233
|
}
|
|
@@ -2628,30 +3238,30 @@ var PostsResource = class extends BaseResource {
|
|
|
2628
3238
|
* для файлов здесь нет.
|
|
2629
3239
|
*/
|
|
2630
3240
|
repost(postId, content = "", options = {}) {
|
|
2631
|
-
return this.http.
|
|
2632
|
-
path: `/api/posts/${
|
|
3241
|
+
return this.http.execute(POSTS_REPOST, {
|
|
3242
|
+
path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}/repost`,
|
|
2633
3243
|
body: { content },
|
|
2634
3244
|
...options
|
|
2635
3245
|
});
|
|
2636
3246
|
}
|
|
2637
3247
|
/** Отменяет репост. */
|
|
2638
3248
|
unrepost(postId, options = {}) {
|
|
2639
|
-
return this.
|
|
2640
|
-
path: `/api/posts/${
|
|
3249
|
+
return this.voidOperation(POSTS_UNREPOST, {
|
|
3250
|
+
path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}/repost`,
|
|
2641
3251
|
...options
|
|
2642
3252
|
});
|
|
2643
3253
|
}
|
|
2644
3254
|
/** Закрепляет пост в профиле. */
|
|
2645
3255
|
pin(postId, options = {}) {
|
|
2646
|
-
return this.http.
|
|
2647
|
-
path: `/api/posts/${
|
|
3256
|
+
return this.http.execute(POSTS_PIN, {
|
|
3257
|
+
path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}/pin`,
|
|
2648
3258
|
...options
|
|
2649
3259
|
});
|
|
2650
3260
|
}
|
|
2651
3261
|
/** Открепляет пост. */
|
|
2652
3262
|
unpin(postId, options = {}) {
|
|
2653
|
-
return this.http.
|
|
2654
|
-
path: `/api/posts/${
|
|
3263
|
+
return this.http.execute(POSTS_UNPIN, {
|
|
3264
|
+
path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}/pin`,
|
|
2655
3265
|
...options
|
|
2656
3266
|
});
|
|
2657
3267
|
}
|
|
@@ -2661,20 +3271,19 @@ var PostsResource = class extends BaseResource {
|
|
|
2661
3271
|
* @param optionIds выбранные варианты; несколько допустимы только при `multipleChoice`
|
|
2662
3272
|
*/
|
|
2663
3273
|
vote(postId, optionIds, options = {}) {
|
|
2664
|
-
return this.http.
|
|
2665
|
-
path: `/api/posts/${
|
|
3274
|
+
return this.http.execute(POSTS_VOTE, {
|
|
3275
|
+
path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}/poll/vote`,
|
|
2666
3276
|
body: { optionIds },
|
|
2667
3277
|
...options
|
|
2668
3278
|
});
|
|
2669
3279
|
}
|
|
2670
3280
|
/** Запрашивает счётчики сразу для нескольких постов. */
|
|
2671
|
-
|
|
2672
|
-
|
|
3281
|
+
stats(ids, options = {}) {
|
|
3282
|
+
return this.http.execute(POSTS_STATS, {
|
|
2673
3283
|
path: "/api/posts/stats",
|
|
2674
3284
|
body: { ids },
|
|
2675
3285
|
...options
|
|
2676
3286
|
});
|
|
2677
|
-
return require_url.pickArray(body, "posts");
|
|
2678
3287
|
}
|
|
2679
3288
|
/**
|
|
2680
3289
|
* Загружает страницу стены пользователя.
|
|
@@ -2744,8 +3353,8 @@ var PostsResource = class extends BaseResource {
|
|
|
2744
3353
|
async comment(postId, input, options = {}) {
|
|
2745
3354
|
const data = resolveComment(typeof input === "string" ? { content: input } : input);
|
|
2746
3355
|
const attachmentIds = await this.#collectAttachments(data, options);
|
|
2747
|
-
return this.http.
|
|
2748
|
-
path: `/api/posts/${
|
|
3356
|
+
return this.http.execute(POSTS_COMMENT, {
|
|
3357
|
+
path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}/comments`,
|
|
2749
3358
|
body: {
|
|
2750
3359
|
content: data.content ?? "",
|
|
2751
3360
|
attachmentIds
|
|
@@ -2865,6 +3474,7 @@ function resolveReport(input) {
|
|
|
2865
3474
|
}
|
|
2866
3475
|
//#endregion
|
|
2867
3476
|
//#region src/resources/reports.ts
|
|
3477
|
+
const REPORTS_CREATE = require_url.passthroughOperation("reports.create");
|
|
2868
3478
|
/**
|
|
2869
3479
|
* Жалобы на контент и пользователей.
|
|
2870
3480
|
*
|
|
@@ -2885,7 +3495,7 @@ var ReportsResource = class extends BaseResource {
|
|
|
2885
3495
|
*/
|
|
2886
3496
|
create(input, options = {}) {
|
|
2887
3497
|
const data = resolveReport(input);
|
|
2888
|
-
return this.http.
|
|
3498
|
+
return this.http.execute(REPORTS_CREATE, {
|
|
2889
3499
|
path: "/api/reports",
|
|
2890
3500
|
body: data,
|
|
2891
3501
|
...options
|
|
@@ -2894,6 +3504,10 @@ var ReportsResource = class extends BaseResource {
|
|
|
2894
3504
|
};
|
|
2895
3505
|
//#endregion
|
|
2896
3506
|
//#region src/resources/search.ts
|
|
3507
|
+
const SEARCH_ALL = require_url.defineBuiltInOperation("search.all", (body) => ({
|
|
3508
|
+
users: require_url.pickArray(body, "users"),
|
|
3509
|
+
hashtags: require_url.pickArray(body, "hashtags")
|
|
3510
|
+
}));
|
|
2897
3511
|
/**
|
|
2898
3512
|
* Глобальный поиск.
|
|
2899
3513
|
*
|
|
@@ -2908,20 +3522,23 @@ var SearchResource = class extends BaseResource {
|
|
|
2908
3522
|
* const { users, hashtags } = await itd.search.all('арт');
|
|
2909
3523
|
* ```
|
|
2910
3524
|
*/
|
|
2911
|
-
|
|
2912
|
-
|
|
3525
|
+
all(query, options = {}) {
|
|
3526
|
+
return this.http.execute(SEARCH_ALL, {
|
|
2913
3527
|
path: "/api/search",
|
|
2914
3528
|
query: { q: query },
|
|
2915
3529
|
...options
|
|
2916
3530
|
});
|
|
2917
|
-
return {
|
|
2918
|
-
users: require_url.pickArray(body, "users"),
|
|
2919
|
-
hashtags: require_url.pickArray(body, "hashtags")
|
|
2920
|
-
};
|
|
2921
3531
|
}
|
|
2922
3532
|
};
|
|
2923
3533
|
//#endregion
|
|
2924
3534
|
//#region src/resources/subscription.ts
|
|
3535
|
+
const SUBSCRIPTION_METHODS = require_url.defineBuiltInOperation("subscription.methods", (body) => Array.isArray(body) ? body : []);
|
|
3536
|
+
const SUBSCRIPTION_STATUS = require_url.passthroughOperation("subscription.status");
|
|
3537
|
+
const SUBSCRIPTION_PAY = require_url.passthroughOperation("subscription.pay");
|
|
3538
|
+
const SUBSCRIPTION_SET_AUTO_RENEWAL = require_url.passthroughOperation("subscription.setAutoRenewal");
|
|
3539
|
+
const SUBSCRIPTION_BIND_CARD = require_url.passthroughOperation("subscription.bindCard");
|
|
3540
|
+
const SUBSCRIPTION_SET_DEFAULT_METHOD = require_url.passthroughOperation("subscription.setDefaultMethod");
|
|
3541
|
+
const SUBSCRIPTION_REMOVE_METHOD = require_url.voidOperation("subscription.removeMethod");
|
|
2925
3542
|
/**
|
|
2926
3543
|
* Подписка и способы оплаты.
|
|
2927
3544
|
*
|
|
@@ -2930,7 +3547,7 @@ var SearchResource = class extends BaseResource {
|
|
|
2930
3547
|
var SubscriptionResource = class extends BaseResource {
|
|
2931
3548
|
/** Загружает состояние подписки и её цену. */
|
|
2932
3549
|
status(options = {}) {
|
|
2933
|
-
return this.http.
|
|
3550
|
+
return this.http.execute(SUBSCRIPTION_STATUS, {
|
|
2934
3551
|
path: "/api/v1/subscription/",
|
|
2935
3552
|
...options
|
|
2936
3553
|
});
|
|
@@ -2941,14 +3558,14 @@ var SubscriptionResource = class extends BaseResource {
|
|
|
2941
3558
|
* Форма ответа в документации API не описана, поэтому тип результата не уточняется.
|
|
2942
3559
|
*/
|
|
2943
3560
|
pay(options = {}) {
|
|
2944
|
-
return this.http.
|
|
3561
|
+
return this.http.execute(SUBSCRIPTION_PAY, {
|
|
2945
3562
|
path: "/api/v1/subscription/pay",
|
|
2946
3563
|
...options
|
|
2947
3564
|
});
|
|
2948
3565
|
}
|
|
2949
3566
|
/** Включает или отключает автопродление. */
|
|
2950
3567
|
setAutoRenewal(enabled, options = {}) {
|
|
2951
|
-
return this.http.
|
|
3568
|
+
return this.http.execute(SUBSCRIPTION_SET_AUTO_RENEWAL, {
|
|
2952
3569
|
path: "/api/v1/subscription/auto-renewal",
|
|
2953
3570
|
body: { enabled },
|
|
2954
3571
|
...options
|
|
@@ -2956,30 +3573,29 @@ var SubscriptionResource = class extends BaseResource {
|
|
|
2956
3573
|
}
|
|
2957
3574
|
/** Запускает привязку карты. */
|
|
2958
3575
|
bindCard(options = {}) {
|
|
2959
|
-
return this.http.
|
|
3576
|
+
return this.http.execute(SUBSCRIPTION_BIND_CARD, {
|
|
2960
3577
|
path: "/api/v1/subscription/bind-card",
|
|
2961
3578
|
...options
|
|
2962
3579
|
});
|
|
2963
3580
|
}
|
|
2964
3581
|
/** Загружает список способов оплаты. Пустой массив, если карт нет. */
|
|
2965
|
-
|
|
2966
|
-
|
|
3582
|
+
methods(options = {}) {
|
|
3583
|
+
return this.http.execute(SUBSCRIPTION_METHODS, {
|
|
2967
3584
|
path: "/api/v1/subscription/methods",
|
|
2968
3585
|
...options
|
|
2969
3586
|
});
|
|
2970
|
-
return Array.isArray(body) ? body : [];
|
|
2971
3587
|
}
|
|
2972
3588
|
/** Делает способ оплаты основным. */
|
|
2973
3589
|
setDefaultMethod(methodId, options = {}) {
|
|
2974
|
-
return this.http.
|
|
2975
|
-
path: `/api/v1/subscription/methods/${
|
|
3590
|
+
return this.http.execute(SUBSCRIPTION_SET_DEFAULT_METHOD, {
|
|
3591
|
+
path: `/api/v1/subscription/methods/${require_redact.encodePathSegment(methodId, "methodId")}/default`,
|
|
2976
3592
|
...options
|
|
2977
3593
|
});
|
|
2978
3594
|
}
|
|
2979
3595
|
/** Удаляет способ оплаты. */
|
|
2980
3596
|
removeMethod(methodId, options = {}) {
|
|
2981
|
-
return this.
|
|
2982
|
-
path: `/api/v1/subscription/methods/${
|
|
3597
|
+
return this.voidOperation(SUBSCRIPTION_REMOVE_METHOD, {
|
|
3598
|
+
path: `/api/v1/subscription/methods/${require_redact.encodePathSegment(methodId, "methodId")}`,
|
|
2983
3599
|
...options
|
|
2984
3600
|
});
|
|
2985
3601
|
}
|
|
@@ -3137,7 +3753,7 @@ var TelemetryBatchImpl = class {
|
|
|
3137
3753
|
close() {
|
|
3138
3754
|
return this.#close(false);
|
|
3139
3755
|
}
|
|
3140
|
-
/** Внутренняя
|
|
3756
|
+
/** Внутренняя отправка данных после вызова `dispose()`. */
|
|
3141
3757
|
closeForDispose() {
|
|
3142
3758
|
this.prepareForDispose();
|
|
3143
3759
|
return this.#close(true);
|
|
@@ -3207,7 +3823,7 @@ var TelemetryResource = class extends BaseResource {
|
|
|
3207
3823
|
}
|
|
3208
3824
|
/** Идентификатор сессии телеметрии, общий для всех событий этого ресурса. */
|
|
3209
3825
|
get sessionId() {
|
|
3210
|
-
this.#sessionId ??=
|
|
3826
|
+
this.#sessionId ??= require_redact.createDeviceId();
|
|
3211
3827
|
return this.#sessionId;
|
|
3212
3828
|
}
|
|
3213
3829
|
/** Отправляет события просмотра постов (`POST /api/v1/i`). */
|
|
@@ -3233,7 +3849,7 @@ var TelemetryResource = class extends BaseResource {
|
|
|
3233
3849
|
},
|
|
3234
3850
|
...requestOptions
|
|
3235
3851
|
};
|
|
3236
|
-
return disposeCleanup ? this.http.cleanupOperation(
|
|
3852
|
+
return disposeCleanup ? this.http.cleanupOperation(TELEMETRY_DWELL, options) : this.http.execute(TELEMETRY_DWELL, options);
|
|
3237
3853
|
}
|
|
3238
3854
|
/** Отправляет события взаимодействия с контентом (`POST /api/v1/x`). */
|
|
3239
3855
|
interaction(entries, telemetryOptions = {}, requestOptions = {}) {
|
|
@@ -3257,7 +3873,7 @@ var TelemetryResource = class extends BaseResource {
|
|
|
3257
3873
|
},
|
|
3258
3874
|
...requestOptions
|
|
3259
3875
|
};
|
|
3260
|
-
return disposeCleanup ? this.http.cleanupOperation(
|
|
3876
|
+
return disposeCleanup ? this.http.cleanupOperation(TELEMETRY_INTERACTION, options) : this.http.execute(TELEMETRY_INTERACTION, options);
|
|
3261
3877
|
}
|
|
3262
3878
|
/** Начинает измерять время просмотра и отправляет результат после `finish()`. */
|
|
3263
3879
|
startView(input, options = {}, requestOptions = {}) {
|
|
@@ -3300,12 +3916,43 @@ var TelemetryResource = class extends BaseResource {
|
|
|
3300
3916
|
function prepareTelemetryForDispose(resource) {
|
|
3301
3917
|
if (resource) TELEMETRY_DISPOSE_PREPARERS.get(resource)?.();
|
|
3302
3918
|
}
|
|
3303
|
-
/**
|
|
3919
|
+
/** Отправляет и закрывает созданные накопители после вызова `dispose()`. @internal */
|
|
3304
3920
|
function closeTelemetryForDispose(resource) {
|
|
3305
3921
|
return resource ? TELEMETRY_DISPOSE_CLOSERS.get(resource)?.() ?? Promise.resolve() : Promise.resolve();
|
|
3306
3922
|
}
|
|
3923
|
+
const TELEMETRY_DWELL = require_url.defineBuiltInOperation("telemetry.dwell");
|
|
3924
|
+
const TELEMETRY_INTERACTION = require_url.defineBuiltInOperation("telemetry.interaction");
|
|
3307
3925
|
//#endregion
|
|
3308
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 = require_url.defineBuiltInOperation("users.checkUsername", (body) => require_url.pickBoolean(body, "available"));
|
|
3934
|
+
const USERS_SEARCH = require_url.defineBuiltInOperation("users.search", (body) => require_url.pickArray(body, "users"));
|
|
3935
|
+
const USERS_WHO_TO_FOLLOW = require_url.defineBuiltInOperation("users.whoToFollow", (body) => require_url.pickArray(body, "users"));
|
|
3936
|
+
const USERS_TOP_CLANS = require_url.defineBuiltInOperation("users.topClans", (body) => require_url.pickArray(body, "clans"));
|
|
3937
|
+
const USERS_PINS = require_url.defineBuiltInOperation("users.pins", (body) => ({
|
|
3938
|
+
pins: require_url.pickArray(body, "pins"),
|
|
3939
|
+
activePin: require_url.pickString(body, "activePin") ?? null
|
|
3940
|
+
}));
|
|
3941
|
+
const USERS_ME = require_url.passthroughOperation("users.me");
|
|
3942
|
+
const USERS_UPDATE_ME = require_url.passthroughOperation("users.updateMe");
|
|
3943
|
+
const USERS_CREATE_PROFILE = require_url.passthroughOperation("users.createProfile");
|
|
3944
|
+
const USERS_GET = require_url.passthroughOperation("users.get");
|
|
3945
|
+
const USERS_FOLLOW = require_url.passthroughOperation("users.follow");
|
|
3946
|
+
const USERS_FOLLOW_STATUS = require_url.passthroughOperation("users.followStatus");
|
|
3947
|
+
const USERS_GET_PRIVACY = require_url.passthroughOperation("users.getPrivacy");
|
|
3948
|
+
const USERS_UPDATE_PRIVACY = require_url.passthroughOperation("users.updatePrivacy");
|
|
3949
|
+
const USERS_DEACTIVATE = require_url.voidOperation("users.deactivate");
|
|
3950
|
+
const USERS_RESTORE = require_url.voidOperation("users.restore");
|
|
3951
|
+
const USERS_UNFOLLOW = require_url.voidOperation("users.unfollow");
|
|
3952
|
+
const USERS_BLOCK = require_url.voidOperation("users.block");
|
|
3953
|
+
const USERS_UNBLOCK = require_url.voidOperation("users.unblock");
|
|
3954
|
+
const USERS_SET_PIN = require_url.voidOperation("users.setPin");
|
|
3955
|
+
const USERS_REMOVE_PIN = require_url.voidOperation("users.removePin");
|
|
3309
3956
|
/**
|
|
3310
3957
|
* Пользователи: профили, подписки, блокировки, приватность.
|
|
3311
3958
|
*
|
|
@@ -3326,23 +3973,22 @@ var UsersResource = class extends BaseResource {
|
|
|
3326
3973
|
* сервер его сейчас не читает (см. {@link followers}): когда починят — заработает само.
|
|
3327
3974
|
*/
|
|
3328
3975
|
#userList = this.paginated({
|
|
3329
|
-
|
|
3976
|
+
operation: (p) => p.operation,
|
|
3330
3977
|
path: (p) => p.path,
|
|
3331
3978
|
query: (p) => ({ limit: p.limit }),
|
|
3332
3979
|
start: (p) => p.page !== void 0 ? { page: p.page } : {},
|
|
3333
|
-
read: (body) => readPagedPage(body, "users", "followers", "following", "blocked"),
|
|
3334
3980
|
mode: PaginationMode.Page
|
|
3335
3981
|
});
|
|
3336
3982
|
/** Загружает свой профиль — с подпиской и признаком подтверждённого телефона. */
|
|
3337
3983
|
me(options = {}) {
|
|
3338
|
-
return this.http.
|
|
3984
|
+
return this.http.execute(USERS_ME, {
|
|
3339
3985
|
path: "/api/users/me",
|
|
3340
3986
|
...options
|
|
3341
3987
|
});
|
|
3342
3988
|
}
|
|
3343
3989
|
/** Обновляет свой профиль. Передавайте только изменяемые поля. */
|
|
3344
3990
|
updateMe(input, options = {}) {
|
|
3345
|
-
return this.http.
|
|
3991
|
+
return this.http.execute(USERS_UPDATE_ME, {
|
|
3346
3992
|
path: "/api/users/me",
|
|
3347
3993
|
body: input,
|
|
3348
3994
|
...options
|
|
@@ -3369,21 +4015,21 @@ var UsersResource = class extends BaseResource {
|
|
|
3369
4015
|
}
|
|
3370
4016
|
/** Деактивирует аккаунт. Вернуть его можно через {@link restore}. */
|
|
3371
4017
|
deactivate(options = {}) {
|
|
3372
|
-
return this.
|
|
4018
|
+
return this.voidOperation(USERS_DEACTIVATE, {
|
|
3373
4019
|
path: "/api/users/me",
|
|
3374
4020
|
...options
|
|
3375
4021
|
});
|
|
3376
4022
|
}
|
|
3377
4023
|
/** Восстанавливает деактивированный аккаунт. */
|
|
3378
4024
|
restore(options = {}) {
|
|
3379
|
-
return this.
|
|
4025
|
+
return this.voidOperation(USERS_RESTORE, {
|
|
3380
4026
|
path: "/api/users/me/restore",
|
|
3381
4027
|
...options
|
|
3382
4028
|
});
|
|
3383
4029
|
}
|
|
3384
4030
|
/** Создаёт профиль после регистрации. */
|
|
3385
4031
|
createProfile(input, options = {}) {
|
|
3386
|
-
return this.http.
|
|
4032
|
+
return this.http.execute(USERS_CREATE_PROFILE, {
|
|
3387
4033
|
path: "/api/users/profile",
|
|
3388
4034
|
body: input,
|
|
3389
4035
|
...options
|
|
@@ -3401,23 +4047,22 @@ var UsersResource = class extends BaseResource {
|
|
|
3401
4047
|
* ```
|
|
3402
4048
|
*/
|
|
3403
4049
|
get(user, options = {}) {
|
|
3404
|
-
return this.http.
|
|
3405
|
-
path: `/api/users/${
|
|
4050
|
+
return this.http.execute(USERS_GET, {
|
|
4051
|
+
path: `/api/users/${require_redact.encodePathSegment(user, "user")}`,
|
|
3406
4052
|
...options
|
|
3407
4053
|
});
|
|
3408
4054
|
}
|
|
3409
4055
|
/** Проверяет, свободно ли имя пользователя. */
|
|
3410
|
-
|
|
3411
|
-
|
|
4056
|
+
checkUsername(username, options = {}) {
|
|
4057
|
+
return this.http.execute(USERS_CHECK_USERNAME, {
|
|
3412
4058
|
path: "/api/users/check-username",
|
|
3413
4059
|
query: { username },
|
|
3414
4060
|
...options
|
|
3415
4061
|
});
|
|
3416
|
-
return require_url.pickBoolean(body, "available");
|
|
3417
4062
|
}
|
|
3418
4063
|
/** Ищет пользователей по строке запроса. */
|
|
3419
|
-
|
|
3420
|
-
|
|
4064
|
+
search(query, params = {}, options = {}) {
|
|
4065
|
+
return this.http.execute(USERS_SEARCH, {
|
|
3421
4066
|
path: "/api/users/search",
|
|
3422
4067
|
query: {
|
|
3423
4068
|
q: query,
|
|
@@ -3425,23 +4070,20 @@ var UsersResource = class extends BaseResource {
|
|
|
3425
4070
|
},
|
|
3426
4071
|
...options
|
|
3427
4072
|
});
|
|
3428
|
-
return require_url.pickArray(body, "users");
|
|
3429
4073
|
}
|
|
3430
4074
|
/** Загружает рекомендации, на кого подписаться. */
|
|
3431
|
-
|
|
3432
|
-
|
|
4075
|
+
whoToFollow(options = {}) {
|
|
4076
|
+
return this.http.execute(USERS_WHO_TO_FOLLOW, {
|
|
3433
4077
|
path: "/api/users/suggestions/who-to-follow",
|
|
3434
4078
|
...options
|
|
3435
4079
|
});
|
|
3436
|
-
return require_url.pickArray(body, "users");
|
|
3437
4080
|
}
|
|
3438
4081
|
/** Загружает рейтинг кланов. */
|
|
3439
|
-
|
|
3440
|
-
|
|
4082
|
+
topClans(options = {}) {
|
|
4083
|
+
return this.http.execute(USERS_TOP_CLANS, {
|
|
3441
4084
|
path: "/api/users/stats/top-clans",
|
|
3442
4085
|
...options
|
|
3443
4086
|
});
|
|
3444
|
-
return require_url.pickArray(body, "clans");
|
|
3445
4087
|
}
|
|
3446
4088
|
/**
|
|
3447
4089
|
* Подписывается на пользователя.
|
|
@@ -3449,16 +4091,16 @@ var UsersResource = class extends BaseResource {
|
|
|
3449
4091
|
* У закрытого профиля вместо подписки отправляется заявка — это видно по полю `status`.
|
|
3450
4092
|
*/
|
|
3451
4093
|
follow(user, options = {}) {
|
|
3452
|
-
return this.http.
|
|
3453
|
-
path: `/api/users/${
|
|
4094
|
+
return this.http.execute(USERS_FOLLOW, {
|
|
4095
|
+
path: `/api/users/${require_redact.encodePathSegment(user, "user")}/follow`,
|
|
3454
4096
|
body: {},
|
|
3455
4097
|
...options
|
|
3456
4098
|
});
|
|
3457
4099
|
}
|
|
3458
4100
|
/** Отписывается от пользователя. */
|
|
3459
4101
|
unfollow(user, options = {}) {
|
|
3460
|
-
return this.
|
|
3461
|
-
path: `/api/users/${
|
|
4102
|
+
return this.voidOperation(USERS_UNFOLLOW, {
|
|
4103
|
+
path: `/api/users/${require_redact.encodePathSegment(user, "user")}/follow`,
|
|
3462
4104
|
...options
|
|
3463
4105
|
});
|
|
3464
4106
|
}
|
|
@@ -3474,7 +4116,7 @@ var UsersResource = class extends BaseResource {
|
|
|
3474
4116
|
* на проверенных аккаунтах занижено примерно на 1–4%.
|
|
3475
4117
|
*/
|
|
3476
4118
|
followers(user, params = {}, options = {}) {
|
|
3477
|
-
return this.#userPage("users.followers", `/api/users/${
|
|
4119
|
+
return this.#userPage("users.followers", `/api/users/${require_redact.encodePathSegment(user, "user")}/followers`, params, options);
|
|
3478
4120
|
}
|
|
3479
4121
|
/**
|
|
3480
4122
|
* Перебирает подписчиков.
|
|
@@ -3483,15 +4125,15 @@ var UsersResource = class extends BaseResource {
|
|
|
3483
4125
|
* см. {@link followers}. Метод оставлен на случай, если пагинацию починят.
|
|
3484
4126
|
*/
|
|
3485
4127
|
iterateFollowers(user, params = {}, options = {}) {
|
|
3486
|
-
return this.#userPaginator("users.followers", `/api/users/${
|
|
4128
|
+
return this.#userPaginator("users.followers", `/api/users/${require_redact.encodePathSegment(user, "user")}/followers`, params, options);
|
|
3487
4129
|
}
|
|
3488
4130
|
/** Загружает подписки пользователя. Ограничения те же, что у {@link followers}. */
|
|
3489
4131
|
following(user, params = {}, options = {}) {
|
|
3490
|
-
return this.#userPage("users.following", `/api/users/${
|
|
4132
|
+
return this.#userPage("users.following", `/api/users/${require_redact.encodePathSegment(user, "user")}/following`, params, options);
|
|
3491
4133
|
}
|
|
3492
4134
|
/** Перебирает подписки. Закончится после первых 20 записей — см. {@link followers}. */
|
|
3493
4135
|
iterateFollowing(user, params = {}, options = {}) {
|
|
3494
|
-
return this.#userPaginator("users.following", `/api/users/${
|
|
4136
|
+
return this.#userPaginator("users.following", `/api/users/${require_redact.encodePathSegment(user, "user")}/following`, params, options);
|
|
3495
4137
|
}
|
|
3496
4138
|
/**
|
|
3497
4139
|
* Проверяет, подписаны ли вы, сразу для нескольких пользователей.
|
|
@@ -3505,7 +4147,7 @@ var UsersResource = class extends BaseResource {
|
|
|
3505
4147
|
* ```
|
|
3506
4148
|
*/
|
|
3507
4149
|
followStatus(userIds, options = {}) {
|
|
3508
|
-
return this.http.
|
|
4150
|
+
return this.http.execute(USERS_FOLLOW_STATUS, {
|
|
3509
4151
|
path: "/api/users/follow-status",
|
|
3510
4152
|
body: { userIds },
|
|
3511
4153
|
...options
|
|
@@ -3513,16 +4155,16 @@ var UsersResource = class extends BaseResource {
|
|
|
3513
4155
|
}
|
|
3514
4156
|
/** Блокирует пользователя. */
|
|
3515
4157
|
block(user, options = {}) {
|
|
3516
|
-
return this.
|
|
3517
|
-
path: `/api/users/${
|
|
4158
|
+
return this.voidOperation(USERS_BLOCK, {
|
|
4159
|
+
path: `/api/users/${require_redact.encodePathSegment(user, "user")}/block`,
|
|
3518
4160
|
body: {},
|
|
3519
4161
|
...options
|
|
3520
4162
|
});
|
|
3521
4163
|
}
|
|
3522
4164
|
/** Снимает блокировку. */
|
|
3523
4165
|
unblock(user, options = {}) {
|
|
3524
|
-
return this.
|
|
3525
|
-
path: `/api/users/${
|
|
4166
|
+
return this.voidOperation(USERS_UNBLOCK, {
|
|
4167
|
+
path: `/api/users/${require_redact.encodePathSegment(user, "user")}/block`,
|
|
3526
4168
|
...options
|
|
3527
4169
|
});
|
|
3528
4170
|
}
|
|
@@ -3536,14 +4178,14 @@ var UsersResource = class extends BaseResource {
|
|
|
3536
4178
|
}
|
|
3537
4179
|
/** Загружает настройки приватности. */
|
|
3538
4180
|
getPrivacy(options = {}) {
|
|
3539
|
-
return this.http.
|
|
4181
|
+
return this.http.execute(USERS_GET_PRIVACY, {
|
|
3540
4182
|
path: "/api/users/me/privacy",
|
|
3541
4183
|
...options
|
|
3542
4184
|
});
|
|
3543
4185
|
}
|
|
3544
4186
|
/** Обновляет настройки приватности. Передавайте только изменяемые поля. */
|
|
3545
4187
|
updatePrivacy(input, options = {}) {
|
|
3546
|
-
return this.http.
|
|
4188
|
+
return this.http.execute(USERS_UPDATE_PRIVACY, {
|
|
3547
4189
|
path: "/api/users/me/privacy",
|
|
3548
4190
|
body: input,
|
|
3549
4191
|
...options
|
|
@@ -3554,19 +4196,15 @@ var UsersResource = class extends BaseResource {
|
|
|
3554
4196
|
*
|
|
3555
4197
|
* `activePin` — строка-идентификатор, а не объект.
|
|
3556
4198
|
*/
|
|
3557
|
-
|
|
3558
|
-
|
|
4199
|
+
pins(options = {}) {
|
|
4200
|
+
return this.http.execute(USERS_PINS, {
|
|
3559
4201
|
path: "/api/users/me/pins",
|
|
3560
4202
|
...options
|
|
3561
4203
|
});
|
|
3562
|
-
return {
|
|
3563
|
-
pins: require_url.pickArray(body, "pins"),
|
|
3564
|
-
activePin: require_url.pickString(body, "activePin") ?? null
|
|
3565
|
-
};
|
|
3566
4204
|
}
|
|
3567
4205
|
/** Выбирает активный значок профиля. */
|
|
3568
4206
|
setPin(slug, options = {}) {
|
|
3569
|
-
return this.
|
|
4207
|
+
return this.voidOperation(USERS_SET_PIN, {
|
|
3570
4208
|
path: "/api/users/me/pin",
|
|
3571
4209
|
body: { slug },
|
|
3572
4210
|
...options
|
|
@@ -3574,7 +4212,7 @@ var UsersResource = class extends BaseResource {
|
|
|
3574
4212
|
}
|
|
3575
4213
|
/** Снимает активный значок. */
|
|
3576
4214
|
removePin(options = {}) {
|
|
3577
|
-
return this.
|
|
4215
|
+
return this.voidOperation(USERS_REMOVE_PIN, {
|
|
3578
4216
|
path: "/api/users/me/pin",
|
|
3579
4217
|
...options
|
|
3580
4218
|
});
|
|
@@ -3582,20 +4220,22 @@ var UsersResource = class extends BaseResource {
|
|
|
3582
4220
|
#userPage(operationId, path, params, options) {
|
|
3583
4221
|
return this.#userList.list({
|
|
3584
4222
|
...params,
|
|
3585
|
-
operationId,
|
|
4223
|
+
operation: USER_LIST_OPERATIONS[operationId],
|
|
3586
4224
|
path
|
|
3587
4225
|
}, options);
|
|
3588
4226
|
}
|
|
3589
4227
|
#userPaginator(operationId, path, params, options) {
|
|
3590
4228
|
return this.#userList.iterate({
|
|
3591
4229
|
...params,
|
|
3592
|
-
operationId,
|
|
4230
|
+
operation: USER_LIST_OPERATIONS[operationId],
|
|
3593
4231
|
path
|
|
3594
4232
|
}, options);
|
|
3595
4233
|
}
|
|
3596
4234
|
};
|
|
3597
4235
|
//#endregion
|
|
3598
4236
|
//#region src/resources/verification.ts
|
|
4237
|
+
const VERIFICATION_STATUS = require_url.passthroughOperation("verification.status");
|
|
4238
|
+
const VERIFICATION_SUBMIT = require_url.passthroughOperation("verification.submit");
|
|
3599
4239
|
/**
|
|
3600
4240
|
* Верификация профиля.
|
|
3601
4241
|
*
|
|
@@ -3604,14 +4244,14 @@ var UsersResource = class extends BaseResource {
|
|
|
3604
4244
|
var VerificationResource = class extends BaseResource {
|
|
3605
4245
|
/** Загружает статус заявки. Значение `none` означает, что заявка не подавалась. */
|
|
3606
4246
|
status(options = {}) {
|
|
3607
|
-
return this.http.
|
|
4247
|
+
return this.http.execute(VERIFICATION_STATUS, {
|
|
3608
4248
|
path: "/api/verification/status",
|
|
3609
4249
|
...options
|
|
3610
4250
|
});
|
|
3611
4251
|
}
|
|
3612
4252
|
/** Подаёт заявку на верификацию с видео. */
|
|
3613
4253
|
submit(videoUrl, options = {}) {
|
|
3614
|
-
return this.http.
|
|
4254
|
+
return this.http.execute(VERIFICATION_SUBMIT, {
|
|
3615
4255
|
path: "/api/verification/submit",
|
|
3616
4256
|
body: { videoUrl },
|
|
3617
4257
|
...options
|
|
@@ -3656,11 +4296,11 @@ function createResources(deps) {
|
|
|
3656
4296
|
return comments;
|
|
3657
4297
|
},
|
|
3658
4298
|
get files() {
|
|
3659
|
-
files ??= new FilesResource(deps.http, {
|
|
4299
|
+
files ??= new FilesResource(deps.http, { files: deps.files });
|
|
3660
4300
|
return files;
|
|
3661
4301
|
},
|
|
3662
4302
|
get notifications() {
|
|
3663
|
-
notifications ??=
|
|
4303
|
+
notifications ??= deps.createNotifications(deps.http);
|
|
3664
4304
|
return notifications;
|
|
3665
4305
|
},
|
|
3666
4306
|
get hashtags() {
|
|
@@ -3684,7 +4324,7 @@ function createResources(deps) {
|
|
|
3684
4324
|
return subscription;
|
|
3685
4325
|
},
|
|
3686
4326
|
get platform() {
|
|
3687
|
-
platform ??= new PlatformResource(deps.http);
|
|
4327
|
+
platform ??= new PlatformResource(deps.http, deps.status);
|
|
3688
4328
|
return platform;
|
|
3689
4329
|
},
|
|
3690
4330
|
get telemetry() {
|
|
@@ -3724,7 +4364,7 @@ function fromStream(factory, options = {}) {
|
|
|
3724
4364
|
throw new require_errors.ItdFileError("не удалось открыть поток вложения", {
|
|
3725
4365
|
reason: require_errors.ItdFileErrorReason.Read,
|
|
3726
4366
|
retryable: true,
|
|
3727
|
-
cause: error
|
|
4367
|
+
cause: require_redact.redactErrorCause(error)
|
|
3728
4368
|
});
|
|
3729
4369
|
}
|
|
3730
4370
|
const content = require_options.isReadableByteStream(opened) ? { stream: opened } : opened;
|
|
@@ -4005,12 +4645,24 @@ Object.defineProperty(exports, "BaseResource", {
|
|
|
4005
4645
|
return BaseResource;
|
|
4006
4646
|
}
|
|
4007
4647
|
});
|
|
4648
|
+
Object.defineProperty(exports, "ExtensibleOperationCatalog", {
|
|
4649
|
+
enumerable: true,
|
|
4650
|
+
get: function() {
|
|
4651
|
+
return ExtensibleOperationCatalog;
|
|
4652
|
+
}
|
|
4653
|
+
});
|
|
4008
4654
|
Object.defineProperty(exports, "IMAGE_MIME_TYPES", {
|
|
4009
4655
|
enumerable: true,
|
|
4010
4656
|
get: function() {
|
|
4011
4657
|
return IMAGE_MIME_TYPES;
|
|
4012
4658
|
}
|
|
4013
4659
|
});
|
|
4660
|
+
Object.defineProperty(exports, "NotificationsResource", {
|
|
4661
|
+
enumerable: true,
|
|
4662
|
+
get: function() {
|
|
4663
|
+
return NotificationsResource;
|
|
4664
|
+
}
|
|
4665
|
+
});
|
|
4014
4666
|
Object.defineProperty(exports, "PaginationMode", {
|
|
4015
4667
|
enumerable: true,
|
|
4016
4668
|
get: function() {
|
|
@@ -4047,12 +4699,24 @@ Object.defineProperty(exports, "comment", {
|
|
|
4047
4699
|
return comment;
|
|
4048
4700
|
}
|
|
4049
4701
|
});
|
|
4702
|
+
Object.defineProperty(exports, "createFeatureHost", {
|
|
4703
|
+
enumerable: true,
|
|
4704
|
+
get: function() {
|
|
4705
|
+
return createFeatureHost;
|
|
4706
|
+
}
|
|
4707
|
+
});
|
|
4050
4708
|
Object.defineProperty(exports, "createResources", {
|
|
4051
4709
|
enumerable: true,
|
|
4052
4710
|
get: function() {
|
|
4053
4711
|
return createResources;
|
|
4054
4712
|
}
|
|
4055
4713
|
});
|
|
4714
|
+
Object.defineProperty(exports, "createStatusFeature", {
|
|
4715
|
+
enumerable: true,
|
|
4716
|
+
get: function() {
|
|
4717
|
+
return createStatusFeature;
|
|
4718
|
+
}
|
|
4719
|
+
});
|
|
4056
4720
|
Object.defineProperty(exports, "fromStream", {
|
|
4057
4721
|
enumerable: true,
|
|
4058
4722
|
get: function() {
|
|
@@ -4144,4 +4808,4 @@ Object.defineProperty(exports, "utcStampToIso", {
|
|
|
4144
4808
|
}
|
|
4145
4809
|
});
|
|
4146
4810
|
|
|
4147
|
-
//# sourceMappingURL=render-
|
|
4811
|
+
//# sourceMappingURL=render-DNTESBSl.cjs.map
|