itd-api 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -10
- package/dist/index.cjs +751 -202
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +285 -220
- package/dist/index.d.ts +285 -220
- package/dist/index.js +748 -181
- package/dist/index.js.map +1 -1
- package/dist/{multi-storage-B0r0AInH.js → multi-storage-BUZaLAPO.js} +13 -9
- package/dist/{multi-storage-B0r0AInH.js.map → multi-storage-BUZaLAPO.js.map} +1 -1
- package/dist/{multi-storage-CLqmzq07.d.ts → multi-storage-BUoEoW8f.d.cts} +3 -7
- package/dist/{multi-storage-mBuCOVZY.cjs → multi-storage-Bf84xiO8.cjs} +12 -14
- package/dist/{multi-storage-mBuCOVZY.cjs.map → multi-storage-Bf84xiO8.cjs.map} +1 -1
- package/dist/{multi-storage-s_PcWPGH.d.cts → multi-storage-CQSlI_kn.d.ts} +3 -7
- package/dist/node.cjs +2 -2
- package/dist/node.d.cts +2 -2
- package/dist/node.d.ts +2 -2
- package/dist/node.js +2 -2
- package/dist/{storage-Doe3lpFQ.d.ts → storage-BqMxs76Y.d.cts} +4 -4
- package/dist/{storage-Doe3lpFQ.d.cts → storage-BqMxs76Y.d.ts} +4 -4
- package/dist/{storage-dF8Tio5y.cjs → storage-DnzZPS_9.cjs} +36 -27
- package/dist/{storage-dF8Tio5y.cjs.map → storage-DnzZPS_9.cjs.map} +1 -1
- package/dist/{storage-DWrK3Z4M.js → storage-IHdXw52v.js} +36 -27
- package/dist/{storage-DWrK3Z4M.js.map → storage-IHdXw52v.js.map} +1 -1
- package/dist/web.cjs +1 -1
- package/dist/web.d.cts +1 -1
- package/dist/web.d.ts +1 -1
- package/dist/web.js +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_multi_storage = require("./multi-storage-
|
|
3
|
-
const require_storage = require("./storage-
|
|
2
|
+
const require_multi_storage = require("./multi-storage-Bf84xiO8.cjs");
|
|
3
|
+
const require_storage = require("./storage-DnzZPS_9.cjs");
|
|
4
4
|
//#region src/core/emitter.ts
|
|
5
5
|
/**
|
|
6
6
|
* Минимальный типизированный источник событий.
|
|
@@ -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,31 +521,47 @@ 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",
|
|
436
529
|
retrySafety: RetrySafety.Idempotent
|
|
437
530
|
},
|
|
531
|
+
"realtime.poll.updates": {
|
|
532
|
+
method: "GET",
|
|
533
|
+
retrySafety: RetrySafety.Safe,
|
|
534
|
+
bucket: "notifications"
|
|
535
|
+
},
|
|
536
|
+
"realtime.poll.unread": {
|
|
537
|
+
method: "GET",
|
|
538
|
+
retrySafety: RetrySafety.Safe,
|
|
539
|
+
bucket: "notifications"
|
|
540
|
+
},
|
|
438
541
|
"hashtags.search": {
|
|
439
542
|
method: "GET",
|
|
440
|
-
retrySafety: RetrySafety.Safe
|
|
543
|
+
retrySafety: RetrySafety.Safe,
|
|
544
|
+
bucket: "hashtags"
|
|
441
545
|
},
|
|
442
546
|
"hashtags.trending": {
|
|
443
547
|
method: "GET",
|
|
444
|
-
retrySafety: RetrySafety.Safe
|
|
548
|
+
retrySafety: RetrySafety.Safe,
|
|
549
|
+
bucket: "hashtags.trending"
|
|
445
550
|
},
|
|
446
551
|
"hashtags.posts": {
|
|
447
552
|
method: "GET",
|
|
448
|
-
retrySafety: RetrySafety.Safe
|
|
553
|
+
retrySafety: RetrySafety.Safe,
|
|
554
|
+
bucket: "hashtags"
|
|
449
555
|
},
|
|
450
556
|
"search.all": {
|
|
451
557
|
method: "GET",
|
|
452
|
-
retrySafety: RetrySafety.Safe
|
|
558
|
+
retrySafety: RetrySafety.Safe,
|
|
559
|
+
bucket: "search"
|
|
453
560
|
},
|
|
454
561
|
"reports.create": {
|
|
455
562
|
method: "POST",
|
|
456
|
-
retrySafety: RetrySafety.Unsafe
|
|
563
|
+
retrySafety: RetrySafety.Unsafe,
|
|
564
|
+
bucket: "reports.create"
|
|
457
565
|
},
|
|
458
566
|
"subscription.status": {
|
|
459
567
|
method: "GET",
|
|
@@ -485,11 +593,13 @@ const OPERATIONS = freezeOperations({
|
|
|
485
593
|
},
|
|
486
594
|
"verification.status": {
|
|
487
595
|
method: "GET",
|
|
488
|
-
retrySafety: RetrySafety.Safe
|
|
596
|
+
retrySafety: RetrySafety.Safe,
|
|
597
|
+
bucket: "verification.status"
|
|
489
598
|
},
|
|
490
599
|
"verification.submit": {
|
|
491
600
|
method: "POST",
|
|
492
|
-
retrySafety: RetrySafety.Unsafe
|
|
601
|
+
retrySafety: RetrySafety.Unsafe,
|
|
602
|
+
bucket: "verification.submit"
|
|
493
603
|
},
|
|
494
604
|
"platform.version": {
|
|
495
605
|
method: "GET",
|
|
@@ -533,6 +643,16 @@ function operationMethod(id) {
|
|
|
533
643
|
function operationRetrySafety(id) {
|
|
534
644
|
return OPERATIONS[id].retrySafety;
|
|
535
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
|
+
}
|
|
536
656
|
//#endregion
|
|
537
657
|
//#region src/core/runtime.ts
|
|
538
658
|
/**
|
|
@@ -668,6 +788,11 @@ const AUTH_PATHS = {
|
|
|
668
788
|
* Нужен, чтобы отрисовать виджет капчи и получить токен для `signIn`, `signUp`
|
|
669
789
|
* и `forgotPassword`.
|
|
670
790
|
*
|
|
791
|
+
* Ключ привязан к домену: на чужом origin Cloudflare отказывает виджету с кодом `110200`.
|
|
792
|
+
* Поэтому отрисовать его может только код, выполняемый на самом итд.com. Остальным
|
|
793
|
+
* подходит `@itd-api/turnstile`, готовый токен из другого источника или вовсе вход
|
|
794
|
+
* без капчи — по сохранённой сессии либо по токенам, взятым в браузере.
|
|
795
|
+
*
|
|
671
796
|
* @example
|
|
672
797
|
* ```ts
|
|
673
798
|
* turnstile.render('#captcha', {
|
|
@@ -677,8 +802,6 @@ const AUTH_PATHS = {
|
|
|
677
802
|
* ```
|
|
678
803
|
*/
|
|
679
804
|
const TURNSTILE_SITE_KEY = "0x4AAAAAACHhxczw6fJGwPBg";
|
|
680
|
-
/** Заголовок с идентификатором устройства. Сервер связывает с ним запись в списке сессий. */
|
|
681
|
-
const DEVICE_ID_HEADER = "X-Device-Id";
|
|
682
805
|
function readAccessToken(payload) {
|
|
683
806
|
if (typeof payload !== "object" || payload === null) return void 0;
|
|
684
807
|
const token = payload.accessToken;
|
|
@@ -1183,10 +1306,33 @@ const systemClock = Object.freeze({
|
|
|
1183
1306
|
return () => clearTimeout(timer);
|
|
1184
1307
|
}
|
|
1185
1308
|
});
|
|
1309
|
+
/**
|
|
1310
|
+
* Заводит срок, общий на все ожидания: от ожидания к ожиданию он не продлевается.
|
|
1311
|
+
*
|
|
1312
|
+
* @param timeout срок в миллисекундах; `0` — ждать без ограничения
|
|
1313
|
+
* @internal
|
|
1314
|
+
*/
|
|
1315
|
+
function createDeadline(timeout, clock = systemClock) {
|
|
1316
|
+
if (timeout <= 0) return {
|
|
1317
|
+
wait: async (promise) => {
|
|
1318
|
+
await promise;
|
|
1319
|
+
return true;
|
|
1320
|
+
},
|
|
1321
|
+
cancel: () => {}
|
|
1322
|
+
};
|
|
1323
|
+
let expire;
|
|
1324
|
+
const expired = new Promise((resolve) => {
|
|
1325
|
+
expire = resolve;
|
|
1326
|
+
});
|
|
1327
|
+
return {
|
|
1328
|
+
wait: (promise) => Promise.race([promise.then(() => true), expired.then(() => false)]),
|
|
1329
|
+
cancel: clock.schedule(() => expire(), timeout)
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1186
1332
|
//#endregion
|
|
1187
1333
|
//#region src/core/version.ts
|
|
1188
1334
|
/** Версия библиотеки. Попадает в `User-Agent`. */
|
|
1189
|
-
const LIBRARY_VERSION = "0.
|
|
1335
|
+
const LIBRARY_VERSION = "0.7.0";
|
|
1190
1336
|
//#endregion
|
|
1191
1337
|
//#region src/core/config.ts
|
|
1192
1338
|
/** Базовый URL API итд.com. Домен записан в punycode: `итд.com`. */
|
|
@@ -1201,8 +1347,6 @@ const BUILT_IN_SERVICES = Object.freeze([Object.freeze({
|
|
|
1201
1347
|
baseUrl: DEFAULT_STATUS_BASE_URL,
|
|
1202
1348
|
auth: false
|
|
1203
1349
|
})]);
|
|
1204
|
-
/** Таймаут запроса по умолчанию — 30 секунд. */
|
|
1205
|
-
const DEFAULT_TIMEOUT = 3e4;
|
|
1206
1350
|
/**
|
|
1207
1351
|
* `User-Agent` по умолчанию.
|
|
1208
1352
|
*
|
|
@@ -1217,9 +1361,8 @@ const DEFAULT_USER_AGENT = `Mozilla/5.0 (compatible; itd-api/${LIBRARY_VERSION};
|
|
|
1217
1361
|
/**
|
|
1218
1362
|
* Паузы перед повторами при ответе `429`.
|
|
1219
1363
|
*
|
|
1220
|
-
* Сервер итд.com не присылает `Retry-After` и не сообщает время сброса окна, поэтому
|
|
1221
|
-
*
|
|
1222
|
-
* до полутора минут, если лимит исчерпан всерьёз.
|
|
1364
|
+
* Сервер итд.com не присылает `Retry-After` и не сообщает время сброса окна, поэтому паузу
|
|
1365
|
+
* приходится подбирать лестницей: от секунды, если окно почти истекло, до полутора минут.
|
|
1223
1366
|
*/
|
|
1224
1367
|
const DEFAULT_RATE_LIMIT_DELAYS = Object.freeze([
|
|
1225
1368
|
1e3,
|
|
@@ -1228,6 +1371,13 @@ const DEFAULT_RATE_LIMIT_DELAYS = Object.freeze([
|
|
|
1228
1371
|
6e4,
|
|
1229
1372
|
9e4
|
|
1230
1373
|
]);
|
|
1374
|
+
/**
|
|
1375
|
+
* Встроенные поправки бакетов.
|
|
1376
|
+
*
|
|
1377
|
+
* Таймаут загрузки файла — пять минут против обычных тридцати секунд, поэтому её
|
|
1378
|
+
* одновременность ограничена одним запросом.
|
|
1379
|
+
*/
|
|
1380
|
+
const DEFAULT_BUCKET_OVERRIDES = Object.freeze({ "files.upload": Object.freeze({ concurrency: 1 }) });
|
|
1231
1381
|
function requirePositive(value, name) {
|
|
1232
1382
|
if (!Number.isFinite(value) || value < 0) throw new require_storage.ItdConfigError(`${name} должен быть неотрицательным числом, получено: ${value}`);
|
|
1233
1383
|
return value;
|
|
@@ -1316,6 +1466,54 @@ function resolveRetry(retry) {
|
|
|
1316
1466
|
};
|
|
1317
1467
|
}
|
|
1318
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
|
+
/**
|
|
1319
1517
|
* Приводит настройки очереди к полному виду. `undefined` — очередь не нужна.
|
|
1320
1518
|
*
|
|
1321
1519
|
* Кроме создания клиента вызывается ещё из {@link ItdAccounts}: общая на всех аккаунтов
|
|
@@ -1332,21 +1530,29 @@ function resolveRateLimit(rateLimit) {
|
|
|
1332
1530
|
concurrency: 6,
|
|
1333
1531
|
rps: void 0,
|
|
1334
1532
|
retryDelays: DEFAULT_RATE_LIMIT_DELAYS,
|
|
1335
|
-
|
|
1533
|
+
buckets: true,
|
|
1534
|
+
pacing: RateLimitPacing.React,
|
|
1535
|
+
bucketConcurrency: 6,
|
|
1536
|
+
bucketOverrides: DEFAULT_BUCKET_OVERRIDES,
|
|
1537
|
+
bucket: void 0
|
|
1336
1538
|
};
|
|
1337
1539
|
if (!rateLimit) return defaults;
|
|
1338
|
-
const concurrency = rateLimit.concurrency ?? 6;
|
|
1339
|
-
if (!Number.isInteger(concurrency) || concurrency < 1) throw new require_storage.ItdConfigError(`rateLimit.concurrency должен быть целым числом от 1, получено: ${concurrency}`);
|
|
1540
|
+
const concurrency = requireConcurrency(rateLimit.concurrency ?? 6, "rateLimit.concurrency");
|
|
1340
1541
|
if (rateLimit.rps !== void 0 && (!Number.isFinite(rateLimit.rps) || rateLimit.rps <= 0)) throw new require_storage.ItdConfigError(`rateLimit.rps должен быть положительным числом, получено: ${rateLimit.rps}`);
|
|
1341
1542
|
const retryDelays = rateLimit.retryDelays ?? defaults.retryDelays;
|
|
1342
1543
|
if (!Array.isArray(retryDelays)) throw new require_storage.ItdConfigError("rateLimit.retryDelays должен быть массивом чисел");
|
|
1343
1544
|
for (const delay of retryDelays) requirePositive(delay, "rateLimit.retryDelays");
|
|
1344
|
-
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 должен быть функцией");
|
|
1345
1547
|
return {
|
|
1346
1548
|
concurrency,
|
|
1347
1549
|
rps: rateLimit.rps,
|
|
1348
1550
|
retryDelays: [...retryDelays],
|
|
1349
|
-
|
|
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
|
|
1350
1556
|
};
|
|
1351
1557
|
}
|
|
1352
1558
|
/**
|
|
@@ -1412,6 +1618,7 @@ function resolveConfig(options = {}) {
|
|
|
1412
1618
|
const mode = options.mode ?? RuntimeMode.Auto;
|
|
1413
1619
|
if (!Object.values(RuntimeMode).includes(mode)) throw new require_storage.ItdConfigError(`mode должен быть одним из ${Object.values(RuntimeMode).join(", ")}, получено: ${mode}`);
|
|
1414
1620
|
const timeout = requirePositive(options.timeout ?? 3e4, "timeout");
|
|
1621
|
+
const shutdownTimeout = requirePositive(options.shutdownTimeout ?? 1e4, "shutdownTimeout");
|
|
1415
1622
|
if (options.clock !== void 0 && (typeof options.clock !== "object" || options.clock === null || typeof options.clock.now !== "function" || typeof options.clock.schedule !== "function")) throw new require_storage.ItdConfigError("clock должен предоставлять методы now() и schedule()");
|
|
1416
1623
|
requireOptionalBoolean(options.autoRefresh, "autoRefresh");
|
|
1417
1624
|
requireOptionalBoolean(options.reloginOnRefreshFailure, "reloginOnRefreshFailure");
|
|
@@ -1427,6 +1634,7 @@ function resolveConfig(options = {}) {
|
|
|
1427
1634
|
fetch: resolveFetch(options.fetch),
|
|
1428
1635
|
clock: options.clock ?? systemClock,
|
|
1429
1636
|
timeout,
|
|
1637
|
+
shutdownTimeout,
|
|
1430
1638
|
retry: resolveRetry(options.retry),
|
|
1431
1639
|
rateLimit: resolveRateLimit(options.rateLimit),
|
|
1432
1640
|
hooks: resolveHooks(options.hooks),
|
|
@@ -1442,6 +1650,7 @@ function resolveConfig(options = {}) {
|
|
|
1442
1650
|
//#endregion
|
|
1443
1651
|
//#region src/core/pipeline.ts
|
|
1444
1652
|
const REQUEST_ATTEMPT_STATE = Symbol("itd-api.request-attempt-state");
|
|
1653
|
+
const REQUEST_QUEUE_KEY = Symbol("itd-api.request-queue-key");
|
|
1445
1654
|
const DISPOSE_CLEANUP_REQUEST = Symbol("itd-api.dispose-cleanup-request");
|
|
1446
1655
|
/** Один раз присваивает низкоуровневому запросу семантический ID до входа в middleware. */
|
|
1447
1656
|
function identifyRequest(request) {
|
|
@@ -1473,6 +1682,23 @@ function beginTransportAttempt(request) {
|
|
|
1473
1682
|
function currentTransportAttempt(request) {
|
|
1474
1683
|
return request[REQUEST_ATTEMPT_STATE]?.value ?? 0;
|
|
1475
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
|
+
}
|
|
1476
1702
|
/** Помечает запрос как часть внутренней финализации уже начатого `dispose()`. @internal */
|
|
1477
1703
|
function markDisposeCleanupRequest(request) {
|
|
1478
1704
|
return {
|
|
@@ -1817,7 +2043,7 @@ function createRetryMiddleware(deps) {
|
|
|
1817
2043
|
const transportAttempt = currentTransportAttempt(trackedRequest);
|
|
1818
2044
|
if (require_storage.isItdRateLimitError(error)) rateLimitAttempt += 1;
|
|
1819
2045
|
else retryAttempt += 1;
|
|
1820
|
-
const delay = nextDelay(error, retryAttempt, rateLimitAttempt,
|
|
2046
|
+
const delay = nextDelay(error, retryAttempt, rateLimitAttempt, trackedRequest, policy, backoff);
|
|
1821
2047
|
if (delay === void 0) throw error;
|
|
1822
2048
|
await dispatchRequestHook(deps.hooks, "onRetry", {
|
|
1823
2049
|
operationId: request.operationId,
|
|
@@ -1995,7 +2221,11 @@ var PluginRegistry = class {
|
|
|
1995
2221
|
#entries = /* @__PURE__ */ new Map();
|
|
1996
2222
|
#removing = /* @__PURE__ */ new Set();
|
|
1997
2223
|
#cleanups = /* @__PURE__ */ new Set();
|
|
2224
|
+
#options;
|
|
1998
2225
|
#ordered = [];
|
|
2226
|
+
constructor(options) {
|
|
2227
|
+
this.#options = options;
|
|
2228
|
+
}
|
|
1999
2229
|
/** Сколько плагинов подключено. */
|
|
2000
2230
|
get size() {
|
|
2001
2231
|
return this.#entries.size;
|
|
@@ -2069,9 +2299,11 @@ var PluginRegistry = class {
|
|
|
2069
2299
|
* Отключает плагин и вызывает его функцию очистки.
|
|
2070
2300
|
*
|
|
2071
2301
|
* Новые запросы перестают видеть расширения плагина сразу. Снимок уже начавшейся операции,
|
|
2072
|
-
* включая её будущие retry, остаётся неизменным; очистка дождётся завершения
|
|
2302
|
+
* включая её будущие retry, остаётся неизменным; очистка дождётся завершения операции,
|
|
2303
|
+
* но не дольше отведённого срока.
|
|
2073
2304
|
*
|
|
2074
2305
|
* @returns `false`, если такого плагина не было
|
|
2306
|
+
* @throws {ItdStateError} если операции плагина не завершились за отведённый срок
|
|
2075
2307
|
*/
|
|
2076
2308
|
async remove(name) {
|
|
2077
2309
|
const entry = this.#entries.get(name);
|
|
@@ -2080,21 +2312,24 @@ var PluginRegistry = class {
|
|
|
2080
2312
|
this.#entries.delete(name);
|
|
2081
2313
|
this.#ordered = this.#ordered.filter((current) => current !== entry);
|
|
2082
2314
|
this.#removing.add(name);
|
|
2315
|
+
const deadline = createDeadline(this.#options.shutdownTimeout, this.#options.clock);
|
|
2083
2316
|
const cleanup = this.#trackCleanup((async () => {
|
|
2084
|
-
await this.#
|
|
2085
|
-
|
|
2317
|
+
const expired = await this.#release(entry, deadline);
|
|
2318
|
+
if (expired) throw expired;
|
|
2086
2319
|
})());
|
|
2087
2320
|
try {
|
|
2088
2321
|
await cleanup;
|
|
2089
2322
|
return true;
|
|
2090
2323
|
} finally {
|
|
2324
|
+
deadline.cancel();
|
|
2091
2325
|
this.#removing.delete(name);
|
|
2092
2326
|
}
|
|
2093
2327
|
}
|
|
2094
2328
|
/**
|
|
2095
2329
|
* Отключает все плагины окончательно.
|
|
2096
2330
|
*
|
|
2097
|
-
* Очистка идёт изнутри наружу — в порядке, обратном выполнению расширений.
|
|
2331
|
+
* Очистка идёт изнутри наружу — в порядке, обратном выполнению расширений. Срок ожидания
|
|
2332
|
+
* общий на все плагины.
|
|
2098
2333
|
*/
|
|
2099
2334
|
async dispose() {
|
|
2100
2335
|
const entries = [...this.#ordered].reverse();
|
|
@@ -2102,13 +2337,14 @@ var PluginRegistry = class {
|
|
|
2102
2337
|
this.#entries.clear();
|
|
2103
2338
|
this.#ordered = [];
|
|
2104
2339
|
for (const { plugin } of entries) this.#removing.add(plugin.name);
|
|
2105
|
-
|
|
2340
|
+
const deadline = createDeadline(this.#options.shutdownTimeout, this.#options.clock);
|
|
2341
|
+
const cleanup = this.#trackCleanup((async () => {
|
|
2106
2342
|
const errors = [];
|
|
2107
2343
|
const previous = await Promise.allSettled(previousCleanups);
|
|
2108
2344
|
for (const result of previous) if (result.status === "rejected") errors.push(result.reason);
|
|
2109
2345
|
for (const entry of entries) try {
|
|
2110
|
-
await this.#
|
|
2111
|
-
|
|
2346
|
+
const expired = await this.#release(entry, deadline);
|
|
2347
|
+
if (expired) errors.push(expired);
|
|
2112
2348
|
} catch (error) {
|
|
2113
2349
|
errors.push(error);
|
|
2114
2350
|
} finally {
|
|
@@ -2116,6 +2352,11 @@ var PluginRegistry = class {
|
|
|
2116
2352
|
}
|
|
2117
2353
|
if (errors.length > 0) throw new AggregateError(errors, "Не удалось освободить ресурсы плагинов");
|
|
2118
2354
|
})());
|
|
2355
|
+
try {
|
|
2356
|
+
await cleanup;
|
|
2357
|
+
} finally {
|
|
2358
|
+
deadline.cancel();
|
|
2359
|
+
}
|
|
2119
2360
|
}
|
|
2120
2361
|
/**
|
|
2121
2362
|
* Прогоняет запрос через operation transformers и прикрепляет attempt interceptors.
|
|
@@ -2123,6 +2364,9 @@ var PluginRegistry = class {
|
|
|
2123
2364
|
* Снимки обеих цепочек берутся в начале: `unuse()` влияет на новые операции, но не меняет
|
|
2124
2365
|
* уже выполняющуюся и не удаляет interceptors из её последующих retry.
|
|
2125
2366
|
*
|
|
2367
|
+
* Каждый `next` одноразовый: transformer может завершить операцию сам, но не может породить
|
|
2368
|
+
* вторую — для `posts.create` это была бы вторая публикация.
|
|
2369
|
+
*
|
|
2126
2370
|
* @param execute выполнение логической операции, вызываемое самым внутренним transformer
|
|
2127
2371
|
*/
|
|
2128
2372
|
async run(request, execute) {
|
|
@@ -2133,7 +2377,17 @@ var PluginRegistry = class {
|
|
|
2133
2377
|
interceptor
|
|
2134
2378
|
})));
|
|
2135
2379
|
const scoped = (current) => withAttemptInterceptorScope(current, interceptorScope);
|
|
2136
|
-
const chain = entries.flatMap((entry) => entry.transformers
|
|
2380
|
+
const chain = entries.flatMap((entry) => entry.transformers.map((transformer) => ({
|
|
2381
|
+
plugin: entry.plugin.name,
|
|
2382
|
+
transformer
|
|
2383
|
+
}))).reduceRight((next, { plugin, transformer }) => (current) => {
|
|
2384
|
+
let called = false;
|
|
2385
|
+
return transformer(scoped(current), (prepared) => {
|
|
2386
|
+
if (called) throw new require_storage.ItdConfigError(`operation transformer плагина «${plugin}» вызвал next() больше одного раза`);
|
|
2387
|
+
called = true;
|
|
2388
|
+
return next(scoped(prepared));
|
|
2389
|
+
});
|
|
2390
|
+
}, (current) => execute(scoped(current)));
|
|
2137
2391
|
try {
|
|
2138
2392
|
return await chain(scoped(request));
|
|
2139
2393
|
} finally {
|
|
@@ -2147,6 +2401,18 @@ var PluginRegistry = class {
|
|
|
2147
2401
|
}
|
|
2148
2402
|
}
|
|
2149
2403
|
}
|
|
2404
|
+
/**
|
|
2405
|
+
* Дожидается операций плагина и освобождает его ресурсы.
|
|
2406
|
+
*
|
|
2407
|
+
* `teardown` выполняется в любом случае, в том числе после истечения срока.
|
|
2408
|
+
*
|
|
2409
|
+
* @returns ошибка истёкшего срока, если ждать пришлось дольше отведённого
|
|
2410
|
+
*/
|
|
2411
|
+
async #release(entry, deadline) {
|
|
2412
|
+
const finished = await deadline.wait(this.#waitForDrain(entry));
|
|
2413
|
+
await entry.teardown?.();
|
|
2414
|
+
return finished ? void 0 : new require_storage.ItdStateError(`плагин «${entry.plugin.name}» не завершил операции за ${this.#options.shutdownTimeout} мс; ожидание прекращено, ресурсы плагина освобождены`);
|
|
2415
|
+
}
|
|
2150
2416
|
#waitForDrain(entry) {
|
|
2151
2417
|
if (entry.activeRequests === 0) return Promise.resolve();
|
|
2152
2418
|
entry.drain ??= new Promise((resolve) => {
|
|
@@ -2166,6 +2432,10 @@ var PluginRegistry = class {
|
|
|
2166
2432
|
function queueAbortError() {
|
|
2167
2433
|
return new require_storage.ItdAbortError("Запрос отменён во время ожидания очереди");
|
|
2168
2434
|
}
|
|
2435
|
+
/** Ошибка запроса, которого застала остановка очереди. */
|
|
2436
|
+
function queueStoppedError() {
|
|
2437
|
+
return new require_storage.ItdAbortError("Клиент закрыт, запрос отменён");
|
|
2438
|
+
}
|
|
2169
2439
|
/**
|
|
2170
2440
|
* Очередь запросов: ограничивает одновременность и частоту.
|
|
2171
2441
|
*
|
|
@@ -2181,6 +2451,7 @@ var RequestQueue = class {
|
|
|
2181
2451
|
#concurrency;
|
|
2182
2452
|
/** Минимальный промежуток между стартами, мс. `0` — без ограничения частоты. */
|
|
2183
2453
|
#minGap;
|
|
2454
|
+
#onDispatch;
|
|
2184
2455
|
#waiting = [];
|
|
2185
2456
|
#active = 0;
|
|
2186
2457
|
/** Момент, раньше которого следующий запрос стартовать не должен. */
|
|
@@ -2190,6 +2461,7 @@ var RequestQueue = class {
|
|
|
2190
2461
|
constructor(options, clock = systemClock) {
|
|
2191
2462
|
this.#concurrency = options.concurrency;
|
|
2192
2463
|
this.#minGap = options.rps ? 1e3 / options.rps : 0;
|
|
2464
|
+
this.#onDispatch = options.onDispatch;
|
|
2193
2465
|
this.#clock = clock;
|
|
2194
2466
|
}
|
|
2195
2467
|
/** Сколько задач выполняется прямо сейчас. */
|
|
@@ -2221,6 +2493,7 @@ var RequestQueue = class {
|
|
|
2221
2493
|
run: () => {
|
|
2222
2494
|
detach();
|
|
2223
2495
|
this.#active += 1;
|
|
2496
|
+
this.#onDispatch?.();
|
|
2224
2497
|
Promise.resolve().then(task).then(resolve, reject).finally(() => {
|
|
2225
2498
|
this.#active -= 1;
|
|
2226
2499
|
this.#drain();
|
|
@@ -2246,16 +2519,10 @@ var RequestQueue = class {
|
|
|
2246
2519
|
this.#cancelTimer();
|
|
2247
2520
|
this.#cancelTimer = void 0;
|
|
2248
2521
|
}
|
|
2249
|
-
this.#nextSlot = 0;
|
|
2250
2522
|
const pending = this.#waiting.splice(0, this.#waiting.length);
|
|
2251
|
-
for (const task of pending) task.cancel(
|
|
2523
|
+
for (const task of pending) task.cancel(queueStoppedError());
|
|
2252
2524
|
}
|
|
2253
|
-
/**
|
|
2254
|
-
* Придерживает всю очередь на заданное время.
|
|
2255
|
-
*
|
|
2256
|
-
* Вызывается при получении `429` с заголовком `Retry-After`: тормозить нужно все запросы,
|
|
2257
|
-
* а не только тот, который наткнулся на лимит, — иначе остальные продолжат добивать API.
|
|
2258
|
-
*/
|
|
2525
|
+
/** Придерживает очередь на заданное время: ждут все её задачи, а не только одна. */
|
|
2259
2526
|
pause(ms) {
|
|
2260
2527
|
if (ms <= 0) return;
|
|
2261
2528
|
this.#nextSlot = Math.max(this.#nextSlot, this.#clock.now() + ms);
|
|
@@ -2286,39 +2553,241 @@ var RequestQueue = class {
|
|
|
2286
2553
|
this.#drain();
|
|
2287
2554
|
}
|
|
2288
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
|
+
}
|
|
2289
2735
|
/**
|
|
2290
|
-
* Очереди по
|
|
2736
|
+
* Очереди по парам «направление — серверный счётчик частоты».
|
|
2291
2737
|
*
|
|
2292
|
-
*
|
|
2293
|
-
*
|
|
2738
|
+
* Направление — origin уже разрешённого URL: разные локальные имена одного хоста делят
|
|
2739
|
+
* лимит, а запрос с разовым внешним `baseUrl` не попадает в основную очередь. Мощность
|
|
2740
|
+
* карты ограничена каталогом операций, поэтому ни TTL, ни вытеснение не нужны.
|
|
2294
2741
|
*
|
|
2295
2742
|
* @internal
|
|
2296
2743
|
*/
|
|
2297
2744
|
var RequestQueuePool = class {
|
|
2298
2745
|
#options;
|
|
2299
2746
|
#clock;
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
#byDestination = /* @__PURE__ */ new Map();
|
|
2747
|
+
/** Ключ `undefined` — основная очередь внутренних клиентов без известного направления. */
|
|
2748
|
+
#destinations = /* @__PURE__ */ new Map();
|
|
2303
2749
|
constructor(options, clock = systemClock) {
|
|
2304
2750
|
this.#options = options;
|
|
2305
2751
|
this.#clock = clock;
|
|
2306
|
-
this.#main = new RequestQueue(options, clock);
|
|
2307
2752
|
}
|
|
2308
|
-
/** Очередь
|
|
2309
|
-
for(destination) {
|
|
2310
|
-
|
|
2311
|
-
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);
|
|
2312
2765
|
if (!queue) {
|
|
2313
|
-
queue = new
|
|
2314
|
-
|
|
2766
|
+
queue = new BucketQueue(destination, name, entry.shared, this.#options, this.#clock);
|
|
2767
|
+
entry.buckets.set(name, queue);
|
|
2315
2768
|
}
|
|
2316
2769
|
return queue;
|
|
2317
2770
|
}
|
|
2318
|
-
/**
|
|
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
|
+
*/
|
|
2319
2781
|
stop() {
|
|
2320
|
-
this.#
|
|
2321
|
-
|
|
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();
|
|
2322
2791
|
}
|
|
2323
2792
|
};
|
|
2324
2793
|
//#endregion
|
|
@@ -2686,6 +3155,11 @@ function readIntHeader(headers, name) {
|
|
|
2686
3155
|
const value = Number.parseInt(raw, 10);
|
|
2687
3156
|
return Number.isFinite(value) ? value : void 0;
|
|
2688
3157
|
}
|
|
3158
|
+
/** Читает положительное целое число из заголовка. */
|
|
3159
|
+
function readPositiveIntHeader(headers, name) {
|
|
3160
|
+
const value = readIntHeader(headers, name);
|
|
3161
|
+
return value !== void 0 && value > 0 ? value : void 0;
|
|
3162
|
+
}
|
|
2689
3163
|
/**
|
|
2690
3164
|
* Читает сведения об ограничении частоты.
|
|
2691
3165
|
*
|
|
@@ -2694,7 +3168,7 @@ function readIntHeader(headers, name) {
|
|
|
2694
3168
|
*/
|
|
2695
3169
|
function readRateLimit(headers) {
|
|
2696
3170
|
return {
|
|
2697
|
-
limit:
|
|
3171
|
+
limit: readPositiveIntHeader(headers, "x-ratelimit-limit"),
|
|
2698
3172
|
remaining: readIntHeader(headers, "x-ratelimit-remaining")
|
|
2699
3173
|
};
|
|
2700
3174
|
}
|
|
@@ -2843,17 +3317,25 @@ function abortable(promise, signal) {
|
|
|
2843
3317
|
});
|
|
2844
3318
|
}
|
|
2845
3319
|
/**
|
|
2846
|
-
* Объединяет пользовательский `AbortSignal
|
|
3320
|
+
* Объединяет пользовательский `AbortSignal`, сигнал жизни клиента и таймаут.
|
|
2847
3321
|
*
|
|
2848
3322
|
* Реализовано вручную, а не через `AbortSignal.any`: последний появился только в Node 20,
|
|
2849
|
-
* а библиотека поддерживает Node 18.
|
|
3323
|
+
* а библиотека поддерживает Node 18. Причина отмены переносится от источника как есть.
|
|
2850
3324
|
*/
|
|
2851
|
-
function createAbortBundle(userSignal, timeout, clock) {
|
|
3325
|
+
function createAbortBundle(userSignal, lifetimeSignal, timeout, clock) {
|
|
2852
3326
|
const controller = new AbortController();
|
|
2853
3327
|
let timedOut = false;
|
|
2854
|
-
const
|
|
2855
|
-
|
|
2856
|
-
|
|
3328
|
+
const link = (source) => {
|
|
3329
|
+
if (!source) return void 0;
|
|
3330
|
+
if (source.aborted) {
|
|
3331
|
+
controller.abort(source.reason);
|
|
3332
|
+
return;
|
|
3333
|
+
}
|
|
3334
|
+
const onAbort = () => controller.abort(source.reason);
|
|
3335
|
+
source.addEventListener("abort", onAbort, { once: true });
|
|
3336
|
+
return () => source.removeEventListener("abort", onAbort);
|
|
3337
|
+
};
|
|
3338
|
+
const unlink = [link(userSignal), link(lifetimeSignal)];
|
|
2857
3339
|
const cancelTimer = timeout > 0 ? clock.schedule(() => {
|
|
2858
3340
|
timedOut = true;
|
|
2859
3341
|
controller.abort();
|
|
@@ -2863,7 +3345,7 @@ function createAbortBundle(userSignal, timeout, clock) {
|
|
|
2863
3345
|
timedOut: () => timedOut,
|
|
2864
3346
|
cleanup: () => {
|
|
2865
3347
|
cancelTimer?.();
|
|
2866
|
-
|
|
3348
|
+
for (const detach of unlink) detach?.();
|
|
2867
3349
|
}
|
|
2868
3350
|
};
|
|
2869
3351
|
}
|
|
@@ -2900,7 +3382,7 @@ var Transport = class {
|
|
|
2900
3382
|
const headers = await this.#buildHeaders(request, url);
|
|
2901
3383
|
const attempt = request.attempt ?? 1;
|
|
2902
3384
|
const timeout = request.timeout ?? this.#config.timeout;
|
|
2903
|
-
const abort = createAbortBundle(request.signal, timeout, this.#config.clock);
|
|
3385
|
+
const abort = createAbortBundle(request.signal, this.#deps.lifetimeSignal, timeout, this.#config.clock);
|
|
2904
3386
|
const startedAt = this.#config.clock.now();
|
|
2905
3387
|
let cleanupBody;
|
|
2906
3388
|
try {
|
|
@@ -3117,7 +3599,7 @@ var Transport = class {
|
|
|
3117
3599
|
path: request.path
|
|
3118
3600
|
});
|
|
3119
3601
|
if (aborted) {
|
|
3120
|
-
const reason =
|
|
3602
|
+
const reason = abort.signal.reason;
|
|
3121
3603
|
return new require_storage.ItdAbortError(`Запрос ${method} ${request.path} отменён`, reason !== void 0 ? { cause: reason } : void 0);
|
|
3122
3604
|
}
|
|
3123
3605
|
if (error instanceof require_storage.ItdError) return error;
|
|
@@ -3158,38 +3640,55 @@ function createServiceRegistry(config) {
|
|
|
3158
3640
|
function createClientRuntime(options = {}, internals = {}) {
|
|
3159
3641
|
const config = resolveConfig(options);
|
|
3160
3642
|
const jar = new require_multi_storage.CookieJar();
|
|
3161
|
-
const plugins = new PluginRegistry(
|
|
3643
|
+
const plugins = new PluginRegistry({
|
|
3644
|
+
shutdownTimeout: config.shutdownTimeout,
|
|
3645
|
+
clock: config.clock
|
|
3646
|
+
});
|
|
3162
3647
|
const services = createServiceRegistry(config);
|
|
3648
|
+
const lifetime = new AbortController();
|
|
3163
3649
|
const sharedQueues = config.rateLimit ? internals.queues : void 0;
|
|
3164
3650
|
const queues = sharedQueues ?? (config.rateLimit ? new RequestQueuePool(config.rateLimit, config.clock) : void 0);
|
|
3165
3651
|
const ownsQueues = sharedQueues === void 0;
|
|
3166
3652
|
let auth;
|
|
3167
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
|
+
}));
|
|
3168
3672
|
const queueFor = (request) => {
|
|
3169
|
-
|
|
3170
|
-
|
|
3673
|
+
if (!queues) return void 0;
|
|
3674
|
+
const key = queueKeyFor(request);
|
|
3675
|
+
return queues.for(key.destination, key.bucket);
|
|
3171
3676
|
};
|
|
3172
|
-
/**
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
*/
|
|
3179
|
-
const throttleByHeaders = (limit, remaining, request) => {
|
|
3180
|
-
if (remaining === void 0 || remaining > 0) return;
|
|
3181
|
-
const first = config.rateLimit?.retryDelays[0];
|
|
3182
|
-
if (first === void 0) return;
|
|
3183
|
-
queueFor(request)?.pause(first);
|
|
3184
|
-
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} мс`);
|
|
3185
3683
|
};
|
|
3186
3684
|
transport = new Transport({
|
|
3187
3685
|
...config,
|
|
3188
3686
|
hooks: config.hooks
|
|
3189
3687
|
}, {
|
|
3688
|
+
onRateLimit: queues ? observeRateLimit : void 0,
|
|
3190
3689
|
cookies: config.useCookieJar ? jar : void 0,
|
|
3191
3690
|
getDeviceId: () => auth.getDeviceId(),
|
|
3192
|
-
|
|
3691
|
+
lifetimeSignal: lifetime.signal
|
|
3193
3692
|
});
|
|
3194
3693
|
const stages = [
|
|
3195
3694
|
{
|
|
@@ -3243,6 +3742,7 @@ function createClientRuntime(options = {}, internals = {}) {
|
|
|
3243
3742
|
const clientHandler = (request) => {
|
|
3244
3743
|
try {
|
|
3245
3744
|
if (!isDisposeCleanupRequest(request)) internals.assertActive?.("выполнить новый запрос");
|
|
3745
|
+
if (request.rateLimitBucket !== void 0) assertKnownBucket(request.rateLimitBucket, "rateLimitBucket", config.rateLimit?.bucket);
|
|
3246
3746
|
} catch (error) {
|
|
3247
3747
|
return Promise.reject(error);
|
|
3248
3748
|
}
|
|
@@ -3263,12 +3763,15 @@ function createClientRuntime(options = {}, internals = {}) {
|
|
|
3263
3763
|
services,
|
|
3264
3764
|
stageOrder,
|
|
3265
3765
|
platformHeaders: (url) => transport.platformHeaders(url),
|
|
3766
|
+
rateLimitState: () => queues?.states() ?? [],
|
|
3266
3767
|
close: () => {
|
|
3267
3768
|
if (ownsQueues) queues?.stop();
|
|
3268
3769
|
},
|
|
3269
|
-
dispose: () => {
|
|
3770
|
+
dispose: async () => {
|
|
3771
|
+
lifetime.abort(new require_storage.ItdAbortError("Клиент освобождён через dispose(), запрос отменён"));
|
|
3772
|
+
if (ownsQueues) queues?.clear();
|
|
3270
3773
|
auth.dispose();
|
|
3271
|
-
|
|
3774
|
+
await plugins.dispose();
|
|
3272
3775
|
}
|
|
3273
3776
|
};
|
|
3274
3777
|
}
|
|
@@ -3626,7 +4129,12 @@ var RealtimeDispatcher = class {
|
|
|
3626
4129
|
if (index >= 0) this.#handlers.splice(index, 1);
|
|
3627
4130
|
};
|
|
3628
4131
|
}
|
|
3629
|
-
|
|
4132
|
+
/**
|
|
4133
|
+
* Принимает обновление к обработке.
|
|
4134
|
+
*
|
|
4135
|
+
* @param coalesceKey ожидающая работа с тем же ключом заменяется новой
|
|
4136
|
+
*/
|
|
4137
|
+
dispatch(context, coalesceKey) {
|
|
3630
4138
|
let keys;
|
|
3631
4139
|
let middleware;
|
|
3632
4140
|
try {
|
|
@@ -3636,12 +4144,20 @@ var RealtimeDispatcher = class {
|
|
|
3636
4144
|
this.#hooks.middlewareError(error, context);
|
|
3637
4145
|
return;
|
|
3638
4146
|
}
|
|
3639
|
-
this.#queue.
|
|
4147
|
+
const replaced = coalesceKey === void 0 ? -1 : this.#queue.findIndex((pending) => pending.coalesceKey === coalesceKey);
|
|
4148
|
+
if (replaced < 0 && this.#queue.length >= 256) {
|
|
4149
|
+
this.#hooks.overflow();
|
|
4150
|
+
return;
|
|
4151
|
+
}
|
|
4152
|
+
const work = {
|
|
3640
4153
|
context,
|
|
3641
4154
|
middleware,
|
|
3642
4155
|
handlers: [...this.#handlers],
|
|
3643
|
-
keys
|
|
3644
|
-
|
|
4156
|
+
keys,
|
|
4157
|
+
coalesceKey
|
|
4158
|
+
};
|
|
4159
|
+
if (replaced >= 0) this.#queue[replaced] = work;
|
|
4160
|
+
else this.#queue.push(work);
|
|
3645
4161
|
this.#pump();
|
|
3646
4162
|
}
|
|
3647
4163
|
/** Отбрасывает обновления, обработка которых ещё не началась. */
|
|
@@ -3718,14 +4234,6 @@ const RECONNECT_BACKOFF = Object.freeze([
|
|
|
3718
4234
|
16e3,
|
|
3719
4235
|
3e4
|
|
3720
4236
|
]);
|
|
3721
|
-
/** Доля случайного разброса паузы. */
|
|
3722
|
-
const RECONNECT_JITTER = .3;
|
|
3723
|
-
/**
|
|
3724
|
-
* Сколько раз пытаться переподключиться подряд.
|
|
3725
|
-
*
|
|
3726
|
-
* После исчерпания поток сообщает `giveup` и ждёт ручного `connect()`.
|
|
3727
|
-
*/
|
|
3728
|
-
const MAX_RECONNECT_ATTEMPTS = 15;
|
|
3729
4237
|
/**
|
|
3730
4238
|
* Вычисляет паузу перед попыткой переподключения.
|
|
3731
4239
|
*
|
|
@@ -4026,7 +4534,8 @@ var RealtimeEngine = class {
|
|
|
4026
4534
|
}, {
|
|
4027
4535
|
deliver: (context) => this.#deps.deliver(context.update),
|
|
4028
4536
|
middlewareError: (error, context) => this.#reportDispatchError("middlewareError", error, context),
|
|
4029
|
-
handlerError: (error, context) => this.#reportDispatchError("handlerError", error, context)
|
|
4537
|
+
handlerError: (error, context) => this.#reportDispatchError("handlerError", error, context),
|
|
4538
|
+
overflow: () => this.#handleOverflow()
|
|
4030
4539
|
});
|
|
4031
4540
|
}
|
|
4032
4541
|
/** Текущее состояние соединения. */
|
|
@@ -4107,7 +4616,23 @@ var RealtimeEngine = class {
|
|
|
4107
4616
|
/** Пропускает актуальное обновление через цепочку обработчиков. */
|
|
4108
4617
|
#dispatch(update, raw, origin) {
|
|
4109
4618
|
if (!this.#wanted) return;
|
|
4110
|
-
this.#dispatcher.dispatch(this.#deps.createContext(update, raw, origin));
|
|
4619
|
+
this.#dispatcher.dispatch(this.#deps.createContext(update, raw, origin), this.#deps.coalesceKey?.(update));
|
|
4620
|
+
}
|
|
4621
|
+
/**
|
|
4622
|
+
* Очередь обновлений достигла предела: закрывает соединение и переподключается,
|
|
4623
|
+
* когда обработчики разберут очередь.
|
|
4624
|
+
*/
|
|
4625
|
+
#handleOverflow() {
|
|
4626
|
+
const controller = this.#controller;
|
|
4627
|
+
if (!this.#wanted || !controller) return;
|
|
4628
|
+
const generation = this.#generation;
|
|
4629
|
+
controller.abort();
|
|
4630
|
+
this.#controller = void 0;
|
|
4631
|
+
this.#setStatus(RealtimeStatus.Error);
|
|
4632
|
+
const error = /* @__PURE__ */ new Error("Обработчики не успевают за потоком: очередь обновлений переполнена");
|
|
4633
|
+
this.#dispatcher.drain().then(() => {
|
|
4634
|
+
if (this.#isCurrentGeneration(generation) && !this.#controller) this.#scheduleReconnect(error);
|
|
4635
|
+
});
|
|
4111
4636
|
}
|
|
4112
4637
|
/** Выполняет доменную синхронизацию, не роняя подключение из-за её ошибки. */
|
|
4113
4638
|
async #sync(reason, generation, starting) {
|
|
@@ -4132,6 +4657,7 @@ var RealtimeEngine = class {
|
|
|
4132
4657
|
baseUrl: this.#deps.baseUrl,
|
|
4133
4658
|
authorize: this.#deps.authorize,
|
|
4134
4659
|
fetch: this.#deps.fetch,
|
|
4660
|
+
request: this.#deps.request,
|
|
4135
4661
|
baseHeaders: this.#deps.baseHeaders,
|
|
4136
4662
|
getToken: this.#deps.getToken,
|
|
4137
4663
|
signal: controller.signal,
|
|
@@ -4230,7 +4756,12 @@ var RealtimeEngine = class {
|
|
|
4230
4756
|
this.#starting = void 0;
|
|
4231
4757
|
if (this.#isCurrentGeneration(generation)) this.#run(generation);
|
|
4232
4758
|
}
|
|
4233
|
-
/**
|
|
4759
|
+
/**
|
|
4760
|
+
* Завершает автоматические попытки переподключения.
|
|
4761
|
+
*
|
|
4762
|
+
* Владелец узнаёт об этом так же, как при {@link disconnect}. `onClose` вызывается
|
|
4763
|
+
* до событий: обработчик `giveup` может тут же вызвать `connect()`.
|
|
4764
|
+
*/
|
|
4234
4765
|
#giveUp(error) {
|
|
4235
4766
|
this.#wanted = false;
|
|
4236
4767
|
this.#generation += 1;
|
|
@@ -4238,6 +4769,7 @@ var RealtimeEngine = class {
|
|
|
4238
4769
|
this.#attempt = 0;
|
|
4239
4770
|
this.#detachEnvironment?.();
|
|
4240
4771
|
this.#detachEnvironment = void 0;
|
|
4772
|
+
this.#deps.onClose?.();
|
|
4241
4773
|
this.#emitEngine("error", {
|
|
4242
4774
|
error,
|
|
4243
4775
|
willReconnect: false
|
|
@@ -4310,26 +4842,15 @@ var PollTransport = class {
|
|
|
4310
4842
|
this.#limit = options.limit ?? 20;
|
|
4311
4843
|
}
|
|
4312
4844
|
async connect(context) {
|
|
4845
|
+
const request = context.request;
|
|
4846
|
+
if (!request) throw new require_storage.ItdConfigError("опрос уведомлений выполняется через конвейер клиента; создайте поток вызовом itd.realtime()");
|
|
4313
4847
|
const seen = /* @__PURE__ */ new Set();
|
|
4314
4848
|
let firstRun = true;
|
|
4315
4849
|
let lastUnreadCount;
|
|
4316
4850
|
while (!context.signal.aborted) {
|
|
4317
|
-
const
|
|
4318
|
-
if (context.authorize && !token) throw new UnauthorizedStreamError();
|
|
4319
|
-
const url = `${require_multi_storage.joinUrl(context.baseUrl, "/api/notifications/")}?limit=${this.#limit}&offset=0`;
|
|
4320
|
-
const headers = await context.baseHeaders(url);
|
|
4321
|
-
headers.set("Accept", "application/json");
|
|
4322
|
-
if (token) headers.set("Authorization", `Bearer ${token}`);
|
|
4323
|
-
const response = await context.fetch(url, {
|
|
4324
|
-
method: "GET",
|
|
4325
|
-
headers,
|
|
4326
|
-
signal: context.signal
|
|
4327
|
-
});
|
|
4328
|
-
if (context.authorize && response.status === 401) throw new UnauthorizedStreamError();
|
|
4329
|
-
if (!response.ok) throw new Error(`Опрос уведомлений вернул статус ${response.status}`);
|
|
4851
|
+
const payload = await this.#readUpdates(request, context.signal);
|
|
4330
4852
|
context.onOpen();
|
|
4331
|
-
const
|
|
4332
|
-
const items = pickArray(typeof body === "object" && body !== null && "data" in body ? body.data : body, "notifications");
|
|
4853
|
+
const items = pickArray(payload, "notifications");
|
|
4333
4854
|
for (const item of [...items].reverse()) {
|
|
4334
4855
|
const id = typeof item.id === "string" ? item.id : void 0;
|
|
4335
4856
|
if (!id || seen.has(id)) continue;
|
|
@@ -4343,7 +4864,7 @@ var PollTransport = class {
|
|
|
4343
4864
|
const excess = [...seen].slice(0, seen.size - this.#limit * 2);
|
|
4344
4865
|
for (const id of excess) seen.delete(id);
|
|
4345
4866
|
}
|
|
4346
|
-
const count = await this.#readCount(
|
|
4867
|
+
const count = await this.#readCount(request, context.signal);
|
|
4347
4868
|
if (count !== void 0 && count !== lastUnreadCount) {
|
|
4348
4869
|
lastUnreadCount = count;
|
|
4349
4870
|
context.onEvent({
|
|
@@ -4355,16 +4876,29 @@ var PollTransport = class {
|
|
|
4355
4876
|
await this.#wait(context.signal);
|
|
4356
4877
|
}
|
|
4357
4878
|
}
|
|
4358
|
-
async #
|
|
4879
|
+
async #readUpdates(request, signal) {
|
|
4359
4880
|
try {
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4881
|
+
return await request({
|
|
4882
|
+
operationId: "realtime.poll.updates",
|
|
4883
|
+
path: "/api/notifications/",
|
|
4884
|
+
query: {
|
|
4885
|
+
limit: this.#limit,
|
|
4886
|
+
offset: 0
|
|
4887
|
+
},
|
|
4888
|
+
signal
|
|
4364
4889
|
});
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4890
|
+
} catch (error) {
|
|
4891
|
+
if (require_storage.isItdAuthError(error)) throw new UnauthorizedStreamError();
|
|
4892
|
+
throw error;
|
|
4893
|
+
}
|
|
4894
|
+
}
|
|
4895
|
+
async #readCount(request, signal) {
|
|
4896
|
+
try {
|
|
4897
|
+
return pickNumber(await request({
|
|
4898
|
+
operationId: "realtime.poll.unread",
|
|
4899
|
+
path: "/api/notifications/count",
|
|
4900
|
+
signal
|
|
4901
|
+
}), "count", 0);
|
|
4368
4902
|
} catch {
|
|
4369
4903
|
return;
|
|
4370
4904
|
}
|
|
@@ -4724,6 +5258,7 @@ var ItdRealtime = class {
|
|
|
4724
5258
|
baseUrl: deps.baseUrl,
|
|
4725
5259
|
authorize: deps.authorize ?? true,
|
|
4726
5260
|
fetch: deps.fetch,
|
|
5261
|
+
request: deps.request,
|
|
4727
5262
|
clock: deps.clock,
|
|
4728
5263
|
baseHeaders: deps.baseHeaders,
|
|
4729
5264
|
getToken: deps.getToken,
|
|
@@ -4734,6 +5269,7 @@ var ItdRealtime = class {
|
|
|
4734
5269
|
transport: createTransport(deps, options),
|
|
4735
5270
|
handleFrame: (event) => this.#handleFrame(event),
|
|
4736
5271
|
readUpdate: readRealtimeUpdate,
|
|
5272
|
+
coalesceKey: (update) => update.type === RealtimeUpdateType.UnreadCount ? update.type : void 0,
|
|
4737
5273
|
createContext: (update, raw, origin) => ({
|
|
4738
5274
|
update,
|
|
4739
5275
|
stream: this,
|
|
@@ -6010,10 +6546,6 @@ function createMultipartFileBody(file) {
|
|
|
6010
6546
|
cancel
|
|
6011
6547
|
};
|
|
6012
6548
|
}
|
|
6013
|
-
//#endregion
|
|
6014
|
-
//#region src/resources/files.ts
|
|
6015
|
-
/** Таймаут одной попытки загрузки файла — 5 минут. */
|
|
6016
|
-
const DEFAULT_UPLOAD_TIMEOUT = 3e5;
|
|
6017
6549
|
/** Файлы и медиа. */
|
|
6018
6550
|
var FilesResource = class extends BaseResource {
|
|
6019
6551
|
#fetch;
|
|
@@ -8960,6 +9492,22 @@ var ItdClient = class ItdClient {
|
|
|
8960
9492
|
});
|
|
8961
9493
|
}
|
|
8962
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
|
+
/**
|
|
8963
9511
|
* Подключает плагин.
|
|
8964
9512
|
*
|
|
8965
9513
|
* Плагин может независимо регистрировать transformer логической операции и interceptor
|
|
@@ -9095,6 +9643,11 @@ var ItdClient = class ItdClient {
|
|
|
9095
9643
|
stream = new ItdRealtime({
|
|
9096
9644
|
baseUrl: this.#runtime.config.baseUrl,
|
|
9097
9645
|
fetch: this.#runtime.config.fetch,
|
|
9646
|
+
request: ({ operationId, path, query, signal }) => this.#runtime.http.operation(operationId, {
|
|
9647
|
+
path,
|
|
9648
|
+
query,
|
|
9649
|
+
signal
|
|
9650
|
+
}),
|
|
9098
9651
|
baseHeaders: (url) => this.#runtime.platformHeaders(url),
|
|
9099
9652
|
getAuthIdentity: () => this.#runtime.auth.getCurrentAuthIdentity(),
|
|
9100
9653
|
getAuthScope: () => this.#runtime.auth.getAuthScope(),
|
|
@@ -9114,13 +9667,16 @@ var ItdClient = class ItdClient {
|
|
|
9114
9667
|
* Освобождает ресурсы клиента: закрывает все потоки уведомлений, отправляет открытые
|
|
9115
9668
|
* накопители {@link telemetry}, затем останавливает очередь запросов.
|
|
9116
9669
|
*
|
|
9117
|
-
* Метод дожидается активных обработчиков
|
|
9118
|
-
* снова; ранее созданный поток можно запустить
|
|
9670
|
+
* Метод дожидается активных обработчиков потока, но не дольше `shutdownTimeout`. После
|
|
9671
|
+
* вызова клиентом можно пользоваться снова; ранее созданный поток можно запустить
|
|
9672
|
+
* повторным `connect()`.
|
|
9119
9673
|
*
|
|
9120
9674
|
* Общая очередь, полученная от {@link ItdAccounts}, не останавливается: её гасит сам
|
|
9121
9675
|
* контейнер, когда закрывает все аккаунты разом.
|
|
9122
9676
|
*
|
|
9123
9677
|
* Терминальное освобождение — это {@link dispose}.
|
|
9678
|
+
*
|
|
9679
|
+
* @throws {ItdStateError} если обработчики потока не завершились за отведённый срок
|
|
9124
9680
|
*/
|
|
9125
9681
|
async close() {
|
|
9126
9682
|
return this.#close(false);
|
|
@@ -9128,22 +9684,30 @@ var ItdClient = class ItdClient {
|
|
|
9128
9684
|
async #close(disposeCleanup) {
|
|
9129
9685
|
const streams = this.#disconnectStreams();
|
|
9130
9686
|
if (disposeCleanup) prepareTelemetryForDispose(this.#telemetry);
|
|
9687
|
+
const { shutdownTimeout, clock } = this.#runtime.config;
|
|
9688
|
+
const deadline = createDeadline(shutdownTimeout, clock);
|
|
9689
|
+
let stuck = [];
|
|
9131
9690
|
try {
|
|
9132
|
-
await Promise.all(streams.map((stream) => stream.drain()));
|
|
9691
|
+
stuck = (await Promise.all(streams.map(async (stream) => await deadline.wait(stream.drain()) ? void 0 : stream))).filter((stream) => stream !== void 0);
|
|
9133
9692
|
if (disposeCleanup) await closeTelemetryForDispose(this.#telemetry);
|
|
9134
9693
|
else await this.#telemetry?.close();
|
|
9135
9694
|
} finally {
|
|
9695
|
+
deadline.cancel();
|
|
9136
9696
|
this.#runtime.close();
|
|
9137
9697
|
}
|
|
9698
|
+
if (stuck.length > 0) throw new require_storage.ItdStateError(`обработчики потоков (${stuck.map((stream) => stream.transport).join(", ")}) не завершились за ${shutdownTimeout} мс; ожидание прекращено`);
|
|
9138
9699
|
}
|
|
9139
9700
|
/**
|
|
9140
|
-
* Окончательно освобождает клиент: выполняет {@link close}
|
|
9701
|
+
* Окончательно освобождает клиент: выполняет {@link close}, отменяет незавершённые
|
|
9702
|
+
* запросы и отключает все плагины.
|
|
9141
9703
|
*
|
|
9142
9704
|
* Терминальное состояние устанавливается сразу при первом вызове. После этого новые
|
|
9143
9705
|
* запросы, подключение плагинов, регистрация сервисов и создание или повторный запуск
|
|
9144
9706
|
* realtime-потоков завершаются с {@link ItdStateError}. Повторные вызовы возвращают
|
|
9145
9707
|
* тот же результат очистки.
|
|
9146
9708
|
*
|
|
9709
|
+
* Ожидание обработчиков потока и операций плагинов ограничено `shutdownTimeout`.
|
|
9710
|
+
*
|
|
9147
9711
|
* @example
|
|
9148
9712
|
* ```ts
|
|
9149
9713
|
* await using itd = new ItdClient({ auth: token });
|
|
@@ -9313,7 +9877,7 @@ var ItdAccounts = class ItdAccounts {
|
|
|
9313
9877
|
this.#base = base;
|
|
9314
9878
|
this.#storage = storage ?? new require_multi_storage.MemoryMultiTokenStorage();
|
|
9315
9879
|
this.#plugins = orderPluginDefinitions(plugins ?? []);
|
|
9316
|
-
this.#rateLimitScope = rateLimitScope ?? "
|
|
9880
|
+
this.#rateLimitScope = rateLimitScope ?? "shared";
|
|
9317
9881
|
const rateLimit = this.#rateLimitScope === "shared" ? resolveRateLimit(base.rateLimit) : void 0;
|
|
9318
9882
|
this.#queues = rateLimit ? new RequestQueuePool(rateLimit, base.clock ?? systemClock) : void 0;
|
|
9319
9883
|
const logger = typeof base.logger === "object" ? base.logger : void 0;
|
|
@@ -9605,7 +10169,7 @@ var ItdAccounts = class ItdAccounts {
|
|
|
9605
10169
|
...clients.map((client) => client.dispose()),
|
|
9606
10170
|
...controls.map((control) => control.drain()),
|
|
9607
10171
|
...accountRemovals,
|
|
9608
|
-
Promise.resolve().then(() => this.#queues?.
|
|
10172
|
+
Promise.resolve().then(() => this.#queues?.clear())
|
|
9609
10173
|
]);
|
|
9610
10174
|
this.#plugins.splice(0);
|
|
9611
10175
|
this.#removingPlugins.clear();
|
|
@@ -10122,10 +10686,6 @@ var RealtimeComposer = class RealtimeComposer {
|
|
|
10122
10686
|
return (context, next) => runRealtimeMiddleware(snapshot, context, next);
|
|
10123
10687
|
}
|
|
10124
10688
|
};
|
|
10125
|
-
//#endregion
|
|
10126
|
-
//#region src/realtime/websocket.ts
|
|
10127
|
-
/** Стандартный путь WebSocket-подключения. */
|
|
10128
|
-
const WEBSOCKET_PATH = "/api/ws";
|
|
10129
10689
|
const CONNECTING = 0;
|
|
10130
10690
|
const OPEN = 1;
|
|
10131
10691
|
const NORMAL_CLOSURE = 1e3;
|
|
@@ -10263,6 +10823,7 @@ var WebSocketTransport = class {
|
|
|
10263
10823
|
let discardMessages = false;
|
|
10264
10824
|
let socketError;
|
|
10265
10825
|
let messageQueue = Promise.resolve();
|
|
10826
|
+
let pendingFrames = 0;
|
|
10266
10827
|
let cancelHandshake;
|
|
10267
10828
|
let cancelIdle;
|
|
10268
10829
|
let cancelKeepAlive;
|
|
@@ -10341,7 +10902,13 @@ var WebSocketTransport = class {
|
|
|
10341
10902
|
pending = Promise.resolve({ error });
|
|
10342
10903
|
}
|
|
10343
10904
|
if (pending === void 0) return;
|
|
10905
|
+
if (pendingFrames >= 256) {
|
|
10906
|
+
closeWithError(/* @__PURE__ */ new Error(`WebSocket ${redactUrl(url)}: очередь кадров переполнена`), 4e3, "queue overflow");
|
|
10907
|
+
return;
|
|
10908
|
+
}
|
|
10909
|
+
pendingFrames += 1;
|
|
10344
10910
|
messageQueue = messageQueue.then(async () => {
|
|
10911
|
+
pendingFrames -= 1;
|
|
10345
10912
|
if (settled || discardMessages) return;
|
|
10346
10913
|
const decoded = await pending;
|
|
10347
10914
|
if ("error" in decoded) {
|
|
@@ -10636,21 +11203,12 @@ function renderSpans(content, spans = [], options = {}) {
|
|
|
10636
11203
|
//#endregion
|
|
10637
11204
|
exports.ALLOWED_MIME_TYPES = ALLOWED_MIME_TYPES;
|
|
10638
11205
|
exports.AUDIO_MIME_TYPES = AUDIO_MIME_TYPES;
|
|
10639
|
-
exports.AUTH_FLAG_COOKIE = require_multi_storage.AUTH_FLAG_COOKIE;
|
|
10640
|
-
exports.AUTH_PATHS = AUTH_PATHS;
|
|
10641
11206
|
exports.AccessType = AccessType;
|
|
10642
11207
|
exports.AttachmentType = AttachmentType;
|
|
10643
|
-
exports.
|
|
11208
|
+
exports.BUCKET_LIMITS = BUCKET_LIMITS;
|
|
10644
11209
|
exports.CommentSort = CommentSort;
|
|
10645
11210
|
exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
|
|
10646
|
-
exports.
|
|
10647
|
-
exports.DEFAULT_STATUS_BASE_URL = DEFAULT_STATUS_BASE_URL;
|
|
10648
|
-
exports.DEFAULT_TIMEOUT = DEFAULT_TIMEOUT;
|
|
10649
|
-
exports.DEFAULT_UPLOAD_TIMEOUT = DEFAULT_UPLOAD_TIMEOUT;
|
|
10650
|
-
exports.DEFAULT_URL_FILE_MAX_BYTES = require_multi_storage.DEFAULT_URL_FILE_MAX_BYTES;
|
|
10651
|
-
exports.DEFAULT_USER_AGENT = DEFAULT_USER_AGENT;
|
|
10652
|
-
exports.DEVICE_ID_HEADER = DEVICE_ID_HEADER;
|
|
10653
|
-
exports.DetectedRuntime = DetectedRuntime;
|
|
11211
|
+
exports.DEFAULT_RATE_LIMIT_BUCKET = DEFAULT_RATE_LIMIT_BUCKET;
|
|
10654
11212
|
exports.FeedTab = FeedTab;
|
|
10655
11213
|
exports.FileTransferMode = require_multi_storage.FileTransferMode;
|
|
10656
11214
|
exports.IMAGE_MIME_TYPES = IMAGE_MIME_TYPES;
|
|
@@ -10681,19 +11239,14 @@ exports.ItdTimeoutError = require_storage.ItdTimeoutError;
|
|
|
10681
11239
|
exports.ItdValidationError = require_storage.ItdValidationError;
|
|
10682
11240
|
exports.LIBRARY_VERSION = LIBRARY_VERSION;
|
|
10683
11241
|
exports.LikesVisibility = LikesVisibility;
|
|
10684
|
-
exports.MAX_RECONNECT_ATTEMPTS = MAX_RECONNECT_ATTEMPTS;
|
|
10685
11242
|
exports.MemoryKeyValueStore = require_storage.MemoryKeyValueStore;
|
|
10686
11243
|
exports.MemoryMultiTokenStorage = require_multi_storage.MemoryMultiTokenStorage;
|
|
10687
11244
|
exports.MemoryTokenStorage = require_storage.MemoryTokenStorage;
|
|
10688
|
-
exports.NOTIFICATION_TYPE_ALIASES = NOTIFICATION_TYPE_ALIASES;
|
|
10689
11245
|
exports.NotificationType = NotificationType;
|
|
10690
11246
|
exports.OPERATIONS = OPERATIONS;
|
|
10691
11247
|
exports.PaginationMode = PaginationMode;
|
|
10692
11248
|
exports.Paginator = Paginator;
|
|
10693
|
-
exports.
|
|
10694
|
-
exports.RECONNECT_JITTER = RECONNECT_JITTER;
|
|
10695
|
-
exports.REFRESH_COOKIE = require_multi_storage.REFRESH_COOKIE;
|
|
10696
|
-
exports.REFRESH_COOKIE_PATH = require_multi_storage.REFRESH_COOKIE_PATH;
|
|
11249
|
+
exports.RateLimitPacing = RateLimitPacing;
|
|
10697
11250
|
exports.RealtimeComposer = RealtimeComposer;
|
|
10698
11251
|
exports.RealtimeRouter = RealtimeRouter;
|
|
10699
11252
|
exports.RealtimeStatus = RealtimeStatus;
|
|
@@ -10705,8 +11258,6 @@ exports.ReportTargetType = ReportTargetType;
|
|
|
10705
11258
|
exports.RetrySafety = RetrySafety;
|
|
10706
11259
|
exports.RuntimeMode = RuntimeMode;
|
|
10707
11260
|
exports.STATUS_SERVICE = STATUS_SERVICE;
|
|
10708
|
-
exports.STREAM_PATH = STREAM_PATH;
|
|
10709
|
-
exports.ServiceRegistry = ServiceRegistry;
|
|
10710
11261
|
exports.ServiceState = ServiceState;
|
|
10711
11262
|
exports.SignInStatus = SignInStatus;
|
|
10712
11263
|
exports.SpanRenderFormat = SpanRenderFormat;
|
|
@@ -10716,7 +11267,6 @@ exports.UnauthorizedStreamError = UnauthorizedStreamError;
|
|
|
10716
11267
|
exports.VIDEO_MIME_TYPES = VIDEO_MIME_TYPES;
|
|
10717
11268
|
exports.ViewReason = ViewReason;
|
|
10718
11269
|
exports.ViewSource = ViewSource;
|
|
10719
|
-
exports.WEBSOCKET_PATH = WEBSOCKET_PATH;
|
|
10720
11270
|
exports.WallAccess = WallAccess;
|
|
10721
11271
|
exports.WebSocketTransport = WebSocketTransport;
|
|
10722
11272
|
exports.autoSpans = autoSpans;
|
|
@@ -10751,14 +11301,13 @@ exports.isMyProfile = isMyProfile;
|
|
|
10751
11301
|
exports.mapPage = mapPage;
|
|
10752
11302
|
exports.markup = markup;
|
|
10753
11303
|
exports.normalizeNotification = normalizeNotification;
|
|
11304
|
+
exports.operationBucket = operationBucket;
|
|
10754
11305
|
exports.operationMethod = operationMethod;
|
|
10755
11306
|
exports.operationRetrySafety = operationRetrySafety;
|
|
10756
11307
|
exports.parseHtml = parseHtml;
|
|
10757
11308
|
exports.parseMarkdown = parseMarkdown;
|
|
10758
11309
|
exports.poll = poll;
|
|
10759
11310
|
exports.post = post;
|
|
10760
|
-
exports.readNotificationEvent = readNotificationEvent;
|
|
10761
|
-
exports.readUnreadCountEvent = readUnreadCountEvent;
|
|
10762
11311
|
exports.renderSpans = renderSpans;
|
|
10763
11312
|
exports.report = report;
|
|
10764
11313
|
exports.resolveNotificationUrl = resolveNotificationUrl;
|