Despite the lack of async/await
or .then()
, many in-built Meteor functions are asynchronous and therefore non-blocking. They use something called “Fibers” under the hood which are an alternative method of unblocking the thread while an external time-consuming task (querying the db, waiting for a http call, etc) is happening.
So even though it looks like you’re writing blocking synchronous code, it’s actually not. Some examples:
// now
const docs = myCollection.find({...}).fetch();
// sometime in the future
Let’s say that was a big query, or your database is far away from your server on a slow connection and that line of code took 10s to execute. As soon as the request from your server to the db is sent, your thread is free to do other stuff - even though you haven’t used await
and this is not an async
function. The only time this call blocks the thread is when the data is received back from the database and loaded into memory - if it’s a lot of data then this can take time.
To prove this to yourself, try this:
const handle = Meteor.setInterval(() => console.log("I'm still running!", new Date()), 10);
console.log("Getting data...");
const docs = myCollection.find({ a really big query }).fetch();
// sometime in the future...
console.log("Got data", docs.length);
Meteor.clearInterval(handle);
You will see the console logs still happening while the query is running, and they might not fire towards the end while the data is being loaded into memory.
Of course, for a big query this is preferable:
const handle = Meteor.setInterval(() => console.log("I'm still running!", new Date()), 10);
console.log("Getting data one at a time...");
myCollection.find({ a really big query }).forEach(doc => {
console.log("Got a document", new Date());
});
console.log("Finished!");
Meteor.clearInterval(handle);
Here you will see the "Got a document"
logs interspersed with I'm still running"
, with "Finished!"
at the very end.
All this time your thread is also free to handle incoming requests from your users, make other database requests, and return results to the users so they probably wont even notice any extra delay.
If you’re making http calls to an external API within that forEach
callback, then each of those API calls is also magically unblocked by Meteor’s HTTP methods:
console.log("Getting data one at a time...");
myCollection.find({ a really big query }).forEach(doc => {
console.log("Got a document", new Date());
try {
const result = HTTP.get(`https://external.api/collection/${doc.externalId}`);
console.log(" Got a result", new Date())
} catch (error) {
console.log(" That didn't work!", doc.externalId, error);
}
});
console.log("Finished!");
If you were using a 3rd party library which did require promises or async/await
, you could do:
console.log("Getting data one at a time...");
myCollection.find({ a really big query }).forEach(async doc => {
console.log("Got a document", new Date());
try {
const result = await thirdPartyLibrary.get(`https://external.api/collection/${doc.externalId}`);
console.log(" Got a result", new Date());
} catch (error) {
console.log(" That didn't work!", doc.externalId, error);
}
});
console.log("Finished!");
Or, if you had an asynchronous 3rd party library which required a (error, result)
you could use Meteor.wrapAsync
:
import { libary } from 'thirdpartylibrary';
// wrap library.methodWithCallback(apiKey, docId, (error, result) => {}) as an async method:
const wrappedMethod = Meteor.wrapAsync(library.methodWithCallback, library);
console.log("Getting data one at a time...");
myCollection.find({ a really big query }).forEach(doc => {
console.log("Got a document", new Date());
try {
const result = wrappedMethod(MY_API_KEY, doc.externalId);
console.log(" Got a result", new Date());
} catch (error) {
console.log(" That didn't work!", doc.externalId, error);
}
});
console.log("Finished!");
This loop is actually doing very little and will not block your thread very much. The only time the thread is being blocked is when each doc
and result
is being loaded into memory.
Magic! Thanks Meteor, you’re awesome!