Using Meteor APIs asynchronously?

Continuing the discussion from Meteor 1.3 early beta now available:

That would be great. I’d like to have (or at least feel like I have) control over everything asynchronous about my app (server and client).

@benjamn Is there a (recommended?) way of using Meteor APIs (like Collection#findOne()) asynchronously instead of synchronously?

I already know I can provide an asyncCallback for things like Meteor.methods and HTTP.* methods. What about collection methods?

1 Like

Can we use cursor.fetch() asynchronously on the server-side somehow? I feel like I might be trying to work against Meteor’s synchronous-style API, but I feel that I having asynchronous control of everything is the way I’d rather go (especially with async/await around now).

The biggest regret we have about fibers is that they encourage synchronous code when it would be much more efficient (and not that much harder) to write asynchronous code.

The great news is that async functions work very nicely with fibers, if you think of each async function call as creating an independent Fiber.

While there are some temporary issues between Regenerator (the transpiler Babel uses for async and generator functions) and fibers, here’s the idiom I would like to enable:

Promise.all(
  // Turn each cursor fetch into a Promise.
  cursors.map(cursor => (async () => cursor.fetch())())
).then(results => {
  // Called as soon as the final cursor returns.
});

I’ll be working on those remaining issues in my spare time this week.

4 Likes

I anticipate we will eventually introduce Promise-returning versions of Collection methods, such as Docs.findAsync(docId)

In the meantime, the same idiom I suggested above should work:

let findAsync = async (docId) => Docs.find(docId);
findAsync(docId).then(doc => {
  // Use the resulting doc.
});

Note that this style only works on the server, because it assumes Docs.find is implemented using fibers.

The real advantage of the Docs.findAsync API is that it could work on both the client and the server, assuming the implementation no longer uses fibers.

3 Likes

@benjamn

Promise-versions of various APIs will be awesome!

I tried this on the server-side using regenerator (with Babel’s transform from async to generator form). I also tried just using plain Promises as in

let promise = Promise.resolve(Docs.find(docId))
promise.then(doc => {
  // Use the resulting doc.
})

In both cases, the find call seems to be blocked indefinitely, which led me to post this issue on GitHub. It seems like the problem might be that some Meteor APIs don’t work as expected when deferred to another tick (as Promises do when resolving even static values).

Is there some way to make this work with Promises, perhaps using Meteor.bindEnvironment or some other API that isn’t in the docs?

For now, I ended up settling with the purely synchronous form of everything, since I just need to get things working, but ultimately I’d like to make as many asynchronous things as possible run in parallel.

Hum, are we really going to rename all collection methods? Since both insert and update also return some value, will we have to call insertAsync and updateAsync in the future? Appending Async to dozen of methods of the Meteor API isn’t super appealing…

I guess the migration path will be a bit hard but wouldn’t it better to recognize that this is a breaking change and replace fibers on the server and callbacks on the client by async/await on both environments?

Or do you think that appending Async will be an useful convention to be sure about which methods supports promises?

const doc = Docs.findOne(docId); //before
const doc = await Docs.findOneAsync(docId); //after

Edit: Fixed the above snippet as I confused async and await as noted by @trusktr just below.

2 Likes

@mquandalle I think you meant await instead of async, but yeah, I think that would make sense. I don’t think the non-Async versions of the APIs should go away, so we can still use the sync APIs.

The extra verbiage in

const doc = await Docs.findOneAsync(docId)

is far out-weighed by the asynchronous control that we’ll be able to opt in to having. Another possibility could be to keep the same API symbols, but accept options in the function call. For example, some methods already have an options parameter:

const doc = Docs.findOne(docId); //before
const doc = await Docs.findOne(docId, {async: true}) //after

but that’s more verbose than the extra Async in the method name.

1 Like

After playing a little bit with async/await with Typescript, there are a number of gotchas which make them not that easy.

Much more isomorphic, for sure.

Meteor isomorphism is kind of a lie because of some details, the worse of them is fibers, a native module that will never be present on the browser.

I watched some explanations about why async functions don’t complete replace fibers. I understand and agree, but isomorphism is so much more important than any feature fibers could add to the game, server only.

3 Likes

@vjau Could you elaborate on that? What made them not so easy?

True, since if we use entirely async APIs, then we’ll have the exact same code everywhere! :} The Meteor.methods system will need a revamp so that it can accept async functions on both side, client and server.

1 Like

From what i remember, nested async functions are not possible.
There are workarounds though.

@vjau

We can nest async functions. A contrived example:

~async function() {

    console.log('Waiting 4 seconds before printing...')

    await (async function() {
        return new Promise(resolve => setTimeout(resolve, 4000))
    })()

    console.log('Hello, after 4 seconds!')
}()

It works. I wouldn’t recommend writing code like that though. We’d abstract our functions, like this:

function sleep(duration) {
    return new Promise(resolve => setTimeout(resolve, duration))
}

~async function() {

    console.log('Waiting 4 seconds before printing...')

    await sleep(4000)

    console.log('Hello, after 4 seconds!')
}()

We could also write the sleep function as

async function sleep(duration) {
    await new Promise(resolve => setTimeout(resolve, duration))
}

but this one is slightly less performant as now there’s an extra async layer and two promises instead of one because async functions return promises; we’re making a new one, then a second one is implicitly returned even though we don’t write an actual return statement. But, if it helps readability, and the use-case isn’t graphics, then I’d say just use the async version so it’s clear that it is meant to be used in an await expression.

Furthermore, I think we can prevent an implicit Promise from being returned from an async function if we just return our own promise, so the sleep function can also be written as

async function sleep(duration) {
    return new Promise(resolve => setTimeout(resolve, duration))
}
1 Like