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 };