Unable to create a stable websocket implementation - javascript

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);
}

Related

NestJS, SuperTest - Socket doesn't get event

I am struggling with my e2e test about my socket.
The socket connects and logs well to the NestJS Gateway, but it doesn't come into my listener bedRequest.
My test consists of sending a create request through supertest and at the end, the gateway broadcasts a message to connected sockets and I want to verify it.
In Gateway logs, I see the client connected, logIn, and disconnected if it can help.
Thank you in advance.
it("/POST bedRequest", (done) => {
let bedRequestCreate = some payload
let expectedResult = other payload
const address = app.getHttpServer().listen().address();
const baseAddress = `http://[${address.address}]:${address.port}`;
const client = io(`${baseAddress}/`);
client.on("connect", () => { // this works
client.emit("logIn", {access_token: accessToken}, (isConnected) => {
expect(isConnected).toBeTruthy(); // this works
client.on("bedRequest", (data) => { // this doesn't work
expect(JSON.parse(data)).toMatchObject({
siteId: bedRequestCreate.siteId,
...expectedResult
});
done();
});
});
});
return request(app.getHttpServer())
.post("/api/bedRequest/")
.send(bedRequestCreate)
.set('Accept', 'application/json')
.set('Authorization', 'Bearer ' + accessToken)
.expect('Content-Type', /json/)
.expect(201)
.end((err, res) => {
if (err) return done(err);
expect(res.body.generatedMaps[0]).toMatchObject(expectedResult); // this works
});
});
Gateway :
#WebSocketGateway()
export class SocketGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
#WebSocketServer() server: Server;
wsClients = [];
private logger: Logger = new Logger('SocketGateway');
handleDisconnect(client: Client) {
for (let i = 0; i < this.wsClients.length; i++) {
if (this.wsClients[i] === client) {
this.wsClients.splice(i, 1);
break;
}
}
this.logger.log(`Client disconnected: ${client.id}`);
}
broadcast(event: String, message: any) {
const broadCastMessage = JSON.stringify(message);
for (let c of this.wsClients) { // sends to the right client
c.send(event, broadCastMessage);
}
}
#UseGuards(WebSocketJwtAuthGuard)
#SubscribeMessage("logIn")
handleLogIn(client) { // works
this.logger.log(`Socket Client connected : ${client.id} for user ${client.user.id} / ${client.mail}`);
this.wsClients.push(client);
return true;
}
handleConnection(client) {
return `Client connected: ${client.id}`;
}
afterInit() {
this.logger.log("SocketGateway initialized")
}
}
Okay I found it...
It was because of this line : c.send(event, broadCastMessage);
I changed send by emit and it works fine.

Websocket connection with apollo-server returns gibberish for connectionParams

onConnect should receive the connectionParams supplied by the client and then validate that the token has not expired by checking the token property on the connectionParams object. On the client, I am sending these params as follows:
const subOptions = {
reconnect: true,
connectionParams: async () => {
let token = await get("token")
let ret = {
token,
}
console.log("WEBSOCKET RETURN OBJECT", ret)
return ret
},
}
const subClient = new SubscriptionClient(subEndpoint, subOptions)
const subLink = new WebSocketLink(subClient)
On Client:
The object printed after "ON CONNECT" is scrambled and shows as follows. How does it end up in this format coming from the client? How can I debug this further?
On Server:
const ws = createServer(app)
ws.listen(PORT, () => {
console.log(`GraphQL Server is now running on http://localhost:${PORT}`)
// Set up the WebSocket for handling GraphQL subscriptions
new SubscriptionServer(
{
execute,
subscribe,
schema,
onConnect: (connectionParams, webSocket) => {
let req = {}
console.log("ON CONNECT")
console.log(connectionParams)
return checkToken(connectionParams.token, function(payload) {
return {
user: {
id: payload.userId,
exp: payload.exp,
iat: payload.iat,
},
}
})
},
},
{
server: ws,
path: "/subscriptions",
}
)
})
You can't use an async function for connectionParams for the moment.
Here is a workaround:
const wsLink = new WebSocketLink({
uri: wsUri,
options: {
reconnect: true,
},
});
wsLink.subscriptionClient.use([{
async applyMiddleware(options, next) {
const token = await getLoginToken();
options.context = { token };
next();
},
}]);

