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