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/README.md +6 -6
- package/dist/index.cjs +540 -106
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +232 -43
- package/dist/index.d.ts +232 -43
- package/dist/index.js +537 -107
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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
|
-
*
|
|
2367
|
-
*
|
|
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
|
-
|
|
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
|
-
/** Очередь
|
|
2383
|
-
for(destination) {
|
|
2384
|
-
|
|
2385
|
-
let
|
|
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
|
|
2388
|
-
|
|
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.#
|
|
2395
|
-
|
|
2396
|
-
|
|
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:
|
|
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
|
-
|
|
3257
|
-
|
|
3673
|
+
if (!queues) return void 0;
|
|
3674
|
+
const key = queueKeyFor(request);
|
|
3675
|
+
return queues.for(key.destination, key.bucket);
|
|
3258
3676
|
};
|
|
3259
|
-
/**
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
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 ?? "
|
|
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?.
|
|
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;
|