Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(ws): refactor ws code #20

Merged
merged 5 commits into from
Jan 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/common/lib/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const ROUTES = {
},
AWS_CDN: "https://s3-us-west-2.amazonaws.com/www.guilded.gg/" as const,
BASE_DOMAIN: "www.guilded.gg" as const,
WS_DOMAIN: "api.guilded.gg" as const,
CDN: "img.guildedcdn.com" as const,
IMAGE_CDN_DOMAIN: "img.guildedcdn.com" as const,
MEDIA_DOMAIN: "media.guilded.gg" as const,
Expand Down
4 changes: 1 addition & 3 deletions packages/guilded.js/lib/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ export class Client extends (EventEmitter as unknown as new () => TypedEmitter<C

/** The gateway manager for the bot to manage all gateway connections through websockets. */
gateway = new WebsocketManager({
token: this.options.token,
// todo: redo in ws refactor
handleEventPacket: (packet) => {},
token: this.options.token
});

/** The gateway event handlers will be processed by this manager. */
Expand Down
168 changes: 168 additions & 0 deletions packages/ws/lib/WebSocketManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import WebSocket, { EventEmitter } from "ws";
import { ROUTES } from "@guildedjs/common";
import type TypedEmitter from "typed-emitter";
import { SkeletonWSPayload, WSOpCodes } from "@guildedjs/guilded-api-typings";

export default class WebSocketManager {
/** The version of the websocket to connect to. */
version = this.options.version ?? 1;

/** Token used to authenticate requests. */
token = this.options.token;

/** The websocket connected to guilded. */
socket: WebSocket | null = null;

/** Whether or not this connection is connected and heartbeating. */
isAlive = false;

/** The amount of milliseconds the websocket took to respond to the last ping request. */
ping: number | null = null;

/** The timestamp in milliseconds of the last ping request. */
lastPingedAt = 0;

/** The last message id received. Used in the event of resuming connections. */
messageId: string | null = null;

/** The date since the last initial connection was established. */
connectedAt: Date | null = null;

/** Emitter in charge of emitting ws gateway related events */
emitter = new EventEmitter() as TypedEmitter<WebsocketManagerEvents>;

/** Count of how many times a reconnect has been attempted */
reconnectAttemptAmount = 0;

constructor(public readonly options: WebSocketOptions) {
if (this.options.autoConnect) this.connect();
}

/** The url that will be used to connect. Prioritizes proxy url and if not available uses the default base url for guidled. */
get wsURL() {
return this.options.proxyURL ?? `wss://${ROUTES.WS_DOMAIN}/v${this.version}/websocket`;
}

get reconnectAttemptExceeded() {
return this.reconnectAttemptAmount >= (this.options.reconnectAttemptLimit ?? Infinity);
}

connect() {
this.socket = new WebSocket(this.wsURL, {
headers: {
Authorization: `Bearer ${this.token}`,
},
});

this.socket.on("open", this.onSocketOpen.bind(this));

this.socket.on("message", (data) => {
this._debug(data);
this.onSocketMessage(data);
});

this.socket.on("error", (err) => {
this._debug("Gateway connection errored.");
this.emitter.emit("error", "Gateway Error", err);
if (!(this.options.autoConnectOnErr ?? true) || this.reconnectAttemptExceeded) {
this.reconnectAttemptAmount++;
return this.connect();
}
this.destroy();
this.emitter.emit("exit", "Gateway connection permanently closed due to error.");
});

this.socket.on("close", (code: number, reason: string) => {
this._debug(`Gateway connection terminated with code ${code} for reason: ${reason}`);
if (!(this.options.autoConnect ?? true) || this.reconnectAttemptExceeded) {
this.reconnectAttemptAmount++;
return this.connect();
}
this.destroy();
this.emitter.emit("exit", "Gateway connection permanently closed.");
});

this.socket.on("pong", this.onSocketPong.bind(this));
}

destroy() {
if (!this.socket) throw new Error("There is no active connection to destroy.");
this.socket.removeAllListeners();
if (this.socket.OPEN) this.socket.close();
}

onSocketMessage(packet: WebSocket.Data) {
this.emitter.emit("raw", packet);
if (typeof packet !== "string") {
this.emitter.emit("error", "packet was not typeof string", null);
return void 0;
}

let EVENT_NAME;
let EVENT_DATA;

try {
const data = JSON.parse(packet) as SkeletonWSPayload;
EVENT_NAME = data.t;
EVENT_DATA = data;
} catch (error) {
this.emitter.emit("error", "ERROR PARSING WS EVENT", error as Error, packet);
return void 0;
}

// SAVE THE ID IF AVAILABLE. USED FOR RESUMING CONNECTIONS.
if (EVENT_DATA.s) this.messageId = EVENT_DATA.s;

switch (EVENT_DATA.op) {
// Normal event based packets
case WSOpCodes.SUCCESS:
this.emitter.emit("gatewayEvent", EVENT_NAME, EVENT_DATA);
break;
// Auto handled by ws lib
case WSOpCodes.WELCOME:
break;
default:
this.emitter.emit("unknown", "unknown opcode", packet);
break;
}
}

onSocketOpen() {
this.isAlive = true;
this.connectedAt = new Date();
}

onSocketPong() {
this.ping = Date.now() - this.lastPingedAt;
}

_debug(str: any) {
return this.emitter.emit("debug", str);
}
}

export interface WebSocketOptions {
/** The bot's token. */
token: string;
/** The base url that the websocket will connect to. */
proxyURL?: string;
/** The version of the websocket to connect to. */
version?: 1;
/** Whether to connect automatically on instantiation. */
autoConnect?: boolean;
/** Whether to try to re-establish connection on error */
autoConnectOnErr?: boolean;
/** Limit of how many times a reconnection should be attempted */
reconnectAttemptLimit?: number;
}

// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
type WebsocketManagerEvents = {
debug: (data: any) => unknown;
raw: (data: any) => unknown;
error: (reason: string, err: Error | null, data?: any) => unknown;
exit: (info: string) => unknown;
unknown: (reason: string, data: any) => unknown;
reconnect: () => unknown;
gatewayEvent: (event: string, data: Record<string, any>) => unknown;
};
163 changes: 0 additions & 163 deletions packages/ws/lib/WebsocketManager.ts

This file was deleted.

4 changes: 2 additions & 2 deletions packages/ws/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import WebsocketManager from "./WebsocketManager";
import WebsocketManager from "./WebSocketManager";

export * from "./WebsocketManager";
export * from "./WebSocketManager";
export default WebsocketManager;
2 changes: 2 additions & 0 deletions packages/ws/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@
"devDependencies": {
"@guildedjs/guilded-api-typings": "^2.0.0",
"@types/ws": "^7.4.0",
"typed-emitter": "^2.1.0",
"typescript": "^4.4.2"
},
"dependencies": {
"@guildedjs/common": "^2.0.0",
"ws": "^7.4.0"
},
"files": [
Expand Down
Loading