I'm building a control where there is visual feedback as progress is made for the server responding to the input from the client.
The control will be visible on multiple clients at once, and I want the client that made the change on the control to get slightly different feedback to all the others that will see less information about the state changes.
Is there a meteor inbuilt function to uniquely identify each client which I could use for this? If not, how could I go about making a non-repudiated identifier? It would need to identify two different tabs in the same browser as two different clients.
I couldn't find any easy way to do that built into Meteor, but you can try this depending on your exact use-case.
Here is a technique to track unique client connections per browser window or tab. Each connectionId below can be thought of as a chat room. Since the this.connection.id property inside a Meteor method is not unique per open window or tab, this will store the connection id along with a timestamp inside a collection. When the client closes the browser tab or window, you can use the callback inside the server method this.connection.onClose to lookup that particular connection by its id along with the timestamp and flag it as closed or offline.
Fiber = Npm.require('fibers');
Future = Npm.require('fibers/future');
Meteor.methods({
'client.disconnect': function(connectionId){
check(connectionId, String);
let query = {_id: connectionId};
let options = {$set: {isOnline: false}};
return Connections.update(query, options);
},
'client.connect': function(connectionId){
check(connectionId, String);
let lastSessionTime = Number(new Date().getTime());
let lastSessionId = this.connection.id;
let offlineQuery = {
_id: connectionId,
lastSessionTime: lastSessionTime,
lastSessionId: lastSessionId
}
let offlineOptions = {
$set: {isOnline: false}
}
// When the connection closes, turn this connection offline.
this.connection.onClose(function(){
// You could also remove the document.
Connections.update(offlineQuery, offlineOptions);
});
let onlineQuery = {
_id: connectionId
}
let onlineOptions = {
$set: {
lastSessionTime: lastSessionTime,
lastSessionId: lastSessionId,
isOnline: true}
}
var future = new Future();
Connections.upsert(onlineQuery, onlineOptions, function(err, res){
if (err){
future.throw('Connections.online error');
}else{
future.return(res);
}
});
}
});
Related
So I have this code guys
Template.mainLayout.onCreated(function () { //HERE
console.log("mainLayout created");
var context = FlowRouter.current();
// use context to access the URL state
console.log(context);
var visitedOne = context.path;
//getting the connID
var clientIp = headers.getClientIP(); // no need for this anymore
var clientConnId = Meteor.connection._lastSessionId; // HERE
console.log(clientIp);
console.log(clientConnId); //HERE
// console.log(Meteor.connection._lastSessionId);
Meteor.call("updateHistory", {clientIp,clientConnId,visitedOne}, function(error, result){
if(error){
console.log("error", error);
});
if(result){
}
});//Meteor.call
});
My problems are marked by the comments //HERE
Meteor.connection._lastSessionId returns undefined at onCreated event. However if I try to get on click event it works just fine. Why is this caused, what's a workaround for this?
You're attempting to log the session ID before the connection has received it. For example, wrap your call in a setTimeout:
...
setTimeout(() => {
console.log(Meteor.connection._lastSessionId);
}, 500);
...
You might have to tweak the timeout value a bit, but it will be logged. Using setTimeout in this fashion really isn't that reliable though, as the amount of time it takes for the session ID to get set can vary. You'll likely want to look into setting up some kind of simple polling to continuously check for the session ID, until it's set.
Basically _lastSessionId isn't yet available on the client when the template is originally created (it's probably the first template rendered in your app). However there is no need to get this on the client since you're calling a server method anyway, just use the variable directly there where it will already exist!
So simplify:
Meteor.call("updateHistory", {clientIp,clientConnId,visitedOne}, callback)
to:
Meteor.call("updateHistory", visitedOne, callback)
and get the clientIp (if necessary) and use this.connection.id on the server.
I have the following scenario:
Client side has a button clicking it will execute Meteor.call method on the server-side which will call API and fetch products, During this time I wan't to disable this button + block this method from executing again basically nothing stops you from clicking the button 100x times and server will keep on executing same method again and again.
Few ideas I had in my mind: Use sessions to disable button (Problem: can still using the console Meteor.call and abuse it)
I also looked at Meteor.apply in the docs with wait:true didn't seems to stop from method execution. I honestly not sure how this kind of thing is handled with no hacks.
Client-side:
'click .button-products': function(e){
Meteor.call('getActiveProducts', function(error, results){
if (error)
return Alerts.add(error.reason, 'danger', {autoHide: 5000});
if (results.success)
return Alerts.add('Finished Importing Products Successfully', 'success', {autoHide: 5000});
})
}
Server-side
Meteor.methods({
getActiveProducts: function(){
var user = Meteor.user();
var api = api.forUser(user);
importProducts = function(items){
nextPage = items.pagination.next_page;
items.results.forEach(function(product){
var sameproduct = apiProducts.findOne({listing_id: product.listing_id});
if (sameproduct) {
return;
}
var productExtend = _.extend(product, {userId: Meteor.userId()});
apiProducts.insert(productExtend);
});
};
var products = api.ProductsActive('GET', {includes: 'Images', limit: 1});
importProducts(products);
while (nextPage !== null) {
products = api.ProductsActive('GET', {includes: 'Images', page: nextPage, limit: 1});
importProducts(products);
}
return {success: true};
}
});
From the Meteor docs:
On the server, methods from a given client run one at a time. The N+1th invocation from a client won't start until the Nth invocation returns. However, you can change this by calling this.unblock. This will allow the N+1th invocation to start running in a new fiber.
What this means is that subsequent calls to the method won't actually know that they were made while the first call was still running, because the first call will have already finished running. But you could do something like this:
Meteor.methods({
getActiveProducts: function() {
var currentUser = Meteor.users.findOne(this.userId);
if (currentUser && !currentUser.gettingProducts) {
Meteor.users.update(this.userId, {$set: {gettingProducts: true}});
// let the other calls run, but now they won't get past the if block
this.unblock();
// do your actual method stuff here
Meteor.users.update(this.userId, {$set: {gettingProducts: false}});
}
}
});
Now subsequent calls may run while the first is still running, but they won't run anything inside the if block. Theoretically, if the user sends enough calls, the first call could finish before all of the others have started. But this should at least significantly limit the number of etsy calls that can be initiated by a user. You could adapt this technique to be more robust, such as storing the last time a successful call was initiated and making sure X seconds have passed, or storing the number of times the method has been called in the last hour and limiting that number, etc.
A package I wrote a while back might come in handy for you. Essentially it exposes the Session api on the server side (hence the name), meaning you can do something like ServerSession.set('doingSomethingImportant', true) within the call, and then check this session's value in subsequent calls. The session can only be set on the server, and expires upon connection close (so they could spam calls, but only as fast as they can refresh the page).
In the event of error, you can just reset the session. There shouldn't be any issues related to unexpected errors either because the session will just expire upon connection close. Let me know what you think :)
I'm making a simple app that informs a client that other clients clicked a button. I'm storing the clicks in a Firebase (db) using:
db.push({msg:data});
All clients get notified of other user's clicks with an on, such as
db.on('child_added',function(snapshot) {
var msg = snapshot.val().msg;
});
However, when the page first loads I want to discard any existing data on the stack. My strategy is to call db.once() before I define the db.on('child_added',...) in order to get the initial number of children, and then use that to discard that number of calls to db.on('child_added',...).
Unfortunately, though, all of the calls to db.on('child_added',...) are happening before I'm able to get the initial count, so it fails.
How can I effectively and simply discard the initial data?
For larger data sets, Firebase now offers (as of 2.0) some query methods that can make this simpler.
If we add a timestamp field on each record, we can construct a query that only looks at new values. Consider this contrived data:
{
"messages": {
"$messageid": {
"sender": "kato",
"message": "hello world"
"created": 123456 // Firebase.ServerValue.TIMESTAMP
}
}
}
We could find messages only after "now" using something like this:
var ref = new Firebase('https://<your instance>.firebaseio.com/messages');
var queryRef = ref.orderBy('created').startAt(Firebase.ServerValue.TIMESTAMP);
queryRef.on('child_added', function(snap) {
console.log(snap.val());
});
If I understand your question correctly, it sounds like you only want data that has been added since the user visited the page. In Firebase, the behavior you describe is by design, as the data is always changing and there isn't a notion of "old" data vs "new" data.
However, if you only want to display data added after the page has loaded, try ignoring all events prior until the complete set of children has loaded at least once. For example:
var ignoreItems = true;
var ref = new Firebase('https://<your-Firebase>.firebaseio.com');
ref.on('child_added', function(snapshot) {
if (!ignoreItems) {
var msg = snapshot.val().msg;
// do something here
}
});
ref.once('value', function(snapshot) {
ignoreItems = false;
});
The alternative to this approach would be to write your new items with a priority as well, where the priority is Firebase.ServerValue.TIMESTAMP (the current server time), and then use a .startAt(...) query using the current timestamp. However, this is more complex than the approach described above.
I was having success with using AngularFire in a scenario where there is one user on my application.
Now that I have authentication up and running, I'm noticing that assigning items to $scope.items is catastrophic when switching users, mainly due to the $scope failing to update correctly.
Reading directly from the docs...
var ref = new Firebase('https://<my-firebase>.firebaseio.com/items');
angularFire(ref, $scope, 'items');
I need these to be only the items of the currently authorized user. So currently, I do this (if there's a better way, don't hesitate to tell me!)
var ref = new Firebase('https://<my-firebase>.firebaseio.com/items/userId');
angularFire(ref, $scope, 'items');
I generate userId using auth.provider and auth.id, btw. Now that my items are namespaced in (let's say) user1
var ref = new Firebase('https://<my-firebase>.firebaseio.com/items/[user1id]');
angularFire(ref, $scope, 'items');
I add items to $scope.items
$scope.create = function(item) {
$scope.items.push(item)
/* Pretend the user adds these from the interface.
[
{ name: 'eenie' },
{ name: 'meenie' },
{ name: 'miney' },
{ name: 'moe' }
]
*/
}
The problem
Now if I just log out and login as someone else, magically that user has eenie meenie miney and moe because $scope.items held the array between logout and login.
I tried to set $scope.items = [] on logout event, but that actually empties all the records. I'm pulling my hair out. This is 0.001% of what I need to do in my project and it's taking my whole weekend.
Update New method
$scope.create = function() {
$scope.selectedDevice = {
name: 'New Device',
userId: $scope.user.provider + $scope.user.id
};
return $scope.devices.push($scope.selectedDevice);
};
$scope.$on('angularFireAuth:login', function(evt, user) {
var promise, ref;
ref = new Firebase('https://mysite.firebaseio.com/users/' + (user.provider + user.id) + '/registry/');
promise = angularFire(ref, $scope, 'devices');
});
It now will accurately create items under the user's id. However, still, once you logout and log back in, those items do not get cleared from $scope.devices. Therefore, they just add themselves to data but under the newly logged in user.
Update
I did a lot of trial and error. I probably set $scope.devices to [] and moved around login events in every possible combination. What eventually worked was #hiattp's fiddle in the accepted answer.
This is a result of the implicit data binding remaining intact as you switch users. If the new user shows up and creates a new binding, it will consider the existing data to be local changes that it should assimilate (that's why you see the original user's items being added to the new user), but if you try to clear them first without releasing the binding then you are implicitly telling Firebase to delete that data from the original user's item list (also not what you want). So you need to release the data bindings when you detect the logout (or login) events as needed.
The callback in the angularFire promise provides an "unbind" method (see here and here):
var promise = angularFire(ref, $scope, 'items');
promise.then(function(unbind){
// Calling unbind() will disassociate $scope.items from Firebase
// and generally it's useful to add unbind to the $scope for future use.
});
You have a few idiosyncrasies in your code that are likely causing it not to work, and remember that unbind won't clear the local collection for you. But just so you have an idea of how it should work (and to prove it does work) here is a fiddle.
You need to unbind $scope.items on logout. The best way to do this will be to save the unbind function given to your promise in $scope:
var ref = new Firebase('https://<my-firebase>.firebaseio.com/items/[user1id]');
angularFire(ref, $scope, 'items').then(function(unbind) {
$scope.unbindItems = unbind;
});
$scope.$on('angularFireAuth:logout', function() {
$scope.unbindItems();
});
I need to inform clients about changes on server side. In my case I am using different Collections on server and on client (more about it in this question: how would you build pinterest like page with meteor.js).
On the server I am getting new Products from external API. I would like to publish the number of new items to all clients that they could update their local variables needed for layout to work well.
How to do it?
It would be nice if I could publish/subscribe other kinds of data than Meteor.Collection. I found Meteor.deps, but what I understand it works only on client side.
To accomplish what you want you do need another collection - on the client. On the server, in a publish function, build a document from scratch assigning the current count of Products to an attribute. Using observe() and set, modify count when documents are added or removed from Products. Subscribe to the count "record set" on the client.
// Server
Meteor.publish('count', function () {
// Build a document from scratch
var self = this;
var uuid = Meteor.uuid();
var count = Products.find().count();
// Assign initial Products count to document attribute
self.set('count', uuid, {count: count});
// Observe Products for additions and removals
var handle = Products.find().observe({
added: function (doc, idx) {
count++;
self.set('counts', uuid, {count: count});
self.flush();
},
removed: function (doc, idx) {
count--;
self.set('counts', uuid, {count: count});
self.flush();
}
});
self.complete();
self.flush();
self.onStop(function () {
handle.stop();
});
});
// Client
Counts = new Meteor.Collection('count');
Meteor.subscribe('count');
console.log('Count: ' + Counts.findOne().count);
I must say the above solution showed me one way, but still, what if I need to publish to client data that are not connected with observe()? Or with any collection?
In my case I have i.e. 1000 products. To engage visitors I am "refreshig" the collection by updating the timestamp of random number of products, and displaying collection sorted by timestamp. Thank to this visitors have impression that something is happening.
My refresh method returns number of products (it is random). I need to pass that number to all clients. I did it, but using (I think) ugly workaround.
My refresh method sets Session.set('lastRandomNo', random). BTW: I didn't know that Session works on server side. refresh updates Products collection.
Then accoriding to above answer:
Meteor.publish 'refreshedProducts', ->
self = this
uuid = Meteor.uuid()
# create a new collection to pass ProductsMeta data
self.set('products_meta', uuid, { refreshedNo: 0 })
handle = Products.find().observe
changed: (newDocument, atIndex, oldDocument) ->
self.set('products_meta', uuid, { refreshedNo: Session.get('lastRandomNo') })
self.flush()
self.complete()
self.flush()
self.onStop ->
handle.stop()
and on client side:
ProductsMeta = new Meteor.Collection('products_meta')
# subscribe to server 'products_meta' collection that is generated by server
Meteor.subscribe('refreshedProducts')
ProductsMeta.find({}).observe
changed: (newDocument, atIndex, oldDocument) ->
# I have access to refreshedNo by
console.log ProductsMeta.findOne().refreshedNo
What do you think?