websocket_class.js

/** @module websocket_class */
/* eslint no-unused-vars: 0 */

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

import { wsURL } from "./api_codes.js";
import { setDwarfDeviceID, analyzePacket } from "./api_utils.js";

class Queue {
  constructor(...elements) {
    // Initializing the queue with given arguments
    this.elements = [...elements];
  }
  // Proxying the push/shift methods
  push(...args) {
    return this.elements.push(...args);
  }
  shift(...args) {
    return this.elements.shift();
  }
  // Add some length utility methods
  get length() {
    return this.elements.length;
  }
  set length(length) {
    this.elements.length = length;
  }
}

/**
 * WebSocketHandler is the main interface to send messages to the Dwarf via websocket
 * It will analyse all messages received by the Dwarf and send the recieved messages to the caller
 * It's a singleton class
 * @class
 * @constructor
 * @public
 */
export class WebSocketHandler {
  static instance = undefined;
  socket = null;
  is_opened = false;

  IPDwarf = undefined;
  proxyURL = undefined;
  useHttps = false;
  WS_Packet = {};

  isCallbackMessages = false;
  packetCallbackMessages = {};
  isCallbackErrors = false;
  packetCallbackErrors = {};
  isCallbackConnectStates = false;
  packetCallbackConnectStates = {};

  callbackReconnectFunction = undefined;

  /**
   * closeSocketTimer : Timer could be defined to manage a connection time of the Dwarf that is too long
   */
  closeSocketTimer = undefined;

  /**
   * closeTimerHandler : function could be defined : to close the timeout timer.
   */
  closeTimerHandler = function () {};

  /**
   * onStopTimerHandler : function could be defined : this function will be called in case of error during connection
   */
  onStopTimerHandler = function () {};

  keep_connection = false;
  is_running = false;
  is_sending = false;
  is_receiving = false;
  is_stopping = false;
  is_buffered = false;
  sendingQueue = new Queue();

  is_pong_received = false;
  is_ping_stopped = true;
  signal_ping_stop = false;
  ping_interval = 10;

  nb_reconnect_default = 3;
  nb_reconnect = 3;
  nb_ping_error_default = 10;
  nb_ping_error = 10;

  /**
   * Create a link to the Api and set the IP address of the Dwarf to connect to
   * @param {string | undefined} IPDwarf ; Set the IP address of the Dwarf to connect to
   */
  constructor(IPDwarf) {
    if (IPDwarf) this.IPDwarf = IPDwarf;
    if (!WebSocketHandler.instance) {
      WebSocketHandler.instance = this;
      console.info("Creating a new WebSocketHandler with IP: ", IPDwarf);
    }
    return WebSocketHandler.instance;
  }

  /**
   * Set the IP address of the Dwarf to connect to
   * @param {string} IPDwarf ; Set the IP address of the Dwarf to connect to, force another one that was configured when calling the constructor.
   * @returns {Promise<void>}
   */
  async setNewIpDwarf(IPDwarf) {
    console.debug("websocket_class : setIpDwarf : ", IPDwarf);
    if (IPDwarf != this.IPDwarf) {
      console.debug(
        "websocket_class : new IP received, closing old one: ",
        this.IPDwarf
      );
      this.close();
      await sleep(1000);
    }
    this.IPDwarf = IPDwarf;
    console.debug("websocket_class : new Ip: ", this.IPDwarf);
  }

  /**
   * Set the URL address of the proxy uses to connect to the dwarf,
   * by default not used, set it to empty param for not using a proxy
   * @param {string} proxyURL ; Set the URL address of the Proxy the Dwarf to connect to.
   * @returns {Promise<void>}
   */
  async setProxyUrl(proxyURL = undefined) {
    if (!proxyURL) console.debug("websocket_class : Resetting Proxy URL value");
    else console.debug("websocket_class : Setting Proxy URL: ", proxyURL);
    if (proxyURL != this.proxyURL) {
      console.debug(
        "websocket_class : new Proxy Url received, closing connection: ",
        this.proxyURL
      );
      this.close();
      await sleep(1000);
    }
    this.proxyURL = proxyURL;
    if (this.proxyURL)
      console.debug("websocket_class : Using Proxy URL: ", this.proxyURL);
    else console.debug("websocket_class : Proxy URL reset");
  }

