refactor(rest): switch api to fetch-like and provide strategies (#9416)

BREAKING CHANGE: NodeJS v18+ is required when using node due to the use of global `fetch`
BREAKING CHANGE: The raw method of REST now returns a web compatible `Respone` object.
BREAKING CHANGE: The `parseResponse` utility method has been updated to operate on a web compatible `Response` object.
BREAKING CHANGE: Many underlying internals have changed, some of which were exported.
BREAKING CHANGE: `DefaultRestOptions` used to contain a default `agent`, which is now set to `null` instead.
This commit is contained in:
ckohen
2023-05-06 12:09:19 -07:00
committed by GitHub
parent fc5b9c523b
commit cdaa0a36f5
28 changed files with 317 additions and 203 deletions

View File

@@ -1,8 +1,12 @@
import process from 'node:process';
import { lazy } from '@discordjs/util';
import { APIVersion } from 'discord-api-types/v10';
import { Agent } from 'undici';
import type { RESTOptions } from '../REST.js';
const getUndiciRequest = lazy(async () => {
return import('../../strategies/undiciRequest.js');
});
export const DefaultUserAgent =
`DiscordBot (https://discord.js.org, [VI]{{inject}}[/VI])` as `DiscordBot (https://discord.js.org, ${string})`;
@@ -12,13 +16,7 @@ export const DefaultUserAgent =
export const DefaultUserAgentAppendix = process.release?.name === 'node' ? `Node.js/${process.version}` : '';
export const DefaultRestOptions = {
get agent() {
return new Agent({
connect: {
timeout: 30_000,
},
});
},
agent: null,
api: 'https://discord.com/api',
authPrefix: 'Bot',
cdn: 'https://cdn.discordapp.com',
@@ -34,6 +32,10 @@ export const DefaultRestOptions = {
hashSweepInterval: 14_400_000, // 4 Hours
hashLifetime: 86_400_000, // 24 Hours
handlerSweepInterval: 3_600_000, // 1 Hour
async makeRequest(...args) {
const strategy = await getUndiciRequest();
return strategy.makeRequest(...args);
},
} as const satisfies Required<RESTOptions>;
/**

View File

@@ -1,20 +1,9 @@
import { Blob, Buffer } from 'node:buffer';
import { URLSearchParams } from 'node:url';
import { types } from 'node:util';
import type { RESTPatchAPIChannelJSONBody } from 'discord-api-types/v10';
import { FormData, type Dispatcher, type RequestInit } from 'undici';
import type { RateLimitData, RequestOptions } from '../REST.js';
import type { RateLimitData, ResponseLike } from '../REST.js';
import { type RequestManager, RequestMethod } from '../RequestManager.js';
import { RateLimitError } from '../errors/RateLimitError.js';
export function parseHeader(header: string[] | string | undefined): string | undefined {
if (header === undefined || typeof header === 'string') {
return header;
}
return header.join(';');
}
function serializeSearchParam(value: unknown): string | null {
switch (typeof value) {
case 'string':
@@ -61,13 +50,12 @@ export function makeURLSearchParams<T extends object>(options?: Readonly<T>) {
*
* @param res - The fetch response
*/
export async function parseResponse(res: Dispatcher.ResponseData): Promise<unknown> {
const header = parseHeader(res.headers['content-type']);
if (header?.startsWith('application/json')) {
return res.body.json();
export async function parseResponse(res: ResponseLike): Promise<unknown> {
if (res.headers.get('Content-Type')?.startsWith('application/json')) {
return res.json();
}
return res.body.arrayBuffer();
return res.arrayBuffer();
}
/**
@@ -94,49 +82,6 @@ export function hasSublimit(bucketRoute: string, body?: unknown, method?: string
return true;
}
export async function resolveBody(body: RequestInit['body']): Promise<RequestOptions['body']> {
// eslint-disable-next-line no-eq-null, eqeqeq
if (body == null) {
return null;
} else if (typeof body === 'string') {
return body;
} else if (types.isUint8Array(body)) {
return body;
} else if (types.isArrayBuffer(body)) {
return new Uint8Array(body);
} else if (body instanceof URLSearchParams) {
return body.toString();
} else if (body instanceof DataView) {
return new Uint8Array(body.buffer);
} else if (body instanceof Blob) {
return new Uint8Array(await body.arrayBuffer());
} else if (body instanceof FormData) {
return body;
} else if ((body as Iterable<Uint8Array>)[Symbol.iterator]) {
const chunks = [...(body as Iterable<Uint8Array>)];
const length = chunks.reduce((a, b) => a + b.length, 0);
const uint8 = new Uint8Array(length);
let lengthUsed = 0;
return chunks.reduce((a, b) => {
a.set(b, lengthUsed);
lengthUsed += b.length;
return a;
}, uint8);
} else if ((body as AsyncIterable<Uint8Array>)[Symbol.asyncIterator]) {
const chunks: Uint8Array[] = [];
for await (const chunk of body as AsyncIterable<Uint8Array>) {
chunks.push(chunk);
}
return Buffer.concat(chunks);
}
throw new TypeError(`Unable to resolve body.`);
}
/**
* Check whether an error indicates that a retry can be attempted
*