Rethinkdb pattern for dynamic pubsubs through socketio - meteor

Coming from the Meteor world & I'm curious how to replicate the cached pubsub/observers functionality. For a basic example, let's say I have a todolist where each todo has a userId and I want to keep todos private to each userId (but a userId could exist on multiple connected devices, eg phone + desktop). I imagine I have to create some publish function that verifies the userId by the socketId from the sent request, then create a socket namespace specific to that query (since the query could include more than a userId constraint). Then, register an emitter that only sends the changes to those socketIds that are verified to listen to the given namespace. Am I close? All my research just returns basic things like publishing to all connected users based on keywords. Any links to reading material would be great! Here's a first attempt with the missing logic in comments...
export function sendTodosByUserId(io, userId) {
//How to auth? By linking a client socketId to a user in a lookup table?
connect()
.then(conn => {
r
.table('todos')
.filter(todos => todos("userId").eq(userId))
.changes().run(conn, (err, cursor) => {
cursor.each((err, change) => {
//Do I emit a unique message? namespace? How do I handle 2 clients using the same userId?
io.emit('TODO_CHANGE', change);
});
});
});
}

You could implement some way of mapping new sockets to the correct user. For example, you could put the userId in a Express session and store the user's socket ids in a simple object.
var userSockets = {};
io.sockets.on('connection', function(socket) {
var userId = socket.handshake.session.userId;
if(userSockets[userId]) {
userSockets[userId].push(socket.id);
} else {
userSockets[userId] = [socket.id];
sendTodosByUserId(io, userId)
}
socket.on('disconnect', function() {
var i = userSockets[userId].indexOf(socket.id);
userSockets[userId].splice(i);
if(userSockets[userId].length === 0) {
delete userSockets[userId];
}
});
});
In your sendTodosByUsedId, you would just loop over the socket array belonging to the user, and emit to every socket.
var sockets = userSockets[userId];
for(var i = 0; i < sockets.length; ++i) {
io.to(sockets[i]).emit('TODO_CHANGE', change);
}
Note that this will not work if you have multiple nodes the user can be connected to. Then you might have to store the userSockets-object in e.g. Redis.
Alternatively, you could just have your users join a room named e.g. user:<userId>, and emit to this on every todo-change.
io.sockets.on('connection', function(socket) {
var userId = socket.handshake.session.userId;
socket.join('user:' + userId);
socket.on('disconnect', function() {
console.log("Rooms are left automatically on disconnect");
}
});
On todo change:
io.to('user:' + userId).emit('TODO_CHANGE', change);

Related

Read data from firebase and display in chat using Node Js

I am new to the Firebase realtime database and Google Dialogflow. I have gone through the documents and working on through. I am reading the data from database and I want to display it in my chat. I am able to see the data in the logs but unable to display in the chat conversation. If I check the logs I am able to see the success or failed result but unable to view in the chat conversation.
This is the code below:
var childData = "";
var message = '';
var query = '';
var key = '';
function wheretogo(agent) {
//taking country name as input from user
var country = request.body.queryResult.parameters.country;
//reference country from the database
query = admin.database().ref("country").orderByKey();
query.once("value")
.then(function(snapshot) {
snapshot.forEach(function(childSnapshot) {
key = childSnapshot.key;
childData = childSnapshot.val();
//matching the input from user and the country name(key) from database
if (country === key) {
console.log("sucess");
message = 'Thats nice ! You are travelling to ' + key;
agent.add(message);
}
else
{
console.log("failed");
}
});
});
}
I expect the output 'Thats nice ! You are travelling to ' with country name in my chat conversation.
The problem is that the Dialogflow library expects you to return a Promise if you're doing any asynchronous calls. Since you're doing an async call (the query.once() call), you must return a Promise. Otherwise the handler dispatcher won't wait for the reply to come from the database before it tries to send a reply to the user.
You don't show all of your code, but in your case it looks fairly straightforward. Since query.once() returns a Promise, you can return this Promise. Something like this change
return query.once("value")
might be all that is necessary.

SignalR needs to target specific games with Game ID and not all live games

I didnt think about this but this code is sending the game model to all clients. I need to use the GameID from this controller action and only target the clients watching that game. How do I do that?
Publish Controller Action
public UpdateGameResponse UpdateGame(int gameId)
{
...
var model = Game.Create(XDocument.Load(httpRequest.Files[0].InputStream)).Parse();
GlobalHost.ConnectionManager.GetHubContext<GameCastHub>().Clients.All.receiveUpdates(Newtonsoft.Json.JsonConvert.SerializeObject(model));
}
Hub
[HubName("gamecastHub")]
public class GameCastHub : Hub
{
}
Client
var connected = false;
var gamecastHub = $.connection.gamecastHub;
if (gamecastHub) {
gamecastHub.client.receiveUpdates = function (updates) {
console.log('New updates received');
processUpdates(updates);
};
connectLiveUpdates();
$.connection.hub.connectionSlow(function () {
console.log('Live updates connection running slow');
});
$.connection.hub.disconnected(function () {
connected = false;
console.log('Live updates disconnected');
setTimeout(connectLiveUpdates, 10000);
});
$.connection.hub.reconnecting(function () {
console.log('Live updates reconnecting...');
});
$.connection.hub.reconnected(function () {
connected = false;
console.log('Live updates reconnected');
});
}
I suggest using either the connection Id associated with each connection to the hub or creating groups.
Note: Each GameID must have its own connection to the hub in order to use the connection Id solution.
I prefer to use groups from personal experience but either way can be done.
To create a group in the hub you will need to create a method in your hub class.
public async void setGroup(string groupName){
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
}
Secondly, you will need a JS function on the client side to call the hub function.
$.connection.hub.invoke("setGroup", groupName).catch(err => console.error(err.toString()));
In your case, you can place your gameID as the groupname and then call GlobalHost.ConnectionManager.GetHubContext<GameCastHub>().Clients.Groups(gameID).receiveUpdates(Newtonsoft.Json.JsonConvert.SerializeObject(model));
To retrieve the connection Id:
var _connectionId = $.connection.hub.id;
Then send the connection Id to the server,
and proceed to using the call GlobalHost.ConnectionManager.GetHubContext<GameCastHub>().Clients.Clients.Client(_connectionId).receiveUpdates(Newtonsoft.Json.JsonConvert.SerializeObject(model)); to call that specific connection.

Changing application server key in push manager subscription

I implemented web push notifications using service worker. I collected user subscriptions with a particular application server key. Suppose if we change the application server key, then when we get the subscription using "reg.pushManager.getSubscription()", we will get the old subscription information which was created using the old application server key. How to handle this scenario? How to get the new subscription from the user?
Get the subscription using reg.pushManager.getSubscription() and check whether current subscription uses the new application server key. If not, then call unsubscribe() function on the existing subscription and resubscribe again.
After properly starting the service worker and getting the permissions, call navigator.serviceWorker.ready in order to get access to the *.pushManager object.
From this object we call another promise to get the pushSubscription object we actually care about.
If the user was never subscribed pushSubscription will be null otherwise we get the key from it and check if it's different, if that's the case we unsubscribe the user and subscribe them again.
var NEW_PUBLIC_KEY = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
Notification.requestPermission(function (result) {
if (permissionResult == 'granted'){
subscribeUser();
}
});
function subscribeUser() {
navigator.serviceWorker.ready
.then(registration => {
registration.pushManager.getSubscription()
.then(pushSubscription => {
if(!pushSubscription){
//the user was never subscribed
subscribe(registration);
}
else{
//check if user was subscribed with a different key
let json = pushSubscription.toJSON();
let public_key = json.keys.p256dh;
console.log(public_key);
if(public_key != NEW_PUBLIC_KEY){
pushSubscription.unsubscribe().then(successful => {
// You've successfully unsubscribed
subscribe(registration);
}).catch(e => {
// Unsubscription failed
})
}
}
});
})
}
function subscribe(registration){
registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(NEW_PUBLIC_KEY)
})
.then(pushSubscription => {
//successfully subscribed to push
//save it to your DB etc....
});
}
function urlBase64ToUint8Array(base64String) {
var padding = '='.repeat((4 - base64String.length % 4) % 4);
var base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
var rawData = window.atob(base64);
var outputArray = new Uint8Array(rawData.length);
for (var i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
In my case, I managed to solve it by clearing the cache and cookies
The key you get from calling sub.getKey('p256dh') (or sub.toJSON.keys.p256dh) is the client's public key, it will always be different from the server public key. You need to compare the new public server key and sub.options.applicationServerKey.
sub above is the resolved promise from reg.pushManager.getSubscription().
Therefore:
Get PushSubscription interface - reg.pushManager.getSubscription().then(sub => {...}), if sub is null no subscription exists, therefore no worry, but if it's defined:
Inside the block get the current key in use sub.options.applicationServerKey
Convert it to string, because you can't compare ArrayBuffer directly - const curKey = btoa(String.fromCharCode.apply(null, new Uint8Array(sub.options.applicationServerKey)))
Compare it with your new key. If the keys are different call sub.unsubscribe() and then subscribe again by calling reg.pushManager.subscribe(subscribeOptions), where subscribeOptions uses your new key. You call 'unsubscribe' on PushSubscription, but subscribe on PushManager

On Meteor, how can I validate with Collection2 on the client side?

I always use methods to insert, update and remove. This is the way my code look just now:
Client side
Template.createClient.events({
'submit form': function(event, tmpl) {
e.preventDefault();
var client = {
name: event.target.name.value,
// .... more fields
}
var validatedData = Clients.validate(client);
if (validatedData.errors) {
// Display validation errors
return;
}
Meteor.call('createClient', validatedData.client, function(error) {
if (error)
// Display error
});
}
});
Client and server side:
Clients = new Mongo.Collection("clients");
Clients.validate = function(client) {
// ---- Clean data ----
client.name = _.str.trim(client.name);
// .... more fields clean
// ---- Validate data ---
var errors = [];
if (!client.name)
errors.push("The name is required.");
// .... more fields validation
// Return and object with errors and cleaned data
return { errors: _.isEmpty(errors) ? undefined : errors, client: client };
}
Meteor.methods({
'createClient': function (client) {
// --- Validate user permisions ---
// If server, validate data again
if (Meteor.isServer) {
var validatedData = Clients.validate(client);
if (validatedData.errors)
// There is no need to send a detailed error, because data was validated on client before
throw new Meteor.Error(500, "Invalid client.");
client = validatedData.client;
}
check(client, {
name: String,
// .... more fields
});
return Clients.insert(client);
}
});
Meteor.call is executed on client and server side, but Meteor doesn't have a way stop the running on the server side if the validation on the client side fails (or at least, I don't know how). With this pattern, I avoid sending data to the server with Meteor.call if validation fail.
I want to start using Collection2, but I can't figure how to get the same pattern. All the examples I found involve the usage of direct Insert and Update on client side and Allow/Deny to manage security, but I want to stick with Meteor.call.
I found on documentation that I can validate before insert or update, but I don't know how to get this to work:
Books.simpleSchema().namedContext().validate({title: "Ulysses", author: "James Joyce"}, {modifier: false});
I know the autoform package, but I want to avoid that package for now.
How can I validate with Collection2 on the client side before sending data to the server side with Meteor.call? Is my pattern wrong or incompatible with Collection2 and I need to do it in another way?
In under 30 lines you can write your very own, full-featured validation package for Collection2. Let's walk through an example:
"use strict"; //keep it clean
var simplyValid = window.simplyValid = {}; //OK, not that clean (global object)
simplyValid.RD = new ReactiveDict(); //store error messages here
/**
*
* #param data is an object with the collection name, index (if storing an array), and field name, as stored in the schema (e.g. 'foo.$.bar')
* #param value is the user-inputted value
* #returns {boolean} true if it's valid
*/
simplyValid.validateField = function (data, value) {
var schema = R.C[data.collection]._c2._simpleSchema; //access the schema from the local collection, 'R.C' is where I store all my collections
var field = data.field;
var fieldVal = field.replace('$', data.idx); //make a seperate key for each array val
var objToValidate = {};
var dbValue = schema._schema[field].dbValue; //custom conversion (standard to metric, dollars to cents, etc.) IGNORE
if (dbValue && value) value = dbValue.call({value: value}); //IGNORE
objToValidate[field] = value; //create a doc to clean
schema.clean(objToValidate, {removeEmptyStrings: false}); //clean the data (trim, etc.)
var isValid = schema.namedContext().validateOne(objToValidate, field, {extendedCustomContext: true}); //FINALLY, we validate
if (isValid) {
simplyValid.RD.set(fieldVal, undefined); //The RD stores error messages, if it's valid, it won't have one
return true;
}
var errorType = schema.namedContext()._getInvalidKeyObject(field).type; //get the error type
var errorMessage = schema.messageForError(errorType, field); //get the message for the given error type
simplyValid.RD.set(fieldVal, errorMessage); //set the error message. it's important to validate on error message because changing an input could get rid of an error message & produce another one
return false;
};
simplyValid.isFieldValid = function (field) {
return simplyValid.RD.equals(field, undefined); //a very cheap function to get the valid state
};
Feel free to hack out the pieces you need and shoot me any questions you might have.
You can send the schema to the client and validate before sending to the server. If you want to use Collection" you need to attach the schema to the collection and use the insert which is something that you don't want. So the best option, for your scenario, is sending the schema to the client and use it to validate.
Also reconsider using mini-mongo instead of using Methods for everything, it will save you lots of time and don't think your app is secure jut because you're using Methods.

Detect if Firebase connection is lost/regained

Is there a strategy that would work within the current Firebase offering to detect if the server connection is lost and/or regained?
I'm considering some offline contingencies for mobile devices and I would like a reliable means to determine when the Firebase data layer is available.
This is a commonly requested feature, and we just released an API update to let you do this!
var firebaseRef = new Firebase('http://INSTANCE.firebaseio.com');
firebaseRef.child('.info/connected').on('value', function(connectedSnap) {
if (connectedSnap.val() === true) {
/* we're connected! */
} else {
/* we're disconnected! */
}
});
Full docs are available at https://firebase.google.com/docs/database/web/offline-capabilities.
Updated:
For many presence-related features, it is useful for a client to know when it is online or offline. Firebase Realtime Database clients provide a special location at /.info/connected which is updated every time the client's connection state changes. Here is an example:
DatabaseReference connectedRef = FirebaseDatabase.getInstance().getReference(".info/connected");
connectedRef.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot snapshot) {
boolean connected = snapshot.getValue(Boolean.class);
if (connected) {
System.out.println("connected");
} else {
System.out.println("not connected");
}
}
#Override
public void onCancelled(DatabaseError error) {
System.err.println("Listener was cancelled");
}
});
I guess this changed in the last couple of months. Currently the instructions are here:
https://firebase.google.com/docs/database/web/offline-capabilities
In summation:
var presenceRef = firebase.database().ref("disconnectmessage");
// Write a string when this client loses connection
presenceRef.onDisconnect().set("I disconnected!");
and:
var connectedRef = firebase.database().ref(".info/connected");
connectedRef.on("value", function(snap) {
if (snap.val() === true) {
alert("connected");
} else {
alert("not connected");
}
});
I'll admit I don't know a lot about how references are set, or what that means (are you making them out of thin air or do you have to have already created them beforehand?) or which one of those would trigger something on the server as opposed to something on the front end, but if the link is still current when you read this, a little more reading might help.
For android you can make user offline by just a single function called onDisconnect()
I did this in one of my chat app where user needs to get offline automatically if network connection lost or user disconnected from Internet
DatabaseReference presenceRef = FirebaseDatabase.getInstance().getReference("USERS/24/online_status");
presenceRef.onDisconnect().setValue(0);
On disconnecting from network Here I am making online_status 0 of user whose Id is 24 in firebase.
getReference("USERS/24/online_status") is the path to the value you need to update on offline/online.
You can read about it in offline capabilities
Note that firebase takes time around 2-10 minutes to execute onDisconnect() function.
firebase for web
firebase.database().ref(".info/connected").on("value",(snap)=> {});
The suggested solution didn't work for me, so I decided to check the connection by writing and reading 'health/check' value. This is the code:
const config = {databaseURL: `https://${projectName.trim()}.firebaseio.com/`};
//if app was already initialised delete it
if (firebase.apps.length) {
await firebase.app().delete();
}
// initialise app
let cloud = firebase.initializeApp(config).database();
// checking connection with the app/database
let connectionRef = cloud.ref('health');
connectionRef.set('check')
.then(() => {
return connectionRef.once("value");
})
.then(async (snap) => {
if (snap.val() === 'check') {
// clear the check input
await connectionRef.remove();
// do smth here becasue it works
}
});

Resources