Receiving one notification for each tab in browser in foreground with FCM

I'm using FCM API to receive push notifications from browser. The firebase-messaging-sw.js works as expected and messaging.setBackgroundMessageHandler fires only once when the web app is in background. However, when the app is in foreground, I'm receiving one notification for each browser tab (if I have the app opened in 3 tabs, I receive 3 notifications). I wonder how should I handle this, since I can't find any reference to this issue. This is the code for FCM messages in the foreground:
import NotificationActionCreators from '../actions/NotificationActionCreators';
import NotificationService from './NotificationService';
import LocalStorageService from './LocalStorageService';
import { FIREBASE_SCRIPT, FCM_URL, FCM_API_KEY, FCM_AUTH_DOMAIN, FCM_PROJECT_ID, FCM_SENDER_ID, PUSH_PUBLIC_KEY } from '../constants/Constants';
class ServiceWorkerService {
constructor() {
this._messaging = null;
this._subscriptionData = null;
}
// This function is called once
init() {
this.loadScript(FIREBASE_SCRIPT, () => this.onFirebaseLoaded());
}
onFirebaseLoaded() {
// Initialize Firebase
let config = {
apiKey: FCM_API_KEY,
authDomain: FCM_AUTH_DOMAIN,
projectId: FCM_PROJECT_ID,
messagingSenderId: FCM_SENDER_ID
};
firebase.initializeApp(config);
this._messaging = firebase.messaging();
this.requestPermission();
// Callback fired if Instance ID token is updated.
this._messaging.onTokenRefresh(() => {
this._messaging.getToken()
.then((refreshedToken) => {
console.log('Token refreshed.');
NotificationActionCreators.unSubscribe(this._subscriptionData).then(() => {
// Indicate that the new Instance ID token has not yet been sent to the
// app server.
this.setTokenSentToServer(false);
// Send Instance ID token to app server.
this.sendTokenToServer(refreshedToken);
}, () => console.log('Error unsubscribing user'));
})
.catch(function(err) {
console.log('Unable to retrieve refreshed token ', err);
});
});
// Handle incoming messages.
// *** THIS IS FIRED ONCE PER TAB ***
this._messaging.onMessage(function(payload) {
console.log("Message received. ", payload);
const data = payload.data;
NotificationActionCreators.notify(data);
});
}
requestPermission() {
console.log('Requesting permission...');
return this._messaging.requestPermission()
.then(() => {
console.log('Notification permission granted.');
this.getToken();
})
.catch(function(err) {
console.log('Unable to get permission to notify.', err);
});
}
getToken() {
// Get Instance ID token. Initially this makes a network call, once retrieved
// subsequent calls to getToken will return from cache.
return this._messaging.getToken()
.then((currentToken) => {
if (currentToken) {
this.sendTokenToServer(currentToken);
} else {
// Show permission request.
console.log('No Instance ID token available. Request permission to generate one.');
this.setTokenSentToServer(false);
}
})
.catch(function(err) {
console.log('An error occurred while retrieving token. ', err);
this.setTokenSentToServer(false);
});
}
sendTokenToServer(currentToken) {
const subscriptionData = {
endpoint: FCM_URL + currentToken,
platform: 'Web'
};
if (!this.isTokenSentToServer()) {
console.log('Sending token to server...');
this.updateSubscriptionOnServer(subscriptionData);
} else {
console.log('Token already sent to server so won\'t send it again ' +
'unless it changes');
}
this._subscriptionData = subscriptionData;
}
isTokenSentToServer() {
return LocalStorageService.get('sentToServer') == 1;
}
setTokenSentToServer(sent) {
LocalStorageService.set('sentToServer', sent ? 1 : 0);
}
updateSubscriptionOnServer(subscriptionData) {
if (subscriptionData) {
NotificationActionCreators.subscribe(subscriptionData);
this.setTokenSentToServer(true);
this._subscriptionData = subscriptionData;
} else {
console.log('Not subscribed');
}
}
unSubscribe() {
this.removeSetTokenSentToServer();
return this._messaging.getToken()
.then((currentToken) => {
return this._messaging.deleteToken(currentToken)
.then(() => {
console.log('Token deleted.');
return NotificationActionCreators.unSubscribe(this._subscriptionData);
})
.catch(function(err) {
console.log('Unable to delete token. ', err);
return new Promise(function(resolve, reject) {
reject(error)
});
});
})
.catch(function(err) {
console.log('Error retrieving Instance ID token. ', err);
return new Promise(function(resolve, reject) {
reject(error)
});
});
}
}
removeSetTokenSentToServer() {
LocalStorageService.remove('sentToServer');
}
loadScript = function (url, callback) {
let head = document.getElementsByTagName('head')[0];
let script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = callback;
// Fire the loading
head.appendChild(script);
}
}
Is there any way to show the notification just for the first tab found?
The only way I've found to achieve this is to check and set a "notification id" variable in the local storage with a setTimeout with a random time:
this._messaging.onMessage(function(payload) {
const data = payload.data;
// This prevents to show one notification for each tab
setTimeout(() => {
if (localStorage.getItem('lastNotificationId')) != parseInt(data.notId)) {
localStorage.setItem('lastNotificationId', parseInt(data.notId))
NotificationActionCreators.notify(data);
}
}, Math.random() * 1000);
});
The notId is sent within the push notification and is an identifier for the notification.
One way is to create a unique variable per tab, say Math.random() or (new Date()).getMilliseconds() and store that on the server with the token. Now the server can target each tab by attaching the variable to the message, with each tab checking the message variable before acting.
To reduces odds of targeting a closed tab, send the variable up with each request, so the server always targets the latest one.
use document.hidden to detect active tab
if (!document.hidden) {
NotificationActionCreators.notify(data);
}

