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..5805a0142
--- /dev/null
+++ b/src/plugins/websocket/README.md
@@ -0,0 +1,135 @@
+# 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, binaryType)`
+ * Connects to a WebSocket server.
+ * `url`: The WebSocket server URL.
+ * `protocols`: (Optional) An array of subprotocol strings.
+ * `headers`: (Optional) Custom headers as key-value pairs.
+ * `binaryType`: (Optional) Initial binary type setting.
+ * **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, binary)`
+ * Sends a message to the server using an instance ID.
+ * `instanceId`: The ID of the WebSocket instance.
+ * `message`: The message to send (string or ArrayBuffer/ArrayBufferView).
+ * `binary`: (Optional) Whether to send the message as binary, accepts `boolean`
+ * **Returns:** `Promise` that resolves when the message is sent.
+
+* `WebSocketPlugin.close(instanceId, code, reason)`
+ * same as `WebSocketInstance.close(code, reason)` but needs `instanceId`.
+ * **Returns:** `Promise` that resolves.
+
+### WebSocketInstance Methods
+
+* `WebSocketInstance.send(message, binary)`
+ * Sends a message to the server.
+ * `message`: The message to send (string or ArrayBuffer/ArrayBufferView).
+ * `binary`: (Optional) Whether to send the message as binary. accepts `boolean`
+ * 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.
+* `binaryType`: (string) Type of binary data to use ('arraybuffer' or '' (binary payload returned as strings.)).
+* `url`: (string) The WebSocket server URL.
+* `instanceId`: (string) Unique identifier for this WebSocket instance.
+
+### Event Handling
+
+`WebSocketInstance` extends `EventTarget`, providing standard event handling methods:
+
+* `addEventListener(type, listener)`: Registers an event listener.
+* `removeEventListener(type, listener)`: Removes an event listener.
+* `dispatchEvent(event)`: Dispatches an event to the object.
+
+Example of using event listeners:
+```javascript
+const ws = await WebSocketPlugin.connect("wss://example.com/socket");
+
+// Using on* properties
+ws.onmessage = (event) => console.log("Message:", event.data);
+
+// Using addEventListener
+ws.addEventListener('message', (event) => console.log("Message:", event.data));
+```
+
+### Constants
+
+* `WebSocketInstance.CONNECTING`: 0
+* `WebSocketInstance.OPEN`: 1
+* `WebSocketInstance.CLOSING`: 2
+* `WebSocketInstance.CLOSED`: 3
+
+---
+
+## 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..dd9241aea
--- /dev/null
+++ b/src/plugins/websocket/src/android/WebSocketInstance.java
@@ -0,0 +1,217 @@
+package com.foxdebug.websocket;
+
+import android.util.Base64;
+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 String TAG = "WebSocketInstance";
+ 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, boolean isBinary) {
+ if (this.webSocket != null) {
+ Log.d(TAG, "websocket instanceId=" + this.instanceId + " received send(..., isBinary=" + isBinary + ") action call, sending message=" + message);
+ if(isBinary) {
+ this.sendBinary(message);
+ return;
+ }
+ this.webSocket.send(message);
+ } else {
+ Log.d(TAG, "websocket instanceId=" + this.instanceId + " received send(..., isBinary=" + isBinary + ") ignoring... as webSocket is null (not present/connected)");
+ }
+ }
+
+ /**
+ * Sends bytes as the data of a binary (type 0x2) message.
+ * @param base64Data Binary Data received from JS bridge encoded as base64 String
+ */
+ private void sendBinary(String base64Data) {
+ byte[] data = Base64.decode(base64Data, Base64.DEFAULT);
+ this.webSocket.send(ByteString.of(data));
+ }
+
+ 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(TAG, "websocket instanceId=" + this.instanceId + " received close() action call, code=" + code + " reason=" + reason + " close method result: " + result);
+
+ // 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(TAG, "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(TAG, "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(TAG, "websocket instanceId=" + this.instanceId + " Opened" + "received extensions=" + this.extensions);
+ sendEvent("open", null, false, false);
+ }
+
+ @Override
+ public void onMessage(@NonNull WebSocket webSocket, @NonNull String text) {
+ Log.d(TAG, "websocket instanceId=" + this.instanceId + " Received message: " + text);
+ sendEvent("message", text, false, 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(TAG, "websocket instanceId=" + this.instanceId + " Received message(bytes/binary payload): " + bytes.toString());
+
+ try {
+ if ("arraybuffer".equals(this.binaryType)) {
+ String base64 = bytes.base64();
+ sendEvent("message", base64, true, false);
+ } else {
+ sendEvent("message", bytes.utf8(), true, true);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error sending message", e);
+ }
+
+ }
+
+ @Override
+ public void onClosing(@NonNull WebSocket webSocket, int code, @NonNull String reason) {
+ this.readyState = 2; // CLOSING
+ Log.i(TAG, "websocket instanceId=" + this.instanceId + " is Closing code: " + code + " reason: " + reason);
+ this.webSocket.close(code, reason);
+ }
+
+ @Override
+ public void onClosed(@NonNull WebSocket webSocket, int code, @NonNull String reason) {
+ this.readyState = 3; // CLOSED
+ Log.i(TAG, "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(TAG, "Error creating close event", e);
+ }
+ sendEvent("close", closedEvent.toString(), false, false);
+ // remove instance after receiving close.
+ WebSocketPlugin.removeInstance(this.instanceId);
+ }
+
+ @Override
+ public void onFailure(@NonNull WebSocket webSocket, Throwable t, Response response) {
+ this.readyState = 3; // CLOSED
+ sendEvent("error", t.getMessage(), false, false);
+ Log.e(TAG, "websocket instanceId=" + this.instanceId + " Error: " + t.getMessage());
+ }
+
+ public void setBinaryType(String binaryType) {
+ this.binaryType = binaryType;
+ }
+
+ private void sendEvent(String type, String data, boolean isBinary, boolean parseAsText) {
+ 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);
+ event.put("parseAsText", parseAsText);
+ if (data != null) event.put("data", data);
+ Log.d(TAG, "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(TAG, "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..c8fb832f3
--- /dev/null
+++ b/src/plugins/websocket/src/android/WebSocketPlugin.java
@@ -0,0 +1,130 @@
+package com.foxdebug.websocket;
+
+import android.util.Log;
+
+import org.apache.cordova.*;
+import org.json.*;
+
+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 {
+ cordova.getThreadPool().execute(new Runnable() {
+ @Override
+ public void run() {
+
+ 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, okHttpMainClient, cordova, id);
+ instances.put(id, instance);
+ callbackContext.success(id);
+ return;
+
+ case "send":
+ String instanceId = args.optString(0);
+ String message = args.optString(1);
+ boolean isBinary = args.optBoolean(2, false);
+
+ WebSocketInstance inst = instances.get(instanceId);
+ Log.d("WebSocketPlugin", "send called");
+ if (inst != null) {
+ inst.send(message, isBinary);
+ callbackContext.success();
+ } else {
+ callbackContext.error("Invalid instance ID");
+ }
+ return;
+
+ 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) {
+ callbackContext.success();
+ return;
+ } else if(!error.isEmpty()) {
+ // if error is empty means the websocket is not ready/open.
+ callbackContext.error(error);
+ return;
+ }
+ } else {
+ callbackContext.error("Invalid instance ID");
+ }
+ return;
+
+ case "registerListener":
+ instanceId = args.optString(0);
+ inst = instances.get(instanceId);
+ if (inst != null) {
+ inst.setCallback(callbackContext);
+ } else {
+ callbackContext.error("Invalid instance ID");
+ }
+ return;
+
+ case "setBinaryType":
+ instanceId = args.optString(0);
+ String type = args.optString(1);
+
+ inst = instances.get(instanceId);
+ if (inst != null) {
+ inst.setBinaryType(type);
+ } else {
+ Log.d("WebSocketPlugin", "setBinaryType called for instanceId=" + instanceId + " but It's not found. ignoring....");
+ }
+ return;
+
+ case "listClients":
+ JSONArray clientIds = new JSONArray();
+ for (String clientId : instances.keySet()) {
+ clientIds.put(clientId);
+ }
+ callbackContext.success(clientIds);
+ return;
+ default:
+ return;
+ }
+ }
+ });
+ return true;
+ }
+
+ @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");
+ }
+
+ public static void removeInstance(String instanceId) {
+ instances.remove(instanceId);
+ }
+}
diff --git a/src/plugins/websocket/www/websocket.js b/src/plugins/websocket/www/websocket.js
new file mode 100644
index 000000000..f1d1e93b1
--- /dev/null
+++ b/src/plugins/websocket/www/websocket.js
@@ -0,0 +1,180 @@
+var exec = require('cordova/exec');
+/**
+ * Whether to log debug messages
+ */
+let DEBUG = false;
+
+const logIfDebug = (...args) => {
+ console.log("DEBUG flag -> ", cordova.websocket.DEBUG)
+ if (cordova.websocket.DEBUG) {
+ console.log(...args);
+ }
+};
+
+class WebSocketInstance extends EventTarget {
+ constructor(url, instanceId, binaryType) {
+ 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;
+ // NOTE: blob is not supported currently.
+ this._binaryType = binaryType ? binaryType : ''; // empty as Default is string (Same Plugins might require this behavior)
+
+ 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;
+ // parseAsText solely takes care of the state of binaryType,
+ // sometimes, syncing binaryType to Java side might take longer. it's there to not wrongly pass normal string as base64.
+ if (event.isBinary && this.binaryType === 'arraybuffer' && !event.parseAsText) {
+ 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 });
+
+ Object.defineProperty(msgEvent, "binary", { enumerable: true, value: event.isBinary })
+
+ 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]);
+ }
+
+ get binaryType() {
+ return this._binaryType || '';
+ }
+
+ set binaryType(type) {
+ // blob isn't supported but checked as browser compatibility, & it default to empty string
+ if (type === 'blob' || type === 'arraybuffer' || type === '') {
+ this._binaryType = type !== 'blob' ? type : '';
+
+ exec(null, null, "WebSocketPlugin", "setBinaryType", [this.instanceId, type]);
+ } else {
+ console.warn('Invalid binaryType, expected "blob" or "arraybuffer"');
+ }
+ }
+
+
+ send(message, binary) {
+ 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));
+
+ // set to true as it's the data of a binary (type 0x2) message.
+ binary = true;
+
+ exec(() => logIfDebug(`[Cordova WebSocket - ID=${this.instanceId}] Sent message(binary payload):`, finalMessage), (err) => console.error(`[Cordova WebSocket - ID=${this.instanceId}] Send error:`, err), "WebSocketPlugin", "send", [this.instanceId, finalMessage, binary]);
+ } else if (typeof message === 'string') {
+ finalMessage = message;
+
+ // maybe a String to be sent as Binary (if it's true)
+ if(binary) finalMessage = btoa(message)
+
+ exec(() => logIfDebug(`[Cordova WebSocket - ID=${this.instanceId}] Sent message(binary=${binary}):`, finalMessage), (err) => console.error(`[Cordova WebSocket - ID=${this.instanceId}] Send error:`, err), "WebSocketPlugin", "send", [this.instanceId, finalMessage, binary]);
+ } 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, binary) {
+ return new Promise((resolve, reject) => {
+ if (typeof message === 'string') {
+
+ if(binary) message = btoa(message);
+
+ exec(resolve, reject, "WebSocketPlugin", "send", [instanceId, message, binary]);
+ } 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, true]);
+ } 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 };