itd-api 0.6.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/dist/index.cjs CHANGED
@@ -130,6 +130,56 @@ function readTokenSubject(token) {
130
130
  return readTokenIdentity(token).subject;
131
131
  }
132
132
  //#endregion
133
+ //#region src/core/buckets.ts
134
+ /**
135
+ * Ёмкость серверных счётчиков частоты, запросов в минуту.
136
+ *
137
+ * Таблица действует до первого ответа бакета; дальше ёмкость берётся из заголовка
138
+ * `x-ratelimit-limit` и заменяет табличную. `default` — счётчик любого пути без
139
+ * собственного правила на сервере.
140
+ */
141
+ const BUCKET_LIMITS = Object.freeze({
142
+ "posts.stats": 180,
143
+ default: 150,
144
+ feed: 90,
145
+ "posts.like": 85,
146
+ "posts.comments": 80,
147
+ hashtags: 50,
148
+ users: 40,
149
+ notifications: 40,
150
+ "files.get": 40,
151
+ auth: 35,
152
+ "auth.refresh": 25,
153
+ search: 25,
154
+ "comments.like": 22,
155
+ "files.upload": 15,
156
+ "files.remove": 15,
157
+ "posts.comment": 14,
158
+ "hashtags.trending": 13,
159
+ "posts.repost": 7,
160
+ "users.follow": 7,
161
+ "verification.status": 6,
162
+ "posts.create": 5,
163
+ "users.updateMe": 3,
164
+ "reports.create": 3,
165
+ "verification.submit": 3
166
+ });
167
+ /** Счётчик, из которого списывается путь без собственного правила на сервере. */
168
+ const DEFAULT_RATE_LIMIT_BUCKET = "default";
169
+ /** Известно ли библиотеке имя бакета. */
170
+ function isKnownBucket(name) {
171
+ return Object.hasOwn(BUCKET_LIMITS, name);
172
+ }
173
+ /** Реакция на остаток лимита из заголовков ответа. */
174
+ const RateLimitPacing = Object.freeze({
175
+ /** Задержек нет, пока в бакете есть остаток; исчерпанный бакет ждёт `60000 / limit`. */
176
+ React: "react",
177
+ /** Ровный темп в пределах минутного лимита: задержки идут с первого запроса. */
178
+ Smooth: "smooth",
179
+ /** Остаток на темп не влияет; остаётся пауза после `429`. */
180
+ Off: "off"
181
+ });
182
+ //#endregion
133
183
  //#region src/core/operations.ts
134
184
  /** Семантическая безопасность автоматического повтора операции. */
