Issue in publishing custom events to socket.io channels in feathersjs - node.js

I am trying to publish data to a frontend angular application using socket.io in feathersjs. I have created a channel as follow and created mockData stream.
function mockData(app) {
app.emit('testEvent', { test: 'Something happened'});
};
setInterval(mockData.bind(null, app), 1000);
app.publish('testEvent', (data) => {
console.log('test');
return app.channel('testStream1');}
)
And when client is connected, I have added the client to testStream1 channel as follows.
app.on('connection', connection => {
// On a new real-time connection, add it to the anonymous channel
app.channel('testStream1');
app.channel('anonymous').join(connection);
app.channel('testStream1').join(connection);
});
Here nothing in app.publish callback executes, but if I try to directly emit from socketio object then I can receive values in client.
function mockData(app) {
app.emit('testEvent', { test: 'Something happened'});
app.io.emit('testStream1', { text: 'A client connected!' }); // Adding this line, I am able to get values from socketio client
};
Seems that app.publish([event,] fn) doesn't register the publishing function for custom event testEvent. How to get the data using app.publish ?

It might be neat feature in the future but currently, publishers and channels are only intended for service events, not global events. Although the app object is an event emitter, it won't send anything to the client. If you create a custom service event named testEvent you will be able to do
app.service('myservice').emit('testEvent', 'this will be published to clients');
You can also emit any Socket.io event on app.io directly but it will not go through Feathers channel/publishing mechanism.

Related

Custom room Socket.io NodeJS

