From 89d2b414f9ca88d95e8e2d6a0ed6f944d0aa8b7e Mon Sep 17 00:00:00 2001 From: wisplite Date: Tue, 21 Apr 2026 22:26:47 -0500 Subject: [PATCH] separation of concerns --- dist/index.d.ts | 2 +- dist/index.js | 45 ++++------------------- dist/utils/websocket.d.ts | 15 ++++++++ dist/utils/websocket.js | 64 +++++++++++++++++++++++++++++++++ src/index.ts | 44 ++++------------------- src/utils/websocket.ts | 75 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 169 insertions(+), 76 deletions(-) create mode 100644 dist/utils/websocket.d.ts create mode 100644 dist/utils/websocket.js create mode 100644 src/utils/websocket.ts diff --git a/dist/index.d.ts b/dist/index.d.ts index 65da3ec..1060551 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -1,5 +1,5 @@ export declare class TetherClient { - private ws; + private websocketHandler; private subscribedQueries; connect: (url: string) => void; disconnect: () => void; diff --git a/dist/index.js b/dist/index.js index b076d71..44796dd 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,60 +1,29 @@ +import { WebSocketHandler } from './utils/websocket.js'; export class TetherClient { - ws = null; + websocketHandler = new WebSocketHandler(); subscribedQueries = new Map(); connect = (url) => { - this.ws = new WebSocket(url); - this.ws.onopen = () => { - console.log('Connected to Tether'); - }; - this.ws.onmessage = (event) => { - const data = JSON.parse(event.data); - if (data.type === 'query') { - this.subscribedQueries.forEach((callback, query) => { - if (data.query === query) { - callback(data.data); - } - }); - } - else if (data.type === 'error') { - console.error(data.error); - } - }; - this.ws.onclose = () => { - console.log('Disconnected from Tether'); - }; + this.websocketHandler.startConnection(url); }; disconnect = () => { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - throw new Error('Not connected to Tether'); - } - this.ws.close(); - this.ws = null; + this.websocketHandler.close(); }; subscribe = (query, callback) => { this.subscribedQueries.set(query, callback); - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - throw new Error('Not connected to Tether'); - } - this.ws.send(JSON.stringify({ + this.websocketHandler.send(JSON.stringify({ type: 'subscribe', query: query })); }; unsubscribe = (query) => { this.subscribedQueries.delete(query); - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - throw new Error('Not connected to Tether'); - } - this.ws.send(JSON.stringify({ + this.websocketHandler.send(JSON.stringify({ type: 'unsubscribe', query: query })); }; sendMutation = (mutationName, params) => { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - throw new Error('Not connected to Tether'); - } - this.ws.send(JSON.stringify({ + this.websocketHandler.send(JSON.stringify({ type: 'mutation', name: mutationName, payload: params, diff --git a/dist/utils/websocket.d.ts b/dist/utils/websocket.d.ts new file mode 100644 index 0000000..ae4be2f --- /dev/null +++ b/dist/utils/websocket.d.ts @@ -0,0 +1,15 @@ +export declare class WebSocketHandler { + private ws; + private url; + private subscribedQueries; + private onOpen; + private onClose; + private reconnectAttempts; + private maxReconnectAttempts; + private reconnectInterval; + private sendQueue; + startConnection: (url: string) => void; + attemptReconnect: () => void; + close: () => void; + send: (message: string) => void; +} diff --git a/dist/utils/websocket.js b/dist/utils/websocket.js new file mode 100644 index 0000000..b15a41a --- /dev/null +++ b/dist/utils/websocket.js @@ -0,0 +1,64 @@ +export class WebSocketHandler { + ws = null; + url = ''; + subscribedQueries = new Map(); + onOpen = () => { }; + onClose = () => { }; + reconnectAttempts = 0; + maxReconnectAttempts = 5; + reconnectInterval = 1000; + sendQueue = []; + startConnection = (url) => { + this.url = url; + this.ws = new WebSocket(url); + const ws = this.ws; + ws.onopen = () => { + console.log('Connected to Tether'); + this.onOpen(); + if (this.sendQueue.length > 0) { + this.sendQueue.forEach(message => this.ws?.send(message)); + this.sendQueue = []; + } + this.reconnectAttempts = 0; + }; + ws.onmessage = (event) => { + const data = JSON.parse(String(event.data)); + if (data.type === 'query') { + this.subscribedQueries.forEach((callback, query) => { + if (data.query === query) { + callback(data.data); + } + }); + } + else if (data.type === 'error') { + console.error(data.error); + } + }; + ws.onclose = () => { + console.log('Disconnected from Tether'); + this.attemptReconnect(); + }; + }; + attemptReconnect = () => { + this.ws?.close(); + this.reconnectAttempts++; + if (this.reconnectAttempts > this.maxReconnectAttempts) { + console.error('Max reconnect attempts reached'); + return; + } + setTimeout(() => { + this.startConnection(this.url); + }, this.reconnectInterval); + }; + close = () => { + this.ws?.close(); + this.ws = null; + }; + send = (message) => { + if (this.ws?.readyState !== WebSocket.OPEN) { + this.sendQueue.push(message); + return; + } + this.ws?.send(message); + }; +} diff --git a/src/index.ts b/src/index.ts index 4e83ee4..a33c823 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,43 +1,19 @@ +import { WebSocketHandler } from './utils/websocket.js'; export class TetherClient { - private ws: WebSocket | null = null; + private websocketHandler: WebSocketHandler = new WebSocketHandler(); private subscribedQueries = new Map void>(); connect = (url: string) => { - this.ws = new WebSocket(url); - this.ws.onopen = () => { - console.log('Connected to Tether'); - }; - this.ws.onmessage = (event) => { - const data = JSON.parse(event.data); - if (data.type === 'query') { - this.subscribedQueries.forEach((callback, query) => { - if (data.query === query) { - callback(data.data); - } - }); - } else if (data.type === 'error') { - console.error(data.error); - } - }; - this.ws.onclose = () => { - console.log('Disconnected from Tether'); - }; + this.websocketHandler.startConnection(url); }; disconnect = () => { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - throw new Error('Not connected to Tether'); - } - this.ws.close(); - this.ws = null; + this.websocketHandler.close(); }; subscribe = (query: string, callback: (data: any) => void) => { this.subscribedQueries.set(query, callback); - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - throw new Error('Not connected to Tether'); - } - this.ws.send(JSON.stringify({ + this.websocketHandler.send(JSON.stringify({ type: 'subscribe', query: query })); @@ -45,20 +21,14 @@ export class TetherClient { unsubscribe = (query: string) => { this.subscribedQueries.delete(query); - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - throw new Error('Not connected to Tether'); - } - this.ws.send(JSON.stringify({ + this.websocketHandler.send(JSON.stringify({ type: 'unsubscribe', query: query })); }; sendMutation = (mutationName: string, params: any) => { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - throw new Error('Not connected to Tether'); - } - this.ws.send(JSON.stringify({ + this.websocketHandler.send(JSON.stringify({ type: 'mutation', name: mutationName, payload: params, diff --git a/src/utils/websocket.ts b/src/utils/websocket.ts new file mode 100644 index 0000000..8bb8dec --- /dev/null +++ b/src/utils/websocket.ts @@ -0,0 +1,75 @@ +export class WebSocketHandler { + private ws: WebSocket | null = null; + private url: string = ''; + private subscribedQueries = new Map void>(); + private onOpen: () => void = () => {}; + private onClose: () => void = () => {}; + private reconnectAttempts: number = 0; + private maxReconnectAttempts: number = 5; + private reconnectInterval: number = 1000; + private sendQueue: string[] = []; + + startConnection = (url: string) => { + this.url = url; + this.ws = new WebSocket(url); + const ws = this.ws; + + ws.onopen = () => { + console.log('Connected to Tether'); + this.onOpen(); + if (this.sendQueue.length > 0) { + this.sendQueue.forEach(message => this.ws?.send(message)); + this.sendQueue = []; + } + this.reconnectAttempts = 0; + }; + + ws.onmessage = (event: MessageEvent) => { + const data = JSON.parse(String(event.data)) as { + type: string; + query?: string; + data?: unknown; + error?: string; + }; + if (data.type === 'query') { + this.subscribedQueries.forEach((callback, query) => { + if (data.query === query) { + callback(data.data); + } + }); + } else if (data.type === 'error') { + console.error(data.error); + } + }; + + ws.onclose = () => { + console.log('Disconnected from Tether'); + this.attemptReconnect(); + }; + }; + + attemptReconnect = () => { + this.ws?.close(); + this.reconnectAttempts++; + if (this.reconnectAttempts > this.maxReconnectAttempts) { + console.error('Max reconnect attempts reached'); + return; + } + setTimeout(() => { + this.startConnection(this.url); + }, this.reconnectInterval); + }; + + close = () => { + this.ws?.close(); + this.ws = null; + }; + + send = (message: string) => { + if (this.ws?.readyState !== WebSocket.OPEN) { + this.sendQueue.push(message); + return; + } + this.ws?.send(message); + }; +}