feat: implement BenncClient class with connection management and messaging capabilities
This commit is contained in:
250
src/BenncClient.ts
Normal file
250
src/BenncClient.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import { MessageTypes } from "./common";
|
||||
import { packOutgoingPacket, unpackIncomingPacket } from "./messages/packet";
|
||||
import { packers, unpackers } from "./mapping";
|
||||
import { numberToUint16BE } from "./utilities/number";
|
||||
import Color from "color";
|
||||
|
||||
export interface BenncClientOptions {
|
||||
url: string;
|
||||
protocols?: string[];
|
||||
autoReconnect?: boolean;
|
||||
reconnectDelay?: number;
|
||||
maxReconnectAttempts?: number;
|
||||
}
|
||||
|
||||
export interface ConnectionState {
|
||||
connected: boolean;
|
||||
connecting: boolean;
|
||||
reconnectAttempts: number;
|
||||
}
|
||||
|
||||
type EventListener = (...args: any[]) => void;
|
||||
|
||||
export class BenncClient {
|
||||
private ws: WebSocket | null = null;
|
||||
private options: BenncClientOptions;
|
||||
private state: ConnectionState = {
|
||||
connected: false,
|
||||
connecting: false,
|
||||
reconnectAttempts: 0,
|
||||
};
|
||||
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private eventListeners: Map<string, EventListener[]> = new Map();
|
||||
|
||||
constructor(options: BenncClientOptions) {
|
||||
this.options = {
|
||||
autoReconnect: true,
|
||||
reconnectDelay: 5000,
|
||||
maxReconnectAttempts: 10,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
on(event: string, listener: EventListener): void {
|
||||
if (!this.eventListeners.has(event)) {
|
||||
this.eventListeners.set(event, []);
|
||||
}
|
||||
this.eventListeners.get(event)!.push(listener);
|
||||
}
|
||||
|
||||
off(event: string, listener: EventListener): void {
|
||||
const listeners = this.eventListeners.get(event);
|
||||
if (listeners) {
|
||||
const index = listeners.indexOf(listener);
|
||||
if (index !== -1) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit(event: string, ...args: any[]): void {
|
||||
const listeners = this.eventListeners.get(event);
|
||||
if (listeners) {
|
||||
listeners.forEach((listener) => listener(...args));
|
||||
}
|
||||
}
|
||||
|
||||
connect(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.state.connected || this.state.connecting) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.connecting = true;
|
||||
this.emit("connecting");
|
||||
|
||||
try {
|
||||
this.ws = new WebSocket(this.options.url, this.options.protocols);
|
||||
this.ws.binaryType = "arraybuffer";
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.state.connected = true;
|
||||
this.state.connecting = false;
|
||||
this.state.reconnectAttempts = 0;
|
||||
this.emit("connected");
|
||||
resolve();
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
this.handleMessage(event.data);
|
||||
};
|
||||
|
||||
this.ws.onclose = (event) => {
|
||||
this.state.connected = false;
|
||||
this.state.connecting = false;
|
||||
this.emit("disconnected", { code: event.code, reason: event.reason });
|
||||
|
||||
if (this.options.autoReconnect && this.shouldReconnect()) {
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
this.state.connecting = false;
|
||||
this.emit("error", error);
|
||||
reject(error);
|
||||
};
|
||||
} catch (error) {
|
||||
this.state.connecting = false;
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
|
||||
this.state.connected = false;
|
||||
this.state.connecting = false;
|
||||
}
|
||||
|
||||
isConnected(): boolean {
|
||||
return this.state.connected;
|
||||
}
|
||||
|
||||
getConnectionState(): ConnectionState {
|
||||
return { ...this.state };
|
||||
}
|
||||
|
||||
private sendRawMessage(messageType: number, data?: Uint8Array): void {
|
||||
if (!this.state.connected || !this.ws) {
|
||||
throw new Error("Client is not connected");
|
||||
}
|
||||
|
||||
const packet = packOutgoingPacket({
|
||||
messageType: numberToUint16BE(messageType),
|
||||
data,
|
||||
});
|
||||
|
||||
this.ws.send(packet);
|
||||
}
|
||||
|
||||
subscribe(messageType: MessageTypes): void {
|
||||
const packer = packers[MessageTypes.Subscribe];
|
||||
const data = packer({ messageType });
|
||||
this.sendRawMessage(MessageTypes.Subscribe, data);
|
||||
}
|
||||
|
||||
unsubscribe(messageType: MessageTypes): void {
|
||||
const packer = packers[MessageTypes.Unsubscribe];
|
||||
const data = packer({ messageType });
|
||||
this.sendRawMessage(MessageTypes.Unsubscribe, data);
|
||||
}
|
||||
|
||||
sendBasicMessage(message: string, key?: Uint8Array): void {
|
||||
const encoder = new TextEncoder();
|
||||
const messageBytes = encoder.encode(message);
|
||||
const packer = packers[MessageTypes.Basic];
|
||||
const data = packer(messageBytes, key);
|
||||
this.sendRawMessage(MessageTypes.Basic, data);
|
||||
}
|
||||
|
||||
sendUserDataRequest(
|
||||
username: string,
|
||||
colour: Color,
|
||||
clientId: string,
|
||||
key?: Uint8Array,
|
||||
): void {
|
||||
const packer = packers[MessageTypes.UserDataRequest];
|
||||
const data = packer({ username, colour, clientId }, key);
|
||||
this.sendRawMessage(MessageTypes.UserDataRequest, data);
|
||||
}
|
||||
|
||||
sendUserDataResponse(
|
||||
username: string,
|
||||
colour: Color,
|
||||
clientId: string,
|
||||
key?: Uint8Array,
|
||||
): void {
|
||||
const packer = packers[MessageTypes.UserDataResponse];
|
||||
const data = packer({ username, colour, clientId }, key);
|
||||
this.sendRawMessage(MessageTypes.UserDataResponse, data);
|
||||
}
|
||||
|
||||
sendKeepalive(): void {
|
||||
const packer = packers[MessageTypes.Keepalive];
|
||||
const data = packer();
|
||||
this.sendRawMessage(MessageTypes.Keepalive, data);
|
||||
}
|
||||
|
||||
requestHistory(): void {
|
||||
const packer = packers[MessageTypes.GetHistory];
|
||||
const data = packer();
|
||||
this.sendRawMessage(MessageTypes.GetHistory, data);
|
||||
}
|
||||
|
||||
private handleMessage(data: ArrayBuffer): void {
|
||||
try {
|
||||
const packet = unpackIncomingPacket(data);
|
||||
this.emit("packet", packet);
|
||||
|
||||
const unpacker = unpackers[packet.messageType as keyof typeof unpackers];
|
||||
if (unpacker) {
|
||||
const unpackedData = unpacker(packet.data);
|
||||
this.emit("message", {
|
||||
messageType: packet.messageType,
|
||||
senderId: packet.senderId,
|
||||
data: unpackedData,
|
||||
});
|
||||
this.emit(`message:${packet.messageType}`, {
|
||||
senderId: packet.senderId,
|
||||
data: unpackedData,
|
||||
});
|
||||
} else {
|
||||
this.emit("unknown-message", packet);
|
||||
}
|
||||
} catch (error) {
|
||||
this.emit("parse-error", error);
|
||||
}
|
||||
}
|
||||
|
||||
private shouldReconnect(): boolean {
|
||||
const maxAttempts = this.options.maxReconnectAttempts || 10;
|
||||
return this.state.reconnectAttempts < maxAttempts;
|
||||
}
|
||||
|
||||
private scheduleReconnect(): void {
|
||||
const delay = this.options.reconnectDelay || 5000;
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.state.reconnectAttempts++;
|
||||
this.emit("reconnecting", this.state.reconnectAttempts);
|
||||
this.connect().catch((error) => {
|
||||
this.emit("reconnect-failed", error);
|
||||
if (this.shouldReconnect()) {
|
||||
this.scheduleReconnect();
|
||||
} else {
|
||||
this.emit("reconnect-exhausted");
|
||||
}
|
||||
});
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
@@ -7,3 +7,5 @@ export type { SubscribeMessage } from "./messages/subscribe";
|
||||
export type { BasicMessage } from "./messages/basic";
|
||||
export type { UserDataRequestMessage } from "./messages/userDataRequest";
|
||||
export type { UserDataResponseMessage } from "./messages/userDataResponse";
|
||||
export { BenncClient } from "./BenncClient";
|
||||
export type { BenncClientOptions, ConnectionState } from "./BenncClient";
|
||||
|
||||
Reference in New Issue
Block a user