Skip to content

Commit 7de490a

Browse files
committed
Merge branch 'websocket-server' into main
2 parents 04c3d5e + ee83687 commit 7de490a

File tree

6 files changed

+126
-5
lines changed

6 files changed

+126
-5
lines changed

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,15 @@ This implementation utilizes:
1212
- [x] Start the simple (default) json-server with `yarn serve:json`. (add commit hash)
1313
Run `curl http://localhost:3000/transactions` on a separate terminal to retrieve all transaction records.
1414

15-
- [x] Start the custom json-server with `yarn serve:custom-json`. (add commit hash)
15+
- [x] Start the fully capable custom websocket-json-server with `yarn serve`. (add commit hash)
1616
- [x] This script allows serving endpoint under a subpath, e.g. `/api/`,
1717
- [x] as well as extend json-server functionality with custom logic (e.g. datetime filtering).
18+
- [x] Add websocket capabilities on top of the custom server, under the subpath `/feed`.
19+
1820
Run `curl http://localhost:4000/api/transactions` on a separate terminal to retrieve all transaction records.
21+
1922
Or `curl -X GET "http://127.0.0.1:4000/api/transactions/?datetime=2022-06-26T10:00:00Z"` to retrieve transaction after the provided datetime.
23+
24+
Connect to the WebSocket on the endpoint `ws://localhost:4000/feed`.
25+
26+
Use a reverse proxy with TLS termination capabilities (e.g. NGinX, Caddy) to enable secure communications.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"license": "MIT",
99
"scripts": {
1010
"serve:json": "json-server --watch src/json-server-db.json",
11-
"serve:custom-json": "node src/index.js"
11+
"serve": "node src/index.js"
1212
},
1313
"dependencies": {
1414
"json-server": "^0.17.0",

src/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
const http = require('http');
2+
const webSocketServer = require('./websocket-routes');
23
const jsonServerApp = require('./json-routes');
34

45
const server = http.createServer(jsonServerApp);
56

7+
webSocketServer(server, '/feed', 5_000);
8+
69
server.listen(4000, '0.0.0.0', () => {
7-
console.log('HTTP (JSON) Server is running');
10+
console.log('HTTP (JSON/WebSocket) Server is running');
811
});

src/json-routes.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const jsonServer = require('json-server');
22
const app = jsonServer.create();
3-
const router = jsonServer.router('./src/json-server-db.json');
3+
const jsonDB = require('./json-server-db.json');
4+
const router = jsonServer.router(jsonDB);
45
const middlewares = jsonServer.defaults();
56

67
app.use(middlewares);
@@ -24,7 +25,10 @@ app.get('/api/transactions/', (req, res) => {
2425
});
2526

2627

27-
// Add any auto-generated routes from json-server-db.json
28+
/**
29+
* Add any auto-generated routes from json-server-db.json
30+
* This will automatically serve 'notifications' under the /api/notifications/ endpoint.
31+
*/
2832
app.use('/api', router);
2933

3034
module.exports = app;

src/json-server-db.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,19 @@
2727
"itemAgreedPrice": 5.2,
2828
"priceUnit": ""
2929
}
30+
],
31+
"notifications":[
32+
{
33+
"uuid": "8437d56c-2131-4742-a087-ebe8c1036307",
34+
"datetime": "2022-06-27T10:00:00Z",
35+
"readStatus": "READ",
36+
"message": "Some text message or inner JSON."
37+
},
38+
{
39+
"uuid": "a7410c73-bc77-44a7-833e-0cdef2dd0665",
40+
"datetime": "2022-06-27T11:00:00Z",
41+
"readStatus": "UNREAD",
42+
"message": "Another text message or inner JSON."
43+
}
3044
]
3145
}

