vigor-roblox 1.0.4 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,1746 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const VigorErrorMessageFuncs = {
4
- INVALID_TARGET: ({ expected, received }) => `Invalid Task: ${typeof received} (expected: ${expected.join(', ')})`,
5
- EXHAUSTED: ({ maxAttempts }) => `Retry exhausted, (max ${maxAttempts})`,
6
- TIMED_OUT: ({ limit, attempt }) => `Timeout: exceeded ${limit}ms (attempt: ${attempt})`,
7
- INVALID_CONTENT_TYPE: ({ expected, received, response }) => `Invalid Content Type Header: ${typeof received} (expected: ${expected.join(', ')})`,
8
- PARSER_NOT_FOUND: ({ expected, received, response }) => `Parser Not Found For Header: ${typeof received} (expected: ${expected.join(', ')})`,
9
- PARSER_ALL_FAILED: ({ tried, response }) => `All Parser Failed, Tried: ${tried.join(', ')}`,
10
- INVALID_PROTOCOL: ({ expected, received }) => `Invalid Protocol: ${typeof received} (expected: ${expected.join(', ')})`,
11
- INVALID_BODY: ({ expected, received }) => `Invalid Body: ${typeof received} (expected: ${expected.join(', ')})`,
12
- FETCH_FAILED: ({ status, response, url, headers, body, statusText }) => `Fetch Failed: ${status}`,
13
- EMPTY_TARGET: ({}) => `Empty Body`
14
- };
15
- class VigorError extends Error {
16
- timestamp = new Date();
17
- cause;
18
- code;
19
- data;
20
- method;
21
- stats;
22
- context;
23
- constructor(code, options) {
24
- const messageFn = VigorErrorMessageFuncs[code];
25
- const message = `[${code}] ${messageFn(options?.data)}`;
26
- super(message, { cause: options?.cause });
27
- this.name = new.target.name;
28
- this.code = code;
29
- this.cause = options.cause;
30
- this.data = options.data;
31
- this.method = options.method;
32
- this.stats = options.stats;
33
- this.context = options.context;
34
- Object.setPrototypeOf(this, new.target.prototype);
35
- Error.captureStackTrace?.(this, new.target);
36
- }
37
- }
38
- class VigorRetryError extends VigorError {
39
- constructor(code, options) {
40
- super(code, options);
41
- }
42
- }
43
- class VigorParseError extends VigorError {
44
- constructor(code, options) {
45
- super(code, options);
46
- }
47
- }
48
- class VigorFetchError extends VigorError {
49
- constructor(code, options) {
50
- super(code, options);
51
- }
52
- }
53
- class VigorAllError extends VigorError {
54
- constructor(code, options) {
55
- super(code, options);
56
- }
57
- }
58
- class VigorStatus {
59
- _base;
60
- ctor;
61
- _config;
62
- constructor(config = {}, _base, ctor) {
63
- this._base = _base;
64
- this.ctor = ctor;
65
- this._config = { ...this._base, ...(config || {}) };
66
- }
67
- _mergeConfig(source, target) {
68
- const isPlainObject = (val) => val !== null && typeof val === 'object' && Object.getPrototypeOf(val) === Object.prototype;
69
- if (target === undefined || target === null) {
70
- return source;
71
- }
72
- if (isPlainObject(source) && isPlainObject(target)) {
73
- const result = { ...source };
74
- Object.keys(target).forEach((key) => {
75
- result[key] = this._mergeConfig(result[key], target[key]);
76
- });
77
- return result;
78
- }
79
- if (Array.isArray(source) && Array.isArray(target)) {
80
- return [...source, ...target];
81
- }
82
- return target;
83
- }
84
- _next(config) { return this.ctor(this._mergeConfig(this._config, config)); }
85
- _getConfig() { return this._config; }
86
- _getBase() { return this._base; }
87
- }
88
- const VigorDefault = Symbol("DEFAULT");
89
- class VigorRetrySettings extends VigorStatus {
90
- constructor(config) {
91
- const base = {
92
- default: VigorDefault,
93
- timeout: 20 * 1000,
94
- attempt: 5,
95
- jitter: 1000
96
- };
97
- super(config, base, (c) => new VigorRetrySettings(c));
98
- }
99
- default(unk) { return this._next({ default: unk }); }
100
- timeout(num) { return this._next({ timeout: num }); }
101
- attempt(num) { return this._next({ attempt: num }); }
102
- jitter(num) { return this._next({ jitter: num }); }
103
- }
104
- class VigorRetryInterceptors extends VigorStatus {
105
- constructor(config) {
106
- const base = {
107
- before: [],
108
- after: [],
109
- result: [],
110
- retryIf: [],
111
- onRetry: [],
112
- onError: []
113
- };
114
- super(config, base, (c) => new VigorRetryInterceptors(c));
115
- }
116
- before(...funcs) { return this._next({ before: funcs.flat() }); }
117
- after(...funcs) { return this._next({ after: funcs.flat() }); }
118
- result(...funcs) { return this._next({ result: funcs.flat() }); }
119
- retryIf(...funcs) { return this._next({ retryIf: funcs.flat() }); }
120
- onRetry(...funcs) { return this._next({ onRetry: funcs.flat() }); }
121
- onError(...funcs) { return this._next({ onError: funcs.flat() }); }
122
- }
123
- class VigorRetryAlgorithmsConstant extends VigorStatus {
124
- constructor(config) {
125
- const base = {
126
- interval: 2000
127
- };
128
- super(config, base, (c) => new VigorRetryAlgorithmsConstant(c));
129
- }
130
- interval(num) { return this._next({ interval: num }); }
131
- _calculateDelay(attempt) {
132
- return this._config.interval;
133
- }
134
- }
135
- class VigorRetryAlgorithmsLinear extends VigorStatus {
136
- constructor(config) {
137
- const base = {
138
- initial: 1000,
139
- increment: 1000,
140
- minDelay: 500,
141
- maxDelay: 20 * 1000
142
- };
143
- super(config, base, (c) => new VigorRetryAlgorithmsLinear(c));
144
- }
145
- initial(num) { return this._next({ initial: num }); }
146
- increment(num) { return this._next({ increment: num }); }
147
- minDelay(num) { return this._next({ minDelay: num }); }
148
- maxDelay(num) { return this._next({ maxDelay: num }); }
149
- _calculateDelay(attempt) {
150
- const { initial, increment, minDelay, maxDelay } = this._config;
151
- return Math.max(minDelay, Math.min(maxDelay, initial + increment * attempt));
152
- }
153
- }
154
- class VigorRetryAlgorithmsBackoff extends VigorStatus {
155
- constructor(config) {
156
- const base = {
157
- initial: 1000,
158
- multiplier: 1.7,
159
- unit: 1000,
160
- minDelay: 500,
161
- maxDelay: 20 * 1000
162
- };
163
- super(config, base, (c) => new VigorRetryAlgorithmsBackoff(c));
164
- }
165
- initial(num) { return this._next({ initial: num }); }
166
- multiplier(num) { return this._next({ multiplier: num }); }
167
- unit(num) { return this._next({ unit: num }); }
168
- minDelay(num) { return this._next({ minDelay: num }); }
169
- maxDelay(num) { return this._next({ maxDelay: num }); }
170
- _calculateDelay(attempt) {
171
- const { initial, multiplier, unit, minDelay, maxDelay } = this._config;
172
- return Math.max(minDelay, Math.min(maxDelay, initial + unit * Math.pow(multiplier, attempt)));
173
- }
174
- }
175
- class VigorRetryAlgorithmsCustom extends VigorStatus {
176
- constructor(config) {
177
- const base = {
178
- func: (attempt) => attempt * 1000,
179
- minDelay: 500,
180
- maxDelay: 20 * 1000
181
- };
182
- super(config, base, (c) => new VigorRetryAlgorithmsCustom(c));
183
- }
184
- func(num) { return this._next({ func: num }); }
185
- _calculateDelay(attempt) {
186
- const { func, minDelay, maxDelay } = this._config;
187
- return Math.max(minDelay, Math.min(maxDelay, func(attempt)));
188
- }
189
- }
190
- class VigorRetry extends VigorStatus {
191
- constructor(config) {
192
- const base = {
193
- target: VigorDefault,
194
- settings: new VigorRetrySettings()._getBase(),
195
- interceptors: new VigorRetryInterceptors()._getBase(),
196
- algorithm: (attempt) => new VigorRetryAlgorithmsBackoff()._calculateDelay(attempt),
197
- abortSignals: []
198
- };
199
- super(config, base, (c) => new VigorRetry(c));
200
- }
201
- RetryAlgorithms = {
202
- constant: (config) => new VigorRetryAlgorithmsConstant(config),
203
- linear: (config) => new VigorRetryAlgorithmsLinear(config),
204
- backoff: (config) => new VigorRetryAlgorithmsBackoff(config),
205
- custom: (config) => new VigorRetryAlgorithmsCustom(config)
206
- };
207
- _createTimelineHandler(timeline) {
208
- return (action, content) => {
209
- timeline.push({
210
- action: action,
211
- content: content,
212
- time: Date.now()
213
- });
214
- };
215
- }
216
- _createInterceptorHandler(ctx, addTimeline) {
217
- return async (interceptorType, api) => {
218
- const interceptorsConfig = ctx["stats"]["interceptors"];
219
- const interceptors = interceptorsConfig[interceptorType];
220
- addTimeline("INTERCEPTOR_LOOP_STARTED", {
221
- interceptorType: interceptorType,
222
- interceptors,
223
- });
224
- const startTime = performance.now();
225
- for (const func of interceptors) {
226
- const scopedApi = api(interceptorType, func);
227
- await func(ctx, scopedApi);
228
- }
229
- const endTime = performance.now();
230
- addTimeline("INTERCEPTOR_LOOP_ENDED", {
231
- interceptorType: interceptorType,
232
- interceptors,
233
- took: endTime - startTime
234
- });
235
- };
236
- }
237
- target(func) { return this._next({ target: func }); }
238
- settings(func) {
239
- if (func instanceof VigorRetrySettings) {
240
- return this._next({ settings: func._getConfig() });
241
- }
242
- if (typeof func === 'function') {
243
- return this._next({ settings: func(new VigorRetrySettings(this._config.settings))._getConfig() });
244
- }
245
- return this._next({ settings: func });
246
- }
247
- interceptors(func) {
248
- if (func instanceof VigorRetryInterceptors) {
249
- return this._next({ interceptors: func._getConfig() });
250
- }
251
- if (typeof func === 'function') {
252
- return this._next({ interceptors: func(new VigorRetryInterceptors(this._config.interceptors))._getConfig() });
253
- }
254
- return this._next({ interceptors: func });
255
- }
256
- algorithms(func) {
257
- const instance = func(this.RetryAlgorithms);
258
- return this._next({ algorithm: (attempt) => instance._calculateDelay(attempt) });
259
- }
260
- abortSignals(...abortSignals) {
261
- return this._next({ abortSignals: abortSignals.flat() });
262
- }
263
- async request(config, timeline = []) {
264
- const stats = this._mergeConfig(this._config, config);
265
- let ctx = {
266
- result: VigorDefault,
267
- error: VigorDefault,
268
- attempt: 0,
269
- delay: 0,
270
- controller: VigorDefault,
271
- timeline: timeline,
272
- stats,
273
- flag: {
274
- broke: false,
275
- overwritten: false,
276
- restarted: false
277
- }
278
- };
279
- const addTimeline = this._createTimelineHandler(ctx.timeline);
280
- const handleInterceptor = this._createInterceptorHandler(ctx, addTimeline);
281
- addTimeline("PROCESS_HANDLING", {
282
- type: "REQUEST_START",
283
- data: {}
284
- });
285
- try {
286
- if (typeof stats.target !== 'function')
287
- throw new VigorRetryError("INVALID_TARGET", {
288
- method: "request",
289
- data: {
290
- expected: ["function"],
291
- received: stats.target
292
- },
293
- stats: stats,
294
- context: ctx
295
- });
296
- while (ctx.attempt < stats.settings.attempt) {
297
- ctx.attempt++;
298
- addTimeline("ATTEMPT_INCREASED", {
299
- attempt: ctx.attempt
300
- });
301
- try {
302
- addTimeline("PROCESS_HANDLING", {
303
- type: "RETRY_START",
304
- data: {}
305
- });
306
- const controller = new AbortController();
307
- const timeoutController = new AbortController();
308
- const signal = AbortSignal.any([controller.signal, timeoutController.signal, ...stats.abortSignals]);
309
- await handleInterceptor("before", (interceptorType, func) => ({
310
- abort: (error) => {
311
- addTimeline("INTERCEPTOR_API_CALLED", {
312
- interceptorType,
313
- interceptor: func,
314
- method: "abort",
315
- args: [error]
316
- });
317
- controller.abort(error);
318
- throw error;
319
- },
320
- breakRetry: (error) => {
321
- addTimeline("INTERCEPTOR_API_CALLED", {
322
- interceptorType,
323
- interceptor: func,
324
- method: "breakRetry",
325
- args: [error]
326
- });
327
- ctx.flag.broke = true;
328
- throw error;
329
- },
330
- throwError: (error) => {
331
- addTimeline("INTERCEPTOR_API_CALLED", {
332
- interceptorType,
333
- interceptor: func,
334
- method: "throwError",
335
- args: [error]
336
- });
337
- throw error;
338
- }
339
- }));
340
- const timeoutTimer = setTimeout(() => {
341
- clearTimeout(timeoutTimer);
342
- timeoutController.abort(new VigorRetryError("TIMED_OUT", {
343
- method: "request",
344
- data: {
345
- limit: stats.settings.timeout,
346
- attempt: ctx.attempt
347
- },
348
- }));
349
- }, stats.settings.timeout);
350
- signal.throwIfAborted();
351
- let onAbort;
352
- try {
353
- addTimeline("TARGET_REQUEST_STARTED", {
354
- target: stats.target
355
- });
356
- const abort = (error) => {
357
- addTimeline("TARGET_API_CALLED", {
358
- target: stats.target,
359
- method: "abort"
360
- });
361
- controller.abort(error);
362
- throw error;
363
- };
364
- const started = performance.now();
365
- ctx.result = await Promise.race([
366
- stats.target(ctx, { abort, signal }),
367
- new Promise((_, rej) => {
368
- onAbort = () => rej(signal.reason);
369
- signal.addEventListener("abort", onAbort);
370
- })
371
- ]);
372
- const endTime = performance.now();
373
- addTimeline("TARGET_REQUEST_ENDED", {
374
- target: stats.target,
375
- took: endTime - started
376
- });
377
- }
378
- finally {
379
- clearTimeout(timeoutTimer);
380
- if (onAbort)
381
- signal.removeEventListener("abort", onAbort);
382
- }
383
- await handleInterceptor("after", (interceptorType, func) => ({
384
- setResult: (unknown) => {
385
- addTimeline("INTERCEPTOR_API_CALLED", {
386
- interceptorType,
387
- interceptor: func,
388
- method: "setResult",
389
- args: [unknown]
390
- });
391
- ctx.result = unknown;
392
- return unknown;
393
- },
394
- throwError: (error) => {
395
- addTimeline("INTERCEPTOR_API_CALLED", {
396
- interceptorType,
397
- interceptor: func,
398
- method: "throwError",
399
- args: [error]
400
- });
401
- throw error;
402
- },
403
- breakRetry: (error) => {
404
- addTimeline("INTERCEPTOR_API_CALLED", {
405
- interceptorType,
406
- interceptor: func,
407
- method: "breakRetry",
408
- args: [error]
409
- });
410
- ctx.flag.broke = true;
411
- throw error;
412
- },
413
- }));
414
- await handleInterceptor("result", (interceptorType, func) => ({
415
- setResult: (unknown) => {
416
- addTimeline("INTERCEPTOR_API_CALLED", {
417
- interceptorType,
418
- interceptor: func,
419
- method: "setResult",
420
- args: [unknown]
421
- });
422
- ctx.result = unknown;
423
- return unknown;
424
- },
425
- throwError: (error) => {
426
- addTimeline("INTERCEPTOR_API_CALLED", {
427
- interceptorType,
428
- interceptor: func,
429
- method: "throwError",
430
- args: [error]
431
- });
432
- throw error;
433
- },
434
- }));
435
- return ctx.result;
436
- }
437
- catch (error) {
438
- ctx.error = error;
439
- addTimeline("PROCESS_HANDLING", {
440
- type: "RETRY_ERROR",
441
- data: {
442
- error
443
- }
444
- });
445
- if (ctx.flag.broke)
446
- throw error;
447
- let proceed = true;
448
- await handleInterceptor("retryIf", (interceptorType, func) => ({
449
- proceedRetry: () => {
450
- addTimeline("INTERCEPTOR_API_CALLED", {
451
- interceptorType,
452
- interceptor: func,
453
- method: "proceedRetry",
454
- args: []
455
- });
456
- return proceed = true;
457
- },
458
- cancelRetry: () => {
459
- addTimeline("INTERCEPTOR_API_CALLED", {
460
- interceptorType,
461
- interceptor: func,
462
- method: "cancelRetry",
463
- args: []
464
- });
465
- return proceed = false;
466
- }
467
- }));
468
- if (!proceed)
469
- throw error;
470
- ctx.delay = VigorDefault;
471
- await handleInterceptor("onRetry", (interceptorType, func) => ({
472
- throwError: (error) => {
473
- addTimeline("INTERCEPTOR_API_CALLED", {
474
- interceptorType,
475
- interceptor: func,
476
- method: "throwError",
477
- args: [error]
478
- });
479
- throw error;
480
- },
481
- setDelay: (number) => {
482
- addTimeline("INTERCEPTOR_API_CALLED", {
483
- interceptorType,
484
- interceptor: func,
485
- method: "setDelay",
486
- args: [number]
487
- });
488
- return ctx.delay = number;
489
- },
490
- setAttempt: (number) => {
491
- addTimeline("INTERCEPTOR_API_CALLED", {
492
- interceptorType,
493
- interceptor: func,
494
- method: "setAttempt",
495
- args: [number]
496
- });
497
- return ctx.attempt = number;
498
- }
499
- }));
500
- if (typeof ctx.delay !== 'number')
501
- ctx.delay = stats.algorithm(ctx.attempt) + Math.random() * stats.settings.jitter;
502
- const delay = ctx.delay;
503
- await new Promise(r => setTimeout(r, delay));
504
- }
505
- }
506
- throw new VigorRetryError("EXHAUSTED", {
507
- method: "request",
508
- data: {
509
- maxAttempts: stats.settings.attempt,
510
- },
511
- context: ctx
512
- });
513
- }
514
- catch (error) {
515
- ctx.error = error;
516
- addTimeline("PROCESS_HANDLING", {
517
- type: "REQUEST_ERROR",
518
- data: {
519
- error
520
- }
521
- });
522
- await handleInterceptor("onError", (interceptorType, func) => ({
523
- setResult: (unknown) => {
524
- addTimeline("INTERCEPTOR_API_CALLED", {
525
- interceptorType,
526
- interceptor: func,
527
- method: "setResult",
528
- args: [unknown]
529
- });
530
- ctx.result = unknown;
531
- ctx.flag.overwritten = true;
532
- return unknown;
533
- },
534
- throwError: (error) => {
535
- addTimeline("INTERCEPTOR_API_CALLED", {
536
- interceptorType,
537
- interceptor: func,
538
- method: "throwError",
539
- args: [error]
540
- });
541
- throw error;
542
- },
543
- restart: () => {
544
- addTimeline("INTERCEPTOR_API_CALLED", {
545
- interceptorType,
546
- interceptor: func,
547
- method: "restart",
548
- args: []
549
- });
550
- ctx.flag.restarted = true;
551
- }
552
- }));
553
- if (ctx.flag.restarted) {
554
- return await this.request(stats, ctx.timeline);
555
- }
556
- if (ctx.flag.overwritten)
557
- return ctx.result;
558
- if (stats.settings.default !== VigorDefault)
559
- return stats.settings.default;
560
- throw error;
561
- }
562
- }
563
- }
564
- class VigorParseSettings extends VigorStatus {
565
- constructor(config) {
566
- const base = {
567
- raw: false,
568
- default: VigorDefault
569
- };
570
- super(config, base, (c) => new VigorParseSettings(c));
571
- }
572
- original(bool) { return this._next({ raw: bool }); }
573
- default(unk) { return this._next({ default: unk }); }
574
- }
575
- class VigorParseStrategies extends VigorStatus {
576
- constructor(config) {
577
- const base = {
578
- funcs: []
579
- };
580
- super(config, base, (c) => new VigorParseStrategies(c));
581
- }
582
- ParseAutoHeaders = [
583
- { header: "application/json", regExp: /application\/(.+\+)?json(.+\+)?/i, method: (res) => res.json() },
584
- { header: "application/xml", regExp: /application\/(.+\+)?xml(.+\+)?/i, method: (res) => res.text() },
585
- { header: "application/x-www-form-urlencoded", regExp: /application\/(.+\+)?x-www-form-urlencoded(.+\+)?/i, method: (res) => res.formData() },
586
- { header: "application/octet-stream", regExp: /application\/(.+\+)?octet-stream(.+\+)?/i, method: (res) => res.arrayBuffer() },
587
- { header: "image/*", regExp: /^image\/.+/i, method: (res) => res.blob() },
588
- { header: "audio/*", regExp: /^audio\/.+/i, method: (res) => res.blob() },
589
- { header: "video/*", regExp: /^video\/.+/i, method: (res) => res.blob() },
590
- { header: "multipart/form-data", regExp: /multipart\/(.+\+)?form-data(.+\+)?/i, method: (res) => res.formData() },
591
- { header: "text/*", regExp: /^text\/.+/i, method: (res) => res.text() },
592
- ];
593
- ParseAutoMethods = [
594
- { title: "json", method: (res) => res.json() },
595
- { title: "formData", method: (res) => res.formData() },
596
- { title: "text", method: (res) => res.text() },
597
- { title: "blob", method: (res) => res.blob() },
598
- ];
599
- ParseAutoAlgorithms = {
600
- contentType: async (response) => {
601
- const parsers = this.ParseAutoHeaders;
602
- const contentTypeHeader = response.headers.get("content-type");
603
- if (!contentTypeHeader)
604
- throw new VigorParseError("INVALID_CONTENT_TYPE", {
605
- method: "ParseAutoAlgorithms.contentType",
606
- data: {
607
- expected: ["string"],
608
- received: contentTypeHeader,
609
- response: response
610
- }
611
- });
612
- const toDo = parsers.find(parser => parser.regExp.test(contentTypeHeader));
613
- if (!toDo)
614
- throw new VigorParseError("PARSER_NOT_FOUND", {
615
- method: "ParseAutoAlgorithms.contentType",
616
- data: {
617
- expected: parsers.map(parser => parser.header),
618
- received: contentTypeHeader,
619
- response: response
620
- }
621
- });
622
- return await toDo.method(response);
623
- },
624
- sniff: async (response) => {
625
- const parsers = this.ParseAutoMethods;
626
- for (const [i, parser] of parsers.entries()) {
627
- const cloned = (i === parsers.length - 1)
628
- ? response
629
- : response.clone();
630
- try {
631
- const data = await parser.method(cloned);
632
- return data;
633
- }
634
- catch { }
635
- }
636
- throw new VigorParseError("PARSER_ALL_FAILED", {
637
- method: "ParseAutoAlgorithms.sniff",
638
- data: {
639
- tried: parsers.map(parser => parser.title),
640
- response: response
641
- }
642
- });
643
- }
644
- };
645
- contentType() { return this._next({ funcs: [this.ParseAutoAlgorithms.contentType] }); }
646
- sniff() { return this._next({ funcs: [this.ParseAutoAlgorithms.sniff] }); }
647
- json() { return this._next({ funcs: [(res) => res.json()] }); }
648
- text() { return this._next({ funcs: [(res) => res.text()] }); }
649
- arrayBuffer() { return this._next({ funcs: [(res) => res.arrayBuffer()] }); }
650
- blob() { return this._next({ funcs: [(res) => res.blob()] }); }
651
- bytes() { return this._next({ funcs: [(res) => res.arrayBuffer().then(r => new Uint8Array(r))] }); }
652
- formData() { return this._next({ funcs: [(res) => res.formData()] }); }
653
- }
654
- class VigorParseInterceptors extends VigorStatus {
655
- constructor(config) {
656
- const base = {
657
- before: [],
658
- after: [],
659
- result: [],
660
- onError: []
661
- };
662
- super(config, base, (c) => new VigorParseInterceptors(c));
663
- }
664
- before(...funcs) { return this._next({ before: funcs.flat() }); }
665
- after(...funcs) { return this._next({ after: funcs.flat() }); }
666
- result(...funcs) { return this._next({ result: funcs.flat() }); }
667
- onError(...funcs) { return this._next({ onError: funcs.flat() }); }
668
- }
669
- class VigorParse extends VigorStatus {
670
- constructor(config) {
671
- const base = {
672
- target: VigorDefault,
673
- settings: new VigorParseSettings()._getBase(),
674
- strategies: new VigorParseStrategies()._getBase(),
675
- interceptors: new VigorParseInterceptors()._getBase()
676
- };
677
- super(config, base, (c) => new VigorParse(c));
678
- }
679
- _createTimelineHandler(timeline) {
680
- return (action, content) => {
681
- timeline.push({
682
- action: action,
683
- content: content,
684
- time: Date.now()
685
- });
686
- };
687
- }
688
- _createInterceptorHandler(ctx, addTimeline) {
689
- return async (interceptorType, api) => {
690
- const interceptorsConfig = ctx["stats"]["interceptors"];
691
- const interceptors = interceptorsConfig[interceptorType];
692
- addTimeline("INTERCEPTOR_LOOP_STARTED", {
693
- interceptorType: interceptorType,
694
- interceptors,
695
- });
696
- const startTime = performance.now();
697
- for (const func of interceptors) {
698
- const scopedApi = api(interceptorType, func);
699
- await func(ctx, scopedApi);
700
- }
701
- const endTime = performance.now();
702
- addTimeline("INTERCEPTOR_LOOP_ENDED", {
703
- interceptorType: interceptorType,
704
- interceptors,
705
- took: endTime - startTime
706
- });
707
- };
708
- }
709
- target(response) { return this._next({ target: response }); }
710
- settings(func) {
711
- if (func instanceof VigorParseSettings) {
712
- return this._next({ settings: func._getConfig() });
713
- }
714
- if (typeof func === 'function') {
715
- return this._next({ settings: func(new VigorParseSettings(this._config.settings))._getConfig() });
716
- }
717
- return this._next({ settings: func });
718
- }
719
- strategies(func) {
720
- if (func instanceof VigorParseStrategies) {
721
- return this._next({ strategies: func._getConfig() });
722
- }
723
- if (typeof func === 'function') {
724
- return this._next({ strategies: func(new VigorParseStrategies(this._config.strategies))._getConfig() });
725
- }
726
- return this._next({ strategies: func });
727
- }
728
- interceptors(func) {
729
- if (func instanceof VigorParseInterceptors) {
730
- return this._next({ interceptors: func._getConfig() });
731
- }
732
- if (typeof func === 'function') {
733
- return this._next({ interceptors: func(new VigorParseInterceptors(this._config.interceptors))._getConfig() });
734
- }
735
- return this._next({ interceptors: func });
736
- }
737
- async request(config, timeline = []) {
738
- const stats = this._mergeConfig(this._config, config);
739
- const target = stats.target;
740
- let ctx = {
741
- timeline: timeline,
742
- stats,
743
- response: target,
744
- result: VigorDefault,
745
- error: VigorDefault,
746
- flag: {
747
- overwritten: false
748
- }
749
- };
750
- const addTimeline = this._createTimelineHandler(ctx.timeline);
751
- const handleInterceptor = this._createInterceptorHandler(ctx, addTimeline);
752
- addTimeline("PROCESS_HANDLING", {
753
- type: "REQUEST_START",
754
- data: {}
755
- });
756
- try {
757
- if (target === VigorDefault)
758
- throw new VigorParseError("INVALID_TARGET", {
759
- method: "request",
760
- data: {
761
- expected: ["Response"],
762
- received: target
763
- },
764
- context: ctx
765
- });
766
- await handleInterceptor("before", (interceptorType, func) => ({
767
- throwError: (error) => {
768
- addTimeline("INTERCEPTOR_API_CALLED", {
769
- interceptorType,
770
- interceptor: func,
771
- method: "throwError",
772
- args: [error]
773
- });
774
- throw error;
775
- },
776
- }));
777
- if (stats.settings.raw) {
778
- ctx.result = ctx.response;
779
- }
780
- else {
781
- let parsed = false;
782
- for (const [i, func] of stats.strategies.funcs.length > 0
783
- ? stats.strategies.funcs.entries()
784
- : new VigorParseStrategies().contentType()._getConfig().funcs.entries()) {
785
- const cloned = (i === stats.strategies.funcs.length - 1)
786
- ? ctx.response
787
- : ctx.response.clone();
788
- try {
789
- ctx.result = await func(cloned);
790
- parsed = true;
791
- break;
792
- }
793
- catch { }
794
- }
795
- if (!parsed)
796
- throw new VigorParseError("PARSER_ALL_FAILED", {
797
- method: "request",
798
- data: {
799
- tried: stats.strategies.funcs,
800
- response: ctx.response
801
- },
802
- context: ctx
803
- });
804
- }
805
- await handleInterceptor("after", (interceptorType, func) => ({
806
- setResult: (unknown) => {
807
- addTimeline("INTERCEPTOR_API_CALLED", {
808
- interceptorType,
809
- interceptor: func,
810
- method: "setResult",
811
- args: [unknown]
812
- });
813
- ctx.result = unknown;
814
- return unknown;
815
- },
816
- throwError: (error) => {
817
- addTimeline("INTERCEPTOR_API_CALLED", {
818
- interceptorType,
819
- interceptor: func,
820
- method: "throwError",
821
- args: [error]
822
- });
823
- throw error;
824
- },
825
- }));
826
- await handleInterceptor("result", (interceptorType, func) => ({
827
- setResult: (unknown) => {
828
- addTimeline("INTERCEPTOR_API_CALLED", {
829
- interceptorType,
830
- interceptor: func,
831
- method: "setResult",
832
- args: [unknown]
833
- });
834
- ctx.result = unknown;
835
- return unknown;
836
- },
837
- throwError: (error) => {
838
- addTimeline("INTERCEPTOR_API_CALLED", {
839
- interceptorType,
840
- interceptor: func,
841
- method: "throwError",
842
- args: [error]
843
- });
844
- throw error;
845
- },
846
- }));
847
- return ctx.result;
848
- }
849
- catch (error) {
850
- ctx.error = error;
851
- addTimeline("PROCESS_HANDLING", {
852
- type: "REQUEST_ERROR",
853
- data: {
854
- error
855
- }
856
- });
857
- await handleInterceptor("onError", (interceptorType, func) => ({
858
- setResult: (unknown) => {
859
- addTimeline("INTERCEPTOR_API_CALLED", {
860
- interceptorType,
861
- interceptor: func,
862
- method: "setResult",
863
- args: [unknown]
864
- });
865
- ctx.result = unknown;
866
- ctx.flag.overwritten = true;
867
- return unknown;
868
- },
869
- throwError: (error) => {
870
- addTimeline("INTERCEPTOR_API_CALLED", {
871
- interceptorType,
872
- interceptor: func,
873
- method: "throwError",
874
- args: [error]
875
- });
876
- throw error;
877
- },
878
- }));
879
- if (ctx.flag.overwritten)
880
- return ctx.result;
881
- if (stats.settings.default !== VigorDefault)
882
- return stats.settings.default;
883
- throw error;
884
- }
885
- }
886
- }
887
- class VigorFetchSettings extends VigorStatus {
888
- constructor(config) {
889
- const base = {
890
- retryHeaders: ["retry-after", "ratelimit-reset", "x-ratelimit-reset", "x-retry-after", "x-amz-retry-after", "chrome-proxy-next-link"],
891
- unretryStatus: [400, 401, 403, 404, 405, 413, 422],
892
- default: VigorDefault
893
- };
894
- super(config, base, (c) => new VigorFetchSettings(c));
895
- }
896
- retryHeaders(...strs) { return this._next({ retryHeaders: strs.flat() }); }
897
- unretryStatus(...nums) { return this._next({ unretryStatus: nums.flat() }); }
898
- default(unk) { return this._next({ default: unk }); }
899
- }
900
- class VigorFetchInterceptors extends VigorStatus {
901
- constructor(config) {
902
- const base = {
903
- before: [],
904
- after: [],
905
- result: [],
906
- onError: []
907
- };
908
- super(config, base, (c) => new VigorFetchInterceptors(c));
909
- }
910
- before(...funcs) { return this._next({ before: funcs.flat() }); }
911
- after(...funcs) { return this._next({ after: funcs.flat() }); }
912
- result(...funcs) { return this._next({ result: funcs.flat() }); }
913
- onError(...funcs) { return this._next({ onError: funcs.flat() }); }
914
- }
915
- class VigorFetch extends VigorStatus {
916
- constructor(config) {
917
- const base = {
918
- origin: VigorDefault,
919
- path: [],
920
- query: [],
921
- hash: "",
922
- options: {
923
- headers: {},
924
- body: VigorDefault
925
- },
926
- settings: new VigorFetchSettings()._getBase(),
927
- interceptors: new VigorFetchInterceptors()._getBase(),
928
- retryConfig: new VigorRetry()._getBase(),
929
- parseConfig: new VigorParse()._getBase()
930
- };
931
- super(config, base, (c) => new VigorFetch(c));
932
- }
933
- _createTimelineHandler(timeline) {
934
- return (action, content) => {
935
- timeline.push({
936
- action: action,
937
- content: content,
938
- time: Date.now()
939
- });
940
- };
941
- }
942
- _createInterceptorHandler(ctx, addTimeline) {
943
- return async (interceptorType, api) => {
944
- const interceptorsConfig = ctx["stats"]["interceptors"];
945
- const interceptors = interceptorsConfig[interceptorType];
946
- addTimeline("INTERCEPTOR_LOOP_STARTED", {
947
- interceptorType: interceptorType,
948
- interceptors,
949
- });
950
- const startTime = performance.now();
951
- for (const func of interceptors) {
952
- const scopedApi = api(interceptorType, func);
953
- await func(ctx, scopedApi);
954
- }
955
- const endTime = performance.now();
956
- addTimeline("INTERCEPTOR_LOOP_ENDED", {
957
- interceptorType: interceptorType,
958
- interceptors,
959
- took: endTime - startTime
960
- });
961
- };
962
- }
963
- _stringifyList(unkList) {
964
- return unkList
965
- .filter(unk => unk !== null && unk !== undefined)
966
- .map(unk => {
967
- if (unk instanceof Date)
968
- return unk.toISOString();
969
- return String(unk);
970
- });
971
- }
972
- method(str) { return this._next({ method: str }); }
973
- origin(str) { return this._next({ origin: str }); }
974
- path(...strs) { return this._next({ path: this._stringifyList(strs.flat()) }); }
975
- query(...strs) { return this._next({ query: strs.flat() }); }
976
- hash(str) { return this._next({ hash: str }); }
977
- options(obj) { return this._next({ options: obj }); }
978
- headers(obj) { return this._next({ options: { headers: obj } }); }
979
- body(obj) { return this._next({ options: { headers: this._config.options.headers, body: obj } }); }
980
- _buildUrl(origin, path, query, hash) {
981
- const originObj = new URL(origin);
982
- const baseStr = originObj.origin;
983
- const pathObj = [originObj.pathname.replace(/^\/+|\/+$/g, '')];
984
- for (const str of path) {
985
- pathObj.push(str.replace(/^\/+|\/+$/g, ''));
986
- }
987
- const pathStr = pathObj.join('/');
988
- const mainObj = new URL(pathStr, baseStr);
989
- const parseVal = (val) => {
990
- if (val instanceof Date)
991
- return val.toISOString();
992
- return String(val);
993
- };
994
- const queryObj = [...Array.from(originObj.searchParams.entries()), ...query.flatMap(qu => Object.entries(qu))];
995
- for (const [key, val] of queryObj) {
996
- if (val === undefined || val === null)
997
- continue;
998
- if (Array.isArray(val))
999
- for (const e of val) {
1000
- mainObj.searchParams.append(key, parseVal(e));
1001
- }
1002
- else {
1003
- mainObj.searchParams.append(key, parseVal(val));
1004
- }
1005
- }
1006
- mainObj.hash = hash ?? originObj.hash;
1007
- return mainObj.href;
1008
- }
1009
- _normalizeOptions(body) {
1010
- if (body == null)
1011
- return { isJson: false, headers: {}, body };
1012
- if (typeof body === "string")
1013
- return { isJson: false, headers: {
1014
- "Content-Type": "text/plain;charset=UTF-8"
1015
- }, body };
1016
- if (body instanceof Blob)
1017
- return { isJson: false, headers: {
1018
- ...(body.type && { "Content-Type": body.type })
1019
- }, body };
1020
- if (body instanceof ArrayBuffer)
1021
- return { isJson: false, headers: {
1022
- "Content-Type": "application/octet-stream"
1023
- }, body };
1024
- if (body instanceof URLSearchParams)
1025
- return { isJson: false, headers: {
1026
- "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
1027
- }, body };
1028
- if (body instanceof FormData)
1029
- return { isJson: false, headers: {}, body };
1030
- if (typeof body === "object") {
1031
- return { isJson: true, headers: {
1032
- "Content-Type": "application/json"
1033
- }, body: JSON.stringify(body) };
1034
- }
1035
- throw new VigorFetchError("INVALID_BODY", {
1036
- method: "_normalizeBody",
1037
- data: {
1038
- expected: ["string", "Blob", "ArrayBuffer", "URLSearchParams", "FormData"],
1039
- received: body
1040
- }
1041
- });
1042
- }
1043
- settings(func) {
1044
- if (func instanceof VigorFetchSettings) {
1045
- return this._next({ settings: func._getConfig() });
1046
- }
1047
- if (typeof func === 'function') {
1048
- return this._next({ settings: func(new VigorFetchSettings(this._config.settings))._getConfig() });
1049
- }
1050
- return this._next({ settings: func });
1051
- }
1052
- interceptors(func) {
1053
- if (func instanceof VigorFetchInterceptors) {
1054
- return this._next({ interceptors: func._getConfig() });
1055
- }
1056
- if (typeof func === 'function') {
1057
- return this._next({ interceptors: func(new VigorFetchInterceptors(this._config.interceptors))._getConfig() });
1058
- }
1059
- return this._next({ interceptors: func });
1060
- }
1061
- retryConfig(func) {
1062
- if (func instanceof VigorRetry) {
1063
- return this._next({ retryConfig: func._getConfig() });
1064
- }
1065
- if (typeof func === 'function') {
1066
- return this._next({ retryConfig: func(new VigorRetry(this._config.retryConfig))._getConfig() });
1067
- }
1068
- return this._next({ retryConfig: func });
1069
- }
1070
- parseConfig(func) {
1071
- if (func instanceof VigorParse) {
1072
- return this._next({ parseConfig: func._getConfig() });
1073
- }
1074
- if (typeof func === 'function') {
1075
- return this._next({ parseConfig: func(new VigorParse(this._config.parseConfig))._getConfig() });
1076
- }
1077
- return this._next({ parseConfig: func });
1078
- }
1079
- async request(config, timeline = []) {
1080
- const stats = this._mergeConfig(this._config, config);
1081
- let ctx = {
1082
- href: "",
1083
- result: VigorDefault,
1084
- response: VigorDefault,
1085
- options: {
1086
- headers: VigorDefault,
1087
- body: VigorDefault
1088
- },
1089
- error: VigorDefault,
1090
- timeline: timeline,
1091
- stats,
1092
- flag: {
1093
- overwritten: false,
1094
- restarted: false
1095
- }
1096
- };
1097
- const addTimeline = this._createTimelineHandler(ctx.timeline);
1098
- const handleInterceptor = this._createInterceptorHandler(ctx, addTimeline);
1099
- addTimeline("PROCESS_HANDLING", {
1100
- type: "REQUEST_START",
1101
- data: {}
1102
- });
1103
- try {
1104
- try {
1105
- new URL(stats.origin);
1106
- }
1107
- catch {
1108
- throw new VigorFetchError("INVALID_PROTOCOL", {
1109
- method: "request",
1110
- data: {
1111
- expected: ["valid URL protocol"],
1112
- received: stats.origin
1113
- }
1114
- });
1115
- }
1116
- ctx.href = this._buildUrl(stats.origin, stats.path, stats.query, stats.hash);
1117
- addTimeline("BUILT_URL", {
1118
- url: ctx.href
1119
- });
1120
- const { headers, body, ...others } = stats.options;
1121
- const hasBody = body !== VigorDefault &&
1122
- body !== undefined;
1123
- const method = stats.method || (hasBody ? 'POST' : 'GET');
1124
- ctx.options = {
1125
- ...others,
1126
- method: method,
1127
- headers: {}
1128
- };
1129
- if (hasBody) {
1130
- const normalized = this._normalizeOptions(body);
1131
- if (normalized.body !== undefined) {
1132
- ctx.options.body = normalized.body;
1133
- }
1134
- Object.assign(ctx.options.headers, normalized.headers);
1135
- }
1136
- Object.assign(ctx.options.headers, headers);
1137
- addTimeline("SET_OPTIONS", {
1138
- options: ctx.options
1139
- });
1140
- const fetchTask = async (ctx2, { abort, signal }) => {
1141
- ctx.options.signal = signal;
1142
- const result = await fetch(ctx.href, ctx.options);
1143
- return result;
1144
- };
1145
- const throwStatus = async (ctx2, api) => {
1146
- const response = ctx2.result;
1147
- if (!response.ok) {
1148
- api.throwError(new VigorFetchError("FETCH_FAILED", {
1149
- method: "request",
1150
- data: {
1151
- status: response.status,
1152
- response: response,
1153
- url: response.url,
1154
- headers: response.headers,
1155
- body: response.body,
1156
- statusText: response.statusText
1157
- }
1158
- }));
1159
- }
1160
- };
1161
- const handleBlacklist = async (ctx2, api) => {
1162
- const response = ctx2.result;
1163
- ctx.error = ctx2.error;
1164
- if (response instanceof Response) {
1165
- if (stats.settings.unretryStatus.includes(response.status))
1166
- api.cancelRetry();
1167
- else
1168
- api.proceedRetry();
1169
- }
1170
- };
1171
- const handleRatelimit = async (ctx2, api) => {
1172
- const response = ctx2.result;
1173
- ctx.error = ctx2.error;
1174
- if (response instanceof Response) {
1175
- if (response.status === 429) {
1176
- let retryHeader = null;
1177
- for (const header of stats.settings.retryHeaders) {
1178
- retryHeader = response.headers.get(header);
1179
- if (retryHeader)
1180
- break;
1181
- }
1182
- if (retryHeader) {
1183
- const toNumber = Number(retryHeader);
1184
- const delay = !isNaN(toNumber)
1185
- ? toNumber * 1000
1186
- : (() => {
1187
- const toDate = new Date(retryHeader).getTime();
1188
- return !isNaN(toDate)
1189
- ? toDate - Date.now()
1190
- : null;
1191
- })();
1192
- if (delay !== null && delay > 0)
1193
- api.setDelay(delay + Math.random() * ctx2.stats.settings.jitter);
1194
- }
1195
- }
1196
- }
1197
- };
1198
- stats.retryConfig.interceptors.after = [throwStatus, ...stats.retryConfig.interceptors.after];
1199
- stats.retryConfig.interceptors.retryIf = [handleBlacklist, ...stats.retryConfig.interceptors.retryIf];
1200
- stats.retryConfig.interceptors.onRetry = [handleRatelimit, ...stats.retryConfig.interceptors.onRetry];
1201
- const retryEngine = new VigorRetry(stats.retryConfig)
1202
- .target(fetchTask);
1203
- const parseEngine = new VigorParse(stats.parseConfig);
1204
- addTimeline("ENGINE_CREATED", {
1205
- retryEngine,
1206
- parseEngine
1207
- });
1208
- await handleInterceptor("before", (interceptorType, func) => ({
1209
- throwError: (error) => {
1210
- addTimeline("INTERCEPTOR_API_CALLED", {
1211
- interceptorType,
1212
- interceptor: func,
1213
- method: "throwError",
1214
- args: [error]
1215
- });
1216
- throw error;
1217
- },
1218
- setOptions: (unknown) => {
1219
- addTimeline("INTERCEPTOR_API_CALLED", {
1220
- interceptorType,
1221
- interceptor: func,
1222
- method: "setOptions",
1223
- args: [unknown]
1224
- });
1225
- return ctx.options = unknown;
1226
- },
1227
- setHeaders: (unknown) => {
1228
- addTimeline("INTERCEPTOR_API_CALLED", {
1229
- interceptorType,
1230
- interceptor: func,
1231
- method: "setHeaders",
1232
- args: [unknown]
1233
- });
1234
- return ctx.options.headers = unknown;
1235
- },
1236
- setBody: (unknown) => {
1237
- addTimeline("INTERCEPTOR_API_CALLED", {
1238
- interceptorType,
1239
- interceptor: func,
1240
- method: "setBody",
1241
- args: [unknown]
1242
- });
1243
- return ctx.options.body = unknown;
1244
- }
1245
- }));
1246
- addTimeline("RETRY_STARTED", {
1247
- engine: retryEngine
1248
- });
1249
- const retryStart = performance.now();
1250
- const retryTimeline = [];
1251
- ctx.response = await retryEngine.request(undefined, retryTimeline);
1252
- const retryEnd = performance.now();
1253
- addTimeline("RETRY_ENDED", {
1254
- engine: retryEngine,
1255
- timeline: retryTimeline,
1256
- took: retryEnd - retryStart,
1257
- response: ctx.response
1258
- });
1259
- addTimeline("PARSE_STARTED", {
1260
- engine: parseEngine
1261
- });
1262
- const parseStart = performance.now();
1263
- const parseTimeline = [];
1264
- ctx.result = await parseEngine.target(ctx.response).request(undefined, parseTimeline);
1265
- const parseEnd = performance.now();
1266
- addTimeline("PARSE_ENDED", {
1267
- engine: parseEngine,
1268
- timeline: parseTimeline,
1269
- took: parseEnd - parseStart,
1270
- result: ctx.result
1271
- });
1272
- await handleInterceptor("after", (interceptorType, func) => ({
1273
- setResult: (unknown) => {
1274
- addTimeline("INTERCEPTOR_API_CALLED", {
1275
- interceptorType,
1276
- interceptor: func,
1277
- method: "setResult",
1278
- args: [unknown]
1279
- });
1280
- ctx.result = unknown;
1281
- return unknown;
1282
- },
1283
- throwError: (error) => {
1284
- addTimeline("INTERCEPTOR_API_CALLED", {
1285
- interceptorType,
1286
- interceptor: func,
1287
- method: "throwError",
1288
- args: [error]
1289
- });
1290
- throw error;
1291
- },
1292
- }));
1293
- await handleInterceptor("result", (interceptorType, func) => ({
1294
- setResult: (unknown) => {
1295
- addTimeline("INTERCEPTOR_API_CALLED", {
1296
- interceptorType,
1297
- interceptor: func,
1298
- method: "setResult",
1299
- args: [unknown]
1300
- });
1301
- ctx.result = unknown;
1302
- return unknown;
1303
- },
1304
- throwError: (error) => {
1305
- addTimeline("INTERCEPTOR_API_CALLED", {
1306
- interceptorType,
1307
- interceptor: func,
1308
- method: "throwError",
1309
- args: [error]
1310
- });
1311
- throw error;
1312
- },
1313
- }));
1314
- return ctx.result;
1315
- }
1316
- catch (error) {
1317
- ctx.error = error;
1318
- addTimeline("PROCESS_HANDLING", {
1319
- type: "REQUEST_ERROR",
1320
- data: {
1321
- error
1322
- }
1323
- });
1324
- await handleInterceptor("onError", (interceptorType, func) => ({
1325
- setResult: (unknown) => {
1326
- addTimeline("INTERCEPTOR_API_CALLED", {
1327
- interceptorType,
1328
- interceptor: func,
1329
- method: "setResult",
1330
- args: [unknown]
1331
- });
1332
- ctx.result = unknown;
1333
- ctx.flag.overwritten = true;
1334
- return unknown;
1335
- },
1336
- throwError: (error) => {
1337
- addTimeline("INTERCEPTOR_API_CALLED", {
1338
- interceptorType,
1339
- interceptor: func,
1340
- method: "throwError",
1341
- args: [error]
1342
- });
1343
- throw error;
1344
- },
1345
- restart: () => {
1346
- addTimeline("INTERCEPTOR_API_CALLED", {
1347
- interceptorType,
1348
- interceptor: func,
1349
- method: "restart",
1350
- args: []
1351
- });
1352
- ctx.flag.restarted = true;
1353
- }
1354
- }));
1355
- if (ctx.flag.restarted) {
1356
- return await this.request(stats, ctx.timeline);
1357
- }
1358
- if (ctx.flag.overwritten)
1359
- return ctx.result;
1360
- if (stats.settings.default !== VigorDefault)
1361
- return stats.settings.default;
1362
- throw error;
1363
- }
1364
- }
1365
- }
1366
- class VigorAllSettings extends VigorStatus {
1367
- constructor(config) {
1368
- const base = {
1369
- concurrency: 5,
1370
- onlySuccess: false
1371
- };
1372
- super(config, base, (c) => new VigorAllSettings(c));
1373
- }
1374
- concurrency(num) { return this._next({ concurrency: num }); }
1375
- onlySuccess(num) { return this._next({ onlySuccess: num }); }
1376
- }
1377
- class VigorAllInterceptors extends VigorStatus {
1378
- constructor(config) {
1379
- const base = {
1380
- before: [],
1381
- after: [],
1382
- result: [],
1383
- onError: []
1384
- };
1385
- super(config, base, (c) => new VigorAllInterceptors(c));
1386
- }
1387
- before(...funcs) { return this._next({ before: funcs.flat() }); }
1388
- after(...funcs) { return this._next({ after: funcs.flat() }); }
1389
- result(...funcs) { return this._next({ result: funcs.flat() }); }
1390
- onError(...funcs) { return this._next({ onError: funcs.flat() }); }
1391
- }
1392
- class VigorAll extends VigorStatus {
1393
- constructor(config) {
1394
- const base = {
1395
- target: [],
1396
- settings: new VigorAllSettings()._getBase(),
1397
- interceptors: new VigorAllInterceptors()._getBase()
1398
- };
1399
- super(config, base, (c) => new VigorAll(c));
1400
- }
1401
- _createTimelineHandler(timeline) {
1402
- return (action, content) => {
1403
- timeline.push({
1404
- action: action,
1405
- content: content,
1406
- time: Date.now()
1407
- });
1408
- };
1409
- }
1410
- _createInterceptorHandler(ctx, addTimeline) {
1411
- return async (interceptorType, api) => {
1412
- const interceptorsConfig = ctx["stats"]["interceptors"];
1413
- const interceptors = interceptorsConfig[interceptorType];
1414
- addTimeline("INTERCEPTOR_LOOP_STARTED", {
1415
- interceptorType: interceptorType,
1416
- interceptors,
1417
- });
1418
- const startTime = performance.now();
1419
- for (const func of interceptors) {
1420
- const scopedApi = api(interceptorType, func);
1421
- await func(ctx, scopedApi);
1422
- }
1423
- const endTime = performance.now();
1424
- addTimeline("INTERCEPTOR_LOOP_ENDED", {
1425
- interceptorType: interceptorType,
1426
- interceptors,
1427
- took: endTime - startTime
1428
- });
1429
- };
1430
- }
1431
- _createEachTimelineHandler(timeline) {
1432
- return (action, content) => {
1433
- timeline.push({
1434
- action: action,
1435
- content: content,
1436
- time: Date.now()
1437
- });
1438
- };
1439
- }
1440
- _createEachInterceptorHandler(ctx, addEachTimeline) {
1441
- return async (interceptorType, api) => {
1442
- const interceptorsConfig = ctx["stats"]["interceptors"];
1443
- const interceptors = interceptorsConfig[interceptorType];
1444
- addEachTimeline("INTERCEPTOR_LOOP_STARTED", {
1445
- interceptorType: interceptorType,
1446
- interceptors,
1447
- });
1448
- const startTime = performance.now();
1449
- for (const func of interceptors) {
1450
- const scopedApi = api(interceptorType, func);
1451
- await func(ctx, scopedApi);
1452
- }
1453
- const endTime = performance.now();
1454
- addEachTimeline("INTERCEPTOR_LOOP_ENDED", {
1455
- interceptorType: interceptorType,
1456
- interceptors,
1457
- took: endTime - startTime
1458
- });
1459
- };
1460
- }
1461
- target(...funcs) { return this._next({ target: funcs.flat() }); }
1462
- settings(func) {
1463
- if (func instanceof VigorAllSettings) {
1464
- return this._next({ settings: func._getConfig() });
1465
- }
1466
- if (typeof func === 'function') {
1467
- return this._next({ settings: func(new VigorAllSettings(this._config.settings))._getConfig() });
1468
- }
1469
- return this._next({ settings: func });
1470
- }
1471
- interceptors(func) {
1472
- if (func instanceof VigorAllInterceptors) {
1473
- return this._next({ interceptors: func._getConfig() });
1474
- }
1475
- if (typeof func === 'function') {
1476
- return this._next({ interceptors: func(new VigorAllInterceptors(this._config.interceptors))._getConfig() });
1477
- }
1478
- return this._next({ interceptors: func });
1479
- }
1480
- async runTask(task, { stats, root }, semaphore) {
1481
- let ctx = {
1482
- result: VigorDefault,
1483
- error: VigorDefault,
1484
- timeline: [],
1485
- stats,
1486
- root,
1487
- target: task,
1488
- semaphore,
1489
- flag: {
1490
- overwritten: false
1491
- }
1492
- };
1493
- const addEachTimeline = this._createEachTimelineHandler(ctx.timeline);
1494
- const handleEachInterceptor = this._createEachInterceptorHandler(ctx, addEachTimeline);
1495
- addEachTimeline("PROCESS_HANDLING", {
1496
- type: "TASK_START",
1497
- data: {}
1498
- });
1499
- try {
1500
- try {
1501
- await semaphore.acquire();
1502
- addEachTimeline("TASK_ACQUIRED", {
1503
- target: ctx.target
1504
- });
1505
- await handleEachInterceptor("before", (interceptorType, func) => ({
1506
- throwError: (error) => {
1507
- addEachTimeline("INTERCEPTOR_API_CALLED", {
1508
- interceptorType,
1509
- interceptor: func,
1510
- method: "throwError",
1511
- args: [error]
1512
- });
1513
- throw error;
1514
- }
1515
- }));
1516
- addEachTimeline("TASK_STARTED", {
1517
- target: ctx.target
1518
- });
1519
- const startTime = performance.now();
1520
- ctx.result = await ctx.target(ctx);
1521
- const endTime = performance.now();
1522
- addEachTimeline("TASK_ENDED", {
1523
- target: ctx.target,
1524
- took: endTime - startTime
1525
- });
1526
- await handleEachInterceptor("after", (interceptorType, func) => ({
1527
- setResult: (unknown) => {
1528
- addEachTimeline("INTERCEPTOR_API_CALLED", {
1529
- interceptorType,
1530
- interceptor: func,
1531
- method: "setResult",
1532
- args: [unknown]
1533
- });
1534
- ctx.result = unknown;
1535
- return unknown;
1536
- },
1537
- throwError: (error) => {
1538
- addEachTimeline("INTERCEPTOR_API_CALLED", {
1539
- interceptorType,
1540
- interceptor: func,
1541
- method: "throwError",
1542
- args: [error]
1543
- });
1544
- throw error;
1545
- }
1546
- }));
1547
- }
1548
- finally {
1549
- semaphore.release();
1550
- addEachTimeline("TASK_RELEASED", {
1551
- target: ctx.target
1552
- });
1553
- }
1554
- }
1555
- catch (error) {
1556
- ctx.error = error;
1557
- addEachTimeline("PROCESS_HANDLING", {
1558
- type: "TASK_ERROR",
1559
- data: {
1560
- error
1561
- }
1562
- });
1563
- await handleEachInterceptor("onError", (interceptorType, func) => ({
1564
- setResult: (unknown) => {
1565
- addEachTimeline("INTERCEPTOR_API_CALLED", {
1566
- interceptorType,
1567
- interceptor: func,
1568
- method: "setResult",
1569
- args: [unknown]
1570
- });
1571
- ctx.result = unknown;
1572
- ctx.flag.overwritten = true;
1573
- return unknown;
1574
- },
1575
- throwError: (error) => {
1576
- addEachTimeline("INTERCEPTOR_API_CALLED", {
1577
- interceptorType,
1578
- interceptor: func,
1579
- method: "throwError",
1580
- args: [error]
1581
- });
1582
- throw error;
1583
- },
1584
- }));
1585
- if (ctx.flag.overwritten)
1586
- return ctx.result;
1587
- throw error;
1588
- }
1589
- return ctx.result;
1590
- }
1591
- async request(config, timeline = []) {
1592
- const stats = this._mergeConfig(this._config, config);
1593
- let ctx = {
1594
- result: VigorDefault,
1595
- timeline,
1596
- stats,
1597
- queue: new Set(),
1598
- active: 0
1599
- };
1600
- const addTimeline = this._createTimelineHandler(ctx.timeline);
1601
- const handleInterceptor = this._createInterceptorHandler(ctx, addTimeline);
1602
- addTimeline("PROCESS_HANDLING", {
1603
- type: "REQUEST_START",
1604
- data: {}
1605
- });
1606
- if (stats.target.length === 0)
1607
- throw new VigorAllError("EMPTY_TARGET", {
1608
- method: "request",
1609
- data: {}
1610
- });
1611
- const waitQueue = [];
1612
- const acquire = () => {
1613
- if (ctx.active < stats.settings.concurrency) {
1614
- ctx.active++;
1615
- return Promise.resolve();
1616
- }
1617
- return new Promise((res) => waitQueue.push(() => { ctx.active++; res(); }));
1618
- };
1619
- const release = () => {
1620
- ctx.active--;
1621
- if (waitQueue.length > 0) {
1622
- const next = waitQueue.shift();
1623
- if (next)
1624
- next();
1625
- }
1626
- };
1627
- for (const task of stats.target) {
1628
- let promise;
1629
- promise = this.runTask(task, { stats, root: ctx }, { acquire, release })
1630
- .then(res => ({ success: true, value: res }))
1631
- .catch(err => ({ success: false, value: err }))
1632
- .finally(() => ctx.queue.delete(promise));
1633
- ctx.queue.add(promise);
1634
- }
1635
- addTimeline("QUEUE_REQUEST_STARTED", {
1636
- queue: ctx.queue
1637
- });
1638
- const startTime = performance.now();
1639
- const raw = await Promise.all(ctx.queue);
1640
- const endTime = performance.now();
1641
- addTimeline("QUEUE_REQUEST_ENDED", {
1642
- queue: ctx.queue,
1643
- took: endTime - startTime
1644
- });
1645
- ctx.result = stats.settings.onlySuccess
1646
- ? raw.filter(r => r.success).map(r => r.value)
1647
- : raw.map(r => r.value);
1648
- await handleInterceptor("result", (interceptorType, func) => ({
1649
- setResult: (unknown) => {
1650
- addTimeline("INTERCEPTOR_API_CALLED", {
1651
- interceptorType,
1652
- interceptor: func,
1653
- method: "setResult",
1654
- args: [unknown]
1655
- });
1656
- ctx.result = unknown;
1657
- return unknown;
1658
- },
1659
- throwError: (error) => {
1660
- addTimeline("INTERCEPTOR_API_CALLED", {
1661
- interceptorType,
1662
- interceptor: func,
1663
- method: "throwError",
1664
- args: [error]
1665
- });
1666
- throw error;
1667
- },
1668
- }));
1669
- return ctx.result;
1670
- }
1671
- }
1672
- const VigorEntry = {
1673
- retry: {
1674
- main: VigorRetry,
1675
- settings: VigorRetrySettings,
1676
- interceptors: VigorRetryInterceptors,
1677
- error: VigorRetryError,
1678
- algorithms: {
1679
- constant: VigorRetryAlgorithmsConstant,
1680
- linear: VigorRetryAlgorithmsLinear,
1681
- backoff: VigorRetryAlgorithmsBackoff,
1682
- custom: VigorRetryAlgorithmsCustom
1683
- }
1684
- },
1685
- parse: {
1686
- main: VigorParse,
1687
- settings: VigorParseSettings,
1688
- interceptors: VigorParseInterceptors,
1689
- error: VigorParseError,
1690
- strategies: VigorParseStrategies
1691
- },
1692
- fetch: {
1693
- main: VigorFetch,
1694
- settings: VigorFetchSettings,
1695
- interceptors: VigorFetchInterceptors,
1696
- error: VigorFetchError,
1697
- },
1698
- all: {
1699
- main: VigorAll,
1700
- settings: VigorAllSettings,
1701
- interceptors: VigorAllInterceptors,
1702
- error: VigorAllError
1703
- }
1704
- };
1705
- const vigor = {
1706
- use: async (func, config) => {
1707
- return await func(VigorEntry, config);
1708
- },
1709
- fetch: (str) => {
1710
- return new VigorFetch().origin(str);
1711
- },
1712
- retry: (target) => {
1713
- return new VigorRetry().target(target);
1714
- },
1715
- parse: (response) => {
1716
- return new VigorParse().target(response);
1717
- },
1718
- all: (...funcs) => {
1719
- return new VigorAll().target(...funcs);
1720
- },
1721
- builder: {
1722
- fetch: {
1723
- settings: (c) => new VigorFetchSettings(c),
1724
- interceptors: (c) => new VigorFetchInterceptors(c),
1725
- },
1726
- retry: {
1727
- settings: (c) => new VigorRetrySettings(c),
1728
- interceptors: (c) => new VigorRetryInterceptors(c),
1729
- },
1730
- parse: {
1731
- settings: (c) => new VigorParseSettings(c),
1732
- interceptors: (c) => new VigorParseInterceptors(c),
1733
- },
1734
- all: {
1735
- settings: (c) => new VigorAllSettings(c),
1736
- interceptors: (c) => new VigorAllInterceptors(c),
1737
- }
1738
- }
1739
- };
3
+ var vigorFetch = require('vigor-fetch');
1740
4
 