  /**
   * Set the Https mode of the proxy connection, the Dwarf connect to
   * @param {boolean} useHttps ; true if Https, then wss should be used with a proxy connection
   * @returns {Promise<void>}
   */
  async setHttpsMode(useHttps) {
    console.debug("websocket_class : setHttpsMode : ", useHttps);
    if (useHttps !== this.useHttps) {
      console.debug(
        "websocket_class : change Https mode, closing connection. Previous  mode : ",
        this.useHttps ? "on" : "off"
      );
      this.close();
      await sleep(1000);
    }
    this.useHttps = useHttps;
    console.debug(
      "websocket_class : Updated HTTPS mode : ",
      this.useHttps ? "on" : "off"
    );
  }

  /**
   * Set the device ID of the Dwarf connected (readen from the Dwarf or from the config file on the Dwarf)
   * @param {number} deviceIdDwarf ; Set the device ID of the Dwarf connected.
   * @returns {boolean} status
   */
  setDeviceIdDwarf(deviceIdDwarf) {
    console.debug("websocket_class : setDeviceIdDwarf : ", deviceIdDwarf);
    if (setDwarfDeviceID(deviceIdDwarf)) {
      console.debug(
        "websocket_class : success setting the device ID of the Dwarf : ",
        deviceIdDwarf
      );
      return true;
    } else {
      console.error(
        "websocket_class : error setting the device ID of the Dwarf : ",
        deviceIdDwarf
      );
      return false;
    }
  }

  /**
   * Set the intervall of the build in Ping function defult is 10s
   * @param {number} IntervalInSecond ; in Seconds
   * @returns {void}
   */
  setPingInterval(IntervalInSecond) {
    if (IntervalInSecond) {
      this.ping_interval = IntervalInSecond;
    }
  }

  /**
   * Set the nb of times for trying to reconnect to the Dwarf if the connection closes, default is 5.
   * @param {number} nbTimes ;
   * @returns {void}
   */
  setDefaultReconnect(nbTimes) {
    this.nb_reconnect_default = nbTimes;
  }

  /**
   * Set the nb of times to stop connection after not receiving pong, default is 10.
   * @param {number} nbTimes ;
   * @returns {void}
   */
  setDefaultPongError(nbTimes) {
    this.nb_ping_error_default = nbTimes;
  }

  /**
   * Verify the status of the connection with the Dwarf
   * @returns {boolean} status of the connection
   */
  isConnected() {
    if (
      this.socket &&
      this.is_opened &&
      this.socket.readyState === WebSocket.OPEN
    )
      return true;
    else return false;
  }

  /**
   * Main function, to call after prepare function, send the message and start dialogue with the Dwarf
   * @returns {Promise<boolean>} false if the IP has not been set or if old Socket can't be closed
   */
  async run() {
    // Check if ipDwarf is defined before calling wsURL
    if (!this.IPDwarf) {
      console.error("IPDwarf is undefined. Unable to create WebSocket.");
      return false;
    }
    console.debug("websocket_class : running function starting...");

    try {
      await sleep(10);
      this.keep_connection = false;

      if (this.socket && this.socket.readyState === WebSocket.OPEN) {
        console.log("Keep old Websocket opened");
        this.keep_connection = true;
      } else {
        if (this.socket && this.socket.readyState !== WebSocket.OPEN) {
          // Socket still hangs, hard close
          console.log("Old Websocket force close");
          this.cleanup_socket();
          await sleep(100);
        }
      }

      if (this.keep_connection) {
        // Need Stopping Ping
        await this.wait_ping_stop();
        // Start manually no open event
        this.start();
      } else {
        // restarting
        // verify Stopping Ping
        await this.wait_ping_stop();
        this.is_stopping = false;
        this.is_opened = false;

        // Create WebSocket
        this.socket = undefined;
        let new_socket = undefined;
        new_socket = new WebSocket(
          wsURL(this.IPDwarf, this.proxyURL, this.useHttps)
        );
        console.log("Launch open new Socket");

        // Socket Binary Mode
        new_socket.binaryType = "arraybuffer";

        new_socket.onopen = () => {
          if (new_socket) {
            this.socket = new_socket;
            console.debug("new socket created", new_socket);
            this.is_opened = true;

            if (!this.proxyURL)
              console.debug(
                `websocket_class : open... on IP : ${this.IPDwarf}`
              );
            else {
              console.debug(
                `websocket_class: open... on IP: ${this.IPDwarf} using proxy: ${
                  this.proxyURL || "none"
                }${this.useHttps ? " (HTTPS on)" : ""}`
              );
            }
            console.debug("class instance open:", this);

            // Start on the open event
            this.start();
          } else {
            console.debug(`websocket_class : open error socket undefined`);
          }
        };

        new_socket.onmessage = async (event) => {
          console.debug("websocket_class : onmessage function...");
          while (this.is_sending || this.is_buffered) {
            await sleep(10);
          }
          console.debug("websocket_class : onmessage function starting...");

          this.is_receiving = true;
          await this.handleMessage(event);
          this.is_receiving = false;

          console.debug("websocket_class : onmessage function ending...");
        };

        new_socket.onerror = (message) => {
          if (this.is_opened) {
            this.handleError(message);
          }
        };

        new_socket.onclose = async (message) => {
          if (this.is_opened) {
            await this.handleClose(message);
          }
          // Cleanup event handlers after disconnection
          await this.cleanup();
          if (false && new_socket) {
            new_socket.onopen = null;
            new_socket.onerror = null;
            new_socket.onclose = null;
          }
        };
      }
      console.debug("class instance :", this);

      return true;
    } catch (error) {
      console.error(
        "websocket_class Exception Error creating WebSocket:",
        error
      );
      this.socket = undefined;
      return false;
    }
  }

