That’s really cool @alisnic where were you three weeks ago when we started ripping out all our stubs?
Just kidding, if that works that’s really great and we’ll definitely check that out. It also could be turned into a package for people to play with quite easily I think…
And/or of course be included in the core for Meteor 3 possibly.
I was thinking about making a PR to Meteor, but the whole approach of setInterval seems a bit hacky to me and I thought Meteor core team will figure out something better in meantime
That’s great! @grubba will create two PRs, one implementing Zodern’s solution and the other with @alisnic’s solution. The core tests will run in both of them, and we can analyze to see which will work best, which will cover more edge cases, etc.
Web apis. As @radekmie mentioned it is unlikely to be a problem.
My concern is about the browser creating unrelated microtasks while a stub is running. Looking at the spec again it seems like it would mostly prevents this. The exceptions would be api’s like MutationObserver that create a microtask to run a callback in response to something the stub did (though this specific case shouldn’t be a problem since stubs shouldn’t modify the DOM).
I’ve looked into this several times in the past, and as @banjerluke described it doesn’t seem the current design for method stubs is a good solution. You might be able to after redesigning how method stubs work, but then you could also design them to work well with indexedDB.
I don’t have much experience with indexedDB, and the spec isn’t very clear on how it interacts with the event loop, but it seems like reading might work, and writing should work. The main problem is that the stub can’t wait to see if the transaction succeeded or not.
The wait option was broken with Meteor.apply, but I published a new version today to fix it. Besides that, I think it is done.
If it is fixed in Meteor 3, I think it should also be fixed in Meteor 2 so apps can migrate to async stubs without the current problems. My projects still use sync stubs and I know a few other developers are doing the same, so fixing it would allow those projects to use async stubs before updating to Meteor 3.
Thanks for sharing @alisnic. The currentRunningStack/savedStack with running each stub in a separate macrotask prevents the common causes of the issues better than I had expected.
I might be misunderstanding the code, but it seems macrotasks are still important. If a stub runs across multiple macrotasks, the issues still exist for apps that use the allow-deny package or Meteor’s api’s that don’t allow themselves to be used in stubs.
This can also affect outside code calling a method while an async stub is running. When calling a method that doesn’t have a stub, or when using Meteor.call or Meteor.apply, it immediately calls the method which causes them to be part of the simulation. For methods without a stub and maybe when using Meteor.call it can queue it to fix the problem.
Meteor.apply is more difficult since it can’t be queued when using the returnStubValue or throwStubExceptions options, and at the same time it can’t run a stub when another stub is running. That is one of the main problems with my previous idea of fixing this by tracking the async context.
There seems to be one case where currentRunningStack/savedStack doesn’t prevent an outside Meteor.callAsync call from running in an async simulation: a stub creates at least two macrotasks and takes at least 10ms to run. If outside code calls a method (call 2) while an async stub is running (call 1), call 2 will include call 1 in its stack. After the 10ms delay from the interval, if call 1 is still running, then call 2 will be part of the simulation and not run on the server.
I’m not sure how common those two conditions would be. Though calling a method from a stub can create the conditions - the nested method call adds at least a 10ms delay and creates at least one macrotask.
We (quave) have implemented this in the past, and we may be able to create a package with this code.
Maybe you should open an issue somewhere to discuss details. Could be in Meteor itself or Quave OSS.
Our solution was working well with offline Minimongo and in an app running in production at a large scale. The only downside was the lack of indexes in Minimongo so big collections (more than 1,000 documents) were a problem in some cases (doing full-scan many times if you don’t be careful with react renders, for example).
The original problem is from outside code running during a simulation. My proposal was:
Each async method stub should run in a separate macrotask. This removes the most common ways outside code can run during a simulation (including all of the issues mentioned in the 2.8 migration guide).
Method stubs should not wait for other macrotasks. This should remove all of the remaining ways it can happen.
@alisnic’s solution does the first one, but currently does not do the second. You can use this code to test:
if (Meteor.isServer) {
Meteor.methods({
'stub-only'() {
console.log('stub only called??');
},
'server-only'() {
console.log('server-only called');
},
'method'() {
console.log('method called');
}
});
} else {
Meteor.methods({
'stub-only'() {},
async 'method' () {
await Meteor.applyAsync('stub-only', [], {})
await Meteor.applyAsync('stub-only', [], {})
}
});
Meteor.callAsync('method');
// with the current implementation, this will run inside the simulation
setTimeout(() => {
Meteor.applyAsync('server-only', [], {});
}, 15);
}
The server should log server-only called, but since the timeout callback runs inside the simulation it does not.
I tested this code in a blank Meteor app. Interestingly, I noticed that around 10% of the time I loaded the page Meteor would fail to setup the ddp heartbeat. Sometimes the client would receive a websocket message while the simulation was running and try to start the heartbeat, which uses a Meteor api that behaves differently in a simulation.
Implementing the second part of the proposal would fix these issues.
I published zodern:fix-async-stubs@1.0.2. Meteor’s tests showed that the call order of methods and subscriptions needs to be maintained, so I added queueing for subscriptions.
These microtask shenanigans you’re doing for the async stubs, couldn’t we actually use the same trick for the Meteor Tracker?
To keep the Tracker.currentComputation around if it’s all in the same microtask etc pp?
Because I think before Macrotasks wouldn’t have worked with Tracker computations either, right?
Why doesn’t this just work out of the box and could we somehow fix / use this somehow to restore Tracker to some of its previous glory?
Edit:
If people insist to use Macrotasks in their client code (oh my!), we could hack these & queue those until no Tracker.currentComputation exists, if that’d help.