Bidirectional websockets with Redux Saga

Saga eventChannel with an alternative handling of websocket connections in a login system

It is a widely used scenario when users have permanent websockets directly on a landing page. Even if a user is not logged in. According to this scenario a permanent connection established at every page visit independently whether the user logged in or not. After a successful login the client sends a subscription request to the server.

Maybe there are enough reasons for the above model. But my preference is the model when the connection is established first after a user has logged in. It is more elegant and scalable. To enable bidirectional data flow two listeners have to be implemented. One for the external websocket server and one for the internal actions, for example user triggered events.

Without discussing the pros and cons of these two models, the last one will be described in a use case based on Redux Saga. Redux Saga is a middleware for redux side effects. For more information about this library please visit its github repository (https://github.com/redux-saga/redux-saga).

A websocket connection can be established using event eventChannel. “eventChannel (a factory function, not an Effect) creates a Channel for events but from event sources other than the Redux Store. Here an example how it works:

function watchMessages(socket) {
return eventChannel((emit) => {
socket.onopen = () => {
socket.send('Connection estabished'); // Send data to server
};
socket.onmessage = (event) => {
const msg = JSON.parse(event.data);
emit({ item: `v${msg.variable});
};
return () => {
socket.close();
};
});
}

This works pretty straightforward in one direction, namely if you are going to only subscribe to a websocket server to receive messages from it. However the event channel doesn’t enable you to send data to the websocket server. Once a channel created you are no more able to pass any variable into it. Ans sending data to server doesn’t represent an (asynchronious) side effect, so this would extend the scope of the library. Another function sharing the same connection has to be used for this purpose.

It would be nice to integrate an opening and closing connection for login/logout. The websocket instance should be implemented within the websocket init function scope and not in global scope. After some trials and errors the following combination enabled an elegant bundling of these functionalities:

function* wsHandling() {
while (true) {
const data = yield take('START_WEBSOCKET');
const socket = new WebSocket(`wss://example.com/?token=${token}`);
const socketChannel = yield call(watchMessages, socket);
    const { cancel } = yield race({
task: [call(externalListener, socketChannel), call(internalListener, socket)],
cancel: take('STOP_WEBSOCKET')
});
    if (cancel) {
socketChannel.close();
}
}
}

This generator function (loop within it) waits for the redux action ‘’START_WEBSOCKET’ and consequently establishes a websocket connection as soon a user successfully passes login. The interesting component is the “race” command. Straight after the user login the “task” begins the execution of a passed function, until the action ‘STOP_WEBSOCKET’ is fired. And this is first fired when e.g. the user pushed log out button.

In order to have a bidirectional websocket, two functions have to be passed to the “race” command, the event channel (incoming messages from ws server) and the push generator(sending to ws server). This can be achieved by putting them into an array:

[call(externalListener, socketChannel), call(internalListener, socket)]

These two params are listeners: external and internal. The fisrt param function externalListener is the Saga channel listening to the external websocket server and processing the incoming data. The second one, internalListener, is a generator function listening to user commands, for example button events, dropdown changes etc. Here is an example how this listener can be implemented:

function* internalListener(socket) {
while (true) {
const data = yield take('EXE_TASK');
socket.send(JSON.stringify({ type: 'setTask', status: 'open' }))
}
}

These both listeners are disabled, as soon the user logged out.

This way incoming and outgoing websocket messages can be combined, starting the two listeners just after login and removing the listeners and the connection just after logout.

This is one of many possible scenarios for a login system with websockets based on redux saga. I hope you will find this article useful. If you have suggestions or ideas for improvements please feel free to contact me.