  start() {
    console.debug("websocket_class : start function ...");

    this.is_running = true;
    this.nb_reconnect = this.nb_reconnect_default;

    // Start ping command
    this.is_pong_received = true;
    this.pingDwarf();

    // start send function
    this.send();

    this.sendCallbackConnectStates(true);
  }

  /**
   * Prepare function : Define the message to send and the command to listen to and the callbacks functions
   * @param {Object|Object[]} WS_Packet ; Message or Array of Messages from the API to send to the Dwarf
   * @param {string} senderId ; identifier of the sender
   * @param {string[]} expectedResponseCmd ; List of the Command Id to listen to, can be "*" to get all commands.
   * @param {function} callbackMessage ; Callback Fonction (const customMessageHandler = (txt_info:string, result_data:object)) to analyse reponses from the Dwarf
   * @param {function} callbackConnectState ; Callback Fonction (const customStateHandler = (state)) to get the status result of the current connection of the Dwarf
   * @param {function} callbackError ; Callback Fonction (const customErrorHandler = ()) called after an socket error.
   * @param {function} callbackReconnect ; Callback Fonction (const customReconnectHandler = ()) called after a socket reconnection.
   * @returns {Promise<void>}
   */
  async prepare(
    WS_Packet, // can be an array of Packets
    senderId,
    expectedResponseCmd = [],
    callbackMessage = function () {},
    callbackConnectState = function () {},
    callbackError = function () {},
    callbackReconnect = undefined
  ) {
    console.debug("websocket_class : prepare function...");

    while (this.is_sending || this.is_receiving) {
      await sleep(10);
    }

    console.debug("websocket_class : prepare function starting...");

    this.is_buffered = true;

    // Store the callback function and expected response types for later use
    if (senderId) {
      if (callbackMessage && typeof callbackMessage === "function") {
        if (!this.packetCallbackMessages[senderId]) {
          this.packetCallbackMessages[senderId] = {};
        }

        expectedResponseCmd.forEach((idCmd) => {
          console.log(` -> Prepare for  ${senderId} => ${idCmd}`);
          // empty the tab if exist : just one callback function per command
          this.packetCallbackMessages[senderId][idCmd] = [];
          this.packetCallbackMessages[senderId][idCmd].push(callbackMessage);
        });
      }

      if (callbackConnectState && typeof callbackConnectState === "function") {
        // empty the tab if exist : just one callback function per sender
        this.packetCallbackConnectStates[senderId] = [];

        this.packetCallbackConnectStates[senderId].push(callbackConnectState);
      }

      if (callbackError && typeof callbackError === "function") {
        // empty the tab if exist : just one callback function per sender
        this.packetCallbackErrors[senderId] = [];

        this.packetCallbackErrors[senderId].push(callbackError);
      }
      if (callbackReconnect && typeof callbackReconnect === "function") {
        // empty the tab if exist : just one callback Reconnect function
        this.callbackReconnectFunction = callbackReconnect;

        console.log(
          ` -> Add a callbackReconnect function => ${callbackReconnect}`
        );
      }
      this.verifyCallBacks();
    }

    if (Array.isArray(WS_Packet)) {
      console.log(` -> Prepare ${WS_Packet.length} packets for ${senderId}`);
      this.sendingQueue.push(...WS_Packet);
    } else {
      console.log(` -> Prepare one packet for ${senderId}`);
      this.sendingQueue.push(WS_Packet);
    }

    await sleep(50);

    this.is_buffered = false;

    console.debug("websocket_class : prepare function ending...");
  }

