From fc082c45e8f4a4f4a6128d2bc93f2f69b04c68a6 Mon Sep 17 00:00:00 2001 From: Luca Conte Date: Wed, 21 Aug 2024 16:57:49 +0200 Subject: [PATCH] add SmartWebSocket.js and documentation --- README.md | 39 +++++++++- SmartWebSocket.js | 176 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 SmartWebSocket.js diff --git a/README.md b/README.md index d51486e..cbdd0dd 100644 --- a/README.md +++ b/README.md @@ -1 +1,38 @@ -# Smart WebSocket \ No newline at end of file +# Smart WebSocket + +```js +// constructor(socketAddress, options = [], autoReconnect = true, debugName = null) +let socket = SmartWebSocket("ws://127.0.0.1:8080", [], true, "sock"); + +// close if connection is open, then re-open connection +socket.reconnect() + +// close socket and disable auto-reconnect +socket.close() + +// is socket connection open +if (socket.isReady()) { + + // sends + // {"event" : "myEvent", "data" : { "abc" : "def" }} + // to server + // data is stringified using JSON before sending + socket.send("myEvent", { "abc" : "def"}); + + // sends + // abcdef + // to server. Same as calling socket.send on a regular WebSocket + socket.sendRaw("abcdef"); +} + +// is called when a JSON of the kind {"event" : "anotherEvent", "data" : {}} is received +socket.on("anotherEvent", (data) => { + console.log(data); +}); + +// same as calling socket.addEventListener("open") on a regular WebSocket +// same behaviour applies to events "close", "error" and "message" +socket.on("open", (event) => { + console.log(event); +}); +``` \ No newline at end of file diff --git a/SmartWebSocket.js b/SmartWebSocket.js new file mode 100644 index 0000000..caf9c56 --- /dev/null +++ b/SmartWebSocket.js @@ -0,0 +1,176 @@ +class SmartWebSocket{ + + socket; + url; + protocols; + debugName; + + autoreconnect; + + #events; + #reconnecting; + + + constructor(url, protocols = [], autoreconnect = true, debugName = null) { + this.url = url; + this.protocols = protocols; + this.autoreconnect = autoreconnect; + this.debugName = debugName; + + this.#events = {}; + this.#reconnecting = false; + + this.#connect(); + } + + #formatedConsole(method, message, color) { + if (color && typeof message == "string") { + console[method]("%c%s\t | %c%s", "font-weight: bold; color: cyan;", this.debugName ?? this.url, `color: ${color};`, message); + } else { + console[method]("%c%s\t | %c%s", "font-weight: bold; color: cyan;", this.debugName ?? this.url, "", message); + } + } + + #log(message, color = null) { + this.#formatedConsole("log", message, color); + } + #error(message) { + this.#formatedConsole("error", message); + } + #warn(message) { + this.#formatedConsole("warn", message); + } + #info(message, color = null) { + this.#formatedConsole("info", message, color); + } + + #connect() { + this.#info("Connecting socket...", "yellow"); + this.socket = new WebSocket(this.url, this.protocols); + + this.socket.addEventListener("open", (eventData) => this.#openHandler(eventData)); + this.socket.addEventListener("close", (eventData) => this.#closeHandler(eventData)); + this.socket.addEventListener("error", (eventData) => this.#errorHandler(eventData)); + this.socket.addEventListener("message", (eventData) => this.#messageHandler(eventData)); + } + + + #eventHandler(event, data = null) { + if (!(event in this.#events)) { + return; + } + for (let i = 0; i < this.#events[event].length; i++) { + this.#events[event][i](data); + } + } + + #openHandler(eventData) { + this.#info("Socket connected.", "lime"); + this.#reconnecting = false; + + this.#eventHandler("open", eventData); + } + + #closeHandler(eventData) { + if (eventData.wasClean) { + this.#info("Socket disconnected cleanly."); + } else { + if (!this.#reconnecting) { + this.#warn("Socket disconnected apruptly!"); + if (!this.autoreconnect) { + this.#warn("autoreconnect is set to false! Socket will not automatically reconnect in case of an unexpected loss of connection."); + } + } + } + if (this.autoreconnect) { + if (!this.#reconnecting) { + this.#info("Automatically reconnecting."); + this.#reconnecting = true; + } + this.reconnect(); + } + + this.#eventHandler("close", eventData); + } + + #errorHandler(eventData) { + if (!this.#reconnecting) { + this.#error(eventData); + } + this.#eventHandler("error", eventData); + } + + #messageHandler(eventData) { + let jsonObj = false; + try { + jsonObj = JSON.parse(eventData.data); + } catch (err) { + this.#warn("received non-JSON data from WebSocket"); + jsonObj = false; + } + + if (jsonObj && "event" in jsonObj) { + if ("data" in jsonObj) { + this.#eventHandler(jsonObj.event, jsonObj.data); + } else { + this.#eventHandler(jdonObj.event); + } + } + + this.#eventHandler("message", eventData); + } + + reconnect() { + if (this.socket != undefined && this.socket.readyState < 3) { + if (!this.autoreconnect) { + this.socket.addEventListener("close", () => this.#connect()); + } + this.socket.close(); + this.#info("Closing socket.", "lime") + } else { + this.#connect(); + } + } + + close() { + if (this.socket.readyState == 4) { + throw "socket already closed"; + } + this.autoreconnect = false; + this.socket.close(); + } + + isReady() { + return this.socket.readyState == 1; + } + + on(event, handler) { + if (typeof event != "string") { + throw "argument event is expected to be of type string"; + } + if (typeof handler != "function") { + throw "argument handler is expected to be of type function"; + } + + if (event in this.#events) { + this.#events[event].push(handler); + } else { + this.#events[event] = [handler]; + } + } + + send(event, data = null) { + if (typeof event != "string") { + throw "argument event is expected to be of type string"; + } + this.socket.send(JSON.stringify({event: event, data: data})); + } + + sendRaw(data) { + this.socket.send(data); + } + + get events() { + return this.#events; + } +} \ No newline at end of file