Usecase: This runs on the server side (Keystone) of an Android application
- App connects to the socket with the user's accesstoken
- App shows indicators for all the other user's who are connected to the socket
- When a user changes some data in the app, a force refresh is send over the socket to all the "online" users so that they know to fetch the latest data
Main problem:
- It works until a client loses it's internet connection right in between the intervals. Then the socket connection is closed and not reopened.
I don't know if it's a problem with my implementation or a problem with implementation on the client side
Implementation uses:
- https://github.com/websockets/ws
- More specifically https://github.com/websockets/ws#how-to-detect-and-close-broken-connections
Here is the implementation on the server:
const clients = {};
let wss = null;
const delimiter = '_';
/**
* Clients are stored as "companyId_deviceId"
*/
function getClients() {
return clients;
}
function sendMessage(companyId, msg) {
try {
const clientKey = Object.keys(clients).find((a) => a.split(delimiter)[0] === companyId.toString());
const socketForUser = clients[clientKey];
if (socketForUser && socketForUser.readyState === WebSocket.OPEN) {
socketForUser.send(JSON.stringify(msg));
} else {
console.info(`WEBSOCKET: could not send message to company ${companyId}`);
}
} catch (ex) {
console.error(`WEBSOCKET: could not send message to company ${companyId}: `, ex);
}
}
function noop() { }
function heartbeat() {
this.isAlive = true;
}
function deleteClient(clientInfo) {
delete clients[`${clientInfo.companyId}${delimiter}${clientInfo.deviceId}`];
// notify all clients
forceRefreshAllClients();
}
function createSocket(server) {
wss = new WebSocket.Server({ server });
wss.on('connection', async (ws, req) => {
try {
// verify socket connection
let { query: { accessToken } } = url.parse(req.url, true);
const decoded = await tokenHelper.decode(accessToken);
// add new websocket to clients store
ws.isAlive = true;
clients[`${decoded.companyId}${delimiter}${decoded.deviceId}`] = ws;
console.info(`WEBSOCKET: ➕ Added client for company ${decoded.companyId} and device ${decoded.deviceId}`);
await tokenHelper.verify(accessToken);
// notify all clients about new client coming up
// including the newly created socket client...
forceRefreshAllClients();
ws.on('pong', heartbeat);
} catch (ex) {
console.error('WEBSOCKET: WebSocket Error', ex);
ws.send(JSON.stringify({ type: 'ERROR', data: { status: 401, title: 'invalid token' } }));
}
ws.on('close', async () => {
const location = url.parse(req.url, true);
const decoded = await tokenHelper.decode(location.query.accessToken);
deleteClient({ companyId: decoded.companyId, deviceId: decoded.deviceId });
});
});
// Ping pong on interval will remove the client if the client has no internet connection
setInterval(() => {
Object.keys(clients).forEach((clientKey) => {
const ws = clients[clientKey];
if (ws.isAlive === false) return ws.terminate();
ws.isAlive = false;
ws.ping(noop);
});
}, 15000);
}
function forceRefreshAllClients() {
setTimeout(function () {
Object.keys(clients).forEach((key) => {
const companyId = key.split(delimiter)[0];
sendMessage(companyId, createForcedRefreshMessage());
});
}, 1000);
}