itd-api 0.5.0 → 0.7.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 +12 -10
- package/dist/index.cjs +751 -202
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +285 -220
- package/dist/index.d.ts +285 -220
- package/dist/index.js +748 -181
- package/dist/index.js.map +1 -1
- package/dist/{multi-storage-B0r0AInH.js → multi-storage-BUZaLAPO.js} +13 -9
- package/dist/{multi-storage-B0r0AInH.js.map → multi-storage-BUZaLAPO.js.map} +1 -1
- package/dist/{multi-storage-CLqmzq07.d.ts → multi-storage-BUoEoW8f.d.cts} +3 -7
- package/dist/{multi-storage-mBuCOVZY.cjs → multi-storage-Bf84xiO8.cjs} +12 -14
- package/dist/{multi-storage-mBuCOVZY.cjs.map → multi-storage-Bf84xiO8.cjs.map} +1 -1
- package/dist/{multi-storage-s_PcWPGH.d.cts → multi-storage-CQSlI_kn.d.ts} +3 -7
- package/dist/node.cjs +2 -2
- package/dist/node.d.cts +2 -2
- package/dist/node.d.ts +2 -2
- package/dist/node.js +2 -2
- package/dist/{storage-Doe3lpFQ.d.ts → storage-BqMxs76Y.d.cts} +4 -4
- package/dist/{storage-Doe3lpFQ.d.cts → storage-BqMxs76Y.d.ts} +4 -4
- package/dist/{storage-dF8Tio5y.cjs → storage-DnzZPS_9.cjs} +36 -27
- package/dist/{storage-dF8Tio5y.cjs.map → storage-DnzZPS_9.cjs.map} +1 -1
- package/dist/{storage-DWrK3Z4M.js → storage-IHdXw52v.js} +36 -27
- package/dist/{storage-DWrK3Z4M.js.map → storage-IHdXw52v.js.map} +1 -1
- package/dist/web.cjs +1 -1
- package/dist/web.d.cts +1 -1
- package/dist/web.d.ts +1 -1
- package/dist/web.js +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as
|
|
2
|
-
import { A as isItdApiError, B as isItdStateError, C as ItdNotFoundError, D as ItdStateError, E as ItdServerError, F as isItdForbiddenError, I as isItdNotFoundError, L as isItdPhoneVerificationError, M as isItdConflictError, N as isItdError, O as ItdTimeoutError, P as isItdFileError, R as isItdRateLimitError, S as ItdNetworkError, T as ItdRateLimitError, V as isItdValidationError, _ as ItdError, b as ItdFileErrorReason, c as isEnumerableKeyValueStore, d as ItdAbortError, f as ItdApiError, g as ItdConflictError, h as ItdConfigError, i as MemoryKeyValueStore, j as isItdAuthError, k as ItdValidationError, l as withCodec, m as ItdAuthError, n as copySession, o as createKeyValueStore, p as ItdApiErrorKind, r as createTokenStorage, s as createRecordKeyValueStore, t as MemoryTokenStorage, u as withNamespace, v as ItdErrorKind, w as ItdPhoneVerificationError, x as ItdForbiddenError, y as ItdFileError, z as isItdServerError } from "./storage-
|
|
1
|
+
import { C as normalizeBaseUrl, S as joinUrl, _ as REFRESH_COOKIE_PATH, a as scopedTokenStorage, b as hostOf, c as isBoundedFileStream, d as optionalBytes, f as DEFAULT_URL_FILE_MAX_BYTES, g as REFRESH_COOKIE, h as CookieJar, i as isRestorableSession, l as isReadableByteStream, n as controlledTokenStorage, o as resolveFileStreamOptions, p as FileTransferMode, r as createMultiTokenStorage, s as boundedFileStream, t as MemoryMultiTokenStorage, u as fileTooLarge, v as buildQuery, w as originOf, x as isSameSite, y as encodePathSegment } from "./multi-storage-BUZaLAPO.js";
|
|
2
|
+
import { A as isItdApiError, B as isItdStateError, C as ItdNotFoundError, D as ItdStateError, E as ItdServerError, F as isItdForbiddenError, I as isItdNotFoundError, L as isItdPhoneVerificationError, M as isItdConflictError, N as isItdError, O as ItdTimeoutError, P as isItdFileError, R as isItdRateLimitError, S as ItdNetworkError, T as ItdRateLimitError, V as isItdValidationError, _ as ItdError, b as ItdFileErrorReason, c as isEnumerableKeyValueStore, d as ItdAbortError, f as ItdApiError, g as ItdConflictError, h as ItdConfigError, i as MemoryKeyValueStore, j as isItdAuthError, k as ItdValidationError, l as withCodec, m as ItdAuthError, n as copySession, o as createKeyValueStore, p as ItdApiErrorKind, r as createTokenStorage, s as createRecordKeyValueStore, t as MemoryTokenStorage, u as withNamespace, v as ItdErrorKind, w as ItdPhoneVerificationError, x as ItdForbiddenError, y as ItdFileError, z as isItdServerError } from "./storage-IHdXw52v.js";
|
|
3
3
|
//#region src/core/emitter.ts
|
|
4
4
|
/**
|
|
5
5
|
* Минимальный типизированный источник событий.
|
|
@@ -129,6 +129,56 @@ function readTokenSubject(token) {
|
|
|
129
129
|
return readTokenIdentity(token).subject;
|
|
130
130
|
}
|
|
131
131
|
//#endregion
|
|
132
|
+
//#region src/core/buckets.ts
|
|
133
|
+
/**
|
|
134
|
+
* Ёмкость серверных счётчиков частоты, запросов в минуту.
|
|
135
|
+
*
|
|
136
|
+
* Таблица действует до первого ответа бакета; дальше ёмкость берётся из заголовка
|
|
137
|
+
* `x-ratelimit-limit` и заменяет табличную. `default` — счётчик любого пути без
|
|
138
|
+
* собственного правила на сервере.
|
|
139
|
+
*/
|
|
140
|
+
const BUCKET_LIMITS = Object.freeze({
|
|
141
|
+
"posts.stats": 180,
|
|
142
|
+
default: 150,
|
|
143
|
+
feed: 90,
|
|
144
|
+
"posts.like": 85,
|
|
145
|
+
"posts.comments": 80,
|
|
146
|
+
hashtags: 50,
|
|
147
|
+
users: 40,
|
|
148
|
+
notifications: 40,
|
|
149
|
+
"files.get": 40,
|
|
150
|
+
auth: 35,
|
|
151
|
+
"auth.refresh": 25,
|
|
152
|
+
search: 25,
|
|
153
|
+
"comments.like": 22,
|
|
154
|
+
"files.upload": 15,
|
|
155
|
+
"files.remove": 15,
|
|
156
|
+
"posts.comment": 14,
|
|
157
|
+
"hashtags.trending": 13,
|
|
158
|
+
"posts.repost": 7,
|
|
159
|
+
"users.follow": 7,
|
|
160
|
+
"verification.status": 6,
|
|
161
|
+
"posts.create": 5,
|
|
162
|
+
"users.updateMe": 3,
|
|
163
|
+
"reports.create": 3,
|
|
164
|
+
"verification.submit": 3
|
|
165
|
+
});
|
|
166
|
+
/** Счётчик, из которого списывается путь без собственного правила на сервере. */
|
|
167
|
+
const DEFAULT_RATE_LIMIT_BUCKET = "default";
|
|
168
|
+
/** Известно ли библиотеке имя бакета. */
|
|
169
|
+
function isKnownBucket(name) {
|
|
170
|
+
return Object.hasOwn(BUCKET_LIMITS, name);
|
|
171
|
+
}
|
|
172
|
+
/** Реакция на остаток лимита из заголовков ответа. */
|
|
173
|
+
const RateLimitPacing = Object.freeze({
|
|
174
|
+
/** Задержек нет, пока в бакете есть остаток; исчерпанный бакет ждёт `60000 / limit`. */
|
|
175
|
+
React: "react",
|
|
176
|
+
/** Ровный темп в пределах минутного лимита: задержки идут с первого запроса. */
|
|
177
|
+
Smooth: "smooth",
|
|
178
|
+
/** Остаток на темп не влияет; остаётся пауза после `429`. */
|
|
179
|
+
Off: "off"
|
|
180
|
+
});
|
|
181
|
+
//#endregion
|
|
132
182
|
//#region src/core/operations.ts
|
|
133
183
|
/** Семантическая безопасность автоматического повтора операции. */
|
|
134
184
|
const RetrySafety = Object.freeze({
|
|
@@ -156,59 +206,73 @@ const OPERATIONS = freezeOperations({
|
|
|
156
206
|
},
|
|
157
207
|
"auth.signUp": {
|
|
158
208
|
method: "POST",
|
|
159
|
-
retrySafety: RetrySafety.Unsafe
|
|
209
|
+
retrySafety: RetrySafety.Unsafe,
|
|
210
|
+
bucket: "auth"
|
|
160
211
|
},
|
|
161
212
|
"auth.signIn": {
|
|
162
213
|
method: "POST",
|
|
163
|
-
retrySafety: RetrySafety.Safe
|
|
214
|
+
retrySafety: RetrySafety.Safe,
|
|
215
|
+
bucket: "auth"
|
|
164
216
|
},
|
|
165
217
|
"auth.verifyOtp": {
|
|
166
218
|
method: "POST",
|
|
167
|
-
retrySafety: RetrySafety.Unsafe
|
|
219
|
+
retrySafety: RetrySafety.Unsafe,
|
|
220
|
+
bucket: "auth"
|
|
168
221
|
},
|
|
169
222
|
"auth.resendOtp": {
|
|
170
223
|
method: "POST",
|
|
171
|
-
retrySafety: RetrySafety.Unsafe
|
|
224
|
+
retrySafety: RetrySafety.Unsafe,
|
|
225
|
+
bucket: "auth"
|
|
172
226
|
},
|
|
173
227
|
"auth.refresh": {
|
|
174
228
|
method: "POST",
|
|
175
|
-
retrySafety: RetrySafety.Unsafe
|
|
229
|
+
retrySafety: RetrySafety.Unsafe,
|
|
230
|
+
bucket: "auth.refresh"
|
|
176
231
|
},
|
|
177
232
|
"auth.logout": {
|
|
178
233
|
method: "POST",
|
|
179
|
-
retrySafety: RetrySafety.Unsafe
|
|
234
|
+
retrySafety: RetrySafety.Unsafe,
|
|
235
|
+
bucket: "auth"
|
|
180
236
|
},
|
|
181
237
|
"auth.forgotPassword": {
|
|
182
238
|
method: "POST",
|
|
183
|
-
retrySafety: RetrySafety.Unsafe
|
|
239
|
+
retrySafety: RetrySafety.Unsafe,
|
|
240
|
+
bucket: "auth"
|
|
184
241
|
},
|
|
185
242
|
"auth.resetPassword": {
|
|
186
243
|
method: "POST",
|
|
187
|
-
retrySafety: RetrySafety.Unsafe
|
|
244
|
+
retrySafety: RetrySafety.Unsafe,
|
|
245
|
+
bucket: "auth"
|
|
188
246
|
},
|
|
189
247
|
"auth.changePassword": {
|
|
190
248
|
method: "POST",
|
|
191
|
-
retrySafety: RetrySafety.Unsafe
|
|
249
|
+
retrySafety: RetrySafety.Unsafe,
|
|
250
|
+
bucket: "auth"
|
|
192
251
|
},
|
|
193
252
|
"auth.sessions": {
|
|
194
253
|
method: "GET",
|
|
195
|
-
retrySafety: RetrySafety.Safe
|
|
254
|
+
retrySafety: RetrySafety.Safe,
|
|
255
|
+
bucket: "auth"
|
|
196
256
|
},
|
|
197
257
|
"auth.revokeSession": {
|
|
198
258
|
method: "DELETE",
|
|
199
|
-
retrySafety: RetrySafety.Unsafe
|
|
259
|
+
retrySafety: RetrySafety.Unsafe,
|
|
260
|
+
bucket: "auth"
|
|
200
261
|
},
|
|
201
262
|
"auth.revokeOtherSessions": {
|
|
202
263
|
method: "DELETE",
|
|
203
|
-
retrySafety: RetrySafety.Unsafe
|
|
264
|
+
retrySafety: RetrySafety.Unsafe,
|
|
265
|
+
bucket: "auth"
|
|
204
266
|
},
|
|
205
267
|
"users.me": {
|
|
206
268
|
method: "GET",
|
|
207
|
-
retrySafety: RetrySafety.Safe
|
|
269
|
+
retrySafety: RetrySafety.Safe,
|
|
270
|
+
bucket: "users"
|
|
208
271
|
},
|
|
209
272
|
"users.updateMe": {
|
|
210
273
|
method: "PUT",
|
|
211
|
-
retrySafety: RetrySafety.Idempotent
|
|
274
|
+
retrySafety: RetrySafety.Idempotent,
|
|
275
|
+
bucket: "users.updateMe"
|
|
212
276
|
},
|
|
213
277
|
"users.deactivate": {
|
|
214
278
|
method: "DELETE",
|
|
@@ -224,39 +288,48 @@ const OPERATIONS = freezeOperations({
|
|
|
224
288
|
},
|
|
225
289
|
"users.get": {
|
|
226
290
|
method: "GET",
|
|
227
|
-
retrySafety: RetrySafety.Safe
|
|
291
|
+
retrySafety: RetrySafety.Safe,
|
|
292
|
+
bucket: "users"
|
|
228
293
|
},
|
|
229
294
|
"users.checkUsername": {
|
|
230
295
|
method: "GET",
|
|
231
|
-
retrySafety: RetrySafety.Safe
|
|
296
|
+
retrySafety: RetrySafety.Safe,
|
|
297
|
+
bucket: "users"
|
|
232
298
|
},
|
|
233
299
|
"users.search": {
|
|
234
300
|
method: "GET",
|
|
235
|
-
retrySafety: RetrySafety.Safe
|
|
301
|
+
retrySafety: RetrySafety.Safe,
|
|
302
|
+
bucket: "users"
|
|
236
303
|
},
|
|
237
304
|
"users.whoToFollow": {
|
|
238
305
|
method: "GET",
|
|
239
|
-
retrySafety: RetrySafety.Safe
|
|
306
|
+
retrySafety: RetrySafety.Safe,
|
|
307
|
+
bucket: "users"
|
|
240
308
|
},
|
|
241
309
|
"users.topClans": {
|
|
242
310
|
method: "GET",
|
|
243
|
-
retrySafety: RetrySafety.Safe
|
|
311
|
+
retrySafety: RetrySafety.Safe,
|
|
312
|
+
bucket: "users"
|
|
244
313
|
},
|
|
245
314
|
"users.follow": {
|
|
246
315
|
method: "POST",
|
|
247
|
-
retrySafety: RetrySafety.Unsafe
|
|
316
|
+
retrySafety: RetrySafety.Unsafe,
|
|
317
|
+
bucket: "users.follow"
|
|
248
318
|
},
|
|
249
319
|
"users.unfollow": {
|
|
250
320
|
method: "DELETE",
|
|
251
|
-
retrySafety: RetrySafety.Unsafe
|
|
321
|
+
retrySafety: RetrySafety.Unsafe,
|
|
322
|
+
bucket: "users.follow"
|
|
252
323
|
},
|
|
253
324
|
"users.followers": {
|
|
254
325
|
method: "GET",
|
|
255
|
-
retrySafety: RetrySafety.Safe
|
|
326
|
+
retrySafety: RetrySafety.Safe,
|
|
327
|
+
bucket: "users"
|
|
256
328
|
},
|
|
257
329
|
"users.following": {
|
|
258
330
|
method: "GET",
|
|
259
|
-
retrySafety: RetrySafety.Safe
|
|
331
|
+
retrySafety: RetrySafety.Safe,
|
|
332
|
+
bucket: "users"
|
|
260
333
|
},
|
|
261
334
|
"users.followStatus": {
|
|
262
335
|
method: "POST",
|
|
@@ -272,11 +345,13 @@ const OPERATIONS = freezeOperations({
|
|
|
272
345
|
},
|
|
273
346
|
"users.blocked": {
|
|
274
347
|
method: "GET",
|
|
275
|
-
retrySafety: RetrySafety.Safe
|
|
348
|
+
retrySafety: RetrySafety.Safe,
|
|
349
|
+
bucket: "users"
|
|
276
350
|
},
|
|
277
351
|
"users.getPrivacy": {
|
|
278
352
|
method: "GET",
|
|
279
|
-
retrySafety: RetrySafety.Safe
|
|
353
|
+
retrySafety: RetrySafety.Safe,
|
|
354
|
+
bucket: "users"
|
|
280
355
|
},
|
|
281
356
|
"users.updatePrivacy": {
|
|
282
357
|
method: "PUT",
|
|
@@ -284,7 +359,8 @@ const OPERATIONS = freezeOperations({
|
|
|
284
359
|
},
|
|
285
360
|
"users.pins": {
|
|
286
361
|
method: "GET",
|
|
287
|
-
retrySafety: RetrySafety.Safe
|
|
362
|
+
retrySafety: RetrySafety.Safe,
|
|
363
|
+
bucket: "users"
|
|
288
364
|
},
|
|
289
365
|
"users.setPin": {
|
|
290
366
|
method: "PUT",
|
|
@@ -296,11 +372,13 @@ const OPERATIONS = freezeOperations({
|
|
|
296
372
|
},
|
|
297
373
|
"posts.list": {
|
|
298
374
|
method: "GET",
|
|
299
|
-
retrySafety: RetrySafety.Safe
|
|
375
|
+
retrySafety: RetrySafety.Safe,
|
|
376
|
+
bucket: "feed"
|
|
300
377
|
},
|
|
301
378
|
"posts.create": {
|
|
302
379
|
method: "POST",
|
|
303
|
-
retrySafety: RetrySafety.Unsafe
|
|
380
|
+
retrySafety: RetrySafety.Unsafe,
|
|
381
|
+
bucket: "posts.create"
|
|
304
382
|
},
|
|
305
383
|
"posts.get": {
|
|
306
384
|
method: "GET",
|
|
@@ -320,19 +398,23 @@ const OPERATIONS = freezeOperations({
|
|
|
320
398
|
},
|
|
321
399
|
"posts.like": {
|
|
322
400
|
method: "POST",
|
|
323
|
-
retrySafety: RetrySafety.Unsafe
|
|
401
|
+
retrySafety: RetrySafety.Unsafe,
|
|
402
|
+
bucket: "posts.like"
|
|
324
403
|
},
|
|
325
404
|
"posts.unlike": {
|
|
326
405
|
method: "DELETE",
|
|
327
|
-
retrySafety: RetrySafety.Unsafe
|
|
406
|
+
retrySafety: RetrySafety.Unsafe,
|
|
407
|
+
bucket: "posts.like"
|
|
328
408
|
},
|
|
329
409
|
"posts.repost": {
|
|
330
410
|
method: "POST",
|
|
331
|
-
retrySafety: RetrySafety.Unsafe
|
|
411
|
+
retrySafety: RetrySafety.Unsafe,
|
|
412
|
+
bucket: "posts.repost"
|
|
332
413
|
},
|
|
333
414
|
"posts.unrepost": {
|
|
334
415
|
method: "DELETE",
|
|
335
|
-
retrySafety: RetrySafety.Unsafe
|
|
416
|
+
retrySafety: RetrySafety.Unsafe,
|
|
417
|
+
bucket: "posts.repost"
|
|
336
418
|
},
|
|
337
419
|
"posts.pin": {
|
|
338
420
|
method: "POST",
|
|
@@ -348,7 +430,8 @@ const OPERATIONS = freezeOperations({
|
|
|
348
430
|
},
|
|
349
431
|
"posts.stats": {
|
|
350
432
|
method: "POST",
|
|
351
|
-
retrySafety: RetrySafety.Safe
|
|
433
|
+
retrySafety: RetrySafety.Safe,
|
|
434
|
+
bucket: "posts.stats"
|
|
352
435
|
},
|
|
353
436
|
"posts.byUser": {
|
|
354
437
|
method: "GET",
|
|
@@ -360,11 +443,13 @@ const OPERATIONS = freezeOperations({
|
|
|
360
443
|
},
|
|
361
444
|
"posts.comments": {
|
|
362
445
|
method: "GET",
|
|
363
|
-
retrySafety: RetrySafety.Safe
|
|
446
|
+
retrySafety: RetrySafety.Safe,
|
|
447
|
+
bucket: "posts.comments"
|
|
364
448
|
},
|
|
365
449
|
"posts.comment": {
|
|
366
450
|
method: "POST",
|
|
367
|
-
retrySafety: RetrySafety.Unsafe
|
|
451
|
+
retrySafety: RetrySafety.Unsafe,
|
|
452
|
+
bucket: "posts.comment"
|
|
368
453
|
},
|
|
369
454
|
"comments.replies": {
|
|
370
455
|
method: "GET",
|
|
@@ -388,31 +473,38 @@ const OPERATIONS = freezeOperations({
|
|
|
388
473
|
},
|
|
389
474
|
"comments.like": {
|
|
390
475
|
method: "POST",
|
|
391
|
-
retrySafety: RetrySafety.Unsafe
|
|
476
|
+
retrySafety: RetrySafety.Unsafe,
|
|
477
|
+
bucket: "comments.like"
|
|
392
478
|
},
|
|
393
479
|
"comments.unlike": {
|
|
394
480
|
method: "DELETE",
|
|
395
|
-
retrySafety: RetrySafety.Unsafe
|
|
481
|
+
retrySafety: RetrySafety.Unsafe,
|
|
482
|
+
bucket: "comments.like"
|
|
396
483
|
},
|
|
397
484
|
"files.upload": {
|
|
398
485
|
method: "POST",
|
|
399
|
-
retrySafety: RetrySafety.Unsafe
|
|
486
|
+
retrySafety: RetrySafety.Unsafe,
|
|
487
|
+
bucket: "files.upload"
|
|
400
488
|
},
|
|
401
489
|
"files.get": {
|
|
402
490
|
method: "GET",
|
|
403
|
-
retrySafety: RetrySafety.Safe
|
|
491
|
+
retrySafety: RetrySafety.Safe,
|
|
492
|
+
bucket: "files.get"
|
|
404
493
|
},
|
|
405
494
|
"files.remove": {
|
|
406
495
|
method: "DELETE",
|
|
407
|
-
retrySafety: RetrySafety.Unsafe
|
|
496
|
+
retrySafety: RetrySafety.Unsafe,
|
|
497
|
+
bucket: "files.remove"
|
|
408
498
|
},
|
|
409
499
|
"notifications.list": {
|
|
410
500
|
method: "GET",
|
|
411
|
-
retrySafety: RetrySafety.Safe
|
|
501
|
+
retrySafety: RetrySafety.Safe,
|
|
502
|
+
bucket: "notifications"
|
|
412
503
|
},
|
|
413
504
|
"notifications.count": {
|
|
414
505
|
method: "GET",
|
|
415
|
-
retrySafety: RetrySafety.Safe
|
|
506
|
+
retrySafety: RetrySafety.Safe,
|
|
507
|
+
bucket: "notifications"
|
|
416
508
|
},
|
|
417
509
|
"notifications.markRead": {
|
|
418
510
|
method: "POST",
|
|
@@ -428,31 +520,47 @@ const OPERATIONS = freezeOperations({
|
|
|
428
520
|
},
|
|
429
521
|
"notifications.getSettings": {
|
|
430
522
|
method: "GET",
|
|
431
|
-
retrySafety: RetrySafety.Safe
|
|
523
|
+
retrySafety: RetrySafety.Safe,
|
|
524
|
+
bucket: "notifications"
|
|
432
525
|
},
|
|
433
526
|
"notifications.updateSettings": {
|
|
434
527
|
method: "PUT",
|
|
435
528
|
retrySafety: RetrySafety.Idempotent
|
|
436
529
|
},
|
|
530
|
+
"realtime.poll.updates": {
|
|
531
|
+
method: "GET",
|
|
532
|
+
retrySafety: RetrySafety.Safe,
|
|
533
|
+
bucket: "notifications"
|
|
534
|
+
},
|
|
535
|
+
"realtime.poll.unread": {
|
|
536
|
+
method: "GET",
|
|
537
|
+
retrySafety: RetrySafety.Safe,
|
|
538
|
+
bucket: "notifications"
|
|
539
|
+
},
|
|
437
540
|
"hashtags.search": {
|
|
438
541
|
method: "GET",
|
|
439
|
-
retrySafety: RetrySafety.Safe
|
|
542
|
+
retrySafety: RetrySafety.Safe,
|
|
543
|
+
bucket: "hashtags"
|
|
440
544
|
},
|
|
441
545
|
"hashtags.trending": {
|
|
442
546
|
method: "GET",
|
|
443
|
-
retrySafety: RetrySafety.Safe
|
|
547
|
+
retrySafety: RetrySafety.Safe,
|
|
548
|
+
bucket: "hashtags.trending"
|
|
444
549
|
},
|
|
445
550
|
"hashtags.posts": {
|
|
446
551
|
method: "GET",
|
|
447
|
-
retrySafety: RetrySafety.Safe
|
|
552
|
+
retrySafety: RetrySafety.Safe,
|
|
553
|
+
bucket: "hashtags"
|
|
448
554
|
},
|
|
449
555
|
"search.all": {
|
|
450
556
|
method: "GET",
|
|
451
|
-
retrySafety: RetrySafety.Safe
|
|
557
|
+
retrySafety: RetrySafety.Safe,
|
|
558
|
+
bucket: "search"
|
|
452
559
|
},
|
|
453
560
|
"reports.create": {
|
|
454
561
|
method: "POST",
|
|
455
|
-
retrySafety: RetrySafety.Unsafe
|
|
562
|
+
retrySafety: RetrySafety.Unsafe,
|
|
563
|
+
bucket: "reports.create"
|
|
456
564
|
},
|
|
457
565
|
"subscription.status": {
|
|
458
566
|
method: "GET",
|
|
@@ -484,11 +592,13 @@ const OPERATIONS = freezeOperations({
|
|
|
484
592
|
},
|
|
485
593
|
"verification.status": {
|
|
486
594
|
method: "GET",
|
|
487
|
-
retrySafety: RetrySafety.Safe
|
|
595
|
+
retrySafety: RetrySafety.Safe,
|
|
596
|
+
bucket: "verification.status"
|
|
488
597
|
},
|
|
489
598
|
"verification.submit": {
|
|
490
599
|
method: "POST",
|
|
491
|
-
retrySafety: RetrySafety.Unsafe
|
|
600
|
+
retrySafety: RetrySafety.Unsafe,
|
|
601
|
+
bucket: "verification.submit"
|
|
492
602
|
},
|
|
493
603
|
"platform.version": {
|
|
494
604
|
method: "GET",
|
|
@@ -532,6 +642,16 @@ function operationMethod(id) {
|
|
|
532
642
|
function operationRetrySafety(id) {
|
|
533
643
|
return OPERATIONS[id].retrySafety;
|
|
534
644
|
}
|
|
645
|
+
/**
|
|
646
|
+
* Бакет операции.
|
|
647
|
+
*
|
|
648
|
+
* `raw` и `custom:*` попадают в `default`; назвать бакет явно позволяет
|
|
649
|
+
* `rateLimitBucket` у запроса.
|
|
650
|
+
*/
|
|
651
|
+
function operationBucket(id) {
|
|
652
|
+
if (!isBuiltInOperationId(id)) return DEFAULT_RATE_LIMIT_BUCKET;
|
|
653
|
+
return OPERATIONS[id].bucket ?? "default";
|
|
654
|
+
}
|
|
535
655
|
//#endregion
|
|
536
656
|
//#region src/core/runtime.ts
|
|
537
657
|
/**
|
|
@@ -667,6 +787,11 @@ const AUTH_PATHS = {
|
|
|
667
787
|
* Нужен, чтобы отрисовать виджет капчи и получить токен для `signIn`, `signUp`
|
|
668
788
|
* и `forgotPassword`.
|
|
669
789
|
*
|
|
790
|
+
* Ключ привязан к домену: на чужом origin Cloudflare отказывает виджету с кодом `110200`.
|
|
791
|
+
* Поэтому отрисовать его может только код, выполняемый на самом итд.com. Остальным
|
|
792
|
+
* подходит `@itd-api/turnstile`, готовый токен из другого источника или вовсе вход
|
|
793
|
+
* без капчи — по сохранённой сессии либо по токенам, взятым в браузере.
|
|
794
|
+
*
|
|
670
795
|
* @example
|
|
671
796
|
* ```ts
|
|
672
797
|
* turnstile.render('#captcha', {
|
|
@@ -676,8 +801,6 @@ const AUTH_PATHS = {
|
|
|
676
801
|
* ```
|
|
677
802
|
*/
|
|
678
803
|
const TURNSTILE_SITE_KEY = "0x4AAAAAACHhxczw6fJGwPBg";
|
|
679
|
-
/** Заголовок с идентификатором устройства. Сервер связывает с ним запись в списке сессий. */
|
|
680
|
-
const DEVICE_ID_HEADER = "X-Device-Id";
|
|
681
804
|
function readAccessToken(payload) {
|
|
682
805
|
if (typeof payload !== "object" || payload === null) return void 0;
|
|
683
806
|
const token = payload.accessToken;
|
|
@@ -1182,10 +1305,33 @@ const systemClock = Object.freeze({
|
|
|
1182
1305
|
return () => clearTimeout(timer);
|
|
1183
1306
|
}
|
|
1184
1307
|
});
|
|
1308
|
+
/**
|
|
1309
|
+
* Заводит срок, общий на все ожидания: от ожидания к ожиданию он не продлевается.
|
|
1310
|
+
*
|
|
1311
|
+
* @param timeout срок в миллисекундах; `0` — ждать без ограничения
|
|
1312
|
+
* @internal
|
|
1313
|
+
*/
|
|
1314
|
+
function createDeadline(timeout, clock = systemClock) {
|
|
1315
|
+
if (timeout <= 0) return {
|
|
1316
|
+
wait: async (promise) => {
|
|
1317
|
+
await promise;
|
|
1318
|
+
return true;
|
|
1319
|
+
},
|
|
1320
|
+
cancel: () => {}
|
|
1321
|
+
};
|
|
1322
|
+
let expire;
|
|
1323
|
+
const expired = new Promise((resolve) => {
|
|
1324
|
+
expire = resolve;
|
|
1325
|
+
});
|
|
1326
|
+
return {
|
|
1327
|
+
wait: (promise) => Promise.race([promise.then(() => true), expired.then(() => false)]),
|
|
1328
|
+
cancel: clock.schedule(() => expire(), timeout)
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1185
1331
|
//#endregion
|
|
1186
1332
|
//#region src/core/version.ts
|
|
1187
1333
|
/** Версия библиотеки. Попадает в `User-Agent`. */
|
|
1188
|
-
const LIBRARY_VERSION = "0.
|
|
1334
|
+
const LIBRARY_VERSION = "0.7.0";
|
|
1189
1335
|
//#endregion
|
|
1190
1336
|
//#region src/core/config.ts
|
|
1191
1337
|
/** Базовый URL API итд.com. Домен записан в punycode: `итд.com`. */
|
|
@@ -1200,8 +1346,6 @@ const BUILT_IN_SERVICES = Object.freeze([Object.freeze({
|
|
|
1200
1346
|
baseUrl: DEFAULT_STATUS_BASE_URL,
|
|
1201
1347
|
auth: false
|
|
1202
1348
|
})]);
|
|
1203
|
-
/** Таймаут запроса по умолчанию — 30 секунд. */
|
|
1204
|
-
const DEFAULT_TIMEOUT = 3e4;
|
|
1205
1349
|
/**
|
|
1206
1350
|
* `User-Agent` по умолчанию.
|
|
1207
1351
|
*
|
|
@@ -1216,9 +1360,8 @@ const DEFAULT_USER_AGENT = `Mozilla/5.0 (compatible; itd-api/${LIBRARY_VERSION};
|
|
|
1216
1360
|
/**
|
|
1217
1361
|
* Паузы перед повторами при ответе `429`.
|
|
1218
1362
|
*
|
|
1219
|
-
* Сервер итд.com не присылает `Retry-After` и не сообщает время сброса окна, поэтому
|
|
1220
|
-
*
|
|
1221
|
-
* до полутора минут, если лимит исчерпан всерьёз.
|
|
1363
|
+
* Сервер итд.com не присылает `Retry-After` и не сообщает время сброса окна, поэтому паузу
|
|
1364
|
+
* приходится подбирать лестницей: от секунды, если окно почти истекло, до полутора минут.
|
|
1222
1365
|
*/
|
|
1223
1366
|
const DEFAULT_RATE_LIMIT_DELAYS = Object.freeze([
|
|
1224
1367
|
1e3,
|
|
@@ -1227,6 +1370,13 @@ const DEFAULT_RATE_LIMIT_DELAYS = Object.freeze([
|
|
|
1227
1370
|
6e4,
|
|
1228
1371
|
9e4
|
|
1229
1372
|
]);
|
|
1373
|
+
/**
|
|
1374
|
+
* Встроенные поправки бакетов.
|
|
1375
|
+
*
|
|
1376
|
+
* Таймаут загрузки файла — пять минут против обычных тридцати секунд, поэтому её
|
|
1377
|
+
* одновременность ограничена одним запросом.
|
|
1378
|
+
*/
|
|
1379
|
+
const DEFAULT_BUCKET_OVERRIDES = Object.freeze({ "files.upload": Object.freeze({ concurrency: 1 }) });
|
|
1230
1380
|
function requirePositive(value, name) {
|
|
1231
1381
|
if (!Number.isFinite(value) || value < 0) throw new ItdConfigError(`${name} должен быть неотрицательным числом, получено: ${value}`);
|
|
1232
1382
|
return value;
|
|
@@ -1315,6 +1465,54 @@ function resolveRetry(retry) {
|
|
|
1315
1465
|
};
|
|
1316
1466
|
}
|
|
1317
1467
|
/**
|
|
1468
|
+
* Сверяет имя бакета со встроенной картой.
|
|
1469
|
+
*
|
|
1470
|
+
* Своё правило выбора заводит собственное пространство имён, и встроенные имена в нём
|
|
1471
|
+
* необязательны — тогда проверка снимается.
|
|
1472
|
+
*
|
|
1473
|
+
* @param option путь опции для текста ошибки
|
|
1474
|
+
* @throws {ItdConfigError} если имени нет во встроенной карте
|
|
1475
|
+
*
|
|
1476
|
+
* @internal
|
|
1477
|
+
*/
|
|
1478
|
+
function assertKnownBucket(name, option, bucket) {
|
|
1479
|
+
if (bucket !== void 0 || isKnownBucket(name)) return;
|
|
1480
|
+
throw new ItdConfigError(`${option}: бакета «${name}» нет. Известны: ${Object.keys(BUCKET_LIMITS).join(", ")}`);
|
|
1481
|
+
}
|
|
1482
|
+
function requireConcurrency(value, name) {
|
|
1483
|
+
if (!Number.isInteger(value) || value < 1) throw new ItdConfigError(`${name} должен быть целым числом от 1, получено: ${value}`);
|
|
1484
|
+
return value;
|
|
1485
|
+
}
|
|
1486
|
+
function resolvePacing(pacing) {
|
|
1487
|
+
const known = Object.values(RateLimitPacing);
|
|
1488
|
+
if (pacing !== void 0 && !known.includes(pacing)) throw new ItdConfigError(`rateLimit.pacing должен быть одним из ${known.join(", ")}, получено: ${pacing}`);
|
|
1489
|
+
return pacing ?? RateLimitPacing.React;
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Проверяет поправки бакетов и накладывает их на встроенные.
|
|
1493
|
+
*
|
|
1494
|
+
* Имена сверяются со встроенной картой, пока не задано своё правило выбора бакета.
|
|
1495
|
+
* Поля сливаются по отдельности: своя ёмкость `files.upload` не снимает встроенный
|
|
1496
|
+
* предел одновременности.
|
|
1497
|
+
*/
|
|
1498
|
+
function resolveBucketOverrides(overrides, bucket) {
|
|
1499
|
+
if (overrides === void 0) return DEFAULT_BUCKET_OVERRIDES;
|
|
1500
|
+
if (!isRecord$1(overrides)) throw new ItdConfigError("rateLimit.bucketOverrides должен быть объектом");
|
|
1501
|
+
const resolved = { ...DEFAULT_BUCKET_OVERRIDES };
|
|
1502
|
+
for (const [name, override] of Object.entries(overrides)) {
|
|
1503
|
+
if (!isRecord$1(override)) throw new ItdConfigError(`rateLimit.bucketOverrides.${name} должен быть объектом`);
|
|
1504
|
+
assertKnownBucket(name, "rateLimit.bucketOverrides", bucket);
|
|
1505
|
+
if (override.concurrency !== void 0) requireConcurrency(override.concurrency, `rateLimit.bucketOverrides.${name}.concurrency`);
|
|
1506
|
+
if (override.limit !== void 0 && (!Number.isFinite(override.limit) || override.limit <= 0)) throw new ItdConfigError(`rateLimit.bucketOverrides.${name}.limit должен быть положительным числом, получено: ${override.limit}`);
|
|
1507
|
+
const built = DEFAULT_BUCKET_OVERRIDES[name];
|
|
1508
|
+
resolved[name] = Object.freeze({
|
|
1509
|
+
concurrency: override.concurrency ?? built?.concurrency,
|
|
1510
|
+
limit: override.limit ?? built?.limit
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
return Object.freeze(resolved);
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1318
1516
|
* Приводит настройки очереди к полному виду. `undefined` — очередь не нужна.
|
|
1319
1517
|
*
|
|
1320
1518
|
* Кроме создания клиента вызывается ещё из {@link ItdAccounts}: общая на всех аккаунтов
|
|
@@ -1331,21 +1529,29 @@ function resolveRateLimit(rateLimit) {
|
|
|
1331
1529
|
concurrency: 6,
|
|
1332
1530
|
rps: void 0,
|
|
1333
1531
|
retryDelays: DEFAULT_RATE_LIMIT_DELAYS,
|
|
1334
|
-
|
|
1532
|
+
buckets: true,
|
|
1533
|
+
pacing: RateLimitPacing.React,
|
|
1534
|
+
bucketConcurrency: 6,
|
|
1535
|
+
bucketOverrides: DEFAULT_BUCKET_OVERRIDES,
|
|
1536
|
+
bucket: void 0
|
|
1335
1537
|
};
|
|
1336
1538
|
if (!rateLimit) return defaults;
|
|
1337
|
-
const concurrency = rateLimit.concurrency ?? 6;
|
|
1338
|
-
if (!Number.isInteger(concurrency) || concurrency < 1) throw new ItdConfigError(`rateLimit.concurrency должен быть целым числом от 1, получено: ${concurrency}`);
|
|
1539
|
+
const concurrency = requireConcurrency(rateLimit.concurrency ?? 6, "rateLimit.concurrency");
|
|
1339
1540
|
if (rateLimit.rps !== void 0 && (!Number.isFinite(rateLimit.rps) || rateLimit.rps <= 0)) throw new ItdConfigError(`rateLimit.rps должен быть положительным числом, получено: ${rateLimit.rps}`);
|
|
1340
1541
|
const retryDelays = rateLimit.retryDelays ?? defaults.retryDelays;
|
|
1341
1542
|
if (!Array.isArray(retryDelays)) throw new ItdConfigError("rateLimit.retryDelays должен быть массивом чисел");
|
|
1342
1543
|
for (const delay of retryDelays) requirePositive(delay, "rateLimit.retryDelays");
|
|
1343
|
-
requireOptionalBoolean(rateLimit.
|
|
1544
|
+
requireOptionalBoolean(rateLimit.buckets, "rateLimit.buckets");
|
|
1545
|
+
if (rateLimit.bucket !== void 0 && typeof rateLimit.bucket !== "function") throw new ItdConfigError("rateLimit.bucket должен быть функцией");
|
|
1344
1546
|
return {
|
|
1345
1547
|
concurrency,
|
|
1346
1548
|
rps: rateLimit.rps,
|
|
1347
1549
|
retryDelays: [...retryDelays],
|
|
1348
|
-
|
|
1550
|
+
buckets: rateLimit.buckets ?? true,
|
|
1551
|
+
pacing: resolvePacing(rateLimit.pacing),
|
|
1552
|
+
bucketConcurrency: requireConcurrency(rateLimit.bucketConcurrency ?? concurrency, "rateLimit.bucketConcurrency"),
|
|
1553
|
+
bucketOverrides: resolveBucketOverrides(rateLimit.bucketOverrides, rateLimit.bucket),
|
|
1554
|
+
bucket: rateLimit.bucket
|
|
1349
1555
|
};
|
|
1350
1556
|
}
|
|
1351
1557
|
/**
|
|
@@ -1411,6 +1617,7 @@ function resolveConfig(options = {}) {
|
|
|
1411
1617
|
const mode = options.mode ?? RuntimeMode.Auto;
|
|
1412
1618
|
if (!Object.values(RuntimeMode).includes(mode)) throw new ItdConfigError(`mode должен быть одним из ${Object.values(RuntimeMode).join(", ")}, получено: ${mode}`);
|
|
1413
1619
|
const timeout = requirePositive(options.timeout ?? 3e4, "timeout");
|
|
1620
|
+
const shutdownTimeout = requirePositive(options.shutdownTimeout ?? 1e4, "shutdownTimeout");
|
|
1414
1621
|
if (options.clock !== void 0 && (typeof options.clock !== "object" || options.clock === null || typeof options.clock.now !== "function" || typeof options.clock.schedule !== "function")) throw new ItdConfigError("clock должен предоставлять методы now() и schedule()");
|
|
1415
1622
|
requireOptionalBoolean(options.autoRefresh, "autoRefresh");
|
|
1416
1623
|
requireOptionalBoolean(options.reloginOnRefreshFailure, "reloginOnRefreshFailure");
|
|
@@ -1426,6 +1633,7 @@ function resolveConfig(options = {}) {
|
|
|
1426
1633
|
fetch: resolveFetch(options.fetch),
|
|
1427
1634
|
clock: options.clock ?? systemClock,
|
|
1428
1635
|
timeout,
|
|
1636
|
+
shutdownTimeout,
|
|
1429
1637
|
retry: resolveRetry(options.retry),
|
|
1430
1638
|
rateLimit: resolveRateLimit(options.rateLimit),
|
|
1431
1639
|
hooks: resolveHooks(options.hooks),
|
|
@@ -1441,6 +1649,7 @@ function resolveConfig(options = {}) {
|
|
|
1441
1649
|
//#endregion
|
|
1442
1650
|
//#region src/core/pipeline.ts
|
|
1443
1651
|
const REQUEST_ATTEMPT_STATE = Symbol("itd-api.request-attempt-state");
|
|
1652
|
+
const REQUEST_QUEUE_KEY = Symbol("itd-api.request-queue-key");
|
|
1444
1653
|
const DISPOSE_CLEANUP_REQUEST = Symbol("itd-api.dispose-cleanup-request");
|
|
1445
1654
|
/** Один раз присваивает низкоуровневому запросу семантический ID до входа в middleware. */
|
|
1446
1655
|
function identifyRequest(request) {
|
|
@@ -1472,6 +1681,23 @@ function beginTransportAttempt(request) {
|
|
|
1472
1681
|
function currentTransportAttempt(request) {
|
|
1473
1682
|
return request[REQUEST_ATTEMPT_STATE]?.value ?? 0;
|
|
1474
1683
|
}
|
|
1684
|
+
/**
|
|
1685
|
+
* Вычисляет ключ очереди один раз на логическую операцию.
|
|
1686
|
+
*
|
|
1687
|
+
* Ключ спрашивают трижды: при постановке в очередь, при чтении заголовков ответа и при
|
|
1688
|
+
* паузе после `429`. Значение пишется прямо в объект запроса — слои ниже копируют его
|
|
1689
|
+
* через spread, и перечислимое символьное поле переходит в копии.
|
|
1690
|
+
*
|
|
1691
|
+
* @internal
|
|
1692
|
+
*/
|
|
1693
|
+
function requestQueueKey(request, compute) {
|
|
1694
|
+
const internal = request;
|
|
1695
|
+
const cached = internal[REQUEST_QUEUE_KEY];
|
|
1696
|
+
if (cached) return cached;
|
|
1697
|
+
const key = compute(request);
|
|
1698
|
+
internal[REQUEST_QUEUE_KEY] = key;
|
|
1699
|
+
return key;
|
|
1700
|
+
}
|
|
1475
1701
|
/** Помечает запрос как часть внутренней финализации уже начатого `dispose()`. @internal */
|
|
1476
1702
|
function markDisposeCleanupRequest(request) {
|
|
1477
1703
|
return {
|
|
@@ -1816,7 +2042,7 @@ function createRetryMiddleware(deps) {
|
|
|
1816
2042
|
const transportAttempt = currentTransportAttempt(trackedRequest);
|
|
1817
2043
|
if (isItdRateLimitError(error)) rateLimitAttempt += 1;
|
|
1818
2044
|
else retryAttempt += 1;
|
|
1819
|
-
const delay = nextDelay(error, retryAttempt, rateLimitAttempt,
|
|
2045
|
+
const delay = nextDelay(error, retryAttempt, rateLimitAttempt, trackedRequest, policy, backoff);
|
|
1820
2046
|
if (delay === void 0) throw error;
|
|
1821
2047
|
await dispatchRequestHook(deps.hooks, "onRetry", {
|
|
1822
2048
|
operationId: request.operationId,
|
|
@@ -1994,7 +2220,11 @@ var PluginRegistry = class {
|
|
|
1994
2220
|
#entries = /* @__PURE__ */ new Map();
|
|
1995
2221
|
#removing = /* @__PURE__ */ new Set();
|
|
1996
2222
|
#cleanups = /* @__PURE__ */ new Set();
|
|
2223
|
+
#options;
|
|
1997
2224
|
#ordered = [];
|
|
2225
|
+
constructor(options) {
|
|
2226
|
+
this.#options = options;
|
|
2227
|
+
}
|
|
1998
2228
|
/** Сколько плагинов подключено. */
|
|
1999
2229
|
get size() {
|
|
2000
2230
|
return this.#entries.size;
|
|
@@ -2068,9 +2298,11 @@ var PluginRegistry = class {
|
|
|
2068
2298
|
* Отключает плагин и вызывает его функцию очистки.
|
|
2069
2299
|
*
|
|
2070
2300
|
* Новые запросы перестают видеть расширения плагина сразу. Снимок уже начавшейся операции,
|
|
2071
|
-
* включая её будущие retry, остаётся неизменным; очистка дождётся завершения
|
|
2301
|
+
* включая её будущие retry, остаётся неизменным; очистка дождётся завершения операции,
|
|
2302
|
+
* но не дольше отведённого срока.
|
|
2072
2303
|
*
|
|
2073
2304
|
* @returns `false`, если такого плагина не было
|
|
2305
|
+
* @throws {ItdStateError} если операции плагина не завершились за отведённый срок
|
|
2074
2306
|
*/
|
|
2075
2307
|
async remove(name) {
|
|
2076
2308
|
const entry = this.#entries.get(name);
|
|
@@ -2079,21 +2311,24 @@ var PluginRegistry = class {
|
|
|
2079
2311
|
this.#entries.delete(name);
|
|
2080
2312
|
this.#ordered = this.#ordered.filter((current) => current !== entry);
|
|
2081
2313
|
this.#removing.add(name);
|
|
2314
|
+
const deadline = createDeadline(this.#options.shutdownTimeout, this.#options.clock);
|
|
2082
2315
|
const cleanup = this.#trackCleanup((async () => {
|
|
2083
|
-
await this.#
|
|
2084
|
-
|
|
2316
|
+
const expired = await this.#release(entry, deadline);
|
|
2317
|
+
if (expired) throw expired;
|
|
2085
2318
|
})());
|
|
2086
2319
|
try {
|
|
2087
2320
|
await cleanup;
|
|
2088
2321
|
return true;
|
|
2089
2322
|
} finally {
|
|
2323
|
+
deadline.cancel();
|
|
2090
2324
|
this.#removing.delete(name);
|
|
2091
2325
|
}
|
|
2092
2326
|
}
|
|
2093
2327
|
/**
|
|
2094
2328
|
* Отключает все плагины окончательно.
|
|
2095
2329
|
*
|
|
2096
|
-
* Очистка идёт изнутри наружу — в порядке, обратном выполнению расширений.
|
|
2330
|
+
* Очистка идёт изнутри наружу — в порядке, обратном выполнению расширений. Срок ожидания
|
|
2331
|
+
* общий на все плагины.
|
|
2097
2332
|
*/
|
|
2098
2333
|
async dispose() {
|
|
2099
2334
|
const entries = [...this.#ordered].reverse();
|
|
@@ -2101,13 +2336,14 @@ var PluginRegistry = class {
|
|
|
2101
2336
|
this.#entries.clear();
|
|
2102
2337
|
this.#ordered = [];
|
|
2103
2338
|
for (const { plugin } of entries) this.#removing.add(plugin.name);
|
|
2104
|
-
|
|
2339
|
+
const deadline = createDeadline(this.#options.shutdownTimeout, this.#options.clock);
|
|
2340
|
+
const cleanup = this.#trackCleanup((async () => {
|
|
2105
2341
|
const errors = [];
|
|
2106
2342
|
const previous = await Promise.allSettled(previousCleanups);
|
|
2107
2343
|
for (const result of previous) if (result.status === "rejected") errors.push(result.reason);
|
|
2108
2344
|
for (const entry of entries) try {
|
|
2109
|
-
await this.#
|
|
2110
|
-
|
|
2345
|
+
const expired = await this.#release(entry, deadline);
|
|
2346
|
+
if (expired) errors.push(expired);
|
|
2111
2347
|
} catch (error) {
|
|
2112
2348
|
errors.push(error);
|
|
2113
2349
|
} finally {
|
|
@@ -2115,6 +2351,11 @@ var PluginRegistry = class {
|
|
|
2115
2351
|
}
|
|
2116
2352
|
if (errors.length > 0) throw new AggregateError(errors, "Не удалось освободить ресурсы плагинов");
|
|
2117
2353
|
})());
|
|
2354
|
+
try {
|
|
2355
|
+
await cleanup;
|
|
2356
|
+
} finally {
|
|
2357
|
+
deadline.cancel();
|
|
2358
|
+
}
|
|
2118
2359
|
}
|
|
2119
2360
|
/**
|
|
2120
2361
|
* Прогоняет запрос через operation transformers и прикрепляет attempt interceptors.
|
|
@@ -2122,6 +2363,9 @@ var PluginRegistry = class {
|
|
|
2122
2363
|
* Снимки обеих цепочек берутся в начале: `unuse()` влияет на новые операции, но не меняет
|
|
2123
2364
|
* уже выполняющуюся и не удаляет interceptors из её последующих retry.
|
|
2124
2365
|
*
|
|
2366
|
+
* Каждый `next` одноразовый: transformer может завершить операцию сам, но не может породить
|
|
2367
|
+
* вторую — для `posts.create` это была бы вторая публикация.
|
|
2368
|
+
*
|
|
2125
2369
|
* @param execute выполнение логической операции, вызываемое самым внутренним transformer
|
|
2126
2370
|
*/
|
|
2127
2371
|
async run(request, execute) {
|
|
@@ -2132,7 +2376,17 @@ var PluginRegistry = class {
|
|
|
2132
2376
|
interceptor
|
|
2133
2377
|
})));
|
|
2134
2378
|
const scoped = (current) => withAttemptInterceptorScope(current, interceptorScope);
|
|
2135
|
-
const chain = entries.flatMap((entry) => entry.transformers
|
|
2379
|
+
const chain = entries.flatMap((entry) => entry.transformers.map((transformer) => ({
|
|
2380
|
+
plugin: entry.plugin.name,
|
|
2381
|
+
transformer
|
|
2382
|
+
}))).reduceRight((next, { plugin, transformer }) => (current) => {
|
|
2383
|
+
let called = false;
|
|
2384
|
+
return transformer(scoped(current), (prepared) => {
|
|
2385
|
+
if (called) throw new ItdConfigError(`operation transformer плагина «${plugin}» вызвал next() больше одного раза`);
|
|
2386
|
+
called = true;
|
|
2387
|
+
return next(scoped(prepared));
|
|
2388
|
+
});
|
|
2389
|
+
}, (current) => execute(scoped(current)));
|
|
2136
2390
|
try {
|
|
2137
2391
|
return await chain(scoped(request));
|
|
2138
2392
|
} finally {
|
|
@@ -2146,6 +2400,18 @@ var PluginRegistry = class {
|
|
|
2146
2400
|
}
|
|
2147
2401
|
}
|
|
2148
2402
|
}
|
|
2403
|
+
/**
|
|
2404
|
+
* Дожидается операций плагина и освобождает его ресурсы.
|
|
2405
|
+
*
|
|
2406
|
+
* `teardown` выполняется в любом случае, в том числе после истечения срока.
|
|
2407
|
+
*
|
|
2408
|
+
* @returns ошибка истёкшего срока, если ждать пришлось дольше отведённого
|
|
2409
|
+
*/
|
|
2410
|
+
async #release(entry, deadline) {
|
|
2411
|
+
const finished = await deadline.wait(this.#waitForDrain(entry));
|
|
2412
|
+
await entry.teardown?.();
|
|
2413
|
+
return finished ? void 0 : new ItdStateError(`плагин «${entry.plugin.name}» не завершил операции за ${this.#options.shutdownTimeout} мс; ожидание прекращено, ресурсы плагина освобождены`);
|
|
2414
|
+
}
|
|
2149
2415
|
#waitForDrain(entry) {
|
|
2150
2416
|
if (entry.activeRequests === 0) return Promise.resolve();
|
|
2151
2417
|
entry.drain ??= new Promise((resolve) => {
|
|
@@ -2165,6 +2431,10 @@ var PluginRegistry = class {
|
|
|
2165
2431
|
function queueAbortError() {
|
|
2166
2432
|
return new ItdAbortError("Запрос отменён во время ожидания очереди");
|
|
2167
2433
|
}
|
|
2434
|
+
/** Ошибка запроса, которого застала остановка очереди. */
|
|
2435
|
+
function queueStoppedError() {
|
|
2436
|
+
return new ItdAbortError("Клиент закрыт, запрос отменён");
|
|
2437
|
+
}
|
|
2168
2438
|
/**
|
|
2169
2439
|
* Очередь запросов: ограничивает одновременность и частоту.
|
|
2170
2440
|
*
|
|
@@ -2180,6 +2450,7 @@ var RequestQueue = class {
|
|
|
2180
2450
|
#concurrency;
|
|
2181
2451
|
/** Минимальный промежуток между стартами, мс. `0` — без ограничения частоты. */
|
|
2182
2452
|
#minGap;
|
|
2453
|
+
#onDispatch;
|
|
2183
2454
|
#waiting = [];
|
|
2184
2455
|
#active = 0;
|
|
2185
2456
|
/** Момент, раньше которого следующий запрос стартовать не должен. */
|
|
@@ -2189,6 +2460,7 @@ var RequestQueue = class {
|
|
|
2189
2460
|
constructor(options, clock = systemClock) {
|
|
2190
2461
|
this.#concurrency = options.concurrency;
|
|
2191
2462
|
this.#minGap = options.rps ? 1e3 / options.rps : 0;
|
|
2463
|
+
this.#onDispatch = options.onDispatch;
|
|
2192
2464
|
this.#clock = clock;
|
|
2193
2465
|
}
|
|
2194
2466
|
/** Сколько задач выполняется прямо сейчас. */
|
|
@@ -2220,6 +2492,7 @@ var RequestQueue = class {
|
|
|
2220
2492
|
run: () => {
|
|
2221
2493
|
detach();
|
|
2222
2494
|
this.#active += 1;
|
|
2495
|
+
this.#onDispatch?.();
|
|
2223
2496
|
Promise.resolve().then(task).then(resolve, reject).finally(() => {
|
|
2224
2497
|
this.#active -= 1;
|
|
2225
2498
|
this.#drain();
|
|
@@ -2245,16 +2518,10 @@ var RequestQueue = class {
|
|
|
2245
2518
|
this.#cancelTimer();
|
|
2246
2519
|
this.#cancelTimer = void 0;
|
|
2247
2520
|
}
|
|
2248
|
-
this.#nextSlot = 0;
|
|
2249
2521
|
const pending = this.#waiting.splice(0, this.#waiting.length);
|
|
2250
|
-
for (const task of pending) task.cancel(
|
|
2522
|
+
for (const task of pending) task.cancel(queueStoppedError());
|
|
2251
2523
|
}
|
|
2252
|
-
/**
|
|
2253
|
-
* Придерживает всю очередь на заданное время.
|
|
2254
|
-
*
|
|
2255
|
-
* Вызывается при получении `429` с заголовком `Retry-After`: тормозить нужно все запросы,
|
|
2256
|
-
* а не только тот, который наткнулся на лимит, — иначе остальные продолжат добивать API.
|
|
2257
|
-
*/
|
|
2524
|
+
/** Придерживает очередь на заданное время: ждут все её задачи, а не только одна. */
|
|
2258
2525
|
pause(ms) {
|
|
2259
2526
|
if (ms <= 0) return;
|
|
2260
2527
|
this.#nextSlot = Math.max(this.#nextSlot, this.#clock.now() + ms);
|
|
@@ -2285,39 +2552,241 @@ var RequestQueue = class {
|
|
|
2285
2552
|
this.#drain();
|
|
2286
2553
|
}
|
|
2287
2554
|
};
|
|
2555
|
+
/** Длина окна лимита на сервере. */
|
|
2556
|
+
const RATE_LIMIT_WINDOW = 6e4;
|
|
2557
|
+
/**
|
|
2558
|
+
* Пауза при исчерпании бакета неизвестной ёмкости.
|
|
2559
|
+
*
|
|
2560
|
+
* Ёмкость приходит в заголовке вместе с остатком, поэтому случай возможен только у чужого
|
|
2561
|
+
* прокси, который прислал `remaining` без `limit`.
|
|
2562
|
+
*/
|
|
2563
|
+
const UNKNOWN_CAPACITY_PAUSE = 1e3;
|
|
2564
|
+
/**
|
|
2565
|
+
* Очередь одного бакета поверх общей очереди направления.
|
|
2566
|
+
*
|
|
2567
|
+
* Задача занимает слот бакета, затем общий слот направления, поэтому суммарная
|
|
2568
|
+
* одновременность остаётся равной `concurrency`. Пауза бакета удерживает задачу до
|
|
2569
|
+
* захвата общего слота: притормозивший счётчик не занимает общую ёмкость.
|
|
2570
|
+
*
|
|
2571
|
+
* @internal
|
|
2572
|
+
*/
|
|
2573
|
+
var BucketQueue = class {
|
|
2574
|
+
#destination;
|
|
2575
|
+
#bucket;
|
|
2576
|
+
#gate;
|
|
2577
|
+
#shared;
|
|
2578
|
+
#clock;
|
|
2579
|
+
#pacing;
|
|
2580
|
+
/** Ровный темп. Требует раздельных бакетов: без них ёмкость счётчика неизвестна. */
|
|
2581
|
+
#smooth;
|
|
2582
|
+
/**
|
|
2583
|
+
* Пауза на исчерпанный остаток в режиме `buckets: false`; `undefined` — бакеты разделены.
|
|
2584
|
+
*
|
|
2585
|
+
* Одна очередь на направление принимает заголовки всех счётчиков вперемешку, поэтому
|
|
2586
|
+
* `x-ratelimit-limit` принадлежит тому счётчику, который ответил последним, и ёмкость
|
|
2587
|
+
* очереди из него не выводится: ответ `posts.create` с ёмкостью 5 остановил бы всё
|
|
2588
|
+
* направление на двенадцать секунд. Вместо расчёта берётся первая ступень `retryDelays`.
|
|
2589
|
+
*/
|
|
2590
|
+
#flatPause;
|
|
2591
|
+
/** Лимит бакета до первого ответа. */
|
|
2592
|
+
#seedLimit;
|
|
2593
|
+
/** Последнее, что сказал сервер. Живёт и в режиме `off` — ради `rateLimitState()`. */
|
|
2594
|
+
#limit;
|
|
2595
|
+
#remaining;
|
|
2596
|
+
/**
|
|
2597
|
+
* Оценка остатка для режима `smooth`.
|
|
2598
|
+
*
|
|
2599
|
+
* Начинается с единицы, а не с полной ёмкости: где сейчас граница минутного окна,
|
|
2600
|
+
* из ответа не вывести, и считать бакет нетронутым нельзя.
|
|
2601
|
+
*/
|
|
2602
|
+
#tokens = 1;
|
|
2603
|
+
#tokensAt;
|
|
2604
|
+
/**
|
|
2605
|
+
* Номер поколения очереди. `stop()` увеличивает его, отсекая задачи, которые уже взяли
|
|
2606
|
+
* слот бакета, но до общей очереди ещё не дошли.
|
|
2607
|
+
*/
|
|
2608
|
+
#generation = 0;
|
|
2609
|
+
constructor(destination, bucket, shared, options, clock) {
|
|
2610
|
+
this.#destination = destination;
|
|
2611
|
+
this.#bucket = bucket;
|
|
2612
|
+
this.#shared = shared;
|
|
2613
|
+
this.#clock = clock;
|
|
2614
|
+
this.#pacing = options.pacing;
|
|
2615
|
+
this.#smooth = options.buckets && options.pacing === RateLimitPacing.Smooth;
|
|
2616
|
+
this.#flatPause = options.buckets ? void 0 : options.retryDelays[0] ?? 0;
|
|
2617
|
+
this.#seedLimit = options.buckets ? seedLimit(bucket, options) : void 0;
|
|
2618
|
+
this.#gate = new RequestQueue({
|
|
2619
|
+
concurrency: options.buckets ? options.bucketOverrides[bucket]?.concurrency ?? options.bucketConcurrency : options.concurrency,
|
|
2620
|
+
onDispatch: this.#smooth ? () => this.#spend() : void 0
|
|
2621
|
+
}, clock);
|
|
2622
|
+
}
|
|
2623
|
+
/** Имя счётчика. При `buckets: false` — всегда `default`, каким бы ни был запрос. */
|
|
2624
|
+
get bucket() {
|
|
2625
|
+
return this.#bucket;
|
|
2626
|
+
}
|
|
2627
|
+
/** Запросов бакета прошло в общую очередь и ещё не завершилось. */
|
|
2628
|
+
get active() {
|
|
2629
|
+
return this.#gate.active;
|
|
2630
|
+
}
|
|
2631
|
+
/** Запросов бакета ждёт своей очереди. */
|
|
2632
|
+
get pending() {
|
|
2633
|
+
return this.#gate.pending;
|
|
2634
|
+
}
|
|
2635
|
+
/** Ставит запрос в очередь: сначала слот бакета, затем общий слот направления. */
|
|
2636
|
+
schedule(task, signal) {
|
|
2637
|
+
const generation = this.#generation;
|
|
2638
|
+
return this.#gate.schedule(() => {
|
|
2639
|
+
if (generation !== this.#generation) return Promise.reject(queueStoppedError());
|
|
2640
|
+
return this.#shared.schedule(task, signal);
|
|
2641
|
+
}, signal);
|
|
2642
|
+
}
|
|
2643
|
+
/**
|
|
2644
|
+
* Учитывает заголовки ответа.
|
|
2645
|
+
*
|
|
2646
|
+
* Вызывается после каждого ответа, включая ошибочные: сервер списывает квоту одинаково
|
|
2647
|
+
* с `404`, `422` и `200`.
|
|
2648
|
+
*
|
|
2649
|
+
* @returns на сколько миллисекунд придержан бакет; `0` — темп не ограничен
|
|
2650
|
+
*/
|
|
2651
|
+
observe(limit, remaining) {
|
|
2652
|
+
if (limit !== void 0 && Number.isFinite(limit) && limit > 0) this.#limit = limit;
|
|
2653
|
+
if (remaining !== void 0) this.#remaining = remaining;
|
|
2654
|
+
if (this.#pacing === RateLimitPacing.Off || remaining === void 0) return 0;
|
|
2655
|
+
if (this.#flatPause !== void 0) {
|
|
2656
|
+
if (remaining > 0) return 0;
|
|
2657
|
+
this.#gate.pause(this.#flatPause);
|
|
2658
|
+
return this.#flatPause;
|
|
2659
|
+
}
|
|
2660
|
+
const capacity = this.#capacity();
|
|
2661
|
+
if (this.#smooth) {
|
|
2662
|
+
if (capacity === void 0) return 0;
|
|
2663
|
+
this.#refill(capacity);
|
|
2664
|
+
if (remaining < this.#tokens) this.#tokens = remaining;
|
|
2665
|
+
return this.#armPause(capacity);
|
|
2666
|
+
}
|
|
2667
|
+
if (remaining > 0) return 0;
|
|
2668
|
+
const wait = capacity === void 0 ? UNKNOWN_CAPACITY_PAUSE : Math.ceil(RATE_LIMIT_WINDOW / Math.max(capacity, 1));
|
|
2669
|
+
this.#gate.pause(wait);
|
|
2670
|
+
return wait;
|
|
2671
|
+
}
|
|
2672
|
+
/** Придерживает бакет на названное время — путь ответа `429`. Оценка остатка обнуляется. */
|
|
2673
|
+
pause(ms) {
|
|
2674
|
+
if (this.#smooth) {
|
|
2675
|
+
this.#tokens = 0;
|
|
2676
|
+
this.#tokensAt = this.#clock.now();
|
|
2677
|
+
}
|
|
2678
|
+
this.#gate.pause(ms);
|
|
2679
|
+
}
|
|
2680
|
+
/** Снимок для `rateLimitState()`. */
|
|
2681
|
+
state() {
|
|
2682
|
+
return {
|
|
2683
|
+
destination: this.#destination,
|
|
2684
|
+
bucket: this.#bucket,
|
|
2685
|
+
limit: this.#limit,
|
|
2686
|
+
remaining: this.#remaining,
|
|
2687
|
+
active: this.#gate.active,
|
|
2688
|
+
pending: this.#gate.pending
|
|
2689
|
+
};
|
|
2690
|
+
}
|
|
2691
|
+
/**
|
|
2692
|
+
* Останавливает уровень бакета. Общая очередь направления гасится пулом.
|
|
2693
|
+
*
|
|
2694
|
+
*/
|
|
2695
|
+
stop() {
|
|
2696
|
+
this.#generation += 1;
|
|
2697
|
+
this.#gate.stop();
|
|
2698
|
+
}
|
|
2699
|
+
/** Лимит бакета: сказанный сервером, иначе табличный. */
|
|
2700
|
+
#capacity() {
|
|
2701
|
+
return this.#limit ?? this.#seedLimit;
|
|
2702
|
+
}
|
|
2703
|
+
/** Списывает токен на уходящий запрос и придерживает бакет до следующего. */
|
|
2704
|
+
#spend() {
|
|
2705
|
+
const capacity = this.#capacity();
|
|
2706
|
+
if (capacity === void 0) return;
|
|
2707
|
+
this.#refill(capacity);
|
|
2708
|
+
this.#tokens -= 1;
|
|
2709
|
+
this.#armPause(capacity);
|
|
2710
|
+
}
|
|
2711
|
+
/** Возвращает накопленное с прошлой проверки: `limit` единиц за минуту. */
|
|
2712
|
+
#refill(capacity) {
|
|
2713
|
+
const now = this.#clock.now();
|
|
2714
|
+
if (this.#tokensAt !== void 0) {
|
|
2715
|
+
const restored = (now - this.#tokensAt) * capacity / RATE_LIMIT_WINDOW;
|
|
2716
|
+
this.#tokens = Math.min(capacity, this.#tokens + restored);
|
|
2717
|
+
}
|
|
2718
|
+
this.#tokensAt = now;
|
|
2719
|
+
}
|
|
2720
|
+
/** Держит бакет, пока не накопится хотя бы один токен. */
|
|
2721
|
+
#armPause(capacity) {
|
|
2722
|
+
if (this.#tokens >= 1) return 0;
|
|
2723
|
+
const wait = Math.ceil((1 - this.#tokens) * RATE_LIMIT_WINDOW / capacity);
|
|
2724
|
+
this.#gate.pause(wait);
|
|
2725
|
+
return wait;
|
|
2726
|
+
}
|
|
2727
|
+
};
|
|
2728
|
+
/** Лимит бакета до первого ответа: поправка пользователя важнее табличного значения. */
|
|
2729
|
+
function seedLimit(bucket, options) {
|
|
2730
|
+
const override = options.bucketOverrides[bucket]?.limit;
|
|
2731
|
+
if (override !== void 0) return override;
|
|
2732
|
+
return isKnownBucket(bucket) ? BUCKET_LIMITS[bucket] : void 0;
|
|
2733
|
+
}
|
|
2288
2734
|
/**
|
|
2289
|
-
* Очереди по
|
|
2735
|
+
* Очереди по парам «направление — серверный счётчик частоты».
|
|
2290
2736
|
*
|
|
2291
|
-
*
|
|
2292
|
-
*
|
|
2737
|
+
* Направление — origin уже разрешённого URL: разные локальные имена одного хоста делят
|
|
2738
|
+
* лимит, а запрос с разовым внешним `baseUrl` не попадает в основную очередь. Мощность
|
|
2739
|
+
* карты ограничена каталогом операций, поэтому ни TTL, ни вытеснение не нужны.
|
|
2293
2740
|
*
|
|
2294
2741
|
* @internal
|
|
2295
2742
|
*/
|
|
2296
2743
|
var RequestQueuePool = class {
|
|
2297
2744
|
#options;
|
|
2298
2745
|
#clock;
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
#byDestination = /* @__PURE__ */ new Map();
|
|
2746
|
+
/** Ключ `undefined` — основная очередь внутренних клиентов без известного направления. */
|
|
2747
|
+
#destinations = /* @__PURE__ */ new Map();
|
|
2302
2748
|
constructor(options, clock = systemClock) {
|
|
2303
2749
|
this.#options = options;
|
|
2304
2750
|
this.#clock = clock;
|
|
2305
|
-
this.#main = new RequestQueue(options, clock);
|
|
2306
2751
|
}
|
|
2307
|
-
/** Очередь
|
|
2308
|
-
for(destination) {
|
|
2309
|
-
|
|
2310
|
-
let
|
|
2752
|
+
/** Очередь бакета на направлении. При `buckets: false` бакет всегда `default`. */
|
|
2753
|
+
for(destination, bucket = DEFAULT_RATE_LIMIT_BUCKET) {
|
|
2754
|
+
const name = this.#options.buckets ? bucket : DEFAULT_RATE_LIMIT_BUCKET;
|
|
2755
|
+
let entry = this.#destinations.get(destination);
|
|
2756
|
+
if (!entry) {
|
|
2757
|
+
entry = {
|
|
2758
|
+
shared: new RequestQueue(this.#options, this.#clock),
|
|
2759
|
+
buckets: /* @__PURE__ */ new Map()
|
|
2760
|
+
};
|
|
2761
|
+
this.#destinations.set(destination, entry);
|
|
2762
|
+
}
|
|
2763
|
+
let queue = entry.buckets.get(name);
|
|
2311
2764
|
if (!queue) {
|
|
2312
|
-
queue = new
|
|
2313
|
-
|
|
2765
|
+
queue = new BucketQueue(destination, name, entry.shared, this.#options, this.#clock);
|
|
2766
|
+
entry.buckets.set(name, queue);
|
|
2314
2767
|
}
|
|
2315
2768
|
return queue;
|
|
2316
2769
|
}
|
|
2317
|
-
/**
|
|
2770
|
+
/** Снимки всех бакетов, о которых что-то известно. */
|
|
2771
|
+
states() {
|
|
2772
|
+
const states = [];
|
|
2773
|
+
for (const entry of this.#destinations.values()) for (const queue of entry.buckets.values()) states.push(queue.state());
|
|
2774
|
+
return states;
|
|
2775
|
+
}
|
|
2776
|
+
/**
|
|
2777
|
+
* Останавливает оба уровня всех очередей: ожидающие задачи отклоняются, а состояние
|
|
2778
|
+
* счётчиков и отложенные паузы сохраняются до следующего запуска — путь `close()`.
|
|
2779
|
+
*/
|
|
2318
2780
|
stop() {
|
|
2319
|
-
this.#
|
|
2320
|
-
|
|
2781
|
+
for (const entry of this.#destinations.values()) {
|
|
2782
|
+
for (const queue of entry.buckets.values()) queue.stop();
|
|
2783
|
+
entry.shared.stop();
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
/** Останавливает очереди и забывает всё, что известно о счётчиках, — путь `dispose()`. */
|
|
2787
|
+
clear() {
|
|
2788
|
+
this.stop();
|
|
2789
|
+
this.#destinations.clear();
|
|
2321
2790
|
}
|
|
2322
2791
|
};
|
|
2323
2792
|
//#endregion
|
|
@@ -2685,6 +3154,11 @@ function readIntHeader(headers, name) {
|
|
|
2685
3154
|
const value = Number.parseInt(raw, 10);
|
|
2686
3155
|
return Number.isFinite(value) ? value : void 0;
|
|
2687
3156
|
}
|
|
3157
|
+
/** Читает положительное целое число из заголовка. */
|
|
3158
|
+
function readPositiveIntHeader(headers, name) {
|
|
3159
|
+
const value = readIntHeader(headers, name);
|
|
3160
|
+
return value !== void 0 && value > 0 ? value : void 0;
|
|
3161
|
+
}
|
|
2688
3162
|
/**
|
|
2689
3163
|
* Читает сведения об ограничении частоты.
|
|
2690
3164
|
*
|
|
@@ -2693,7 +3167,7 @@ function readIntHeader(headers, name) {
|
|
|
2693
3167
|
*/
|
|
2694
3168
|
function readRateLimit(headers) {
|
|
2695
3169
|
return {
|
|
2696
|
-
limit:
|
|
3170
|
+
limit: readPositiveIntHeader(headers, "x-ratelimit-limit"),
|
|
2697
3171
|
remaining: readIntHeader(headers, "x-ratelimit-remaining")
|
|
2698
3172
|
};
|
|
2699
3173
|
}
|
|
@@ -2842,17 +3316,25 @@ function abortable(promise, signal) {
|
|
|
2842
3316
|
});
|
|
2843
3317
|
}
|
|
2844
3318
|
/**
|
|
2845
|
-
* Объединяет пользовательский `AbortSignal
|
|
3319
|
+
* Объединяет пользовательский `AbortSignal`, сигнал жизни клиента и таймаут.
|
|
2846
3320
|
*
|
|
2847
3321
|
* Реализовано вручную, а не через `AbortSignal.any`: последний появился только в Node 20,
|
|
2848
|
-
* а библиотека поддерживает Node 18.
|
|
3322
|
+
* а библиотека поддерживает Node 18. Причина отмены переносится от источника как есть.
|
|
2849
3323
|
*/
|
|
2850
|
-
function createAbortBundle(userSignal, timeout, clock) {
|
|
3324
|
+
function createAbortBundle(userSignal, lifetimeSignal, timeout, clock) {
|
|
2851
3325
|
const controller = new AbortController();
|
|
2852
3326
|
let timedOut = false;
|
|
2853
|
-
const
|
|
2854
|
-
|
|
2855
|
-
|
|
3327
|
+
const link = (source) => {
|
|
3328
|
+
if (!source) return void 0;
|
|
3329
|
+
if (source.aborted) {
|
|
3330
|
+
controller.abort(source.reason);
|
|
3331
|
+
return;
|
|
3332
|
+
}
|
|
3333
|
+
const onAbort = () => controller.abort(source.reason);
|
|
3334
|
+
source.addEventListener("abort", onAbort, { once: true });
|
|
3335
|
+
return () => source.removeEventListener("abort", onAbort);
|
|
3336
|
+
};
|
|
3337
|
+
const unlink = [link(userSignal), link(lifetimeSignal)];
|
|
2856
3338
|
const cancelTimer = timeout > 0 ? clock.schedule(() => {
|
|
2857
3339
|
timedOut = true;
|
|
2858
3340
|
controller.abort();
|
|
@@ -2862,7 +3344,7 @@ function createAbortBundle(userSignal, timeout, clock) {
|
|
|
2862
3344
|
timedOut: () => timedOut,
|
|
2863
3345
|
cleanup: () => {
|
|
2864
3346
|
cancelTimer?.();
|
|
2865
|
-
|
|
3347
|
+
for (const detach of unlink) detach?.();
|
|
2866
3348
|
}
|
|
2867
3349
|
};
|
|
2868
3350
|
}
|
|
@@ -2899,7 +3381,7 @@ var Transport = class {
|
|
|
2899
3381
|
const headers = await this.#buildHeaders(request, url);
|
|
2900
3382
|
const attempt = request.attempt ?? 1;
|
|
2901
3383
|
const timeout = request.timeout ?? this.#config.timeout;
|
|
2902
|
-
const abort = createAbortBundle(request.signal, timeout, this.#config.clock);
|
|
3384
|
+
const abort = createAbortBundle(request.signal, this.#deps.lifetimeSignal, timeout, this.#config.clock);
|
|
2903
3385
|
const startedAt = this.#config.clock.now();
|
|
2904
3386
|
let cleanupBody;
|
|
2905
3387
|
try {
|
|
@@ -3116,7 +3598,7 @@ var Transport = class {
|
|
|
3116
3598
|
path: request.path
|
|
3117
3599
|
});
|
|
3118
3600
|
if (aborted) {
|
|
3119
|
-
const reason =
|
|
3601
|
+
const reason = abort.signal.reason;
|
|
3120
3602
|
return new ItdAbortError(`Запрос ${method} ${request.path} отменён`, reason !== void 0 ? { cause: reason } : void 0);
|
|
3121
3603
|
}
|
|
3122
3604
|
if (error instanceof ItdError) return error;
|
|
@@ -3157,38 +3639,55 @@ function createServiceRegistry(config) {
|
|
|
3157
3639
|
function createClientRuntime(options = {}, internals = {}) {
|
|
3158
3640
|
const config = resolveConfig(options);
|
|
3159
3641
|
const jar = new CookieJar();
|
|
3160
|
-
const plugins = new PluginRegistry(
|
|
3642
|
+
const plugins = new PluginRegistry({
|
|
3643
|
+
shutdownTimeout: config.shutdownTimeout,
|
|
3644
|
+
clock: config.clock
|
|
3645
|
+
});
|
|
3161
3646
|
const services = createServiceRegistry(config);
|
|
3647
|
+
const lifetime = new AbortController();
|
|
3162
3648
|
const sharedQueues = config.rateLimit ? internals.queues : void 0;
|
|
3163
3649
|
const queues = sharedQueues ?? (config.rateLimit ? new RequestQueuePool(config.rateLimit, config.clock) : void 0);
|
|
3164
3650
|
const ownsQueues = sharedQueues === void 0;
|
|
3165
3651
|
let auth;
|
|
3166
3652
|
let transport;
|
|
3653
|
+
/**
|
|
3654
|
+
* Бакет, из которого спишется запрос.
|
|
3655
|
+
*
|
|
3656
|
+
* Источники по убыванию приоритета: `rateLimitBucket` запроса, правило `rateLimit.bucket`,
|
|
3657
|
+
* каталог операций.
|
|
3658
|
+
*/
|
|
3659
|
+
const bucketFor = (request) => {
|
|
3660
|
+
if (request.rateLimitBucket !== void 0) return request.rateLimitBucket;
|
|
3661
|
+
return config.rateLimit?.bucket?.({
|
|
3662
|
+
operationId: request.operationId,
|
|
3663
|
+
method: request.method,
|
|
3664
|
+
path: request.path
|
|
3665
|
+
}) ?? operationBucket(request.operationId);
|
|
3666
|
+
};
|
|
3667
|
+
const queueKeyFor = (request) => requestQueueKey(request, (target) => ({
|
|
3668
|
+
destination: originOf(transport.buildUrl(target)) || void 0,
|
|
3669
|
+
bucket: bucketFor(target)
|
|
3670
|
+
}));
|
|
3167
3671
|
const queueFor = (request) => {
|
|
3168
|
-
|
|
3169
|
-
|
|
3672
|
+
if (!queues) return void 0;
|
|
3673
|
+
const key = queueKeyFor(request);
|
|
3674
|
+
return queues.for(key.destination, key.bucket);
|
|
3170
3675
|
};
|
|
3171
|
-
/**
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
*/
|
|
3178
|
-
const throttleByHeaders = (limit, remaining, request) => {
|
|
3179
|
-
if (remaining === void 0 || remaining > 0) return;
|
|
3180
|
-
const first = config.rateLimit?.retryDelays[0];
|
|
3181
|
-
if (first === void 0) return;
|
|
3182
|
-
queueFor(request)?.pause(first);
|
|
3183
|
-
config.logger?.debug(`лимит сервера исчерпан (${remaining} из ${limit ?? "?"}), очередь ждёт ${first} мс`);
|
|
3676
|
+
/** Передаёт остаток из заголовков ответа бакету запроса; тот решает, тормозить ли себя. */
|
|
3677
|
+
const observeRateLimit = (limit, remaining, request) => {
|
|
3678
|
+
const queue = queueFor(request);
|
|
3679
|
+
if (!queue) return;
|
|
3680
|
+
const waited = queue.observe(limit, remaining);
|
|
3681
|
+
if (waited > 0) config.logger?.debug(`остаток лимита ${remaining} из ${limit ?? "?"}, бакет ${queue.bucket} ждёт ${waited} мс`);
|
|
3184
3682
|
};
|
|
3185
3683
|
transport = new Transport({
|
|
3186
3684
|
...config,
|
|
3187
3685
|
hooks: config.hooks
|
|
3188
3686
|
}, {
|
|
3687
|
+
onRateLimit: queues ? observeRateLimit : void 0,
|
|
3189
3688
|
cookies: config.useCookieJar ? jar : void 0,
|
|
3190
3689
|
getDeviceId: () => auth.getDeviceId(),
|
|
3191
|
-
|
|
3690
|
+
lifetimeSignal: lifetime.signal
|
|
3192
3691
|
});
|
|
3193
3692
|
const stages = [
|
|
3194
3693
|
{
|
|
@@ -3242,6 +3741,7 @@ function createClientRuntime(options = {}, internals = {}) {
|
|
|
3242
3741
|
const clientHandler = (request) => {
|
|
3243
3742
|
try {
|
|
3244
3743
|
if (!isDisposeCleanupRequest(request)) internals.assertActive?.("выполнить новый запрос");
|
|
3744
|
+
if (request.rateLimitBucket !== void 0) assertKnownBucket(request.rateLimitBucket, "rateLimitBucket", config.rateLimit?.bucket);
|
|
3245
3745
|
} catch (error) {
|
|
3246
3746
|
return Promise.reject(error);
|
|
3247
3747
|
}
|
|
@@ -3262,12 +3762,15 @@ function createClientRuntime(options = {}, internals = {}) {
|
|
|
3262
3762
|
services,
|
|
3263
3763
|
stageOrder,
|
|
3264
3764
|
platformHeaders: (url) => transport.platformHeaders(url),
|
|
3765
|
+
rateLimitState: () => queues?.states() ?? [],
|
|
3265
3766
|
close: () => {
|
|
3266
3767
|
if (ownsQueues) queues?.stop();
|
|
3267
3768
|
},
|
|
3268
|
-
dispose: () => {
|
|
3769
|
+
dispose: async () => {
|
|
3770
|
+
lifetime.abort(new ItdAbortError("Клиент освобождён через dispose(), запрос отменён"));
|
|
3771
|
+
if (ownsQueues) queues?.clear();
|
|
3269
3772
|
auth.dispose();
|
|
3270
|
-
|
|
3773
|
+
await plugins.dispose();
|
|
3271
3774
|
}
|
|
3272
3775
|
};
|
|
3273
3776
|
}
|
|
@@ -3625,7 +4128,12 @@ var RealtimeDispatcher = class {
|
|
|
3625
4128
|
if (index >= 0) this.#handlers.splice(index, 1);
|
|
3626
4129
|
};
|
|
3627
4130
|
}
|
|
3628
|
-
|
|
4131
|
+
/**
|
|
4132
|
+
* Принимает обновление к обработке.
|
|
4133
|
+
*
|
|
4134
|
+
* @param coalesceKey ожидающая работа с тем же ключом заменяется новой
|
|
4135
|
+
*/
|
|
4136
|
+
dispatch(context, coalesceKey) {
|
|
3629
4137
|
let keys;
|
|
3630
4138
|
let middleware;
|
|
3631
4139
|
try {
|
|
@@ -3635,12 +4143,20 @@ var RealtimeDispatcher = class {
|
|
|
3635
4143
|
this.#hooks.middlewareError(error, context);
|
|
3636
4144
|
return;
|
|
3637
4145
|
}
|
|
3638
|
-
this.#queue.
|
|
4146
|
+
const replaced = coalesceKey === void 0 ? -1 : this.#queue.findIndex((pending) => pending.coalesceKey === coalesceKey);
|
|
4147
|
+
if (replaced < 0 && this.#queue.length >= 256) {
|
|
4148
|
+
this.#hooks.overflow();
|
|
4149
|
+
return;
|
|
4150
|
+
}
|
|
4151
|
+
const work = {
|
|
3639
4152
|
context,
|
|
3640
4153
|
middleware,
|
|
3641
4154
|
handlers: [...this.#handlers],
|
|
3642
|
-
keys
|
|
3643
|
-
|
|
4155
|
+
keys,
|
|
4156
|
+
coalesceKey
|
|
4157
|
+
};
|
|
4158
|
+
if (replaced >= 0) this.#queue[replaced] = work;
|
|
4159
|
+
else this.#queue.push(work);
|
|
3644
4160
|
this.#pump();
|
|
3645
4161
|
}
|
|
3646
4162
|
/** Отбрасывает обновления, обработка которых ещё не началась. */
|
|
@@ -3717,14 +4233,6 @@ const RECONNECT_BACKOFF = Object.freeze([
|
|
|
3717
4233
|
16e3,
|
|
3718
4234
|
3e4
|
|
3719
4235
|
]);
|
|
3720
|
-
/** Доля случайного разброса паузы. */
|
|
3721
|
-
const RECONNECT_JITTER = .3;
|
|
3722
|
-
/**
|
|
3723
|
-
* Сколько раз пытаться переподключиться подряд.
|
|
3724
|
-
*
|
|
3725
|
-
* После исчерпания поток сообщает `giveup` и ждёт ручного `connect()`.
|
|
3726
|
-
*/
|
|
3727
|
-
const MAX_RECONNECT_ATTEMPTS = 15;
|
|
3728
4236
|
/**
|
|
3729
4237
|
* Вычисляет паузу перед попыткой переподключения.
|
|
3730
4238
|
*
|
|
@@ -4025,7 +4533,8 @@ var RealtimeEngine = class {
|
|
|
4025
4533
|
}, {
|
|
4026
4534
|
deliver: (context) => this.#deps.deliver(context.update),
|
|
4027
4535
|
middlewareError: (error, context) => this.#reportDispatchError("middlewareError", error, context),
|
|
4028
|
-
handlerError: (error, context) => this.#reportDispatchError("handlerError", error, context)
|
|
4536
|
+
handlerError: (error, context) => this.#reportDispatchError("handlerError", error, context),
|
|
4537
|
+
overflow: () => this.#handleOverflow()
|
|
4029
4538
|
});
|
|
4030
4539
|
}
|
|
4031
4540
|
/** Текущее состояние соединения. */
|
|
@@ -4106,7 +4615,23 @@ var RealtimeEngine = class {
|
|
|
4106
4615
|
/** Пропускает актуальное обновление через цепочку обработчиков. */
|
|
4107
4616
|
#dispatch(update, raw, origin) {
|
|
4108
4617
|
if (!this.#wanted) return;
|
|
4109
|
-
this.#dispatcher.dispatch(this.#deps.createContext(update, raw, origin));
|
|
4618
|
+
this.#dispatcher.dispatch(this.#deps.createContext(update, raw, origin), this.#deps.coalesceKey?.(update));
|
|
4619
|
+
}
|
|
4620
|
+
/**
|
|
4621
|
+
* Очередь обновлений достигла предела: закрывает соединение и переподключается,
|
|
4622
|
+
* когда обработчики разберут очередь.
|
|
4623
|
+
*/
|
|
4624
|
+
#handleOverflow() {
|
|
4625
|
+
const controller = this.#controller;
|
|
4626
|
+
if (!this.#wanted || !controller) return;
|
|
4627
|
+
const generation = this.#generation;
|
|
4628
|
+
controller.abort();
|
|
4629
|
+
this.#controller = void 0;
|
|
4630
|
+
this.#setStatus(RealtimeStatus.Error);
|
|
4631
|
+
const error = /* @__PURE__ */ new Error("Обработчики не успевают за потоком: очередь обновлений переполнена");
|
|
4632
|
+
this.#dispatcher.drain().then(() => {
|
|
4633
|
+
if (this.#isCurrentGeneration(generation) && !this.#controller) this.#scheduleReconnect(error);
|
|
4634
|
+
});
|
|
4110
4635
|
}
|
|
4111
4636
|
/** Выполняет доменную синхронизацию, не роняя подключение из-за её ошибки. */
|
|
4112
4637
|
async #sync(reason, generation, starting) {
|
|
@@ -4131,6 +4656,7 @@ var RealtimeEngine = class {
|
|
|
4131
4656
|
baseUrl: this.#deps.baseUrl,
|
|
4132
4657
|
authorize: this.#deps.authorize,
|
|
4133
4658
|
fetch: this.#deps.fetch,
|
|
4659
|
+
request: this.#deps.request,
|
|
4134
4660
|
baseHeaders: this.#deps.baseHeaders,
|
|
4135
4661
|
getToken: this.#deps.getToken,
|
|
4136
4662
|
signal: controller.signal,
|
|
@@ -4229,7 +4755,12 @@ var RealtimeEngine = class {
|
|
|
4229
4755
|
this.#starting = void 0;
|
|
4230
4756
|
if (this.#isCurrentGeneration(generation)) this.#run(generation);
|
|
4231
4757
|
}
|
|
4232
|
-
/**
|
|
4758
|
+
/**
|
|
4759
|
+
* Завершает автоматические попытки переподключения.
|
|
4760
|
+
*
|
|
4761
|
+
* Владелец узнаёт об этом так же, как при {@link disconnect}. `onClose` вызывается
|
|
4762
|
+
* до событий: обработчик `giveup` может тут же вызвать `connect()`.
|
|
4763
|
+
*/
|
|
4233
4764
|
#giveUp(error) {
|
|
4234
4765
|
this.#wanted = false;
|
|
4235
4766
|
this.#generation += 1;
|
|
@@ -4237,6 +4768,7 @@ var RealtimeEngine = class {
|
|
|
4237
4768
|
this.#attempt = 0;
|
|
4238
4769
|
this.#detachEnvironment?.();
|
|
4239
4770
|
this.#detachEnvironment = void 0;
|
|
4771
|
+
this.#deps.onClose?.();
|
|
4240
4772
|
this.#emitEngine("error", {
|
|
4241
4773
|
error,
|
|
4242
4774
|
willReconnect: false
|
|
@@ -4309,26 +4841,15 @@ var PollTransport = class {
|
|
|
4309
4841
|
this.#limit = options.limit ?? 20;
|
|
4310
4842
|
}
|
|
4311
4843
|
async connect(context) {
|
|
4844
|
+
const request = context.request;
|
|
4845
|
+
if (!request) throw new ItdConfigError("опрос уведомлений выполняется через конвейер клиента; создайте поток вызовом itd.realtime()");
|
|
4312
4846
|
const seen = /* @__PURE__ */ new Set();
|
|
4313
4847
|
let firstRun = true;
|
|
4314
4848
|
let lastUnreadCount;
|
|
4315
4849
|
while (!context.signal.aborted) {
|
|
4316
|
-
const
|
|
4317
|
-
if (context.authorize && !token) throw new UnauthorizedStreamError();
|
|
4318
|
-
const url = `${joinUrl(context.baseUrl, "/api/notifications/")}?limit=${this.#limit}&offset=0`;
|
|
4319
|
-
const headers = await context.baseHeaders(url);
|
|
4320
|
-
headers.set("Accept", "application/json");
|
|
4321
|
-
if (token) headers.set("Authorization", `Bearer ${token}`);
|
|
4322
|
-
const response = await context.fetch(url, {
|
|
4323
|
-
method: "GET",
|
|
4324
|
-
headers,
|
|
4325
|
-
signal: context.signal
|
|
4326
|
-
});
|
|
4327
|
-
if (context.authorize && response.status === 401) throw new UnauthorizedStreamError();
|
|
4328
|
-
if (!response.ok) throw new Error(`Опрос уведомлений вернул статус ${response.status}`);
|
|
4850
|
+
const payload = await this.#readUpdates(request, context.signal);
|
|
4329
4851
|
context.onOpen();
|
|
4330
|
-
const
|
|
4331
|
-
const items = pickArray(typeof body === "object" && body !== null && "data" in body ? body.data : body, "notifications");
|
|
4852
|
+
const items = pickArray(payload, "notifications");
|
|
4332
4853
|
for (const item of [...items].reverse()) {
|
|
4333
4854
|
const id = typeof item.id === "string" ? item.id : void 0;
|
|
4334
4855
|
if (!id || seen.has(id)) continue;
|
|
@@ -4342,7 +4863,7 @@ var PollTransport = class {
|
|
|
4342
4863
|
const excess = [...seen].slice(0, seen.size - this.#limit * 2);
|
|
4343
4864
|
for (const id of excess) seen.delete(id);
|
|
4344
4865
|
}
|
|
4345
|
-
const count = await this.#readCount(
|
|
4866
|
+
const count = await this.#readCount(request, context.signal);
|
|
4346
4867
|
if (count !== void 0 && count !== lastUnreadCount) {
|
|
4347
4868
|
lastUnreadCount = count;
|
|
4348
4869
|
context.onEvent({
|
|
@@ -4354,16 +4875,29 @@ var PollTransport = class {
|
|
|
4354
4875
|
await this.#wait(context.signal);
|
|
4355
4876
|
}
|
|
4356
4877
|
}
|
|
4357
|
-
async #
|
|
4878
|
+
async #readUpdates(request, signal) {
|
|
4358
4879
|
try {
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4880
|
+
return await request({
|
|
4881
|
+
operationId: "realtime.poll.updates",
|
|
4882
|
+
path: "/api/notifications/",
|
|
4883
|
+
query: {
|
|
4884
|
+
limit: this.#limit,
|
|
4885
|
+
offset: 0
|
|
4886
|
+
},
|
|
4887
|
+
signal
|
|
4363
4888
|
});
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4889
|
+
} catch (error) {
|
|
4890
|
+
if (isItdAuthError(error)) throw new UnauthorizedStreamError();
|
|
4891
|
+
throw error;
|
|
4892
|
+
}
|
|
4893
|
+
}
|
|
4894
|
+
async #readCount(request, signal) {
|
|
4895
|
+
try {
|
|
4896
|
+
return pickNumber(await request({
|
|
4897
|
+
operationId: "realtime.poll.unread",
|
|
4898
|
+
path: "/api/notifications/count",
|
|
4899
|
+
signal
|
|
4900
|
+
}), "count", 0);
|
|
4367
4901
|
} catch {
|
|
4368
4902
|
return;
|
|
4369
4903
|
}
|
|
@@ -4723,6 +5257,7 @@ var ItdRealtime = class {
|
|
|
4723
5257
|
baseUrl: deps.baseUrl,
|
|
4724
5258
|
authorize: deps.authorize ?? true,
|
|
4725
5259
|
fetch: deps.fetch,
|
|
5260
|
+
request: deps.request,
|
|
4726
5261
|
clock: deps.clock,
|
|
4727
5262
|
baseHeaders: deps.baseHeaders,
|
|
4728
5263
|
getToken: deps.getToken,
|
|
@@ -4733,6 +5268,7 @@ var ItdRealtime = class {
|
|
|
4733
5268
|
transport: createTransport(deps, options),
|
|
4734
5269
|
handleFrame: (event) => this.#handleFrame(event),
|
|
4735
5270
|
readUpdate: readRealtimeUpdate,
|
|
5271
|
+
coalesceKey: (update) => update.type === RealtimeUpdateType.UnreadCount ? update.type : void 0,
|
|
4736
5272
|
createContext: (update, raw, origin) => ({
|
|
4737
5273
|
update,
|
|
4738
5274
|
stream: this,
|
|
@@ -6009,10 +6545,6 @@ function createMultipartFileBody(file) {
|
|
|
6009
6545
|
cancel
|
|
6010
6546
|
};
|
|
6011
6547
|
}
|
|
6012
|
-
//#endregion
|
|
6013
|
-
//#region src/resources/files.ts
|
|
6014
|
-
/** Таймаут одной попытки загрузки файла — 5 минут. */
|
|
6015
|
-
const DEFAULT_UPLOAD_TIMEOUT = 3e5;
|
|
6016
6548
|
/** Файлы и медиа. */
|
|
6017
6549
|
var FilesResource = class extends BaseResource {
|
|
6018
6550
|
#fetch;
|
|
@@ -8959,6 +9491,22 @@ var ItdClient = class ItdClient {
|
|
|
8959
9491
|
});
|
|
8960
9492
|
}
|
|
8961
9493
|
/**
|
|
9494
|
+
* Остаток серверных лимитов по бакетам, через которые уже проходили запросы.
|
|
9495
|
+
*
|
|
9496
|
+
* Значения берутся из последнего ответа каждого бакета и быстро устаревают: сервер
|
|
9497
|
+
* восстанавливает квоту линейно и границу окна не сообщает. Пустой массив при
|
|
9498
|
+
* `rateLimit: false`. {@link close} снимок сохраняет, {@link dispose} очищает.
|
|
9499
|
+
*
|
|
9500
|
+
* @example
|
|
9501
|
+
* ```ts
|
|
9502
|
+
* const posts = itd.rateLimitState().find((state) => state.bucket === 'posts.create');
|
|
9503
|
+
* if ((posts?.remaining ?? Number.POSITIVE_INFINITY) < 3) await sleep(60_000);
|
|
9504
|
+
* ```
|
|
9505
|
+
*/
|
|
9506
|
+
rateLimitState() {
|
|
9507
|
+
return this.#runtime.rateLimitState();
|
|
9508
|
+
}
|
|
9509
|
+
/**
|
|
8962
9510
|
* Подключает плагин.
|
|
8963
9511
|
*
|
|
8964
9512
|
* Плагин может независимо регистрировать transformer логической операции и interceptor
|
|
@@ -9094,6 +9642,11 @@ var ItdClient = class ItdClient {
|
|
|
9094
9642
|
stream = new ItdRealtime({
|
|
9095
9643
|
baseUrl: this.#runtime.config.baseUrl,
|
|
9096
9644
|
fetch: this.#runtime.config.fetch,
|
|
9645
|
+
request: ({ operationId, path, query, signal }) => this.#runtime.http.operation(operationId, {
|
|
9646
|
+
path,
|
|
9647
|
+
query,
|
|
9648
|
+
signal
|
|
9649
|
+
}),
|
|
9097
9650
|
baseHeaders: (url) => this.#runtime.platformHeaders(url),
|
|
9098
9651
|
getAuthIdentity: () => this.#runtime.auth.getCurrentAuthIdentity(),
|
|
9099
9652
|
getAuthScope: () => this.#runtime.auth.getAuthScope(),
|
|
@@ -9113,13 +9666,16 @@ var ItdClient = class ItdClient {
|
|
|
9113
9666
|
* Освобождает ресурсы клиента: закрывает все потоки уведомлений, отправляет открытые
|
|
9114
9667
|
* накопители {@link telemetry}, затем останавливает очередь запросов.
|
|
9115
9668
|
*
|
|
9116
|
-
* Метод дожидается активных обработчиков
|
|
9117
|
-
* снова; ранее созданный поток можно запустить
|
|
9669
|
+
* Метод дожидается активных обработчиков потока, но не дольше `shutdownTimeout`. После
|
|
9670
|
+
* вызова клиентом можно пользоваться снова; ранее созданный поток можно запустить
|
|
9671
|
+
* повторным `connect()`.
|
|
9118
9672
|
*
|
|
9119
9673
|
* Общая очередь, полученная от {@link ItdAccounts}, не останавливается: её гасит сам
|
|
9120
9674
|
* контейнер, когда закрывает все аккаунты разом.
|
|
9121
9675
|
*
|
|
9122
9676
|
* Терминальное освобождение — это {@link dispose}.
|
|
9677
|
+
*
|
|
9678
|
+
* @throws {ItdStateError} если обработчики потока не завершились за отведённый срок
|
|
9123
9679
|
*/
|
|
9124
9680
|
async close() {
|
|
9125
9681
|
return this.#close(false);
|
|
@@ -9127,22 +9683,30 @@ var ItdClient = class ItdClient {
|
|
|
9127
9683
|
async #close(disposeCleanup) {
|
|
9128
9684
|
const streams = this.#disconnectStreams();
|
|
9129
9685
|
if (disposeCleanup) prepareTelemetryForDispose(this.#telemetry);
|
|
9686
|
+
const { shutdownTimeout, clock } = this.#runtime.config;
|
|
9687
|
+
const deadline = createDeadline(shutdownTimeout, clock);
|
|
9688
|
+
let stuck = [];
|
|
9130
9689
|
try {
|
|
9131
|
-
await Promise.all(streams.map((stream) => stream.drain()));
|
|
9690
|
+
stuck = (await Promise.all(streams.map(async (stream) => await deadline.wait(stream.drain()) ? void 0 : stream))).filter((stream) => stream !== void 0);
|
|
9132
9691
|
if (disposeCleanup) await closeTelemetryForDispose(this.#telemetry);
|
|
9133
9692
|
else await this.#telemetry?.close();
|
|
9134
9693
|
} finally {
|
|
9694
|
+
deadline.cancel();
|
|
9135
9695
|
this.#runtime.close();
|
|
9136
9696
|
}
|
|
9697
|
+
if (stuck.length > 0) throw new ItdStateError(`обработчики потоков (${stuck.map((stream) => stream.transport).join(", ")}) не завершились за ${shutdownTimeout} мс; ожидание прекращено`);
|
|
9137
9698
|
}
|
|
9138
9699
|
/**
|
|
9139
|
-
* Окончательно освобождает клиент: выполняет {@link close}
|
|
9700
|
+
* Окончательно освобождает клиент: выполняет {@link close}, отменяет незавершённые
|
|
9701
|
+
* запросы и отключает все плагины.
|
|
9140
9702
|
*
|
|
9141
9703
|
* Терминальное состояние устанавливается сразу при первом вызове. После этого новые
|
|
9142
9704
|
* запросы, подключение плагинов, регистрация сервисов и создание или повторный запуск
|
|
9143
9705
|
* realtime-потоков завершаются с {@link ItdStateError}. Повторные вызовы возвращают
|
|
9144
9706
|
* тот же результат очистки.
|
|
9145
9707
|
*
|
|
9708
|
+
* Ожидание обработчиков потока и операций плагинов ограничено `shutdownTimeout`.
|
|
9709
|
+
*
|
|
9146
9710
|
* @example
|
|
9147
9711
|
* ```ts
|
|
9148
9712
|
* await using itd = new ItdClient({ auth: token });
|
|
@@ -9312,7 +9876,7 @@ var ItdAccounts = class ItdAccounts {
|
|
|
9312
9876
|
this.#base = base;
|
|
9313
9877
|
this.#storage = storage ?? new MemoryMultiTokenStorage();
|
|
9314
9878
|
this.#plugins = orderPluginDefinitions(plugins ?? []);
|
|
9315
|
-
this.#rateLimitScope = rateLimitScope ?? "
|
|
9879
|
+
this.#rateLimitScope = rateLimitScope ?? "shared";
|
|
9316
9880
|
const rateLimit = this.#rateLimitScope === "shared" ? resolveRateLimit(base.rateLimit) : void 0;
|
|
9317
9881
|
this.#queues = rateLimit ? new RequestQueuePool(rateLimit, base.clock ?? systemClock) : void 0;
|
|
9318
9882
|
const logger = typeof base.logger === "object" ? base.logger : void 0;
|
|
@@ -9604,7 +10168,7 @@ var ItdAccounts = class ItdAccounts {
|
|
|
9604
10168
|
...clients.map((client) => client.dispose()),
|
|
9605
10169
|
...controls.map((control) => control.drain()),
|
|
9606
10170
|
...accountRemovals,
|
|
9607
|
-
Promise.resolve().then(() => this.#queues?.
|
|
10171
|
+
Promise.resolve().then(() => this.#queues?.clear())
|
|
9608
10172
|
]);
|
|
9609
10173
|
this.#plugins.splice(0);
|
|
9610
10174
|
this.#removingPlugins.clear();
|
|
@@ -10121,10 +10685,6 @@ var RealtimeComposer = class RealtimeComposer {
|
|
|
10121
10685
|
return (context, next) => runRealtimeMiddleware(snapshot, context, next);
|
|
10122
10686
|
}
|
|
10123
10687
|
};
|
|
10124
|
-
//#endregion
|
|
10125
|
-
//#region src/realtime/websocket.ts
|
|
10126
|
-
/** Стандартный путь WebSocket-подключения. */
|
|
10127
|
-
const WEBSOCKET_PATH = "/api/ws";
|
|
10128
10688
|
const CONNECTING = 0;
|
|
10129
10689
|
const OPEN = 1;
|
|
10130
10690
|
const NORMAL_CLOSURE = 1e3;
|
|
@@ -10262,6 +10822,7 @@ var WebSocketTransport = class {
|
|
|
10262
10822
|
let discardMessages = false;
|
|
10263
10823
|
let socketError;
|
|
10264
10824
|
let messageQueue = Promise.resolve();
|
|
10825
|
+
let pendingFrames = 0;
|
|
10265
10826
|
let cancelHandshake;
|
|
10266
10827
|
let cancelIdle;
|
|
10267
10828
|
let cancelKeepAlive;
|
|
@@ -10340,7 +10901,13 @@ var WebSocketTransport = class {
|
|
|
10340
10901
|
pending = Promise.resolve({ error });
|
|
10341
10902
|
}
|
|
10342
10903
|
if (pending === void 0) return;
|
|
10904
|
+
if (pendingFrames >= 256) {
|
|
10905
|
+
closeWithError(/* @__PURE__ */ new Error(`WebSocket ${redactUrl(url)}: очередь кадров переполнена`), 4e3, "queue overflow");
|
|
10906
|
+
return;
|
|
10907
|
+
}
|
|
10908
|
+
pendingFrames += 1;
|
|
10343
10909
|
messageQueue = messageQueue.then(async () => {
|
|
10910
|
+
pendingFrames -= 1;
|
|
10344
10911
|
if (settled || discardMessages) return;
|
|
10345
10912
|
const decoded = await pending;
|
|
10346
10913
|
if ("error" in decoded) {
|
|
@@ -10633,6 +11200,6 @@ function renderSpans(content, spans = [], options = {}) {
|
|
|
10633
11200
|
return result;
|
|
10634
11201
|
}
|
|
10635
11202
|
//#endregion
|
|
10636
|
-
export { ALLOWED_MIME_TYPES, AUDIO_MIME_TYPES,
|
|
11203
|
+
export { ALLOWED_MIME_TYPES, AUDIO_MIME_TYPES, AccessType, AttachmentType, BUCKET_LIMITS, CommentSort, DEFAULT_BASE_URL, DEFAULT_RATE_LIMIT_BUCKET, FeedTab, FileTransferMode, IMAGE_MIME_TYPES, IncidentKind, InteractionType, ItdAbortError, ItdAccounts, ItdApiError, ItdApiErrorKind, ItdAuthError, ItdClient, ItdConfigError, ItdConflictError, ItdError, ItdErrorCode, ItdErrorKind, ItdFileError, ItdFileErrorReason, ItdForbiddenError, ItdNetworkError, ItdNotFoundError, ItdPhoneVerificationError, ItdRateLimitError, ItdRealtime, ItdServerError, ItdStateError, ItdTimeoutError, ItdValidationError, LIBRARY_VERSION, LikesVisibility, MemoryKeyValueStore, MemoryMultiTokenStorage, MemoryTokenStorage, NotificationType, OPERATIONS, PaginationMode, Paginator, RateLimitPacing, RealtimeComposer, RealtimeRouter, RealtimeStatus, RealtimeTransportKind, RealtimeUpdateOrigin, RealtimeUpdateType, ReportReason, ReportTargetType, RetrySafety, RuntimeMode, STATUS_SERVICE, ServiceState, SignInStatus, SpanRenderFormat, SpanType, TURNSTILE_SITE_KEY, UnauthorizedStreamError, VIDEO_MIME_TYPES, ViewReason, ViewSource, WallAccess, WebSocketTransport, autoSpans, canonicalNotificationType, comment, createAccounts, createClient, createKeyValueStore, createMultiTokenStorage, createRecordKeyValueStore, createTokenStorage, formatNotificationText, fromStream, fromUrl, isBuilder, isBuiltInOperationId, isEnumerableKeyValueStore, isItdApiError, isItdAuthError, isItdConflictError, isItdError, isItdFileError, isItdForbiddenError, isItdNotFoundError, isItdPhoneVerificationError, isItdRateLimitError, isItdServerError, isItdStateError, isItdValidationError, isKnownNotificationType, isMyProfile, mapPage, markup, normalizeNotification, operationBucket, operationMethod, operationRetrySafety, parseHtml, parseMarkdown, poll, post, renderSpans, report, resolveNotificationUrl, runRealtimeMiddleware, scopedTokenStorage, statusDays, systemClock, toDate, utcStampToIso, withCodec, withNamespace };
|
|
10637
11204
|
|
|
10638
11205
|
//# sourceMappingURL=index.js.map
|