How to setup routes for websocket event actions in express.js? - node.js

I'm trying to create a router for every websocket event action.
Let's user is on the chat room, I want the url be ws://localhost:3001/chat but for the other event, let's say vote should be smth like this ws://localhost:3001/vote.
I found this example: const ws = new ws.Server({server:httpServer, path:"/chat"}), but this is a generic one, and doesn't aplly to different event actions.
Here is my structure. Hopefully will make more sense
As you see there I have two event actions: chat and vote and my goal is to create a route for each action types.
Socket.ts
import { IncomingMessage } from 'http';
import internal from 'stream';
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ noServer: true });
wss.on('connection', (ws, request) => {
...
ws.on('message', (data: string) => {
try {
switch (action) {
case 'chat':
wss.clients.forEach(each(client)=> {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message));
}
});
break;
case 'vote':
wss.clients.forEach((player) => {
if (client.readyState === WebSocket.OPEN) {
player.client.send(JSON.stringify(voteMessage));
}
});
break;
default: {
throw new Error('Unknown message type.');
}
}
} catch (error) {
ws.send(`Error: ${error.message}`);
}
});
});
const socketUpgrade = (
request: IncomingMessage,
socket: internal.Duplex,
head: Buffer
) =>
wss.handleUpgrade(request, socket, head, (ws) =>
wss.emit('connection', ws, request)
);
export { socketUpgrade };
app.ts
import express from 'express';
import { socketUpgrade } from './Socket';
const app = express();
const port = 3001;
const server = app.listen(port, () => {
console.log(`listening on port ${port}`);
});
server.on('upgrade', socketUpgrade);
Any help will be appreciated

Related

Jest websocket test is throwing an error "thrown: "Exceeded timeout of 5000 ms for a test"

I am using the ws library to create a websocket server
I am getting this error on running the test
src/server/server.ts
import { Server } from 'http';
import WebSocket from 'ws';
import { sessionHandler } from './sessionHandler';
export const websocketServer = new WebSocket.Server({
noServer: true,
clientTracking: false,
path: '/ws',
});
const WEBSOCKET_CHECK_IDLE_CONNECTION_FREQUENCY =
+process.env.WEBSOCKET_CHECK_IDLE_CONNECTION_FREQUENCY;
function checkIfConnectionIsAlive(ws: WebSocket) {
if (ws.isAlive === false) {
clearTimeout(ws.timer);
return ws.terminate();
}
ws.isAlive = false;
ws.ping();
ws.timer = setTimeout(
checkIfConnectionIsAlive,
WEBSOCKET_CHECK_IDLE_CONNECTION_FREQUENCY,
ws,
);
}
function heartbeat() {
this.isAlive = true;
}
export function upgradeToWebSocketConnection(server: Server) {
server.on('upgrade', (request, socket, head) => {
sessionHandler(request, {}, () => {
websocketServer.handleUpgrade(request, socket, head, (ws) => {
websocketServer.emit('connection', ws, request);
});
});
});
}
websocketServer.on('connection', (ws, request) => {
ws.isAlive = true;
ws.on('pong', heartbeat);
ws.send('hello');
checkIfConnectionIsAlive(ws);
});
websocketServer.on('close', () => {
console.debug('Shutting down websocket server');
});
I wrote a simple test to check if I get that hello message on connecting to the server
src/tests/websocket.test.ts
import http, { Server } from 'http';
import {
upgradeToWebSocketConnection,
websocketServer,
} from 'server/websocket';
import WebSocket from 'ws';
function startServer(port: number): Promise<Server> {
const server = http.createServer();
return new Promise((resolve) => {
server.listen(port, () => resolve(server));
});
}
function waitForSocketState(socket: WebSocket, state: number): Promise<void> {
return new Promise(function (resolve) {
setTimeout(function () {
if (socket.readyState === state) {
resolve();
} else {
waitForSocketState(socket, state).then(resolve);
}
});
});
}
let server: Server;
describe('Websocket server tests', () => {
beforeEach(async () => {
server = await startServer(8000);
upgradeToWebSocketConnection(server);
});
afterEach(() => {
websocketServer.emit('close');
server.close();
});
test('Server sends hello to the client on connect', async () => {
const client = new WebSocket(`ws://localhost:8000/ws`);
let message: WebSocket.Data;
client.on('message', (data) => {
message = data.toString();
client.close();
});
await waitForSocketState(client, client.OPEN);
await waitForSocketState(client, client.CLOSED);
expect(message).toBe('hello');
});
});
Can someone kindly tell me how to fix this error and make the test succeed? My guess is that it has a problem with the setTimeout section

Unit Test cases for websocket server "ws" library using jest

