From a3e4a44ba024f478192a807b94e858d523ad4599 Mon Sep 17 00:00:00 2001 From: Shulhan Date: Mon, 27 Sep 2021 01:37:03 +0700 Subject: all: implement WebSocket client The WebSocket client have only one method "Send" that send request to the server based on predefined format WuiWebSocketRequest in synchronous way, which means it will wait for the response and pass it back to the caller based on the request ID. --- websocket_client.d.ts | 45 ++++++++++++++ websocket_client.ts | 167 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+) create mode 100644 websocket_client.d.ts create mode 100644 websocket_client.ts diff --git a/websocket_client.d.ts b/websocket_client.d.ts new file mode 100644 index 0000000..737fb21 --- /dev/null +++ b/websocket_client.d.ts @@ -0,0 +1,45 @@ +import { WuiResponseInterface } from "./response.js"; +interface RequestQueue { + req: WuiWebSocketRequest; + cbSuccess: (res: WuiWebSocketResponse) => void; + cbFail: (err: string) => void; +} +export interface WuiWebSocketOptions { + address: string; + insecure: boolean; + auto_reconnect: boolean; + auto_reconnect_interval: number; + onBroadcast: (res: WuiWebSocketResponse) => void; + onConnected: () => void; + onDisconnected: () => void; + onError: () => void; +} +export interface WuiWebSocketRequest { + id: number; + method: string; + target: string; + body?: string; +} +export interface WuiWebSocketResponse { + id: number; + code: number; + message: string; + body: string; +} +export declare class WuiWebSocketClient { + opts: WuiWebSocketOptions; + address: string; + conn: WebSocket; + requestQueue: RequestQueue[]; + reconnect_id: number; + isOpen: boolean; + error: string; + constructor(opts: WuiWebSocketOptions); + Send(req: WuiWebSocketRequest): Promise; + connect(): void; + onClose(ev: CloseEvent): void; + onError(ev: Event): void; + onMessage(ev: MessageEvent): void; + onOpen(ev: Event): void; +} +export {}; diff --git a/websocket_client.ts b/websocket_client.ts new file mode 100644 index 0000000..62d11fc --- /dev/null +++ b/websocket_client.ts @@ -0,0 +1,167 @@ +import { WuiResponseInterface } from "./response.js" + +const AUTO_RECONNECT_INTERVAL = 5000 + +interface RequestQueue { + req: WuiWebSocketRequest + cbSuccess: (res: WuiWebSocketResponse) => void + cbFail: (err: string) => void +} + +export interface WuiWebSocketOptions { + address: string + insecure: boolean // If true the client will connect without SSL. + auto_reconnect: boolean // If true the client will handle auto-reconnect. + auto_reconnect_interval: number // The interval for auto-reconnect, default to 5 seconds. + onBroadcast: (res: WuiWebSocketResponse) => void + onConnected: () => void + onDisconnected: () => void + onError: () => void +} + +export interface WuiWebSocketRequest { + id: number + method: string + target: string + body?: string +} + +export interface WuiWebSocketResponse { + id: number + code: number + message: string + body: string +} + +export class WuiWebSocketClient { + address: string + conn!: WebSocket + requestQueue: RequestQueue[] = [] + reconnect_id: number = 0 + isOpen: boolean = false + error: string = "" + + constructor(public opts: WuiWebSocketOptions) { + if (opts.insecure) { + this.address = "ws://" + opts.address + } else { + this.address = "wss://" + opts.address + } + if (opts.auto_reconnect) { + if (opts.auto_reconnect_interval <= 0) { + opts.auto_reconnect_interval = + AUTO_RECONNECT_INTERVAL + } + } + this.connect() + } + + // + // Send the request and wait for response similar to HTTP + // request-response. + // + async Send(req: WuiWebSocketRequest): Promise { + return new Promise((resolve, reject) => { + let wuiRes: WuiResponseInterface = { + code: 0, + message: "", + } + let reqQueue: RequestQueue = { + req: req, + cbSuccess: (res: WuiWebSocketResponse) => { + wuiRes.code = res.code + wuiRes.message = res.message + if ( + res.code === 200 && + res.body.length > 0 + ) { + wuiRes.data = JSON.parse( + atob(res.body), + ) + } + resolve(wuiRes) + }, + cbFail: (err: string) => { + wuiRes.code = 500 + wuiRes.message = err + resolve(wuiRes) + }, + } + this.requestQueue.push(reqQueue) + this.conn.send(JSON.stringify(req)) + }) + } + + connect() { + this.conn = new WebSocket(this.address) + + this.conn.onclose = (ev: CloseEvent) => { + this.onClose(ev) + } + this.conn.onerror = (ev: Event) => { + this.onError(ev) + } + this.conn.onmessage = (ev: MessageEvent) => { + this.onMessage(ev) + } + this.conn.onopen = (ev: Event) => { + this.onOpen(ev) + } + } + + // onClose handle connection closed by cleaning up the request + // queue. + onClose(ev: CloseEvent) { + for (let x = 0; x < this.requestQueue.length; x++) { + this.requestQueue[x].cbFail("connection closed") + } + + this.isOpen = false + this.error = "connection is closed by server" + + if (this.opts.auto_reconnect && !this.reconnect_id) { + this.reconnect_id = setInterval(() => { + this.connect() + }, this.opts.auto_reconnect_interval) + } + if (this.opts.onDisconnected) { + this.opts.onDisconnected() + } + } + + onError(ev: Event) { + if (this.opts.onError) { + this.opts.onError() + } + } + + onMessage(ev: MessageEvent) { + let res: WuiWebSocketResponse = JSON.parse(ev.data) + + for (let x = 0; x < this.requestQueue.length; x++) { + let reqq = this.requestQueue[x] + if (reqq.req.id === res.id) { + reqq.cbSuccess(res) + this.requestQueue.splice(x, 1) + return + } + } + + if (this.opts.onBroadcast) { + this.opts.onBroadcast(res) + } + } + + onOpen(ev: Event) { + this.isOpen = true + this.error = "" + + if (this.reconnect_id) { + clearInterval(this.reconnect_id) + this.reconnect_id = 0 + } + if (this.opts.onConnected) { + this.opts.onConnected() + } + } +} -- cgit v1.3