1741
- // ----------------------------------------------------------------
1742
- // Error system
1743
- // ----------------------------------------------------------------
1744
5
  const RobloxErrorMessageFuncs = {
1745
6
  AUTH_FAILED: ({ status, cookie }) => `Cookie authentication failed (status: ${status ?? 'unknown'}, cookie: ${cookie.slice(0, 8)}...)`,
1746
7
  RATE_LIMITED: ({ status, url, retryAfterMs }) => `Rate limited (status: ${status}, url: ${url ?? 'unknown'}, retryAfter: ${retryAfterMs ?? 'unknown'}ms)`,
@@ -1751,7 +12,6 @@ class RobloxApiError extends Error {
1751
12
  cause;
1752
13
  code;
1753
14
  data;
1754
- timeline;
1755
15
  context;
1756
16
  constructor(code, options) {
1757
17
  const messageFn = RobloxErrorMessageFuncs[code];
@@ -1760,7 +20,6 @@ class RobloxApiError extends Error {
1760
20
  this.code = code;
1761
21
  this.cause = options.cause;
1762
22
  this.data = options.data;
1763
- this.timeline = options.timeline ?? [];
1764
23
  this.context = options.context;
1765
24
  Object.setPrototypeOf(this, new.target.prototype);
1766
25
  Error.captureStackTrace?.(this, new.target);
@@ -1782,14 +41,7 @@ class RobloxRequestError extends RobloxApiError {
1782
41
  }
1783
42
  }
1784
43
  function isFetchFailed(cause) {
1785
- return cause instanceof VigorFetchError && cause.code === 'FETCH_FAILED' && cause.data != null;
1786
- }
1787
- function extractTimeline(cause) {
1788
- if (cause instanceof VigorFetchError)
1789
- return (cause.context?.timeline ?? []);
1790
- if (cause instanceof VigorRetryError)
1791
- return (cause.context?.timeline ?? []);
1792
- return [];
44
+ return cause instanceof vigorFetch.VigorFetchError && cause.code === 'FETCH_FAILED' && cause.data != null;
1793
45
  }
1794
46
  function extractStatus(cause) {
1795
47
  return isFetchFailed(cause) ? cause.data.status : null;
@@ -1816,14 +68,12 @@ function extractRetryAfterMs(cause) {
1816
68
  function wrapVigorError(cause) {
1817
69
  const status = extractStatus(cause);
1818
70
  const url = extractUrl(cause);
1819
- const timeline = extractTimeline(cause);
1820
71
  if (status === 429)
1821
72
  return new RobloxRateLimitError({
1822
73
  data: { status, url, retryAfterMs: extractRetryAfterMs(cause) },
1823
- timeline,
1824
74
  cause,
1825
75
  });
1826
- return new RobloxRequestError({ data: { status, url }, timeline, cause });
76
+ return new RobloxRequestError({ data: { status, url }, cause });
1827
77
  }
1828
78
  function chunk(arr, size) {
1829
79
  const out = [];
@@ -1837,49 +87,24 @@ function partition(arr, pred) {
1837
87
  (pred(item) ? pass : fail).push(item);
1838
88
  return { pass, fail };
1839
89
  }
1840
- // ----------------------------------------------------------------
1841
- // CSRF Token Manager
1842
- // ----------------------------------------------------------------
1843
90
  class CsrfTokenManager {
1844
- // cookie → token 매핑 (쿠키별로 독립 토큰 관리)
1845
91
  tokenMap = new Map();
1846
- // 쿠키별 진행 중인 갱신 Promise (중복 갱신 방지)
1847
92
  pendingMap = new Map();
1848
- /**
1849
- * 저장된 토큰을 반환합니다. 없으면 null.
1850
- */
1851
93
  get(cookie) {
1852
94
  return this.tokenMap.get(cookie) ?? null;
1853
95
  }
1854
- /**
1855
- * 토큰을 수동으로 저장합니다.
1856
- * (403 응답 헤더의 x-csrf-token 값을 받아서 저장할 때 사용)
1857
- */
1858
96
  set(cookie, token) {
1859
97
  this.tokenMap.set(cookie, token);
1860
98
  }
1861
- /**
1862
- * 토큰을 무효화합니다.
1863
- * (재시도 전 갱신이 필요할 때 호출)
1864
- */
1865
99
  invalidate(cookie) {
1866
100
  this.tokenMap.delete(cookie);
1867
101
  }
1868
- /**
1869
- * Roblox의 CSRF 토큰 갱신 방식:
1870
- * POST /v1/logout 등 아무 인증 엔드포인트에 빈 body로 요청하면
1871
- * 403 응답과 함께 x-csrf-token 헤더로 새 토큰을 내려줍니다.
1872
- *
1873
- * 동일 쿠키에 대해 동시에 여러 갱신 요청이 오면
1874
- * 하나만 실제로 요청하고 나머지는 그 결과를 공유합니다.
1875
- */
1876
102
  async refresh(cookie) {
1877
103
  const existing = this.pendingMap.get(cookie);
1878
104
  if (existing)
1879
105
  return existing;
1880
106
  const pending = (async () => {
1881
107
  try {
1882
- // Roblox는 POST에 body가 없어도 403 + x-csrf-token을 내려줌
1883
108
  const response = await fetch('https://auth.roblox.com/v2/logout', {
1884
109
  method: 'POST',
1885
110
  headers: {
@@ -1901,9 +126,6 @@ class CsrfTokenManager {
1901
126
  this.pendingMap.set(cookie, pending);
1902
127
  return pending;
1903
128
  }
1904
- /**
1905
- * 저장된 토큰을 반환하되, 없으면 자동으로 갱신 후 반환합니다.
1906
- */
1907
129
  async getOrRefresh(cookie) {
1908
130
  const cached = this.get(cookie);
1909
131
  if (cached)
@@ -1911,9 +133,6 @@ class CsrfTokenManager {
1911
133
  return this.refresh(cookie);
1912
134
  }
1913
135
  }
1914
- // ----------------------------------------------------------------
1915
- // Factory
1916
- // ----------------------------------------------------------------
1917
136
  function createRobloxApi({ cache, cookies: cookiesList, ipgeolocationKey, }) {
1918
137
  const cookiePool = cookiesList.map(cookie => ({ cookie, lastUsed: 0 }));
1919
138
  const csrfManager = new CsrfTokenManager();
@@ -1922,116 +141,93 @@ function createRobloxApi({ cache, cookies: cookiesList, ipgeolocationKey, }) {
1922
141
  entry.lastUsed = Date.now();
1923
142
  return entry.cookie;
1924
143
  }
144
+ function makeHeaderMiddlewares(opts) {
145
+ const { getCookie, winInet = false, csrf = false } = opts;
146
+ let builder = vigorFetch.vigor.builders.fetch.middlewares()
147
+ .before("intercept", async (ctx, api) => {
148
+ const cookie = getCookie();
149
+ ctx.record.cookie = cookie;
150
+ const headers = {
151
+ Cookie: `.ROBLOSECURITY=${cookie}`,
152
+ };
153
+ if (winInet)
154
+ headers['User-Agent'] = 'Roblox/WinInet';
155
+ if (csrf)
156
+ headers['X-CSRF-Token'] = await csrfManager.getOrRefresh(cookie);
157
+ api.setHeaders(headers);
158
+ return ctx;
159
+ });
160
+ if (csrf) {
161
+ builder = builder.onError("intercept", async (ctx, api) => {
162
+ const cause = ctx.error;
163
+ if (isFetchFailed(cause) && cause.data.status === 403) {
164
+ const cookie = ctx.record.cookie ?? getCookie();
165
+ const newToken = cause.data.response.headers.get('x-csrf-token');
166
+ if (newToken) {
167
+ csrfManager.set(cookie, newToken);
168
+ }
169
+ else {
170
+ csrfManager.invalidate(cookie);
171
+ await csrfManager.refresh(cookie);
172
+ }
173
+ api.proceedRestart();
174
+ }
175
+ return ctx;
176
+ });
177
+ }
178
+ return builder;
179
+ }
180
+ /** Reusable `after` middleware that unwraps a `{ data: T }`-shaped response envelope. */
1925
181
  function pickKey(key) {
1926
- return vigor.builder.fetch.interceptors()
1927
- .result((ctx, api) => {
182
+ return vigorFetch.vigor.builders.fetch.middlewares()
183
+ .after("intercept", async (ctx, api) => {
1928
184
  api.setResult(ctx.result[key]);
185
+ return ctx;
1929
186
  });
1930
187
  }
1931
188
  const dataInterceptor = pickKey('data');
1932
- // ----------------------------------------------------------------
1933
- // CSRF 인터셉터 팩토리
1934
- // ----------------------------------------------------------------
1935
- // cookie를 인자로 받아 해당 쿠키 전용 CSRF 인터셉터를 생성합니다.
1936
- // 동작 방식:
1937
- // before → 캐시된 토큰이 있으면 X-CSRF-Token 헤더에 주입
1938
- // onError 403 응답이고 헤더에 x-csrf-token이 있으면
1939
- // 토큰을 갱신하고 요청을 재시작(restart)
1940
- function makeCsrfInterceptor(getCookie) {
1941
- return vigor.builder.fetch.interceptors()
1942
- .before(async (ctx, api) => {
1943
- const cookie = getCookie();
1944
- const token = await csrfManager.getOrRefresh(cookie);
1945
- api.setHeaders({
1946
- ...ctx.options.headers,
1947
- 'X-CSRF-Token': token,
1948
- });
1949
- })
1950
- .onError(async (ctx, api) => {
1951
- // VigorFetchError + FETCH_FAILED + status 403 여부 확인
1952
- const cause = ctx.error;
1953
- if (cause instanceof VigorFetchError &&
1954
- cause.code === 'FETCH_FAILED' &&
1955
- cause.data?.status === 403) {
1956
- const response = cause.data.response;
1957
- const newToken = response.headers.get('x-csrf-token');
1958
- const cookie = getCookie();
1959
- if (newToken) {
1960
- // 토큰 저장 후 재시도
1961
- csrfManager.set(cookie, newToken);
1962
- api.restart?.();
1963
- }
1964
- else {
1965
- // 헤더에 토큰이 없으면 기존 토큰 무효화 후 강제 갱신 후 재시도
1966
- csrfManager.invalidate(cookie);
1967
- await csrfManager.refresh(cookie);
1968
- api.restart?.();
1969
- }
1970
- }
1971
- });
1972
- }
1973
- const cookieInterceptor = vigor.builder.fetch.interceptors()
1974
- .before((ctx, api) => {
1975
- api.setHeaders({
1976
- ...ctx.options.headers,
1977
- Cookie: `.ROBLOSECURITY=${pickCookie()}`,
1978
- });
1979
- });
1980
- const winInetInterceptor = vigor.builder.fetch.interceptors()
1981
- .before((ctx, api) => {
1982
- api.setHeaders({
1983
- ...ctx.options.headers,
1984
- 'User-Agent': 'Roblox/WinInet',
1985
- });
1986
- });
1987
- // CSRF가 필요한 POST 엔드포인트에 붙일 공용 인터셉터
1988
- // pickCookie()로 현재 선택된 쿠키를 기준으로 토큰 관리
1989
- const csrfInterceptor = makeCsrfInterceptor(pickCookie);
1990
- const usersApi = vigor.fetch('https://users.roblox.com/v1')
1991
- .interceptors(cookieInterceptor)
1992
- .interceptors(winInetInterceptor)
1993
- .retryConfig(c => c
1994
- .settings(s => s.attempt(7))
1995
- .algorithms(a => a.backoff().initial(200).unit(800).multiplier(1.7)));
1996
- const thumbnailsApi = vigor.fetch('https://thumbnails.roblox.com/v1')
1997
- .interceptors(cookieInterceptor)
1998
- .interceptors(winInetInterceptor)
1999
- .retryConfig(c => c
2000
- .settings(s => s.attempt(5))
2001
- .algorithms(a => a.backoff().initial(1000).multiplier(2.5)));
2002
- const gamesApi = vigor.fetch('https://games.roblox.com/v1')
2003
- .interceptors(cookieInterceptor)
2004
- .retryConfig(c => c
2005
- .settings(s => s.attempt(5))
2006
- .algorithms(a => a.backoff().initial(1000).multiplier(2.5)));
2007
- const presenceApi = vigor.fetch('https://presence.roblox.com/v1')
2008
- .interceptors(cookieInterceptor)
2009
- .retryConfig(c => c
2010
- .settings(s => s.attempt(5))
2011
- .algorithms(a => a.backoff().initial(500).multiplier(2)));
2012
- const apisRoblox = vigor.fetch('https://apis.roblox.com')
2013
- .interceptors(cookieInterceptor)
2014
- .retryConfig(c => c
2015
- .settings(s => s.attempt(5))
2016
- .algorithms(a => a.backoff().initial(1000).multiplier(2)));
2017
- const gamejoinApi = vigor.fetch('https://gamejoin.roblox.com/v1')
2018
- .interceptors(cookieInterceptor)
2019
- .retryConfig(c => c
2020
- .settings(s => s.attempt(7))
2021
- .algorithms(a => a.backoff().initial(500).multiplier(1.5)));
2022
- const ipgeolocationApi = vigor.fetch('https://api.ipgeolocation.io')
2023
- .retryConfig(c => c
2024
- .settings(s => s.attempt(4))
2025
- .algorithms(a => a.backoff().initial(500).multiplier(2)));
2026
- // friends.roblox.com — CSRF 인터셉터 포함
2027
- // sendFriendRequest / unfriend 는 POST라 CSRF 토큰 필요
2028
- const friendsApi = vigor.fetch('https://friends.roblox.com/v1')
2029
- .interceptors(cookieInterceptor)
2030
- .interceptors(winInetInterceptor)
2031
- .interceptors(csrfInterceptor) // ← CSRF 추가
2032
- .retryConfig(c => c
2033
- .settings(s => s.attempt(5))
2034
- .algorithms(a => a.backoff().initial(500).multiplier(2)));
189
+ const poolCookieMiddlewares = makeHeaderMiddlewares({ getCookie: pickCookie });
190
+ const poolCookieWinInetMiddlewares = makeHeaderMiddlewares({ getCookie: pickCookie, winInet: true });
191
+ const poolCookieCsrfMiddlewares = makeHeaderMiddlewares({ getCookie: pickCookie, winInet: true, csrf: true });
192
+ const usersApi = vigorFetch.vigor.fetch('https://users.roblox.com/v1')
193
+ .middlewares(poolCookieWinInetMiddlewares)
194
+ .retry(r => r
195
+ .settings(s => s.maxAttempts(7))
196
+ .algorithms(a => a.backoff({ initial: 200, unit: 800, multiplier: 1.7 })));
197
+ const thumbnailsApi = vigorFetch.vigor.fetch('https://thumbnails.roblox.com/v1')
198
+ .middlewares(poolCookieWinInetMiddlewares)
199
+ .retry(r => r
200
+ .settings(s => s.maxAttempts(5))
201
+ .algorithms(a => a.backoff({ initial: 1000, multiplier: 2.5 })));
202
+ const gamesApi = vigorFetch.vigor.fetch('https://games.roblox.com/v1')
203
+ .middlewares(poolCookieMiddlewares)
204
+ .retry(r => r
205
+ .settings(s => s.maxAttempts(5))
206
+ .algorithms(a => a.backoff({ initial: 1000, multiplier: 2.5 })));
207
+ const presenceApi = vigorFetch.vigor.fetch('https://presence.roblox.com/v1')
208
+ .middlewares(poolCookieMiddlewares)
209
+ .retry(r => r
210
+ .settings(s => s.maxAttempts(5))
211
+ .algorithms(a => a.backoff({ initial: 500, multiplier: 2 })));
212
+ const apisRoblox = vigorFetch.vigor.fetch('https://apis.roblox.com')
213
+ .middlewares(poolCookieMiddlewares)
214
+ .retry(r => r
215
+ .settings(s => s.maxAttempts(5))
216
+ .algorithms(a => a.backoff({ initial: 1000, multiplier: 2 })));
217
+ const gamejoinApi = vigorFetch.vigor.fetch('https://gamejoin.roblox.com/v1')
218
+ .middlewares(poolCookieMiddlewares)
219
+ .retry(r => r
220
+ .settings(s => s.maxAttempts(7))
221
+ .algorithms(a => a.backoff({ initial: 500, multiplier: 1.5 })));
222
+ const ipgeolocationApi = vigorFetch.vigor.fetch('https://api.ipgeolocation.io')
223
+ .retry(r => r
224
+ .settings(s => s.maxAttempts(4))
225
+ .algorithms(a => a.backoff({ initial: 500, multiplier: 2 })));
226
+ const friendsApi = vigorFetch.vigor.fetch('https://friends.roblox.com/v1')
227
+ .middlewares(poolCookieCsrfMiddlewares)
228
+ .retry(r => r
229
+ .settings(s => s.maxAttempts(5))
230
+ .algorithms(a => a.backoff({ initial: 500, multiplier: 2 })));
2035
231
  async function withCache(opts) {
2036
232
  const { type, keys, ttlMs, getKey, fetchMissing, fallback } = opts;
2037
233
  const cached = await cache.select(type, keys);
@@ -2045,15 +241,8 @@ function createRobloxApi({ cache, cookies: cookiesList, ipgeolocationKey, }) {
2045
241
  return keys.map(k => cacheMap.get(k) ?? fallback);
2046
242
  }
2047
243
  async function authenticated(cookies) {
2048
- return vigor.all(...cookies.map(cookie => async () => {
2049
- const fixedCookieInterceptor = vigor.builder.fetch.interceptors()
2050
- .before((ctx, api) => {
2051
- api.setHeaders({
2052
- ...ctx.options.headers,
2053
- Cookie: `.ROBLOSECURITY=${cookie}`,
2054
- });
2055
- });
2056
- const base = usersApi.interceptors(fixedCookieInterceptor);
244
+ const results = await vigorFetch.vigor.all(...cookies.map(cookie => async () => {
245
+ const base = usersApi.middlewares(makeHeaderMiddlewares({ getCookie: () => cookie, winInet: true }));
2057
246
  const [user, description, birthdate, gender, ageBracket, countryCode, roles] = await Promise.allSettled([
2058
247
  base.path('users', 'authenticated').request(),
2059
248
  base.path('description').request(),
@@ -2066,14 +255,12 @@ function createRobloxApi({ cache, cookies: cookiesList, ipgeolocationKey, }) {
2066
255
  if (user.status === 'rejected') {
2067
256
  const cause = user.reason;
2068
257
  const status = extractStatus(cause);
2069
- const timeline = extractTimeline(cause);
2070
258
  if (status === 429)
2071
259
  throw new RobloxRateLimitError({
2072
260
  data: { status, url: extractUrl(cause), retryAfterMs: extractRetryAfterMs(cause) },
2073
- timeline,
2074
261
  cause,
2075
262
  });
2076
- throw new RobloxAuthError({ data: { status, cookie }, timeline, cause });
263
+ throw new RobloxAuthError({ data: { status, cookie }, cause });
2077
264
  }
2078
265
  return {
2079
266
  ...user.value,
@@ -2085,6 +272,7 @@ function createRobloxApi({ cache, cookies: cookiesList, ipgeolocationKey, }) {
2085
272
  ...(roles.status === 'fulfilled' ? roles.value : {}),
2086
273
  };
2087
274
  })).request();
275
+ return results;
2088
276
  }
2089
277
  async function usersSimple(userIds) {
2090
278
  return withCache({
@@ -2095,16 +283,12 @@ function createRobloxApi({ cache, cookies: cookiesList, ipgeolocationKey, }) {
2095
283
  fallback: {},
2096
284
  fetchMissing: async (missing) => {
2097
285
  try {
2098
- const results = await vigor.all(...chunk(missing.map(Number), 100).map(group => () => usersApi
286
+ const grouped = await vigorFetch.vigor.all(...chunk(missing.map(Number), 100).map(group => () => usersApi
2099
287
  .path('users')
2100
- .body({ userIds: group, excludeBannedUsers: false })
2101
- .interceptors(dataInterceptor)
2102
- .request()))
2103
- .interceptors(vigor.builder.all.interceptors()
2104
- .result((ctx, api) => {
2105
- api.setResult(ctx.result.flat());
2106
- }))
2107
- .request();
288
+ .body("overwrite", { userIds: group, excludeBannedUsers: false })
289
+ .middlewares(dataInterceptor)
290
+ .request())).request();
291
+ const results = grouped.flat();
2108
292
  return results.filter(u => u.id != null && u.name != null && u.displayName != null);
2109
293
  }
2110
294
  catch (cause) {
@@ -2122,7 +306,7 @@ function createRobloxApi({ cache, cookies: cookiesList, ipgeolocationKey, }) {
2122
306
  fallback: {},
2123
307
  fetchMissing: async (missing) => {
2124
308
  try {
2125
- const results = await vigor.all(...missing.map(id => () => usersApi.path('users', id).request()))
309
+ const results = await vigorFetch.vigor.all(...missing.map(id => () => usersApi.path('users', id).request()))
2126
310
  .settings(s => s.concurrency(2))
2127
311
  .request();
2128
312
  return results.filter(u => u.id != null && u.name != null && u.displayName != null && u.description != null);
@@ -2142,16 +326,12 @@ function createRobloxApi({ cache, cookies: cookiesList, ipgeolocationKey, }) {
2142
326
  fallback: {},
2143
327
  fetchMissing: async (missing) => {
2144
328
  try {
2145
- const results = await vigor.all(...chunk(missing, 100).map(group => () => usersApi
329
+ const grouped = await vigorFetch.vigor.all(...chunk(missing, 100).map(group => () => usersApi
2146
330
  .path('usernames', 'users')
2147
- .body({ usernames: group, excludeBannedUsers: false })
2148
- .interceptors(dataInterceptor)
2149
- .request()))
2150
- .interceptors(vigor.builder.all.interceptors()
2151
- .result((ctx, api) => {
2152
- api.setResult(ctx.result.flat());
2153
- }))
2154
- .request();
331
+ .body("overwrite", { usernames: group, excludeBannedUsers: false })
332
+ .middlewares(dataInterceptor)
333
+ .request())).request();
334
+ const results = grouped.flat();
2155
335
  return results.filter(u => u.id != null && u.name != null && u.displayName != null);
2156
336
  }
2157
337
  catch (cause) {
@@ -2162,16 +342,12 @@ function createRobloxApi({ cache, cookies: cookiesList, ipgeolocationKey, }) {
2162
342
  }
2163
343
  async function presence(userIds) {
2164
344
  try {
2165
- return await vigor.all(...chunk(userIds, 50).map(group => () => presenceApi
345
+ const grouped = await vigorFetch.vigor.all(...chunk(userIds, 50).map(group => () => presenceApi
2166
346
  .path('presence', 'users')
2167
- .body({ userIds: group })
2168
- .interceptors(pickKey('userPresences'))
2169
- .request()))
2170
- .interceptors(vigor.builder.all.interceptors()
2171
- .result((ctx, api) => {
2172
- api.setResult(ctx.result.flat());
2173
- }))
2174
- .request();
347
+ .body("overwrite", { userIds: group })
348
+ .middlewares(pickKey('userPresences'))
349
+ .request())).request();
350
+ return grouped.flat();
2175
351
  }
2176
352
  catch (cause) {
2177
353
  throw wrapVigorError(cause);
@@ -2180,16 +356,12 @@ function createRobloxApi({ cache, cookies: cookiesList, ipgeolocationKey, }) {
2180
356
  async function thumbnailAssets(opts) {
2181
357
  const { assetIds, size = '150x150', format = 'Png' } = opts;
2182
358
  try {
2183
- const results = await vigor.all(...chunk(assetIds, 100).map(group => () => thumbnailsApi
359
+ const grouped = await vigorFetch.vigor.all(...chunk(assetIds, 100).map(group => () => thumbnailsApi
2184
360
  .path('assets')
2185
361
  .query({ assetIds: group.join(','), size, format })
2186
- .interceptors(dataInterceptor)
2187
- .request()))
2188
- .interceptors(vigor.builder.all.interceptors()
2189
- .result((ctx, api) => {
2190
- api.setResult(ctx.result.flat());
2191
- }))
2192
- .request();
362
+ .middlewares(dataInterceptor)
363
+ .request())).request();
364
+ const results = grouped.flat();
2193
365
  return results.map(t => ({ ...t, url: t.state === 'Completed' ? t.imageUrl : null }));
2194
366
  }
2195
367
  catch (cause) {
@@ -2207,16 +379,12 @@ function createRobloxApi({ cache, cookies: cookiesList, ipgeolocationKey, }) {
2207
379
  const batch = targets.map((t, i) => ({ ...defaults, ...t, requestId: String(i) }));
2208
380
  const batchMap = new Map(batch.map(t => [t.requestId, t]));
2209
381
  try {
2210
- const results = await vigor.all(...chunk(batch, 100).map(group => () => thumbnailsApi
382
+ const grouped = await vigorFetch.vigor.all(...chunk(batch, 100).map(group => () => thumbnailsApi
2211
383
  .path('batch')
2212
- .body(group)
2213
- .interceptors(dataInterceptor)
2214
- .request()))
2215
- .interceptors(vigor.builder.all.interceptors()
2216
- .result((ctx, api) => {
2217
- api.setResult(ctx.result.flat());
2218
- }))
2219
- .request();
384
+ .body("overwrite", group)
385
+ .middlewares(dataInterceptor)
386
+ .request())).request();
387
+ const results = grouped.flat();
2220
388
  return results.map(item => {
2221
389
  const original = batchMap.get(item.requestId) ?? {};
2222
390
  const { requestId: _rid, ...rest } = item;
@@ -2288,20 +456,21 @@ function createRobloxApi({ cache, cookies: cookiesList, ipgeolocationKey, }) {
2288
456
  fallback: {},
2289
457
  fetchMissing: async (missing) => {
2290
458
  try {
2291
- const universeEntries = await vigor.all(...missing.map(placeId => () => apisRoblox
459
+ const universeEntries = await vigorFetch.vigor.all(...missing.map(placeId => () => apisRoblox
2292
460
  .path('universes', 'v1', 'places', placeId, 'universe')
2293
- .interceptors(vigor.builder.fetch.interceptors()
2294
- .result((ctx, api) => {
461
+ .middlewares(vigorFetch.vigor.builders.fetch.middlewares()
462
+ .after("intercept", async (ctx, api) => {
2295
463
  const r = ctx.result;
2296
464
  api.setResult({ placeId: Number(placeId), universeId: r?.universeId ?? null });
465
+ return ctx;
2297
466
  }))
2298
467
  .request())).request();
2299
- const metaList = await vigor.all(...universeEntries.map(({ placeId, universeId }) => async () => {
468
+ const metaList = await vigorFetch.vigor.all(...universeEntries.map(({ placeId, universeId }) => async () => {
2300
469
  if (!universeId)
2301
470
  return { placeId, universeId: null, info: null, assetIds: [] };
2302
471
  const [details, media] = await Promise.all([
2303
- gamesApi.path('games').query({ universeIds: universeId }).interceptors(dataInterceptor).request(),
2304
- gamesApi.path('games', universeId, 'media').interceptors(dataInterceptor).request(),
472
+ gamesApi.path('games').query({ universeIds: universeId }).middlewares(dataInterceptor).request(),
473
+ gamesApi.path('games', universeId, 'media').middlewares(dataInterceptor).request(),
2305
474
  ]);
2306
475
  return {
2307
476
  placeId,
@@ -2416,7 +585,7 @@ function createRobloxApi({ cache, cookies: cookiesList, ipgeolocationKey, }) {
2416
585
  try {
2417
586
  const res = await gamejoinApi
2418
587
  .path('join-game-instance')
2419
- .body({ placeId, gameId: jobId })
588
+ .body("overwrite", { placeId, gameId: jobId })
2420
589
  .request();
2421
590
  return {
2422
591
  publicIp: res?.joinScript?.UdmuxEndpoint?.[0]?.Address ?? null,
@@ -2461,7 +630,7 @@ function createRobloxApi({ cache, cookies: cookiesList, ipgeolocationKey, }) {
2461
630
  const missJobIds = jobIds.filter(id => !jobHitMap.has(id));
2462
631
  if (missJobIds.length === 0)
2463
632
  return jobIds.map(id => jobHitMap.get(id));
2464
- const extracted = await vigor.all(...missJobIds.map(jobId => async () => {
633
+ const extracted = await vigorFetch.vigor.all(...missJobIds.map(jobId => async () => {
2465
634
  const { publicIp, machineAddress } = await extractIps(placeId, jobId);
2466
635
  return { jobId, publicIp, machineAddress };
2467
636
  }))
@@ -2484,7 +653,7 @@ function createRobloxApi({ cache, cookies: cookiesList, ipgeolocationKey, }) {
2484
653
  const ipHitMap = new Map(cachedByIp.map(({ separator, data }) => [separator, data]));
2485
654
  const stillMissIps = missPublicIps.filter(ip => !ipHitMap.has(ip));
2486
655
  if (stillMissIps.length > 0) {
2487
- const fetched = await vigor.all(...stillMissIps.map(ip => async () => ({ ip, loc: await fetchIpLocation(ip) })))
656
+ const fetched = await vigorFetch.vigor.all(...stillMissIps.map(ip => async () => ({ ip, loc: await fetchIpLocation(ip) })))
2488
657
  .settings(s => s.concurrency(5))
2489
658
  .request();
2490
659
  const toUpsertIp = fetched.filter((e) => e.loc !== null);
@@ -2526,14 +695,11 @@ function createRobloxApi({ cache, cookies: cookiesList, ipgeolocationKey, }) {
2526
695
  ]);
2527
696
  return jobIds.flatMap(id => { const loc = resultMap.get(id); return loc ? [loc] : []; });
2528
697
  }
2529
- // ----------------------------------------------------------------
2530
- // Friends
2531
- // ----------------------------------------------------------------
2532
698
  async function friends(userId) {
2533
699
  try {
2534
700
  return await friendsApi
2535
701
  .path('users', userId, 'friends')
2536
- .interceptors(dataInterceptor)
702
+ .middlewares(dataInterceptor)
2537
703
  .request();
2538
704
  }
2539
705
  catch (cause) {
@@ -2544,7 +710,7 @@ function createRobloxApi({ cache, cookies: cookiesList, ipgeolocationKey, }) {
2544
710
  try {
2545
711
  await friendsApi
2546
712
  .path('users', targetUserId, 'request-friendship')
2547
- .body({})
713
+ .body("overwrite", {})
2548
714
  .request();
2549
715
  }
2550
716
  catch (cause) {
@@ -2555,7 +721,7 @@ function createRobloxApi({ cache, cookies: cookiesList, ipgeolocationKey, }) {
2555
721
  try {
2556
722
  await friendsApi
2557
723
  .path('users', targetUserId, 'unfriend')
2558
- .body({})
724
+ .body("overwrite", {})
2559
725
  .request();
2560
726
  }
2561
727
  catch (cause) {