diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 000000000..1489d285f
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,6 @@
+{
+ "image": "mcr.microsoft.com/devcontainers/universal:2",
+ "features": {
+ "ghcr.io/nordcominc/devcontainer-features/android-sdk:1": {}
+ }
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 6a6718dd1..27d407663 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -59,6 +59,7 @@
"cordova-plugin-server": "file:src/plugins/server",
"cordova-plugin-sftp": "file:src/plugins/sftp",
"cordova-plugin-system": "file:src/plugins/system",
+ "cordova-plugin-websocket": "file:src/plugins/websocket",
"css-loader": "^7.1.2",
"mini-css-extract-plugin": "^2.9.0",
"path-browserify": "^1.0.1",
@@ -4635,6 +4636,10 @@
"resolved": "src/plugins/system",
"link": true
},
+ "node_modules/cordova-plugin-websocket": {
+ "resolved": "src/plugins/websocket",
+ "link": true
+ },
"node_modules/cordova-serve": {
"version": "4.0.1",
"license": "Apache-2.0",
@@ -10581,6 +10586,12 @@
"version": "1.0.3",
"dev": true,
"license": "ISC"
+ },
+ "src/plugins/websocket": {
+ "name": "cordova-plugin-websocket",
+ "version": "0.0.1",
+ "dev": true,
+ "license": "Apache-2.0"
}
}
}
diff --git a/package.json b/package.json
index c3c31c4f5..3b049cc7a 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,7 @@
"cordova-plugin-advanced-http": {
"ANDROIDBLACKLISTSECURESOCKETPROTOCOLS": "SSLv3,TLSv1"
},
+ "cordova-plugin-websocket": {},
"com.foxdebug.acode.exec": {}
},
"platforms": [
@@ -76,6 +77,7 @@
"cordova-plugin-server": "file:src/plugins/server",
"cordova-plugin-sftp": "file:src/plugins/sftp",
"cordova-plugin-system": "file:src/plugins/system",
+ "cordova-plugin-websocket": "file:src/plugins/websocket",
"css-loader": "^7.1.2",
"mini-css-extract-plugin": "^2.9.0",
"path-browserify": "^1.0.1",
diff --git a/src/plugins/websocket/README.md b/src/plugins/websocket/README.md
new file mode 100644
index 000000000..e2086e762
--- /dev/null
+++ b/src/plugins/websocket/README.md
@@ -0,0 +1,101 @@
+# Cordova Plugin: OkHttp WebSocket
+
+A Cordova plugin that uses [OkHttp](https://square.github.io/okhttp/) to provide WebSocket support in your Cordova app.
+It aims to mimic the [WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) in JavaScript, with additional features.
+
+## Features
+
+* ✅ WebSocket API-like interface
+* ✅ Event support: `onopen`, `onmessage`, `onerror`, `onclose`
+* ✅ `extensions` and `readyState` properties
+* ✅ `listClients()` to list active connections
+* ✅ Support for protocols
+* ✅ Support for Custom Headers.
+* ✅ Compatible with Cordova for Android
+
+---
+
+## Usage
+
+### Import
+
+```javascript
+const WebSocketPlugin = cordova.websocket;
+```
+
+### Connect to WebSocket
+
+```javascript
+WebSocketPlugin.connect("wss://example.com/socket", ["protocol1", "protocol2"], headers)
+ .then(ws => {
+ ws.onopen = (e) => console.log("Connected!", e);
+ ws.onmessage = (e) => console.log("Message:", e.data);
+ ws.onerror = (e) => console.error("Error:", e);
+ ws.onclose = (e) => console.log("Closed:", e);
+
+ ws.send("Hello from Cordova!");
+ ws.close();
+ })
+ .catch(err => console.error("WebSocket connection failed:", err));
+```
+
+---
+
+## API Reference
+
+### Methods
+
+* `WebSocketPlugin.connect(url, protocols, headers)`
+
+ * Connects to a WebSocket server.
+ * `url`: The WebSocket server URL.
+ * `protocols`: (Optional) An array of subprotocol strings.
+ * `headers` (object, optional): Custom headers as key-value pairs.
+ * **Returns:** A Promise that resolves to a `WebSocketInstance`.
+* `WebSocketPlugin.listClients()`
+ * Lists all stored webSocket instance IDs.
+ * **Returns:** `Promise`that resolves to an array of `instanceId` strings.
+
+* `WebSocketPlugin.send(instanceId, message)`
+ * same as `WebSocketInstance.send(message)` but needs `instanceId`.
+ * **Returns:** `Promise` that resolves.
+
+* `WebSocketPlugin.close(instanceId, code, reason)`
+ * same as `WebSocketInstance.close(code, reason)` but needs `instanceId`.
+ * **Returns:** `Promise` that resolves.
+
+* `WebSocketInstance.send(message)`
+
+ * Sends a message to the server.
+ * Throws an error if the connection is not open.
+
+* `WebSocketInstance.close(code, reason)`
+
+ * Closes the connection.
+ * `code`: (Optional) If unspecified, a close code for the connection is automatically set: to 1000 for a normal closure, or otherwise to [another standard value in the range 1001-1015](https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1) that indicates the actual reason the connection was closed.
+ * `reason`: A string providing a [custom WebSocket connection close reason](https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.6) (a concise human-readable prose explanation for the closure). The value must be no longer than 123 bytes (encoded in UTF-8).
+
+---
+
+### Properties of `WebSocketInstance`
+
+* `onopen`: Event listener for connection open.
+* `onmessage`: Event listener for messages received.
+* `onclose`: Event listener for connection close.
+* `onerror`: Event listener for errors.
+* `readyState`: (number) The state of the connection.
+
+ * 0 (`CONNECTING`): Socket created, not yet open.
+ * 1 (`OPEN`): Connection is open and ready.
+ * 2 (`CLOSING`): Connection is closing.
+ * 3 (`CLOSED`): Connection is closed or couldn't be opened.
+* `extensions`: (string) Extensions negotiated by the server (usually empty or a list).
+
+---
+
+## Notes
+
+* Only supported on Android (via OkHttp).
+* Make sure to handle connection lifecycle properly (close sockets when done).
+* `listClients()` is useful for debugging and management.
+---
\ No newline at end of file
diff --git a/src/plugins/websocket/package.json b/src/plugins/websocket/package.json
new file mode 100644
index 000000000..6277a9c93
--- /dev/null
+++ b/src/plugins/websocket/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "cordova-plugin-websocket",
+ "version": "0.0.1",
+ "description": "This cordova plugin is created to use WebSocket (client) in web/js.",
+ "cordova": {
+ "id": "cordova-plugin-websocket",
+ "platforms": [
+ "android"
+ ]
+ },
+ "keywords": [
+ "cordova",
+ "websocket",
+ "cordova-android",
+ "ws"
+ ],
+ "author": "Acode-Foundation (created by UnschooledGamer)",
+ "license": "Apache-2.0",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
\ No newline at end of file
diff --git a/src/plugins/websocket/plugin.xml b/src/plugins/websocket/plugin.xml
new file mode 100644
index 000000000..ad7b68421
--- /dev/null
+++ b/src/plugins/websocket/plugin.xml
@@ -0,0 +1,21 @@
+
+
+ cordova-plugin-websocket
+ Cordova Websocket
+ MIT
+ cordova,ws,WebSocket
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/plugins/websocket/src/android/WebSocketInstance.java b/src/plugins/websocket/src/android/WebSocketInstance.java
new file mode 100644
index 000000000..9f5ab5351
--- /dev/null
+++ b/src/plugins/websocket/src/android/WebSocketInstance.java
@@ -0,0 +1,194 @@
+package com.foxdebug.websocket;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import org.apache.cordova.*;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.Iterator;
+import java.util.concurrent.TimeUnit;
+
+import okhttp3.*;
+
+import okio.ByteString;
+
+public class WebSocketInstance extends WebSocketListener {
+ private static final int DEFAULT_CLOSE_CODE = 1000;
+ private static final String DEFAULT_CLOSE_REASON = "Normal closure";
+
+ private WebSocket webSocket;
+ private CallbackContext callbackContext;
+ private final CordovaInterface cordova;
+ private final String instanceId;
+ private String extensions = "";
+ private String protocol = "";
+ private String binaryType = "";
+ private int readyState = 0; // CONNECTING
+
+ // okHttpMainClient parameter is used. To have a single main client(singleton), with per-websocket configuration using newBuilder method.
+ public WebSocketInstance(String url, JSONArray protocols, JSONObject headers, String binaryType, OkHttpClient okHttpMainClient, CordovaInterface cordova, String instanceId) {
+ this.cordova = cordova;
+ this.instanceId = instanceId;
+ this.binaryType = binaryType;
+
+ OkHttpClient client = okHttpMainClient.newBuilder()
+ .connectTimeout(10, TimeUnit.SECONDS)
+ .build();
+
+ Request.Builder requestBuilder = new Request.Builder().url(url);
+
+ // custom headers support.
+ if (headers != null) {
+ Iterator keys = headers.keys();
+ while (keys.hasNext()) {
+ String key = keys.next();
+ String value = headers.optString(key);
+ requestBuilder.addHeader(key, value);
+ }
+ }
+
+ // adds Sec-WebSocket-Protocol header if protocols is present.
+ if (protocols != null) {
+ StringBuilder protocolHeader = new StringBuilder();
+ for (int i = 0; i < protocols.length(); i++) {
+ protocolHeader.append(protocols.optString(i)).append(",");
+ }
+ if (protocolHeader.length() > 0) {
+ protocolHeader.setLength(protocolHeader.length() - 1);
+ requestBuilder.addHeader("Sec-WebSocket-Protocol", protocolHeader.toString());
+ }
+ }
+
+ client.newWebSocket(requestBuilder.build(), this);
+ }
+
+ public void setCallback(CallbackContext callbackContext) {
+ this.callbackContext = callbackContext;
+ PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT);
+ result.setKeepCallback(true);
+ callbackContext.sendPluginResult(result);
+ }
+
+ public void send(String message) {
+ if (this.webSocket != null) {
+ this.webSocket.send(message);
+ Log.d("WebSocketInstance", "websocket instanceId=" + this.instanceId + " received send() action call, sending message=" + message);
+ } else {
+ Log.d("WebSocketInstance", "websocket instanceId=" + this.instanceId + " received send() action call, ignoring... as webSocket is null (not present)");
+ }
+ }
+
+ public String close(int code, String reason) {
+ if (this.webSocket != null) {
+ this.readyState = 2; // CLOSING
+ try {
+ boolean result = this.webSocket.close(code, reason);
+ Log.d("WebSocketInstance", "websocket instanceId=" + this.instanceId + " received close() action call");
+
+ // if a graceful shutdown was already underway...
+ // or if the web socket is already closed or canceled. do nothing.
+ if(!result) {
+ return null;
+ }
+ } catch (Exception e) {
+ return e.getMessage();
+ }
+
+ return null;
+ } else {
+ Log.d("WebSocketInstance", "websocket instanceId=" + this.instanceId + " received close() action call, ignoring... as webSocket is null (not present)");
+ // TODO: finding a better way of telling it wasn't successful.
+ return "";
+ }
+ }
+
+ public String close() {
+ Log.d("WebSocketInstance", "WebSocket instanceId=" + this.instanceId + " close() called with no arguments. Using defaults.");
+ // Calls the more specific version with default values
+ return close(DEFAULT_CLOSE_CODE, DEFAULT_CLOSE_REASON);
+ }
+
+ @Override
+ public void onOpen(@NonNull WebSocket webSocket, Response response) {
+ this.webSocket = webSocket;
+ this.readyState = 1; // OPEN
+ this.extensions = response.headers("Sec-WebSocket-Extensions").toString();
+ this.protocol = response.header("Sec-WebSocket-Protocol");
+ Log.i("WebSocketInstance", "websocket instanceId=" + this.instanceId + " Opened" + "received extensions=" + this.extensions);
+ sendEvent("open", null, false);
+ }
+
+ @Override
+ public void onMessage(@NonNull WebSocket webSocket, @NonNull String text) {
+ Log.d("WebSocketInstance", "websocket instanceId=" + this.instanceId + " Received message: " + text);
+ sendEvent("message", text, false);
+ }
+
+ // This is called when the Websocket server sends a binary(type 0x2) message.
+ @Override
+ public void onMessage(@NonNull WebSocket webSocket, @NonNull ByteString bytes) {
+ Log.d("WebSocketInstance", "websocket instanceId=" + this.instanceId + " Received message(bytes): " + bytes.toString());
+
+ try {
+ if ("arraybuffer".equals(this.binaryType)) {
+ String base64 = bytes.base64();
+ sendEvent("message", base64, true);
+ } else {
+ sendEvent("message", bytes.utf8(), true);
+ }
+ } catch (Exception e) {
+ Log.e("WebSocketInstance", "Error sending message", e);
+ }
+
+ }
+
+ @Override
+ public void onClosing(@NonNull WebSocket webSocket, int code, @NonNull String reason) {
+ this.readyState = 2; // CLOSING
+ Log.i("WebSocketInstance", "websocket instanceId=" + this.instanceId + " is Closing code: " + code + " reason: " + reason);
+ }
+
+ @Override
+ public void onClosed(@NonNull WebSocket webSocket, int code, @NonNull String reason) {
+ this.readyState = 3; // CLOSED
+ Log.i("WebSocketInstance", "websocket instanceId=" + this.instanceId + " Closed code: " + code + " reason: " + reason);
+ JSONObject closedEvent = new JSONObject();
+ try {
+ closedEvent.put("code", code);
+ closedEvent.put("reason", reason);
+ } catch (JSONException e) {
+ Log.e("WebSocketInstance", "Error creating close event", e);
+ }
+ sendEvent("close", closedEvent.toString(), false);
+ }
+
+ @Override
+ public void onFailure(@NonNull WebSocket webSocket, Throwable t, Response response) {
+ this.readyState = 3; // CLOSED
+ sendEvent("error", t.getMessage(), false);
+ Log.e("WebSocketInstance", "websocket instanceId=" + this.instanceId + " Error: " + t.getMessage());
+ }
+
+ private void sendEvent(String type, String data, boolean isBinary) {
+ if (callbackContext != null) {
+ try {
+ JSONObject event = new JSONObject();
+ event.put("type", type);
+ event.put("extensions", this.extensions);
+ event.put("readyState", this.readyState);
+ event.put("isBinary", isBinary ? true : false);
+ if (data != null) event.put("data", data);
+ Log.d("WebSocketInstance", "sending event: " + type + " eventObj " + event.toString());
+ PluginResult result = new PluginResult(PluginResult.Status.OK, event);
+ result.setKeepCallback(true);
+ callbackContext.sendPluginResult(result);
+ } catch (Exception e) {
+ Log.e("WebSocketInstance", "Error sending event", e);
+ }
+ }
+ }
+}
diff --git a/src/plugins/websocket/src/android/WebSocketPlugin.java b/src/plugins/websocket/src/android/WebSocketPlugin.java
new file mode 100644
index 000000000..67e9e359c
--- /dev/null
+++ b/src/plugins/websocket/src/android/WebSocketPlugin.java
@@ -0,0 +1,107 @@
+package com.foxdebug.websocket;
+
+import android.util.Log;
+
+import org.apache.cordova.*;
+import org.json.*;
+
+import java.util.HashMap;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+import okhttp3.OkHttpClient;
+
+// TODO: plugin init & plugin destroy(closing okhttp clients) lifecycles. (✅)
+public class WebSocketPlugin extends CordovaPlugin {
+ private static final ConcurrentHashMap instances = new ConcurrentHashMap<>();
+ public OkHttpClient okHttpMainClient = null;
+
+ @Override
+ protected void pluginInitialize() {
+ this.okHttpMainClient = new OkHttpClient();
+ }
+
+ @Override
+ public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException {
+ switch (action) {
+ case "connect":
+ String url = args.optString(0);
+ JSONArray protocols = args.optJSONArray(1);
+ JSONObject headers = args.optJSONObject(2);
+ String binaryType = args.optString(3, null);
+ String id = UUID.randomUUID().toString();
+ WebSocketInstance instance = new WebSocketInstance(url, protocols, headers, binaryType, this.okHttpMainClient, cordova, id);
+ instances.put(id, instance);
+ callbackContext.success(id);
+ return true;
+
+ case "send":
+ String instanceId = args.optString(0);
+ String message = args.optString(1);
+ WebSocketInstance inst = instances.get(instanceId);
+ Log.d("WebSocketPlugin", "send called");
+ if (inst != null) {
+ inst.send(message);
+ callbackContext.success();
+ } else {
+ callbackContext.error("Invalid instance ID");
+ }
+ return true;
+
+ case "close":
+ instanceId = args.optString(0);
+ // defaults code to 1000 & reason to "Normal closure"
+ int code = args.optInt(1, 1000);
+ String reason = args.optString(2, "Normal closure");
+ inst = instances.get(instanceId);
+ if (inst != null) {
+ String error = inst.close(code, reason);
+
+ if(error == null) {
+ instances.remove(instanceId);
+ callbackContext.success();
+ return true;
+ } else if(!error.isEmpty()) {
+ // if error is empty means the websocket is not ready/open.
+ callbackContext.error(error);
+ return true;
+ }
+ } else {
+ callbackContext.error("Invalid instance ID");
+ }
+ return true;
+
+ case "registerListener":
+ instanceId = args.optString(0);
+ inst = instances.get(instanceId);
+ if (inst != null) {
+ inst.setCallback(callbackContext);
+ } else {
+ callbackContext.error("Invalid instance ID");
+ }
+ return true;
+
+ case "listClients":
+ JSONArray clientIds = new JSONArray();
+ for (String clientId : instances.keySet()) {
+ clientIds.put(clientId);
+ }
+ callbackContext.success(clientIds);
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ // clear all.
+ for (WebSocketInstance instance : instances.values()) {
+ // Closing them gracefully.
+ instance.close();
+ }
+ instances.clear();
+ okHttpMainClient.dispatcher().executorService().shutdown();
+ Log.i("WebSocketPlugin", "cleaned up... on destroy");
+ }
+}
diff --git a/src/plugins/websocket/www/websocket.js b/src/plugins/websocket/www/websocket.js
new file mode 100644
index 000000000..7d1630acc
--- /dev/null
+++ b/src/plugins/websocket/www/websocket.js
@@ -0,0 +1,148 @@
+var exec = require('cordova/exec');
+/**
+ * Whether to log debug messages
+ */
+let DEBUG = false;
+
+const logIfDebug = (...args) => {
+ if (DEBUG) {
+ console.log(...args);
+ }
+};
+
+class WebSocketInstance extends EventTarget {
+ constructor(url, instanceId) {
+ super();
+ this.instanceId = instanceId;
+ this.extensions = '';
+ this.readyState = WebSocketInstance.CONNECTING;
+ this.onopen = null;
+ this.onmessage = null;
+ this.onclose = null;
+ this.onerror = null;
+ this.url = url;
+ this.binaryType = ''; // empty as Default is string.
+
+ exec((event) => {
+ logIfDebug(`[Cordova WebSocket - ID=${this.instanceId}] Event from native:`, event);
+
+ if (event.type === 'open') {
+ this.readyState = WebSocketInstance.OPEN;
+ this.extensions = event.extensions || '';
+ if (this.onopen) this.onopen(event);
+ this.dispatchEvent(new Event('open'));
+ }
+
+ if (event.type === 'message') {
+ let msgData = event.data;
+ if (event.isBinary && this.binaryType === 'arraybuffer') {
+ let binary = atob(msgData);
+ let bytes = new Uint8Array(binary.length);
+ for (let i = 0; i < binary.length; i++) {
+ bytes[i] = binary.charCodeAt(i);
+ }
+ msgData = bytes.buffer;
+ }
+ logIfDebug(`[Cordova WebSocket - ID=${this.instanceId}] msg Event:`, event, msgData);
+ const msgEvent = new MessageEvent('message', { data: msgData });
+ if (this.onmessage) this.onmessage(msgEvent);
+ this.dispatchEvent(msgEvent);
+ }
+
+ if (event.type === 'close') {
+ this.readyState = WebSocketInstance.CLOSED;
+ const closeEvent = new CloseEvent('close', { code: event.data?.code, reason: event.data?.reason });
+ if (this.onclose) this.onclose(closeEvent);
+ this.dispatchEvent(closeEvent);
+ }
+
+ if (event.type === 'error') {
+ const errorEvent = new Event('error', { message: event?.data });
+ if (this.onerror) this.onerror(errorEvent);
+ this.dispatchEvent(errorEvent);
+ }
+ }, null, "WebSocketPlugin", "registerListener", [this.instanceId]);
+ }
+
+ send(message) {
+ if (this.readyState !== WebSocketInstance.OPEN) {
+ throw new Error(`WebSocket is not open/connected`);
+ }
+
+ let finalMessage = null;
+ if (message instanceof ArrayBuffer || ArrayBuffer.isView(message)) {
+ const uint8Array = message instanceof ArrayBuffer ? new Uint8Array(message) : message;
+ finalMessage = btoa(String.fromCharCode.apply(null, uint8Array));
+
+ exec(() => logIfDebug(`[Cordova WebSocket - ID=${this.instanceId}] Sent message:`, finalMessage), (err) => console.error(`[Cordova WebSocket - ID=${this.instanceId}] Send error:`, err), "WebSocketPlugin", "send", [this.instanceId, finalMessage]);
+ } else if (typeof message === 'string') {
+ finalMessage = message;
+
+ exec(() => logIfDebug(`[Cordova WebSocket - ID=${this.instanceId}] Sent message:`, finalMessage), (err) => console.error(`[Cordova WebSocket - ID=${this.instanceId}] Send error:`, err), "WebSocketPlugin", "send", [this.instanceId, finalMessage]);
+ } else {
+ throw new Error(`Unsupported message type: ${typeof message}`);
+ }
+ }
+
+ /**
+ * Closes the WebSocket connection.
+ *
+ * @param {number} code The status code explaining why the connection is being closed.
+ * @param {string} reason A human-readable string explaining why the connection is being closed.
+ */
+ close(code, reason) {
+ this.readyState = WebSocketInstance.CLOSING;
+ exec(() => logIfDebug(`[Cordova WebSocket - ID=${this.instanceId}] Close requested`, code, reason), (err) => console.error(`[Cordova WebSocket - ID=${this.instanceId}] Close error`, err), "WebSocketPlugin", "close", [this.instanceId, code, reason]);
+ }
+}
+
+WebSocketInstance.CONNECTING = 0;
+WebSocketInstance.OPEN = 1;
+WebSocketInstance.CLOSING = 2;
+WebSocketInstance.CLOSED = 3;
+
+const connect = function(url, protocols = null, headers = null, binaryType) {
+ return new Promise((resolve, reject) => {
+ exec(instanceId => resolve(new WebSocketInstance(url, instanceId)), reject, "WebSocketPlugin", "connect", [url, protocols, binaryType, headers]);
+ });
+};
+
+const listClients = function() {
+ return new Promise((resolve, reject) => {
+ exec(resolve, reject, "WebSocketPlugin", "listClients", []);
+ });
+};
+
+/** Utility functions, in-case you lost the websocketInstance returned from the connect function */
+
+const send = function(instanceId, message) {
+ return new Promise((resolve, reject) => {
+ if (typeof message === 'string') {
+ exec(resolve, reject, "WebSocketPlugin", "send", [instanceId, message]);
+ } else if (message instanceof ArrayBuffer || ArrayBuffer.isView(message)) {
+ const uint8Array = message instanceof ArrayBuffer ? new Uint8Array(message) : message;
+ const base64Message = btoa(String.fromCharCode.apply(null, uint8Array));
+
+ exec(resolve, reject, "WebSocketPlugin", "send", [instanceId, base64Message]);
+ } else {
+ reject(`Unsupported message type: ${typeof message}`);
+ }
+ });
+};
+
+/**
+ * Closes the WebSocket connection.
+ *
+ * @param {string} instanceId The ID of the WebSocketInstance to close.
+ * @param {number} [code] (optional) The status code explaining why the connection is being closed.
+ * @param {string} [reason] (optional) A human-readable string explaining why the connection is being closed.
+ *
+ * @returns {Promise} A promise that resolves when the close operation has completed.
+ */
+const close = function(instanceId, code, reason) {
+ return new Promise((resolve, reject) => {
+ exec(resolve, reject, "WebSocketPlugin", "close", [instanceId, code, reason]);
+ });
+};
+
+module.exports = { connect, listClients, send, close, DEBUG };