From 802da3d9f6fe88c9cffd01751063f6af294bd354 Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Sun, 9 Apr 2023 13:58:53 +0100 Subject: [PATCH] refactor: formalise event client options --- pnpm-lock.yaml | 12 +++++++ src/Client.ts | 28 +++++++++++----- src/events/client.ts | 78 ++++++++++++++++++++++++++++++++------------ src/events/v1.ts | 11 +++---- test.js | 12 +++++++ 5 files changed, 105 insertions(+), 36 deletions(-) create mode 100644 test.js diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f79d5c8e..6db015d1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,9 @@ specifiers: eslint-config-prettier: ^8.8.0 eslint-plugin-solid: ^0.12.0 eslint-plugin-spellcheck: ^0.0.20 + eventemitter3: ^5.0.0 isomorphic-ws: ^5.0.0 + long: ^5.2.1 prettier: ^2.8.7 revolt-api: ^0.5.17 solid-js: ^1.7.2 @@ -26,7 +28,9 @@ specifiers: dependencies: '@solid-primitives/map': 0.4.3_solid-js@1.7.2 '@solid-primitives/set': 0.4.3_solid-js@1.7.2 + eventemitter3: 5.0.0 isomorphic-ws: 5.0.0_ws@8.13.0 + long: 5.2.1 revolt-api: 0.5.17 solid-js: 1.7.2 ulid: 2.3.0 @@ -904,6 +908,10 @@ packages: through: 2.3.8 dev: true + /eventemitter3/5.0.0: + resolution: {integrity: sha512-riuVbElZZNXLeLEoprfNYoDSwTBRR44X3mnhdI1YcnENpWTCsTTVZ2zFuqQcpoyqPQIUXdiPEU0ECAq0KQRaHg==} + dev: false + /fast-deep-equal/3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: true @@ -1404,6 +1412,10 @@ packages: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} dev: true + /long/5.2.1: + resolution: {integrity: sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==} + dev: false + /lru-cache/6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} diff --git a/src/Client.ts b/src/Client.ts index 49681868..203c0d8d 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -1,7 +1,7 @@ import { Accessor, Setter, createSignal } from "solid-js"; import EventEmitter from "eventemitter3"; -import { API, Metadata, RelationshipStatus, Role } from "revolt-api"; +import { API, Metadata, Role } from "revolt-api"; import type { DataLogin, RevoltConfig } from "revolt-api"; import { Channel, Emoji, Message, Server, ServerMember, User } from "./classes"; @@ -87,9 +87,16 @@ export interface ClientOptions { baseURL: string; /** - * Whether to allow partial objects to emit from events. + * Whether to allow partial objects to emit from events + * @default false */ partials: boolean; + + /** + * Whether to reconnect when disconnected + * @default true + */ + autoReconnect: boolean; } /** @@ -108,7 +115,7 @@ export class Client extends EventEmitter { readonly events: EventClient<1>; configuration: RevoltConfig | undefined; - session: Session | undefined; + #session: Session | undefined; user: User | undefined; readonly ready: Accessor; @@ -123,6 +130,7 @@ export class Client extends EventEmitter { this.options = { baseURL: "https://api.revolt.chat", partials: false, + autoReconnect: true, ...options, }; @@ -154,7 +162,9 @@ export class Client extends EventEmitter { break; case ConnectionState.Disconnected: this.emit("disconnected"); - setTimeout(() => this.connect(), 10000); + if (this.options.autoReconnect) { + setTimeout(() => this.connect(), 500); + } break; } }); @@ -172,7 +182,7 @@ export class Client extends EventEmitter { this.#setReady(false); this.events.connect( "wss://ws.revolt.chat", - typeof this.session === "string" ? this.session : this.session!.token + typeof this.#session === "string" ? this.#session : this.#session!.token ); } @@ -192,7 +202,7 @@ export class Client extends EventEmitter { (this.api as API) = new API({ baseURL: this.options.baseURL, authentication: { - revolt: this.session, + revolt: this.#session, }, }); } @@ -206,7 +216,7 @@ export class Client extends EventEmitter { await this.#fetchConfiguration(); const data = await this.api.post("/auth/session/login", details); if (data.result === "Success") { - this.session = data; + this.#session = data; // TODO: return await this.connect(); } else { throw "MFA not implemented!"; @@ -220,7 +230,7 @@ export class Client extends EventEmitter { */ async useExistingSession(session: Session) { await this.#fetchConfiguration(); - this.session = session; + this.#session = session; this.#updateHeaders(); this.connect(); } @@ -231,7 +241,7 @@ export class Client extends EventEmitter { */ async loginBot(token: string) { await this.#fetchConfiguration(); - this.session = token; + this.#session = token; this.#updateHeaders(); this.connect(); } diff --git a/src/events/client.ts b/src/events/client.ts index bef679db..1951346e 100644 --- a/src/events/client.ts +++ b/src/events/client.ts @@ -15,6 +15,29 @@ export enum ConnectionState { Disconnected, } +/** + * Event client options object + */ +export interface EventClientOptions { + /** + * Whether to log events + * @default false + */ + debug: boolean; + + /** + * Time in seconds between Ping packets sent to the server + * @default 30 + */ + heartbeatInterval: number; + + /** + * Maximum time in seconds between Ping and corresponding Pong + * @default 10 + */ + pongTimeout: number; +} + /** * Events provided by the client. */ @@ -30,10 +53,13 @@ type Events> = { export class EventClient extends EventEmitter< Events> > { + readonly options: EventClientOptions; + #protocolVersion: T; #transportFormat: "json" | "msgpack"; - #heartbeatInterval: number; - #pongTimeout: number; + + readonly ping: Accessor; + #setPing: Setter; readonly state: Accessor; #setStateSetter: Setter; @@ -46,26 +72,33 @@ export class EventClient extends EventEmitter< * Create a new event client. * @param protocolVersion Target protocol version * @param transportFormat Communication format - * @param heartbeatInterval Interval in seconds to send ping - * @param pongTimeout Time in seconds until heartbeat times out + * @param options Configuration options */ constructor( protocolVersion: T, transportFormat: "json" = "json", - heartbeatInterval = 30, - pongTimeout = 10 + options?: EventClientOptions ) { super(); this.#protocolVersion = protocolVersion; this.#transportFormat = transportFormat; - this.#heartbeatInterval = heartbeatInterval; - this.#pongTimeout = pongTimeout; + + this.options = { + heartbeatInterval: 30, + pongTimeout: 10, + debug: false, + ...options, + }; const [state, setState] = createSignal(ConnectionState.Idle); this.state = state; this.#setStateSetter = setState; + const [ping, setPing] = createSignal(-1); + this.ping = ping; + this.#setPing = setPing; + this.disconnect = this.disconnect.bind(this); } @@ -94,14 +127,13 @@ export class EventClient extends EventEmitter< ); this.#socket.onopen = () => { - this.#heartbeatIntervalReference = setInterval( - () => - (this.#pongTimeoutReference = setTimeout( - this.disconnect, - this.#pongTimeout * 1e3 - ) as never), - this.#heartbeatInterval & 1e3 - ) as never; + this.#heartbeatIntervalReference = setInterval(() => { + this.send({ type: "Ping", data: +new Date() }); + this.#pongTimeoutReference = setTimeout( + () => this.disconnect(), + this.options.pongTimeout * 1e3 + ) as never; + }, this.options.heartbeatInterval * 1e3) as never; }; this.#socket.onerror = (error) => { @@ -120,8 +152,6 @@ export class EventClient extends EventEmitter< this.#socket.onclose = () => { if (closed) return; closed = true; - - clearInterval(this.#heartbeatIntervalReference); this.disconnect(); }; } @@ -131,7 +161,8 @@ export class EventClient extends EventEmitter< */ disconnect() { if (!this.#socket) return; - let socket = this.#socket; + clearInterval(this.#heartbeatIntervalReference); + const socket = this.#socket; this.#socket = undefined; socket.close(); this.setState(ConnectionState.Disconnected); @@ -142,7 +173,9 @@ export class EventClient extends EventEmitter< * @param event Event */ send(event: EventProtocol["client"]) { - console.info(event); + this.options.debug && console.info("[C->S]", event); + if (!this.#socket) throw "Socket closed, trying to send."; + this.#socket.send(JSON.stringify(event)); } /** @@ -150,6 +183,7 @@ export class EventClient extends EventEmitter< * @param event Event */ handle(event: EventProtocol["server"]) { + this.options.debug && console.info("[S->C]", event); switch (event.type) { case "Ping": this.send({ @@ -159,6 +193,8 @@ export class EventClient extends EventEmitter< return; case "Pong": clearTimeout(this.#pongTimeoutReference); + this.#setPing(+new Date() - event.data); + this.options.debug && console.info(`[ping] ${this.ping()}ms`); return; case "Error": this.emit("error", event as never); @@ -174,7 +210,7 @@ export class EventClient extends EventEmitter< this.emit("event", event); this.setState(ConnectionState.Connected); } else { - console.error("WE ARE IN WRONG STATE"); + throw `Unreachable code. Received ${event.type} in Connecting state.`; } break; case ConnectionState.Connected: diff --git a/src/events/v1.ts b/src/events/v1.ts index 00d56b04..503eee78 100644 --- a/src/events/v1.ts +++ b/src/events/v1.ts @@ -201,7 +201,6 @@ export async function handleEvent( event: ServerMessage, setReady: Setter ) { - console.info("[EVENT]", JSON.stringify(event).substring(0, 64)); switch (event.type) { case "Bulk": { for (const item of event.v) { @@ -252,7 +251,7 @@ export async function handleEvent( }; client.messages.updateUnderlyingObject(event.id, { - ...(event.data as {}), + ...(event.data as object), editedAt: new Date(), }); @@ -760,20 +759,20 @@ export async function handleEvent( break; } case "Auth": { - if (typeof client.session === "string") throw "Unreachable code"; + /*if (typeof client.#session === "string") throw "Unreachable code"; switch (event.event_type) { case "DeleteSession": - if (event.session_id === client.session!._id) { + if (event.session_id === client.#session!._id) { // TODO: logout } break; case "DeleteAllSessions": - if (event.exclude_session_id !== client.session!._id) { + if (event.exclude_session_id !== client.#session!._id) { // TODO: logout } break; - } + }*/ break; } } diff --git a/test.js b/test.js new file mode 100644 index 00000000..4227d818 --- /dev/null +++ b/test.js @@ -0,0 +1,12 @@ +require("dotenv").config(); + +const { Client } = require("."); + +const client = new Client(); + +client.on("ready", () => console.info(`Logged in as ${client.user.username}!`)); +client.on("disconnected", () => console.info("Disconnected.")); + +client.on("messageCreate", (message) => console.info(message.content)); + +client.loginBot(process.env.TOKEN);