Add biome and autofix
This commit is contained in:
parent
0036079c75
commit
ba73b0a1f7
10 changed files with 350 additions and 318 deletions
|
@ -13,231 +13,231 @@ type SocketCloseHandler = () => void;
|
|||
type SocketCatostrophicErrorHandler = (error: ManagedSocketError) => void;
|
||||
|
||||
export type ManagedSocketConnectionState =
|
||||
| "connecting"
|
||||
| "open"
|
||||
| "closing"
|
||||
| "closed"
|
||||
| "reconnecting"
|
||||
| "terminated";
|
||||
| "connecting"
|
||||
| "open"
|
||||
| "closing"
|
||||
| "closed"
|
||||
| "reconnecting"
|
||||
| "terminated";
|
||||
|
||||
export class ManagedSocket {
|
||||
public readonly url: string;
|
||||
public state: ManagedSocketConnectionState = "connecting";
|
||||
private ws: WebSocket;
|
||||
public readonly url: string;
|
||||
public state: ManagedSocketConnectionState = "connecting";
|
||||
private ws: WebSocket;
|
||||
|
||||
private onMessageHandler: SocketDataHandler | null = null;
|
||||
private onErrorHandler: SocketErrorHandler | null = null;
|
||||
private onOpenHandler: SocketOpenHandler | null = null;
|
||||
private onCloseHandler: SocketCloseHandler | null = null;
|
||||
private onCatostrophicErrorHandler: SocketCatostrophicErrorHandler | null =
|
||||
null;
|
||||
private onMessageHandler: SocketDataHandler | null = null;
|
||||
private onErrorHandler: SocketErrorHandler | null = null;
|
||||
private onOpenHandler: SocketOpenHandler | null = null;
|
||||
private onCloseHandler: SocketCloseHandler | null = null;
|
||||
private onCatostrophicErrorHandler: SocketCatostrophicErrorHandler | null =
|
||||
null;
|
||||
|
||||
private reconnectAttempts: number;
|
||||
private maxReconnectDelay: number;
|
||||
private heartbeatInterval: Timer | null;
|
||||
private messageQueue: unknown[] = [];
|
||||
private attemptReconnect = true;
|
||||
private reconnectAttempts: number;
|
||||
private maxReconnectDelay: number;
|
||||
private heartbeatInterval: Timer | null;
|
||||
private messageQueue: unknown[] = [];
|
||||
private attemptReconnect = true;
|
||||
|
||||
private debugMode = false;
|
||||
private debugMode = false;
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
this.ws = this.connect();
|
||||
this.reconnectAttempts = 0;
|
||||
this.maxReconnectDelay = MAX_RECONNECT_DELAY;
|
||||
this.heartbeatInterval = null;
|
||||
}
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
this.ws = this.connect();
|
||||
this.reconnectAttempts = 0;
|
||||
this.maxReconnectDelay = MAX_RECONNECT_DELAY;
|
||||
this.heartbeatInterval = null;
|
||||
}
|
||||
|
||||
get connected() {
|
||||
return this.ws.readyState === WebSocket.OPEN;
|
||||
}
|
||||
get connected() {
|
||||
return this.ws.readyState === WebSocket.OPEN;
|
||||
}
|
||||
|
||||
get native() {
|
||||
return this.ws;
|
||||
}
|
||||
get native() {
|
||||
return this.ws;
|
||||
}
|
||||
|
||||
set debug(value: boolean) {
|
||||
this.debugMode = value;
|
||||
}
|
||||
set debug(value: boolean) {
|
||||
this.debugMode = value;
|
||||
}
|
||||
|
||||
debugMessage(...args: unknown[]) {
|
||||
if (this.debugMode) {
|
||||
const output = ["[ManagedSocketDebug]", ...args];
|
||||
console.log(...output);
|
||||
}
|
||||
}
|
||||
debugMessage(...args: unknown[]) {
|
||||
if (this.debugMode) {
|
||||
const output = ["[ManagedSocketDebug]", ...args];
|
||||
console.log(...output);
|
||||
}
|
||||
}
|
||||
|
||||
private sendQueuedMessages() {
|
||||
// Make a copy of the queue and clear it first.
|
||||
// this way if the messages fail to send again,
|
||||
// they just get requeued
|
||||
const messagesToSend = [...this.messageQueue];
|
||||
this.messageQueue = [];
|
||||
for (const message of messagesToSend) {
|
||||
this.send(message);
|
||||
}
|
||||
}
|
||||
private sendQueuedMessages() {
|
||||
// Make a copy of the queue and clear it first.
|
||||
// this way if the messages fail to send again,
|
||||
// they just get requeued
|
||||
const messagesToSend = [...this.messageQueue];
|
||||
this.messageQueue = [];
|
||||
for (const message of messagesToSend) {
|
||||
this.send(message);
|
||||
}
|
||||
}
|
||||
|
||||
private connect() {
|
||||
this.ws = new WebSocket(this.url);
|
||||
private connect() {
|
||||
this.ws = new WebSocket(this.url);
|
||||
|
||||
this.ws.addEventListener("open", () => {
|
||||
this.reconnectAttempts = 0;
|
||||
this.ws.addEventListener("open", () => {
|
||||
this.reconnectAttempts = 0;
|
||||
|
||||
// Announce successful reconnects
|
||||
if (this.state === "reconnecting") {
|
||||
this.debugMessage("Reconnected to socket server");
|
||||
}
|
||||
// Announce successful reconnects
|
||||
if (this.state === "reconnecting") {
|
||||
this.debugMessage("Reconnected to socket server");
|
||||
}
|
||||
|
||||
this.onOpenHandler?.();
|
||||
this.state = "open";
|
||||
this.startHeartbeat();
|
||||
this.sendQueuedMessages();
|
||||
});
|
||||
this.onOpenHandler?.();
|
||||
this.state = "open";
|
||||
this.startHeartbeat();
|
||||
this.sendQueuedMessages();
|
||||
});
|
||||
|
||||
this.ws.addEventListener("close", (event) => {
|
||||
const { code } = event;
|
||||
this.debugMessage(code);
|
||||
this.onCloseHandler?.();
|
||||
this.ws.addEventListener("close", (event) => {
|
||||
const { code } = event;
|
||||
this.debugMessage(code);
|
||||
this.onCloseHandler?.();
|
||||
|
||||
// If the code received was catostrophic, terminate the connection
|
||||
if (SocketSpeak.isCatatrophicCloseCode(code)) {
|
||||
this.handleCatostophicError(code);
|
||||
}
|
||||
// If the code received was catostrophic, terminate the connection
|
||||
if (SocketSpeak.isCatatrophicCloseCode(code)) {
|
||||
this.handleCatostophicError(code);
|
||||
}
|
||||
|
||||
this.state = "closed";
|
||||
this.state = "closed";
|
||||
|
||||
if (this.attemptReconnect) {
|
||||
this.debugMessage(
|
||||
"Socket connection closed. Attempting reconnection...",
|
||||
);
|
||||
this.stopHeartbeat();
|
||||
this.reconnect();
|
||||
}
|
||||
});
|
||||
if (this.attemptReconnect) {
|
||||
this.debugMessage(
|
||||
"Socket connection closed. Attempting reconnection...",
|
||||
);
|
||||
this.stopHeartbeat();
|
||||
this.reconnect();
|
||||
}
|
||||
});
|
||||
|
||||
this.ws.addEventListener("error", (error) => {
|
||||
this.debugMessage("WebSocket error", error);
|
||||
this.ws.addEventListener("error", (error) => {
|
||||
this.debugMessage("WebSocket error", error);
|
||||
|
||||
const errorType =
|
||||
this.state === "connecting"
|
||||
? ManagedSocketErrorType.CONNECTION_REJECTED
|
||||
: ManagedSocketErrorType.SOCKET_ERROR;
|
||||
const errorType =
|
||||
this.state === "connecting"
|
||||
? ManagedSocketErrorType.CONNECTION_REJECTED
|
||||
: ManagedSocketErrorType.SOCKET_ERROR;
|
||||
|
||||
const socketError = new ManagedSocketError(
|
||||
errorType,
|
||||
new Error(error.currentTarget?.toString()),
|
||||
errorType,
|
||||
);
|
||||
const socketError = new ManagedSocketError(
|
||||
errorType,
|
||||
new Error(error.currentTarget?.toString()),
|
||||
errorType,
|
||||
);
|
||||
|
||||
if (this.onErrorHandler !== null) {
|
||||
return this.onErrorHandler(socketError);
|
||||
}
|
||||
if (this.onErrorHandler !== null) {
|
||||
return this.onErrorHandler(socketError);
|
||||
}
|
||||
|
||||
throw error;
|
||||
});
|
||||
throw error;
|
||||
});
|
||||
|
||||
this.ws.addEventListener("message", (event) => {
|
||||
const message = SocketSpeak.deserialize(event.data.toString());
|
||||
this.ws.addEventListener("message", (event) => {
|
||||
const message = SocketSpeak.deserialize(event.data.toString());
|
||||
|
||||
// Ignore heartbeats
|
||||
if (message.type === "heartbeat") {
|
||||
this.debugMessage("Received heartbeat");
|
||||
return;
|
||||
}
|
||||
// Ignore heartbeats
|
||||
if (message.type === "heartbeat") {
|
||||
this.debugMessage("Received heartbeat");
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.data === undefined || message.data === null) {
|
||||
this.debugMessage("Received message with no data");
|
||||
return;
|
||||
}
|
||||
if (message.data === undefined || message.data === null) {
|
||||
this.debugMessage("Received message with no data");
|
||||
return;
|
||||
}
|
||||
|
||||
this.debugMessage("Received message", message.data);
|
||||
this.onMessageHandler?.(message.data);
|
||||
});
|
||||
this.debugMessage("Received message", message.data);
|
||||
this.onMessageHandler?.(message.data);
|
||||
});
|
||||
|
||||
return this.ws;
|
||||
}
|
||||
return this.ws;
|
||||
}
|
||||
|
||||
handleCatostophicError(code: SocketDisconnectCode) {
|
||||
this.state = "terminated";
|
||||
this.attemptReconnect = false;
|
||||
this.debugMessage(
|
||||
"Socket connection terminated due to non-reconnect close code",
|
||||
);
|
||||
handleCatostophicError(code: SocketDisconnectCode) {
|
||||
this.state = "terminated";
|
||||
this.attemptReconnect = false;
|
||||
this.debugMessage(
|
||||
"Socket connection terminated due to non-reconnect close code",
|
||||
);
|
||||
|
||||
const socketError = new ManagedSocketError(
|
||||
"Socket connection terminated due to non-reconnect close code",
|
||||
new Error(code.toString()),
|
||||
ManagedSocketErrorType.CATOSTROPHIC_CLOSE,
|
||||
);
|
||||
const socketError = new ManagedSocketError(
|
||||
"Socket connection terminated due to non-reconnect close code",
|
||||
new Error(code.toString()),
|
||||
ManagedSocketErrorType.CATOSTROPHIC_CLOSE,
|
||||
);
|
||||
|
||||
if (this.onCatostrophicErrorHandler !== null) {
|
||||
return this.onCatostrophicErrorHandler(socketError);
|
||||
}
|
||||
if (this.onCatostrophicErrorHandler !== null) {
|
||||
return this.onCatostrophicErrorHandler(socketError);
|
||||
}
|
||||
|
||||
throw socketError;
|
||||
}
|
||||
throw socketError;
|
||||
}
|
||||
|
||||
onMessage(handler: SocketDataHandler) {
|
||||
this.onMessageHandler = handler;
|
||||
}
|
||||
onMessage(handler: SocketDataHandler) {
|
||||
this.onMessageHandler = handler;
|
||||
}
|
||||
|
||||
onError(handler: (error: ManagedSocketError) => void) {
|
||||
this.onErrorHandler = handler;
|
||||
}
|
||||
onError(handler: (error: ManagedSocketError) => void) {
|
||||
this.onErrorHandler = handler;
|
||||
}
|
||||
|
||||
onOpen(handler: () => void) {
|
||||
this.onOpenHandler = handler;
|
||||
}
|
||||
onOpen(handler: () => void) {
|
||||
this.onOpenHandler = handler;
|
||||
}
|
||||
|
||||
onClose(handler: () => void) {
|
||||
this.onCloseHandler = handler;
|
||||
}
|
||||
onClose(handler: () => void) {
|
||||
this.onCloseHandler = handler;
|
||||
}
|
||||
|
||||
reconnect() {
|
||||
this.attemptReconnect = true;
|
||||
this.state = "reconnecting";
|
||||
const delay = Math.min(
|
||||
1000 * 2 ** this.reconnectAttempts,
|
||||
this.maxReconnectDelay,
|
||||
);
|
||||
this.debugMessage(`Attempting to reconnect in ${delay}ms`);
|
||||
setTimeout(() => {
|
||||
this.reconnectAttempts++;
|
||||
this.connect();
|
||||
}, delay);
|
||||
}
|
||||
reconnect() {
|
||||
this.attemptReconnect = true;
|
||||
this.state = "reconnecting";
|
||||
const delay = Math.min(
|
||||
1000 * 2 ** this.reconnectAttempts,
|
||||
this.maxReconnectDelay,
|
||||
);
|
||||
this.debugMessage(`Attempting to reconnect in ${delay}ms`);
|
||||
setTimeout(() => {
|
||||
this.reconnectAttempts++;
|
||||
this.connect();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.attemptReconnect = false;
|
||||
this.state = "closing";
|
||||
this.ws.close();
|
||||
}
|
||||
close() {
|
||||
this.attemptReconnect = false;
|
||||
this.state = "closing";
|
||||
this.ws.close();
|
||||
}
|
||||
|
||||
private startHeartbeat() {
|
||||
this.heartbeatInterval = setInterval(() => {
|
||||
if (this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(
|
||||
SocketSpeak.serialize(SocketSpeak.createHeartbeatMessage()),
|
||||
);
|
||||
}
|
||||
}, HEARTBEAT_INTERVAL);
|
||||
}
|
||||
private startHeartbeat() {
|
||||
this.heartbeatInterval = setInterval(() => {
|
||||
if (this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(
|
||||
SocketSpeak.serialize(SocketSpeak.createHeartbeatMessage()),
|
||||
);
|
||||
}
|
||||
}, HEARTBEAT_INTERVAL);
|
||||
}
|
||||
|
||||
private stopHeartbeat() {
|
||||
if (this.heartbeatInterval !== null) {
|
||||
clearInterval(this.heartbeatInterval);
|
||||
this.heartbeatInterval = null;
|
||||
}
|
||||
}
|
||||
private stopHeartbeat() {
|
||||
if (this.heartbeatInterval !== null) {
|
||||
clearInterval(this.heartbeatInterval);
|
||||
this.heartbeatInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
send<T>(data: T) {
|
||||
if (this.ws.readyState !== WebSocket.OPEN) {
|
||||
this.debugMessage("WebSocket is not open. Queuing message.");
|
||||
this.messageQueue.push(data);
|
||||
return;
|
||||
}
|
||||
send<T>(data: T) {
|
||||
if (this.ws.readyState !== WebSocket.OPEN) {
|
||||
this.debugMessage("WebSocket is not open. Queuing message.");
|
||||
this.messageQueue.push(data);
|
||||
return;
|
||||
}
|
||||
|
||||
this.ws.send(SocketSpeak.prepare(data));
|
||||
}
|
||||
this.ws.send(SocketSpeak.prepare(data));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue