Skip to content

Commit 3c231ae

Browse files
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>
1 parent 9f63eb9 commit 3c231ae

File tree

3 files changed

+51
-4
lines changed

3 files changed

+51
-4
lines changed

packages/rest/__tests__/RequestHandler.test.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable id-length */
22
/* eslint-disable promise/prefer-await-to-then */
33
import { performance } from 'node:perf_hooks';
4-
import { setInterval, clearInterval } from 'node:timers';
4+
import { setInterval, clearInterval, setTimeout } from 'node:timers';
55
import { MockAgent, setGlobalDispatcher } from 'undici';
66
import type { Interceptable, MockInterceptor } from 'undici/types/mock-interceptor';
77
import { beforeEach, afterEach, test, expect, vitest } from 'vitest';
@@ -548,3 +548,30 @@ test('malformedRequest', async () => {
548548

549549
await expect(api.get('/malformedRequest')).rejects.toBeInstanceOf(DiscordAPIError);
550550
});
551+
552+
test('abort', async () => {
553+
mockPool
554+
.intercept({
555+
path: genPath('/abort'),
556+
method: 'GET',
557+
})
558+
.reply(200, { message: 'Hello World' }, responseOptions)
559+
.delay(100)
560+
.times(3);
561+
562+
const controller = new AbortController();
563+
const [aP2, bP2, cP2] = [
564+
api.get('/abort', { signal: controller.signal }),
565+
api.get('/abort', { signal: controller.signal }),
566+
api.get('/abort', { signal: controller.signal }),
567+
];
568+
569+
await expect(aP2).resolves.toStrictEqual({ message: 'Hello World' });
570+
controller.abort();
571+
572+
// Abort mid-execution:
573+
await expect(bP2).rejects.toThrowError('Request aborted');
574+
575+
// Abort scheduled:
576+
await expect(cP2).rejects.toThrowError('Request aborted');
577+
});

packages/rest/src/lib/RequestManager.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ export interface RequestData {
9393
* Reason to show in the audit logs
9494
*/
9595
reason?: string;
96+
/**
97+
* The signal to abort the queue entry or the REST call, where applicable
98+
*/
99+
signal?: AbortSignal | undefined;
96100
/**
97101
* If this request should be versioned
98102
*
@@ -133,7 +137,7 @@ export interface InternalRequest extends RequestData {
133137
method: RequestMethod;
134138
}
135139

136-
export type HandlerRequestData = Pick<InternalRequest, 'auth' | 'body' | 'files'>;
140+
export type HandlerRequestData = Pick<InternalRequest, 'auth' | 'body' | 'files' | 'signal'>;
137141

138142
/**
139143
* Parsed route data for an endpoint
@@ -338,6 +342,7 @@ export class RequestManager extends EventEmitter {
338342
body: request.body,
339343
files: request.files,
340344
auth: request.auth !== false,
345+
signal: request.signal,
341346
});
342347
}
343348

packages/rest/src/lib/handlers/SequentialHandler.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ export class SequentialHandler implements IHandler {
175175
}
176176

177177
// Wait for any previous requests to be completed before this one is run
178-
await queue.wait();
178+
await queue.wait({ signal: requestData.signal });
179179
// This set handles retroactively sublimiting requests
180180
if (queueType === QueueType.Standard) {
181181
if (this.#sublimitedQueue && hasSublimit(routeId.bucketRoute, requestData.body, options.method)) {
@@ -293,8 +293,17 @@ export class SequentialHandler implements IHandler {
293293

294294
const controller = new AbortController();
295295
const timeout = setTimeout(() => controller.abort(), this.manager.options.timeout).unref();
296-
let res: Dispatcher.ResponseData;
296+
if (requestData.signal) {
297+
// The type polyfill is required because Node.js's types are incomplete.
298+
const signal = requestData.signal as PolyFillAbortSignal;
299+
// If the user signal was aborted, abort the controller, else abort the local signal.
300+
// The reason why we don't re-use the user's signal, is because users may use the same signal for multiple
301+
// requests, and we do not want to cause unexpected side-effects.
302+
if (signal.aborted) controller.abort();
303+
else signal.addEventListener('abort', () => controller.abort());
304+
}
297305

306+
let res: Dispatcher.ResponseData;
298307
try {
299308
res = await request(url, { ...options, signal: controller.signal });
300309
} catch (error: unknown) {
@@ -492,3 +501,9 @@ export class SequentialHandler implements IHandler {
492501
}
493502
}
494503
}
504+
505+
interface PolyFillAbortSignal {
506+
readonly aborted: boolean;
507+
addEventListener(type: 'abort', listener: () => void): void;
508+
removeEventListener(type: 'abort', listener: () => void): void;
509+
}

0 commit comments

Comments
 (0)