  async pingDwarf() {
    console.debug("websocket_class : init ping function...");

    this.is_ping_stopped = false;
    await sleep(10);

    while (!this.is_running) {
      await sleep(10);
    }

    console.debug("websocket_class : ping function...");
    console.debug("websocket_class : is_running...", this.is_running);
    console.debug("websocket_class : is_stopping...", this.is_stopping);
    console.debug(
      "websocket_class : signal_ping_stop...",
      this.signal_ping_stop
    );
    console.debug("websocket_class : is_stopping...", this.is_stopping);
    console.debug(
      "websocket_class : is_pong_received...",
      this.is_pong_received
    );

    this.is_sending = false;
    this.nb_ping_error = this.nb_ping_error_default;
    let interval_no_ping = this.ping_interval * 10 + 1;
    let interval = this.ping_interval;
    let ping_send = false;

    while (!this.is_stopping && !this.signal_ping_stop) {
      await sleep(100);

      if (!this.is_sending && this.is_pong_received && this.isConnected()) {
        console.debug("websocket_class : ping function starting...");
        this.is_sending = true;
        // reset interval_no_ping
        interval_no_ping = this.ping_interval * 10 + 1;
        this.nb_ping_error = this.nb_ping_error_default;

        // Send Command:
        this.is_pong_received = false;
        //this.socket.ping("");
        this.socket.send("ping");
        console.log("websocket_class : sending ping");
        this.is_sending = false;
        ping_send = true;
        console.debug("websocket_class : ping function waiting...");

        interval = this.ping_interval;
        console.debug(`websocket_class : ping interval wait : ${interval}`);
        while (interval > 0 && !this.is_stopping && !this.signal_ping_stop) {
          await sleep(1000);
          interval = interval - 1;
        }

        console.debug(`websocket_class : ping interval: ${interval}`);
        console.debug(`websocket_class : pong: ${this.is_pong_received}`);
        // Test if wet get Pong before the wait time in normal wait: no is_stopping nor signal_ping_stop
        if (
          interval == 0 &&
          !this.is_pong_received &&
          !this.is_stopping &&
          !this.signal_ping_stop
        ) {
          this.nb_ping_error -= 1;
          console.error(
            `websocket_class : no pong received after sending Ping ${
              this.nb_ping_error_default - this.nb_ping_error
            }`
          );
        } else if (this.is_pong_received) {
          // OK or stop reset
          this.nb_ping_error = this.nb_ping_error_default;
        }
        console.debug("websocket_class : ping function stopping...");
      }

      if (this.is_pong_received) {
        // OK or stop reset
        this.nb_ping_error = this.nb_ping_error_default;
        ping_send = false;
      }
      if (ping_send) {
        if (this.nb_ping_error != this.nb_ping_error_default) {
          interval_no_ping -= 1;

          if (interval_no_ping <= 0) {
            this.nb_ping_error -= 1;
            console.error(
              `websocket_class : no pong received after sending Ping ${
                this.nb_ping_error_default - this.nb_ping_error
              }`
            );

            if (this.nb_ping_error <= 0) {
              console.error(
                `websocket_class : no pong received after ${this.nb_ping_error_default} tries: deconnect!`
              );
              this.signal_ping_stop = true;
              this.cleanup(true);
            } else {
              interval_no_ping = this.ping_interval * 10;
            }
          }
        }
      }
    }

    this.is_sending = false;
    this.is_ping_stopped = true;

    console.debug("websocket_class : ping function ending...");
  }

  async wait_ping_stop() {
    this.signal_ping_stop = true;
    while (!this.is_ping_stopped) await sleep(100);
    this.signal_ping_stop = false;
  }

