Skip to content

Commit 7767eba

Browse files
ckoheniShibiImRodryvladfranguSpaceEEC
committed
feat(Rest): add response and request events
Ported from discordjs/discord.js#6739 Co-authored-by: Shubham Parihar <[email protected]> Co-authored-by: Rodry <[email protected]> Co-authored-by: Vlad Frangu <[email protected]> Co-authored-by: SpaceEEC <[email protected]>
1 parent c22081d commit 7767eba

File tree

5 files changed

+116
-4
lines changed

5 files changed

+116
-4
lines changed

packages/rest/__tests__/REST.test.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import nock from 'nock';
22
import { DiscordSnowflake } from '@sapphire/snowflake';
3-
import { REST, DefaultRestOptions } from '../src';
3+
import { REST, DefaultRestOptions, APIRequest } from '../src';
44
import { Routes, Snowflake } from 'discord-api-types/v9';
5+
import { Response } from 'node-fetch';
56

67
const newSnowflake: Snowflake = DiscordSnowflake.generate().toString();
78

@@ -46,6 +47,9 @@ nock(`${DefaultRestOptions.api}/v${DefaultRestOptions.version}`)
4647
.delete('/channels/339942739275677727/messages/392063687801700356')
4748
.reply(200, { test: true })
4849
.delete(`/channels/339942739275677727/messages/${newSnowflake}`)
50+
.reply(200, { test: true })
51+
.get('/request')
52+
.times(2)
4953
.reply(200, { test: true });
5054

