Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: await-able Async methods #1572

Merged
merged 25 commits into from Oct 1, 2018
Merged

feat: await-able Async methods #1572

merged 25 commits into from Oct 1, 2018

Conversation

aearly
Copy link
Collaborator

@aearly aearly commented Aug 5, 2018

Closes #1515

@@ -0,0 +1,19 @@
const PROMISE_SYMBOL = Symbol('promiseCallback')

function promiseCallback () {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the older strategy. It's a bit more clunky, but doesn't add another frame to the call stack for each method.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to use this in a couple places where the awaitify wrapper couldn't work (usually due to optional arguments).

})
}

awaitable[Symbol.toStringTag] = 'AsyncFunction'
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit of a hack, not sure if we should do this.

}

awaitable[Symbol.toStringTag] = 'AsyncFunction'
awaitable[ASYNC_FN] = true
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I intend to use this so we can avoid an additional layer of wrapping in wrapAsync.

@aearly aearly mentioned this pull request Aug 23, 2018
@aearly
Copy link
Collaborator Author

aearly commented Sep 3, 2018

All the collection methods and relevant control flow methods are now awaitable. However, I'm running into some problems with the Utils methods.

Most of the utils methods are wrappers, e.g. they wrap an async function and return an async function. We use initialParams a lot do to this, it always assumes a callback will be the last argument. However, if we want to make these wrapped functions conditionally awaitable, there are some ambiguities we have to smooth over. We can't know whether the args passed to the wrapped function include a callback or not.

We have avoided looking at fn.length in the past, because it is unreliable, and is 0 for any type of wrapped function or function with rest parameters.
However, we could work in an arity parameter to each method (like I've done for retryable in this PR), defaulting to function.length. If the arity is 0 or undefined, we can just not support awaiting those functions.

The other option is to wrap async functions differently than callback functions. If you pass an async function in to e.g. timeout, you'd get a promise-returning function always. (Currently, we make the wrapAsync() the async function, making it use callbacks internally). This would be a lot of work, as we would have to provide alternate implementations of each utils method, but there might be some cleverness to smooth it over with initialParams.

Or we just punt on this completely. You can't await wrapped functions. This would be a bummer, because await timeout(myFn, 1000)(...args) is pretty powerful.

@Themayu

This comment has been minimized.

@aearly

This comment has been minimized.

@Themayu

This comment has been minimized.

@aearly aearly changed the title [wip] await-able Async methods feat: await-able Async methods Sep 17, 2018
@aearly
Copy link
Collaborator Author

aearly commented Sep 17, 2018

@megawac @hargasinski I would love to get a review on this before merging. In regards to my above comment about the wrapper Utils methods, I'm thinking we can defer a solution there until later (temporary punt).

@hargasinski
Copy link
Collaborator

@aearly really sorry for the delay. These past few weeks have busy for me, and the PR ended up getting drowned out by other things. Thanks for the reminder! I will review it this weekend. I want to spend some time playing around with it, just to make sure I understand it fully.

Copy link
Collaborator

@megawac megawac left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I reviewed this a couple weeks ago to make sure the code made sense. The reason I didn't approve at the time was (and is) I'm not entirely sure such a change is necessary. Currently if the user wants to use the new async/await syntax they can call utils.promisify(myFavouriteAsyncMethod) or even promisify async.

I can definitely can see the draw of having this done by default I'm just trying to convince myself that this should be done on the methods proper or via a wrapper (e.g. import async from async/awaitable;

if (fn === null) return;
var callFn = fn;
fn = null;
callFn.apply(this, args);
};
}
Object.assign(wrapper, fn)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of this change?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So static properties attached to the fn get attached to the wrapper, namely the PROMISE_SYMBOL attached to promiseCallbacks.

@aearly
Copy link
Collaborator Author

aearly commented Sep 20, 2018

@hargasinski Don't sweat it, after all, this project is run on donated time.

@megawac I hear your concerns about whether we should do this by default -- this is a rather significant change. True, users can promisify methods, but I'm thinking about developer convenience. Already, people are omitting the final callback on an Async method and being surprised a promise is not returned. Several other popular libraries already work like this.

async/await is a very fundamental shift in how async code can work in JavaScript, and IMO it finally obsoletes callback-based approaches. It feels weird to not give first-class support to it -- I feel we're somewhat obligated to because we control the package named async on npm. Newer users of JavaScript are going to become more and more confused about the purpose of this library if we don't make some changes.

This next major version seems like a good point to throw more weight behind async/await-based approaches.

@megawac
Copy link
Collaborator

megawac commented Sep 20, 2018

How does this affect performance Alex? If its a significant hit my vote will be to seperate the asyncified and original methods into 2 files. I'd be completely fine with the async compatible version being our default export.

I know several people who use our library due to it's performance for intensive use cases so I'm adamant about maintaining good benchmarks :)

@aearly
Copy link
Collaborator Author

aearly commented Sep 20, 2018

There was no significant difference in the benchmarks on node 8 or 10. I was concerned the extra wrapper would slow things down, but that is not the case.

@hargasinski
Copy link
Collaborator

This is some really awesome work! The change look good to me.

Personally, I'm in favour of it. I think there has been enough interest/requests (#439, #956, promise-async, koa-async, async-q) to justify adding it. Also, looking over past issues, it seems that historically we've been against it because promises weren't implemented natively. Now that they are, and with significant parts of the community moving towards promise-based approaches, I think this change makes sense for us.

@aearly aearly merged commit 8aecf10 into master Oct 1, 2018
@aearly aearly deleted the awaitable branch October 1, 2018 00:00
@aearly aearly added this to the 3.0 milestone Oct 1, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants