Node.js + socket.io + node-amqp and queue binginds when "re" connecting thru socket.io - rabbitmq

I have one scenario which is very close to this sample:
One main screen:
this screen (client side) will connect to the socket.io server thru server:9090/scope (io.connect("http://server:9090/scope)) and will send one event "userBindOk" (socket.emit("userBindOk", message)) to the socket.io server;
the server receives the connection and the "userBindOk". At this moment, the server should get the active connection to rabbitmq server and bind the queue to the respective user that just connected to the application thru socket.io. sample:
socket.on("connection", function(client){
//client id is 1234
// bind rabbitmq exchange, queue, and:
queue.subscribe(//receive callback);
})
So far, no problem - I can send/receive messages thru socket.io without problems.
BUT, If I refresh the page, all those steps will be done again. As consequence, the binding to the queue will occur, but this time related to another session of the socket.io client. This means that if I send a message to the queue which is related to the first socket.io session (before the page refresh), that bind should (I think) receive the message and send it to a invalid socket.io client (page refresh = new client.id on the socket.io context). I can prove this behaviour because every time I refresh the page I need to send x times more messages. For instance: I`ve connected for the first time: - so, 1 message - one screen update; refresh the page: I need to send 2 messages to the queue and only the second message will be received from the "actual" socket.io client session - this behaviour will occur as many as I refresh the page (20 page refreshs, 20 messages to be sent to a queue and the server socket.io "last" client will send the message to the client socket.io to render into the screen).
The solutions I believe are:
Find a way to "unbind" the queue when disconnecting from the socket.io server - I didn`t see this option at the node-amqp api yet (waiting for it :D)
find a way to reconnect the socket.io client using the same client.id. This way I can identify the client that is coming and apply some logic to cache the socket.
Any ideas? I tried to be very clear... But, as you know, it`s not so eaey to expose your problem when trying to clarify something that is very specific to some context...
tks

I solved it like this:
I used to declare the rabbitMq queue as durable=true,autoDelete=false,exclusive=false and in my app there was 1 queue/user and 1 exchange(type=direct) with the routing_key name=queueName, my app also used the queue for other client diffent to browsers like android app or iphone app as push fallback, so i use to crear 1 queue for earch user.
The solution to this problem was to change my rabbitMQ queue and exchange declaration. Now i declare the exchange/user as fanout and autoDelete=True, and the user is going to have N queues with durable=true, autoDelete=true, exclusive=true (No. queue = No. clients) and all the queues are bind to the user-exchange(multicast).
NOTE: my app is wirten in django, and i use node+socket+amqp to be able to comunicate with the browser using web.scokets, so i use node-restler to query my app api to get the user-queue info.
thats the rabbitMQ side, for the node+amqp+socket i did this:
server-side:
onConnect: the declaration of the user exchange as fanout, autoDelete, durable. then declaration of the queue as durable, autodelete and exclusive, then the queue.bind to the user-exchange and finaly the queue.subscribe and the socket.disconnect will destroy the queue so there are going to exist queue as client connected the app and this solve the problem of the refresh and allow the user to have more than 1 window-tab with the app:
Server-side:
/*
* unCaught exception handler
*/
process.on('uncaughtException', function (err) {
sys.p('Caught exception: ' + err);
global.connection.end();
});
/*
* Requiere libraries
*/
global.sys = require('sys');
global.amqp = require('amqp');
var rest = require('restler');
var io = require('socket.io').listen(8080);
/*
* Module global variables
*/
global.amqpReady = 0;
/*
* RabbitMQ connection
*/
global.connection = global.amqp.createConnection({
host: host,
login: adminuser,
password: adminpassword,
vhost: vhost
});
global.connection.addListener('ready',
function () {
sys.p("RabbitMQ connection stablished");
global.amqpReady = 1;
}
);
/*
* Web-Socket declaration
*/
io.sockets.on('connection', function (socket) {
socket.on('message', function (data) {
sys.p(data);
try{
var message = JSON.parse(data);
}catch(error){
socket.emit("message", JSON.stringify({"error": "invalid_params", "code": 400}));
var message = {};
}
var message = JSON.parse(data);
if(message.token != undefined) {
rest.get("http://dev.kinkajougames.com/api/push",
{headers:
{
"x-geochat-auth-token": message.token
}
}).on('complete',
function(data) {
a = data;
}).on('success',
function (data){
sys.p(data);
try{
sys.p("---- creating exchange");
socket.exchange = global.connection.exchange(data.data.bind, {type: 'fanout', durable: true, autoDelete: true});
sys.p("---- declarando queue");
socket.q = global.connection.queue(data.data.queue, {durable: true, autoDelete: true, exclusive: false},
function (){
sys.p("---- bind queue to exchange");
//socket.q.bind(socket.exchange, "*");
socket.q.bind(socket.exchange, "*");
sys.p("---- subscribing queue exchange");
socket.q.subscribe(function (message) {
socket.emit("message", message.data.toString());
});
}
);
}catch(err){
sys.p("Imposible to connection to rabbitMQ-server");
}
}).on('error', function (data){
a = {
data: data,
};
}).on('400', function() {
socket.emit("message", JSON.stringify({"error": "connection_error", "code": 400}));
}).on('401', function() {
socket.emit("message", JSON.stringify({"error": "invalid_token", "code": 401}));
});
}
else {
socket.emit("message", JSON.stringify({"error": "invalid_token", "code": 401}));
}
});
socket.on('disconnect', function () {
socket.q.destroy();
sys.p("closing socket");
});
});
client-side:
The socket intance with options 'force new connection'=true and 'sync disconnect on unload'= false.
The client side use the onbeforeunload and onunload windows object events to send socket.disconnect
The client on socket.connect event send the user token to node.
proces message from socket
var socket;
function webSocket(){
//var socket = new io.Socket();
socket = io.connect("ws.dev.kinkajougames.com", {'force new connection':true, 'sync disconnect on unload': false});
//socket.connect();
onSocketConnect = function(){
alert('Connected');
socket.send(JSON.stringify({
token: Get_Cookie('liveScoopToken')
}));
};
socket.on('connect', onSocketConnect);
socket.on('message', function(data){
message = JSON.parse(data);
if (message.action == "chat") {
if (idList[message.data.sender] != undefined) {
chatboxManager.dispatch(message.data.sender, {
first_name: message.data.sender
}, message.data.message);
}
else {
var username = message.data.sender;
Data.Collections.Chats.add({
id: username,
title: username,
user: username,
desc: "Chat",
first_name: username,
last_name: ""
});
idList[message.data.sender] = message.data.sender;
chatboxManager.addBox(message.data.sender, {
title: username,
user: username,
desc: "Chat",
first_name: username,
last_name: "",
boxClosed: function(id){
alert("closing");
}
});
chatboxManager.dispatch(message.data.sender, {
first_name: message.data.sender
}, message.data.message);
}
}
});
}
webSocket();
window.onbeforeunload = function() {
return "You have made unsaved changes. Would you still like to leave this page?";
}
window.onunload = function (){
socket.disconnect();
}
And that's it, so no more round-robing of the message.

Related

How to distinguish connected users in WebSocket?

I'm trying to create an event in express.js, where connected users will be sent to the client.
My goal is that in client side user can see who belongs that particular message sent from.
As it describes in docs and as I could also run in localhost, broadcasting messages works fine.
wss.on("connection", function connection(ws) {
ws.on("message", function message(data) {
wss.clients.forEach(function each(client) {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(data);
}
});
});
});
So before sending messages to client, there should be another event where connected username should be sent, so that in client side can be defined who that particular message belongs.
My idea is to create users object which supposed to keep track of all connected users like here.
const users = {};
wss.on("connection", function connection(ws) {
ws.on("userName", function message(userName) {//this doesn't work
const user = {
name: userName,
id: uuid(), // some unique id, no idea if websocket has or not
};
users[id]= user
ws.send("user", user) // current user who belongs the message to
ws.send("users", users) // users to show connected users list
});
ws.on("message", function message(data) {
wss.clients.forEach(function each(client) {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(data);
}
});
});
});
user event doesn't work, it is just my idea but not sure if how to do it correctly.
Any help will be appreciated.

NEst RabbitMQ RPC nack doesn't reject the promise

With nestJS rabbit MQ I use RPC from one server to another one. What I basically want is await this.amqpConnection.request to reject the promise when the consumer throw an error. As I understand from this and this and this documentation when you NACK (negative ack) the message it should put the RPC response to a corresponding queue with an error information which would be passed to a client. Here's some code example:
Producer
import { AmqpConnection } from '#golevelup/nestjs-rabbitmq';
let amqpConnection: AmqpConnection;
sendMessage(payload: any) {
try {
const result = await this.amqpConnection.request({
exchange: 'user',
routingKey: 'test',
queue: 'test',
payload,
});
console.log(result);
} catch (e) {
console.error(e); // I expect this flow !!!
}
}
Consumer:
#RabbitRPC({
exchange: 'user',
routingKey: 'test',
queue: 'test',
errorBehavior: MessageHandlerErrorBehavior.NACK
})
public async rpcHandler(payload: any): Promise<any> {
throw Error("Error text that should be proxied to console.error"):
}
But the message doesn't even return to the client. Client gets rejected after a timeout, rather because of an error during consumer processing. If my understanding is not correct I would like to know if there's a builtin mechanism with nestjs to reject the message w/o manually creating some response types for an error and handling it on a client.
You should throw exactly the RpcException from #nestjs/microservices and only with a certain argument
throw new RpcException({
code: 42, // some number code
message: 'wait. oh sh...', // some string value
});
Also if you using a custom exception filter then it should extends the BaseRpcExceptionFilter from #nestjs/microservices. Call the getType() method on an ArgumentsHost and if it's result equals to 'rpc' then using the super.catch method.

Can't get click_action to work on FCM notifications with web app / PWA

I'm trying to get my "click_action" to take users to specific URLs on notifications that I'm sending to clients, but whatever I do it either does nothing (desktop) or just opens the PWA (android). The messages are coming through fine (checked in Chrome console) but clicking just doesn't seem to work.
I have the following in my service worker, cribbed from various places including other answers provided on this site:
importScripts('https://www.gstatic.com/firebasejs/7.14.3/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/7.14.3/firebase-messaging.js');
// importScripts('/__/firebase/init.js');
/* An empty service worker! */
self.addEventListener('fetch', function(event) {
/* An empty fetch handler! */
});
var firebaseConfig = {
//REDACTED
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
const messaging = firebase.messaging();
messaging.setBackgroundMessageHandler(function(payload) {
console.log('[firebase-messaging-sw.js] Received background message ', payload);
// Customize notification here
notificationTitle = payload.notification.title;
notificationOptions = {
body: payload.notification.body,
icon: payload.notification.icon,
click_action: payload.notification.click_action
};
return self.registration.showNotification(notificationTitle,
notificationOptions);
});
self.addEventListener('notificationclick', function(event) {
let url = event.notification.click_action;
// I've also added a data.click_action field in my JSON notification, and have tried using that
// instead, but that didn't work either
console.log('On notification click: ', event.notification.tag);
event.notification.close(); // Android needs explicit close.
event.waitUntil(
clients.matchAll({ includeUncontrolled: true, type: 'window' }).then( windowClients => {
// Check if there is already a window/tab open with the target URL
for (var i = 0; i < windowClients.length; i++) {
var client = windowClients[i];
// If so, just focus it.
if (client.url === url && 'focus' in client) {
return client.focus();
}
}
// If not, then open the target URL in a new window/tab.
if (clients.openWindow) {
return clients.openWindow(url);
}
})
);
});
self.onnotificationclick = function(event) {
let url = event.notification.click_action;
console.log('On notification click: ', event.notification.tag);
event.notification.close();
// This looks to see if the current is already open and
// focuses if it is
event.waitUntil(clients.matchAll({ includeUncontrolled: true, type: 'window' }).then(function(clientList) {
for (var i = 0; i < clientList.length; i++) {
var client = clientList[i];
if (client.url == url && 'focus' in client)
return client.focus();
}
if (clients.openWindow)
return clients.openWindow(url);
}));
};
The notifications come through fine on both android (installed PWA) and chrome, and the message payload in the developer console is well formatted and received fine. In the message I'm sending from the server I have a URL with a custom parameter on the end (e.g. https://[domain]/list.php?userid=123) but, as above, clicking on the notification doesn't do anything on windows/chrome, and on the android it opens the PWA successfully but then doesn't go to the URL in the payload, it just goes to wherever the PWA was when last open. The "userid" changes depending on the message trigger.
Sample JSON of message payload:
{data: {…}, from: "xxx", priority: "high", notification: {…}, collapse_key: "do_not_collapse"}
collapse_key: "do_not_collapse"
data: {gcm.notification.badge: "[logo URL]", click_action: "https://[URL]/list.php?userid=33"}
from: "xxx"
notification:
body: "'5' has just been added"
click_action: "https://[URL]/list.php?userid=33"
icon: "https://[logo URL]"
title: "alert "
I also saw something about "webpush": { "fcm_options": { "link": "https://dummypage.com"}} on https://firebase.google.com/docs/cloud-messaging/js/receive but couldn't figure out if that was relevant or needed also.
Am very surprised just providing a URL in the click_action doesn't seem to just do that action when you click the notificaiton! Is anything needed in the service worker at all?!?!
Could one of the problems be that the PWA doesn't update the SW regularly, and so if my code above should work (a big if!) then i just need to wait for the SW to update on the installed android app? If so, is there a way to speed up its updating?!?
Thanks so much in advance for any assistance. Am tying myself in knots here!
I spent a lot of time looking for a solution for the same problem. Maybe this can help :
if you send notification with firebase messaging, you can use webpush field. firebase messaging client library execute self.registration.showNotification() ... No more need messaging.onBackgroundMessage in your service worker.
// firebabse-coud-function.js
app.messaging().send({
webpush: {
notification: {
title: notification?.title || "Default title",
icon: notification?.icon || "/icon.png",
badge: notification?.icon || "/icon.png",
},
fcmOptions: {
link: `${BASE_URL || ""}${notification?.clickAction || "/"}`,
}
},
data: {
userID: notification.userID,
link: notification?.clickAction || "/",
},
topic
});
Most importantly, in your service worker add a 'notificationclick' event listener before calling firebase.messaging()
so my service worker looks like:
// firebase-messaging-sw.js
// ...
self.addEventListener('notificationclick', function (event) {
console.debug('SW notification click event', event)
const url = event.notification?.data?.FCM_MSG?.data?.link;
// ...
})
const messaging = firebase.messaging();
messaging.onBackgroundMessage(function (payload) {
// received others messages
})
For me, clicking on the event does not go to the correct url. So i add this:
// background client - service worker
const channel = new BroadcastChannel('sw-messages');
self.addEventListener('notificationclick', function (event) {
console.debug('SW notification click event', event)
const url = event.notification?.data?.FCM_MSG?.data?.link;
channel.postMessage({
type: 'notification_clicked',
data: {
title: event.notification.title,
clickAction: url
}
});
})
// foreground client
const channel = new BroadcastChannel('sw-messages');
channel.addEventListener("message", function (event) {
// go the page
})
I hope this helps someone.
This question and other answers seems to be related to the legacy FCM API, not the v1.
In those case, I needed the SW to open any url sent by FCM, which is by default not possible because host differs (see here).
Also, the notification object as changed, and the url for the webpush config is there now: event.notification.data.FCM_MSG.notification.click_action
So adapting others answers to get the correct field and open the url by only editing the firebase-messaging-sw.js:
importScripts('https://www.gstatic.com/firebasejs/8.2.10/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/8.2.10/firebase-messaging.js');
// Initialize the Firebase app in the service worker by passing in
// your app's Firebase config object.
// https://firebase.google.com/docs/web/setup#config-object
firebase.initializeApp({
...
})
self.addEventListener('notificationclick', function(event) {
event.notification.close();
// fcp_options.link field from the FCM backend service goes there, but as the host differ, it not handled by Firebase JS Client sdk, so custom handling
if (event.notification && event.notification.data && event.notification.data.FCM_MSG && event.notification.data.FCM_MSG.notification) {
const url = event.notification.data.FCM_MSG.notification.click_action;
event.waitUntil(
self.clients.matchAll({type: 'window'}).then( windowClients => {
// Check if there is already a window/tab open with the target URL
for (var i = 0; i < windowClients.length; i++) {
var client = windowClients[i];
// If so, just focus it.
if (client.url === url && 'focus' in client) {
return client.focus();
}
}
// If not, then open the target URL in a new window/tab.
if (self.clients.openWindow) {
console.log("open window")
return self.clients.openWindow(url);
}
})
)
}
}, false);
const messaging = firebase.messaging();
(register the addEventListener before initializing messaging)
Just add addeventlistner notification click event before calling firebase.messaging()
Everything will work fine.
importScripts('https://www.gstatic.com/firebasejs/8.4.1/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/8.4.1/firebase-messaging.js');
self.onnotificationclick = function(event) {
console.log('On notification click: ', event.notification.tag);
event.notification.close();
// This looks to see if the current is already open and
// focuses if it is
event.waitUntil(clients.matchAll({
type: "window"
}).then(function(clientList) {
for (var i = 0; i < clientList.length; i++) {
var client = clientList[i];
if (client.url == '/index' && 'focus' in client)
return client.focus();
}
if (clients.openWindow)
return clients.openWindow('/index');
}));
};
var firebaseConfig = {
apiKey: "xcxcxcxcxcxc",
authDomain: "xcxcxc.firebaseapp.com",
projectId: "fdsfdsdfdf",
storageBucket: "dfsdfs",
messagingSenderId: "sdfsdfsdf",
appId: "sdfsdfsdfsdfsdfsdf"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
const messaging = firebase.messaging();

how to add attributes to a PUT request in GUN?

I have the following code in my HTML page
Gun.on('opt', function (ctx) {
if (ctx.once) {
return
}
this.to.next(ctx)
window.auth = ctx.opt.auth
ctx.on('get', function (msg) {
msg.auth = window.auth
this.to.next(msg)
})
ctx.on('put', function (msg) {
msg.put.auth = window.auth
this.to.next(msg)
})
})
var gun = Gun({
peers: ['http://localhost:8765/gun'],
auth: {
user: 'mroon',
password: 'titi'
}
})
On the server, I simply watch the requests
Gun.on('create', function(db) {
console.log('gun created')
this.to.next(db);
db.on('get', function(request) {
// this request contains the auth attribute from the client
this.to.next(request);
});
db.on('put', function(request) {
// this request does not contain the auth attribute from the client
this.to.next(request);
});
});
every time I query the graph with gun.get('someAttribute') the request on the server contains the auth attribute.
but when a gun.get('someAttribute').put({attribute: 'my new value'}) is called, the request on the server does not contain the auth attribute.
How can I add the auth attribute to the put request in such a way that all the peers will get it too?
#micha-roon you jumped straight to GUN's core/internal wire details, which is not the easiest thing to start with, but here is something I do that I'm guessing is what you are looking for:
(if not, please just comment & I'll update)
What this does is it adds a DEBUG flag to all outbound messages in GUN, you can change this to add other metadata or info
Gun.on('opt', function(root){
if(!root.once){
root.on('out', function(msg){
msg.DBG = msg.DBG || +new Date;
this.to.next(msg);
});
}
this.to.next(root);
})
Also another good reference: https://github.com/zrrrzzt/bullet-catcher

How to create consumer Rabbitmq dynamically?

Is it possible to launch RabbitMQ consumer dynamically. A mean connect consumer to exist queue after specific time?
Or all consumers should be created in advance?
My case can be without consumers, when queues are filled by messages. Could I connect consumers after some time?
Yes, you could do it just like channel not yet created.
example for node.js
const conn = await amqplib.connect(`${rabbitmq.url}?heartbeat=300`);
conn.on('error', function (err) {
api.log.error('AMQP:Error:', err);
});
conn.on('close', () => {
api.log.info("AMQP:Closed");
});
const ch = await conn.createChannel();
await ch.assertQueue(queue_name, queue_options); // check if queue_name exists,
// if not, creates it
await ch.consume(queue_name, callback) // message from queue goes to callback