Skip to content
This repository has been archived by the owner on Jan 8, 2022. It is now read-only.

Commit

Permalink
feat(Rest): add response and request events (#85)
Browse files Browse the repository at this point in the history
Co-authored-by: Shubham Parihar <shubhamparihar391@gmail.com>
Co-authored-by: Rodry <38259440+ImRodry@users.noreply.github.com>
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
Co-authored-by: SpaceEEC <24881032+SpaceEEC@users.noreply.github.com>
  • Loading branch information
5 people authored Dec 8, 2021
1 parent 8ba5374 commit c3aba56
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 4 deletions.
46 changes: 45 additions & 1 deletion packages/rest/__tests__/REST.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import nock from 'nock';
import { DiscordSnowflake } from '@sapphire/snowflake';
import { REST, DefaultRestOptions } from '../src';
import { REST, DefaultRestOptions, APIRequest } from '../src';
import { Routes, Snowflake } from 'discord-api-types/v9';
import { Response } from 'node-fetch';

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

Expand Down Expand Up @@ -46,6 +47,9 @@ nock(`${DefaultRestOptions.api}/v${DefaultRestOptions.version}`)
.delete('/channels/339942739275677727/messages/392063687801700356')
.reply(200, { test: true })
.delete(`/channels/339942739275677727/messages/${newSnowflake}`)
.reply(200, { test: true })
.get('/request')
.times(2)
.reply(200, { test: true });

test('simple GET', async () => {
Expand Down Expand Up @@ -202,3 +206,43 @@ test('Old Message Delete Edge-Case: Old message', async () => {
test('Old Message Delete Edge-Case: New message', async () => {
expect(await api.delete(Routes.channelMessage('339942739275677727', newSnowflake))).toStrictEqual({ test: true });
});

test('Request and Response Events', async () => {
const requestListener = jest.fn();
const responseListener = jest.fn();

api.on('request', requestListener);
api.on('response', responseListener);

await api.get('/request');

expect(requestListener).toHaveBeenCalledTimes(1);
expect(responseListener).toHaveBeenCalledTimes(1);
expect(requestListener).toHaveBeenLastCalledWith<[APIRequest]>(
expect.objectContaining({
method: 'get',
path: '/request',
route: '/request',
data: { attachments: undefined, body: undefined },
retries: 0,
}),
);
expect(responseListener).toHaveBeenLastCalledWith<[APIRequest, Response]>(
expect.objectContaining({
method: 'get',
path: '/request',
route: '/request',
data: { attachments: undefined, body: undefined },
retries: 0,
}),
expect.objectContaining({ status: 200, statusText: 'OK' }),
);

api.off('request', requestListener);
api.off('response', responseListener);

await api.get('/request');

expect(requestListener).toHaveBeenCalledTimes(1);
expect(responseListener).toHaveBeenCalledTimes(1);
});
39 changes: 39 additions & 0 deletions packages/rest/src/lib/REST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CDN } from './CDN';
import { InternalRequest, RequestData, RequestManager, RequestMethod, RouteLike } from './RequestManager';
import { DefaultRestOptions, RESTEvents } from './utils/constants';
import type { AgentOptions } from 'node:https';
import type { RequestInit, Response } from 'node-fetch';

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

export interface APIRequest {
/**
* The HTTP method used in this request
*/
method: string;
/**
* The full path used to make the request
*/
path: RouteLike;
/**
* The API route identifying the ratelimit for this request
*/
route: string;
/**
* Additional HTTP options for this request
*/
options: RequestInit;
/**
* The data that was used to form the body of this request
*/
data: Pick<InternalRequest, 'attachments' | 'body'>;
/**
* The number of times this request has been attempted
*/
retries: number;
}

export interface InvalidRequestWarningData {
/**
* Number of invalid requests that have been made in the window
Expand All @@ -136,6 +164,10 @@ export interface RestEvents {
invalidRequestWarning: [invalidRequestInfo: InvalidRequestWarningData];
restDebug: [info: string];
rateLimited: [rateLimitInfo: RateLimitData];
request: [request: APIRequest];
response: [request: APIRequest, response: Response];
newListener: [name: string, listener: (...args: any) => void];
removeListener: [name: string, listener: (...args: any) => void];
}

export interface REST {
Expand Down Expand Up @@ -166,6 +198,13 @@ export class REST extends EventEmitter {
.on(RESTEvents.Debug, this.emit.bind(this, RESTEvents.Debug))
.on(RESTEvents.RateLimited, this.emit.bind(this, RESTEvents.RateLimited))
.on(RESTEvents.InvalidRequestWarning, this.emit.bind(this, RESTEvents.InvalidRequestWarning));

this.on('newListener', (name, listener) => {
if (name === RESTEvents.Request || name === RESTEvents.Response) this.requestManager.on(name, listener);
});
this.on('removeListener', (name, listener) => {
if (name === RESTEvents.Request || name === RESTEvents.Response) this.requestManager.off(name, listener);
});
}

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/rest/src/lib/RequestManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export interface InternalRequest extends RequestData {
export interface RouteData {
majorParameter: string;
bucketRoute: string;
original: string;
original: RouteLike;
}

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

// Get the major id for this route - global otherwise
Expand Down
29 changes: 28 additions & 1 deletion packages/rest/src/lib/handlers/SequentialHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,19 @@ export class SequentialHandler {
}
this.manager.globalRemaining--;

const method = options.method ?? 'get';

if (this.manager.listenerCount(RESTEvents.Request)) {
this.manager.emit(RESTEvents.Request, {
method,
path: routeId.original,
route: routeId.bucketRoute,
options,
data: bodyData,
retries,
});
}

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), this.manager.options.timeout).unref();
let res: Response;
Expand All @@ -303,9 +316,23 @@ export class SequentialHandler {
clearTimeout(timeout);
}

if (this.manager.listenerCount(RESTEvents.Response)) {
this.manager.emit(
RESTEvents.Response,
{
method,
path: routeId.original,
route: routeId.bucketRoute,
options,
data: bodyData,
retries,
},
res.clone(),
);
}

let retryAfter = 0;

const method = options.method ?? 'get';
const limit = res.headers.get('X-RateLimit-Limit');
const remaining = res.headers.get('X-RateLimit-Remaining');
const reset = res.headers.get('X-RateLimit-Reset-After');
Expand Down
2 changes: 2 additions & 0 deletions packages/rest/src/lib/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export const enum RESTEvents {
Debug = 'restDebug',
InvalidRequestWarning = 'invalidRequestWarning',
RateLimited = 'rateLimited',
Request = 'request',
Response = 'response',
}

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

0 comments on commit c3aba56

Please sign in to comment.