-
Notifications
You must be signed in to change notification settings - Fork 29.6k
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
n-api: implement promise #14365
n-api: implement promise #14365
Conversation
7de6cc2
to
26bc950
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The deferred wrapping is beautiful. LGTM, except a few nits.
src/node_api.cc
Outdated
v8::Local<v8::Object>* result) { | ||
auto wrapper_template = EnvObjectTemplate(env->isolate, obj_template, 2); | ||
auto maybe_object = | ||
wrapper_template->NewInstance(env->isolate->GetCurrentContext()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Four-space indentation please.
src/node_api.cc
Outdated
NAPI_EXTERN napi_status napi_conclude_deferred(napi_env env, | ||
napi_value deferred, | ||
napi_value conclusion, | ||
bool is_resolution) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer is_fulfilled
, as "resolution" can be interpreted as synonymous with "conclusion", generally speaking.
src/node_api.cc
Outdated
|
||
v8::Local<v8::Object> v8_deferred; | ||
CHECK_TO_OBJECT(env, context, v8_deferred, | ||
v8impl::JsValueFromV8LocalValue(js_deferred->GetInternalField(0))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indentation... and also a few lines below.
napi_property_descriptor descriptors[] = { | ||
DECLARE_NAPI_PROPERTY("createPromise", createPromise), | ||
DECLARE_NAPI_PROPERTY("concludeCurrentPromise", concludeCurrentPromise), | ||
DECLARE_NAPI_PROPERTY("isPromise", isPromise), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be an anti-case for passing a promise to napi_conclude_deferred()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR inspired me to think of a potentially better idea for object wrapping. Please take a look: #14367 |
doc/api/n-api.md
Outdated
Promises facilitate the construction of chains of asynchronous actions such | ||
that each action hinges on the outcome of the previous action. | ||
|
||
N-API implements promises as a pair of two objects - a "deferred" and a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe describe (quickly) their motivation? A "deferred" - which is in charge of resolving the promise
doc/api/n-api.md
Outdated
@@ -3155,6 +3156,81 @@ support it: | |||
* If the function is not available, provide an alternate implementation | |||
that does not use the function. | |||
|
|||
## Promises | |||
|
|||
Promises facilitate the construction of chains of asynchronous actions such |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is broad and I'm not sure it's very helpful to users.
I prefer a more informational tone about what promises are in N-API and not what they enable users to do.
"A promise represents an eventual value - the result of an asynchronous action. A promise can be await
ed on JavaScript's side or a callback could be attached to it".
The chaining aspect is interesting, but not really related to n-api from what I know.
doc/api/n-api.md
Outdated
``` | ||
|
||
- `[in] env`: The environment that the API is invoked under. | ||
- `[out] deferred`: The deferred object. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"A deferred object in charge of resolving the promise" perhaps?
doc/api/n-api.md
Outdated
|
||
Returns `napi_ok` if the API succeeded. | ||
|
||
This API creates a deferred object and a JavaScript promise. The two objects |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This paragraph repeats the one at the top a little (especially the start).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess I feel like I cannot sufficiently emphasize that it is not the promise one resolves, but the deferred.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that if we fix the motivation part above it will get a lot clearer.
doc/api/n-api.md
Outdated
the functionality of a JavaScript promise. However, the JavaScript promise | ||
returned in the third parameter can be passed into JavaScript. | ||
|
||
### napi_conclude_deferred |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A usage example would be amazing)
doc/api/n-api.md
Outdated
- `[in] env`: The environment that the API is invoked under. | ||
- `[in] deferred`: The deferred object whose associated promise to conclude. | ||
- `[in] conclusion`: The value with which to conclude the promise. | ||
- `[in] is_rejection`: Flag indicating whether `conclusion` is a resolution |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't it be better to have napi_promise_fulfill(env, deferred, value)
and napi_promise_reject(env, deferred, value)
?
When I think about it - why do we even need a deferred object at all?
|
||
- `[in] env`: The environment that the API is invoked under. | ||
- `[in] promise`: The promise to examine | ||
- `[out] is_promise`: Flag indicating whether `promise` is a native promise |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As someone who has helped maintain large promise libraries - I'm totally fine with making people Promise.resolve
promises they pass to napi_*
methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you mean by "I'm totally fine making people Promise.resolve
promises"? Do you mean that you don't mind if the native side resolves a promise created in JS and then passed to the native side?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gabrielschulhof I mean I'm fine with is_promise
only caring about and working with native promises - and ignoring non-native promises in n-api completely
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I started reviewing it - but I'm not sure about several more general things API wise in this PR - the API we're exposing seems complicated.
What about exporting the following instead?
napi_create_promise(env, *promise); // no deferred
napi_promise_resolve(env, *promise, *value);
napi_promise_reject(env, *promise, *reason);
We also need to consider what happens if a promise is passed as a fulfillment value (the state should be assimilated).
@benjamingr Some context about the deferred object is in an earlier iteration of the PR (#13717). +1 about nested promises though, going to need some tests for that. |
@benjamingr the V8 implementation provides two objects: a resolver and a promise. Now, it's true that they are in reality one and the same object, but that's an implementation detail that the API does not document. The v8 API certainly suggests that, given a random So, if somebody passes in a Now, if we hide the Thus, If we are not to covey the impression that |
@benjamingr another unpleasant side-effect of allowing one to resolve any promise natively is this: addon.acceptPromise( new Promise( function( resolve, reject ) {
...
resolve();
...
} ) ); and then napi_value the_passed_in_promise;
...
napi_promise_resolve(env, the_passed_in_promise, the_result); Now, the JS function responsible for resolving the promise is completely shoved aside. In fact, we don't know whether the promise will be resolved by the native side or by the JS side - depends on which finishes first. |
Yes, we can keep a map of native promises and resolvers for unresolved promises - when a promise is resolved we remove it from the map.
That's a good point, although we can return a
This is how promises work though. The first resolver wins - I'd assume someone writing that sort of code is using this as a feature and not running into it as a bug. |
Thanks a lot for working on this by the way - I'm sorry I'm not familiar with all the previous discussion and intend to read it asap. I care about having a convenient n_api promise interface because I intend to use it a lot :) |
Wait, what? Multiple resolvers? I mean, OK, Now, on the native side we do not have the luxury of using scope to at least discourage people from passing around the resolve and reject callbacks (collected in the deferred in the N-API implementation), but we still have the same concept of a single resolver being associated with a promise. That's why I believe it would be confusing if a user created a Promise in JS and found that its conclusion was not the result of having performed whatever was given in the executor.
We are already storing a lot of things in the Also, users who pass a JS-created native promise into I guess the fundamental questions are: Do we value the abstraction imposed by JS wherein upon receiving a Promise object from somewhere, the receiver cannot resolve it, but can only wait for its conclusion? Do we value that the promise can only be resolved by its executor, unless the user jumps through some (to-be-frowned-upon?) hoops of assigning the resolve and reject to the Promise instance? If so, then we should seek to encourage code that keeps native code from resolving JS promises, and keeps JS code from resolving native promises. |
Making new Promise((resolve, reject) => { // valid JS
resolve(3);
resolve(5); // noop
reject(null); // also noop, only the first one counts
}); This is by design, and in fact how you'd implement
Let me clarify - the JS side should not be able to resolve the promise, what I'm suggesting is returning the promise object to the JS side (it can't resolve it) and resolving it only on the V8 side (since V8 can resolve on promise objects - or we can keep a mapping).
Simple - don't allow resolving promises that were created outside of n-api (or ones you don't have in the map).
That sounds like a pretty good idea to me regardless .- similarly to JS not resolving these promises. (I think?)
I think we should.
These hooks were never frowned upon, the promise constructor was designed to run synchronously precisely so these use cases would be enabled. That said - I'm 100% for having JS users only resolve promises through the executor (and hopefully through Basically, my main issue with this PR is that we have an extra type for a capability (which is something that is often done in OO) - but we're writing C so we don't really need that - since simply access to Your point about this meaning we're able to resolve promises we did not create is very good which is why I think the map is a good idea. That said, having a deferred is nice because it converts a would-be runtime error to a compile time error. I'd like to think about having a nicer API for a day or two (we can also solve it with a resolvable-promise type - but I'm not sure that's better. |
7de46a6
to
7eef77d
Compare
Actually, it currently doesn't convert it to a compile time error because a deferred is currently simply a |
7eef77d
to
2ff291f
Compare
This would need to be rebased again, sorry. |
@addaleax NP. Rebasing when I get a chance. |
2ff291f
to
48f7deb
Compare
@benjamingr have you had a chance to think about a nicer API? Is it OK to proceed with this API? |
Oh sorry, I thought you were going to follow up with:
I didn't realize you were waiting for me - if you'd like I can sit on the API this week. |
8b35e05
to
b16ac9e
Compare
@addaleax Done. |
Addressing the failure of node-test-binary-arm: https://ci.nodejs.org/job/node-test-binary-arm/9844/ |
Landed in 7efb8f7. |
Promise is implemented as a pair of objects. `napi_create_promise()` returns both a JavaScript promise and a newly allocated "deferred" in its out-params. The deferred is linked to the promise such that the deferred can be passed to `napi_resolve_deferred()` or `napi_reject_deferred()` to reject/resolve the promise. `napi_is_promise()` can be used to check if a `napi_value` is a native promise - that is, a promise created by the underlying engine, rather than a pure JS implementation of a promise. PR-URL: #14365 Fixes: nodejs/abi-stable-node#242 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Timothy Gu <[email protected]>
Promise is implemented as a pair of objects. `napi_create_promise()` returns both a JavaScript promise and a newly allocated "deferred" in its out-params. The deferred is linked to the promise such that the deferred can be passed to `napi_resolve_deferred()` or `napi_reject_deferred()` to reject/resolve the promise. `napi_is_promise()` can be used to check if a `napi_value` is a native promise - that is, a promise created by the underlying engine, rather than a pure JS implementation of a promise. PR-URL: nodejs/node#14365 Fixes: nodejs/abi-stable-node#242 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Timothy Gu <[email protected]>
Promise is implemented as a pair of objects. `napi_create_promise()` returns both a JavaScript promise and a newly allocated "deferred" in its out-params. The deferred is linked to the promise such that the deferred can be passed to `napi_resolve_deferred()` or `napi_reject_deferred()` to reject/resolve the promise. `napi_is_promise()` can be used to check if a `napi_value` is a native promise - that is, a promise created by the underlying engine, rather than a pure JS implementation of a promise. PR-URL: nodejs/node#14365 Fixes: nodejs/abi-stable-node#242 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Timothy Gu <[email protected]>
Promise is implemented as a pair of objects. `napi_create_promise()` returns both a JavaScript promise and a newly allocated "deferred" in its out-params. The deferred is linked to the promise such that the deferred can be passed to `napi_resolve_deferred()` or `napi_reject_deferred()` to reject/resolve the promise. `napi_is_promise()` can be used to check if a `napi_value` is a native promise - that is, a promise created by the underlying engine, rather than a pure JS implementation of a promise. PR-URL: #14365 Fixes: nodejs/abi-stable-node#242 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Timothy Gu <[email protected]>
Promise is implemented as a pair of objects. `napi_create_promise()` returns both a JavaScript promise and a newly allocated "deferred" in its out-params. The deferred is linked to the promise such that the deferred can be passed to `napi_resolve_deferred()` or `napi_reject_deferred()` to reject/resolve the promise. `napi_is_promise()` can be used to check if a `napi_value` is a native promise - that is, a promise created by the underlying engine, rather than a pure JS implementation of a promise. PR-URL: #14365 Fixes: nodejs/abi-stable-node#242 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Timothy Gu <[email protected]>
Notable Changes * build: * Snapshots are now re-enabled in V8 #14875 * console: * Implement minimal `console.group()`. #14910 * deps: * upgrade libuv to 1.14.1 #14866 * update nghttp2 to v1.25.0 #14955 * dns: * Add `verbatim` option to dns.lookup(). When true, results from the DNS resolver are passed on as-is, without the reshuffling that Node.js otherwise does that puts IPv4 addresses before IPv6 addresses. #14731 * fs: * add fs.copyFile and fs.copyFileSync which allows for more efficient copying of files. #15034 * inspector: * Enable async stack traces #13870 * module: * Add support for ESM. This is currently behind the `--experimental-modules` flag and requires the .mjs extension. `node --experimental-modules index.mjs` #14369 * napi: * implement promise #14365 * os: * Add support for CIDR notation to the output of the networkInterfaces() method. #14307 * perf_hooks: * An initial implementation of the Performance Timing API for Node.js. This is the same Performance Timing API implemented by modern browsers with a number of Node.js specific properties. The User Timing mark() and measure() APIs are implemented, as is a Node.js specific flavor of the Frame Timing for measuring event loop duration. #14680 * tls: * multiple PFX in createSecureContext [#14793](#14793) * Added new collaborators: * BridgeAR – Ruben Bridgewater PR-URL: #15308
Notable Changes * build: * Snapshots are now re-enabled in V8 #14875 * console: * Implement minimal `console.group()`. #14910 * deps: * upgrade libuv to 1.14.1 #14866 * update nghttp2 to v1.25.0 #14955 * dns: * Add `verbatim` option to dns.lookup(). When true, results from the DNS resolver are passed on as-is, without the reshuffling that Node.js otherwise does that puts IPv4 addresses before IPv6 addresses. #14731 * fs: * add fs.copyFile and fs.copyFileSync which allows for more efficient copying of files. #15034 * inspector: * Enable async stack traces #13870 * module: * Add support for ESM. This is currently behind the `--experimental-modules` flag and requires the .mjs extension. `node --experimental-modules index.mjs` #14369 * napi: * implement promise #14365 * os: * Add support for CIDR notation to the output of the networkInterfaces() method. #14307 * perf_hooks: * An initial implementation of the Performance Timing API for Node.js. This is the same Performance Timing API implemented by modern browsers with a number of Node.js specific properties. The User Timing mark() and measure() APIs are implemented, as is a Node.js specific flavor of the Frame Timing for measuring event loop duration. #14680 * tls: * multiple PFX in createSecureContext [#14793](#14793) * Added new collaborators: * BridgeAR – Ruben Bridgewater PR-URL: #15308
Notable Changes * build: * Snapshots are now re-enabled in V8 nodejs#14875 * console: * Implement minimal `console.group()`. nodejs#14910 * deps: * upgrade libuv to 1.14.1 nodejs#14866 * update nghttp2 to v1.25.0 nodejs#14955 * dns: * Add `verbatim` option to dns.lookup(). When true, results from the DNS resolver are passed on as-is, without the reshuffling that Node.js otherwise does that puts IPv4 addresses before IPv6 addresses. nodejs#14731 * fs: * add fs.copyFile and fs.copyFileSync which allows for more efficient copying of files. nodejs#15034 * inspector: * Enable async stack traces nodejs#13870 * module: * Add support for ESM. This is currently behind the `--experimental-modules` flag and requires the .mjs extension. `node --experimental-modules index.mjs` nodejs#14369 * napi: * implement promise nodejs#14365 * os: * Add support for CIDR notation to the output of the networkInterfaces() method. nodejs#14307 * perf_hooks: * An initial implementation of the Performance Timing API for Node.js. This is the same Performance Timing API implemented by modern browsers with a number of Node.js specific properties. The User Timing mark() and measure() APIs are implemented, as is a Node.js specific flavor of the Frame Timing for measuring event loop duration. nodejs#14680 * tls: * multiple PFX in createSecureContext [nodejs#14793](nodejs#14793) * Added new collaborators: * BridgeAR – Ruben Bridgewater PR-URL: nodejs#15308
Promise is implemented as a pair of objects. `napi_create_promise()` returns both a JavaScript promise and a newly allocated "deferred" in its out-params. The deferred is linked to the promise such that the deferred can be passed to `napi_resolve_deferred()` or `napi_reject_deferred()` to reject/resolve the promise. `napi_is_promise()` can be used to check if a `napi_value` is a native promise - that is, a promise created by the underlying engine, rather than a pure JS implementation of a promise. PR-URL: nodejs#14365 Fixes: nodejs/abi-stable-node#242 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Timothy Gu <[email protected]>
Promise is implemented as a pair of objects. `napi_create_promise()` returns both a JavaScript promise and a newly allocated "deferred" in its out-params. The deferred is linked to the promise such that the deferred can be passed to `napi_resolve_deferred()` or `napi_reject_deferred()` to reject/resolve the promise. `napi_is_promise()` can be used to check if a `napi_value` is a native promise - that is, a promise created by the underlying engine, rather than a pure JS implementation of a promise. Backport-PR-URL: #19447 PR-URL: #14365 Fixes: nodejs/abi-stable-node#242 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Timothy Gu <[email protected]>
Promise is implemented as a pair of objects.
napi_create_promise()
returns both a JavaScript promise and a "deferred" in its out-params.
The deferred is linked to the promise such that the deferred can be
passed to
napi_conclude_deferred()
to rejct/resolve the promise. Thedeferred is a valid JavaScript value, but it is a shell object offering
no useful JavaScript functionality. In contrast, the promise returned
by
napi_create_promise()
is a full-fledged native JavaScript Promisewhich can be used for chaining in JavaScript.
napi_is_promise()
can be used to check if anapi_value
is a nativepromise - that is, a promise created by the underlying engine, rather
than a pure JS implementation of a promise.
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesAffected core subsystem(s)
n-api