401 using hybrid relay connections with node.js

I was just running the sample code from Microsoft to test out hybrid relay connections using node.js
running:
node listener.js
producing the following error:
errorError: unexpected server response (401)
Here is my code (node listener.js)...
const WebSocket = require('hyco-ws');
const ns = "hcrelay.servicebus.windows.net";
const path = "hc1";
const keyrule = "hc1key";
const key = "Password#1234";
var wss = WebSocket.createRelayedServer(
{
server : WebSocket.createRelayListenUri(ns, path),
token: WebSocket.createRelayToken('http://' + ns, keyrule,key)
},
function (ws) {
console.log('connection accepted');
ws.onmessage = function (event) {
console.log(event.data);
};
ws.on('close', function () {
console.log('connection closed');
});
});
console.log('listening');
wss.on('error', function(err) {
console.log('error' + err);
});
I tried your code and it worked fine. The key point is that I set keyrule equal to the name of Shared access policy and set key equal to the primary key of Shared access policy.
const WebSocket = require('hyco-ws');
const ns = "hcrelay.servicebus.windows.net"; // Relay namespace
const path = "hc1"; // Hybrid connection name
const keyrule = "RootManageSharedAccessKey"; // Policy name
const key = "sjSqVUo..."; // Primary key
var wss = WebSocket.createRelayedServer(
{
server : WebSocket.createRelayListenUri(ns, path),
token: WebSocket.createRelayToken('http://' + ns, keyrule,key)
},
function (ws) {
console.log('connection accepted');
ws.onmessage = function (event) {
console.log(event.data);
};
ws.on('close', function () {
console.log('connection closed');
});
});
console.log('listening');
wss.on('error', function(err) {
console.log('error' + err);
});

Using Node.js to connect to a REST API