  async send() {
    await sleep(250);

    while (!this.is_running) {
      await sleep(10);
    }

    console.debug("websocket_class : send function...");

    this.is_sending = false;

    while (!this.is_stopping) {
      await sleep(10);
      let lenQueue = this.sendingQueue.length;

      if (
        !this.is_buffered &&
        !this.is_sending &&
        this.sendingQueue.length > 0 &&
        this.isConnected()
      ) {
        console.debug("websocket_class : send function starting...");
        this.is_sending = true;

        this.WS_Packet = this.sendingQueue.shift();

        // Send Command:
        if (this.WS_Packet) {
          this.socket.send(this.WS_Packet);
          console.log(
            `websocket_class : sending buffer = ${Array.prototype.toString.call(
              this.WS_Packet
            )}`
          );
          await sleep(100);
        } else {
          console.error(
            `websocket_class : sending buffer empty : lenqueue = ${lenQueue}`
          );
        }
        this.is_sending = false;
        console.debug("websocket_class : send function stopping...");
      }
    }

    this.is_sending = false;

    console.debug("websocket_class : send function ending...");
  }

  /**
   * stopCallbacks function : Stop receiving on the callbacks functions
   * @param {string} senderId ; Identifier of caller
   * @returns {void}
   **/
  stopCallbacks(senderId) {
    this.deleteCallbacks(senderId);
  }

  deleteCallbacks(senderId = "") {
    console.debug(`deleteCallbacks for  : ${senderId}`);
    if (senderId) {
      if (this.packetCallbackMessages[senderId]) {
        this.packetCallbackMessages[senderId] = {};
      }
      if (this.packetCallbackConnectStates[senderId]) {
        this.packetCallbackConnectStates[senderId] = [];
        this.isCallbackConnectStates = false;
      }
      if (this.packetCallbackErrors[senderId]) {
        this.packetCallbackErrors[senderId] = [];
        this.isCallbackErrors = false;
      }
    } else if (senderId == "*") {
      this.packetCallbackMessages = {};
      this.packetCallbackErrors = {};
      this.packetCallbackConnectStates = {};
      this.packetCallbackReconnect = [];
    }
    this.verifyCallBacks();
  }

  verifyCallBacks() {
    this.isCallbackMessages =
      Object.keys(this.packetCallbackMessages).length > 0;
    this.isCallbackConnectStates =
      Object.keys(this.packetCallbackConnectStates).length > 0;
    this.isCallbackErrors = Object.keys(this.packetCallbackErrors).length > 0;
  }

  sendCallbackConnectStates(state) {
    // Iterate over all stored callback functions
    Object.values(this.packetCallbackConnectStates).forEach(
      (callbacksConnectStates) => {
        console.log("Retrieve1 - Structure:", callbacksConnectStates);

        if (
          Array.isArray(callbacksConnectStates) &&
          callbacksConnectStates[0] &&
          typeof callbacksConnectStates[0] === "function"
        ) {
          // Call the callback function with the received data
          console.log(" -> Sending back data to callbackState....");
          callbacksConnectStates[0](state);
        }
      }
    );
  }

  sendCallbackErrors() {
    // Iterate over all stored callback functions
    Object.values(this.packetCallbackErrors).forEach((callbacksErrors) => {
      console.log("Retrieve2 - Structure:", callbacksErrors);

      if (
        Array.isArray(callbacksErrors) &&
        callbacksErrors[0] &&
        typeof callbacksErrors[0] === "function"
      ) {
        // Call the callback function with the received data
        console.log(" -> Sending back data to callbackError....");
        callbacksErrors[0]();
      }
    });
  }