src/websocket-routes.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
const WebSocketServer = require('ws').WebSocketServer;
2+
const parse = require('url').parse;
3+
const mockNotifications = require('./json-server-db.json').notifications;
4+
5+
/**
6+
* newNotificationObj A notification that is intented to be delivered after Xmsec,
7+
* instead during the initial 'db' lookup.
8+
* Part of the `on('connection',()=>{}) callback`.
9+
*/
10+
const newNotificationObj = {
11+
uuid: "ed3c55cc-0f6a-46d8-8b08-59623c949103",
12+
timestamp: "2022-06-10T11:00:00Z",
13+
readStatus: "UNREAD",
14+
message: "A new text message or inner JSON.",
15+
};
16+
17+
const responseNotificationObj = {
18+
uuid: "50522abe-2758-45cd-9ec7-d26573856f6d",
19+
timestamp: "2022-06-10T12:00:00Z",
20+
readStatus: "READ",
21+
message: "Changes accepted.",
22+
};
23+
24+
25+
/**
26+
* The websocket part of the mock server along with some http server specific handlings (upgrade, match route).
27+
* To test connectivity use any ws test client (e.g. postman, or some browser extension and connect to `ws://localhost:4000/feed`).
28+
* Upon connection, any existing db notifications will be received. After 5 seconds, a new notification will automatically arrive,
29+
* overriding the old message.
30+
* Send a test message, e.g. {"userResponse": "UPDATE_BID"} to trigger the 'on message' logic execution.
31+
* @param {*} httpServer The underlying http Server instance. This will actually serve the ws endpoint(s).
32+
* @param {*} wsPathName The pathname of the ws capable endpoint
33+
* @param {*} feedIntervalStep This is an interval to emulate some external event, resulting in the presence of a new notification object. In Xmsec a new notification will arrive, emulating automated real-time message delivery.
34+
*/
35+
module.exports = (httpServer, wsPathName = '/feed', feedIntervalStep = 30_000) => {
36+
const notificationsWS = new WebSocketServer({ noServer: true });
37+
let responseTimeout;
38+
let feedInterval;
39+
40+
notificationsWS.on('connection', function onConnection(ws, connectionRequest) {
41+
console.log('WebSocket connection established');
42+
// Send any existing db notifications.
43+
ws.send(JSON.stringify({ notifications: mockNotifications }));
44+
45+
feedInterval = setInterval(() => {
46+
// this could also be a setTimeout since it's just a single, notification item, message emulated.
47+
ws.send(JSON.stringify({ notifications: [newNotificationObj] })); // send array of objects, to be consistent of what to expect in the client side.
48+
console.debug('Interval placeholder to send new notifications.');
49+
}, feedIntervalStep);
50+
51+
/**
52+
* Register the on message callback. Executed each time a web app/client, sends data to the ws server.
53+
*/
54+
ws.on('message', function onMessage(data) {
55+
try {
56+
let dataObj = JSON.parse(data); // assuming json payload
57+
console.log(`Received message from web client with data: ${data}.`);
58+
59+
// assuming the client sends an object with the 'userResponse' attribute in, set to 'UPDATE_BID'.
60+
if (dataObj?.userResponse === "UPDATE_BID") {
61+
responseTimeout = setTimeout(() => {
62+
ws.send(JSON.stringify({ notifications: [responseNotificationObj] }));
63+
}, 1_000);
64+
}
65+
}
66+
catch (error) {
67+
// handle error
68+
}
69+
});
70+
71+
/**
72+
* Register the on close callback. Cleanup before closing the connection.
73+
*/
74+
ws.on('close', () => {
75+
clearInterval(feedInterval);
76+
clearTimeout(responseTimeout);
77+
console.log('WebSocket connection closed.');
78+
});
79+
});
80+
81+
httpServer.on('upgrade', function upgrade(request, socket, head) {
82+
const { pathname } = parse(request.url);
83+
84+
if (pathname === wsPathName) {
85+
notificationsWS.handleUpgrade(request, socket, head, function done(ws) {
86+
notificationsWS.emit('connection', ws, request);
87+
});
88+
} else {
89+
console.log('Closing the socket!!');
90+
socket.destroy();
91+
}
92+
});
93+
};

0 commit comments

Comments
 (0)