Is it sensible to use Node.js to write a stand alone app that will connect two REST API's?
One end will be a POS - Point of sale - system
The other will be a hosted eCommerce platform
There will be a minimal interface for configuration of the service. nothing more.
Yes, Node.js is perfectly suited to making calls to external APIs. Just like everything in Node, however, the functions for making these calls are based around events, which means doing things like buffering response data as opposed to receiving a single completed response.
For example:
// get walking directions from central park to the empire state building
var http = require("http");
url = "http://maps.googleapis.com/maps/api/directions/json?origin=Central Park&destination=Empire State Building&sensor=false&mode=walking";
// get is a simple wrapper for request()
// which sets the http method to GET
var request = http.get(url, function (response) {
// data is streamed in chunks from the server
// so we have to handle the "data" event
var buffer = "",
data,
route;
response.on("data", function (chunk) {
buffer += chunk;
});
response.on("end", function (err) {
// finished transferring data
// dump the raw data
console.log(buffer);
console.log("\n");
data = JSON.parse(buffer);
route = data.routes[0];
// extract the distance and time
console.log("Walking Distance: " + route.legs[0].distance.text);
console.log("Time: " + route.legs[0].duration.text);
});
});
It may make sense to find a simple wrapper library (or write your own) if you are going to be making a lot of these calls.
Sure. The node.js API contains methods to make HTTP requests:
http.request
http.get
I assume the app you're writing is a web app. You might want to use a framework like Express to remove some of the grunt work (see also this question on node.js web frameworks).
/*Below logics covered in below sample GET API
-DB connection created in class
-common function to execute the query
-logging through bunyan library*/
const { APIResponse} = require('./../commonFun/utils');
const createlog = require('./../lib/createlog');
var obj = new DB();
//Test API
routes.get('/testapi', (req, res) => {
res.status(201).json({ message: 'API microservices test' });
});
dbObj = new DB();
routes.get('/getStore', (req, res) => {
try {
//create DB instance
const store_id = req.body.storeID;
const promiseReturnwithResult = selectQueryData('tablename', whereField, dbObj.conn);
(promiseReturnwithResult).then((result) => {
APIResponse(200, 'Data fetched successfully', result).then((result) => {
res.send(result);
});
}).catch((err) => { console.log(err); throw err; })
} catch (err) {
console.log('Exception caught in getuser API', err);
const e = new Error();
if (err.errors && err.errors.length > 0) {
e.Error = 'Exception caught in getuser API';
e.message = err.errors[0].message;
e.code = 500;
res.status(404).send(APIResponse(e.code, e.message, e.Error));
createlog.writeErrorInLog(err);
}
}
});
//create connection
"use strict"
const mysql = require("mysql");
class DB {
constructor() {
this.conn = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'pass',
database: 'db_name'
});
}
connect() {
this.conn.connect(function (err) {
if (err) {
console.error("error connecting: " + err.stack);
return;
}
console.log("connected to DBB");
});
}
//End class
}
module.exports = DB
//queryTransaction.js File
selectQueryData= (table,where,db_conn)=>{
return new Promise(function(resolve,reject){
try{
db_conn.query(`SELECT * FROM ${table} WHERE id = ${where}`,function(err,result){
if(err){
reject(err);
}else{
resolve(result);
}
});
}catch(err){
console.log(err);
}
});
}
module.exports= {selectQueryData};
//utils.js file
APIResponse = async (status, msg, data = '',error=null) => {
try {
if (status) {
return { statusCode: status, message: msg, PayLoad: data,error:error }
}
} catch (err) {
console.log('Exception caught in getuser API', err);
}
}
module.exports={
logsSetting: {
name: "USER-API",
streams: [
{
level: 'error',
path: '' // log ERROR and above to a file
}
],
},APIResponse
}
//createlogs.js File
var bunyan = require('bunyan');
const dateFormat = require('dateformat');
const {logsSetting} = require('./../commonFun/utils');
module.exports.writeErrorInLog = (customError) => {
let logConfig = {...logsSetting};
console.log('reached in writeErrorInLog',customError)
const currentDate = dateFormat(new Date(), 'yyyy-mm-dd');
const path = logConfig.streams[0].path = `${__dirname}/../log/${currentDate}error.log`;
const log = bunyan.createLogger(logConfig);
log.error(customError);
}
A more easy and useful tool is just using an API like Unirest; URest is a package in NPM that is just too easy to use jus like
app.get('/any-route', function(req, res){
unirest.get("https://rest.url.to.consume/param1/paramN")
.header("Any-Key", "XXXXXXXXXXXXXXXXXX")
.header("Accept", "text/plain")
.end(function (result) {
res.render('name-of-the-page-according-to-your-engine', {
layout: 'some-layout-if-you-want',
markup: result.body.any-property,
});
});

Categories

Resources