135
185
  const RetrySafety = Object.freeze({
@@ -157,59 +207,73 @@ const OPERATIONS = freezeOperations({
157
207
  },
158
208
  "auth.signUp": {
159
209
  method: "POST",
160
- retrySafety: RetrySafety.Unsafe
210
+ retrySafety: RetrySafety.Unsafe,
211
+ bucket: "auth"
161
212
  },
162
213
  "auth.signIn": {
163
214
  method: "POST",
164
- retrySafety: RetrySafety.Safe
215
+ retrySafety: RetrySafety.Safe,
216
+ bucket: "auth"
165
217
  },
166
218
  "auth.verifyOtp": {
167
219
  method: "POST",
168
- retrySafety: RetrySafety.Unsafe
220
+ retrySafety: RetrySafety.Unsafe,
221
+ bucket: "auth"
169
222
  },
170
223
  "auth.resendOtp": {
171
224
  method: "POST",
172
- retrySafety: RetrySafety.Unsafe
225
+ retrySafety: RetrySafety.Unsafe,
226
+ bucket: "auth"
173
227
  },
174
228
  "auth.refresh": {
175
229
  method: "POST",
176
- retrySafety: RetrySafety.Unsafe
230
+ retrySafety: RetrySafety.Unsafe,
231
+ bucket: "auth.refresh"
177
232
  },
178
233
  "auth.logout": {
179
234
  method: "POST",
180
- retrySafety: RetrySafety.Unsafe
235
+ retrySafety: RetrySafety.Unsafe,
236
+ bucket: "auth"
181
237
  },
182
238
  "auth.forgotPassword": {
183
239
  method: "POST",
184
- retrySafety: RetrySafety.Unsafe
240
+ retrySafety: RetrySafety.Unsafe,
241
+ bucket: "auth"
185
242
  },
186
243
  "auth.resetPassword": {
187
244
  method: "POST",
188
- retrySafety: RetrySafety.Unsafe
245
+ retrySafety: RetrySafety.Unsafe,
246
+ bucket: "auth"
189
247
  },
190
248
  "auth.changePassword": {
191
249
  method: "POST",
192
- retrySafety: RetrySafety.Unsafe
250
+ retrySafety: RetrySafety.Unsafe,
251
+ bucket: "auth"
193
252
  },
194
253
  "auth.sessions": {
195
254
  method: "GET",
196
- retrySafety: RetrySafety.Safe
255
+ retrySafety: RetrySafety.Safe,
256
+ bucket: "auth"
197
257
  },
198
258
  "auth.revokeSession": {
199
259
  method: "DELETE",
200
- retrySafety: RetrySafety.Unsafe
260
+ retrySafety: RetrySafety.Unsafe,
261
+ bucket: "auth"
201
262
  },
202
263
  "auth.revokeOtherSessions": {
203
264
  method: "DELETE",
204
- retrySafety: RetrySafety.Unsafe
265
+ retrySafety: RetrySafety.Unsafe,
266
+ bucket: "auth"
205
267
  },
206
268
  "users.me": {
207
269
  method: "GET",
208
- retrySafety: RetrySafety.Safe
270
+ retrySafety: RetrySafety.Safe,
271
+ bucket: "users"
209
272
  },
210
273
  "users.updateMe": {
211
274
  method: "PUT",
212
- retrySafety: RetrySafety.Idempotent
275
+ retrySafety: RetrySafety.Idempotent,
276
+ bucket: "users.updateMe"
213
277
  },
214
278
  "users.deactivate": {
215
279
  method: "DELETE",
@@ -225,39 +289,48 @@ const OPERATIONS = freezeOperations({
225
289
  },
226
290
  "users.get": {
227
291
  method: "GET",
228
- retrySafety: RetrySafety.Safe
292
+ retrySafety: RetrySafety.Safe,
293
+ bucket: "users"
229
294
  },
230
295
  "users.checkUsername": {
231
296
  method: "GET",
232
- retrySafety: RetrySafety.Safe
297
+ retrySafety: RetrySafety.Safe,
298
+ bucket: "users"
233
299
  },
234
300
  "users.search": {
235
301
  method: "GET",
236
- retrySafety: RetrySafety.Safe
302
+ retrySafety: RetrySafety.Safe,
303
+ bucket: "users"
237
304
  },
238
305
  "users.whoToFollow": {
239
306
  method: "GET",
240
- retrySafety: RetrySafety.Safe
307
+ retrySafety: RetrySafety.Safe,
308
+ bucket: "users"
241
309
  },
242
310
  "users.topClans": {
243
311
  method: "GET",
244
- retrySafety: RetrySafety.Safe
312
+ retrySafety: RetrySafety.Safe,
313
+ bucket: "users"
245
314
  },
246
315
  "users.follow": {
247
316
  method: "POST",
248
- retrySafety: RetrySafety.Unsafe
317
+ retrySafety: RetrySafety.Unsafe,
318
+ bucket: "users.follow"
249
319
  },
250
320
  "users.unfollow": {
251
321
  method: "DELETE",
252
- retrySafety: RetrySafety.Unsafe
322
+ retrySafety: RetrySafety.Unsafe,
323
+ bucket: "users.follow"
253
324
  },
254
325
  "users.followers": {
255
326
  method: "GET",
256
- retrySafety: RetrySafety.Safe
327
+ retrySafety: RetrySafety.Safe,
328
+ bucket: "users"
257
329
  },
258
330
  "users.following": {
259
331
  method: "GET",
260
- retrySafety: RetrySafety.Safe
332
+ retrySafety: RetrySafety.Safe,
333
+ bucket: "users"
261
334
  },
262
335
  "users.followStatus": {
263
336
  method: "POST",
@@ -273,11 +346,13 @@ const OPERATIONS = freezeOperations({
273
346
  },
274
347
  "users.blocked": {
275
348
  method: "GET",
276
- retrySafety: RetrySafety.Safe
349
+ retrySafety: RetrySafety.Safe,
350
+ bucket: "users"
277
351
  },
278
352
  "users.getPrivacy": {
279
353
  method: "GET",
280
- retrySafety: RetrySafety.Safe
354
+ retrySafety: RetrySafety.Safe,
355
+ bucket: "users"
281
356
  },
282
357
  "users.updatePrivacy": {
283
358
  method: "PUT",
@@ -285,7 +360,8 @@ const OPERATIONS = freezeOperations({
285
360
  },
286
361
  "users.pins": {
287
362
  method: "GET",
288
- retrySafety: RetrySafety.Safe
363
+ retrySafety: RetrySafety.Safe,
364
+ bucket: "users"
289
365
  },
290
366
  "users.setPin": {
291
367
  method: "PUT",
@@ -297,11 +373,13 @@ const OPERATIONS = freezeOperations({
297
373
  },
298
374
  "posts.list": {
299
375
  method: "GET",
300
- retrySafety: RetrySafety.Safe
376
+ retrySafety: RetrySafety.Safe,
377
+ bucket: "feed"
301
378
  },
302
379
  "posts.create": {
303
380
  method: "POST",
304
- retrySafety: RetrySafety.Unsafe
381
+ retrySafety: RetrySafety.Unsafe,
382
+ bucket: "posts.create"
305
383
  },
306
384
  "posts.get": {
307
385
  method: "GET",
@@ -321,19 +399,23 @@ const OPERATIONS = freezeOperations({
321
399
  },
322
400
  "posts.like": {
323
401
  method: "POST",
324
- retrySafety: RetrySafety.Unsafe
402
+ retrySafety: RetrySafety.Unsafe,
403
+ bucket: "posts.like"
325
404
  },
326
405
  "posts.unlike": {
327
406
  method: "DELETE",
328
- retrySafety: RetrySafety.Unsafe
407
+ retrySafety: RetrySafety.Unsafe,
408
+ bucket: "posts.like"
329
409
  },
330
410
  "posts.repost": {
331
411
  method: "POST",
332
- retrySafety: RetrySafety.Unsafe
412
+ retrySafety: RetrySafety.Unsafe,
413
+ bucket: "posts.repost"
333
414
  },
334
415
  "posts.unrepost": {
335
416
  method: "DELETE",
336
- retrySafety: RetrySafety.Unsafe
417
+ retrySafety: RetrySafety.Unsafe,
418
+ bucket: "posts.repost"
337
419
  },
338
420
  "posts.pin": {
339
421
  method: "POST",
@@ -349,7 +431,8 @@ const OPERATIONS = freezeOperations({
349
431
  },
350
432
  "posts.stats": {
351
433
  method: "POST",
352
- retrySafety: RetrySafety.Safe
434
+ retrySafety: RetrySafety.Safe,
435
+ bucket: "posts.stats"
353
436
  },
354
437
  "posts.byUser": {
355
438
  method: "GET",
@@ -361,11 +444,13 @@ const OPERATIONS = freezeOperations({
361
444
  },
362
445
  "posts.comments": {
363
446
  method: "GET",
364
- retrySafety: RetrySafety.Safe
447
+ retrySafety: RetrySafety.Safe,
448
+ bucket: "posts.comments"
365
449
  },
366
450
  "posts.comment": {
367
451
  method: "POST",
368
- retrySafety: RetrySafety.Unsafe
452
+ retrySafety: RetrySafety.Unsafe,
453
+ bucket: "posts.comment"
369
454
  },
370
455
  "comments.replies": {
371
456
  method: "GET",
@@ -389,31 +474,38 @@ const OPERATIONS = freezeOperations({
389
474
  },
390
475
  "comments.like": {
391
476
  method: "POST",
392
- retrySafety: RetrySafety.Unsafe
477
+ retrySafety: RetrySafety.Unsafe,
478
+ bucket: "comments.like"
393
479
  },
394
480
  "comments.unlike": {
395
481
  method: "DELETE",
396
- retrySafety: RetrySafety.Unsafe
482
+ retrySafety: RetrySafety.Unsafe,
483
+ bucket: "comments.like"
397
484
  },
398
485
  "files.upload": {
399
486
  method: "POST",
400
- retrySafety: RetrySafety.Unsafe
487
+ retrySafety: RetrySafety.Unsafe,
488
+ bucket: "files.upload"
401
489
  },
402
490
  "files.get": {
403
491
  method: "GET",
404
- retrySafety: RetrySafety.Safe
492
+ retrySafety: RetrySafety.Safe,
493
+ bucket: "files.get"
405
494
  },
406
495
  "files.remove": {
407
496
  method: "DELETE",
408
- retrySafety: RetrySafety.Unsafe
497
+ retrySafety: RetrySafety.Unsafe,
498
+ bucket: "files.remove"
409
499
  },
410
500
  "notifications.list": {
411
501
  method: "GET",
412
- retrySafety: RetrySafety.Safe
502
+ retrySafety: RetrySafety.Safe,
503
+ bucket: "notifications"
413
504
  },
414
505
  "notifications.count": {
415
506
  method: "GET",
416
- retrySafety: RetrySafety.Safe
507
+ retrySafety: RetrySafety.Safe,
508
+ bucket: "notifications"
417
509
  },
418
510
  "notifications.markRead": {
419
511
  method: "POST",
@@ -429,7 +521,8 @@ const OPERATIONS = freezeOperations({
429
521
  },
430
522
  "notifications.getSettings": {
431
523
  method: "GET",
432
- retrySafety: RetrySafety.Safe
524
+ retrySafety: RetrySafety.Safe,
525
+ bucket: "notifications"
433
526
  },
434
527
  "notifications.updateSettings": {
435
528
  method: "PUT",
@@ -437,31 +530,38 @@ const OPERATIONS = freezeOperations({
437
530
  },
438
531
  "realtime.poll.updates": {
439
532
  method: "GET",
440
- retrySafety: RetrySafety.Safe
533
+ retrySafety: RetrySafety.Safe,
534
+ bucket: "notifications"
441
535
  },
442
536
  "realtime.poll.unread": {
443
537
  method: "GET",
444
- retrySafety: RetrySafety.Safe
538
+ retrySafety: RetrySafety.Safe,
539
+ bucket: "notifications"
445
540
  },
446
541
  "hashtags.search": {
447
542
  method: "GET",
448
- retrySafety: RetrySafety.Safe
543
+ retrySafety: RetrySafety.Safe,
544
+ bucket: "hashtags"
449
545
  },
450
546
  "hashtags.trending": {
451
547
  method: "GET",
452
- retrySafety: RetrySafety.Safe
548
+ retrySafety: RetrySafety.Safe,
549
+ bucket: "hashtags.trending"
453
550
  },
454
551
  "hashtags.posts": {
455
552
  method: "GET",
456
- retrySafety: RetrySafety.Safe
553
+ retrySafety: RetrySafety.Safe,
554
+ bucket: "hashtags"
457
555
  },
458
556
  "search.all": {
459
557
  method: "GET",
460
- retrySafety: RetrySafety.Safe
558
+ retrySafety: RetrySafety.Safe,
559
+ bucket: "search"
461
560
  },
462
561
  "reports.create": {
463
562
  method: "POST",
464
- retrySafety: RetrySafety.Unsafe
563
+ retrySafety: RetrySafety.Unsafe,
564
+ bucket: "reports.create"
465
565
  },
466
566
  "subscription.status": {
467
567
  method: "GET",
@@ -493,11 +593,13 @@ const OPERATIONS = freezeOperations({
493
593
  },
494
594
  "verification.status": {
495
595
  method: "GET",
496
- retrySafety: RetrySafety.Safe
596
+ retrySafety: RetrySafety.Safe,
597
+ bucket: "verification.status"
497
598
  },
498
599
  "verification.submit": {
499
600
  method: "POST",
500
- retrySafety: RetrySafety.Unsafe
601
+ retrySafety: RetrySafety.Unsafe,
602
+ bucket: "verification.submit"
501
603
  },
502
604
  "platform.version": {
503
605
  method: "GET",
@@ -541,6 +643,16 @@ function operationMethod(id) {
541
643
  function operationRetrySafety(id) {
542
644
  return OPERATIONS[id].retrySafety;
543
645
  }
646
+ /**
647
+ * Бакет операции.
648
+ *
649
+ * `raw` и `custom:*` попадают в `default`; назвать бакет явно позволяет
650
+ * `rateLimitBucket` у запроса.
651
+ */
652
+ function operationBucket(id) {
653
+ if (!isBuiltInOperationId(id)) return DEFAULT_RATE_LIMIT_BUCKET;
654
+ return OPERATIONS[id].bucket ?? "default";
655
+ }
544
656
  //#endregion
545
657
  //#region src/core/runtime.ts
546
658
  /**
@@ -1220,7 +1332,7 @@ function createDeadline(timeout, clock = systemClock) {
1220
1332
  //#endregion
1221
1333
  //#region src/core/version.ts
1222
1334
  /** Версия библиотеки. Попадает в `User-Agent`. */
1223
- const LIBRARY_VERSION = "0.6.0";
1335
+ const LIBRARY_VERSION = "0.7.0";
1224
1336
  //#endregion
1225
1337
  //#region src/core/config.ts
1226
1338
  /** Базовый URL API итд.com. Домен записан в punycode: `итд.com`. */
@@ -1249,9 +1361,8 @@ const DEFAULT_USER_AGENT = `Mozilla/5.0 (compatible; itd-api/${LIBRARY_VERSION};
1249
1361
  /**
1250
1362
  * Паузы перед повторами при ответе `429`.
1251
1363
  *
1252
- * Сервер итд.com не присылает `Retry-After` и не сообщает время сброса окна, поэтому
1253
- * паузу приходится подбирать лестницей: от секунды, если окно почти истекло,
1254
- * до полутора минут, если лимит исчерпан всерьёз.
1364
+ * Сервер итд.com не присылает `Retry-After` и не сообщает время сброса окна, поэтому паузу
1365
+ * приходится подбирать лестницей: от секунды, если окно почти истекло, до полутора минут.
1255
1366
  */
1256
1367
  const DEFAULT_RATE_LIMIT_DELAYS = Object.freeze([
1257
1368
  1e3,
@@ -1260,6 +1371,13 @@ const DEFAULT_RATE_LIMIT_DELAYS = Object.freeze([
1260
1371
  6e4,
1261
1372
  9e4
1262
1373
  ]);
1374
+ /**
1375
+ * Встроенные поправки бакетов.
1376
+ *
1377
+ * Таймаут загрузки файла — пять минут против обычных тридцати секунд, поэтому её
1378
+ * одновременность ограничена одним запросом.
1379
+ */
1380
+ const DEFAULT_BUCKET_OVERRIDES = Object.freeze({ "files.upload": Object.freeze({ concurrency: 1 }) });
1263
1381
  function requirePositive(value, name) {
1264
1382
  if (!Number.isFinite(value) || value < 0) throw new require_storage.ItdConfigError(`${name} должен быть неотрицательным числом, получено: ${value}`);
1265
1383
  return value;
@@ -1348,6 +1466,54 @@ function resolveRetry(retry) {
1348
1466
  };
1349
1467
  }
1350
1468
  /**
1469
+ * Сверяет имя бакета со встроенной картой.
1470
+ *
1471
+ * Своё правило выбора заводит собственное пространство имён, и встроенные имена в нём
1472
+ * необязательны — тогда проверка снимается.
1473
+ *
1474
+ * @param option путь опции для текста ошибки
1475
+ * @throws {ItdConfigError} если имени нет во встроенной карте
1476
+ *
1477
+ * @internal
1478
+ */
1479
+ function assertKnownBucket(name, option, bucket) {
1480
+ if (bucket !== void 0 || isKnownBucket(name)) return;
1481
+ throw new require_storage.ItdConfigError(`${option}: бакета «${name}» нет. Известны: ${Object.keys(BUCKET_LIMITS).join(", ")}`);
1482
+ }
1483
+ function requireConcurrency(value, name) {
1484
+ if (!Number.isInteger(value) || value < 1) throw new require_storage.ItdConfigError(`${name} должен быть целым числом от 1, получено: ${value}`);
1485
+ return value;
1486
+ }
1487
+ function resolvePacing(pacing) {
1488
+ const known = Object.values(RateLimitPacing);
1489
+ if (pacing !== void 0 && !known.includes(pacing)) throw new require_storage.ItdConfigError(`rateLimit.pacing должен быть одним из ${known.join(", ")}, получено: ${pacing}`);
1490
+ return pacing ?? RateLimitPacing.React;
1491
+ }
1492
+ /**
1493
+ * Проверяет поправки бакетов и накладывает их на встроенные.
1494
+ *
1495
+ * Имена сверяются со встроенной картой, пока не задано своё правило выбора бакета.
1496
+ * Поля сливаются по отдельности: своя ёмкость `files.upload` не снимает встроенный
1497
+ * предел одновременности.
1498
+ */
1499
+ function resolveBucketOverrides(overrides, bucket) {
1500
+ if (overrides === void 0) return DEFAULT_BUCKET_OVERRIDES;
1501
+ if (!isRecord$1(overrides)) throw new require_storage.ItdConfigError("rateLimit.bucketOverrides должен быть объектом");
1502
+ const resolved = { ...DEFAULT_BUCKET_OVERRIDES };
1503
+ for (const [name, override] of Object.entries(overrides)) {
1504
+ if (!isRecord$1(override)) throw new require_storage.ItdConfigError(`rateLimit.bucketOverrides.${name} должен быть объектом`);
1505
+ assertKnownBucket(name, "rateLimit.bucketOverrides", bucket);
1506
+ if (override.concurrency !== void 0) requireConcurrency(override.concurrency, `rateLimit.bucketOverrides.${name}.concurrency`);
1507
+ if (override.limit !== void 0 && (!Number.isFinite(override.limit) || override.limit <= 0)) throw new require_storage.ItdConfigError(`rateLimit.bucketOverrides.${name}.limit должен быть положительным числом, получено: ${override.limit}`);
1508
+ const built = DEFAULT_BUCKET_OVERRIDES[name];
1509
+ resolved[name] = Object.freeze({
1510
+ concurrency: override.concurrency ?? built?.concurrency,
1511
+ limit: override.limit ?? built?.limit
1512
+ });
1513
+ }
1514
+ return Object.freeze(resolved);
1515
+ }
1516
+ /**
1351
1517
  * Приводит настройки очереди к полному виду. `undefined` — очередь не нужна.
1352
1518
  *
1353
1519
  * Кроме создания клиента вызывается ещё из {@link ItdAccounts}: общая на всех аккаунтов
@@ -1364,21 +1530,29 @@ function resolveRateLimit(rateLimit) {
1364
1530
  concurrency: 6,
1365
1531
  rps: void 0,
1366
1532
  retryDelays: DEFAULT_RATE_LIMIT_DELAYS,
1367
- respectHeaders: true
1533
+ buckets: true,
1534
+ pacing: RateLimitPacing.React,
1535
+ bucketConcurrency: 6,
1536
+ bucketOverrides: DEFAULT_BUCKET_OVERRIDES,
1537
+ bucket: void 0
1368
1538
  };
1369
1539
  if (!rateLimit) return defaults;
1370
- const concurrency = rateLimit.concurrency ?? 6;
1371
- if (!Number.isInteger(concurrency) || concurrency < 1) throw new require_storage.ItdConfigError(`rateLimit.concurrency должен быть целым числом от 1, получено: ${concurrency}`);
1540
+ const concurrency = requireConcurrency(rateLimit.concurrency ?? 6, "rateLimit.concurrency");
1372
1541
  if (rateLimit.rps !== void 0 && (!Number.isFinite(rateLimit.rps) || rateLimit.rps <= 0)) throw new require_storage.ItdConfigError(`rateLimit.rps должен быть положительным числом, получено: ${rateLimit.rps}`);
1373
1542
  const retryDelays = rateLimit.retryDelays ?? defaults.retryDelays;
1374
1543
  if (!Array.isArray(retryDelays)) throw new require_storage.ItdConfigError("rateLimit.retryDelays должен быть массивом чисел");
1375
1544
  for (const delay of retryDelays) requirePositive(delay, "rateLimit.retryDelays");
1376
- requireOptionalBoolean(rateLimit.respectHeaders, "rateLimit.respectHeaders");
1545
+ requireOptionalBoolean(rateLimit.buckets, "rateLimit.buckets");
1546
+ if (rateLimit.bucket !== void 0 && typeof rateLimit.bucket !== "function") throw new require_storage.ItdConfigError("rateLimit.bucket должен быть функцией");
1377
1547
  return {
1378
1548
  concurrency,
1379
1549
  rps: rateLimit.rps,
1380
1550
  retryDelays: [...retryDelays],
1381
- respectHeaders: rateLimit.respectHeaders ?? true
1551
+ buckets: rateLimit.buckets ?? true,
1552
+ pacing: resolvePacing(rateLimit.pacing),
1553
+ bucketConcurrency: requireConcurrency(rateLimit.bucketConcurrency ?? concurrency, "rateLimit.bucketConcurrency"),
1554
+ bucketOverrides: resolveBucketOverrides(rateLimit.bucketOverrides, rateLimit.bucket),
1555
+ bucket: rateLimit.bucket
1382
1556
  };
1383
1557
  }
1384
1558
  /**
@@ -1476,6 +1650,7 @@ function resolveConfig(options = {}) {
1476
1650
  //#endregion
1477
1651
  //#region src/core/pipeline.ts
1478
1652
  const REQUEST_ATTEMPT_STATE = Symbol("itd-api.request-attempt-state");
1653
+ const REQUEST_QUEUE_KEY = Symbol("itd-api.request-queue-key");
1479
1654
  const DISPOSE_CLEANUP_REQUEST = Symbol("itd-api.dispose-cleanup-request");
1480
1655
  /** Один раз присваивает низкоуровневому запросу семантический ID до входа в middleware. */
1481
1656
  function identifyRequest(request) {
@@ -1507,6 +1682,23 @@ function beginTransportAttempt(request) {
1507
1682
  function currentTransportAttempt(request) {
1508
1683
  return request[REQUEST_ATTEMPT_STATE]?.value ?? 0;
1509
1684
  }
1685
+ /**
1686
+ * Вычисляет ключ очереди один раз на логическую операцию.
1687
+ *
1688
+ * Ключ спрашивают трижды: при постановке в очередь, при чтении заголовков ответа и при
1689
+ * паузе после `429`. Значение пишется прямо в объект запроса — слои ниже копируют его
1690
+ * через spread, и перечислимое символьное поле переходит в копии.
1691
+ *
1692
+ * @internal
1693
+ */
1694
+ function requestQueueKey(request, compute) {
1695
+ const internal = request;
1696
+ const cached = internal[REQUEST_QUEUE_KEY];
1697
+ if (cached) return cached;
1698
+ const key = compute(request);
1699
+ internal[REQUEST_QUEUE_KEY] = key;
1700
+ return key;
1701
+ }
1510
1702
  /** Помечает запрос как часть внутренней финализации уже начатого `dispose()`. @internal */
1511
1703
  function markDisposeCleanupRequest(request) {
1512
1704
  return {
@@ -1851,7 +2043,7 @@ function createRetryMiddleware(deps) {
1851
2043
  const transportAttempt = currentTransportAttempt(trackedRequest);
1852
2044
  if (require_storage.isItdRateLimitError(error)) rateLimitAttempt += 1;
1853
2045
  else retryAttempt += 1;
1854
- const delay = nextDelay(error, retryAttempt, rateLimitAttempt, request, policy, backoff);
2046
+ const delay = nextDelay(error, retryAttempt, rateLimitAttempt, trackedRequest, policy, backoff);
1855
2047
  if (delay === void 0) throw error;
1856
2048
  await dispatchRequestHook(deps.hooks, "onRetry", {
1857
2049
  operationId: request.operationId,
@@ -2240,6 +2432,10 @@ var PluginRegistry = class {
2240
2432
  function queueAbortError() {
2241
2433
  return new require_storage.ItdAbortError("Запрос отменён во время ожидания очереди");
2242
2434
  }
2435
+ /** Ошибка запроса, которого застала остановка очереди. */
2436
+ function queueStoppedError() {
2437
+ return new require_storage.ItdAbortError("Клиент закрыт, запрос отменён");
2438
+ }
2243
2439
  /**
2244
2440
  * Очередь запросов: ограничивает одновременность и частоту.
2245
2441
  *
@@ -2255,6 +2451,7 @@ var RequestQueue = class {
2255
2451
  #concurrency;
2256
2452
  /** Минимальный промежуток между стартами, мс. `0` — без ограничения частоты. */
2257
2453
  #minGap;
2454
+ #onDispatch;
2258
2455
  #waiting = [];
2259
2456
  #active = 0;
2260
2457
  /** Момент, раньше которого следующий запрос стартовать не должен. */
@@ -2264,6 +2461,7 @@ var RequestQueue = class {
2264
2461
  constructor(options, clock = systemClock) {
2265
2462
  this.#concurrency = options.concurrency;
2266
2463
  this.#minGap = options.rps ? 1e3 / options.rps : 0;
2464
+ this.#onDispatch = options.onDispatch;
2267
2465
  this.#clock = clock;
2268
2466
  }
2269
2467
  /** Сколько задач выполняется прямо сейчас. */
@@ -2295,6 +2493,7 @@ var RequestQueue = class {
2295
2493
  run: () => {
2296
2494
  detach();
2297
2495
  this.#active += 1;
2496
+ this.#onDispatch?.();
2298
2497
  Promise.resolve().then(task).then(resolve, reject).finally(() => {
2299
2498
  this.#active -= 1;
2300
2499
  this.#drain();
@@ -2320,16 +2519,10 @@ var RequestQueue = class {
2320
2519
  this.#cancelTimer();
2321
2520
  this.#cancelTimer = void 0;
2322
2521
  }
2323
- this.#nextSlot = 0;
2324
2522
  const pending = this.#waiting.splice(0, this.#waiting.length);
2325
- for (const task of pending) task.cancel(new require_storage.ItdAbortError("Клиент закрыт, запрос отменён"));
2523
+ for (const task of pending) task.cancel(queueStoppedError());
2326
2524
  }
2327
- /**
2328
- * Придерживает всю очередь на заданное время.
2329
- *
2330
- * Вызывается при получении `429` с заголовком `Retry-After`: тормозить нужно все запросы,
2331
- * а не только тот, который наткнулся на лимит, — иначе остальные продолжат добивать API.
2332
- */
2525
+ /** Придерживает очередь на заданное время: ждут все её задачи, а не только одна. */
2333
2526
  pause(ms) {
2334
2527
  if (ms <= 0) return;
2335
2528
  this.#nextSlot = Math.max(this.#nextSlot, this.#clock.now() + ms);
@@ -2360,40 +2553,241 @@ var RequestQueue = class {
2360
2553
  this.#drain();
2361
2554
  }
2362
2555
  };
2556
+ /** Длина окна лимита на сервере. */
2557
+ const RATE_LIMIT_WINDOW = 6e4;
2558
+ /**
2559
+ * Пауза при исчерпании бакета неизвестной ёмкости.
2560
+ *
2561
+ * Ёмкость приходит в заголовке вместе с остатком, поэтому случай возможен только у чужого
2562
+ * прокси, который прислал `remaining` без `limit`.
2563
+ */
2564
+ const UNKNOWN_CAPACITY_PAUSE = 1e3;
2565
+ /**
2566
+ * Очередь одного бакета поверх общей очереди направления.
2567
+ *
2568
+ * Задача занимает слот бакета, затем общий слот направления, поэтому суммарная
2569
+ * одновременность остаётся равной `concurrency`. Пауза бакета удерживает задачу до
2570
+ * захвата общего слота: притормозивший счётчик не занимает общую ёмкость.
2571
+ *
2572
+ * @internal
2573
+ */
2574
+ var BucketQueue = class {
2575
+ #destination;
2576
+ #bucket;
2577
+ #gate;
2578
+ #shared;
2579
+ #clock;
2580
+ #pacing;
2581
+ /** Ровный темп. Требует раздельных бакетов: без них ёмкость счётчика неизвестна. */
2582
+ #smooth;
2583
+ /**
2584
+ * Пауза на исчерпанный остаток в режиме `buckets: false`; `undefined` — бакеты разделены.
2585
+ *
2586
+ * Одна очередь на направление принимает заголовки всех счётчиков вперемешку, поэтому
2587
+ * `x-ratelimit-limit` принадлежит тому счётчику, который ответил последним, и ёмкость
2588
+ * очереди из него не выводится: ответ `posts.create` с ёмкостью 5 остановил бы всё
2589
+ * направление на двенадцать секунд. Вместо расчёта берётся первая ступень `retryDelays`.
2590
+ */
2591
+ #flatPause;
2592
+ /** Лимит бакета до первого ответа. */
2593
+ #seedLimit;
2594
+ /** Последнее, что сказал сервер. Живёт и в режиме `off` — ради `rateLimitState()`. */
2595
+ #limit;
2596
+ #remaining;
2597
+ /**
2598
+ * Оценка остатка для режима `smooth`.
2599
+ *
2600
+ * Начинается с единицы, а не с полной ёмкости: где сейчас граница минутного окна,
2601
+ * из ответа не вывести, и считать бакет нетронутым нельзя.
2602
+ */
2603
+ #tokens = 1;
2604
+ #tokensAt;
2605
+ /**
2606
+ * Номер поколения очереди. `stop()` увеличивает его, отсекая задачи, которые уже взяли
2607
+ * слот бакета, но до общей очереди ещё не дошли.
2608
+ */
2609
+ #generation = 0;
2610
+ constructor(destination, bucket, shared, options, clock) {
2611
+ this.#destination = destination;
2612
+ this.#bucket = bucket;
2613
+ this.#shared = shared;
2614
+ this.#clock = clock;
2615
+ this.#pacing = options.pacing;
2616
+ this.#smooth = options.buckets && options.pacing === RateLimitPacing.Smooth;
2617
+ this.#flatPause = options.buckets ? void 0 : options.retryDelays[0] ?? 0;
2618
+ this.#seedLimit = options.buckets ? seedLimit(bucket, options) : void 0;
2619
+ this.#gate = new RequestQueue({
2620
+ concurrency: options.buckets ? options.bucketOverrides[bucket]?.concurrency ?? options.bucketConcurrency : options.concurrency,
2621
+ onDispatch: this.#smooth ? () => this.#spend() : void 0
2622
+ }, clock);
2623
+ }
2624
+ /** Имя счётчика. При `buckets: false` — всегда `default`, каким бы ни был запрос. */
2625
+ get bucket() {
2626
+ return this.#bucket;
2627
+ }
2628
+ /** Запросов бакета прошло в общую очередь и ещё не завершилось. */
2629
+ get active() {
2630
+ return this.#gate.active;
2631
+ }
2632
+ /** Запросов бакета ждёт своей очереди. */
2633
+ get pending() {
2634
+ return this.#gate.pending;
2635
+ }
2636
+ /** Ставит запрос в очередь: сначала слот бакета, затем общий слот направления. */
2637
+ schedule(task, signal) {
2638
+ const generation = this.#generation;
2639
+ return this.#gate.schedule(() => {
2640
+ if (generation !== this.#generation) return Promise.reject(queueStoppedError());
2641
+ return this.#shared.schedule(task, signal);
2642
+ }, signal);
2643
+ }
2644
+ /**
2645
+ * Учитывает заголовки ответа.
2646
+ *
2647
+ * Вызывается после каждого ответа, включая ошибочные: сервер списывает квоту одинаково
2648
+ * с `404`, `422` и `200`.
2649
+ *
2650
+ * @returns на сколько миллисекунд придержан бакет; `0` — темп не ограничен
2651
+ */
2652
+ observe(limit, remaining) {
2653
+ if (limit !== void 0 && Number.isFinite(limit) && limit > 0) this.#limit = limit;
2654
+ if (remaining !== void 0) this.#remaining = remaining;
2655
+ if (this.#pacing === RateLimitPacing.Off || remaining === void 0) return 0;
2656
+ if (this.#flatPause !== void 0) {
2657
+ if (remaining > 0) return 0;
2658
+ this.#gate.pause(this.#flatPause);
2659
+ return this.#flatPause;
2660
+ }
2661
+ const capacity = this.#capacity();
2662
+ if (this.#smooth) {
2663
+ if (capacity === void 0) return 0;
2664
+ this.#refill(capacity);
2665
+ if (remaining < this.#tokens) this.#tokens = remaining;
2666
+ return this.#armPause(capacity);
2667
+ }
2668
+ if (remaining > 0) return 0;
2669
+ const wait = capacity === void 0 ? UNKNOWN_CAPACITY_PAUSE : Math.ceil(RATE_LIMIT_WINDOW / Math.max(capacity, 1));
2670
+ this.#gate.pause(wait);
2671
+ return wait;
2672
+ }
2673
+ /** Придерживает бакет на названное время — путь ответа `429`. Оценка остатка обнуляется. */
2674
+ pause(ms) {
2675
+ if (this.#smooth) {
2676
+ this.#tokens = 0;
2677
+ this.#tokensAt = this.#clock.now();
2678
+ }
2679
+ this.#gate.pause(ms);
2680
+ }
2681
+ /** Снимок для `rateLimitState()`. */
2682
+ state() {
2683
+ return {
2684
+ destination: this.#destination,
2685
+ bucket: this.#bucket,
2686
+ limit: this.#limit,
2687
+ remaining: this.#remaining,
2688
+ active: this.#gate.active,
2689
+ pending: this.#gate.pending
2690
+ };
2691
+ }
2692
+ /**
2693
+ * Останавливает уровень бакета. Общая очередь направления гасится пулом.
2694
+ *
2695
+ */
2696
+ stop() {
2697
+ this.#generation += 1;
2698
+ this.#gate.stop();
2699
+ }
2700
+ /** Лимит бакета: сказанный сервером, иначе табличный. */
2701
+ #capacity() {
2702
+ return this.#limit ?? this.#seedLimit;
2703
+ }
2704
+ /** Списывает токен на уходящий запрос и придерживает бакет до следующего. */
2705
+ #spend() {
2706
+ const capacity = this.#capacity();
2707
+ if (capacity === void 0) return;
2708
+ this.#refill(capacity);
2709
+ this.#tokens -= 1;
2710
+ this.#armPause(capacity);
2711
+ }
2712
+ /** Возвращает накопленное с прошлой проверки: `limit` единиц за минуту. */
2713
+ #refill(capacity) {
2714
+ const now = this.#clock.now();
2715
+ if (this.#tokensAt !== void 0) {
2716
+ const restored = (now - this.#tokensAt) * capacity / RATE_LIMIT_WINDOW;
2717
+ this.#tokens = Math.min(capacity, this.#tokens + restored);
2718
+ }
2719
+ this.#tokensAt = now;
2720
+ }
2721
+ /** Держит бакет, пока не накопится хотя бы один токен. */
2722
+ #armPause(capacity) {
2723
+ if (this.#tokens >= 1) return 0;
2724
+ const wait = Math.ceil((1 - this.#tokens) * RATE_LIMIT_WINDOW / capacity);
2725
+ this.#gate.pause(wait);
2726
+ return wait;
2727
+ }
2728
+ };
2729
+ /** Лимит бакета до первого ответа: поправка пользователя важнее табличного значения. */
2730
+ function seedLimit(bucket, options) {
2731
+ const override = options.bucketOverrides[bucket]?.limit;
2732
+ if (override !== void 0) return override;
2733
+ return isKnownBucket(bucket) ? BUCKET_LIMITS[bucket] : void 0;
2734
+ }
2363
2735
  /**
2364
- * Очереди по конечным направлениям запросов.
2736
+ * Очереди по парам «направление — серверный счётчик частоты».
2365
2737
  *
2366
- * Ключом обычно служит origin уже разрешённого URL. Поэтому разные локальные имена одного
2367
- * хоста разделяют лимит, а запрос с разовым внешним `baseUrl` не попадает в основную очередь.
2738
+ * Направление origin уже разрешённого URL: разные локальные имена одного хоста делят
2739
+ * лимит, а запрос с разовым внешним `baseUrl` не попадает в основную очередь. Мощность
2740
+ * карты ограничена каталогом операций, поэтому ни TTL, ни вытеснение не нужны.
2368
2741
  *
2369
2742
  * @internal
2370
2743
  */
2371
2744
  var RequestQueuePool = class {
2372
2745
  #options;
2373
2746
  #clock;
2374
- #main;
2375
- /** Очереди направлений заводятся при первом запросе. */
2376
- #byDestination = /* @__PURE__ */ new Map();
2747
+ /** Ключ `undefined` — основная очередь внутренних клиентов без известного направления. */
2748
+ #destinations = /* @__PURE__ */ new Map();
2377
2749
  constructor(options, clock = systemClock) {
2378
2750
  this.#options = options;
2379
2751
  this.#clock = clock;
2380
- this.#main = new RequestQueue(options, clock);
2381
2752
  }
2382
- /** Очередь направления. `undefined` сохраняет основную очередь для внутренних клиентов. */
2383
- for(destination) {
2384
- if (destination === void 0) return this.#main;
2385
- let queue = this.#byDestination.get(destination);
2753
+ /** Очередь бакета на направлении. При `buckets: false` бакет всегда `default`. */
2754
+ for(destination, bucket = DEFAULT_RATE_LIMIT_BUCKET) {
2755
+ const name = this.#options.buckets ? bucket : DEFAULT_RATE_LIMIT_BUCKET;
2756
+ let entry = this.#destinations.get(destination);
2757
+ if (!entry) {
2758
+ entry = {
2759
+ shared: new RequestQueue(this.#options, this.#clock),
2760
+ buckets: /* @__PURE__ */ new Map()
2761
+ };
2762
+ this.#destinations.set(destination, entry);
2763
+ }
2764
+ let queue = entry.buckets.get(name);
2386
2765
  if (!queue) {
2387
- queue = new RequestQueue(this.#options, this.#clock);
2388
- this.#byDestination.set(destination, queue);
2766
+ queue = new BucketQueue(destination, name, entry.shared, this.#options, this.#clock);
2767
+ entry.buckets.set(name, queue);
2389
2768
  }
2390
2769
  return queue;
2391
2770
  }
2392
- /** Останавливает все очереди. */
2771
+ /** Снимки всех бакетов, о которых что-то известно. */
2772
+ states() {
2773
+ const states = [];
2774
+ for (const entry of this.#destinations.values()) for (const queue of entry.buckets.values()) states.push(queue.state());
2775
+ return states;
2776
+ }
2777
+ /**
2778
+ * Останавливает оба уровня всех очередей: ожидающие задачи отклоняются, а состояние
2779
+ * счётчиков и отложенные паузы сохраняются до следующего запуска — путь `close()`.
2780
+ */
2393
2781
  stop() {
2394
- this.#main.stop();
2395
- for (const queue of this.#byDestination.values()) queue.stop();
2396
- this.#byDestination.clear();
2782
+ for (const entry of this.#destinations.values()) {
2783
+ for (const queue of entry.buckets.values()) queue.stop();
2784
+ entry.shared.stop();
2785
+ }
2786
+ }
2787
+ /** Останавливает очереди и забывает всё, что известно о счётчиках, — путь `dispose()`. */
2788
+ clear() {
2789
+ this.stop();
2790
+ this.#destinations.clear();
2397
2791
  }
2398
2792
  };
2399
2793
  //#endregion
@@ -2761,6 +3155,11 @@ function readIntHeader(headers, name) {
2761
3155
  const value = Number.parseInt(raw, 10);
2762
3156
  return Number.isFinite(value) ? value : void 0;
2763
3157
  }
3158
+ /** Читает положительное целое число из заголовка. */
3159
+ function readPositiveIntHeader(headers, name) {
3160
+ const value = readIntHeader(headers, name);
3161
+ return value !== void 0 && value > 0 ? value : void 0;
3162
+ }
2764
3163
  /**
2765
3164
  * Читает сведения об ограничении частоты.
2766
3165
  *
@@ -2769,7 +3168,7 @@ function readIntHeader(headers, name) {
2769
3168
  */
2770
3169
  function readRateLimit(headers) {
2771
3170
  return {
2772
- limit: readIntHeader(headers, "x-ratelimit-limit"),
3171
+ limit: readPositiveIntHeader(headers, "x-ratelimit-limit"),
2773
3172
  remaining: readIntHeader(headers, "x-ratelimit-remaining")
2774
3173
  };
2775
3174
  }
@@ -3252,31 +3651,43 @@ function createClientRuntime(options = {}, internals = {}) {
3252
3651
  const ownsQueues = sharedQueues === void 0;
3253
3652
  let auth;
3254
3653
  let transport;
3654
+ /**
3655
+ * Бакет, из которого спишется запрос.
3656
+ *
3657
+ * Источники по убыванию приоритета: `rateLimitBucket` запроса, правило `rateLimit.bucket`,
3658
+ * каталог операций.
3659
+ */
3660
+ const bucketFor = (request) => {
3661
+ if (request.rateLimitBucket !== void 0) return request.rateLimitBucket;
3662
+ return config.rateLimit?.bucket?.({
3663
+ operationId: request.operationId,
3664
+ method: request.method,
3665
+ path: request.path
3666
+ }) ?? operationBucket(request.operationId);
3667
+ };
3668
+ const queueKeyFor = (request) => requestQueueKey(request, (target) => ({
3669
+ destination: require_multi_storage.originOf(transport.buildUrl(target)) || void 0,
3670
+ bucket: bucketFor(target)
3671
+ }));
3255
3672
  const queueFor = (request) => {
3256
- const destination = require_multi_storage.originOf(transport.buildUrl(request));
3257
- return queues?.for(destination || void 0);
3673
+ if (!queues) return void 0;
3674
+ const key = queueKeyFor(request);
3675
+ return queues.for(key.destination, key.bucket);
3258
3676
  };
3259
- /**
3260
- * Сервер сообщает остаток окна в `x-ratelimit-remaining`, но не сообщает момент сброса.
3261
- * При нуле заранее ставим очередь конечного origin на первую, самую короткую паузу из
3262
- * `retryDelays`: окно могло почти истечь, поэтому длинный backoff здесь преждевременен.
3263
- * Если окно всё ещё закрыто, следующий `429` применит самостоятельную лестницу повторов.
3264
- * Общая пауза очереди не даёт параллельным запросам одновременно ударить в тот же лимит.
3265
- */
3266
- const throttleByHeaders = (limit, remaining, request) => {
3267
- if (remaining === void 0 || remaining > 0) return;
3268
- const first = config.rateLimit?.retryDelays[0];
3269
- if (first === void 0) return;
3270
- queueFor(request)?.pause(first);
3271
- config.logger?.debug(`лимит сервера исчерпан (${remaining} из ${limit ?? "?"}), очередь ждёт ${first} мс`);
3677
+ /** Передаёт остаток из заголовков ответа бакету запроса; тот решает, тормозить ли себя. */
3678
+ const observeRateLimit = (limit, remaining, request) => {
3679
+ const queue = queueFor(request);
3680
+ if (!queue) return;
3681
+ const waited = queue.observe(limit, remaining);
3682
+ if (waited > 0) config.logger?.debug(`остаток лимита ${remaining} из ${limit ?? "?"}, бакет ${queue.bucket} ждёт ${waited} мс`);
3272
3683
  };
3273
3684
  transport = new Transport({
3274
3685
  ...config,
3275
3686
  hooks: config.hooks
3276
3687
  }, {
3688
+ onRateLimit: queues ? observeRateLimit : void 0,
3277
3689
  cookies: config.useCookieJar ? jar : void 0,
3278
3690
  getDeviceId: () => auth.getDeviceId(),
3279
- onRateLimit: queues && config.rateLimit?.respectHeaders ? throttleByHeaders : void 0,
3280
3691
  lifetimeSignal: lifetime.signal
3281
3692
  });
3282
3693
  const stages = [
@@ -3331,6 +3742,7 @@ function createClientRuntime(options = {}, internals = {}) {
3331
3742
  const clientHandler = (request) => {
3332
3743
  try {
3333
3744
  if (!isDisposeCleanupRequest(request)) internals.assertActive?.("выполнить новый запрос");
3745
+ if (request.rateLimitBucket !== void 0) assertKnownBucket(request.rateLimitBucket, "rateLimitBucket", config.rateLimit?.bucket);
3334
3746
  } catch (error) {
3335
3747
  return Promise.reject(error);
3336
3748
  }
@@ -3351,11 +3763,13 @@ function createClientRuntime(options = {}, internals = {}) {
3351
3763
  services,
3352
3764
  stageOrder,
3353
3765
  platformHeaders: (url) => transport.platformHeaders(url),
3766
+ rateLimitState: () => queues?.states() ?? [],
3354
3767
  close: () => {
3355
3768
  if (ownsQueues) queues?.stop();
3356
3769
  },
3357
3770
  dispose: async () => {
3358
3771
  lifetime.abort(new require_storage.ItdAbortError("Клиент освобождён через dispose(), запрос отменён"));
3772
+ if (ownsQueues) queues?.clear();
3359
3773
  auth.dispose();
3360
3774
  await plugins.dispose();
3361
3775
  }
@@ -9078,6 +9492,22 @@ var ItdClient = class ItdClient {
9078
9492
  });
9079
9493
  }
9080
9494
  /**
9495
+ * Остаток серверных лимитов по бакетам, через которые уже проходили запросы.
9496
+ *
9497
+ * Значения берутся из последнего ответа каждого бакета и быстро устаревают: сервер
9498
+ * восстанавливает квоту линейно и границу окна не сообщает. Пустой массив при
9499
+ * `rateLimit: false`. {@link close} снимок сохраняет, {@link dispose} очищает.
9500
+ *
9501
+ * @example
9502
+ * ```ts
9503
+ * const posts = itd.rateLimitState().find((state) => state.bucket === 'posts.create');
9504
+ * if ((posts?.remaining ?? Number.POSITIVE_INFINITY) < 3) await sleep(60_000);
9505
+ * ```
9506
+ */
9507
+ rateLimitState() {
9508
+ return this.#runtime.rateLimitState();
9509
+ }
9510
+ /**
9081
9511
  * Подключает плагин.
9082
9512
  *
9083
9513
  * Плагин может независимо регистрировать transformer логической операции и interceptor
@@ -9447,7 +9877,7 @@ var ItdAccounts = class ItdAccounts {
9447
9877
  this.#base = base;
9448
9878
  this.#storage = storage ?? new require_multi_storage.MemoryMultiTokenStorage();
9449
9879
  this.#plugins = orderPluginDefinitions(plugins ?? []);
9450
- this.#rateLimitScope = rateLimitScope ?? "account";
9880
+ this.#rateLimitScope = rateLimitScope ?? "shared";
9451
9881
  const rateLimit = this.#rateLimitScope === "shared" ? resolveRateLimit(base.rateLimit) : void 0;
9452
9882
  this.#queues = rateLimit ? new RequestQueuePool(rateLimit, base.clock ?? systemClock) : void 0;
9453
9883
  const logger = typeof base.logger === "object" ? base.logger : void 0;
@@ -9739,7 +10169,7 @@ var ItdAccounts = class ItdAccounts {
9739
10169
  ...clients.map((client) => client.dispose()),
9740
10170
  ...controls.map((control) => control.drain()),
9741
10171
  ...accountRemovals,
9742
- Promise.resolve().then(() => this.#queues?.stop())
10172
+ Promise.resolve().then(() => this.#queues?.clear())
9743
10173
  ]);
9744
10174
  this.#plugins.splice(0);
9745
10175
  this.#removingPlugins.clear();
@@ -10775,8 +11205,10 @@ exports.ALLOWED_MIME_TYPES = ALLOWED_MIME_TYPES;
10775
11205
  exports.AUDIO_MIME_TYPES = AUDIO_MIME_TYPES;
10776
11206
  exports.AccessType = AccessType;
10777
11207
  exports.AttachmentType = AttachmentType;
11208
+ exports.BUCKET_LIMITS = BUCKET_LIMITS;
10778
11209
  exports.CommentSort = CommentSort;
10779
11210
  exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
11211
+ exports.DEFAULT_RATE_LIMIT_BUCKET = DEFAULT_RATE_LIMIT_BUCKET;
10780
11212
  exports.FeedTab = FeedTab;
10781
11213
  exports.FileTransferMode = require_multi_storage.FileTransferMode;
10782
11214
  exports.IMAGE_MIME_TYPES = IMAGE_MIME_TYPES;
@@ -10814,6 +11246,7 @@ exports.NotificationType = NotificationType;
10814
11246
  exports.OPERATIONS = OPERATIONS;
10815
11247
  exports.PaginationMode = PaginationMode;
10816
11248
  exports.Paginator = Paginator;
11249
+ exports.RateLimitPacing = RateLimitPacing;
10817
11250
  exports.RealtimeComposer = RealtimeComposer;
10818
11251
  exports.RealtimeRouter = RealtimeRouter;
10819
11252
  exports.RealtimeStatus = RealtimeStatus;
@@ -10868,6 +11301,7 @@ exports.isMyProfile = isMyProfile;
10868
11301
  exports.mapPage = mapPage;
10869
11302
  exports.markup = markup;
10870
11303
  exports.normalizeNotification = normalizeNotification;
11304
+ exports.operationBucket = operationBucket;
10871
11305
  exports.operationMethod = operationMethod;
10872
11306
  exports.operationRetrySafety = operationRetrySafety;
10873
11307
  exports.parseHtml = parseHtml;