5155
test('simple GET', async () => {
@@ -176,3 +180,43 @@ test('Old Message Delete Edge-Case: Old message', async () => {
176180
test('Old Message Delete Edge-Case: New message', async () => {
177181
expect(await api.delete(Routes.channelMessage('339942739275677727', newSnowflake))).toStrictEqual({ test: true });
178182
});
183+
184+
test('Request and Response Events', async () => {
185+
const requestListener = jest.fn();
186+
const responseListener = jest.fn();
187+
188+
api.on('request', requestListener);
189+
api.on('response', responseListener);
190+
191+
await api.get('/request');
192+
193+
expect(requestListener).toHaveBeenCalledTimes(1);
194+
expect(responseListener).toHaveBeenCalledTimes(1);
195+
expect(requestListener).toHaveBeenLastCalledWith<[APIRequest]>(
196+
expect.objectContaining({
197+
method: 'get',
198+
path: '/request',
199+
route: '/request',
200+
data: { attachments: undefined, body: undefined },
201+
retries: 0,
202+
}),
203+
);
204+
expect(responseListener).toHaveBeenLastCalledWith<[APIRequest, Response]>(
205+
expect.objectContaining({
206+
method: 'get',
207+
path: '/request',
208+
route: '/request',
209+
data: { attachments: undefined, body: undefined },
210+
retries: 0,
211+
}),
212+
expect.objectContaining({ status: 200, statusText: 'OK' }),
213+
);
214+
215+
api.off('request', requestListener);
216+
api.off('response', responseListener);
217+
218+
await api.get('/request');
219+
220+
expect(requestListener).toHaveBeenCalledTimes(1);
221+
expect(responseListener).toHaveBeenCalledTimes(1);
222+
});

packages/rest/src/lib/REST.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { CDN } from './CDN';
33
import { InternalRequest, RequestData, RequestManager, RequestMethod, RouteLike } from './RequestManager';
44
import { DefaultRestOptions, RESTEvents } from './utils/constants';
55
import type { AgentOptions } from 'node:https';
6+
import type { RequestInit, Response } from 'node-fetch';
67

78
/**
89
* Options to be passed when creating the REST instance
@@ -121,6 +122,33 @@ export interface RateLimitData {
121122
*/
122123
export type RateLimitQueueFilter = (rateLimitData: RateLimitData) => boolean | Promise<boolean>;
123124

125+
export interface APIRequest {
126+
/**
127+
* The HTTP method used in this request
128+
*/
129+
method: string;
130+
/**
131+
* The full path used to make the request
132+
*/
133+
path: RouteLike;
134+
/**
135+
* The API route identifying the ratelimit for this request
136+
*/
137+
route: string;
138+
/**
139+
* Additional HTTP options for this request
140+
*/
141+
options: RequestInit;
142+
/**
143+
* The data that was used to form the body of this request
144+
*/
145+
data: Pick<InternalRequest, 'attachments' | 'body'>;
146+
/**
147+
* The number of times this request has been attempted
148+
*/
149+
retries: number;
150+
}
151+
124152
export interface InvalidRequestWarningData {
125153
/**
126154
* Number of invalid requests that have been made in the window
@@ -136,6 +164,10 @@ export interface RestEvents {
136164
invalidRequestWarning: [invalidRequestInfo: InvalidRequestWarningData];
137165
restDebug: [info: string];
138166
rateLimited: [rateLimitInfo: RateLimitData];
167+
request: [request: APIRequest];
168+
response: [request: APIRequest, response: Response];
169+
newListener: [name: string, listener: (...args: any) => void];
170+
removeListener: [name: string, listener: (...args: any) => void];
139171
}
140172

141173
export interface REST {
@@ -166,6 +198,13 @@ export class REST extends EventEmitter {
166198
.on(RESTEvents.Debug, this.emit.bind(this, RESTEvents.Debug))
167199
.on(RESTEvents.RateLimited, this.emit.bind(this, RESTEvents.RateLimited))
168200
.on(RESTEvents.InvalidRequestWarning, this.emit.bind(this, RESTEvents.InvalidRequestWarning));
201+
202+
this.on('newListener', (name, listener) => {
203+
if (name === RESTEvents.Request || name === RESTEvents.Response) this.requestManager.on(name, listener);
204+
});
205+
this.on('removeListener', (name, listener) => {
206+
if (name === RESTEvents.Request || name === RESTEvents.Response) this.requestManager.off(name, listener);
207+
});
169208
}
170209

171210
/**

packages/rest/src/lib/RequestManager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export interface InternalRequest extends RequestData {
114114
export interface RouteData {
115115
majorParameter: string;
116116
bucketRoute: string;
117-
original: string;
117+
original: RouteLike;
118118
}
119119

120120
export interface RequestManager {
@@ -315,7 +315,7 @@ export class RequestManager extends EventEmitter {
315315
* @param method The HTTP method this endpoint is called without
316316
* @private
317317
*/
318-
private static generateRouteData(endpoint: string, method: RequestMethod): RouteData {
318+
private static generateRouteData(endpoint: RouteLike, method: RequestMethod): RouteData {
319319
const majorIdMatch = /^\/(?:channels|guilds|webhooks)\/(\d{16,19})/.exec(endpoint);
320320

321321
// Get the major id for this route - global otherwise

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,19 @@ export class SequentialHandler {
284284
}
285285
this.manager.globalRemaining--;
286286

287+
const method = options.method ?? 'get';
288+
289+
if (this.manager.listenerCount(RESTEvents.Request)) {
290+
this.manager.emit(RESTEvents.Request, {
291+
method,
292+
path: routeId.original,
293+
route: routeId.bucketRoute,
294+
options,
295+
data: bodyData,
296+
retries,
297+
});
298+
}
299+
287300
const controller = new AbortController();
288301
const timeout = setTimeout(() => controller.abort(), this.manager.options.timeout).unref();
289302
let res: Response;
@@ -303,9 +316,23 @@ export class SequentialHandler {
303316
clearTimeout(timeout);
304317
}
305318

319+
if (this.manager.listenerCount(RESTEvents.Response)) {
320+
this.manager.emit(
321+
RESTEvents.Response,
322+
{
323+
method,
324+
path: routeId.original,
325+
route: routeId.bucketRoute,
326+
options,
327+
data: bodyData,
328+
retries,
329+
},
330+
res.clone(),
331+
);
332+
}
333+
306334
let retryAfter = 0;
307335

308-
const method = options.method ?? 'get';
309336
const limit = res.headers.get('X-RateLimit-Limit');
310337
const remaining = res.headers.get('X-RateLimit-Remaining');
311338
const reset = res.headers.get('X-RateLimit-Reset-After');

packages/rest/src/lib/utils/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export const enum RESTEvents {
2828
Debug = 'restDebug',
2929
InvalidRequestWarning = 'invalidRequestWarning',
3030
RateLimited = 'rateLimited',
31+
Request = 'request',
32+
Response = 'response',
3133
}
3234

3335
export const ALLOWED_EXTENSIONS = ['webp', 'png', 'jpg', 'jpeg', 'gif'] as const;

0 commit comments

Comments
 (0)