  handleMessage(event) {
    // Close Timer if exist
    if (this.closeSocketTimer !== undefined) {
      clearTimeout(this.closeSocketTimer);
      if (this.closeTimerHandler !== undefined) this.closeTimerHandler();
    }
    // it can be a string like an array ??
    let find_real_string_buffer = false;
    if (typeof event.data === "string") {
      // Count the occurrences of commas
      const numberOfCommas = (event.data.match(/,/g) || []).length;

      if (numberOfCommas < 5) {
        console.log("Received: '" + event.data + "'");

        // test pong received ?
        if (event.data == "pong") {
          this.is_pong_received = true;
          console.log("Pong Received");
        }
        find_real_string_buffer = true;
      } else console.log("Received: a string buffer that matchs a binary one");
    }
    if (!find_real_string_buffer) {
      console.log(" -> Receiving data .....");

      // send Callback Status OK
      this.sendCallbackConnectStates(true);

      let decodedmessage = analyzePacket(event.data);
      console.log(decodedmessage);
      let result_data = JSON.parse(decodedmessage);

      // Call the subscriber if found
      let idCmd = "";
      if (result_data.cmd) {
        idCmd = result_data.cmd;
        console.log(`-> Sending back data for ${idCmd}`);

        // Retrieve the callback functions associated with the idCmd
        Object.entries(this.packetCallbackMessages).forEach(
          ([sender, callbacksMessageInfo]) => {
            // the special value "*" accepts all Cmd and will be transmited back
            const callbacksMessageCmd =
              callbacksMessageInfo[idCmd] || callbacksMessageInfo["*"] || {};

            console.log("Retrieve - Structure:", callbacksMessageCmd);

            if (
              Array.isArray(callbacksMessageCmd) &&
              callbacksMessageCmd[0] &&
              typeof callbacksMessageCmd[0] === "function"
            ) {
              // Call the callback function with the received data
              console.log(
                ` -> Sending back data to callbackInfo.... for ${sender}`
              );
              callbacksMessageCmd[0](sender, result_data);
            }
          }
        );
      }
    }
  }

  handleError(message) {
    // Stop Timer if exist
    if (this.closeSocketTimer !== undefined) {
      clearTimeout(this.closeSocketTimer);
      if (this.onStopTimerHandler !== undefined) this.onStopTimerHandler();
    }
    // send Callback Status KO
    this.sendCallbackConnectStates(false);
    // send Callback Error
    this.sendCallbackErrors();
  }

  /**
   * Handle close event of the socket connection with the Dwarf
   * To call from a timeout function during the connection with the Dwarf
   * @returns {Promise<void>}
   */
  async handleClose(message) {
    // Stop Timer if exist
    if (this.closeSocketTimer !== undefined) {
      clearTimeout(this.closeSocketTimer);
      if (this.onStopTimerHandler !== undefined) this.onStopTimerHandler();
    }
    // send Callback Status KO
    this.sendCallbackConnectStates(false);
    // send Callback Error
    this.sendCallbackErrors();
    // Stop ping command
    await this.wait_ping_stop();
  }

  /**
   * Force close the socket connection with the Dwarf
   * @returns {Promise<void>}
   */
  async close() {
    // need closing socket if connected
    if (this.socket && this.socket.readyState === WebSocket.OPEN) {
      console.log("Websocket close");
      this.socket.close(1000, "Normal closure");
      await sleep(1000);
    }
    if (this.socket && this.socket.readyState != WebSocket.CLOSED) {
      // Socket still hangs, hard close
      this.socket.close(1000, "Force close after timeout");
      console.log("Websocket force close");
      await sleep(1000);
    }
  }

  /**
   * cleanup function : Stop all the functions
   * @param {boolean} forceStop ; if true do not try a reconnection, false by default
   * @returns {Promise<void>}
   **/
  async cleanup(forceStop = false) {
    console.log("WebSocketHandler cleanup");
    if (forceStop) {
      // send Callback Status KO
      this.handleClose();
      console.error("WebSocketHandler Force Stop!");
    }
    let continue_cleanup = true;
    let needDisconnect = forceStop || !this.is_running;
    let initial_running = this.is_running;
    if (!forceStop && this.is_running) {
      // need to verify if callback functions are still OK if running
      let testCallbackMessages =
        Object.keys(this.packetCallbackMessages).length > 0;
      let testCallbackConnectStates =
        Object.keys(this.packetCallbackConnectStates).length > 0;
      let testCallbackErrors =
        Object.keys(this.packetCallbackErrors).length > 0;
      if (
        testCallbackMessages != this.isCallbackMessages ||
        testCallbackConnectStates != this.isCallbackConnectStates ||
        testCallbackErrors != this.isCallbackErrors
      ) {
        needDisconnect = true;
        console.log("WebSocketHandler need to be disconnect");
      }
    }
    if (this.closeSocketTimer !== undefined)
      clearTimeout(this.closeSocketTimer);
    this.is_stopping = true;
    this.is_running = false;
    console.log("WebSocketHandler close ping");
    await this.wait_ping_stop();
    // Remove event listeners during cleanup
    await this.cleanup_socket();
    await sleep(2000);
    let stop_try_reconnect = false;
    if (initial_running) {
      console.log(
        "WebSocketHandler max try connection: %d",
        this.nb_reconnect_default
      );
    }

    if (!needDisconnect && this.nb_reconnect > 0) {
      this.is_running = initial_running;
      this.nb_reconnect -= 1;
      console.log(
        "WebSocketHandler retry connection: %d",
        this.nb_reconnect_default - this.nb_reconnect
      );

      continue_cleanup = false;
      console.log("WebSocketHandler retry connection OK");
      if (this.callbackReconnectFunction) {
        console.log("WebSocketHandler launch Reconnect function");
        this.callbackReconnectFunction();
      }

      continue_cleanup = await !this.run();
    }
    if (continue_cleanup) {
      this.handleClose();
      if (this.closeSocketTimer !== undefined)
        clearTimeout(this.closeSocketTimer);
      // delete CallbacksFunction
      console.log("WebSocketHandler final closing functions");
      this.stopCallbacks("*");
      this.is_opened = false;
    }
  }