I am making an application with sockets and the need arises to broadcast information, but only to people who are inside a room.
This is my code from the server.ts
// Dependencies
import express from 'express'
import http from 'http'
import socket from 'socket.io';
import {connect, disconnect, orderChanged} from './sockets/socket';
import {config} from 'dotenv';
config ();
// Main class
export default class server {
_private static instance: server
public app: express.Application
public port: number
http: http.Server private
public io: socket.Server
// Initialize variables and methods
// Singleton pattern implementation
private constructor () {
this.app = express ()
this.port = Number (process.env.SRV_PORT)
this.http = new http.Server (this.app)
this.io = new socket.Server (this.http, {
cors: {
origin: true,
credentials: true
}
})
this.listenSockets ();
}
// Return the instance running Singleton pattern
public static get instance () {
returns this._instance || (this._instance = new Server ())
}
// Method to start the server
start (callback: any) {
this.http.listen (this.port, callback)
}
private listenSockets (): void {
console.log ('Listening Sockets');
this.io.on ('connection', client => {
console.log ('Connected to room', client.rooms, '-', client.id);
// User disconnected
disconnect (client);
connect (client);
});
}
}
Since node starts, an instance is created in DP Singleton and the socket listener is launched
When an operation happens in the database, anywhere in the app, I send it to call and send information to the front-end which is correctly received by the front-end and does what it has to do. Example url / edit-products
import server from '../core/server';
// Socket broadcast, new information
const __id = String (req.headers.id);
const updatedData = await getNewData (__id);
Server.instance.io.emit ('data changed', updatedData);
The problem is that this information is sent indiscriminately to all users connected to the socket. Now, I have a unique ID that brings multiple users together in a MongoDB model. You could use that ID to broadcast only to users with that ID. There is a logic that implies that if the user connects from Mexico, add it to an Array of people in MongoDB, otherwise it will add it to another MongoDB document, then they are two different IDs.
I would love the room to be that ID.
I saw that I could use the socket's join () method, but that function derives from the connected client, not from the server itself. I try to issue the information like this
// Socket broadcast, new information
const __id = String (req.headers.id);
const updatedData = await getNewData (__id);
Server.instance.io.in (updatedData._id) .emit ('data changed', updatedData);
But at no point did I set up that "ROOM". When the user login, he could add it but I don't know how to create a custom room, he tried something like this
const user = await UserModel.find (_data);
Server.instance.io.join (user.channel._id);
But that function within io does not exist.
It exists this way, but it doesn't work for me
Server.instance.io.on ('user-join', (socket: Socket) => {
console.log (plug);
socket.join (uuid);
});
Server.instance.io.emit ('user join');
What could I do?
.join() is a method on an individual socket. That's how you use it as socket.join(roomName). When the first user joins a room, the room is created automatically and other users can also join it. When the last user leaves the room, the room is removed automatically from the server. So, you join a user's socket to a room - you don't join something to the server.
Similarly, when you tried this:
Server.instance.io.on ('user-join', (socket: Socket) => {
console.log (plug);
socket.join (uuid);
});
That doesn't work because you don't listen for incoming messages from a socket on the server (except for the connection message - which introduces the socket object). You listen for incoming client messages on a socket itself:
Server.instance.io.on ('connection', (socket: Socket) => {
socket.on('user-join', () => {
// you will have to find the room name that goes with this socket
socket.join(someRoomName);
});
});
Also, note that this code:
private listenSockets (): void {
console.log ('Listening Sockets');
this.io.on ('connection', client => {
console.log ('Connected to room', client.rooms, '-', client.id);
// User disconnected
disconnect (client);
connect (client);
});
}
looks problematic. Why would you disconnect a client when they connect? You don't show those functions disconnect() and connect() so it's unclear what they actually do - I would guess they keep track of connected clients somehow. If you're just trying to clean up any state that might have been previously left hanging, then you should be doing something like this:
private listenSockets (): void {
console.log ('Listening Sockets');
this.io.on ('connection', client => {
console.log ('Connected to room', client.rooms, '-', client.id);
client.on('disconnect', () => {
// User disconnected
disconnect(client);
});
// user connected now
connect(client);
});
}
You don't have to worry about inaccurate housekeeping on whether a socket is connected or not. You will always get a disconnect event for a socket when it disconnects. This is for two reasons. For a browser window that closes or a page that the user navigates away from, the browser cleans up all objects associated with that page, including the open socket.io connection. This will always close the socket and cause a disconnect event. Second, socket.io uses ping and pong messages to regularly check if an existing connection is still working. If it's not, it will get disconnected. The client may or may not retry to open a new connection depending upon the circumstance. But, any disfunctional connection (one that isn't respond to ping messages) will get closed by the server and a disconnect event will occur for that too. So, those two circumstances make sure that a disconnect event always happens.
Server.instance.io.emit ('data changed', updatedData); The problem is that this information is sent indiscriminately to all users connected to the socket.
This sends to all users connected to your server and is how it was designed.
To send to a single socket, you would use:
socket.emit(...);
where socket is what you're code calls client, the object you get from the connection event.
To send to all sockets who have joined a room, you would use:
io.in(roomName).emit(...)
where io is the socket.io server instance.
And, there are many, many more variations of .emit() depending upon exactly what you're trying to send to.
Now, I have a unique ID that brings multiple users together in a MongoDB model. You could use that ID to broadcast only to users with that ID. There is a logic that implies that if the user connects from Mexico, add it to an Array of people in MongoDB, otherwise it will add it to another MongoDB document, then they are two different IDs. I would love the room to be that ID.
I don't completely follow what you're trying to do, but it seems like inside your connect(client) function, you could just call client.join(uniqueIDForMultipleUsers) and that would create a room with this uniqueID and add this client to that room. In the future, you can send to everyone in that room with io.in(uniqueIDForMultipleUsers).emit(...).
But at no point did I set up that "ROOM". When the user login, he could add it but I don't know how to create a custom room, he tried something like this
You don't create rooms manually. You just use socket.join(roomName) and the socket.io infrastructure automatically creates the room if it doesn't already exist. Similarly when the last socket in a room either leaves the room or disconnects, the room is automatically removed. So you just don't have to manage the room creation or deletion yourself. In fact, a room object is not something you ever deal with directly - it's a housekeeping item inside of the socket.io server that contains a list of sockets that are currently in the room. A socket can be in as many rooms as it wants to be. You use these on the server:
socket.join(roomName); // add a client's socket to a room
socket.leave(roomName); // remove a client's socket from a room
io.in(roomName).emit(...); // broadcast a message to every socket in a room
What is sometimes a bit confusing about the above logic is that socket.join() and socket.leave() are socket methods, but they actually modify a data structure in the server (where the list of rooms/sockets are kept). For whatever reason, that's just how they chose to originally design the API. Logically, it's more like io.join(socket, roomName) since it's modifying something on the server. But, since the socket knows the server object it's part of, they can leave that off and just do socket.join(roomName).

How can I send a message from bot framework sdk after a period of inactivity? In nodejs

I am using the nodejs SDK for Bot Framework to develop a chatbot. I want to send a message to the user if they do not write in 5 minutes.
I do not find an example in bot-framework documentation and, in stackoverflow there are not solutions for a started bot (I do not need it to start the conversation). Where do I need to create the code? I have an index.js and a dialog file. How can I set the timer and restart it when the user send a message?
I'm using directline.
Thanks
There are two different ways you can approach this, one for directline only using events and one for all channels using setTimeout. The directline solution requires some code on your webchat client, but the latter requires you to save the conversation reference and start a new bot adapter. Both approaches could work.
Directline Only
You need to set up your webchat client to set up the timer and send an event to your bot if no activities are sent before the timer expires. You need to create a custom store to do this. Here is an example I used in the past:
const store = window.WebChat.createStore({}, function(dispatch) { return function(next) { return function(action) {
if (action.type === 'WEB_CHAT/SEND_MESSAGE') {
// Message sent by the user
clearTimeout(interval);
} else if (action.type === 'DIRECT_LINE/INCOMING_ACTIVITY' && action.payload.activity.name !== "inactive") {
// Message sent by the bot
clearInterval(interval);
interval = setTimeout(function() {
// Notify bot the user has been inactive
dispatch.dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'inactive',
value: ''
}
});
}, 300000)
}
return next(action);
}}});
This will send an event to your bot with the name 'inactive'. Now you need to set up your bot to handle it. So in your this.onEvent handler you need to do something like this:
if (context.activity.name && context.activity.name === 'inactive') {
await context.sendActivity({
text: 'Are you still there? Is there anything else I can help you with?',
name: 'inactive'
});
}
All channels
As I'm typing this up, I'm realizing you should be able to emit the event from your bot itself and forego starting a new bot adapter instance. But I haven't tried that before, so I'm providing my existing solution. But you may wish to experiment with emitting an inactive event if the timeout is reached instead of the actions below.
That said, here is a solution you can use within your this.onMessage handler.
// Inactivity messages
// Reset the inactivity timer
clearTimeout(this.inactivityTimer);
this.inactivityTimer = setTimeout(async function(conversationReference) {
console.log('User is inactive');
try {
const adapter = new BotFrameworkAdapter({
appId: process.env.microsoftAppID,
appPassword: process.env.microsoftAppPassword
});
await adapter.continueConversation(conversationReference, async turnContext => {
await turnContext.sendActivity('Are you still there?');
});
} catch (error) {
//console.log('Bad Request. Please ensure your message contains the conversation reference and message text.');
console.log(error);
}
}, 300000, conversationData.conversationReference);
Note that you have to get and save the conversationReference if you go this route, so that you can call continueConversation if the timer expires. I typically do this in my this.onMessage handler as well just to make sure I always have a valid conversation reference. You can get it with the below code (I'm assuming you already have your conversation state and state accessor defined).
const conversationData = await this.dialogState.get(context, {});
conversationData.conversationReference = TurnContext.getConversationReference(context.activity);
Now as I mentioned in the first solution, I believe you should be able to send an inactivity event in your try block instead of initiating the bot adapter. If you try that and it works, please let me know so I can update this solution!

Tracking WS Clients in Nodejs

I am using ws through an express plugin, express-ws. My back end needs to track clients and, send message only to a specific or clients. Right now, I am just saving the client socket along with a token. So, when the client connects, a message is sent like the following by the client:
{"path":"/openUserSocket","token":"<CLIENT_TOKEN_VALUE>"}
This registers a socket in the back end. The socket is managed by a class wscManager which, saves the socket in a plain object. So, to register a socket, I do this:
// adding web socket support
const expressWs = require('express-ws')(app, null, {
wsOptions: {
clientTracking: true
}
});
let man = new wscManager();
// then in the web socket handler
app.ws('/', (sck,req) => {
sck.on('message',(msg)=>{
// handle messages
if(msg.path === '/openUserSocket'){
man.set(msg.token, sck);
return sck.send(JSON.stringify({ status: 200, message: 'Ok' }));
}
else if(msg.path === '/<SOME_OTHER_PATH>'){
// use that socket
}
// ... other route handling
}
}
Now, as I showed in the code above, I am trying to save the socket for later use. set just uses the token as a key for saving the socket object in another object. Later, get(token) can be used with that token to use the socket. I also extended the express app prototype to allow other route handles to use the wscManager:
app.response._man = man;
Now, my question is this the right approach to client tracking? In other route handlers, the manager is used like this:
// in route handler
res._man.send(JSON.stringify({ status: <STATUS>, message: <MSG> }));
Thanks for your time and patience.

NodeJS - Response stream

I built a simple API endpoint with NodeJS using Sails.js.
When someone access my API endpoint, the server starts to wait for data and whenever a new data appears, he broadcasts it using sockets. Each client should receive his own stream of data based on his user input.
var Cap = require('cap').Cap;
collect: function (req, res) {
var iface = req.param("ip");
var c = new Cap(),
device = Cap.findDevice(ip);
c.on('data', function(myData) {
sails.sockets.blast('message', {"host": myData});
});
});
The response do not complete (I never send a res.json() - what actually happens is that the browser keep loading - but the above functionality works).
2 Problems:
I'm trying to subscribe and unsubscribe to to this API endpoint from my client (using RxJS). When I subscribe, I start to receive data via sockets - but I can't unsubscribe to the API endpoint (the browser expect the request to be completed).
Each client should subscribe to his own socket room based on the request IP parameter ( see updated code ). Currently it blasts the message to everyone.
How I can create a stream/service-like API endpoint with Sails.js that will emit new data to each user based on his input?
My goal is to be able to subscribe / unsubscribe to this API endpoint from each client.
Revised Answer
Let's assume your API endpoint is defined in config/routes.js like this:
...
'get /collect': 'SomeController.collectSubscribe',
'delete /collect': 'SomeController.collectUnsubscribe',
Since each Cap instance is tied to one device, we need one instance for each subscription. Instead of using the sails join/leave methods, we keep track of Cap instances in memory and just broadcast to the request socket's id. This works because Sails sockets are subscribed to their own ids by default.
In api/controllers/SomeController.js:
// In order for the `Cap` instances to persist after `collectSubscribe` finishes, we store them all in an Object, associated with which socket the were created for.
var caps = {/* req.socket.id: <instance of Cap>, */};
module.exports = {
...
collectSubscribe: function(req, res) {
if (!res.isSocket) return res.badRequest("I need a websocket! Help!");
if (!!caps[req.socket.id]) return res.badRequest("Dude, you are already subscribed.");
caps[req.socket.id] = new Cap();
var c = caps[req.socket.id]; // remember that `c` is a reference to our new `Cap`, not a copy.
var device = c.findDevice(req.param('ip'));
c.open(device, ...);
c.on('data', function(myData) {
sails.sockets.broadcast(req.socket.id, 'message', {host: myData});
});
return res.ok();
},
collectUnsubscribe: function(req, res) {
if (!res.isSocket) return res.badRequest("I need a websocket! Help!");
if (!caps[req.socket.id]) return res.badRequest("I can't unsubscribe you unless you actually subscribe first.");
caps[req.socket.id].removeAllListeners('data');
delete caps[req.socket.id];
return res.ok();
}
}
Basically, it goes like this: when a browser request triggers collectSubscribe, a new Cap instance listens to the provided IP. When the browser triggers collectUnsubscribe, the server retreives that Cap instance, tells it to stop listening, and then deletes it.
Production Considerations: please be aware that the list of Caps is NOT PERSISTENT (since it is stored in memory and not a DB)! So if your server is turned off and rebooted (due to lightning storm, etc), the list will be cleared, but considering that all websocket connections will be dropped anyway, I don't see any need to worry about this.
Old Answer, Kept for Reference
You can use sails.sockets.join(req, room) and sails.sockets.leave(req, room) to manage socket rooms. Essentially you have a room called "collect", and only sockets joined in that room will receive a sails.sockets.broadcast(room, eventName, data).
More info on how to user sails.sockets here.
In api/controllers/SomeController.js:
collectSubscribe: function(req, res) {
if (!res.isSocket) return res.badRequest();
sails.sockets.join(req, 'collect');
return res.ok();
},
collectUnsubscribe: function(req, res) {
if (!res.isSocket) return res.badRequest();
sails.sockets.leave(req, 'collect');
return res.ok();
}
Finally, we need to tell the server to broadcast messages to our 'collect' room.
Note that this only need to happen once, so you can do this in a file under the config/ directory.
For this example, I'll put this in config/sockets.js
module.exports = {
// ...
};
c.on('data', function(myData) {
var eventName = 'message';
var data = {host: myData};
sails.sockets.broadcast('collect', eventName, data);
});
I am assuming that c is accessible here; If not, you could define it as sails.c = ... to make it globally accessible.

How to organise multiple Redis clients

I'm using the redis-sentinel-client library to manage a connection to a Redis sentinel group. The issue I have is that upon connecting I need to process records which may or may not already be present in the Redis store.
As I have two clients (due to the fact that one is a subscriber) I am not sure the best way to organise my event listeners so that I guarantee that both clients are ready prior to attempting any operations.
At the moment I have the following:
var sentinelSubscriberClient = RedisSentinel.createClient(opts);
var sentinelPublisherClient = RedisSentinel.createClient(opts);
sentinelSubscriberClient.on('ready', function redisSubscriberClientReady() {
sentinelPublisherClient.removeAllListeners('ready');
sentinelPublisherClient.on('ready', function () {
supportedChannels.forEach(function (channel) {
sentinelSubscriberClient.subscribe(channel);
});
// Includes reading + publishing via `sentinelPublisherClient`
processUnprocessed();
});
});
(there are also error listeners but I've removed them to make the code easier to read)
This current approach falls over if the publisher client emits ready before the subscriber client. My question is how can I organise the event listeners so that I can safely call .subscribe() on the subscriber client and various methods (.lrange(), .publish() etc.) of the publisher listener?
Thanks!
Simply move client creation into the ready callback function.
var sentinelSubscriberClient = RedisSentinel.createClient(opts);
var sentinelPublisherClient = null;
sentinelSubscriberClient.on('ready', function redisSubscriberClientReady() {
sentinelPublisherClient = RedisSentinel.createClient(opts);
sentinelPublisherClient.on('ready', function () {
supportedChannels.forEach(function (channel) {
sentinelSubscriberClient.subscribe(channel);
});
// Includes reading + publishing via `sentinelPublisherClient`
processUnprocessed();
});
});

Resources