vigor-roblox 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +239 -0
- package/dist/index.js +2423 -0
- package/dist/index.mjs +2418 -0
- package/package.json +44 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2423 @@
|
|
|
1
|
+
'use strict';
|
|
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[0]);
|
|
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
|
+
};
|
|
1740
|
+
|
|
1741
|
+
// ----------------------------------------------------------------
|
|
1742
|
+
// Error system
|
|
1743
|
+
// ----------------------------------------------------------------
|
|
1744
|
+
const RobloxErrorMessageFuncs = {
|
|
1745
|
+
AUTH_FAILED: ({ status, cookie }) => `Cookie authentication failed (status: ${status ?? 'unknown'}, cookie: ${cookie.slice(0, 8)}...)`,
|
|
1746
|
+
RATE_LIMITED: ({ status, url, retryAfterMs }) => `Rate limited (status: ${status}, url: ${url ?? 'unknown'}, retryAfter: ${retryAfterMs ?? 'unknown'}ms)`,
|
|
1747
|
+
REQUEST_FAILED: ({ status, url }) => `Request failed (status: ${status ?? 'unknown'}, url: ${url ?? 'unknown'})`,
|
|
1748
|
+
};
|
|
1749
|
+
class RobloxApiError extends Error {
|
|
1750
|
+
timestamp = new Date();
|
|
1751
|
+
cause;
|
|
1752
|
+
code;
|
|
1753
|
+
data;
|
|
1754
|
+
timeline;
|
|
1755
|
+
context;
|
|
1756
|
+
constructor(code, options) {
|
|
1757
|
+
const messageFn = RobloxErrorMessageFuncs[code];
|
|
1758
|
+
super(`[${code}] ${messageFn(options.data)}`, { cause: options.cause });
|
|
1759
|
+
this.name = new.target.name;
|
|
1760
|
+
this.code = code;
|
|
1761
|
+
this.cause = options.cause;
|
|
1762
|
+
this.data = options.data;
|
|
1763
|
+
this.timeline = options.timeline ?? [];
|
|
1764
|
+
this.context = options.context;
|
|
1765
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
1766
|
+
Error.captureStackTrace?.(this, new.target);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
class RobloxAuthError extends RobloxApiError {
|
|
1770
|
+
constructor(options) {
|
|
1771
|
+
super('AUTH_FAILED', options);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
class RobloxRateLimitError extends RobloxApiError {
|
|
1775
|
+
constructor(options) {
|
|
1776
|
+
super('RATE_LIMITED', options);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
class RobloxRequestError extends RobloxApiError {
|
|
1780
|
+
constructor(options) {
|
|
1781
|
+
super('REQUEST_FAILED', options);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
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 [];
|
|
1793
|
+
}
|
|
1794
|
+
function extractStatus(cause) {
|
|
1795
|
+
return isFetchFailed(cause) ? cause.data.status : null;
|
|
1796
|
+
}
|
|
1797
|
+
function extractUrl(cause) {
|
|
1798
|
+
return isFetchFailed(cause) ? cause.data.url : null;
|
|
1799
|
+
}
|
|
1800
|
+
function extractRetryAfterMs(cause) {
|
|
1801
|
+
if (!isFetchFailed(cause))
|
|
1802
|
+
return null;
|
|
1803
|
+
const headers = cause.data.response.headers;
|
|
1804
|
+
const raw = headers.get('retry-after')
|
|
1805
|
+
?? headers.get('ratelimit-reset')
|
|
1806
|
+
?? headers.get('x-ratelimit-reset')
|
|
1807
|
+
?? null;
|
|
1808
|
+
if (raw === null)
|
|
1809
|
+
return null;
|
|
1810
|
+
const asNum = Number(raw);
|
|
1811
|
+
if (!isNaN(asNum))
|
|
1812
|
+
return asNum * 1000;
|
|
1813
|
+
const asDate = new Date(raw).getTime();
|
|
1814
|
+
return !isNaN(asDate) ? asDate - Date.now() : null;
|
|
1815
|
+
}
|
|
1816
|
+
function wrapVigorError(cause) {
|
|
1817
|
+
const status = extractStatus(cause);
|
|
1818
|
+
const url = extractUrl(cause);
|
|
1819
|
+
const timeline = extractTimeline(cause);
|
|
1820
|
+
if (status === 429)
|
|
1821
|
+
return new RobloxRateLimitError({
|
|
1822
|
+
data: { status, url, retryAfterMs: extractRetryAfterMs(cause) },
|
|
1823
|
+
timeline,
|
|
1824
|
+
cause,
|
|
1825
|
+
});
|
|
1826
|
+
return new RobloxRequestError({ data: { status, url }, timeline, cause });
|
|
1827
|
+
}
|
|
1828
|
+
function chunk(arr, size) {
|
|
1829
|
+
const out = [];
|
|
1830
|
+
for (let i = 0; i < arr.length; i += size)
|
|
1831
|
+
out.push(arr.slice(i, i + size));
|
|
1832
|
+
return out;
|
|
1833
|
+
}
|
|
1834
|
+
function partition(arr, pred) {
|
|
1835
|
+
const pass = [], fail = [];
|
|
1836
|
+
for (const item of arr)
|
|
1837
|
+
(pred(item) ? pass : fail).push(item);
|
|
1838
|
+
return { pass, fail };
|
|
1839
|
+
}
|
|
1840
|
+
// ----------------------------------------------------------------
|
|
1841
|
+
// Factory
|
|
1842
|
+
// ----------------------------------------------------------------
|
|
1843
|
+
function createRobloxApi({ cache, cookies: cookiesList, ipgeolocationKey, }) {
|
|
1844
|
+
const cookiePool = cookiesList.map(cookie => ({ cookie, lastUsed: 0 }));
|
|
1845
|
+
function pickCookie() {
|
|
1846
|
+
const entry = cookiePool.reduce((a, b) => a.lastUsed < b.lastUsed ? a : b);
|
|
1847
|
+
entry.lastUsed = Date.now();
|
|
1848
|
+
return entry.cookie;
|
|
1849
|
+
}
|
|
1850
|
+
function pickKey(key) {
|
|
1851
|
+
return vigor.builder.fetch.interceptors()
|
|
1852
|
+
.result((ctx, api) => {
|
|
1853
|
+
api.setResult(ctx.result[key]);
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
const dataInterceptor = pickKey('data');
|
|
1857
|
+
const cookieInterceptor = vigor.builder.fetch.interceptors()
|
|
1858
|
+
.before((ctx, api) => {
|
|
1859
|
+
api.setHeaders({
|
|
1860
|
+
...ctx.options.headers,
|
|
1861
|
+
Cookie: `.ROBLOSECURITY=${pickCookie()}`,
|
|
1862
|
+
});
|
|
1863
|
+
});
|
|
1864
|
+
const winInetInterceptor = vigor.builder.fetch.interceptors()
|
|
1865
|
+
.before((ctx, api) => {
|
|
1866
|
+
api.setHeaders({
|
|
1867
|
+
...ctx.options.headers,
|
|
1868
|
+
'User-Agent': 'Roblox/WinInet',
|
|
1869
|
+
});
|
|
1870
|
+
});
|
|
1871
|
+
const usersApi = vigor.fetch('https://users.roblox.com/v1')
|
|
1872
|
+
.interceptors(cookieInterceptor)
|
|
1873
|
+
.interceptors(winInetInterceptor)
|
|
1874
|
+
.retryConfig(c => c
|
|
1875
|
+
.settings(s => s.attempt(7))
|
|
1876
|
+
.algorithms(a => a.backoff().initial(200).unit(800).multiplier(1.7)));
|
|
1877
|
+
const thumbnailsApi = vigor.fetch('https://thumbnails.roblox.com/v1')
|
|
1878
|
+
.interceptors(cookieInterceptor)
|
|
1879
|
+
.interceptors(winInetInterceptor)
|
|
1880
|
+
.retryConfig(c => c
|
|
1881
|
+
.settings(s => s.attempt(5))
|
|
1882
|
+
.algorithms(a => a.backoff().initial(1000).multiplier(2.5)));
|
|
1883
|
+
const gamesApi = vigor.fetch('https://games.roblox.com/v1')
|
|
1884
|
+
.interceptors(cookieInterceptor)
|
|
1885
|
+
.retryConfig(c => c
|
|
1886
|
+
.settings(s => s.attempt(5))
|
|
1887
|
+
.algorithms(a => a.backoff().initial(1000).multiplier(2.5)));
|
|
1888
|
+
const presenceApi = vigor.fetch('https://presence.roblox.com/v1')
|
|
1889
|
+
.interceptors(cookieInterceptor)
|
|
1890
|
+
.retryConfig(c => c
|
|
1891
|
+
.settings(s => s.attempt(5))
|
|
1892
|
+
.algorithms(a => a.backoff().initial(500).multiplier(2)));
|
|
1893
|
+
const apisRoblox = vigor.fetch('https://apis.roblox.com')
|
|
1894
|
+
.interceptors(cookieInterceptor)
|
|
1895
|
+
.retryConfig(c => c
|
|
1896
|
+
.settings(s => s.attempt(5))
|
|
1897
|
+
.algorithms(a => a.backoff().initial(1000).multiplier(2)));
|
|
1898
|
+
const gamejoinApi = vigor.fetch('https://gamejoin.roblox.com/v1')
|
|
1899
|
+
.interceptors(cookieInterceptor)
|
|
1900
|
+
.retryConfig(c => c
|
|
1901
|
+
.settings(s => s.attempt(7))
|
|
1902
|
+
.algorithms(a => a.backoff().initial(500).multiplier(1.5)));
|
|
1903
|
+
const ipgeolocationApi = vigor.fetch('https://api.ipgeolocation.io')
|
|
1904
|
+
.retryConfig(c => c
|
|
1905
|
+
.settings(s => s.attempt(4))
|
|
1906
|
+
.algorithms(a => a.backoff().initial(500).multiplier(2)));
|
|
1907
|
+
async function withCache(opts) {
|
|
1908
|
+
const { type, keys, ttlMs, getKey, fetchMissing, fallback } = opts;
|
|
1909
|
+
const cached = await cache.select(type, keys);
|
|
1910
|
+
const cacheMap = new Map(cached.map(({ separator, data }) => [separator, data]));
|
|
1911
|
+
const missing = keys.filter(k => !cacheMap.has(k));
|
|
1912
|
+
if (missing.length > 0) {
|
|
1913
|
+
const fetched = await fetchMissing(missing);
|
|
1914
|
+
await cache.upsert(type, ttlMs, fetched.map(item => ({ separator: getKey(item), data: item })));
|
|
1915
|
+
fetched.forEach(item => cacheMap.set(getKey(item), item));
|
|
1916
|
+
}
|
|
1917
|
+
return keys.map(k => cacheMap.get(k) ?? fallback);
|
|
1918
|
+
}
|
|
1919
|
+
async function authenticated(cookies) {
|
|
1920
|
+
return vigor.all(...cookies.map(cookie => async () => {
|
|
1921
|
+
const fixedCookieInterceptor = vigor.builder.fetch.interceptors()
|
|
1922
|
+
.before((ctx, api) => {
|
|
1923
|
+
api.setHeaders({
|
|
1924
|
+
...ctx.options.headers,
|
|
1925
|
+
Cookie: `.ROBLOSECURITY=${cookie}`,
|
|
1926
|
+
});
|
|
1927
|
+
});
|
|
1928
|
+
const base = usersApi.interceptors(fixedCookieInterceptor);
|
|
1929
|
+
const [user, description, birthdate, gender, ageBracket, countryCode, roles] = await Promise.allSettled([
|
|
1930
|
+
base.path('users', 'authenticated').request(),
|
|
1931
|
+
base.path('description').request(),
|
|
1932
|
+
base.path('birthdate').request(),
|
|
1933
|
+
base.path('gender').request(),
|
|
1934
|
+
base.path('users', 'authenticated', 'age-bracket').request(),
|
|
1935
|
+
base.path('users', 'authenticated', 'country-code').request(),
|
|
1936
|
+
base.path('users', 'authenticated', 'roles').request(),
|
|
1937
|
+
]);
|
|
1938
|
+
if (user.status === 'rejected') {
|
|
1939
|
+
const cause = user.reason;
|
|
1940
|
+
const status = extractStatus(cause);
|
|
1941
|
+
const timeline = extractTimeline(cause);
|
|
1942
|
+
if (status === 429)
|
|
1943
|
+
throw new RobloxRateLimitError({
|
|
1944
|
+
data: { status, url: extractUrl(cause), retryAfterMs: extractRetryAfterMs(cause) },
|
|
1945
|
+
timeline,
|
|
1946
|
+
cause,
|
|
1947
|
+
});
|
|
1948
|
+
throw new RobloxAuthError({ data: { status, cookie }, timeline, cause });
|
|
1949
|
+
}
|
|
1950
|
+
return {
|
|
1951
|
+
...user.value,
|
|
1952
|
+
...(description.status === 'fulfilled' ? description.value : {}),
|
|
1953
|
+
...(birthdate.status === 'fulfilled' ? birthdate.value : {}),
|
|
1954
|
+
...(gender.status === 'fulfilled' ? gender.value : {}),
|
|
1955
|
+
...(ageBracket.status === 'fulfilled' ? ageBracket.value : {}),
|
|
1956
|
+
...(countryCode.status === 'fulfilled' ? countryCode.value : {}),
|
|
1957
|
+
...(roles.status === 'fulfilled' ? roles.value : {}),
|
|
1958
|
+
};
|
|
1959
|
+
})).request();
|
|
1960
|
+
}
|
|
1961
|
+
async function usersSimple(userIds) {
|
|
1962
|
+
return withCache({
|
|
1963
|
+
type: 'usersSimple',
|
|
1964
|
+
keys: userIds.map(String),
|
|
1965
|
+
ttlMs: 30 * 60 * 1000,
|
|
1966
|
+
getKey: item => String(item.id),
|
|
1967
|
+
fallback: {},
|
|
1968
|
+
fetchMissing: async (missing) => {
|
|
1969
|
+
try {
|
|
1970
|
+
const results = await vigor.all(...chunk(missing.map(Number), 100).map(group => () => usersApi
|
|
1971
|
+
.path('users')
|
|
1972
|
+
.body({ userIds: group, excludeBannedUsers: false })
|
|
1973
|
+
.interceptors(dataInterceptor)
|
|
1974
|
+
.request()))
|
|
1975
|
+
.interceptors(vigor.builder.all.interceptors()
|
|
1976
|
+
.result((ctx, api) => {
|
|
1977
|
+
api.setResult(ctx.result.flat());
|
|
1978
|
+
}))
|
|
1979
|
+
.request();
|
|
1980
|
+
return results.filter(u => u.id != null && u.name != null && u.displayName != null);
|
|
1981
|
+
}
|
|
1982
|
+
catch (cause) {
|
|
1983
|
+
throw wrapVigorError(cause);
|
|
1984
|
+
}
|
|
1985
|
+
},
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
async function users(userIds) {
|
|
1989
|
+
return withCache({
|
|
1990
|
+
type: 'users',
|
|
1991
|
+
keys: userIds.map(String),
|
|
1992
|
+
ttlMs: 60 * 60 * 1000,
|
|
1993
|
+
getKey: item => String(item.id),
|
|
1994
|
+
fallback: {},
|
|
1995
|
+
fetchMissing: async (missing) => {
|
|
1996
|
+
try {
|
|
1997
|
+
const results = await vigor.all(...missing.map(id => () => usersApi.path('users', id).request()))
|
|
1998
|
+
.settings(s => s.concurrency(2))
|
|
1999
|
+
.request();
|
|
2000
|
+
return results.filter(u => u.id != null && u.name != null && u.displayName != null && u.description != null);
|
|
2001
|
+
}
|
|
2002
|
+
catch (cause) {
|
|
2003
|
+
throw wrapVigorError(cause);
|
|
2004
|
+
}
|
|
2005
|
+
},
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
async function usersByName(usernames) {
|
|
2009
|
+
return withCache({
|
|
2010
|
+
type: 'usernames',
|
|
2011
|
+
keys: usernames,
|
|
2012
|
+
ttlMs: 30 * 60 * 1000,
|
|
2013
|
+
getKey: item => item.requestedUsername ?? item.name,
|
|
2014
|
+
fallback: {},
|
|
2015
|
+
fetchMissing: async (missing) => {
|
|
2016
|
+
try {
|
|
2017
|
+
const results = await vigor.all(...chunk(missing, 100).map(group => () => usersApi
|
|
2018
|
+
.path('usernames', 'users')
|
|
2019
|
+
.body({ usernames: group, excludeBannedUsers: false })
|
|
2020
|
+
.interceptors(dataInterceptor)
|
|
2021
|
+
.request()))
|
|
2022
|
+
.interceptors(vigor.builder.all.interceptors()
|
|
2023
|
+
.result((ctx, api) => {
|
|
2024
|
+
api.setResult(ctx.result.flat());
|
|
2025
|
+
}))
|
|
2026
|
+
.request();
|
|
2027
|
+
return results.filter(u => u.id != null && u.name != null && u.displayName != null);
|
|
2028
|
+
}
|
|
2029
|
+
catch (cause) {
|
|
2030
|
+
throw wrapVigorError(cause);
|
|
2031
|
+
}
|
|
2032
|
+
},
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
async function presence(userIds) {
|
|
2036
|
+
try {
|
|
2037
|
+
return await vigor.all(...chunk(userIds, 50).map(group => () => presenceApi
|
|
2038
|
+
.path('presence', 'users')
|
|
2039
|
+
.body({ userIds: group })
|
|
2040
|
+
.interceptors(pickKey('userPresences'))
|
|
2041
|
+
.request()))
|
|
2042
|
+
.interceptors(vigor.builder.all.interceptors()
|
|
2043
|
+
.result((ctx, api) => {
|
|
2044
|
+
api.setResult(ctx.result.flat());
|
|
2045
|
+
}))
|
|
2046
|
+
.request();
|
|
2047
|
+
}
|
|
2048
|
+
catch (cause) {
|
|
2049
|
+
throw wrapVigorError(cause);
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
async function thumbnailAssets(opts) {
|
|
2053
|
+
const { assetIds, size = '150x150', format = 'Png' } = opts;
|
|
2054
|
+
try {
|
|
2055
|
+
const results = await vigor.all(...chunk(assetIds, 100).map(group => () => thumbnailsApi
|
|
2056
|
+
.path('assets')
|
|
2057
|
+
.query({ assetIds: group.join(','), size, format })
|
|
2058
|
+
.interceptors(dataInterceptor)
|
|
2059
|
+
.request()))
|
|
2060
|
+
.interceptors(vigor.builder.all.interceptors()
|
|
2061
|
+
.result((ctx, api) => {
|
|
2062
|
+
api.setResult(ctx.result.flat());
|
|
2063
|
+
}))
|
|
2064
|
+
.request();
|
|
2065
|
+
return results.map(t => ({ ...t, url: t.state === 'Completed' ? t.url : null }));
|
|
2066
|
+
}
|
|
2067
|
+
catch (cause) {
|
|
2068
|
+
throw wrapVigorError(cause);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
async function thumbnailsBatch(targets, formatDefaults = {}) {
|
|
2072
|
+
const defaults = {
|
|
2073
|
+
type: 'AvatarHeadShot',
|
|
2074
|
+
size: '150x150',
|
|
2075
|
+
format: 'Png',
|
|
2076
|
+
isCircular: false,
|
|
2077
|
+
...formatDefaults,
|
|
2078
|
+
};
|
|
2079
|
+
const batch = targets.map((t, i) => ({ ...defaults, ...t, requestId: String(i) }));
|
|
2080
|
+
const batchMap = new Map(batch.map(t => [t.requestId, t]));
|
|
2081
|
+
try {
|
|
2082
|
+
const results = await vigor.all(...chunk(batch, 100).map(group => () => thumbnailsApi
|
|
2083
|
+
.path('batch')
|
|
2084
|
+
.body(group)
|
|
2085
|
+
.interceptors(dataInterceptor)
|
|
2086
|
+
.request()))
|
|
2087
|
+
.interceptors(vigor.builder.all.interceptors()
|
|
2088
|
+
.result((ctx, api) => {
|
|
2089
|
+
api.setResult(ctx.result.flat());
|
|
2090
|
+
}))
|
|
2091
|
+
.request();
|
|
2092
|
+
return results.map(item => {
|
|
2093
|
+
const original = batchMap.get(item.requestId) ?? {};
|
|
2094
|
+
const { requestId: _rid, ...rest } = item;
|
|
2095
|
+
return { ...original, ...rest, url: rest.state === 'Completed' ? rest.url : null };
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
catch (cause) {
|
|
2099
|
+
throw wrapVigorError(cause);
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
async function serversSimple(opts) {
|
|
2103
|
+
const { placeId, count = 1, serverType = 'Public', cursor, thumbnailFormat } = opts;
|
|
2104
|
+
let nextCursor = cursor ?? null;
|
|
2105
|
+
let prevCursor = null;
|
|
2106
|
+
const rawData = [];
|
|
2107
|
+
try {
|
|
2108
|
+
for (let i = 0; i < count; i++) {
|
|
2109
|
+
const page = await gamesApi
|
|
2110
|
+
.path('games', placeId, 'servers', serverType)
|
|
2111
|
+
.query({ limit: 100, ...(nextCursor ? { cursor: nextCursor } : {}) })
|
|
2112
|
+
.request();
|
|
2113
|
+
if (i === 0)
|
|
2114
|
+
prevCursor = page.previousPageCursor;
|
|
2115
|
+
nextCursor = page.nextPageCursor;
|
|
2116
|
+
rawData.push(...page.data);
|
|
2117
|
+
if (!nextCursor)
|
|
2118
|
+
break;
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
catch (cause) {
|
|
2122
|
+
throw wrapVigorError(cause);
|
|
2123
|
+
}
|
|
2124
|
+
const thumbTargets = rawData
|
|
2125
|
+
.flatMap(s => s.playerTokens.map(token => ({ token, type: 'AvatarHeadShot', size: '150x150', format: 'Png', ...thumbnailFormat })));
|
|
2126
|
+
const thumbResults = await thumbnailsBatch(thumbTargets, thumbnailFormat);
|
|
2127
|
+
const thumbMap = new Map(thumbResults.map(t => [t.token, t.url]));
|
|
2128
|
+
return {
|
|
2129
|
+
previousPageCursor: prevCursor,
|
|
2130
|
+
nextPageCursor: nextCursor,
|
|
2131
|
+
data: rawData.map(s => ({
|
|
2132
|
+
jobId: s.id,
|
|
2133
|
+
maxPlayers: s.maxPlayers,
|
|
2134
|
+
playing: s.playing,
|
|
2135
|
+
fps: s.fps,
|
|
2136
|
+
ping: s.ping,
|
|
2137
|
+
playerImgs: s.playerTokens.map(tok => thumbMap.get(tok)).filter((url) => url != null),
|
|
2138
|
+
})),
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
async function servers(opts) {
|
|
2142
|
+
const result = await serversSimple(opts);
|
|
2143
|
+
const jobIds = result.data.map(s => s.jobId);
|
|
2144
|
+
const locationList = await serversRegion({ placeId: opts.placeId, jobIds }).catch(() => []);
|
|
2145
|
+
const locationMap = new Map(locationList.map(l => [l.jobId, l]));
|
|
2146
|
+
return {
|
|
2147
|
+
...result,
|
|
2148
|
+
data: result.data.map(s => ({
|
|
2149
|
+
...s,
|
|
2150
|
+
location: locationMap.get(s.jobId) ?? null,
|
|
2151
|
+
})),
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
async function placeInfo(placeIds) {
|
|
2155
|
+
return withCache({
|
|
2156
|
+
type: 'placeInfo',
|
|
2157
|
+
keys: placeIds.map(String),
|
|
2158
|
+
ttlMs: 60 * 60 * 1000,
|
|
2159
|
+
getKey: item => String(item.placeId),
|
|
2160
|
+
fallback: {},
|
|
2161
|
+
fetchMissing: async (missing) => {
|
|
2162
|
+
try {
|
|
2163
|
+
const universeEntries = await vigor.all(...missing.map(placeId => () => apisRoblox
|
|
2164
|
+
.path('universes', 'v1', 'places', placeId, 'universe')
|
|
2165
|
+
.interceptors(vigor.builder.fetch.interceptors()
|
|
2166
|
+
.result((ctx, api) => {
|
|
2167
|
+
const r = ctx.result;
|
|
2168
|
+
api.setResult({ placeId: Number(placeId), universeId: r?.universeId ?? null });
|
|
2169
|
+
}))
|
|
2170
|
+
.request())).request();
|
|
2171
|
+
const metaList = await vigor.all(...universeEntries.map(({ placeId, universeId }) => async () => {
|
|
2172
|
+
if (!universeId)
|
|
2173
|
+
return { placeId, universeId: null, info: null, assetIds: [] };
|
|
2174
|
+
const [details, media] = await Promise.all([
|
|
2175
|
+
gamesApi.path('games').query({ universeIds: universeId }).interceptors(dataInterceptor).request(),
|
|
2176
|
+
gamesApi.path('games', universeId, 'media').interceptors(dataInterceptor).request(),
|
|
2177
|
+
]);
|
|
2178
|
+
return {
|
|
2179
|
+
placeId,
|
|
2180
|
+
universeId,
|
|
2181
|
+
info: details?.[0] ?? null,
|
|
2182
|
+
assetIds: (media ?? []).map(m => m.imageId).filter((id) => id != null),
|
|
2183
|
+
};
|
|
2184
|
+
})).request();
|
|
2185
|
+
const allAssetIds = [...new Set(metaList.flatMap(m => m.assetIds))];
|
|
2186
|
+
const assetUrlMap = new Map();
|
|
2187
|
+
if (allAssetIds.length > 0) {
|
|
2188
|
+
const thumbs = await thumbnailAssets({ assetIds: allAssetIds, size: '768x432', format: 'Png' });
|
|
2189
|
+
thumbs.forEach(t => { if (t.targetId != null && t.url)
|
|
2190
|
+
assetUrlMap.set(t.targetId, t.url); });
|
|
2191
|
+
}
|
|
2192
|
+
return metaList.map(({ placeId, universeId, info, assetIds }) => ({
|
|
2193
|
+
...(info ?? {}),
|
|
2194
|
+
placeId,
|
|
2195
|
+
universeId,
|
|
2196
|
+
logos: assetIds.map(id => assetUrlMap.get(id)).filter((u) => u != null),
|
|
2197
|
+
}));
|
|
2198
|
+
}
|
|
2199
|
+
catch (cause) {
|
|
2200
|
+
throw wrapVigorError(cause);
|
|
2201
|
+
}
|
|
2202
|
+
},
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
2205
|
+
async function usersSimpleWithImg(userIds) {
|
|
2206
|
+
const [userList, thumbs] = await Promise.all([
|
|
2207
|
+
usersSimple(userIds),
|
|
2208
|
+
thumbnailsBatch(userIds.map(id => ({ targetId: id }))),
|
|
2209
|
+
]);
|
|
2210
|
+
const imgMap = new Map(thumbs.map(t => [t.targetId, t.url]));
|
|
2211
|
+
return userList.map(u => ({ ...u, img: imgMap.get(u.id) ?? null }));
|
|
2212
|
+
}
|
|
2213
|
+
async function usersWithImg(userIds) {
|
|
2214
|
+
const [userList, thumbs] = await Promise.all([
|
|
2215
|
+
users(userIds),
|
|
2216
|
+
thumbnailsBatch(userIds.map(id => ({ targetId: id }))),
|
|
2217
|
+
]);
|
|
2218
|
+
const imgMap = new Map(thumbs.map(t => [t.targetId, t.url]));
|
|
2219
|
+
return userList.map(u => ({ ...u, img: imgMap.get(u.id) ?? null }));
|
|
2220
|
+
}
|
|
2221
|
+
async function track(opts) {
|
|
2222
|
+
const { placeId, targets } = opts;
|
|
2223
|
+
const { pass: rawIds, fail: names } = partition(targets, t => !Number.isNaN(Number(t)));
|
|
2224
|
+
const resolvedIds = (await usersByName(names)).map(u => u.id);
|
|
2225
|
+
const idList = [...rawIds.map(Number), ...resolvedIds];
|
|
2226
|
+
const [userList, serverResult, thumbs] = await Promise.all([
|
|
2227
|
+
usersSimple(idList),
|
|
2228
|
+
serversSimple({ placeId, count: 20 }),
|
|
2229
|
+
thumbnailsBatch(idList.map(id => ({ targetId: id }))),
|
|
2230
|
+
]);
|
|
2231
|
+
const thumbnailsMap = new Map(thumbs.map(t => [t.targetId, t.url]));
|
|
2232
|
+
const defaultHashes = new Set([
|
|
2233
|
+
'5816BB6B457A7A2FD8F0299D6F79DADF', 'D517857E5CC51E2FF93E63E20241169E',
|
|
2234
|
+
'56DFC0F87BABBE49C6D1BE708AE9A66A', 'C16BE31B5A403C45279B3FF5533980E9',
|
|
2235
|
+
'51E47F0C53DA3A617158586DF73B1236', 'ACCF91F734E311F4A0EF23C3EDA54284',
|
|
2236
|
+
'CF083BB49C3304C593C43617FF06418E', '3259891600987E41060EC3A43511F2F9',
|
|
2237
|
+
'19F6EB627A565DF5ABC0B82925B2C760', '5CB6042A80C64D34BA98721C96F5D6A3',
|
|
2238
|
+
'E592BA2BBFA44C9021643D25BC014BD5', '661AD135B4409FF51BC4A6D80E6AC0C7',
|
|
2239
|
+
'8E0E19FD517F46AD46A8A322377CA89B', '1E8FFEC57F042949AEFAC69FECC72D38',
|
|
2240
|
+
'64D3D8C3021F7E8442CCA2825051A87A',
|
|
2241
|
+
]);
|
|
2242
|
+
const getHash = (url) => {
|
|
2243
|
+
if (!url)
|
|
2244
|
+
return null;
|
|
2245
|
+
try {
|
|
2246
|
+
const segments = new URL(url).pathname.split('/');
|
|
2247
|
+
const segment = segments.find(s => s.includes('-'));
|
|
2248
|
+
if (!segment)
|
|
2249
|
+
return null;
|
|
2250
|
+
const parts = segment.split('-');
|
|
2251
|
+
return parts.length >= 3 ? parts.slice(1, -1).join('-') : null;
|
|
2252
|
+
}
|
|
2253
|
+
catch {
|
|
2254
|
+
return null;
|
|
2255
|
+
}
|
|
2256
|
+
};
|
|
2257
|
+
const serverHashMap = new Map();
|
|
2258
|
+
serverResult.data.forEach(s => s.playerImgs.forEach(img => {
|
|
2259
|
+
const h = getHash(img);
|
|
2260
|
+
if (h)
|
|
2261
|
+
serverHashMap.set(h, s);
|
|
2262
|
+
}));
|
|
2263
|
+
const matchedJobIds = new Set();
|
|
2264
|
+
const userServerMap = new Map();
|
|
2265
|
+
for (const user of userList) {
|
|
2266
|
+
const img = thumbnailsMap.get(user.id) ?? null;
|
|
2267
|
+
const hash = getHash(img);
|
|
2268
|
+
const server = hash && !defaultHashes.has(hash) ? (serverHashMap.get(hash) ?? null) : null;
|
|
2269
|
+
if (server) {
|
|
2270
|
+
userServerMap.set(user.id, server);
|
|
2271
|
+
matchedJobIds.add(server.jobId);
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
const locationList = matchedJobIds.size > 0
|
|
2275
|
+
? await serversRegion({ placeId, jobIds: [...matchedJobIds] })
|
|
2276
|
+
: [];
|
|
2277
|
+
const locationMap = new Map(locationList.map(l => [l.jobId, l]));
|
|
2278
|
+
return userList.map(user => {
|
|
2279
|
+
const img = thumbnailsMap.get(user.id) ?? null;
|
|
2280
|
+
const server = userServerMap.get(user.id) ?? null;
|
|
2281
|
+
return {
|
|
2282
|
+
user: { ...user, img },
|
|
2283
|
+
server: server ? { ...server, location: locationMap.get(server.jobId) ?? null } : null,
|
|
2284
|
+
};
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
async function extractIps(placeId, jobId) {
|
|
2288
|
+
try {
|
|
2289
|
+
const res = await gamejoinApi
|
|
2290
|
+
.path('join-game-instance')
|
|
2291
|
+
.body({ placeId, gameId: jobId })
|
|
2292
|
+
.request();
|
|
2293
|
+
return {
|
|
2294
|
+
publicIp: res?.joinScript?.UdmuxEndpoint?.[0]?.Address ?? null,
|
|
2295
|
+
machineAddress: res?.joinScript?.MachineAddress ?? null,
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
catch {
|
|
2299
|
+
return { publicIp: null, machineAddress: null };
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
async function fetchIpLocation(ip) {
|
|
2303
|
+
try {
|
|
2304
|
+
const raw = await ipgeolocationApi
|
|
2305
|
+
.path('ipgeo')
|
|
2306
|
+
.query({ apiKey: ipgeolocationKey, ip, fields: 'country_code2,country_name,state_prov,city,latitude,longitude,isp,time_zone' })
|
|
2307
|
+
.request();
|
|
2308
|
+
return {
|
|
2309
|
+
ip,
|
|
2310
|
+
countryCode: String(raw.country_code2 ?? ''),
|
|
2311
|
+
countryName: String(raw.country_name ?? ''),
|
|
2312
|
+
regionName: String(raw.state_prov ?? ''),
|
|
2313
|
+
city: String(raw.city ?? ''),
|
|
2314
|
+
latitude: Number(raw.latitude ?? 0),
|
|
2315
|
+
longitude: Number(raw.longitude ?? 0),
|
|
2316
|
+
isp: String(raw.isp ?? ''),
|
|
2317
|
+
timezone: String(raw.time_zone?.name ?? ''),
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2320
|
+
catch {
|
|
2321
|
+
return null;
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
async function serversRegion(opts) {
|
|
2325
|
+
const { placeId, jobIds } = opts;
|
|
2326
|
+
if (jobIds.length === 0)
|
|
2327
|
+
return [];
|
|
2328
|
+
const JOB_TTL = 12 * 60 * 60 * 1000;
|
|
2329
|
+
const IP_TTL = 31 * 24 * 60 * 60 * 1000;
|
|
2330
|
+
const MACHINE_TTL = 2 * 24 * 60 * 60 * 1000;
|
|
2331
|
+
const cachedByJob = await cache.select('serverLocation:job', jobIds);
|
|
2332
|
+
const jobHitMap = new Map(cachedByJob.map(({ separator, data }) => [separator, data]));
|
|
2333
|
+
const missJobIds = jobIds.filter(id => !jobHitMap.has(id));
|
|
2334
|
+
if (missJobIds.length === 0)
|
|
2335
|
+
return jobIds.map(id => jobHitMap.get(id));
|
|
2336
|
+
const extracted = await vigor.all(...missJobIds.map(jobId => async () => {
|
|
2337
|
+
const { publicIp, machineAddress } = await extractIps(placeId, jobId);
|
|
2338
|
+
return { jobId, publicIp, machineAddress };
|
|
2339
|
+
}))
|
|
2340
|
+
.settings(s => s.concurrency(3))
|
|
2341
|
+
.request();
|
|
2342
|
+
const validExtracted = extracted.filter((e) => e.publicIp !== null);
|
|
2343
|
+
const machineAddresses = [...new Set(validExtracted.map(e => e.machineAddress).filter((m) => m !== null))];
|
|
2344
|
+
const cachedByMachine = await cache.select('serverLocation:machine', machineAddresses);
|
|
2345
|
+
const machineHitMap = new Map(cachedByMachine.map(({ separator, data }) => [separator, data]));
|
|
2346
|
+
const { pass: machineHits, fail: machineMiss } = validExtracted.reduce((acc, e) => {
|
|
2347
|
+
const cached = e.machineAddress ? machineHitMap.get(e.machineAddress) : undefined;
|
|
2348
|
+
if (cached)
|
|
2349
|
+
acc.pass.push({ ...e, loc: cached });
|
|
2350
|
+
else
|
|
2351
|
+
acc.fail.push(e);
|
|
2352
|
+
return acc;
|
|
2353
|
+
}, { pass: [], fail: [] });
|
|
2354
|
+
const missPublicIps = [...new Set(machineMiss.map(e => e.publicIp))];
|
|
2355
|
+
const cachedByIp = await cache.select('serverLocation:ip', missPublicIps);
|
|
2356
|
+
const ipHitMap = new Map(cachedByIp.map(({ separator, data }) => [separator, data]));
|
|
2357
|
+
const stillMissIps = missPublicIps.filter(ip => !ipHitMap.has(ip));
|
|
2358
|
+
if (stillMissIps.length > 0) {
|
|
2359
|
+
const fetched = await vigor.all(...stillMissIps.map(ip => async () => ({ ip, loc: await fetchIpLocation(ip) })))
|
|
2360
|
+
.settings(s => s.concurrency(5))
|
|
2361
|
+
.request();
|
|
2362
|
+
const toUpsertIp = fetched.filter((e) => e.loc !== null);
|
|
2363
|
+
if (toUpsertIp.length > 0) {
|
|
2364
|
+
await cache.upsert('serverLocation:ip', IP_TTL, toUpsertIp.map(({ ip, loc }) => ({ separator: ip, data: loc })));
|
|
2365
|
+
toUpsertIp.forEach(({ ip, loc }) => ipHitMap.set(ip, loc));
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
const toUpsertMachine = [];
|
|
2369
|
+
for (const e of machineMiss) {
|
|
2370
|
+
const loc = ipHitMap.get(e.publicIp);
|
|
2371
|
+
if (loc && e.machineAddress && !machineHitMap.has(e.machineAddress)) {
|
|
2372
|
+
toUpsertMachine.push({ separator: e.machineAddress, data: loc });
|
|
2373
|
+
machineHitMap.set(e.machineAddress, loc);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
if (toUpsertMachine.length > 0)
|
|
2377
|
+
await cache.upsert('serverLocation:machine', MACHINE_TTL, toUpsertMachine);
|
|
2378
|
+
const jobLocations = [];
|
|
2379
|
+
const toUpsertJob = [];
|
|
2380
|
+
for (const { jobId, loc } of machineHits) {
|
|
2381
|
+
const full = { ...loc, jobId: jobId };
|
|
2382
|
+
jobLocations.push(full);
|
|
2383
|
+
toUpsertJob.push({ separator: jobId, data: full });
|
|
2384
|
+
}
|
|
2385
|
+
for (const e of machineMiss) {
|
|
2386
|
+
const loc = ipHitMap.get(e.publicIp);
|
|
2387
|
+
if (!loc)
|
|
2388
|
+
continue;
|
|
2389
|
+
const full = { ...loc, jobId: e.jobId };
|
|
2390
|
+
jobLocations.push(full);
|
|
2391
|
+
toUpsertJob.push({ separator: e.jobId, data: full });
|
|
2392
|
+
}
|
|
2393
|
+
if (toUpsertJob.length > 0)
|
|
2394
|
+
await cache.upsert('serverLocation:job', JOB_TTL, toUpsertJob);
|
|
2395
|
+
const resultMap = new Map([
|
|
2396
|
+
...jobHitMap.entries(),
|
|
2397
|
+
...jobLocations.map(loc => [loc.jobId, loc]),
|
|
2398
|
+
]);
|
|
2399
|
+
return jobIds.flatMap(id => { const loc = resultMap.get(id); return loc ? [loc] : []; });
|
|
2400
|
+
}
|
|
2401
|
+
return {
|
|
2402
|
+
authenticated,
|
|
2403
|
+
usersSimple,
|
|
2404
|
+
users,
|
|
2405
|
+
usersByName,
|
|
2406
|
+
thumbnailAssets,
|
|
2407
|
+
thumbnailsBatch,
|
|
2408
|
+
serversSimple,
|
|
2409
|
+
servers,
|
|
2410
|
+
presence,
|
|
2411
|
+
placeInfo,
|
|
2412
|
+
usersSimpleWithImg,
|
|
2413
|
+
usersWithImg,
|
|
2414
|
+
track,
|
|
2415
|
+
serversRegion,
|
|
2416
|
+
_internal: { gamejoinApi, gamesApi, apisRoblox },
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
exports.RobloxAuthError = RobloxAuthError;
|
|
2421
|
+
exports.RobloxRateLimitError = RobloxRateLimitError;
|
|
2422
|
+
exports.RobloxRequestError = RobloxRequestError;
|
|
2423
|
+
exports.createRobloxApi = createRobloxApi;
|