I am trying to figure out how to write unit test cases for Websocket server which is using the ws library.
I did go through jest-websocket-mock but I think this is for browser based APIs and I want to test server using JEST.
Basic Code:
Server.js
import { createServer } from 'https';
import { WebSocketServer } from 'ws';
import { readFileSync } from 'fs';
const server = createServer({
cert: readFileSync(config.certs.sslCertPath),
key: readFileSync(config.certs.sslKeyPath),
});
const wss = new WebSocketServer({ noServer: true });
server.on('upgrade', (request, socket, head) => {
const origin = request && request.headers && request.headers.origin;
const corsRegex = <regex>;
if (origin.match(corsRegex) != null) {
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit('connection', ws, request);
});
} else {
socket.destroy();
}
});
wss.on('connection', (ws, req) => {
ws.on('message', (messageg) => {
try {
console.log(message);
} catch (error) {
console.log(error);
}
});
ws.on('close', () => {
console.log('close');
});
ws.on('error', (error) => {
console.log(error);
});
});
Can someone please help me with how can I test the original server?
you need to create some kind of dependency injection mechanism here
lets for example move all the socket initialization logic into a separate function
function initSocketEvents(wss) {
wss.on('connection', (ws, req) => {
ws.on('message', (messageg) => {
try {
console.log(message);
} catch (error) {
console.log(error);
}
});
ws.on('close', () => {
console.log('close');
});
ws.on('error', (error) => {
console.log(error);
});
});
return wss;
}
now at the server initilization just call the function from a different file
...
const {initSocketEvents} = require("./socket-handler")
const wss = new WebSocketServer({ noServer: true });
initSocketEvents(was);
...
everything stays the same except the fact its way much easier to test it now
at the test file
const {initSocketEvents} = require("./socket-handler")
const { assert } = require('console');
const { EventEmitter } = require('events');
class MyTestWebSocket extends EventEmitter { }
const mockWSS = new MyTestWebSocket()
initSocketEvents(mockWSS)
mockWSS.emit('connection', mockWSS)
assert(mockWSS.listenerCount('connection')===1)
assert(mockWSS.listenerCount('message')===1)
assert(mockWSS.listenerCount('close')===1)
assert(mockWSS.listenerCount('error')===1)
now it should be straightforward to separate each listener's logic and inject it outside the function and then assert the desired logic.

How to use node-redis client in node typescript

I have a node typescript project where I have created a TS file for the Redis connection which is below.
import { createClient } from 'redis';
import { promisify } from 'util';
import Logger from 'utils/logger';
const { REDIS_URL = 'redis://localhost:6379' } = process.env;
const options = {
legacyMode: true,
url: REDIS_URL,
}
const client = createClient(options);
// client.connect();
client.on('connect', () => {
Logger.info("Connected to Redis");
});
client.on('error', err => {
Logger.error('redis error: ' + err);
init();
});
client.on('ready', err => {
Logger.info("redis is ready");
});
client.on('end', err => {
Logger.info("redis connection is ended");
});
//reconnecting
client.on('reconnecting', err => {
Logger.info("redis connection is reconnecting");
});
const init = async () => {
await client.connect();
}
export { init,client };
then I am importing it and connected it to index.ts
import { init } from 'dataSource/redis';
(async () => {
await init();
})();
app.listen(PORT,() => {
// console.log(`server is running on PORT ${PORT}`)
Logger.info(`Server Started in port : ${PORT}!`);
})
then I am trying to use the client in my controller file.
import { client as redisClient } from 'datasource/redis';
redisClient.setEx("Key",Number(process.env.REDIS_EXPIRE_TIME),"VALUE");
but I am getting this error
Error: The client is closed
uncomment the "client.connect();" on line 13.
This should make it work

Acknowledge RabbitMQ message after socket IO event received from React browser

