Skip to content

Commit 0cc9b24

Browse files
authored
Create websockets-and-rest.js
1 parent 0db9a30 commit 0cc9b24

File tree

1 file changed

+268
-0
lines changed

1 file changed

+268
-0
lines changed

websockets-and-rest.js

+268
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
/*
2+
3+
Module: WebSocket
4+
Author: Ashok Khanna
5+
Last Update: 09-04-2022
6+
License: MIT
7+
8+
Based on Bergi's Solution on Stack Overflow:
9+
https://stackoverflow.com/questions/60512129/websocket-waiting-for-server-response-with-a-queue
10+
11+
How to use:
12+
13+
1. Import the module and create a socket instance:
14+
15+
```
16+
import WebSocket from './Components/Websocket'
17+
18+
export const ws = new WebSocket("wss://www.url.com/socket-point");
19+
```
20+
21+
2. Then simply use it in your functions as following (first import below
22+
is to import the `ws' instance created above into the module where you are
23+
using the socket:
24+
25+
```
26+
import {ws} from './index'
27+
28+
...
29+
30+
function login() {
31+
...
32+
ws.sendRequest(someMessageInJSONFormat,
33+
(value) => {
34+
...<insert code to handle response here>>
35+
)}
36+
}
37+
```
38+
39+
Usually I like to create some sort of JSON object as in the above,
40+
but if you read the below code then you can see there is a `sendMessage'
41+
variant that can handle plain strings
42+
43+
*/
44+
45+
export default class WebSocket {
46+
47+
constructor(url) {
48+
49+
// Here we create an empty array [] which we will add to later
50+
// Note that we can use {} also, which is for empty objects
51+
// (arrays are objects)
52+
this.waitingResponse = [];
53+
54+
// Here we create an empty array [] that represents the queue of
55+
// messages that didn't send because the socket was closed and
56+
// are queued up to be sent during the onopen handler (which iterates
57+
// through this array)
58+
this.messageQueue = [];
59+
60+
this.url = url;
61+
62+
// We separate out the socket initialisation into its own function
63+
// as we will also call it during a reconnect attempt
64+
this.createSocket();
65+
66+
}
67+
68+
69+
// The reconnection logic is that whenever a message fails to send, the
70+
// message is added to messageQueue and a reconnection attempt is made.
71+
// So, when a connection is lost, it is reconnected to after a certain
72+
// time, but rather only when the user initiates an action that must
73+
// message (i.e.) interact with the WebSocket
74+
createSocket() {
75+
this.socket = new WebSocket(this.url);
76+
77+
// Iterate through the queue of messages that haven't been sent
78+
// If this queue is empty then no messages are sent
79+
80+
// All messages in the message queue arise from a previous
81+
// sendPayload event, thus are parsed in the correct JSON form
82+
// and have an associated request object in waitingResponse
83+
this.socket.onopen = () => {
84+
this.messageQueue.forEach(item => this.socket.send(item))
85+
this.messageQueue = [];
86+
}
87+
88+
this.socket.onclose = () => console.log("ws closed");
89+
90+
this.socket.onmessage = e => { this.processMessage(e); }
91+
}
92+
93+
// Creates a new socket and adds any unsent
94+
// messages onto the message queue
95+
recreateSocket(message) {
96+
console.log("Reconnection Attempted");
97+
this.messageQueue.push(message);
98+
this.createSocket();
99+
}
100+
101+
// Closes a socket, which can take a bit
102+
// of time (few seconds) since a roundtrip to
103+
// the server is done
104+
closeSocket(){
105+
this.socket.close();
106+
console.log("Socket closed manually.")
107+
}
108+
109+
// Exposes a function for users to start a new
110+
// socket - there is no way to 'reconnect' to
111+
// a socket, a new websocket needs to be created
112+
openSocket(){
113+
this.createSocket();
114+
console.log("Socket opened manually.")
115+
}
116+
117+
async sendPayload(details) {
118+
// Create a request where request = { sent: + new Date()} and this.waiting... = request
119+
// this means both request and waitingResponse[details.requestid] point to the same thing
120+
// so that changing request.test will also result in waitingResponse[details.requestid].test
121+
// having the same value
122+
123+
// Note that details.requestid here is an index = the timestamp. Later when we process
124+
// messages received, we will check the timestamp of the requestid of the message received
125+
// against this waitingResponse array and resolve the request if a match is found
126+
127+
let requestid = +new Date();
128+
const request = this.waitingResponse[requestid] = { sent: requestid };
129+
130+
// Here we combine the request (which at this point is just { sent: ...} with the
131+
// actual data to be sent to form the final message to send
132+
const message = { ...request, ...details }
133+
134+
// If Socket open then send the details (message) in String Format
135+
try {
136+
if (this.socket.readyState === WebSocket.OPEN) {
137+
this.socket.send(JSON.stringify(message));
138+
} else {
139+
// Otherwise we try to recreate the socket and send the message
140+
// after recreating the socket
141+
this.recreateSocket(JSON.stringify(message));
142+
}
143+
144+
// Here we create a new promise function
145+
// We set the resolve property of request [which is also referenced
146+
// by waitingResponse[details.requestid] to the Promise's resolve function
147+
148+
// Thus we can resolve the promise from processMessage (refer below)
149+
150+
// We reject after 5 seconds of not receiving the associated message
151+
// with the same requestid
152+
const result = await new Promise(function(resolve, reject) {
153+
// This will automatically run, allowing us to access
154+
// the resolve function from outside this function
155+
request.resolve = resolve;
156+
157+
console.log(request);
158+
// This will take 5 seconds to run, which becomes the lifecycle
159+
// of this Promise function - the resolve function must be
160+
// called before this point
161+
setTimeout(() => {
162+
reject('Timeout'); // or resolve({action: "to"}), or whatever
163+
}, 5000);
164+
});
165+
166+
console.info("Time took", (+new Date() - request.sent) / 1000);
167+
168+
// function returns result
169+
return result; // or {...request, ...result} if you care
170+
}
171+
172+
// code to run regardless of whether try worked or error thrown
173+
finally {
174+
console.log("Exit code ran successfully")
175+
176+
delete this.waitingResponse[requestid];
177+
}
178+
}
179+
180+
181+
// Message Receiver, we attach this to the onmessage handler
182+
// Expects message to be in JSON format, otherwise throws
183+
// an error and simply logs the message to console
184+
185+
// The message must also have a requestid property (we
186+
// use lowercase "i" here because Common Lisp's JZON library
187+
// lowercases property names in JSON messages
188+
189+
// Test if the requestid passed in has an entry in the waitingResponse
190+
// queue (data.requestid is the array index and the sendPayload function
191+
// sets a value in this array for various id indexes to { sent: .. }
192+
// This index also has a reference to the resolve function for the
193+
// associated promise for that request id
194+
195+
// If that is true ('truthy' via if (request)), then resolve the
196+
// associated promise via request.resolve(data), where data is
197+
// the value resolved by the promise
198+
199+
// Otherwise pass a variety of console warnings / logs - the message
200+
// will not be handled and disappear from the future (i.e. every
201+
// message needs a requestid set in waitingResponse to be caught
202+
203+
// We could probably add in a router for server initiated messages
204+
// to be handled (under the second warning below)
205+
async processMessage(msg) {
206+
207+
try {
208+
let data = JSON.parse(msg.data);
209+
210+
if (data.hasOwnProperty("requestid")) {
211+
const request = this.waitingResponse[data.requestid]
212+
if (request)
213+
request.resolve(data)
214+
else
215+
console.warn("Got data but found no associated request, already timed out?", data)
216+
} else {
217+
// Add handlers here for messages without request ID
218+
console.warn("Got data without request id", data);
219+
}
220+
} catch {
221+
console.log(msg.data);
222+
}
223+
224+
}
225+
226+
// Main entry point for calling functions with a simple
227+
// callback to action to perform on the received data
228+
// Exists here to reduce boilerplate for the calling function
229+
async sendRequest(details, resolution, rejection = (error) => {console.log(error)}) {
230+
this.sendPayload(details).then(
231+
function(value) {
232+
resolution(value);
233+
},
234+
function(error) {
235+
rejection(error);
236+
})
237+
}
238+
239+
// Second entry point for one direction messages
240+
// i.e. not expecting any responses. This bypasses
241+
// the request-response promise functions above
242+
243+
// Attempts to JSON.stringify the object first,
244+
// and just sends the object if cannot be made
245+
// into a JSON string
246+
247+
sendMessage(details) {
248+
// Example of an Immediately-Invoked Function Expression
249+
const message = (() => {
250+
try {
251+
return JSON.stringify(details)
252+
}
253+
catch (e) {
254+
return details
255+
}
256+
})()
257+
258+
if (this.socket.readyState === WebSocket.OPEN) {
259+
this.socket.send(message);
260+
} else {
261+
// Otherwise we try to recreate the socket and send the message
262+
// after recreating the socket
263+
this.recreateSocket(message);
264+
}
265+
}
266+
267+
268+
}

0 commit comments

Comments
 (0)