How can I render data after 10 seconds of actual database update reactively in a particular template in Meteor.js? - mongodb

I'm currently working on a simple game called Bingo. Now I've made a spectate option in which I need to broadcast the game not real time but with a 10 second delay. Now how can I do that easily ?

The idea to use observe routine seems good but there are at least a couple of ways this can be implemented. One way is to delay the subscription itself. Here's a working example:
import { Meteor } from 'meteor/meteor';
import { TheCollection } from '/imports/collections.js';
Meteor.publish('delayed', function (delay) {
let isStopped = false;
const handle = TheCollection.find({}).observeChanges({
added: (id, fields) => {
Meteor.setTimeout(() => {
if (!isStopped) {
this.added(TheCollection._name, id, fields);
}
}, delay);
},
changed: (id, fields) => {
Meteor.setTimeout(() => {
if (!isStopped) {
this.changed(TheCollection._name, id, fields);
}
}, delay);
},
removed: (id) => {
Meteor.setTimeout(() => {
if (!isStopped) {
this.removed(TheCollection._name, id);
}
}, delay);
}
});
this.onStop(() => {
isStopped = true;
handle.stop();
});
this.ready();
});
Another way would be to create a local ProxyCollection that is only used for rendering purpose. The data would be copied from TheCollection to ProxyCollection with some delay using the same "observe technique" as in the subscription case.
In both scenarios you will need to handle some edge cases, for example:
Should the data be delayed on the initial load?
Should the update be delayed if document is removed?
Should the update be delayed for the user that initialized the change?
They can all be solved by utilizing and adjusting the technique presented above. I believe though, they're outside the scope of this question.
EDIT
To prevent delays on the initial data load you can update the above code as follows:
let initializing = true;
const handle = TheCollection.find({}).observeChanges({
added: (id, fields) => {
if (initializing) {
this.added(TheCollection._name, id, fields);
} else {
Meteor.setTimeout(() => {
if (!isStopped) {
this.added(TheCollection._name, id, fields);
}
}, delay);
}
},
// ...
});
// ...
this.ready();
initializing = false;
At first, it may not be obvious why this works, but everything here is being executed within a fiber. The observeChanges routine "blocks" and it first calls added for each document of the entire initial dataset. Only then it proceeds to the next part of your publish method body.
Something that one should be aware of is because of the behavior described above, a subscription may be stopped before the initial data set is processed and so, before the onStop callback is even defined. In this particular case it shouldn't hurt but sometimes it can be problematic.

You can use .observe(). It will tell you when added/changed events fire and you can do whatever you want in those events. Documentation here.
CollectionName.find().observe({
added: function (document) {
//do something here, like delaying the update
},
changed: function (document) {
//do something here, like delaying the update
},
});

Related

Rx.Net - Publish method missing first few items when subscribing to Cold Observable

