I have node.js and socket.io performing real-time updates on a DataTables table. When a row is edited, other clients see the updates to the row without refreshing.
This all works well, but I'm at a loss regarding how to automatically refresh the table when node/socket reconnect to the server after a disconnect.
Currently, this is what happens:
Go to page containing table
Let device sleep/disconnect from
server
Power on device and see page containing table
socket.io reconnects to the server, but the table doesn't refresh to get the latest changes.
How can I get socket.io to refresh the table upon reconnect to the server? Ideally, the process would look like this:
Power on device
socket.io reconnects to server, triggers CSS "Loading..." overlay to prevent user from making changes to table
socket.io refreshes the table to show latest content
CSS "Loading..." overlay closes
Server-side script:
console.log('Starting the server');
var app = require('express')();
var express = require('express');
var fs = require("fs");
var server = require('https').createServer(SSL, app);
var io = require('socket.io')(server);
server.listen(100);
io.on('connection', function (socket) {
/*socket.emit('news', { hello: 'world' });
/socket.on('my other event', function (data) {
console.log(data);
});*/
});
io.set('authorization', function(handshakeData, accept) {
console.log('io.authorization called');
accept(null, true);
} );
var lockedRowIDs = [];
io.sockets.on('connection', function(socket) {
console.log('io.connection called');
socket.on('lock', function(rowID) {
console.log('Lock event for rowID: '+rowID);
lock(socket, rowID);
lockedRowIDs.push(rowID);
} );
socket.on('clientJoin', function(username) {
console.log('clientJoin event received');
socket.join('clients');
if (typeof lockedRowIDs !== 'undefined' && lockedRowIDs.length > 0) {
socket.emit('lockedRows', lockedRowIDs);
}
} );
socket.on('unlock', function(rowID) {
console.log('Unlock event for rowID: '+rowID);
unlock(socket, rowID);
removeItemFromArray(lockedRowIDs, rowID);
} );
socket.on('updateData', function(json, action, id) {
if (action == "edit" || action == "create") {
console.log('updateData event for rowID: '+json.row['DT_RowId']);
}
updateData(socket, json, action, id);
} );
} );
function lock(socket, rowID) {
socket.broadcast.to('clients').emit('lock', rowID);
setTimeout(function() {
io.sockets.in('clients').emit('timeout', rowID);
removeItemFromArray(lockedRowIDs, rowID);
},
180000);
}
function unlock(socket, rowID) {
socket.broadcast.to('clients').emit('unlock', rowID);
}
function removeItemFromArray(array, item) {
console.log('removeItemFromArray called with item: '+item);
for(var i = array.length - 1; i >= 0; i--) {
if(array[i] === item) {
array.splice(i, 1);
}
}
}
function updateData(socket, json, action, id) {
if (action == "edit" || action == "create") {
console.log('updateData called with rowID:'+json.row['DT_RowId']);
}
socket.broadcast.to('clients').emit('updateData', json, action, id);
}
Client-side script:
var socket = io('https://www.***.com:100');
socket.on('connect', function() {
console.log('Connected');
socket.emit('clientJoin');
} );
socket.on('lock', function(rowID) {
console.log('Lock event received for rowID: '+rowID);
row = $("tr[id='"+rowID+"']");
row.addClass('locked');
/* Pagenation fix Start */
var table = $('#example').DataTable();
table.row('#'+rowID).nodes().to$().addClass('locked');
/* Pagenation fix End */
} );
socket.on('unlock', function(rowID) {
console.log('Unlock event received for rowID: '+rowID);
row = $("tr[id='"+rowID+"']");
row.removeClass('locked');
/* Pagenation fix Start */
var table = $('#example').DataTable();
table.row('#'+rowID).nodes().to$().removeClass('locked');
/* Pagenation fix End */
} );
socket.on('timeout', function(rowID) {
console.log('Time out event received for rowID: '+rowID);
row = $("tr[id='"+rowID+"']");
row.removeClass('locked');
/* Pagenation fix Start */
var table = $('#example').DataTable();
table.row('#'+rowID).nodes().to$().removeClass('locked');
/* Pagenation fix End */
/* Check if the editor corresponds to the timed out rowID - start */
var modifier = editor.modifier();
if (modifier) {
var data = table.row(modifier).data();
console.log('rowID is '+data.DT_RowId);
if (data.DT_RowId == rowID) {
console.log('Timed out rowID: '+rowID+' matches Editor rowID: '+data.DT_RowId+'. Closing Editor now.');
editor.close();
}
else {
console.log('Timed out rowID: '+rowID+' does not match Editor rowID: '+data.DT_RowId+'. Keeping Editor open.');
}
}
/* Check if the editor corresponds to the timed out rowID - end */
} );
socket.on('lockedRows', function (rowIDs) {
console.log('Iterate through the list of rows and mark it as locked');
table = $('#example').DataTable();
rowCount = rowIDs.length;
console.log('Row count: '+rowCount);
for (var i=0; i<rowCount; i++) {
console.log(rowIDs[i]);
row = $("tr[id='"+rowIDs[i]+"']");
row.addClass('locked');
table.row('#'+rowIDs[i]).nodes().to$().addClass('locked');
}
} );
socket.on('updateData', function(json, action, id) {
if (action == "create" || action == "edit") {
var DT_RowId = json.row['DT_RowId'];
console.log('updateData socket event for rowID: '+DT_RowId+' and action: '+action);
}
var table = $('table#example').DataTable();
if (action == "edit") {
var editedRow = table.row('#'+DT_RowId).nodes().to$();
table.row(editedRow).data(json.row).draw();
console.log('Row updated');
}
if (action == "create") {
console.log('Row created');
table.row.add(json.row).draw();
}
if (action == "remove") {
var removedRow = table.row('#'+id).nodes().to$();
table.row(removedRow).remove().draw();
console.log('Row removed with id '+id);
}
} );
/* Ajax request has been completed, data retrieved from the server */
editor.on('postSubmit', function(e,json, data, action) {
console.log('Post submit');
console.log(data);
console.log('With JSON:');
console.log(json);
console.log('With action:');
console.log(action);
if (action == "create" || action== "edit") {
if (json.row){
console.log('rowID from JSON: '+json.row['DT_RowId']);
socket.emit('updateData', json, action);
}
}
if (action == "remove") {
console.log('rowID from JSON: '+data.id[0]);
socket.emit('updateData', null, action, data.id[0]);
}
} );
editor.on('close', function(e) {
console.log('Close event');
console.log(e);
var modifier = editor.modifier();
console.log(modifier)
if (modifier !== null) {
console.log('Inside modifier')
table = $('#example').DataTable();
if (table.row(modifier).node()) {
rowID = table.row(modifier).node().id;
console.log('rowID='+rowID);
row = $("tr[id='"+rowID+"']");
row.removeClass('locked');
table.row('#'+rowID).nodes().to$().removeClass('locked');
socket.emit('unlock', rowID);
}
}
} );
As I think you know, when you're connected, you're keeping the data up-to-date, but if you have a momentary disconnect and then reconnect, you might have missed some data updates.
There are a number of possible strategies for dealing with this.
Brute-force. Upon a reconnect, get a fresh copy of all the data as if the device had just been turned on. Less efficient, but easy to implement.
Transaction ID or Transaction Time. Each time the server sends an update, it sends either a transaction ID or a transaction server time with that update. The client then keeps track of the last transaction ID or transaction time that it received. When it does a reconnect, it sends an initial message with the last transaction ID or transaction time asking for any updates that have happened since that last transaction. The server can then look through its database to see if there are any newer transactions and, if so, send them to that client. This requires keep track of transactions at your database. It is common that the server can possibly return a "transaction id not supported" type of response which then forces the client back to a brute-force update from scratch. This is a back-stop in case a database rebuild or server crash causes older transaction values to get lost.
True Synchronization. The client reconnects and then does a true synchronization with the server where the client essentially says "this is what I have, do you have anything newer". In the interest of efficiency, many synchronization systems solve this problem by implementing the transaction id described in option 2, but there are certainly many other ways to do synchronization. True synchronization is more useful if the client might also be changing the data while it was disconnected.
As a simplifying way of implementing option 2, if your database does not already support the notion of journaled transactions, then some servers will implement a transaction log in memory where it keeps track of the last N hours of transactions. As long as the server stays up and a disconnect doesn't last longer than N hours, then a request for new data can be satisfied from the in memory transaction log (which is efficient). If the server restarts or the client has been gone longer than N hours, then the client is just forced to do a brute-force update, the same as the client would have done if it was powered off and then powered back on (losing all prior knowledge of the data).
Of course, if there are multiple users of the database, then the transaction log has to be implemented by the database itself in order to make sure it includes all possible transactions.
Related
Please help me figure out why my socket.io app fires too many emit messages.
Yes, I have spent weeks on the socket.io docs and relevant stackO questions, experimented with the nesting of my functions and everything but I am just totally stuck.
This is supposed to allow people in a 'room' to vote on an issue. Each client gets one vote (yea, nay, or abstain). The problem is when someone votes, it cannot just add one vote it adds a vote for the number of people in the socket. For example, if three people are in a room and the first vote is cast for 'yea' it registers 3x 'yea' votes. I cant get a good count ever!
client site:
var socket = io.connect('http://localhost');
const url = window.location.href;
let voteName = url.substring(url.lastIndexOf("/") + 1);
let notVoted = true;
$('.voteName').html(voteName.replace(/_/g, ' '));
function getRoom(){
return voteName;
}
socket.on('connect', () => {
// Connected, let's sign-up for to receive messages for this room
socket.emit('room', getRoom());
console.dir(socket.id);
});
function yea () {
if(notVoted){
socket.emit('yea',voteName);
console.log(voteName)
notVoted = false;
}
}
function nay () {
if(notVoted){
io.emit('nay',voteName);
notVoted = false;
}
}
function abs () {
if(notVoted){
io.emit('abs',voteName);
notVoted = false;
}
}
Server Side:
app.post('/votePick', (req, res) => {
let voteName = req.body.votePick;
res.send(voteName);
app.get('/'+voteName,(req,res)=>{
res.sendFile(__dirname + '/canvas.html');
});
var nsp = io.of('/');
io.sockets.on('connection', (socket) => {
socket.on('room', (voteName) => {
//TODO check if room exists
socket.join(voteName);
if(!db[voteName]) {
db[voteName]={'yea':0,'nay':0,'abs':0,'cnctCount':1};
io.emit('update',db[voteName]);
} else {
io.emit('update',db[voteName]);
}
});
socket.on('yea', (voteName)=>{
db[voteName].yea++;
io.emit('update',db[voteName]);
console.log(db[voteName].cnctCount);
});
socket.on('nay', (voteName)=>{
db[voteName].nay++;
io.emit('update',db[voteName]);
console.log(db[voteName]);
});
socket.on('abs', (voteName)=>{
db[voteName].abs++;
io.emit('update',db[voteName]);
console.log(db[voteName]);
});
});
});
my full repo is here:
https://github.com/Bokeefe/sockerPractice
You should do user validation. No one can prevent code on the clientside to be changed, so you always need to check on the serverside. For example you could store the socket ids that have already voted:
const voted = new Set;
then inside the socket event handlers:
if(voted.has(socket)) return;
voted.add(socket);
//vote
this should also solve the problem od votes counted twice.
I don't understand why updating a label on one page is affecting the label on another page. I did not think the DOM was shared like that. Opening one tab or page successfully updates the label to 'player1', but when I open another tab/pg, it updates both labels to 'player2'.
<script>
var socket = io.connect('http://localhost:3000');
socket.on('connect', function() {
socket.emit('join');
socket.on('joinSuccess', function (playerSlot) {
if (playerSlot === 'player1') {
$("#playerID").text("you are player1");
} else if (playerSlot === 'player2') {
$("#playerID").text("you are player2");
}
}); //end joinSuccess
}); //end connect
I am merely trying to notify the user which player they are.
solution:
else if (playerSlot === 'player2') {
var elm = $("#playerID");
var empty = !elm.text().trim();
if (empty) {
elm.text("you are " + playerSlot);
}
}
Are you pushing the 'joinSuccess' message when new user joins? In such case this message will be passed to both the pages with same playerSlot value. So, all pages will be updated last joined player name.
In such case you can handle this with simple condition,
socket.on('joinSuccess', function (playerSlot) {
var elm = $("#playerID");
if (!elm.text().trim()) {
elm.text("you are " + playerSlot);
}
});
I have a simple app, which displays a list of available signalR hubs. A user selects a hub and it connects to it, this subscribes an event to add messages to a table on the page. The user can then send messaged to that hub which will also fire the subscription adding that message to the table. This all works great.
Now if the user selects another hub, the app connects and sets up a new subscription, however the original subscription still fires causing duplicate messages to be added to the table. Each time the hub is changed further subscriptions get added causing one send to result in many messages in the table.
I have tried disconnecting the hub, disposing the hub and trying to remove the subscription with hubProxy.off(eventName), but nothing seems to work, other than a page reload.
Here is the code I have just added the onHub changed function as this is where everything is happening.
Any ideas appreciated. :)
function HubViewModel() {
var self = this;
self.hubConnection = '';
self.hub = '';
$.getScript("../signalR/hubs");
self.hubs = ko.observableArray();
self.selectedHub = ko.observable();
self.messageText = ko.observable();
self.messageCollection = ko.observableArray();
self.hubChanged = function () {
// Setup hub connection.
$.connection.hub.url = "../signalR";
self.hubConnection = $.hubConnection();
// Get the selected hub name.
var selectedHubName;
_.each(self.hubs(), function(item) {
if (item.hubId == self.selectedHub()) {
selectedHubName = item.hubName;
}
});
// Check for a selected connection
if (self.selectedHub()) {
// Create proxy.
self.hub = self.hubConnection.createHubProxy(selectedHubName);
// Remove any existing listener(s).
self.hub.off('addNewMessageToPage');
// Setup listener.
self.hub.On('addNewMessageToPage', function (sender, message) {
self.messageCollection().push({ hubName: selectedHubName, name: selectedHubName, message: message, dateTime: new Date().toLocaleString() });
$('#hubMessageGrid').dxDataGrid('instance').refresh();
});
// start connection.
self.hubConnection.start()
.done(function() {
toastr.success('hub connected');
$('#sendMessageButton').click(function() {
self.hub.invoke('sendAll', 'hub management page', self.messageText());
self.messageText('');
});
})
.fail(function(error) {
toastr.error('hub connection ' + error);
});
}
};
You can to disconnect the hub first by calling the self.hub.stop(); function
You need to pass the exact same handler instance when unsubscribing. Passing a different instance (even if the function body is the same) will not remove the handler.
See https://learn.microsoft.com/en-us/javascript/api/#microsoft/signalr/hubconnection?view=signalr-js-latest#off-string---args--any-------void-
Here is my code
function registerPushNotifications() {
Titanium.Network.registerForPushNotifications({
types : [Titanium.Network.NOTIFICATION_TYPE_BADGE, Titanium.Network.NOTIFICATION_TYPE_ALERT],
success : function(e) {
var deviceToken = e.deviceToken;
Ti.API.info("Push notification device token is: " + deviceToken);
Ti.API.info("Push notification types: " + Titanium.Network.remoteNotificationTypes);
Ti.API.info("Push notification enabled: " + Titanium.Network.remoteNotificationsEnabled);
Ti.API.error("device Token is: " + e.deviceToken);
//return device Token to store in Model.
return e.deviceToken;
},
error : function(e) {
Ti.API.info("Error during registration: " + e.error);
},
callback : function(e) {
// called when a push notification is received.
//var data = JSON.parse(e.data);
var data = e.data;
var badge = data.badge;
if (badge > 0) {
Titanium.UI.iPhone.appBadge = badge;
}
var message = data.message;
if (message != '') {
var my_alert = Ti.UI.createAlertDialog({
title : '',
message : message
});
my_alert.show();
}
Ti.App.addEventListener('resume', function() {
alert('do another event if coming from background');
}
});
};
Depending on whether the push notification comes from the background or foreground I want to run different events.
I have tried
Ti.App.addEventListener('resume', function() {
but this fires EVERYTIME I return to the app from the background (the event handlers will be stacked every time a push notification to sent and fires all of them). As opposed to only executing that part of the callback code for push notifications that have come in from the background.
How do I know if the push notification came from the background or whilst app is running? cheers
Update:
Solved it.
If you check the call back object, you will find IsBackground as a property to check where the push notification came from.
Solution:
Check JSON object of callback for the isBackground boolean event.
If 1 it means that it was called from the background, if 0, this means that it wasn't.
I created a simple chatroom using socket.io. I have these scripts in my index.html :
var socket = io.connect('http://imageworkz.asia:8080');
// on connection to server, ask for user's name with an anonymous callback
socket.on('connect', function(){
// call the server-side function 'adduser' and send one parameter (value of prompt)
socket.emit('adduser', prompt("What's your name?"));
});
// listener, whenever the server emits 'updatechat', this updates the chat body
socket.on('updatechat', function (username, data) {
$('#conversation').append('<b>'+username + ':</b> ' + data + '<br>');
});
// listener, whenever the server emits 'updateusers', this updates the username list
socket.on('updateusers', function(data) {
$('#users').empty();
$.each(data, function(key, value) {
$('#users').append('<div>' + key + '</div>');
});
});
// on load of page
$(function(){
// when the client clicks SEND
$('#datasend').click( function() {
var message = $('#data').val();
$('#data').val('');
// tell server to execute 'sendchat' and send along one parameter
socket.emit('sendchat', message);
});
// when the client hits ENTER on their keyboard
$('#data').keypress(function(e) {
if(e.which == 13) {
$(this).blur();
$('#datasend').focus().click();
}
});
});
when i change the connection to http://localhost:8080 and start it using 'node app.js' command in console, it works fine but when I upload it and change it to http://imageworkz.asia:8080, it is not working whenever I go to url: http://imageworkz.asia:8080. Am I missing something or are there still things I should do to make it work when it is uploaded? or am I going to the wrong url? Thanks!
Try updating your node.js version to the latest one on the net (http://imageworkz.asia:8080).
Also check whether all necessary node modules are installed on the net, and if needed, change the logic such that you dont require prompt() to transmit a message.
I'm not completely sure, but I think this should work:
var socket = io.connect('http://imageworkz.asia');
// on connection to server, ask for user's name with an anonymous callback
socket.on('connect', function(){
// call the server-side function 'adduser' and send one parameter (value of prompt)
socket.emit('adduser', prompt("What's your name?"));
});
// listener, whenever the server emits 'updatechat', this updates the chat body
socket.on('updatechat', function (username, data) {
$('#conversation').append(''+username + ': ' + data + '');
});
// listener, whenever the server emits 'updateusers', this updates the username list
socket.on('updateusers', function(data) {
$('#users').empty();
$.each(data, function(key, value) {
$('#users').append('' + key + '');
});
});
// on load of page
$(function(){
// when the client clicks SEND
$('#datasend').click( function() {
var message = $('#data').val();
$('#data').val('');
// tell server to execute 'sendchat' and send along one parameter
socket.emit('sendchat', message);
});
// when the client hits ENTER on their keyboard
$('#data').keypress(function(e) {
if(e.which == 13) {
$(this).blur();
$('#datasend').focus().click();
}
});
});
it just removes :8080