Subscribing to a reactive data source using autorun - meteor

I'm trying to write a webapp using Meteor and I'm definitely failing to grok something about subscribing to published datasets. The entire app is up on github (linked to the latest commit for posterity), but I'll try to summarize below.
I have a collection called teams which is available to both client and server:
Teams = new Meteor.Collection( "teams" );
On the server, I want to publish a list of all of the teams:
Meteor.publish( "allteams", function() { ...
There's a very simple cursor which makes up this published list:
var handle = Teams.find( {} ).observeChanges({
added: function( id ) {
console.log( "New team added" );
if ( !initializing ) {
console.log( "Telling subscribers it's all change" );
self.added( "teams", id, {} );
self.ready();
}
}
});
The client subscribes to that source, and when elements are added the client will add pins to a map:
Meteor.autorun( function() {
Meteor.subscribe( "allteams", function() {
console.log( "All teams has been updated" );
// Do more stuff
}
};
When the list is initially populated the autorun runs fine, but if I add another element to the collection then the publisher method logs to say "I noticed this" but nothing happens in the subscriber.
The aim of the above is as follows:
There is a list of teams on the server which consists of a name and long/lat details
When a client connects, they receive that list of teams and they're plotted on a map
If a team is added to the list on the server side, each client is notified and a new pin appears on the map.
In terms of the app, I probably don't need pins to magically appear, but it's a useful way of learning publish and subscribe, especially when I don't get it right! Eventually, "allteams" will probably be a bit more fine-grained than just the entire list of teams, so I guess it's akin to making a view on the data.
Am I missing something completely obvious?
Edit: I worked it out and put the answer below. tl;dr I wasn't subscribing to a reactive data source at all.

It's probably not polite to answer my own question, but I worked out what I was doing wrong and I figured that other people might come across the same thing.
Simple answer is that I wasn't doing what I claimed to be doing in the title of the queston, specifically, not subscribing to a reactive data source.
Meteor.autorun( function() {
Meteor.subscribe( "allteams", function() {
console.log( "All teams has been updated" );
// Do more stuff
}
};
Here I've passed the subscribe method to autorun, but that method itself isn't a reactive data source. However, it returns something which is!
// Define a subscription
var handle = Meteor.subscribe( "foo", { onReady: function() { ... } } );
Meteor.autorun( function() {
if ( handle.ready() ) {
// Now do something every time the subscription is marked as ready
}
};
The ready method of a subscription handle is reactive, so the autorun now executes every time the published document set is updated. That leads me to further questions about the efficiency of multiple clients subscribing to a database cursor and watching for changes, but I'll come to that in another question.

If autopublish is still on, this may be the cause of your problems. Try disabling autopublish and seeing if the publish statement now works properly.

Related

Meteor GroundDB granularity for offline/online syncing

Let's say that two users do changes to the same document while offline, but in different sections of the document. If user 2 goes back online after user 1, will the changes made by user 1 be lost?
In my database, each row contains a JS object, and one property of this object is an array. This array is bound to a series of check-boxes on the interface. What I would like is that if two users do changes to those check-boxes, the latest change is kept for each check-box individually, based on the time the when the change was made, not the time when the syncing occurred. Is GroundDB the appropriate tool to achieve this? Is there any mean to add an event handler in which I can add some logic that would be triggered when syncing occurs, and that would take care of the merging ?
The short answer is "yes" none of the ground db versions have conflict resolution since the logic is custom depending on the behaviour of conflict resolution eg. if you want to automate or involve the user.
The old Ground DB simply relied on Meteor's conflict resolution (latest data to the server wins) I'm guessing you can see some issues with that depending on the order of when which client comes online.
Ground db II doesn't have method resume it's more or less just a way to cache data offline. It's observing on an observable source.
I guess you could create a middleware observer for GDB II - one that checks the local data before doing the update and update the client or/and call the server to update the server data. This way you would have a way to handle conflicts.
I think to remember writing some code that supported "deletedAt"/"updatedAt" for some types of conflict handling, but again a conflict handler should be custom for the most part. (opening the door for reusable conflict handlers might be useful)
Especially knowing when data is removed can be tricky if you don't "soft" delete via something like using a "deletedAt" entity.
The "rc" branch is currently grounddb-caching-2016 version "2.0.0-rc.4",
I was thinking about something like:
(mind it's not tested, written directly in SO)
// Create the grounded collection
foo = new Ground.Collection('test');
// Make it observe a source (it's aware of createdAt/updatedAt and
// removedAt entities)
foo.observeSource(bar.find());
bar.find() returns a cursor with a function observe our middleware should do the same. Let's create a createMiddleWare helper for it:
function createMiddleWare(source, middleware) {
const cursor = (typeof (source||{}).observe === 'function') ? source : source.find();
return {
observe: function(observerHandle) {
const sourceObserverHandle = cursor.observe({
added: doc => {
middleware.added.call(observerHandle, doc);
},
updated: (doc, oldDoc) => {
middleware.updated.call(observerHandle, doc, oldDoc);
},
removed: doc => {
middleware.removed.call(observerHandle, doc);
},
});
// Return stop handle
return sourceObserverHandle;
}
};
}
Usage:
foo = new Ground.Collection('test');
foo.observeSource(createMiddleware(bar.find(), {
added: function(doc) {
// just pass it through
this.added(doc);
},
updated: function(doc, oldDoc) {
const fooDoc = foo.findOne(doc._id);
// Example of a simple conflict handler:
if (fooDoc && doc.updatedAt < fooDoc.updatedAt) {
// Seems like the foo doc is newer? lets update the server...
// (we'll just use the regular bar, since thats the meteor
// collection and foo is the grounded data
bar.update(doc._id, fooDoc);
} else {
// pass through
this.updated(doc, oldDoc);
}
},
removed: function(doc) {
// again just pass through for now
this.removed(doc);
}
}));

meteor - get all subscriber session handles for a publisher method

I want to broadcast NON-MONGO-DB data from a server publisher to client collections. Currently I save all registered subscriber handles to use those for posting the data
client.js:
col = new Meteor.Collection("data")
Meteor.subscribe("stream")
On server side it looks like
server.js
all_handles = [];
Meteor.publish("stream", function() {
// safe reference to this sessions
var self = this;
// save reference to this subscriber
all_handles.push(self);
// signal ready
self.ready();
// on stop subscription remove this handle from list
self.onStop(function() {
all_handles = _.without(all_handles, self);
}
}
Then I can use the all_handles somewhere in my app to send data to those clients, like:
function broadcast(msg) {
all_handles.forEach(function(handle) {
handle.added("data", Random.id(), msg);
}
}
This is already in use and running.
Q: What I am looking for is: Can I get all handles from currently already existing meteor (internal) objects like _sessions or something else?
It would be great if I had not to organize the subscribers handle all the time by myself.
Please do not answer with links to other broadcast packages like streamy or else. I want to continue with standard collections but with as less code as possible.
Thanks for help and feedback
Tom
Maybe this might work for you: https://stackoverflow.com/a/30814101/2005564
You could get the connections via var connections = Meteor.server.stream_server.open_sockets; but as looshi said this might break with a future meteor update as it is not part of the public API...
As informed by #laberning I used for now the "undocumented" meteor connections.
You can post to all subscribers of a publishing method like:
// publish updated values to all subscribers
function publish_to_all_subscribers(subscription_name, id, data) {
_.each(Meteor.server.stream_server.open_sockets, function(connection) {
_.each(connection._meteorSession._namedSubs, function(sub) {
if (sub._name == subscription_name) {
sub.insert(subscription_name, id, data);
}
})
});
}
// create stream publisher
Meteor.publish('stream', function(){
// set ready
this.ready();
});
...
// use publishing somewhere in your app
publish_to_all_subscribers('stream', Random.id(), {msg: "Hello to all"});
...
updated: See an example MeteorPad for Publish and Subscribe and Broadcast messages

Meteor.subscribe is skipped, doesn’t work in Tracker.autorun

I’m using user’s profile to publish specific data to client.
However the subscribe doesn’t work and it’s killing me.
Although the Tracker.autorun() runs, the server doesn’t run any publish.
Tracker.autorun(function () {
console.log(‘autorun is running');
console.log(Meteor.user()) //to be reactive to user's update
Meteor.subscribe(“Lists”,{
onStop:function() {
console.log('subscribe call back onStop');
},
onReady: function(){
console.log('subscribe call back onReady');
});
console.log(Lists.find().count());
}
Meteor.publish('Lists', function(){
console.log('publish is running’);
var list = Meteor.users.findOne({_id: this.userId}).list;
return Lists.find({_id: {$in: list}});
}
p.s if I type Meteor.subscribe( “Lists”) in Chrome's console line, it works and server run publish normally.
p.s The Lists collection didn't change. Just publishing a different set. Is it the reason?
Thank for your reading.
If you have a close look at the docs for subscribe, you'll find this note in a section about reactive computations:
However, if the next iteration of your run function subscribes to the same record set (same name and parameters), Meteor is smart enough to skip a wasteful unsubscribe/resubscribe.
So because you are always calling subscribe with the same arguments, meteor isn't actually restarting it. The trick is just to pass extra parameters to defeat this "optimization". For example:
Tracker.autorun(function() {
var user = Meteor.user();
var list = user && user.list;
if (!_.isEmpty(list)) {
Meteor.subscribe('Lists', list, function() {
console.log(Lists.find().count());
});
}
});
Here we are extracting the list variable from the user (assuming it's published) and using it as an extra parameter to force the subscription to rerun. If it isn't published, you could just use a random id like this:
Tracker.autorun(function() {
var user = Meteor.user();
Meteor.subscribe('Lists', Random.id(), function() {
console.log(Lists.find().count());
});
});
This should also work but may be a little less efficient because it will fire whenever any property of the user changes.
I think you should run the "console log" after the subscribe and the publish methods return something, if you do somenthing inmediatly is should not work because the server is not returning anything yet, add a callback to the subscription.
Tracker.autorun( function() {
Meteor.subscribe( "List", function() {
console.log( "okok" );
// do your magic
}
};
hope it help, sorry about my english =)

Meteor: what is the right way to add custom settings object to users collection?

There are multiple examples on publish/subscribe but not clear on what is the best practice for storing custom data in the in-built "users" collection in Meteor (especially in the new possibility of template specific collections).
For example, I need to store user browse history - something that is accessible through Meteor.user().settings.history.lastvisited[]
The challenge is:
Is any special publish / subscribe required for the above? (the
reason being, I am assuming the users collection is already
published and available on client side - so do we need another?)
How to take care of edge cases where user is new and hence settings.history object may not be defined? Can we have a special publish that automatically takes care of creating an empty object if the settings is undefined? How to do it?
I did this :
// server side
Meteor.publish('userSettings', function (maxRows) {
if (this.userId) {
return Meteor.users.find({ _id: this.userId }, { fields: {'settings':1}});
}
this.ready();
});
//client side
Meteor.subscribe('userSettings');
But I do not see anyway how I can access the published "userSettings" object on the client side - what is missing ??
You can create a field and set it to false/'', on each user you create using the accountsOnCreateUser method.
Accounts.onCreateUser(function(options, user) {
//this function gets called each time a user has been created on the Meteor.user collection
if (options.profile)
user.settings = ''; //this is just and example.
return user;
})
Now the publish looks ok, but in order to get it work im always use a Tracker.autorun function.
Tracker.autorun(function(){
Meteor.subscribe('userSettings');
})
Why the autorun? well if you don't call the auto run here, the subscription get only called 1 time when the apps loads, and not when the user documents.
Take care of yours deny/allow permissions, check this meteor:common mistakes post on the Profile editing section
Also the subscribe function have a callback function. Meteor.subscribe(name, [arg1, arg2...], [callbacks]), so you can do something like this.
var myUserSubscription = Meteor.subscribe('userSettings',function(){
console.log("ok im here on the client side")
console.log("this user subscription is ready " + myUserSubscription.ready())
})
console.log("outside the subscription why not? " + myUserSubscription.ready();
About ready();
True if the server has marked the subscription as ready. A reactive
data source.

Can't put data from a Meteor collection into an array

I'm learning Meteor and I was trying to pass the result of a Collection.find() into and array (using a variable) and the simpler code I have is (in a file that is in the root):
CalEvents = new Mongo.Collection('calevents'); //creating a collection
/*------------------------- Populating the database with dummy data-------*/
if (Meteor.isServer) {
Meteor.startup(function () {
if (CalEvents.find().count() === 0) {
CalEvents.insert({
title: "Initial room",
start: '2010-02-02'
});
}
});
}
/*--------------- Creating an array from the collection-----------------*/
events = [];
calEvents = CalEvents.find({});
calEvents.forEach(function(evt){
events.push({
title: evt.title,
start: evt.start,
})
});
The page has nothing to show but using the console I can see (CalEvents.find().fetch()) that I have data in my database but the "events" variable is empty...
I can't understand why because I tried several other things such as changing file names and moving code to guarantee the proper order.
And I already tried to use CalEvents.find().fetch() to create an array an put the result into a variable but I'm not able to do it...
Does anyone know what's so simple that I'm missing?...
Do you use autosubscribe?
You probably need to make sure the sbscription is ready. See Meteor: How can I tell when the database is ready? and Displaying loader while meteor collection loads.
The reason you do see CalEvents.find().fetch() returning items in the console is that by the time you make that call, the subscription is ready. But in your events = []; ... code (which I assume is in a file under the client directory, you might have assumed that the subscription data has arrived when in fact it has not.
A useful debugging tool is Chrome's device mode ("phone" icon near the search icon in DevTools), which lets you simulate slow networks (e.g. GPRS, with 500ms delay for every request).

Resources