I have a Node server which consumes messages from a RabbitMQ queue and forwards them to a React frontend as a socket.io event. In the frontend, I have a button click which sends a socket.io event back to the Node server.
Currently, the Node server only logs the receipt of the socket.io event. In addition to logging, I would like to send a message ack to the RabbitMQ server upon receipt of the socket.io event.
The logging is working fine, but I've been struggling with the message acknowledgement part.
My node server looks like this:
server.js
const io = require('./socket');
const amqp = require('amqplib/callback_api');
const CONFIG = require('./config.json');
amqp.connect(`amqp://${CONFIG.host}`, (err, connection) => {
if (err) {
throw err;
}
connection.createChannel((err, channel) => {
if (err) {
throw err;
}
const queue = CONFIG.queueName;
channel.assertQueue(queue, {
durable: true
});
console.log(` [*] Waiting for messages in ${queue}.`);
channel.consume(queue, function(msg) {
console.log(' [x] Request received from RabbitMQ: %s', msg.content.toString());
io.client.emit('sendReview', msg.content.toString());
}, {
noAck: false
});
})
});
socket.js
const io = require('socket.io')();
const port = process.env.PORT || 8000;
module.exports = {
client : any = io.on('connection', (client) => {
console.log(' [*] New client connected with ID: ' + client.id);
client.on('reportReview', (msg) => {console.log(` [x] Response received from browser: ${msg}`)});
client.on('disconnect', () => console.log(` [*] User ${client.id} disconnected.`));
})
};
io.listen(port);
console.log(`Listening on port ${port}`);
My frontend looks like this:
App.js
import React, { Component } from "react";
import * as API from './api';
export default class App extends Component {
constructor(props, context) {
super(props, context);
this.state = {
data: ["Whoops - no reviews available"],
};
this.updateReview = this.updateReview.bind(this);
this.onMessageReceived = this.onMessageReceived.bind(this);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
API.reportClick(this.state.data[0]);
this.updateReview()
}
updateReview() {
const newArray = this.state.data.slice(1);
if (newArray.length === 0) {
this.setState({data: ["Whoops - no reviews available"]})
} else {
this.setState({data: newArray})
}
}
onMessageReceived(msg) {
console.log(`Request for review received: ${msg}`);
const updatedData = this.state.data.concat(msg);
this.setState({data: updatedData});
if (this.state.data[0] === "Whoops - no reviews available") {
this.updateReview()
}
}
componentDidMount() {
API.subscribe(this.onMessageReceived)
}
render() {
return (
<div className="App">
<p className="App-intro">
Click to confirm review #: {this.state.data[0]}
</p>
<button onClick={this.handleClick}>Click</button>
</div>
);
}
}
Api.js
import clientSocket from 'socket.io-client';
const socket = clientSocket('http://localhost:8000');
function subscribe(onMessageReceived) {
socket.on('sendReview', onMessageReceived);
}
function reportClick(msg) {
socket.emit('reportReview', msg);
}
export { reportClick, subscribe };
As far as I understand, in order to send a message ack I would have to call channel.ack(msg); somewhere on the Node server. However, I am not sure how to pass the channel object to the io module? I have also tried having the socket.io code in server.js so I would have access to the channel object but have not been able to get this to work, either - I have not been able to get the amqp connection and socket.io connection to work together other than using my current approach of having an io module.
Any help would be very much appreciated.
I ended up getting it to work by having the socket code in server.js like this:
const io = require('socket.io')();
function socketIOHandler(callback) {
io.on('connection', (socket) => {
socket.on('error', function(err) {
console.log(err.stack);
});
callback(socket);
});
}
var amqpConn = null;
// start amqp connection to rabbit mq
function start() {
amqp.connect(`amqp://${CONFIG.host}`, (err, connection) => {
if (err) {
throw err;
}
amqpConn = connection;
// start consume worker when connected
startWorker();
});
}
function startWorker() {
socketIOHandler((socket) => {
amqpConn.createChannel((error, channel) => {
... <---- all the bits as before
socket.on('msgSent', (msg) => {
channel.ack(msg);
});
})
});
io.listen(port);
}
start();

socket.io-client on('message') not working

I am using socket.io-client in a react native chat application. The socket connects fine and it responds to on('connection') but it doesn't respond to messages. What is the proper way to configure socket.io-client to handle custom events? All the documentation I find looks like my implementation. My messaging module:
import io from 'socket.io-client';
Messenger = (props) => {
const [data, setData] = useState([]);
const [test, setTest] = useState('');
const socket = io('https://test.com', {
autoConnect: false,
});
const getCredentials = async () => {
await socket.connect();
await fetchMessages();
}
useEffect(() => {
socket.connect();
socket.on('connect', function() {
setTest('connected!');
});
socket.on('message', function(message) {
setTest('message!');
});
socket.on('typing', function(typing) {
setTest('typing');
});
getCredentials();
}, []);
return (...);
}
My server:
var socket_io = require( 'socket.io' );
const io = socket_io();
io.use((socket, next) => {
sessionMiddleware(socket.request, {}, next);
});
io.on( "connection", function( socket )
{
if (socket.request.session.auth_user) {
redisClient.set(socket.request.session.auth_user._id.toString(), socket.id);
socket.on( "disconnect", function() {
console.log( "A user disconnected" );
redisClient.del(socket.request.session.auth_user._id);
});
}
});
It's really hard to say without seeing your server but if comparing your case to my case it should be something like that:
useEffect(() => {
const socket = io('https://test.com', {
autoConnect: false,
});
socket.on('connect', function () {
setTest('connected!');
socket.on('message', function (message) {
setTest('message!');
});
socket.on('typing', function (typing) {
setTest('typing');
});
});
}, []);

Resources