Inspired by Akavache I am trying to create a solution that provides me with an IObservable<IArticle>. The method essentially first try to get all the articles that are present in the database, then it tries to fetch updated articles from the webservice and as it is getting the latest articles from webservice it tries to save them back to the database.
Since the webservice is essentially a cold observable and I don't want to subscribe twice, I used Publish to connect to it. My understanding is that I am using the correct version of the Publish method, however, many times the method tend to miss first couple of Articles from the GetNewsArticles. This was observed through the UI and also the Trace calls added in the call below.
Apart from solving the problem, it would be great to also understand how to debug/test this code (apart from introducing DI to inject NewsService).
public IObservable<IArticle> GetContents(string newsUrl, IScheduler scheduler)
{
var newsService = new NewsService(new HttpClient());
scheduler = scheduler ?? TaskPoolScheduler.Default;
var fetchObject = newsService
.GetNewsArticles(newsUrl,scheduler)
.Do(x => Trace.WriteLine($"Parsing Articles {x.Title}"));
return fetchObject.Publish(fetchSubject =>
{
var updateObs = fetchSubject
.Do( x =>
{
// Save to database, all sync calls
})
.Where(x => false)
.Catch(Observable.Empty<Article>());
var dbArticleObs = Observable.Create<IArticle>(o =>
{
return scheduler.ScheduleAsync(async (ctrl, ct) =>
{
using (var session = dataBase.GetSession())
{
var articles = await session.GetArticlesAsync(newsUrl, ct);
foreach (var article in articles)
{
o.OnNext(article);
}
}
o.OnCompleted();
});
});
return
dbArticleObs // First get all the articles from dataBase cache
.Concat(fetchSubject // Get the latest articles from web service
.Catch(Observable.Empty<Article>())
.Merge(updateObs)) // Update the database with latest articles
.Do(x => Trace.WriteLine($"Displaying {x.Title}"));
});
}
UPDATE - Added GetArticles
public IObservable<IContent> GetArticles(string feedUrl, IScheduler scheduler)
{
return Observable.Create<IContent>(o =>
{
scheduler = scheduler ?? DefaultScheduler.Instance;
scheduler.ScheduleAsync(async (ctrl, ct) =>
{
try
{
using (var inputStream = await Client.GetStreamAsync(feedUrl))
{
var settings = new XmlReaderSettings
{
IgnoreComments = true,
IgnoreProcessingInstructions = true,
IgnoreWhitespace = true,
Async = true
};
//var parsingState = ParsingState.Channel;
Article article = null;
Feed feed = null;
using (var reader = XmlReader.Create(inputStream, settings))
{
while (await reader.ReadAsync())
{
ct.ThrowIfCancellationRequested();
if (reader.IsStartElement())
{
switch (reader.LocalName)
{
...
// parsing logic goes here
...
}
}
else if (reader.LocalName == "item" &&
reader.NodeType == XmlNodeType.EndElement)
{
o.OnNext(article);
}
}
}
o.OnCompleted();
}
}
catch (Exception e)
{
o.OnError(e);
}
});
return Disposable.Empty;
});
}
UPDATE 2
Sharing the link to source code here.
There's a few things I don't like about your code. I assume NewsService is an IDisposable as it takes an HttpClient (which is disposable). You're not doing a proper clean up.
Also, you haven't provided a complete method - because you've tried cutting it down for the question - but that makes it hard to reason about how to rewrite the code.
That said, the one thing that sticks out to me as quite horrid looking is the Observable.Create. Can you please try this code instead and see if it helps things work for you?
var dbArticleObs =
Observable
.Using(
() => dataBase.GetSession(),
session =>
from articles in Observable.FromAsync(ct => session.GetArticlesAsync(newsUrl, ct))
from article in articles
select article);
Now, if that does, try rewriting fetchObject to use the same Observable.Using when newing up the `NewService.
In any case, it would be good if you could provide a complete implementation of GetContents, NewsService and your dataBase code in your question.

Meteor onRendered function and access to Collections

When user refresh a certain page, I want to set some initial values from the mongoDB database.
I tried using the onRendered method, which in the documentation states will run when the template that it is run on is inserted into the DOM. However, the database is not available at that instance?
When I try to access the database from the function:
Template.scienceMC.onRendered(function() {
var currentRad = radiationCollection.find().fetch()[0].rad;
}
I get the following error messages:
Exception from Tracker afterFlush function:
TypeError: Cannot read property 'rad' of undefined
However, when I run the line radiationCollection.find().fetch()[0].rad; in the console I can access the value?
How can I make sure that the copy of the mongoDB is available?
The best way for me was to use the waitOn function in the router. Thanks to #David Weldon for the tip.
Router.route('/templateName', {
waitOn: function () {
return Meteor.subscribe('collectionName');
},
action: function () {
// render all templates and regions for this route
this.render();
}
});
You need to setup a proper publication (it seems you did) and subscribe in the route parameters. If you want to make sure that you effectively have your data in the onRendered function, you need to add an extra step.
Here is an example of how to make it in your route definition:
this.templateController = RouteController.extend({
template: "YourTemplate",
action: function() {
if(this.isReady()) { this.render(); } else { this.render("yourTemplate"); this.render("loading");}
/*ACTION_FUNCTION*/
},
isReady: function() {
var subs = [
Meteor.subscribe("yoursubscription1"),
Meteor.subscribe("yoursubscription2")
];
var ready = true;
_.each(subs, function(sub) {
if(!sub.ready())
ready = false;
});
return ready;
},
data: function() {
return {
params: this.params || {}, //if you have params
yourData: radiationCollection.find()
};
}
});
In this example you get,in the onRendered function, your data both using this.data.yourData or radiationCollection.find()
EDIT: as #David Weldon stated in comment, you could also use an easier alternative: waitOn
I can't see your collection, so I can't guarantee that rad is a key in your collection, that said I believe your problem is that you collection isn't available yet. As #David Weldon says, you need to guard or wait on your subscription to be available (remember it has to load).
What I do in ironrouter is this:
data:function(){
var currentRad = radiationCollection.find().fetch()[0].rad;
if (typeof currentRad != 'undefined') {
// if typeof currentRad is not undefined
return currentRad;
}
}

How to use wait() in OnBeforeAction

I see older posts where old versions of Iron-router do wait() in before:
before: function() {
// let's make sure that the topPosts subscription is ready and the posts are loaded
if (this.data()) {
// we can then extract the userIds of the authors
var userIds = this.data().map(function(p) { return p.userId });
// and add the authors subscription to the route's waiting list as well
this.subscribe('authors', userIds).wait(); **<--- this guy!**
}
}
Above is from https://www.discovermeteor.com/blog/reactive-joins-in-meteor/
If I add wait() to my OnBeforeAction subscribe, I get these errors:
You called wait() after calling ready() inside the same computation tree.
You can fix this problem in two possible ways:
1) Put all of your wait() calls before any ready() calls.
2) Put your ready() call in its own computation with Deps.autorun.
My waitOn is
waitOn: function() {
return Meteor.subscribe('weeks', this.params.league);
},
and OnBeforeAction
onBeforeAction: function() {
if (this.ready()) {
// we can now get the latest (first in the list) week
var week = SheetData.find().fetch()[0].week;
this.subscribe('standings', this.params.league, week).wait();
this.next();
}
}
If I remove the wait() the render template starts before my subscription is ready. The suggested fixes don't seem applicable. What am I missing?
Here's a solution based on radzserg's answer to a similar question. He cleverly uses a callback function in the original waitOn function, and no onBeforeAction. In your case it would be:
waitOn: function() {
var that = this;
return Meteor.subscribe('weeks', this.params.league, function() {
var week = SheetData.find().fetch()[0].week;
that.wait(Meteor.subscribe('standings', that.params.league, week));
});
}

Angular service not updating on model change

In may app I have a service which holds my tasks:
app.factory('Tasks', function () {
var tasks = [];
return {
getAll: function () { return tasks; },
addOne: function (task) { tasks.push(task); },
openBy: function(owner) {
return _.where(tasks, {owner: owner, status: 'open'});
},
doneBy: function(owner) {
return _.where(tasks, {owner: owner, status: 'closed'});
},
};
});
I then show, per owner, either their open tasks or closed tasks.
The problem is that when I update the tasks by using Tasks.addOne(task); the views that use Tasks.openBy(owner) don't get updated. The ones that use Tasks.getAll() do.
Is this because I am returning a new array? If so is there a way of telling the controller to update what it has? Or am I just doing this entirely wrong in the first place and is there a better way to do it?
Any help would be greatly appreciated.
Matthew
What you could do is to call the openBy function in the controller (or directive) rather than in the view and wrap it into a $scope.$watch invocation. As an example:
// Controller body
app.controller('TasksCtrl', function($scope, Tasks) {
$scope.allTasks = Tasks.getAll();
$scope.$watch('allTasks', function() {
// Assume owner is the user and it's initialised somewhere above
$scope.ownedTasks = tasks.openBy(owner);
}, true);
});
This should work, even though I think that it should be possible to do something more elegant maybe!

Meteor code must always run within a fiber when deploy in meteor server

I kept having this error when i deploy my app onto meteor cloud server.
Meteor code must always run within a Fiber
at _.extend.get (app/packages/meteor/dynamics_nodejs.js:14:13)
at _.extend.apply (app/packages/livedata/livedata_server.js:1268:57)
at _.extend.call (app/packages/livedata/livedata_server.js:1229:17)
at Meteor.startup.Meteor.methods.streamTwit (app/server/server.js:50:24)
however, I have already wrapped within Fibers
streamTwit: function (twit){
var userid = '1527228696';
twit.stream(
'statuses/filter',
{ follow: userid},
function(stream) {
stream.on('data', function(tweet) {
Fiber(function(){
if(tweet.user.id_str === userid)
{
Meteor.call('addQn', tweet);
}
}).run();
console.log(tweet);
console.log('---------------------------------------------------------');
console.log(tweet.user.screen_name);
console.log(tweet.user.name);
console.log(tweet.text);
});
}
);
}
I don't know what's the reason but someone suggested that i should wrap it with Meteor.bindEnvironment instead. Hence, I did this:
streamTwit: function (twit){
this.unblock(); // this doesn't seem to work
console.log('... ... trackTweets');
var _this = this;
var userid = '1527228696';
twit.stream(
'statuses/filter',
{ follow: userid},
function(stream) {
stream.on('data', function(tweet) {
Meteor.bindEnvironment(function () {
if(tweet.user.id_str === userid)
{
Meteor.call('addQn', tweet);
}
}, function(e) {
Meteor._debug("Exception from connection close callback:", e);
});
console.log(tweet);
console.log('---------------------------------------------------------');
console.log(tweet.user.screen_name);
console.log(tweet.user.name);
console.log(tweet.text);
});
}
);
}
//add question method
addQn:function(tweet){
questionDB.insert({'tweet': tweet, 'date': new Date()});
}
but now it doesn't even work. I realise that this only happened when I tried to insert some data into mongodb.
May I know what is the problem with my code? Thanks!
All these codes were written in app/server/server.js
You shouldn't need to use Meteor.call on the server side. That is for client-side code only. Just call addQn directly or better yet, inline it since it's just one line of code.