-
-
Notifications
You must be signed in to change notification settings - Fork 4k
/
utils.ts
174 lines (156 loc) · 5.48 KB
/
utils.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
import type { RESTPatchAPIChannelJSONBody, Snowflake } from 'discord-api-types/v10';
import type { REST } from '../REST.js';
import { RateLimitError } from '../errors/RateLimitError.js';
import { DEPRECATION_WARNING_PREFIX } from './constants.js';
import { RequestMethod } from './types.js';
import type { GetRateLimitOffsetFunction, RateLimitData, ResponseLike } from './types.js';
function serializeSearchParam(value: unknown): string | null {
switch (typeof value) {
case 'string':
return value;
case 'number':
case 'bigint':
case 'boolean':
return value.toString();
case 'object':
if (value === null) return null;
if (value instanceof Date) {
return Number.isNaN(value.getTime()) ? null : value.toISOString();
}
// eslint-disable-next-line @typescript-eslint/no-base-to-string
if (typeof value.toString === 'function' && value.toString !== Object.prototype.toString) return value.toString();
return null;
default:
return null;
}
}
/**
* Creates and populates an URLSearchParams instance from an object, stripping
* out null and undefined values, while also coercing non-strings to strings.
*
* @param options - The options to use
* @returns A populated URLSearchParams instance
*/
export function makeURLSearchParams<OptionsType extends object>(options?: Readonly<OptionsType>) {
const params = new URLSearchParams();
if (!options) return params;
for (const [key, value] of Object.entries(options)) {
const serialized = serializeSearchParam(value);
if (serialized !== null) params.append(key, serialized);
}
return params;
}
/**
* Converts the response to usable data
*
* @param res - The fetch response
*/
export async function parseResponse(res: ResponseLike): Promise<unknown> {
if (res.headers.get('Content-Type')?.startsWith('application/json')) {
return res.json();
}
return res.arrayBuffer();
}
/**
* Check whether a request falls under a sublimit
*
* @param bucketRoute - The buckets route identifier
* @param body - The options provided as JSON data
* @param method - The HTTP method that will be used to make the request
* @returns Whether the request falls under a sublimit
*/
export function hasSublimit(bucketRoute: string, body?: unknown, method?: string): boolean {
// TODO: Update for new sublimits
// Currently known sublimits:
// Editing channel `name` or `topic`
if (bucketRoute === '/channels/:id') {
if (typeof body !== 'object' || body === null) return false;
// This should never be a POST body, but just in case
if (method !== RequestMethod.Patch) return false;
const castedBody = body as RESTPatchAPIChannelJSONBody;
return ['name', 'topic'].some((key) => Reflect.has(castedBody, key));
}
// If we are checking if a request has a sublimit on a route not checked above, sublimit all requests to avoid a flood of 429s
return true;
}
/**
* Check whether an error indicates that a retry can be attempted
*
* @param error - The error thrown by the network request
* @returns Whether the error indicates a retry should be attempted
*/
export function shouldRetry(error: Error | NodeJS.ErrnoException) {
// Retry for possible timed out requests
if (error.name === 'AbortError') return true;
// Downlevel ECONNRESET to retry as it may be recoverable
return ('code' in error && error.code === 'ECONNRESET') || error.message.includes('ECONNRESET');
}
/**
* Determines whether the request should be queued or whether a RateLimitError should be thrown
*
* @internal
*/
export async function onRateLimit(manager: REST, rateLimitData: RateLimitData) {
const { options } = manager;
if (!options.rejectOnRateLimit) return;
const shouldThrow =
typeof options.rejectOnRateLimit === 'function'
? await options.rejectOnRateLimit(rateLimitData)
: options.rejectOnRateLimit.some((route) => rateLimitData.route.startsWith(route.toLowerCase()));
if (shouldThrow) {
throw new RateLimitError(rateLimitData);
}
}
/**
* Calculates the default avatar index for a given user id.
*
* @param userId - The user id to calculate the default avatar index for
*/
export function calculateUserDefaultAvatarIndex(userId: Snowflake) {
return Number(BigInt(userId) >> 22n) % 6;
}
/**
* Sleeps for a given amount of time.
*
* @param ms - The amount of time (in milliseconds) to sleep for
*/
export async function sleep(ms: number): Promise<void> {
return new Promise<void>((resolve) => {
setTimeout(() => resolve(), ms);
});
}
/**
* Verifies that a value is a buffer-like object.
*
* @param value - The value to check
*/
export function isBufferLike(value: unknown): value is ArrayBuffer | Buffer | Uint8Array | Uint8ClampedArray {
return value instanceof ArrayBuffer || value instanceof Uint8Array || value instanceof Uint8ClampedArray;
}
/**
* Irrespective environment warning.
*
* @remarks Only the message is needed. The deprecation prefix is handled already.
* @param message - A string the warning will emit with
* @internal
*/
export function deprecationWarning(message: string) {
if (typeof globalThis.process === 'undefined') {
console.warn(`${DEPRECATION_WARNING_PREFIX}: ${message}`);
} else {
process.emitWarning(message, DEPRECATION_WARNING_PREFIX);
}
}
/**
* Normalizes the offset for rate limits. Applies a Math.max(0, N) to prevent negative offsets,
* also deals with callbacks.
*
* @internal
*/
export function normalizeRateLimitOffset(offset: GetRateLimitOffsetFunction | number, route: string): number {
if (typeof offset === 'number') {
return Math.max(0, offset);
}
const result = offset(route);
return Math.max(0, result);
}