  async cleanup_socket() {
    // Remove event listeners during cleanup
    if (this.socket) {
      this.socket.onopen = null;
      this.socket.onmessage = null;
      this.socket.onerror = null;
      this.socket.onclose = null;
      this.socket = undefined;
    }
  }
}

/*

// Example usage:

    const customMessageHandler = (txt_info, result_data) => {
      if (result_data.cmd == Dwarfii_Api.DwarfCMD.CMD_NOTIFY_SDCARD_INFO) {
        setConnecting(false);
        return true;
      } else if (
        result_data.cmd ==
        Dwarfii_Api.DwarfCMD.CMD_CAMERA_TELE_GET_SYSTEM_WORKING_STATE
      ) {
        setConnecting(false);
      } else if (
        result_data.cmd == Dwarfii_Api.DwarfCMD.CMD_NOTIFY_WS_HOST_SLAVE_MODE
      ) {
        if (result_data.data.mode == 1) {
          console.log("WARNING SLAVE MODE");
          setSlavemode(true);
        } else {
          console.log("OK : HOST MODE");
          setSlavemode(false);
        }
        return true;
      } else {
        logger("", result_data, connectionCtx);
      }
      logger(txt_info, result_data, connectionCtx);
    };

    const customErrorHandler = () => {
      console.error("ConnectDwarf : Socket Close!");
      setConnecting(false);
      setConnectionStatus(false);
    };

    const customStateHandler = (state) => {
      setConnecting(false);
      setConnectionStatus(false);
    };

    // Create WebSocketHandler if need
    const webSocketHandler = new WebSocketHandler(IPDwarf);

    // Force IP
    webSocketHandler.setIpDwarf(IPDwarf);

    webSocketHandler.closeTimerHandler = () => {
      setConnecting(true);
    };
    webSocketHandler.onStopTimerHandler = () => {
      setConnecting(false);
    };

    // close socket is request takes too long
    webSocketHandler.closeSocketTimer = setTimeout(() => {
      webSocketHandler.handleClose("");
      console.log(" -> Close Timer.....");
      setConnecting(false);
      setConnectionStatus(false);
    }, 5000);


    // Send Commands : cmdCameraTeleGetSystemWorkingState
    let WS_Packet = messageCameraTeleGetSystemWorkingState();
    let WS_Packet1 = messageCameraTeleOpenCamera();
    let WS_Packet2 = messageCameraWideOpenCamera();
    let txtInfoCommand = "Connection";

    webSocketHandler.prepare(
      [WS_Packet, WS_Packet1, WS_Packet2],
      txtInfoCommand,
      [
        "*", // Get All Data
        Dwarfii_Api.DwarfCMD.CMD_NOTIFY_SDCARD_INFO,
        Dwarfii_Api.DwarfCMD.CMD_CAMERA_TELE_GET_SYSTEM_WORKING_STATE,
        Dwarfii_Api.DwarfCMD.CMD_NOTIFY_WS_HOST_SLAVE_MODE,
        Dwarfii_Api.DwarfCMD.CMD_CAMERA_TELE_OPEN_CAMERA,
        Dwarfii_Api.DwarfCMD.CMD_CAMERA_WIDE_OPEN_CAMERA,
      ],
      customMessageHandler,
      customStateHandler,
      customErrorHandler
    );
    
    if (!webSocketHandler.run()) {
      console.error(" Can't launch Web Socket Run Action!");
    }

*/