Skip to content

Commit 86856b7

Browse files
feat: enhance WebSocket plugin with custom headers and improved close functionality
- Updated `WebSocketPlugin.connect` to accept custom headers. - Enhanced `WebSocketInstance.close` method to allow specifying close code and reason. - Added `WebSocketPlugin.listClients` method to list active WebSocket instances. - Updated JavaScript interface to support new features and improved documentation.
1 parent 410e1df commit 86856b7

4 files changed

Lines changed: 126 additions & 23 deletions

File tree

src/plugins/websocket/README.md

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ It aims to mimic the [WebSocket API](https://developer.mozilla.org/en-US/docs/We
88
* ✅ WebSocket API-like interface
99
* ✅ Event support: `onopen`, `onmessage`, `onerror`, `onclose`
1010
*`extensions` and `readyState` properties
11+
*`listClients()` to list active connections
1112
* ✅ Support for protocols
13+
* ✅ Support for Custom Headers.
1214
* ✅ Compatible with Cordova for Android
1315

1416
---
@@ -24,7 +26,7 @@ const WebSocketPlugin = cordova.websocket;
2426
### Connect to WebSocket
2527

2628
```javascript
27-
WebSocketPlugin.connect("wss://example.com/socket", ["protocol1", "protocol2"])
29+
WebSocketPlugin.connect("wss://example.com/socket", ["protocol1", "protocol2"], headers)
2830
.then(ws => {
2931
ws.onopen = (e) => console.log("Connected!", e);
3032
ws.onmessage = (e) => console.log("Message:", e.data);
@@ -43,21 +45,35 @@ WebSocketPlugin.connect("wss://example.com/socket", ["protocol1", "protocol2"])
4345

4446
### Methods
4547

46-
* `WebSocketPlugin.connect(url, protocols)`
48+
* `WebSocketPlugin.connect(url, protocols, headers)`
4749

4850
* Connects to a WebSocket server.
4951
* `url`: The WebSocket server URL.
5052
* `protocols`: (Optional) An array of subprotocol strings.
51-
* Returns: A Promise that resolves to a `WebSocketInstance`.
53+
* `headers` (object, optional): Custom headers as key-value pairs.
54+
* **Returns:** A Promise that resolves to a `WebSocketInstance`.
55+
* `WebSocketPlugin.listClients()`
56+
* Lists all stored webSocket instance IDs.
57+
* **Returns:** `Promise`that resolves to an array of `instanceId` strings.
58+
59+
* `WebSocketPlugin.send(instanceId, message)`
60+
* same as `WebSocketInstance.send(message)` but needs `instanceId`.
61+
* **Returns:** `Promise` that resolves.
62+
63+
* `WebSocketPlugin.close(instanceId, code, reason)`
64+
* same as `WebSocketInstance.close(code, reason)` but needs `instanceId`.
65+
* **Returns:** `Promise` that resolves.
5266

5367
* `WebSocketInstance.send(message)`
5468

5569
* Sends a message to the server.
5670
* Throws an error if the connection is not open.
5771

58-
* `WebSocketInstance.close()`
72+
* `WebSocketInstance.close(code, reason)`
5973

6074
* Closes the connection.
75+
* `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.
76+
* `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).
6177

6278
---
6379

@@ -81,5 +97,5 @@ WebSocketPlugin.connect("wss://example.com/socket", ["protocol1", "protocol2"])
8197

8298
* Only supported on Android (via OkHttp).
8399
* Make sure to handle connection lifecycle properly (close sockets when done).
84-
100+
* `listClients()` is useful for debugging and management.
85101
---

src/plugins/websocket/src/android/WebSocketInstance.java

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,44 @@
88
import org.json.JSONArray;
99
import org.json.JSONObject;
1010

11+
import java.util.Iterator;
1112
import java.util.concurrent.TimeUnit;
1213

1314
import okhttp3.*;
1415

1516
public class WebSocketInstance extends WebSocketListener {
17+
private static final int DEFAULT_CLOSE_CODE = 1000;
18+
private static final String DEFAULT_CLOSE_REASON = "Normal closure";
19+
1620
private WebSocket webSocket;
1721
private CallbackContext callbackContext;
18-
private CordovaInterface cordova;
19-
private String instanceId;
22+
private final CordovaInterface cordova;
23+
private final String instanceId;
2024
private String extensions = "";
2125
private int readyState = 0; // CONNECTING
2226

23-
public WebSocketInstance(String url, JSONArray protocols, CordovaInterface cordova, String instanceId) {
27+
// okHttpMainClient parameter is used. To have a single main client(singleton), with per-websocket configuration using newBuilder method.
28+
public WebSocketInstance(String url, JSONArray protocols, JSONObject headers, OkHttpClient okHttpMainClient, CordovaInterface cordova, String instanceId) {
2429
this.cordova = cordova;
2530
this.instanceId = instanceId;
2631

27-
OkHttpClient client = new OkHttpClient.Builder()
32+
OkHttpClient client = okHttpMainClient.newBuilder()
2833
.connectTimeout(10, TimeUnit.SECONDS)
2934
.build();
3035

3136
Request.Builder requestBuilder = new Request.Builder().url(url);
37+
38+
// custom headers support.
39+
if (headers != null) {
40+
Iterator<String> keys = headers.keys();
41+
while (keys.hasNext()) {
42+
String key = keys.next();
43+
String value = headers.optString(key);
44+
requestBuilder.addHeader(key, value);
45+
}
46+
}
47+
48+
// adds Sec-WebSocket-Protocol header if protocols is present.
3249
if (protocols != null) {
3350
StringBuilder protocolHeader = new StringBuilder();
3451
for (int i = 0; i < protocols.length(); i++) {
@@ -56,16 +73,22 @@ public void send(String message) {
5673
}
5774
}
5875

59-
public void close() {
76+
public void close(int code, String reason) {
6077
if (webSocket != null) {
6178
readyState = 2; // CLOSING
62-
webSocket.close(1000, "Normal closure");
79+
webSocket.close(code, reason);
6380
Log.d("WebSocketInstance", "websocket instanceId=" + this.instanceId + " received close() action call");
6481
} else {
6582
Log.d("WebSocketInstance", "websocket instanceId=" + this.instanceId + " received close() action call, ignoring... as webSocket is null (not present)");
6683
}
6784
}
6885

86+
public void close() {
87+
Log.d("WebSocketInstance", "WebSocket instanceId=" + this.instanceId + " close() called with no arguments. Using defaults.");
88+
// Calls the more specific version with default values
89+
close(DEFAULT_CLOSE_CODE, DEFAULT_CLOSE_REASON);
90+
}
91+
6992
@Override
7093
public void onOpen(@NonNull WebSocket webSocket, Response response) {
7194
this.webSocket = webSocket;
@@ -76,26 +99,26 @@ public void onOpen(@NonNull WebSocket webSocket, Response response) {
7699
}
77100

78101
@Override
79-
public void onMessage(@NonNull WebSocket webSocket, String text) {
102+
public void onMessage(@NonNull WebSocket webSocket, @NonNull String text) {
80103
sendEvent("message", text);
81104
Log.d("WebSocketInstance", "websocket instanceId=" + this.instanceId + " Received message: " + text);
82105
}
83106

84107
@Override
85-
public void onClosing(WebSocket webSocket, int code, String reason) {
108+
public void onClosing(@NonNull WebSocket webSocket, int code, @NonNull String reason) {
86109
this.readyState = 2; // CLOSING
87110
Log.i("WebSocketInstance", "websocket instanceId=" + this.instanceId + " is Closing code: " + code + " reason: " + reason);
88111
}
89112

90113
@Override
91-
public void onClosed(WebSocket webSocket, int code, String reason) {
114+
public void onClosed(@NonNull WebSocket webSocket, int code, @NonNull String reason) {
92115
this.readyState = 3; // CLOSED
93116
sendEvent("close", reason);
94117
Log.i("WebSocketInstance", "websocket instanceId=" + this.instanceId + " Closed code: " + code + " reason: " + reason);
95118
}
96119

97120
@Override
98-
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
121+
public void onFailure(@NonNull WebSocket webSocket, Throwable t, Response response) {
99122
this.readyState = 3; // CLOSED
100123
sendEvent("error", t.getMessage());
101124
Log.e("WebSocketInstance", "websocket instanceId=" + this.instanceId + " Error: " + t.getMessage());

src/plugins/websocket/src/android/WebSocketPlugin.java

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,27 @@
66
import java.util.HashMap;
77
import java.util.UUID;
88

9-
// @TODO: plugin init & plugin destroy(closing okhttp clients) lifecycles.
9+
import okhttp3.OkHttpClient;
10+
11+
// TODO: plugin init & plugin destroy(closing okhttp clients) lifecycles. (✅)
1012
public class WebSocketPlugin extends CordovaPlugin {
1113
private static final HashMap<String, WebSocketInstance> instances = new HashMap<>();
14+
public OkHttpClient okHttpMainClient = null;
15+
16+
@Override
17+
protected void pluginInitialize() {
18+
this.okHttpMainClient = new OkHttpClient();
19+
}
1220

1321
@Override
1422
public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException {
1523
switch (action) {
1624
case "connect":
1725
String url = args.optString(0);
1826
JSONArray protocols = args.optJSONArray(1);
27+
JSONObject headers = args.optJSONObject(2);
1928
String id = UUID.randomUUID().toString();
20-
WebSocketInstance instance = new WebSocketInstance(url, protocols, cordova, id);
29+
WebSocketInstance instance = new WebSocketInstance(url, protocols, headers, this.okHttpMainClient, cordova, id);
2130
instances.put(id, instance);
2231
callbackContext.success(id);
2332
return true;
@@ -36,9 +45,11 @@ public boolean execute(String action, JSONArray args, final CallbackContext call
3645

3746
case "close":
3847
instanceId = args.optString(0);
48+
int code = args.optInt(1);
49+
String reason = args.optString(2);
3950
inst = instances.get(instanceId);
4051
if (inst != null) {
41-
inst.close();
52+
inst.close(code, reason);
4253
instances.remove(instanceId);
4354
callbackContext.success();
4455
} else {
@@ -56,8 +67,26 @@ public boolean execute(String action, JSONArray args, final CallbackContext call
5667
}
5768
return true;
5869

70+
case "listClients":
71+
JSONArray clientIds = new JSONArray();
72+
for (String clientId : instances.keySet()) {
73+
clientIds.put(clientId);
74+
}
75+
callbackContext.success(clientIds);
76+
return true;
5977
default:
6078
return false;
6179
}
6280
}
81+
82+
@Override
83+
public void onDestroy() {
84+
// clear all.
85+
for (WebSocketInstance instance : instances.values()) {
86+
// Closing them gracefully.
87+
instance.close();
88+
}
89+
instances.clear();
90+
okHttpMainClient.dispatcher().executorService().shutdown();
91+
}
6392
}

src/plugins/websocket/www/websocket.js

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,15 @@ class WebSocketInstance {
3434
}
3535
}
3636

37-
close() {
37+
/**
38+
* Closes the WebSocket connection.
39+
*
40+
* @param {number} code The status code explaining why the connection is being closed.
41+
* @param {string} reason A human-readable string explaining why the connection is being closed.
42+
*/
43+
close(code, reason) {
3844
this.readyState = WebSocketInstance.CLOSING;
39-
exec(null, null, "WebSocketPlugin", "close", [this.instanceId]);
45+
exec(null, null, "WebSocketPlugin", "close", [this.instanceId, code, reason]);
4046
}
4147
}
4248

@@ -45,10 +51,39 @@ WebSocketInstance.OPEN = 1;
4551
WebSocketInstance.CLOSING = 2;
4652
WebSocketInstance.CLOSED = 3;
4753

48-
const connect = function(url, protocols = null) {
54+
const connect = function(url, protocols = null, headers = null) {
4955
return new Promise((resolve, reject) => {
50-
exec(instanceId => resolve(new WebSocketInstance(url, instanceId)), reject, "WebSocketPlugin", "connect", [url, protocols]);
56+
exec(instanceId => resolve(new WebSocketInstance(url, instanceId)), reject, "WebSocketPlugin", "connect", [url, protocols, headers]);
5157
});
5258
};
5359

54-
module.exports = { connect };
60+
const listClients = function() {
61+
return new Promise((resolve, reject) => {
62+
exec(resolve, reject, "WebSocketPlugin", "listClients", []);
63+
});
64+
};
65+
66+
/** Utility functions, in-case you lost the websocketInstance returned from the connect function */
67+
68+
const send = function(instanceId, message) {
69+
return new Promise((resolve, reject) => {
70+
exec(resolve, reject, "WebSocketPlugin", "send", [instanceId, message]);
71+
});
72+
};
73+
74+
/**
75+
* Closes the WebSocket connection.
76+
*
77+
* @param {string} instanceId The ID of the WebSocketInstance to close.
78+
* @param {number} [code] (optional) The status code explaining why the connection is being closed.
79+
* @param {string} [reason] (optional) A human-readable string explaining why the connection is being closed.
80+
*
81+
* @returns {Promise} A promise that resolves when the close operation has completed.
82+
*/
83+
const close = function(instanceId, code, reason) {
84+
return new Promise((resolve, reject) => {
85+
exec(resolve, reject, "WebSocketPlugin", "close", [instanceId, code, reason]);
86+
});
87+
};
88+
89+
module.exports = { connect, listClients, send, close };

0 commit comments

Comments
 (0)