mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-16 19:43:29 +01:00
feat: add AbortSignal support (#8672)
* feat: add `AbortSignal` support * fix: move the expect earlier * fix: pass signal Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable id-length */
|
/* eslint-disable id-length */
|
||||||
/* eslint-disable promise/prefer-await-to-then */
|
/* eslint-disable promise/prefer-await-to-then */
|
||||||
import { performance } from 'node:perf_hooks';
|
import { performance } from 'node:perf_hooks';
|
||||||
import { setInterval, clearInterval } from 'node:timers';
|
import { setInterval, clearInterval, setTimeout } from 'node:timers';
|
||||||
import { MockAgent, setGlobalDispatcher } from 'undici';
|
import { MockAgent, setGlobalDispatcher } from 'undici';
|
||||||
import type { Interceptable, MockInterceptor } from 'undici/types/mock-interceptor';
|
import type { Interceptable, MockInterceptor } from 'undici/types/mock-interceptor';
|
||||||
import { beforeEach, afterEach, test, expect, vitest } from 'vitest';
|
import { beforeEach, afterEach, test, expect, vitest } from 'vitest';
|
||||||
@@ -548,3 +548,30 @@ test('malformedRequest', async () => {
|
|||||||
|
|
||||||
await expect(api.get('/malformedRequest')).rejects.toBeInstanceOf(DiscordAPIError);
|
await expect(api.get('/malformedRequest')).rejects.toBeInstanceOf(DiscordAPIError);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('abort', async () => {
|
||||||
|
mockPool
|
||||||
|
.intercept({
|
||||||
|
path: genPath('/abort'),
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
.reply(200, { message: 'Hello World' }, responseOptions)
|
||||||
|
.delay(100)
|
||||||
|
.times(3);
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const [aP2, bP2, cP2] = [
|
||||||
|
api.get('/abort', { signal: controller.signal }),
|
||||||
|
api.get('/abort', { signal: controller.signal }),
|
||||||
|
api.get('/abort', { signal: controller.signal }),
|
||||||
|
];
|
||||||
|
|
||||||
|
await expect(aP2).resolves.toStrictEqual({ message: 'Hello World' });
|
||||||
|
controller.abort();
|
||||||
|
|
||||||
|
// Abort mid-execution:
|
||||||
|
await expect(bP2).rejects.toThrowError('Request aborted');
|
||||||
|
|
||||||
|
// Abort scheduled:
|
||||||
|
await expect(cP2).rejects.toThrowError('Request aborted');
|
||||||
|
});
|
||||||
|
|||||||
@@ -93,6 +93,10 @@ export interface RequestData {
|
|||||||
* Reason to show in the audit logs
|
* Reason to show in the audit logs
|
||||||
*/
|
*/
|
||||||
reason?: string;
|
reason?: string;
|
||||||
|
/**
|
||||||
|
* The signal to abort the queue entry or the REST call, where applicable
|
||||||
|
*/
|
||||||
|
signal?: AbortSignal | undefined;
|
||||||
/**
|
/**
|
||||||
* If this request should be versioned
|
* If this request should be versioned
|
||||||
*
|
*
|
||||||
@@ -133,7 +137,7 @@ export interface InternalRequest extends RequestData {
|
|||||||
method: RequestMethod;
|
method: RequestMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HandlerRequestData = Pick<InternalRequest, 'auth' | 'body' | 'files'>;
|
export type HandlerRequestData = Pick<InternalRequest, 'auth' | 'body' | 'files' | 'signal'>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parsed route data for an endpoint
|
* Parsed route data for an endpoint
|
||||||
@@ -338,6 +342,7 @@ export class RequestManager extends EventEmitter {
|
|||||||
body: request.body,
|
body: request.body,
|
||||||
files: request.files,
|
files: request.files,
|
||||||
auth: request.auth !== false,
|
auth: request.auth !== false,
|
||||||
|
signal: request.signal,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ export class SequentialHandler implements IHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wait for any previous requests to be completed before this one is run
|
// Wait for any previous requests to be completed before this one is run
|
||||||
await queue.wait();
|
await queue.wait({ signal: requestData.signal });
|
||||||
// This set handles retroactively sublimiting requests
|
// This set handles retroactively sublimiting requests
|
||||||
if (queueType === QueueType.Standard) {
|
if (queueType === QueueType.Standard) {
|
||||||
if (this.#sublimitedQueue && hasSublimit(routeId.bucketRoute, requestData.body, options.method)) {
|
if (this.#sublimitedQueue && hasSublimit(routeId.bucketRoute, requestData.body, options.method)) {
|
||||||
@@ -293,8 +293,17 @@ export class SequentialHandler implements IHandler {
|
|||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeout = setTimeout(() => controller.abort(), this.manager.options.timeout).unref();
|
const timeout = setTimeout(() => controller.abort(), this.manager.options.timeout).unref();
|
||||||
let res: Dispatcher.ResponseData;
|
if (requestData.signal) {
|
||||||
|
// The type polyfill is required because Node.js's types are incomplete.
|
||||||
|
const signal = requestData.signal as PolyFillAbortSignal;
|
||||||
|
// If the user signal was aborted, abort the controller, else abort the local signal.
|
||||||
|
// The reason why we don't re-use the user's signal, is because users may use the same signal for multiple
|
||||||
|
// requests, and we do not want to cause unexpected side-effects.
|
||||||
|
if (signal.aborted) controller.abort();
|
||||||
|
else signal.addEventListener('abort', () => controller.abort());
|
||||||
|
}
|
||||||
|
|
||||||
|
let res: Dispatcher.ResponseData;
|
||||||
try {
|
try {
|
||||||
res = await request(url, { ...options, signal: controller.signal });
|
res = await request(url, { ...options, signal: controller.signal });
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
@@ -492,3 +501,9 @@ export class SequentialHandler implements IHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PolyFillAbortSignal {
|
||||||
|
readonly aborted: boolean;
|
||||||
|
addEventListener(type: 'abort', listener: () => void): void;
|
||||||
|
removeEventListener(type: 'abort', listener: () => void): void;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user