refactor: formalise event client options

This commit is contained in:
Paul Makles
2023-04-09 13:58:53 +01:00
parent 55066a651b
commit 802da3d9f6
5 changed files with 105 additions and 36 deletions
+12
View File
@@ -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'}
+19 -9
View File
@@ -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<Events> {
readonly events: EventClient<1>;
configuration: RevoltConfig | undefined;
session: Session | undefined;
#session: Session | undefined;
user: User | undefined;
readonly ready: Accessor<boolean>;
@@ -123,6 +130,7 @@ export class Client extends EventEmitter<Events> {
this.options = {
baseURL: "https://api.revolt.chat",
partials: false,
autoReconnect: true,
...options,
};
@@ -154,7 +162,9 @@ export class Client extends EventEmitter<Events> {
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<Events> {
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<Events> {
(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<Events> {
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<Events> {
*/
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<Events> {
*/
async loginBot(token: string) {
await this.#fetchConfiguration();
this.session = token;
this.#session = token;
this.#updateHeaders();
this.connect();
}
+57 -21
View File
@@ -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<T extends AvailableProtocols, P extends EventProtocol<T>> = {
export class EventClient<T extends AvailableProtocols> extends EventEmitter<
Events<T, EventProtocol<T>>
> {
readonly options: EventClientOptions;
#protocolVersion: T;
#transportFormat: "json" | "msgpack";
#heartbeatInterval: number;
#pongTimeout: number;
readonly ping: Accessor<number>;
#setPing: Setter<number>;
readonly state: Accessor<ConnectionState>;
#setStateSetter: Setter<ConnectionState>;
@@ -46,26 +72,33 @@ export class EventClient<T extends AvailableProtocols> 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<T extends AvailableProtocols> 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<T extends AvailableProtocols> extends EventEmitter<
this.#socket.onclose = () => {
if (closed) return;
closed = true;
clearInterval(this.#heartbeatIntervalReference);
this.disconnect();
};
}
@@ -131,7 +161,8 @@ export class EventClient<T extends AvailableProtocols> 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<T extends AvailableProtocols> extends EventEmitter<
* @param event Event
*/
send(event: EventProtocol<T>["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<T extends AvailableProtocols> extends EventEmitter<
* @param event Event
*/
handle(event: EventProtocol<T>["server"]) {
this.options.debug && console.info("[S->C]", event);
switch (event.type) {
case "Ping":
this.send({
@@ -159,6 +193,8 @@ export class EventClient<T extends AvailableProtocols> 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<T extends AvailableProtocols> 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:
+5 -6
View File
@@ -201,7 +201,6 @@ export async function handleEvent(
event: ServerMessage,
setReady: Setter<boolean>
) {
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;
}
}
+12
View File
@@ -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);