brawlstars-sdk 0.3.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/CHANGELOG.md +76 -0
- package/CHANGES.md +19 -0
- package/LICENSE +21 -0
- package/README.md +212 -0
- package/dist/index.cjs +1755 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +929 -0
- package/dist/index.d.ts +929 -0
- package/dist/index.js +1718 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -0
- package/types/index.d.ts +1 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1718 @@
|
|
|
1
|
+
// src/errors/api-errors.ts
|
|
2
|
+
var ApiError = class extends Error {
|
|
3
|
+
endpoint;
|
|
4
|
+
method;
|
|
5
|
+
request;
|
|
6
|
+
statusCode;
|
|
7
|
+
headers;
|
|
8
|
+
body;
|
|
9
|
+
responseBody;
|
|
10
|
+
retryAfter;
|
|
11
|
+
retryAfterMs;
|
|
12
|
+
requestId;
|
|
13
|
+
constructor(message, metadata) {
|
|
14
|
+
super(message, metadata.cause ? { cause: metadata.cause } : void 0);
|
|
15
|
+
this.name = "ApiError";
|
|
16
|
+
this.endpoint = metadata.endpoint;
|
|
17
|
+
this.method = metadata.method;
|
|
18
|
+
this.request = metadata.request;
|
|
19
|
+
this.statusCode = metadata.statusCode;
|
|
20
|
+
this.headers = metadata.headers ?? {};
|
|
21
|
+
this.body = metadata.body ?? metadata.responseBody ?? metadata.received;
|
|
22
|
+
this.responseBody = this.body;
|
|
23
|
+
this.retryAfter = metadata.retryAfter ?? (metadata.retryAfterMs !== void 0 ? Math.max(0, Math.ceil(metadata.retryAfterMs / 1e3)) : null);
|
|
24
|
+
this.retryAfterMs = metadata.retryAfterMs ?? (this.retryAfter !== null ? this.retryAfter * 1e3 : void 0);
|
|
25
|
+
this.requestId = metadata.requestId;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var ClientError = class extends ApiError {
|
|
29
|
+
constructor(message, metadata) {
|
|
30
|
+
super(message, metadata);
|
|
31
|
+
this.name = "ClientError";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var RateLimitError = class extends ApiError {
|
|
35
|
+
constructor(message, metadata) {
|
|
36
|
+
super(message, metadata);
|
|
37
|
+
this.name = "RateLimitError";
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var TimeoutError = class extends ApiError {
|
|
41
|
+
constructor(message, metadata) {
|
|
42
|
+
super(message, metadata);
|
|
43
|
+
this.name = "TimeoutError";
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var NetworkError = class extends ApiError {
|
|
47
|
+
constructor(message, metadata) {
|
|
48
|
+
super(message, metadata);
|
|
49
|
+
this.name = "NetworkError";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var ValidationError = class extends ApiError {
|
|
53
|
+
constructor(message, metadata) {
|
|
54
|
+
super(message, metadata);
|
|
55
|
+
this.name = "ValidationError";
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var ResponseValidationError = class extends ValidationError {
|
|
59
|
+
expectedSchema;
|
|
60
|
+
actualPayload;
|
|
61
|
+
constructor(message, metadata) {
|
|
62
|
+
super(message, metadata);
|
|
63
|
+
this.name = "ResponseValidationError";
|
|
64
|
+
this.expectedSchema = metadata.expectedSchema;
|
|
65
|
+
this.actualPayload = metadata.actualPayload;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// src/observability/dispatcher.ts
|
|
70
|
+
function toArray(value) {
|
|
71
|
+
if (value === void 0) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
if (Array.isArray(value)) {
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
return [value];
|
|
78
|
+
}
|
|
79
|
+
var ObservabilityDispatcher = class {
|
|
80
|
+
listeners;
|
|
81
|
+
metrics;
|
|
82
|
+
constructor(listeners, metrics) {
|
|
83
|
+
this.listeners = {
|
|
84
|
+
requestStart: toArray(listeners?.requestStart),
|
|
85
|
+
requestEnd: toArray(listeners?.requestEnd),
|
|
86
|
+
requestError: toArray(listeners?.requestError)
|
|
87
|
+
};
|
|
88
|
+
this.metrics = toArray(metrics);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Emit a request-start event.
|
|
92
|
+
*/
|
|
93
|
+
async emitRequestStart(event) {
|
|
94
|
+
for (const listener of this.listeners.requestStart) {
|
|
95
|
+
await listener(event);
|
|
96
|
+
}
|
|
97
|
+
for (const adapter of this.metrics) {
|
|
98
|
+
adapter.onRequestStart?.(event);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Emit a request-end event.
|
|
103
|
+
*/
|
|
104
|
+
async emitRequestEnd(event) {
|
|
105
|
+
for (const listener of this.listeners.requestEnd) {
|
|
106
|
+
await listener(event);
|
|
107
|
+
}
|
|
108
|
+
for (const adapter of this.metrics) {
|
|
109
|
+
adapter.onRequestEnd?.(event);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Emit a request-error event.
|
|
114
|
+
*/
|
|
115
|
+
async emitRequestError(event) {
|
|
116
|
+
for (const listener of this.listeners.requestError) {
|
|
117
|
+
await listener(event);
|
|
118
|
+
}
|
|
119
|
+
for (const adapter of this.metrics) {
|
|
120
|
+
adapter.onRequestError?.(event);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// src/utils/abort.ts
|
|
126
|
+
function createRequestSignal(timeoutMs, externalSignal) {
|
|
127
|
+
const controller = new AbortController();
|
|
128
|
+
let timeoutId;
|
|
129
|
+
let didTimeout = false;
|
|
130
|
+
const onExternalAbort = () => {
|
|
131
|
+
controller.abort(externalSignal?.reason);
|
|
132
|
+
};
|
|
133
|
+
if (externalSignal) {
|
|
134
|
+
if (externalSignal.aborted) {
|
|
135
|
+
controller.abort(externalSignal.reason);
|
|
136
|
+
} else {
|
|
137
|
+
externalSignal.addEventListener("abort", onExternalAbort, { once: true });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (timeoutMs > 0) {
|
|
141
|
+
timeoutId = setTimeout(() => {
|
|
142
|
+
didTimeout = true;
|
|
143
|
+
controller.abort(new Error("Request timed out"));
|
|
144
|
+
}, timeoutMs);
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
signal: controller.signal,
|
|
148
|
+
cleanup: () => {
|
|
149
|
+
if (timeoutId) {
|
|
150
|
+
clearTimeout(timeoutId);
|
|
151
|
+
}
|
|
152
|
+
externalSignal?.removeEventListener("abort", onExternalAbort);
|
|
153
|
+
},
|
|
154
|
+
timedOut: () => didTimeout
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/utils/cache-key.ts
|
|
159
|
+
var DEFAULT_CACHE_HEADER_WHITELIST = ["accept-language"];
|
|
160
|
+
function normalizeHeaderName(name) {
|
|
161
|
+
return name.toLowerCase().trim();
|
|
162
|
+
}
|
|
163
|
+
function serializeBodyValue(value, seen) {
|
|
164
|
+
if (value === null) {
|
|
165
|
+
return "null";
|
|
166
|
+
}
|
|
167
|
+
const valueType = typeof value;
|
|
168
|
+
if (valueType === "string") {
|
|
169
|
+
return JSON.stringify(value);
|
|
170
|
+
}
|
|
171
|
+
if (valueType === "number" || valueType === "boolean") {
|
|
172
|
+
return String(value);
|
|
173
|
+
}
|
|
174
|
+
if (valueType === "undefined") {
|
|
175
|
+
return "undefined";
|
|
176
|
+
}
|
|
177
|
+
if (valueType === "bigint") {
|
|
178
|
+
return `${String(value)}n`;
|
|
179
|
+
}
|
|
180
|
+
if (value instanceof Date) {
|
|
181
|
+
return `date:${value.toISOString()}`;
|
|
182
|
+
}
|
|
183
|
+
if (value instanceof URLSearchParams) {
|
|
184
|
+
const sorted = [...value.entries()].sort(([leftKey, leftValue], [rightKey, rightValue]) => {
|
|
185
|
+
if (leftKey === rightKey) {
|
|
186
|
+
return leftValue.localeCompare(rightValue);
|
|
187
|
+
}
|
|
188
|
+
return leftKey.localeCompare(rightKey);
|
|
189
|
+
}).map(([key, entryValue]) => `${encodeURIComponent(key)}=${encodeURIComponent(entryValue)}`).join("&");
|
|
190
|
+
return `params:${sorted}`;
|
|
191
|
+
}
|
|
192
|
+
if (value instanceof ArrayBuffer) {
|
|
193
|
+
return `buffer:${Array.from(new Uint8Array(value)).join(".")}`;
|
|
194
|
+
}
|
|
195
|
+
if (ArrayBuffer.isView(value)) {
|
|
196
|
+
const view = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
197
|
+
return `view:${Array.from(view).join(".")}`;
|
|
198
|
+
}
|
|
199
|
+
if (Array.isArray(value)) {
|
|
200
|
+
if (seen.has(value)) {
|
|
201
|
+
return "[circular]";
|
|
202
|
+
}
|
|
203
|
+
seen.add(value);
|
|
204
|
+
const serialized = `[${value.map((entry) => serializeBodyValue(entry, seen)).join(",")}]`;
|
|
205
|
+
seen.delete(value);
|
|
206
|
+
return serialized;
|
|
207
|
+
}
|
|
208
|
+
if (typeof value === "object") {
|
|
209
|
+
if (seen.has(value)) {
|
|
210
|
+
return "{circular}";
|
|
211
|
+
}
|
|
212
|
+
seen.add(value);
|
|
213
|
+
const record = value;
|
|
214
|
+
const serialized = `{${Object.keys(record).sort((left, right) => left.localeCompare(right)).map((key) => `${JSON.stringify(key)}:${serializeBodyValue(record[key], seen)}`).join(",")}}`;
|
|
215
|
+
seen.delete(value);
|
|
216
|
+
return serialized;
|
|
217
|
+
}
|
|
218
|
+
return JSON.stringify(String(value));
|
|
219
|
+
}
|
|
220
|
+
function hashString(input) {
|
|
221
|
+
let hash = 2166136261;
|
|
222
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
223
|
+
hash ^= input.charCodeAt(index);
|
|
224
|
+
hash = Math.imul(hash, 16777619);
|
|
225
|
+
}
|
|
226
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
227
|
+
}
|
|
228
|
+
function createCacheKey(request, whitelistHeaders = DEFAULT_CACHE_HEADER_WHITELIST) {
|
|
229
|
+
const url = new URL(request.url);
|
|
230
|
+
const method = String(request.method).toUpperCase();
|
|
231
|
+
const query = [...url.searchParams.entries()].sort(([leftKey, leftValue], [rightKey, rightValue]) => {
|
|
232
|
+
if (leftKey === rightKey) {
|
|
233
|
+
return leftValue.localeCompare(rightValue);
|
|
234
|
+
}
|
|
235
|
+
return leftKey.localeCompare(rightKey);
|
|
236
|
+
}).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
|
|
237
|
+
const allowed = new Set(whitelistHeaders.map((header) => normalizeHeaderName(header)));
|
|
238
|
+
const requestHeaders = new Headers(request.headers ?? {});
|
|
239
|
+
const relevantHeaders = [...requestHeaders.entries()].filter(([name]) => allowed.has(normalizeHeaderName(name))).sort(([left], [right]) => left.localeCompare(right)).map(([name, value]) => `${normalizeHeaderName(name)}:${value}`).join("|");
|
|
240
|
+
const serializedBody = request.body === void 0 ? "" : serializeBodyValue(request.body, /* @__PURE__ */ new Set());
|
|
241
|
+
const bodyHash = hashString(serializedBody);
|
|
242
|
+
return `${method}|${url.pathname}|${query}|${bodyHash}|${relevantHeaders}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/utils/headers.ts
|
|
246
|
+
function mergeHeaders(defaultHeaders, override) {
|
|
247
|
+
const headers = new Headers(defaultHeaders);
|
|
248
|
+
if (!override) {
|
|
249
|
+
return headers;
|
|
250
|
+
}
|
|
251
|
+
const overrideHeaders = new Headers(override);
|
|
252
|
+
overrideHeaders.forEach((value, key) => {
|
|
253
|
+
headers.set(key, value);
|
|
254
|
+
});
|
|
255
|
+
return headers;
|
|
256
|
+
}
|
|
257
|
+
function toSafeHeaderObject(headers) {
|
|
258
|
+
const entries = {};
|
|
259
|
+
headers.forEach((value, key) => {
|
|
260
|
+
const normalizedKey = key.toLowerCase();
|
|
261
|
+
if (normalizedKey === "authorization") {
|
|
262
|
+
entries[key] = "[REDACTED]";
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
entries[key] = value;
|
|
266
|
+
});
|
|
267
|
+
return entries;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/utils/hooks.ts
|
|
271
|
+
function asArray(hook) {
|
|
272
|
+
if (hook === void 0) {
|
|
273
|
+
return [];
|
|
274
|
+
}
|
|
275
|
+
if (Array.isArray(hook)) {
|
|
276
|
+
return hook;
|
|
277
|
+
}
|
|
278
|
+
return [hook];
|
|
279
|
+
}
|
|
280
|
+
function normalizeHooks(hooks) {
|
|
281
|
+
return {
|
|
282
|
+
beforeRequest: asArray(hooks?.beforeRequest),
|
|
283
|
+
afterResponse: asArray(hooks?.afterResponse),
|
|
284
|
+
onError: asArray(hooks?.onError),
|
|
285
|
+
onRateLimit: asArray(hooks?.onRateLimit)
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
function mergeHooks(globalHooks, requestHooks) {
|
|
289
|
+
const localHooks = normalizeHooks(requestHooks);
|
|
290
|
+
return {
|
|
291
|
+
beforeRequest: [...globalHooks.beforeRequest, ...localHooks.beforeRequest],
|
|
292
|
+
afterResponse: [...globalHooks.afterResponse, ...localHooks.afterResponse],
|
|
293
|
+
onError: [...globalHooks.onError, ...localHooks.onError],
|
|
294
|
+
onRateLimit: [...globalHooks.onRateLimit, ...localHooks.onRateLimit]
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/utils/lru-cache.ts
|
|
299
|
+
var LruTtlCache = class {
|
|
300
|
+
constructor(maxSize) {
|
|
301
|
+
this.maxSize = maxSize;
|
|
302
|
+
if (!Number.isInteger(maxSize) || maxSize <= 0) {
|
|
303
|
+
throw new Error("Cache maxSize must be a positive integer.");
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
entries = /* @__PURE__ */ new Map();
|
|
307
|
+
/**
|
|
308
|
+
* Read an entry and refresh LRU order.
|
|
309
|
+
*/
|
|
310
|
+
get(key, nowMs = Date.now()) {
|
|
311
|
+
const entry = this.entries.get(key);
|
|
312
|
+
if (!entry) {
|
|
313
|
+
return void 0;
|
|
314
|
+
}
|
|
315
|
+
if (entry.expiresAt <= nowMs) {
|
|
316
|
+
this.entries.delete(key);
|
|
317
|
+
return void 0;
|
|
318
|
+
}
|
|
319
|
+
this.entries.delete(key);
|
|
320
|
+
this.entries.set(key, entry);
|
|
321
|
+
return entry.value;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Store an entry with a dedicated TTL.
|
|
325
|
+
*/
|
|
326
|
+
set(key, value, ttlMs, nowMs = Date.now()) {
|
|
327
|
+
if (ttlMs <= 0) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (this.entries.has(key)) {
|
|
331
|
+
this.entries.delete(key);
|
|
332
|
+
}
|
|
333
|
+
this.entries.set(key, {
|
|
334
|
+
value,
|
|
335
|
+
expiresAt: nowMs + ttlMs
|
|
336
|
+
});
|
|
337
|
+
this.evictOverflow();
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Delete a single cache entry.
|
|
341
|
+
*/
|
|
342
|
+
delete(key) {
|
|
343
|
+
return this.entries.delete(key);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Delete entries that match a predicate.
|
|
347
|
+
*/
|
|
348
|
+
deleteWhere(predicate) {
|
|
349
|
+
let removed = 0;
|
|
350
|
+
for (const [key, entry] of this.entries.entries()) {
|
|
351
|
+
if (predicate(key, entry.value)) {
|
|
352
|
+
this.entries.delete(key);
|
|
353
|
+
removed += 1;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return removed;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Current cache size.
|
|
360
|
+
*/
|
|
361
|
+
size() {
|
|
362
|
+
return this.entries.size;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Clear all cached entries.
|
|
366
|
+
*/
|
|
367
|
+
clear() {
|
|
368
|
+
this.entries.clear();
|
|
369
|
+
}
|
|
370
|
+
evictOverflow() {
|
|
371
|
+
while (this.entries.size > this.maxSize) {
|
|
372
|
+
const iteratorResult = this.entries.keys().next();
|
|
373
|
+
if (iteratorResult.done) {
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
this.entries.delete(iteratorResult.value);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// src/utils/rate-limit.ts
|
|
382
|
+
var RETRY_AFTER_HEADERS = ["retry-after"];
|
|
383
|
+
var RESET_AFTER_HEADERS = [
|
|
384
|
+
"x-ratelimit-reset-after",
|
|
385
|
+
"x-rate-limit-reset-after",
|
|
386
|
+
"ratelimit-reset-after"
|
|
387
|
+
];
|
|
388
|
+
var RESET_HEADERS = [
|
|
389
|
+
"x-ratelimit-reset",
|
|
390
|
+
"x-rate-limit-reset",
|
|
391
|
+
"ratelimit-reset"
|
|
392
|
+
];
|
|
393
|
+
var REMAINING_HEADERS = [
|
|
394
|
+
"x-ratelimit-remaining",
|
|
395
|
+
"x-rate-limit-remaining",
|
|
396
|
+
"ratelimit-remaining"
|
|
397
|
+
];
|
|
398
|
+
var SCOPE_HEADERS = [
|
|
399
|
+
"x-ratelimit-scope",
|
|
400
|
+
"x-rate-limit-scope",
|
|
401
|
+
"ratelimit-scope"
|
|
402
|
+
];
|
|
403
|
+
function normalizeHeaderName2(name) {
|
|
404
|
+
return name.toLowerCase().replace(/[^a-z0-9]/gu, "");
|
|
405
|
+
}
|
|
406
|
+
function getHeaderValue(headers, candidates) {
|
|
407
|
+
for (const candidate of candidates) {
|
|
408
|
+
const value = headers.get(candidate);
|
|
409
|
+
if (value !== null) {
|
|
410
|
+
return value;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const candidateNames = new Set(candidates.map((candidate) => normalizeHeaderName2(candidate)));
|
|
414
|
+
for (const [name, value] of headers.entries()) {
|
|
415
|
+
if (candidateNames.has(normalizeHeaderName2(name))) {
|
|
416
|
+
return value;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
function parseRetryAfterSeconds(retryAfterHeader, nowMs = Date.now()) {
|
|
422
|
+
if (!retryAfterHeader) {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
const normalized = retryAfterHeader.trim();
|
|
426
|
+
if (normalized.length === 0) {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
const numericSeconds = Number(normalized);
|
|
430
|
+
if (Number.isFinite(numericSeconds)) {
|
|
431
|
+
return Math.max(0, Math.ceil(numericSeconds));
|
|
432
|
+
}
|
|
433
|
+
const dateValue = Date.parse(normalized);
|
|
434
|
+
if (Number.isNaN(dateValue)) {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
return Math.max(0, Math.ceil((dateValue - nowMs) / 1e3));
|
|
438
|
+
}
|
|
439
|
+
function parseRetryAfterMs(retryAfterHeader, nowMs = Date.now()) {
|
|
440
|
+
const retryAfterSeconds = parseRetryAfterSeconds(retryAfterHeader, nowMs);
|
|
441
|
+
if (retryAfterSeconds === null) {
|
|
442
|
+
return void 0;
|
|
443
|
+
}
|
|
444
|
+
return retryAfterSeconds * 1e3;
|
|
445
|
+
}
|
|
446
|
+
function parseRateLimitResetMs(resetHeader, nowMs = Date.now()) {
|
|
447
|
+
if (!resetHeader) {
|
|
448
|
+
return void 0;
|
|
449
|
+
}
|
|
450
|
+
const numeric = Number(resetHeader.trim());
|
|
451
|
+
if (!Number.isFinite(numeric)) {
|
|
452
|
+
return void 0;
|
|
453
|
+
}
|
|
454
|
+
if (numeric > 1e12) {
|
|
455
|
+
return Math.max(0, Math.round(numeric - nowMs));
|
|
456
|
+
}
|
|
457
|
+
if (numeric > 1e9) {
|
|
458
|
+
return Math.max(0, Math.round(numeric * 1e3 - nowMs));
|
|
459
|
+
}
|
|
460
|
+
return Math.max(0, Math.round(numeric * 1e3));
|
|
461
|
+
}
|
|
462
|
+
function parseRateLimitDelayMs(headers, nowMs = Date.now()) {
|
|
463
|
+
const retryAfterMs = parseRetryAfterMs(getHeaderValue(headers, RETRY_AFTER_HEADERS), nowMs);
|
|
464
|
+
const resetAfterMs = parseRateLimitResetMs(getHeaderValue(headers, RESET_AFTER_HEADERS), nowMs);
|
|
465
|
+
const resetMs = parseRateLimitResetMs(getHeaderValue(headers, RESET_HEADERS), nowMs);
|
|
466
|
+
const values = [retryAfterMs, resetAfterMs, resetMs].filter(
|
|
467
|
+
(value) => value !== void 0
|
|
468
|
+
);
|
|
469
|
+
if (values.length === 0) {
|
|
470
|
+
return void 0;
|
|
471
|
+
}
|
|
472
|
+
return Math.max(...values);
|
|
473
|
+
}
|
|
474
|
+
function extractRetryAfterSeconds(headers, nowMs = Date.now()) {
|
|
475
|
+
return parseRetryAfterSeconds(getHeaderValue(headers, RETRY_AFTER_HEADERS), nowMs);
|
|
476
|
+
}
|
|
477
|
+
function isRateLimitExhausted(headers) {
|
|
478
|
+
const remaining = getHeaderValue(headers, REMAINING_HEADERS);
|
|
479
|
+
if (remaining === null) {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
const numeric = Number(remaining);
|
|
483
|
+
if (!Number.isFinite(numeric)) {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
return numeric <= 0;
|
|
487
|
+
}
|
|
488
|
+
function parseRateLimitScope(headers) {
|
|
489
|
+
const scope = getHeaderValue(headers, SCOPE_HEADERS)?.toLowerCase() ?? "";
|
|
490
|
+
if (scope.includes("global")) {
|
|
491
|
+
return "global";
|
|
492
|
+
}
|
|
493
|
+
return "route";
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// src/utils/retry.ts
|
|
497
|
+
var DEFAULT_RETRY_OPTIONS = {
|
|
498
|
+
maxAttempts: 4,
|
|
499
|
+
baseDelayMs: 100,
|
|
500
|
+
maxDelayMs: 1e4,
|
|
501
|
+
backoffFactor: 2,
|
|
502
|
+
jitter: "full",
|
|
503
|
+
retryableStatusCodes: [429, 500, 502, 503, 504]
|
|
504
|
+
};
|
|
505
|
+
function resolveRetryOptions(input) {
|
|
506
|
+
if (typeof input === "number") {
|
|
507
|
+
return {
|
|
508
|
+
...DEFAULT_RETRY_OPTIONS,
|
|
509
|
+
maxAttempts: Math.max(1, Math.floor(input))
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
if (!input) {
|
|
513
|
+
return DEFAULT_RETRY_OPTIONS;
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
maxAttempts: Math.max(1, Math.floor(input.maxAttempts ?? DEFAULT_RETRY_OPTIONS.maxAttempts)),
|
|
517
|
+
baseDelayMs: Math.max(0, Math.floor(input.baseDelayMs ?? DEFAULT_RETRY_OPTIONS.baseDelayMs)),
|
|
518
|
+
maxDelayMs: Math.max(0, Math.floor(input.maxDelayMs ?? DEFAULT_RETRY_OPTIONS.maxDelayMs)),
|
|
519
|
+
backoffFactor: Math.max(1, input.backoffFactor ?? DEFAULT_RETRY_OPTIONS.backoffFactor),
|
|
520
|
+
jitter: input.jitter ?? DEFAULT_RETRY_OPTIONS.jitter,
|
|
521
|
+
retryableStatusCodes: input.retryableStatusCodes ?? DEFAULT_RETRY_OPTIONS.retryableStatusCodes
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function isRetryableStatus(statusCode, options) {
|
|
525
|
+
return options.retryableStatusCodes.includes(statusCode);
|
|
526
|
+
}
|
|
527
|
+
function computeRetryDelayMs(retryNumber, options, randomValue, minimumDelayMs) {
|
|
528
|
+
const exponent = Math.max(0, retryNumber - 1);
|
|
529
|
+
const cappedDelay = Math.min(
|
|
530
|
+
options.baseDelayMs * options.backoffFactor ** exponent,
|
|
531
|
+
options.maxDelayMs
|
|
532
|
+
);
|
|
533
|
+
const boundedRandom = Math.min(1, Math.max(0, randomValue));
|
|
534
|
+
const delay = options.jitter === "full" ? Math.round(boundedRandom * cappedDelay) : Math.round(cappedDelay);
|
|
535
|
+
if (minimumDelayMs === void 0) {
|
|
536
|
+
return delay;
|
|
537
|
+
}
|
|
538
|
+
return Math.max(delay, minimumDelayMs);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// src/utils/semaphore.ts
|
|
542
|
+
var Semaphore = class {
|
|
543
|
+
constructor(capacity) {
|
|
544
|
+
this.capacity = capacity;
|
|
545
|
+
if (!Number.isInteger(capacity) || capacity <= 0) {
|
|
546
|
+
throw new Error("Semaphore capacity must be a positive integer.");
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
queue = [];
|
|
550
|
+
inUse = 0;
|
|
551
|
+
/**
|
|
552
|
+
* Acquire one slot and return a release function.
|
|
553
|
+
*/
|
|
554
|
+
async acquire() {
|
|
555
|
+
if (this.inUse < this.capacity) {
|
|
556
|
+
this.inUse += 1;
|
|
557
|
+
return () => {
|
|
558
|
+
this.release();
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
await new Promise((resolve) => {
|
|
562
|
+
this.queue.push(resolve);
|
|
563
|
+
});
|
|
564
|
+
this.inUse += 1;
|
|
565
|
+
return () => {
|
|
566
|
+
this.release();
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
release() {
|
|
570
|
+
this.inUse = Math.max(0, this.inUse - 1);
|
|
571
|
+
const next = this.queue.shift();
|
|
572
|
+
if (next) {
|
|
573
|
+
next();
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
// src/utils/sleep.ts
|
|
579
|
+
function sleep(delayMs) {
|
|
580
|
+
return new Promise((resolve) => {
|
|
581
|
+
setTimeout(resolve, delayMs);
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// src/utils/url.ts
|
|
586
|
+
function buildUrl(baseUrl, apiPath, query) {
|
|
587
|
+
const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
588
|
+
const path = apiPath.startsWith("/") ? apiPath : `/${apiPath}`;
|
|
589
|
+
const url = new URL(`${base}${path}`);
|
|
590
|
+
if (!query) {
|
|
591
|
+
return url.toString();
|
|
592
|
+
}
|
|
593
|
+
const queryEntries = Object.entries(query).filter((entry) => entry[1] !== void 0).sort(([left], [right]) => left.localeCompare(right));
|
|
594
|
+
for (const [key, value] of queryEntries) {
|
|
595
|
+
if (value === void 0) {
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
url.searchParams.set(key, String(value));
|
|
599
|
+
}
|
|
600
|
+
return url.toString();
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// src/internal/http-client.ts
|
|
604
|
+
var DEFAULT_TIMEOUT_MS = 12e3;
|
|
605
|
+
var DEFAULT_CONCURRENCY_LIMIT = 8;
|
|
606
|
+
var HttpClient = class {
|
|
607
|
+
fetchFn;
|
|
608
|
+
baseUrl;
|
|
609
|
+
timeoutMs;
|
|
610
|
+
retryOptions;
|
|
611
|
+
validationMode;
|
|
612
|
+
logger;
|
|
613
|
+
defaultHeaders;
|
|
614
|
+
hooks;
|
|
615
|
+
observability;
|
|
616
|
+
cacheSettings;
|
|
617
|
+
cacheKeyHeaders;
|
|
618
|
+
cache;
|
|
619
|
+
semaphore;
|
|
620
|
+
globalRateLimitUntilMs = 0;
|
|
621
|
+
routeRateLimitUntilMs = /* @__PURE__ */ new Map();
|
|
622
|
+
endpointCacheIndex = /* @__PURE__ */ new Map();
|
|
623
|
+
requestCounter = 0;
|
|
624
|
+
constructor(options) {
|
|
625
|
+
const token = options.token.trim();
|
|
626
|
+
if (!token) {
|
|
627
|
+
throw new Error("BrawlStarsClient requires a non-empty token.");
|
|
628
|
+
}
|
|
629
|
+
this.fetchFn = options.fetch ?? globalThis.fetch;
|
|
630
|
+
if (!this.fetchFn) {
|
|
631
|
+
throw new Error("No fetch implementation available. Provide options.fetch.");
|
|
632
|
+
}
|
|
633
|
+
this.baseUrl = (options.baseUrl ?? "https://api.brawlstars.com/v1").replace(/\/+$/u, "");
|
|
634
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
635
|
+
this.retryOptions = resolveRetryOptions(options.retries);
|
|
636
|
+
this.validationMode = this.resolveValidationMode(options.validation, options.validateResponses);
|
|
637
|
+
this.logger = options.logger ?? console;
|
|
638
|
+
this.defaultHeaders = new Headers({
|
|
639
|
+
Authorization: `Bearer ${token}`,
|
|
640
|
+
Accept: "application/json"
|
|
641
|
+
});
|
|
642
|
+
this.hooks = normalizeHooks(options.hooks);
|
|
643
|
+
this.observability = new ObservabilityDispatcher(
|
|
644
|
+
options.observability?.listeners,
|
|
645
|
+
options.observability?.metrics
|
|
646
|
+
);
|
|
647
|
+
this.cacheSettings = this.resolveCacheSettings(options.cache);
|
|
648
|
+
this.cacheKeyHeaders = options.cacheKeyHeaders ?? DEFAULT_CACHE_HEADER_WHITELIST;
|
|
649
|
+
this.cache = this.cacheSettings.enabled ? new LruTtlCache(this.cacheSettings.maxSize) : null;
|
|
650
|
+
const concurrency = options.concurrencyLimit ?? DEFAULT_CONCURRENCY_LIMIT;
|
|
651
|
+
this.semaphore = new Semaphore(concurrency);
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Execute a typed request against the API.
|
|
655
|
+
*/
|
|
656
|
+
async request(config) {
|
|
657
|
+
return this.executeRequest(config);
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Clear all in-memory cached responses.
|
|
661
|
+
*/
|
|
662
|
+
clearCache() {
|
|
663
|
+
this.cache?.clear();
|
|
664
|
+
this.endpointCacheIndex.clear();
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Invalidate cached responses for a given endpoint, or all endpoints when omitted.
|
|
668
|
+
*/
|
|
669
|
+
invalidateCache(endpoint) {
|
|
670
|
+
if (!this.cache) {
|
|
671
|
+
return 0;
|
|
672
|
+
}
|
|
673
|
+
if (!endpoint) {
|
|
674
|
+
const sizeBefore = this.cache.size();
|
|
675
|
+
this.cache.clear();
|
|
676
|
+
this.endpointCacheIndex.clear();
|
|
677
|
+
return sizeBefore;
|
|
678
|
+
}
|
|
679
|
+
const keys = this.endpointCacheIndex.get(endpoint);
|
|
680
|
+
if (!keys) {
|
|
681
|
+
return 0;
|
|
682
|
+
}
|
|
683
|
+
let removed = 0;
|
|
684
|
+
for (const key of keys) {
|
|
685
|
+
if (this.cache.delete(key)) {
|
|
686
|
+
removed += 1;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
this.endpointCacheIndex.delete(endpoint);
|
|
690
|
+
return removed;
|
|
691
|
+
}
|
|
692
|
+
async executeRequest(config) {
|
|
693
|
+
const retryOptions = resolveRetryOptions(config.options?.retries ?? this.retryOptions);
|
|
694
|
+
const timeoutMs = config.options?.timeoutMs ?? this.timeoutMs;
|
|
695
|
+
const validationMode = this.resolveValidationMode(
|
|
696
|
+
config.options?.validation,
|
|
697
|
+
config.options?.validateResponse,
|
|
698
|
+
this.validationMode
|
|
699
|
+
);
|
|
700
|
+
const hooks = mergeHooks(this.hooks, config.options?.hooks);
|
|
701
|
+
const url = buildUrl(this.baseUrl, config.path, config.query);
|
|
702
|
+
const headers = mergeHeaders(this.defaultHeaders, config.options?.headers);
|
|
703
|
+
const safeRequestHeaders = toSafeHeaderObject(headers);
|
|
704
|
+
const requestMetadata = this.createRequestMetadata(config.method, url, config.options, safeRequestHeaders);
|
|
705
|
+
const requestId = this.createRequestId();
|
|
706
|
+
const routeKey = config.endpoint;
|
|
707
|
+
const cachePolicy = this.resolveCachePolicy(config.endpoint, config.options?.cache);
|
|
708
|
+
const cacheKey = createCacheKey({
|
|
709
|
+
method: config.method,
|
|
710
|
+
url,
|
|
711
|
+
headers
|
|
712
|
+
}, this.cacheKeyHeaders);
|
|
713
|
+
const canUseCache = cachePolicy.enabled && this.cache && this.isCacheableMethod(config.method);
|
|
714
|
+
if (canUseCache) {
|
|
715
|
+
const cached = this.cache.get(cacheKey);
|
|
716
|
+
if (cached !== void 0) {
|
|
717
|
+
return cached;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
for (let attempt = 1; attempt <= retryOptions.maxAttempts; attempt += 1) {
|
|
721
|
+
await this.waitForRateLimitWindow(routeKey);
|
|
722
|
+
const release = await this.semaphore.acquire();
|
|
723
|
+
const startedAt = Date.now();
|
|
724
|
+
const { signal, cleanup, timedOut } = createRequestSignal(timeoutMs, config.options?.signal);
|
|
725
|
+
let emittedAttemptError = false;
|
|
726
|
+
try {
|
|
727
|
+
await this.observability.emitRequestStart({
|
|
728
|
+
requestId,
|
|
729
|
+
endpoint: config.endpoint,
|
|
730
|
+
method: config.method,
|
|
731
|
+
url,
|
|
732
|
+
attempt
|
|
733
|
+
});
|
|
734
|
+
for (const hook of hooks.beforeRequest) {
|
|
735
|
+
await hook({
|
|
736
|
+
endpoint: config.endpoint,
|
|
737
|
+
method: config.method,
|
|
738
|
+
url,
|
|
739
|
+
attempt,
|
|
740
|
+
requestId,
|
|
741
|
+
headers: safeRequestHeaders
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
const response = await this.fetchFn(url, {
|
|
745
|
+
method: config.method,
|
|
746
|
+
headers,
|
|
747
|
+
signal
|
|
748
|
+
});
|
|
749
|
+
const durationMs = Date.now() - startedAt;
|
|
750
|
+
const safeResponseHeaders = toSafeHeaderObject(response.headers);
|
|
751
|
+
await this.observability.emitRequestEnd({
|
|
752
|
+
requestId,
|
|
753
|
+
endpoint: config.endpoint,
|
|
754
|
+
method: config.method,
|
|
755
|
+
url,
|
|
756
|
+
attempt,
|
|
757
|
+
statusCode: response.status,
|
|
758
|
+
durationMs
|
|
759
|
+
});
|
|
760
|
+
for (const hook of hooks.afterResponse) {
|
|
761
|
+
await hook({
|
|
762
|
+
endpoint: config.endpoint,
|
|
763
|
+
method: config.method,
|
|
764
|
+
url,
|
|
765
|
+
attempt,
|
|
766
|
+
requestId,
|
|
767
|
+
durationMs,
|
|
768
|
+
statusCode: response.status,
|
|
769
|
+
headers: safeResponseHeaders
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
this.updateRateLimitState(response.headers, response.status, routeKey);
|
|
773
|
+
if (response.ok) {
|
|
774
|
+
const payload = await this.parseResponseBody(response);
|
|
775
|
+
const shouldValidate = validationMode === "strict" || validationMode === "warn";
|
|
776
|
+
let parsed;
|
|
777
|
+
try {
|
|
778
|
+
parsed = config.parser(payload, shouldValidate);
|
|
779
|
+
} catch (error2) {
|
|
780
|
+
if (validationMode === "off") {
|
|
781
|
+
parsed = payload;
|
|
782
|
+
} else if (validationMode === "warn") {
|
|
783
|
+
this.logger.warn(
|
|
784
|
+
`[brawlstars-client] response validation warning (${config.endpoint}): ${error2.message}`
|
|
785
|
+
);
|
|
786
|
+
parsed = payload;
|
|
787
|
+
} else {
|
|
788
|
+
throw new ResponseValidationError("Response schema validation failed.", {
|
|
789
|
+
endpoint: config.endpoint,
|
|
790
|
+
method: config.method,
|
|
791
|
+
statusCode: response.status,
|
|
792
|
+
headers: safeResponseHeaders,
|
|
793
|
+
request: requestMetadata,
|
|
794
|
+
expectedSchema: config.endpoint,
|
|
795
|
+
actualPayload: payload,
|
|
796
|
+
received: payload,
|
|
797
|
+
requestId,
|
|
798
|
+
cause: error2
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
if (canUseCache) {
|
|
803
|
+
this.cache.set(cacheKey, parsed, cachePolicy.ttlMs);
|
|
804
|
+
this.trackEndpointCacheKey(config.endpoint, cacheKey);
|
|
805
|
+
}
|
|
806
|
+
cleanup();
|
|
807
|
+
return parsed;
|
|
808
|
+
}
|
|
809
|
+
const errorBody = await this.parseResponseBody(response);
|
|
810
|
+
const retryAfter = extractRetryAfterSeconds(response.headers);
|
|
811
|
+
const requestHeaderId = response.headers.get("x-request-id") ?? void 0;
|
|
812
|
+
const canRetry = attempt < retryOptions.maxAttempts && isRetryableStatus(response.status, retryOptions);
|
|
813
|
+
const error = this.createApiError(
|
|
814
|
+
config.endpoint,
|
|
815
|
+
config.method,
|
|
816
|
+
url,
|
|
817
|
+
config.options,
|
|
818
|
+
safeRequestHeaders,
|
|
819
|
+
response.status,
|
|
820
|
+
safeResponseHeaders,
|
|
821
|
+
errorBody,
|
|
822
|
+
retryAfter,
|
|
823
|
+
requestHeaderId
|
|
824
|
+
);
|
|
825
|
+
if (response.status === 429) {
|
|
826
|
+
for (const hook of hooks.onRateLimit) {
|
|
827
|
+
await hook({
|
|
828
|
+
endpoint: config.endpoint,
|
|
829
|
+
method: config.method,
|
|
830
|
+
url,
|
|
831
|
+
attempt,
|
|
832
|
+
requestId,
|
|
833
|
+
statusCode: response.status,
|
|
834
|
+
retryAfter,
|
|
835
|
+
headers: safeResponseHeaders
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
await this.emitError(hooks, {
|
|
840
|
+
endpoint: config.endpoint,
|
|
841
|
+
method: config.method,
|
|
842
|
+
url,
|
|
843
|
+
attempt,
|
|
844
|
+
requestId,
|
|
845
|
+
durationMs,
|
|
846
|
+
willRetry: canRetry,
|
|
847
|
+
error
|
|
848
|
+
});
|
|
849
|
+
emittedAttemptError = true;
|
|
850
|
+
cleanup();
|
|
851
|
+
if (!canRetry) {
|
|
852
|
+
throw error;
|
|
853
|
+
}
|
|
854
|
+
const rateLimitDelayMs = parseRateLimitDelayMs(response.headers);
|
|
855
|
+
const retryDelayMs = computeRetryDelayMs(
|
|
856
|
+
attempt,
|
|
857
|
+
retryOptions,
|
|
858
|
+
Math.random(),
|
|
859
|
+
rateLimitDelayMs
|
|
860
|
+
);
|
|
861
|
+
if (retryDelayMs > 0) {
|
|
862
|
+
await sleep(retryDelayMs);
|
|
863
|
+
}
|
|
864
|
+
continue;
|
|
865
|
+
} catch (error) {
|
|
866
|
+
cleanup();
|
|
867
|
+
if (error instanceof ApiError) {
|
|
868
|
+
if (!emittedAttemptError) {
|
|
869
|
+
await this.emitError(hooks, {
|
|
870
|
+
endpoint: config.endpoint,
|
|
871
|
+
method: config.method,
|
|
872
|
+
url,
|
|
873
|
+
attempt,
|
|
874
|
+
requestId,
|
|
875
|
+
durationMs: Date.now() - startedAt,
|
|
876
|
+
willRetry: false,
|
|
877
|
+
error
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
throw error;
|
|
881
|
+
}
|
|
882
|
+
const durationMs = Date.now() - startedAt;
|
|
883
|
+
const wasExternallyAborted = Boolean(config.options?.signal?.aborted) && !timedOut();
|
|
884
|
+
const canRetry = attempt < retryOptions.maxAttempts && !wasExternallyAborted;
|
|
885
|
+
const networkError = timedOut() ? new TimeoutError("Request timed out.", {
|
|
886
|
+
endpoint: config.endpoint,
|
|
887
|
+
method: config.method,
|
|
888
|
+
request: requestMetadata,
|
|
889
|
+
headers: {},
|
|
890
|
+
retryAfter: null,
|
|
891
|
+
requestId,
|
|
892
|
+
cause: error
|
|
893
|
+
}) : new NetworkError(
|
|
894
|
+
wasExternallyAborted ? "Request was aborted by caller." : "Network request failed.",
|
|
895
|
+
{
|
|
896
|
+
endpoint: config.endpoint,
|
|
897
|
+
method: config.method,
|
|
898
|
+
request: requestMetadata,
|
|
899
|
+
headers: {},
|
|
900
|
+
retryAfter: null,
|
|
901
|
+
requestId,
|
|
902
|
+
cause: error
|
|
903
|
+
}
|
|
904
|
+
);
|
|
905
|
+
await this.emitError(hooks, {
|
|
906
|
+
endpoint: config.endpoint,
|
|
907
|
+
method: config.method,
|
|
908
|
+
url,
|
|
909
|
+
attempt,
|
|
910
|
+
requestId,
|
|
911
|
+
durationMs,
|
|
912
|
+
willRetry: canRetry,
|
|
913
|
+
error: networkError
|
|
914
|
+
});
|
|
915
|
+
if (!canRetry) {
|
|
916
|
+
throw networkError;
|
|
917
|
+
}
|
|
918
|
+
const retryDelayMs = computeRetryDelayMs(attempt, retryOptions, Math.random());
|
|
919
|
+
if (retryDelayMs > 0) {
|
|
920
|
+
await sleep(retryDelayMs);
|
|
921
|
+
}
|
|
922
|
+
} finally {
|
|
923
|
+
release();
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
throw new ApiError("Request attempts exhausted.", {
|
|
927
|
+
endpoint: config.endpoint,
|
|
928
|
+
method: config.method,
|
|
929
|
+
request: requestMetadata,
|
|
930
|
+
headers: {},
|
|
931
|
+
retryAfter: null,
|
|
932
|
+
requestId
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
createRequestId() {
|
|
936
|
+
this.requestCounter += 1;
|
|
937
|
+
return `${Date.now().toString(36)}-${this.requestCounter.toString(36)}`;
|
|
938
|
+
}
|
|
939
|
+
createApiError(endpoint, method, url, options, safeRequestHeaders, statusCode, headers, body, retryAfter, requestId) {
|
|
940
|
+
const request = this.createRequestMetadata(method, url, options, safeRequestHeaders);
|
|
941
|
+
const metadata = {
|
|
942
|
+
endpoint,
|
|
943
|
+
method,
|
|
944
|
+
request,
|
|
945
|
+
statusCode,
|
|
946
|
+
headers,
|
|
947
|
+
body,
|
|
948
|
+
retryAfter,
|
|
949
|
+
requestId
|
|
950
|
+
};
|
|
951
|
+
if (statusCode === 429) {
|
|
952
|
+
return new RateLimitError("Rate limit exceeded.", metadata);
|
|
953
|
+
}
|
|
954
|
+
if (statusCode >= 400 && statusCode < 500) {
|
|
955
|
+
return new ClientError(`API request failed with status ${statusCode}.`, metadata);
|
|
956
|
+
}
|
|
957
|
+
return new ApiError(`API request failed with status ${statusCode}.`, metadata);
|
|
958
|
+
}
|
|
959
|
+
async emitError(hooks, context) {
|
|
960
|
+
await this.observability.emitRequestError({
|
|
961
|
+
requestId: context.requestId,
|
|
962
|
+
endpoint: context.endpoint,
|
|
963
|
+
method: context.method,
|
|
964
|
+
url: context.url,
|
|
965
|
+
attempt: context.attempt,
|
|
966
|
+
durationMs: context.durationMs,
|
|
967
|
+
willRetry: context.willRetry,
|
|
968
|
+
error: context.error
|
|
969
|
+
});
|
|
970
|
+
for (const hook of hooks.onError) {
|
|
971
|
+
await hook(context);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
resolveValidationMode(mode, deprecatedValidateFlag, fallbackMode = "warn") {
|
|
975
|
+
if (mode !== void 0) {
|
|
976
|
+
return mode;
|
|
977
|
+
}
|
|
978
|
+
if (deprecatedValidateFlag === void 0) {
|
|
979
|
+
return fallbackMode;
|
|
980
|
+
}
|
|
981
|
+
return deprecatedValidateFlag ? "strict" : "off";
|
|
982
|
+
}
|
|
983
|
+
createRequestMetadata(method, url, options, safeHeaders) {
|
|
984
|
+
if (!options) {
|
|
985
|
+
return { method, url };
|
|
986
|
+
}
|
|
987
|
+
return {
|
|
988
|
+
method,
|
|
989
|
+
url,
|
|
990
|
+
options: {
|
|
991
|
+
timeoutMs: options.timeoutMs,
|
|
992
|
+
retries: options.retries,
|
|
993
|
+
cache: options.cache,
|
|
994
|
+
validation: this.resolveValidationMode(
|
|
995
|
+
options.validation,
|
|
996
|
+
options.validateResponse,
|
|
997
|
+
this.validationMode
|
|
998
|
+
),
|
|
999
|
+
headers: Object.keys(safeHeaders).length > 0 ? safeHeaders : void 0
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
resolveCacheSettings(cache) {
|
|
1004
|
+
if (cache === false) {
|
|
1005
|
+
return {
|
|
1006
|
+
enabled: false,
|
|
1007
|
+
maxSize: 0,
|
|
1008
|
+
ttlMs: 0,
|
|
1009
|
+
endpointTtlMs: {}
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
if (cache === true || cache === void 0) {
|
|
1013
|
+
return {
|
|
1014
|
+
enabled: cache === true,
|
|
1015
|
+
maxSize: 500,
|
|
1016
|
+
ttlMs: 3e4,
|
|
1017
|
+
endpointTtlMs: {}
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
return {
|
|
1021
|
+
enabled: true,
|
|
1022
|
+
maxSize: cache.maxSize ?? 500,
|
|
1023
|
+
ttlMs: cache.ttlMs ?? 3e4,
|
|
1024
|
+
endpointTtlMs: cache.endpointTtlMs ?? {}
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
resolveCachePolicy(endpoint, requestCache) {
|
|
1028
|
+
if (!this.cacheSettings.enabled || this.cache === null) {
|
|
1029
|
+
return { enabled: false, ttlMs: 0 };
|
|
1030
|
+
}
|
|
1031
|
+
if (requestCache === false) {
|
|
1032
|
+
return { enabled: false, ttlMs: 0 };
|
|
1033
|
+
}
|
|
1034
|
+
const endpointTtlMs = this.cacheSettings.endpointTtlMs[endpoint];
|
|
1035
|
+
const ttlMs = typeof requestCache === "object" && requestCache.ttlMs !== void 0 ? requestCache.ttlMs : endpointTtlMs ?? this.cacheSettings.ttlMs;
|
|
1036
|
+
const enabled = requestCache === true || requestCache === void 0 || typeof requestCache === "object" && requestCache.enabled !== false;
|
|
1037
|
+
return {
|
|
1038
|
+
enabled,
|
|
1039
|
+
ttlMs
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
isCacheableMethod(method) {
|
|
1043
|
+
return method === "GET";
|
|
1044
|
+
}
|
|
1045
|
+
trackEndpointCacheKey(endpoint, cacheKey) {
|
|
1046
|
+
const existing = this.endpointCacheIndex.get(endpoint);
|
|
1047
|
+
if (existing) {
|
|
1048
|
+
existing.add(cacheKey);
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
this.endpointCacheIndex.set(endpoint, /* @__PURE__ */ new Set([cacheKey]));
|
|
1052
|
+
}
|
|
1053
|
+
async waitForRateLimitWindow(routeKey) {
|
|
1054
|
+
const now = Date.now();
|
|
1055
|
+
const routeLimit = this.routeRateLimitUntilMs.get(routeKey) ?? 0;
|
|
1056
|
+
const nextAllowedAtMs = Math.max(this.globalRateLimitUntilMs, routeLimit);
|
|
1057
|
+
if (nextAllowedAtMs <= now) {
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
await sleep(nextAllowedAtMs - now);
|
|
1061
|
+
}
|
|
1062
|
+
updateRateLimitState(headers, statusCode, routeKey) {
|
|
1063
|
+
const delayMs = parseRateLimitDelayMs(headers);
|
|
1064
|
+
if (delayMs === void 0) {
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
const shouldUpdate = statusCode === 429 || isRateLimitExhausted(headers);
|
|
1068
|
+
if (!shouldUpdate) {
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
const updateUntil = Date.now() + delayMs;
|
|
1072
|
+
const scope = parseRateLimitScope(headers);
|
|
1073
|
+
if (scope === "global") {
|
|
1074
|
+
this.globalRateLimitUntilMs = Math.max(this.globalRateLimitUntilMs, updateUntil);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
const currentRouteLimit = this.routeRateLimitUntilMs.get(routeKey) ?? 0;
|
|
1078
|
+
this.routeRateLimitUntilMs.set(routeKey, Math.max(currentRouteLimit, updateUntil));
|
|
1079
|
+
}
|
|
1080
|
+
async parseResponseBody(response) {
|
|
1081
|
+
if (response.status === 204) {
|
|
1082
|
+
return void 0;
|
|
1083
|
+
}
|
|
1084
|
+
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
1085
|
+
if (contentType.includes("application/json")) {
|
|
1086
|
+
try {
|
|
1087
|
+
return await response.json();
|
|
1088
|
+
} catch {
|
|
1089
|
+
return void 0;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
const text = await response.text();
|
|
1093
|
+
if (!text) {
|
|
1094
|
+
return void 0;
|
|
1095
|
+
}
|
|
1096
|
+
try {
|
|
1097
|
+
return JSON.parse(text);
|
|
1098
|
+
} catch {
|
|
1099
|
+
return text;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
|
|
1104
|
+
// src/validation/guards.ts
|
|
1105
|
+
function isRecord(value) {
|
|
1106
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1107
|
+
}
|
|
1108
|
+
function isString(value) {
|
|
1109
|
+
return typeof value === "string";
|
|
1110
|
+
}
|
|
1111
|
+
function isNumber(value) {
|
|
1112
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
1113
|
+
}
|
|
1114
|
+
function isBoolean(value) {
|
|
1115
|
+
return typeof value === "boolean";
|
|
1116
|
+
}
|
|
1117
|
+
function isStringMap(value) {
|
|
1118
|
+
if (!isRecord(value)) {
|
|
1119
|
+
return false;
|
|
1120
|
+
}
|
|
1121
|
+
return Object.values(value).every((entry) => typeof entry === "string");
|
|
1122
|
+
}
|
|
1123
|
+
function isArrayOf(value, guard) {
|
|
1124
|
+
return Array.isArray(value) && value.every((entry) => guard(entry));
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// src/validation/list-response.ts
|
|
1128
|
+
function isPaging(value) {
|
|
1129
|
+
if (!isRecord(value)) {
|
|
1130
|
+
return false;
|
|
1131
|
+
}
|
|
1132
|
+
if (value.cursors === void 0) {
|
|
1133
|
+
return true;
|
|
1134
|
+
}
|
|
1135
|
+
if (!isRecord(value.cursors)) {
|
|
1136
|
+
return false;
|
|
1137
|
+
}
|
|
1138
|
+
const before = value.cursors.before;
|
|
1139
|
+
const after = value.cursors.after;
|
|
1140
|
+
return (before === void 0 || isString(before)) && (after === void 0 || isString(after));
|
|
1141
|
+
}
|
|
1142
|
+
function normalizeListResponse(payload, itemGuard) {
|
|
1143
|
+
if (isArrayOf(payload, itemGuard)) {
|
|
1144
|
+
return { items: payload };
|
|
1145
|
+
}
|
|
1146
|
+
if (!isRecord(payload)) {
|
|
1147
|
+
throw new Error("Expected list response object or array.");
|
|
1148
|
+
}
|
|
1149
|
+
const items = payload.items;
|
|
1150
|
+
if (!isArrayOf(items, itemGuard)) {
|
|
1151
|
+
throw new Error("Expected `items` array in list response.");
|
|
1152
|
+
}
|
|
1153
|
+
const paging = payload.paging;
|
|
1154
|
+
if (paging !== void 0 && !isPaging(paging)) {
|
|
1155
|
+
throw new Error("Invalid `paging` object in list response.");
|
|
1156
|
+
}
|
|
1157
|
+
return {
|
|
1158
|
+
items,
|
|
1159
|
+
paging
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// src/validation/validators.ts
|
|
1164
|
+
function hasString(record, key) {
|
|
1165
|
+
return isString(record[key]);
|
|
1166
|
+
}
|
|
1167
|
+
function hasNumber(record, key) {
|
|
1168
|
+
return isNumber(record[key]);
|
|
1169
|
+
}
|
|
1170
|
+
function hasBoolean(record, key) {
|
|
1171
|
+
return isBoolean(record[key]);
|
|
1172
|
+
}
|
|
1173
|
+
function isJsonLocalizedName(value) {
|
|
1174
|
+
return isStringMap(value);
|
|
1175
|
+
}
|
|
1176
|
+
function isPlayerIcon(value) {
|
|
1177
|
+
if (!isRecord(value)) {
|
|
1178
|
+
return false;
|
|
1179
|
+
}
|
|
1180
|
+
return hasNumber(value, "id");
|
|
1181
|
+
}
|
|
1182
|
+
function isAccessory(value) {
|
|
1183
|
+
if (!isRecord(value)) {
|
|
1184
|
+
return false;
|
|
1185
|
+
}
|
|
1186
|
+
return hasNumber(value, "id") && isJsonLocalizedName(value.name);
|
|
1187
|
+
}
|
|
1188
|
+
function isStarPower(value) {
|
|
1189
|
+
if (!isRecord(value)) {
|
|
1190
|
+
return false;
|
|
1191
|
+
}
|
|
1192
|
+
return hasNumber(value, "id") && isJsonLocalizedName(value.name);
|
|
1193
|
+
}
|
|
1194
|
+
function isHyperCharge(value) {
|
|
1195
|
+
if (!isRecord(value)) {
|
|
1196
|
+
return false;
|
|
1197
|
+
}
|
|
1198
|
+
return hasNumber(value, "id") && isJsonLocalizedName(value.name);
|
|
1199
|
+
}
|
|
1200
|
+
function isGearStat(value) {
|
|
1201
|
+
if (!isRecord(value)) {
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1204
|
+
return hasNumber(value, "id") && hasNumber(value, "level") && isJsonLocalizedName(value.name);
|
|
1205
|
+
}
|
|
1206
|
+
function isSkin(value) {
|
|
1207
|
+
if (!isRecord(value)) {
|
|
1208
|
+
return false;
|
|
1209
|
+
}
|
|
1210
|
+
return hasNumber(value, "id") && isJsonLocalizedName(value.name);
|
|
1211
|
+
}
|
|
1212
|
+
function isBrawlerBuffies(value) {
|
|
1213
|
+
if (!isRecord(value)) {
|
|
1214
|
+
return false;
|
|
1215
|
+
}
|
|
1216
|
+
return hasBoolean(value, "gadget") && hasBoolean(value, "starPower") && hasBoolean(value, "hyperCharge");
|
|
1217
|
+
}
|
|
1218
|
+
function isBrawlerStat(value) {
|
|
1219
|
+
if (!isRecord(value)) {
|
|
1220
|
+
return false;
|
|
1221
|
+
}
|
|
1222
|
+
return hasNumber(value, "id") && hasNumber(value, "rank") && hasNumber(value, "trophies") && hasNumber(value, "highestTrophies") && hasNumber(value, "prestigeLevel") && hasNumber(value, "power") && hasNumber(value, "currentWinStreak") && hasNumber(value, "maxWinStreak") && isJsonLocalizedName(value.name) && isArrayOf(value.gadgets, isAccessory) && isArrayOf(value.starPowers, isStarPower) && isArrayOf(value.hyperCharges, isHyperCharge) && isArrayOf(value.gears, isGearStat) && isBrawlerBuffies(value.buffies) && isSkin(value.skin);
|
|
1223
|
+
}
|
|
1224
|
+
function isPlayerClub(value) {
|
|
1225
|
+
if (!isRecord(value)) {
|
|
1226
|
+
return false;
|
|
1227
|
+
}
|
|
1228
|
+
return hasString(value, "tag") && hasString(value, "name");
|
|
1229
|
+
}
|
|
1230
|
+
function isPlayer(value) {
|
|
1231
|
+
if (!isRecord(value)) {
|
|
1232
|
+
return false;
|
|
1233
|
+
}
|
|
1234
|
+
const hasClub = value.club === void 0 || isPlayerClub(value.club);
|
|
1235
|
+
return hasString(value, "tag") && hasString(value, "name") && hasString(value, "nameColor") && isPlayerIcon(value.icon) && hasClub && hasNumber(value, "trophies") && hasNumber(value, "highestTrophies") && hasNumber(value, "totalPrestigeLevel") && hasNumber(value, "expLevel") && hasNumber(value, "expPoints") && hasNumber(value, "soloVictories") && hasNumber(value, "duoVictories") && hasNumber(value, "bestRoboRumbleTime") && hasNumber(value, "bestTimeAsBigBrawler") && hasBoolean(value, "isQualifiedFromChampionshipChallenge") && hasNumber(value, "3vs3Victories") && isArrayOf(value.brawlers, isBrawlerStat);
|
|
1236
|
+
}
|
|
1237
|
+
function isClubMember(value) {
|
|
1238
|
+
if (!isRecord(value)) {
|
|
1239
|
+
return false;
|
|
1240
|
+
}
|
|
1241
|
+
return hasString(value, "tag") && hasString(value, "name") && hasString(value, "nameColor") && hasNumber(value, "trophies") && hasString(value, "role") && isPlayerIcon(value.icon);
|
|
1242
|
+
}
|
|
1243
|
+
function isClub(value) {
|
|
1244
|
+
if (!isRecord(value)) {
|
|
1245
|
+
return false;
|
|
1246
|
+
}
|
|
1247
|
+
return hasString(value, "tag") && hasString(value, "name") && hasString(value, "description") && hasNumber(value, "trophies") && hasNumber(value, "requiredTrophies") && isArrayOf(value.members, isClubMember) && hasString(value, "type") && hasNumber(value, "badgeId") && hasBoolean(value, "isFamilyFriendly");
|
|
1248
|
+
}
|
|
1249
|
+
function isClubRanking(value) {
|
|
1250
|
+
if (!isRecord(value)) {
|
|
1251
|
+
return false;
|
|
1252
|
+
}
|
|
1253
|
+
return hasString(value, "tag") && hasString(value, "name") && hasNumber(value, "trophies") && hasNumber(value, "rank") && hasNumber(value, "memberCount") && hasNumber(value, "badgeId");
|
|
1254
|
+
}
|
|
1255
|
+
function isPlayerRankingClub(value) {
|
|
1256
|
+
if (!isRecord(value)) {
|
|
1257
|
+
return false;
|
|
1258
|
+
}
|
|
1259
|
+
return hasString(value, "name");
|
|
1260
|
+
}
|
|
1261
|
+
function isPlayerRanking(value) {
|
|
1262
|
+
if (!isRecord(value)) {
|
|
1263
|
+
return false;
|
|
1264
|
+
}
|
|
1265
|
+
return hasString(value, "tag") && hasString(value, "name") && hasString(value, "nameColor") && hasNumber(value, "rank") && hasNumber(value, "trophies") && isPlayerIcon(value.icon) && isPlayerRankingClub(value.club);
|
|
1266
|
+
}
|
|
1267
|
+
function isBrawler(value) {
|
|
1268
|
+
if (!isRecord(value)) {
|
|
1269
|
+
return false;
|
|
1270
|
+
}
|
|
1271
|
+
return hasNumber(value, "id") && isJsonLocalizedName(value.name) && isArrayOf(value.gadgets, isAccessory) && isArrayOf(value.starPowers, isStarPower) && isArrayOf(value.hyperCharges, isHyperCharge) && isArrayOf(value.gears, isGearStat);
|
|
1272
|
+
}
|
|
1273
|
+
function isEventType(value) {
|
|
1274
|
+
if (!isRecord(value)) {
|
|
1275
|
+
return false;
|
|
1276
|
+
}
|
|
1277
|
+
return hasNumber(value, "id") && isJsonLocalizedName(value.name);
|
|
1278
|
+
}
|
|
1279
|
+
function isScheduledEventLocation(value) {
|
|
1280
|
+
if (!isRecord(value)) {
|
|
1281
|
+
return false;
|
|
1282
|
+
}
|
|
1283
|
+
const modifiers = value.modifiers;
|
|
1284
|
+
const modifiersValid = modifiers === void 0 || Array.isArray(modifiers) && modifiers.every((entry) => isString(entry));
|
|
1285
|
+
return hasNumber(value, "id") && hasNumber(value, "modeId") && hasString(value, "mode") && isJsonLocalizedName(value.map) && modifiersValid;
|
|
1286
|
+
}
|
|
1287
|
+
function isScheduledEvent(value) {
|
|
1288
|
+
if (!isRecord(value)) {
|
|
1289
|
+
return false;
|
|
1290
|
+
}
|
|
1291
|
+
return hasNumber(value, "slotId") && hasString(value, "startTime") && hasString(value, "endTime") && isScheduledEventLocation(value.event);
|
|
1292
|
+
}
|
|
1293
|
+
function isEvent(value) {
|
|
1294
|
+
if (!isRecord(value)) {
|
|
1295
|
+
return false;
|
|
1296
|
+
}
|
|
1297
|
+
return hasNumber(value, "id") && hasNumber(value, "modeId") && hasString(value, "mode") && isJsonLocalizedName(value.map);
|
|
1298
|
+
}
|
|
1299
|
+
function isBattle(value) {
|
|
1300
|
+
if (!isRecord(value)) {
|
|
1301
|
+
return false;
|
|
1302
|
+
}
|
|
1303
|
+
return hasString(value, "battleTime") && isRecord(value.battle) && isEvent(value.event);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// src/internal/response-parsers.ts
|
|
1307
|
+
function parseSingle(endpoint, payload, validate, guard) {
|
|
1308
|
+
if (!validate) {
|
|
1309
|
+
return payload;
|
|
1310
|
+
}
|
|
1311
|
+
if (!guard(payload)) {
|
|
1312
|
+
throw new Error(`Response validation failed for endpoint '${endpoint}'.`);
|
|
1313
|
+
}
|
|
1314
|
+
return payload;
|
|
1315
|
+
}
|
|
1316
|
+
function normalizeListWithoutValidation(payload) {
|
|
1317
|
+
if (Array.isArray(payload)) {
|
|
1318
|
+
return { items: payload };
|
|
1319
|
+
}
|
|
1320
|
+
if (!isRecord(payload)) {
|
|
1321
|
+
throw new Error("Expected list response object or array.");
|
|
1322
|
+
}
|
|
1323
|
+
if (!Array.isArray(payload.items)) {
|
|
1324
|
+
throw new Error("Expected `items` array in list response.");
|
|
1325
|
+
}
|
|
1326
|
+
if (payload.paging !== void 0 && !isRecord(payload.paging)) {
|
|
1327
|
+
throw new Error("Invalid `paging` value in list response.");
|
|
1328
|
+
}
|
|
1329
|
+
return {
|
|
1330
|
+
items: payload.items,
|
|
1331
|
+
paging: payload.paging
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
function parseList(endpoint, payload, validate, guard) {
|
|
1335
|
+
if (!validate) {
|
|
1336
|
+
return normalizeListWithoutValidation(payload);
|
|
1337
|
+
}
|
|
1338
|
+
try {
|
|
1339
|
+
return normalizeListResponse(payload, guard);
|
|
1340
|
+
} catch (error) {
|
|
1341
|
+
throw new Error(
|
|
1342
|
+
`Response validation failed for endpoint '${endpoint}': ${error.message}`
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
var endpointParsers = {
|
|
1347
|
+
"players.get": (payload, validate) => parseSingle("players.get", payload, validate, isPlayer),
|
|
1348
|
+
"players.getBattleLog": (payload, validate) => parseList("players.getBattleLog", payload, validate, isBattle),
|
|
1349
|
+
"clubs.get": (payload, validate) => parseSingle("clubs.get", payload, validate, isClub),
|
|
1350
|
+
"clubs.getMembers": (payload, validate) => parseList("clubs.getMembers", payload, validate, isClubMember),
|
|
1351
|
+
"rankings.getClubs": (payload, validate) => parseList("rankings.getClubs", payload, validate, isClubRanking),
|
|
1352
|
+
"rankings.getPlayers": (payload, validate) => parseList("rankings.getPlayers", payload, validate, isPlayerRanking),
|
|
1353
|
+
"rankings.getBrawlers": (payload, validate) => parseList("rankings.getBrawlers", payload, validate, isPlayerRanking),
|
|
1354
|
+
"brawlers.list": (payload, validate) => parseList("brawlers.list", payload, validate, isBrawler),
|
|
1355
|
+
"brawlers.get": (payload, validate) => parseSingle("brawlers.get", payload, validate, isBrawler),
|
|
1356
|
+
"gamemodes.list": (payload, validate) => parseList("gamemodes.list", payload, validate, isEventType),
|
|
1357
|
+
"events.getRotation": (payload, validate) => parseList("events.getRotation", payload, validate, isScheduledEvent)
|
|
1358
|
+
};
|
|
1359
|
+
|
|
1360
|
+
// src/utils/pagination.ts
|
|
1361
|
+
function normalizeCursorPaginationQuery(query) {
|
|
1362
|
+
if (!query) {
|
|
1363
|
+
return void 0;
|
|
1364
|
+
}
|
|
1365
|
+
const hasBefore = typeof query.before === "string" && query.before.length > 0;
|
|
1366
|
+
const hasAfter = typeof query.after === "string" && query.after.length > 0;
|
|
1367
|
+
if (hasBefore && hasAfter) {
|
|
1368
|
+
throw new Error("Only one of `before` or `after` can be provided.");
|
|
1369
|
+
}
|
|
1370
|
+
if (query.limit !== void 0) {
|
|
1371
|
+
if (!Number.isInteger(query.limit) || query.limit <= 0) {
|
|
1372
|
+
throw new Error("`limit` must be a positive integer when provided.");
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
return {
|
|
1376
|
+
before: hasBefore ? query.before : void 0,
|
|
1377
|
+
after: hasAfter ? query.after : void 0,
|
|
1378
|
+
limit: query.limit
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// src/utils/options.ts
|
|
1383
|
+
function isObject(value) {
|
|
1384
|
+
return typeof value === "object" && value !== null;
|
|
1385
|
+
}
|
|
1386
|
+
function isValidationMode(value) {
|
|
1387
|
+
return value === "off" || value === "warn" || value === "strict";
|
|
1388
|
+
}
|
|
1389
|
+
function resolvePaginatedArgs(first, second) {
|
|
1390
|
+
if (second !== void 0) {
|
|
1391
|
+
return {
|
|
1392
|
+
query: first,
|
|
1393
|
+
requestOptions: second
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
if (!first) {
|
|
1397
|
+
return {
|
|
1398
|
+
query: void 0,
|
|
1399
|
+
requestOptions: void 0
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
if (!isObject(first)) {
|
|
1403
|
+
return {
|
|
1404
|
+
query: void 0,
|
|
1405
|
+
requestOptions: void 0
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
const query = {
|
|
1409
|
+
before: typeof first.before === "string" ? first.before : void 0,
|
|
1410
|
+
after: typeof first.after === "string" ? first.after : void 0,
|
|
1411
|
+
limit: typeof first.limit === "number" ? first.limit : void 0
|
|
1412
|
+
};
|
|
1413
|
+
const requestOptions = {
|
|
1414
|
+
timeoutMs: typeof first.timeoutMs === "number" ? first.timeoutMs : void 0,
|
|
1415
|
+
retries: typeof first.retries === "number" || isObject(first.retries) ? first.retries : void 0,
|
|
1416
|
+
signal: first.signal instanceof AbortSignal ? first.signal : void 0,
|
|
1417
|
+
headers: first.headers,
|
|
1418
|
+
cache: typeof first.cache === "boolean" || isObject(first.cache) ? first.cache : void 0,
|
|
1419
|
+
validation: isValidationMode(first.validation) ? first.validation : void 0,
|
|
1420
|
+
validateResponse: typeof first.validateResponse === "boolean" ? first.validateResponse : void 0,
|
|
1421
|
+
hooks: isObject(first.hooks) ? first.hooks : void 0
|
|
1422
|
+
};
|
|
1423
|
+
const hasQuery = query.before !== void 0 || query.after !== void 0 || query.limit !== void 0;
|
|
1424
|
+
const hasRequestOptions = requestOptions.timeoutMs !== void 0 || requestOptions.retries !== void 0 || requestOptions.signal !== void 0 || requestOptions.headers !== void 0 || requestOptions.cache !== void 0 || requestOptions.validation !== void 0 || requestOptions.validateResponse !== void 0 || requestOptions.hooks !== void 0;
|
|
1425
|
+
return {
|
|
1426
|
+
query: hasQuery ? query : void 0,
|
|
1427
|
+
requestOptions: hasRequestOptions ? requestOptions : void 0
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// src/resources/brawlers-resource.ts
|
|
1432
|
+
var BrawlersResource = class {
|
|
1433
|
+
constructor(http) {
|
|
1434
|
+
this.http = http;
|
|
1435
|
+
}
|
|
1436
|
+
list(optsOrQuery, legacyOpts) {
|
|
1437
|
+
const { query, requestOptions } = resolvePaginatedArgs(optsOrQuery, legacyOpts);
|
|
1438
|
+
return this.http.request({
|
|
1439
|
+
endpoint: "brawlers.list",
|
|
1440
|
+
method: "GET",
|
|
1441
|
+
path: "/brawlers",
|
|
1442
|
+
query: normalizeCursorPaginationQuery(query),
|
|
1443
|
+
options: requestOptions,
|
|
1444
|
+
parser: endpointParsers["brawlers.list"]
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* Get one brawler by identifier.
|
|
1449
|
+
*/
|
|
1450
|
+
get(brawlerId, opts) {
|
|
1451
|
+
return this.http.request({
|
|
1452
|
+
endpoint: "brawlers.get",
|
|
1453
|
+
method: "GET",
|
|
1454
|
+
path: `/brawlers/${encodeURIComponent(String(brawlerId))}`,
|
|
1455
|
+
options: opts,
|
|
1456
|
+
parser: endpointParsers["brawlers.get"]
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
|
|
1461
|
+
// src/utils/tag.ts
|
|
1462
|
+
function encodeTag(tag, options) {
|
|
1463
|
+
const uppercase = options?.uppercase ?? true;
|
|
1464
|
+
const compactTag = String(tag).replace(/\s+/gu, "");
|
|
1465
|
+
const normalizedTag = uppercase ? compactTag.toUpperCase() : compactTag;
|
|
1466
|
+
const prefixedTag = normalizedTag.startsWith("#") ? normalizedTag : `#${normalizedTag}`;
|
|
1467
|
+
return encodeURIComponent(prefixedTag);
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// src/resources/clubs-resource.ts
|
|
1471
|
+
var ClubsResource = class {
|
|
1472
|
+
constructor(http) {
|
|
1473
|
+
this.http = http;
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Get full club details by tag.
|
|
1477
|
+
*/
|
|
1478
|
+
get(clubTag, opts) {
|
|
1479
|
+
return this.http.request({
|
|
1480
|
+
endpoint: "clubs.get",
|
|
1481
|
+
method: "GET",
|
|
1482
|
+
path: `/clubs/${encodeTag(clubTag)}`,
|
|
1483
|
+
options: opts,
|
|
1484
|
+
parser: endpointParsers["clubs.get"]
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
getMembers(clubTag, optsOrQuery, legacyOpts) {
|
|
1488
|
+
const { query, requestOptions } = resolvePaginatedArgs(optsOrQuery, legacyOpts);
|
|
1489
|
+
return this.http.request({
|
|
1490
|
+
endpoint: "clubs.getMembers",
|
|
1491
|
+
method: "GET",
|
|
1492
|
+
path: `/clubs/${encodeTag(clubTag)}/members`,
|
|
1493
|
+
query: normalizeCursorPaginationQuery(query),
|
|
1494
|
+
options: requestOptions,
|
|
1495
|
+
parser: endpointParsers["clubs.getMembers"]
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
};
|
|
1499
|
+
|
|
1500
|
+
// src/resources/events-resource.ts
|
|
1501
|
+
var EventsResource = class {
|
|
1502
|
+
constructor(http) {
|
|
1503
|
+
this.http = http;
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Get currently active event rotation.
|
|
1507
|
+
*/
|
|
1508
|
+
getRotation(opts) {
|
|
1509
|
+
return this.http.request({
|
|
1510
|
+
endpoint: "events.getRotation",
|
|
1511
|
+
method: "GET",
|
|
1512
|
+
path: "/events/rotation",
|
|
1513
|
+
options: opts,
|
|
1514
|
+
parser: endpointParsers["events.getRotation"]
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
};
|
|
1518
|
+
|
|
1519
|
+
// src/resources/gamemodes-resource.ts
|
|
1520
|
+
var GameModesResource = class {
|
|
1521
|
+
constructor(http) {
|
|
1522
|
+
this.http = http;
|
|
1523
|
+
}
|
|
1524
|
+
list(optsOrQuery, legacyOpts) {
|
|
1525
|
+
const { query, requestOptions } = resolvePaginatedArgs(optsOrQuery, legacyOpts);
|
|
1526
|
+
return this.http.request({
|
|
1527
|
+
endpoint: "gamemodes.list",
|
|
1528
|
+
method: "GET",
|
|
1529
|
+
path: "/gamemodes",
|
|
1530
|
+
query: normalizeCursorPaginationQuery(query),
|
|
1531
|
+
options: requestOptions,
|
|
1532
|
+
parser: endpointParsers["gamemodes.list"]
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
};
|
|
1536
|
+
|
|
1537
|
+
// src/resources/players-resource.ts
|
|
1538
|
+
var PlayersResource = class {
|
|
1539
|
+
constructor(http) {
|
|
1540
|
+
this.http = http;
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Get a player profile by tag.
|
|
1544
|
+
*
|
|
1545
|
+
* @example
|
|
1546
|
+
* const player = await client.players.get("#2PP");
|
|
1547
|
+
*/
|
|
1548
|
+
get(playerTag, opts) {
|
|
1549
|
+
return this.http.request({
|
|
1550
|
+
endpoint: "players.get",
|
|
1551
|
+
method: "GET",
|
|
1552
|
+
path: `/players/${encodeTag(playerTag)}`,
|
|
1553
|
+
options: opts,
|
|
1554
|
+
parser: endpointParsers["players.get"]
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* Get recent battle log entries for a player.
|
|
1559
|
+
*
|
|
1560
|
+
* @example
|
|
1561
|
+
* const battles = await client.players.getBattleLog("#2PP");
|
|
1562
|
+
*/
|
|
1563
|
+
getBattleLog(playerTag, opts) {
|
|
1564
|
+
return this.http.request({
|
|
1565
|
+
endpoint: "players.getBattleLog",
|
|
1566
|
+
method: "GET",
|
|
1567
|
+
path: `/players/${encodeTag(playerTag)}/battlelog`,
|
|
1568
|
+
options: opts,
|
|
1569
|
+
parser: endpointParsers["players.getBattleLog"]
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
};
|
|
1573
|
+
|
|
1574
|
+
// src/resources/rankings-resource.ts
|
|
1575
|
+
var RankingsResource = class {
|
|
1576
|
+
constructor(http) {
|
|
1577
|
+
this.http = http;
|
|
1578
|
+
}
|
|
1579
|
+
getClubs(countryCode, optsOrQuery, legacyOpts) {
|
|
1580
|
+
const { query, requestOptions } = resolvePaginatedArgs(optsOrQuery, legacyOpts);
|
|
1581
|
+
return this.http.request({
|
|
1582
|
+
endpoint: "rankings.getClubs",
|
|
1583
|
+
method: "GET",
|
|
1584
|
+
path: `/rankings/${encodeURIComponent(countryCode)}/clubs`,
|
|
1585
|
+
query: normalizeCursorPaginationQuery(query),
|
|
1586
|
+
options: requestOptions,
|
|
1587
|
+
parser: endpointParsers["rankings.getClubs"]
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
getPlayers(countryCode, optsOrQuery, legacyOpts) {
|
|
1591
|
+
const { query, requestOptions } = resolvePaginatedArgs(optsOrQuery, legacyOpts);
|
|
1592
|
+
return this.http.request({
|
|
1593
|
+
endpoint: "rankings.getPlayers",
|
|
1594
|
+
method: "GET",
|
|
1595
|
+
path: `/rankings/${encodeURIComponent(countryCode)}/players`,
|
|
1596
|
+
query: normalizeCursorPaginationQuery(query),
|
|
1597
|
+
options: requestOptions,
|
|
1598
|
+
parser: endpointParsers["rankings.getPlayers"]
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
getBrawlers(countryCode, brawlerId, optsOrQuery, legacyOpts) {
|
|
1602
|
+
const { query, requestOptions } = resolvePaginatedArgs(optsOrQuery, legacyOpts);
|
|
1603
|
+
return this.http.request({
|
|
1604
|
+
endpoint: "rankings.getBrawlers",
|
|
1605
|
+
method: "GET",
|
|
1606
|
+
path: `/rankings/${encodeURIComponent(countryCode)}/brawlers/${encodeURIComponent(String(brawlerId))}`,
|
|
1607
|
+
query: normalizeCursorPaginationQuery(query),
|
|
1608
|
+
options: requestOptions,
|
|
1609
|
+
parser: endpointParsers["rankings.getBrawlers"]
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
|
|
1614
|
+
// src/client/brawl-stars-client.ts
|
|
1615
|
+
var BrawlStarsClient = class {
|
|
1616
|
+
http;
|
|
1617
|
+
/**
|
|
1618
|
+
* Player operations.
|
|
1619
|
+
*/
|
|
1620
|
+
players;
|
|
1621
|
+
/**
|
|
1622
|
+
* Club operations.
|
|
1623
|
+
*/
|
|
1624
|
+
clubs;
|
|
1625
|
+
/**
|
|
1626
|
+
* Ranking operations.
|
|
1627
|
+
*/
|
|
1628
|
+
rankings;
|
|
1629
|
+
/**
|
|
1630
|
+
* Brawler operations.
|
|
1631
|
+
*/
|
|
1632
|
+
brawlers;
|
|
1633
|
+
/**
|
|
1634
|
+
* Game mode operations.
|
|
1635
|
+
*/
|
|
1636
|
+
gamemodes;
|
|
1637
|
+
/**
|
|
1638
|
+
* Event operations.
|
|
1639
|
+
*/
|
|
1640
|
+
events;
|
|
1641
|
+
/**
|
|
1642
|
+
* Create a new API client.
|
|
1643
|
+
*/
|
|
1644
|
+
constructor(options) {
|
|
1645
|
+
this.http = new HttpClient(options);
|
|
1646
|
+
this.players = new PlayersResource(this.http);
|
|
1647
|
+
this.clubs = new ClubsResource(this.http);
|
|
1648
|
+
this.rankings = new RankingsResource(this.http);
|
|
1649
|
+
this.brawlers = new BrawlersResource(this.http);
|
|
1650
|
+
this.gamemodes = new GameModesResource(this.http);
|
|
1651
|
+
this.events = new EventsResource(this.http);
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* Clear all in-memory cached responses.
|
|
1655
|
+
*/
|
|
1656
|
+
clearCache() {
|
|
1657
|
+
this.http.clearCache();
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* Invalidate cache entries for one endpoint or all endpoints.
|
|
1661
|
+
*/
|
|
1662
|
+
invalidateCache(endpoint) {
|
|
1663
|
+
return this.http.invalidateCache(endpoint);
|
|
1664
|
+
}
|
|
1665
|
+
};
|
|
1666
|
+
|
|
1667
|
+
// src/client/create-client.ts
|
|
1668
|
+
function createBrawlStarsClient(options) {
|
|
1669
|
+
return new BrawlStarsClient(options);
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
// src/observability/prom-client-adapter.ts
|
|
1673
|
+
function createPromClientAdapter(options) {
|
|
1674
|
+
return {
|
|
1675
|
+
onRequestStart(event) {
|
|
1676
|
+
options.requestsStarted?.inc({
|
|
1677
|
+
endpoint: event.endpoint,
|
|
1678
|
+
method: event.method
|
|
1679
|
+
});
|
|
1680
|
+
},
|
|
1681
|
+
onRequestEnd(event) {
|
|
1682
|
+
options.requestsCompleted?.inc({
|
|
1683
|
+
endpoint: event.endpoint,
|
|
1684
|
+
method: event.method,
|
|
1685
|
+
statusCode: event.statusCode
|
|
1686
|
+
});
|
|
1687
|
+
options.requestDurationMs?.observe(
|
|
1688
|
+
{
|
|
1689
|
+
endpoint: event.endpoint,
|
|
1690
|
+
method: event.method,
|
|
1691
|
+
statusCode: event.statusCode
|
|
1692
|
+
},
|
|
1693
|
+
event.durationMs
|
|
1694
|
+
);
|
|
1695
|
+
},
|
|
1696
|
+
onRequestError(event) {
|
|
1697
|
+
options.requestsFailed?.inc({
|
|
1698
|
+
endpoint: event.endpoint,
|
|
1699
|
+
method: event.method,
|
|
1700
|
+
errorName: event.error.name
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
export {
|
|
1706
|
+
ApiError,
|
|
1707
|
+
BrawlStarsClient,
|
|
1708
|
+
ClientError,
|
|
1709
|
+
NetworkError,
|
|
1710
|
+
RateLimitError,
|
|
1711
|
+
ResponseValidationError,
|
|
1712
|
+
TimeoutError,
|
|
1713
|
+
ValidationError,
|
|
1714
|
+
createBrawlStarsClient,
|
|
1715
|
+
createPromClientAdapter,
|
|
1716
|
+
encodeTag
|
|
1717
|
+
};
|
|
1718
|
+
//# sourceMappingURL=index.js.map
|