Skip to content

Commit 503370e

Browse files
Gabriel SchulhofMylesBorins
Gabriel Schulhof
authored andcommitted
n-api: implement promise
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]>
1 parent 3f7bdc5 commit 503370e

File tree

7 files changed

+359
-0
lines changed

7 files changed

+359
-0
lines changed

doc/api/n-api.md

+138
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ The documentation for N-API is structured as follows:
4242
* [Working with JavaScript Functions][]
4343
* [Object Wrap][]
4444
* [Asynchronous Operations][]
45+
* [Promises][]
4546

4647
The N-API is a C API that ensures ABI stability across Node.js versions
4748
and different compiler levels. However, we also understand that a C++
@@ -3395,6 +3396,142 @@ support it:
33953396
33963397
<!-- it's very convenient to have all the anchors indexed -->
33973398
<!--lint disable no-unused-definitions remark-lint-->
3399+
## Promises
3400+
3401+
N-API provides facilities for creating `Promise` objects as described in
3402+
[Section 25.4][] of the ECMA specification. It implements promises as a pair of
3403+
objects. When a promise is created by `napi_create_promise()`, a "deferred"
3404+
object is created and returned alongside the `Promise`. The deferred object is
3405+
bound to the created `Promise` and is the only means to resolve or reject the
3406+
`Promise` using `napi_resolve_deferred()` or `napi_reject_deferred()`. The
3407+
deferred object that is created by `napi_create_promise()` is freed by
3408+
`napi_resolve_deferred()` or `napi_reject_deferred()`. The `Promise` object may
3409+
be returned to JavaScript where it can be used in the usual fashion.
3410+
3411+
For example, to create a promise and pass it to an asynchronous worker:
3412+
```c
3413+
napi_deferred deferred;
3414+
napi_value promise;
3415+
napi_status status;
3416+
3417+
// Create the promise.
3418+
status = napi_create_promise(env, &deferred, &promise);
3419+
if (status != napi_ok) return NULL;
3420+
3421+
// Pass the deferred to a function that performs an asynchronous action.
3422+
do_something_asynchronous(deferred);
3423+
3424+
// Return the promise to JS
3425+
return promise;
3426+
```
3427+
3428+
The above function `do_something_asynchronous()` would perform its asynchronous
3429+
action and then it would resolve or reject the deferred, thereby concluding the
3430+
promise and freeing the deferred:
3431+
```c
3432+
napi_deferred deferred;
3433+
napi_value undefined;
3434+
napi_status status;
3435+
3436+
// Create a value with which to conclude the deferred.
3437+
status = napi_get_undefined(env, &undefined);
3438+
if (status != napi_ok) return NULL;
3439+
3440+
// Resolve or reject the promise associated with the deferred depending on
3441+
// whether the asynchronous action succeeded.
3442+
if (asynchronous_action_succeeded) {
3443+
status = napi_resolve_deferred(env, deferred, undefined);
3444+
} else {
3445+
status = napi_reject_deferred(env, deferred, undefined);
3446+
}
3447+
if (status != napi_ok) return NULL;
3448+
3449+
// At this point the deferred has been freed, so we should assign NULL to it.
3450+
deferred = NULL;
3451+
```
3452+
3453+
### napi_create_promise
3454+
<!-- YAML
3455+
added: REPLACEME
3456+
-->
3457+
```C
3458+
NAPI_EXTERN napi_status napi_create_promise(napi_env env,
3459+
napi_deferred* deferred,
3460+
napi_value* promise);
3461+
```
3462+
3463+
- `[in] env`: The environment that the API is invoked under.
3464+
- `[out] deferred`: A newly created deferred object which can later be passed to
3465+
`napi_resolve_deferred()` or `napi_reject_deferred()` to resolve resp. reject
3466+
the associated promise.
3467+
- `[out] promise`: The JavaScript promise associated with the deferred object.
3468+
3469+
Returns `napi_ok` if the API succeeded.
3470+
3471+
This API creates a deferred object and a JavaScript promise.
3472+
3473+
### napi_resolve_deferred
3474+
<!-- YAML
3475+
added: REPLACEME
3476+
-->
3477+
```C
3478+
NAPI_EXTERN napi_status napi_resolve_deferred(napi_env env,
3479+
napi_deferred deferred,
3480+
napi_value resolution);
3481+
```
3482+
3483+
- `[in] env`: The environment that the API is invoked under.
3484+
- `[in] deferred`: The deferred object whose associated promise to resolve.
3485+
- `[in] resolution`: The value with which to resolve the promise.
3486+
3487+
This API resolves a JavaScript promise by way of the deferred object
3488+
with which it is associated. Thus, it can only be used to resolve JavaScript
3489+
promises for which the corresponding deferred object is available. This
3490+
effectively means that the promise must have been created using
3491+
`napi_create_promise()` and the deferred object returned from that call must
3492+
have been retained in order to be passed to this API.
3493+
3494+
The deferred object is freed upon successful completion.
3495+
3496+
### napi_reject_deferred
3497+
<!-- YAML
3498+
added: REPLACEME
3499+
-->
3500+
```C
3501+
NAPI_EXTERN napi_status napi_reject_deferred(napi_env env,
3502+
napi_deferred deferred,
3503+
napi_value rejection);
3504+
```
3505+
3506+
- `[in] env`: The environment that the API is invoked under.
3507+
- `[in] deferred`: The deferred object whose associated promise to resolve.
3508+
- `[in] rejection`: The value with which to reject the promise.
3509+
3510+
This API rejects a JavaScript promise by way of the deferred object
3511+
with which it is associated. Thus, it can only be used to reject JavaScript
3512+
promises for which the corresponding deferred object is available. This
3513+
effectively means that the promise must have been created using
3514+
`napi_create_promise()` and the deferred object returned from that call must
3515+
have been retained in order to be passed to this API.
3516+
3517+
The deferred object is freed upon successful completion.
3518+
3519+
### napi_is_promise
3520+
<!-- YAML
3521+
added: REPLACEME
3522+
-->
3523+
```C
3524+
NAPI_EXTERN napi_status napi_is_promise(napi_env env,
3525+
napi_value promise,
3526+
bool* is_promise);
3527+
```
3528+
3529+
- `[in] env`: The environment that the API is invoked under.
3530+
- `[in] promise`: The promise to examine
3531+
- `[out] is_promise`: Flag indicating whether `promise` is a native promise
3532+
object - that is, a promise object created by the underlying engine.
3533+
3534+
[Promises]: #n_api_promises
33983535
[Asynchronous Operations]: #n_api_asynchronous_operations
33993536
[Basic N-API Data Types]: #n_api_basic_n_api_data_types
34003537
[ECMAScript Language Specification]: https://tc39.github.io/ecma262/
@@ -3406,6 +3543,7 @@ support it:
34063543
[Section 9.1.6]: https://tc39.github.io/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots-defineownproperty-p-desc
34073544
[Section 12.5.5]: https://tc39.github.io/ecma262/#sec-typeof-operator
34083545
[Section 24.3]: https://tc39.github.io/ecma262/#sec-dataview-objects
3546+
[Section 25.4]: https://tc39.github.io/ecma262/#sec-promise-objects
34093547
[Working with JavaScript Functions]: #n_api_working_with_javascript_functions
34103548
[Working with JavaScript Properties]: #n_api_working_with_javascript_properties
34113549
[Working with JavaScript Values]: #n_api_working_with_javascript_values

src/node_api.cc

+78
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,14 @@ V8EscapableHandleScopeFromJsEscapableHandleScope(
218218
static_assert(sizeof(v8::Local<v8::Value>) == sizeof(napi_value),
219219
"Cannot convert between v8::Local<v8::Value> and napi_value");
220220

221+
napi_deferred JsDeferredFromV8Persistent(v8::Persistent<v8::Value>* local) {
222+
return reinterpret_cast<napi_deferred>(local);
223+
}
224+
225+
v8::Persistent<v8::Value>* V8PersistentFromJsDeferred(napi_deferred local) {
226+
return reinterpret_cast<v8::Persistent<v8::Value>*>(local);
227+
}
228+
221229
napi_value JsValueFromV8LocalValue(v8::Local<v8::Value> local) {
222230
return reinterpret_cast<napi_value>(*local);
223231
}
@@ -774,6 +782,33 @@ napi_status Unwrap(napi_env env,
774782
return napi_ok;
775783
}
776784

785+
napi_status ConcludeDeferred(napi_env env,
786+
napi_deferred deferred,
787+
napi_value result,
788+
bool is_resolved) {
789+
NAPI_PREAMBLE(env);
790+
CHECK_ARG(env, result);
791+
792+
v8::Local<v8::Context> context = env->isolate->GetCurrentContext();
793+
v8::Persistent<v8::Value>* deferred_ref =
794+
V8PersistentFromJsDeferred(deferred);
795+
v8::Local<v8::Value> v8_deferred =
796+
v8::Local<v8::Value>::New(env->isolate, *deferred_ref);
797+
798+
auto v8_resolver = v8::Local<v8::Promise::Resolver>::Cast(v8_deferred);
799+
800+
v8::Maybe<bool> success = is_resolved ?
801+
v8_resolver->Resolve(context, v8impl::V8LocalValueFromJsValue(result)) :
802+
v8_resolver->Reject(context, v8impl::V8LocalValueFromJsValue(result));
803+
804+
deferred_ref->Reset();
805+
delete deferred_ref;
806+
807+
RETURN_STATUS_IF_FALSE(env, success.FromMaybe(false), napi_generic_failure);
808+
809+
return GET_RETURN_STATUS(env);
810+
}
811+
777812
} // end of namespace v8impl
778813

779814
// Intercepts the Node-V8 module registration callback. Converts parameters
@@ -3332,3 +3367,46 @@ napi_status napi_cancel_async_work(napi_env env, napi_async_work work) {
33323367

33333368
return napi_clear_last_error(env);
33343369
}
3370+
3371+
NAPI_EXTERN napi_status napi_create_promise(napi_env env,
3372+
napi_deferred* deferred,
3373+
napi_value* promise) {
3374+
NAPI_PREAMBLE(env);
3375+
CHECK_ARG(env, deferred);
3376+
CHECK_ARG(env, promise);
3377+
3378+
auto maybe = v8::Promise::Resolver::New(env->isolate->GetCurrentContext());
3379+
CHECK_MAYBE_EMPTY(env, maybe, napi_generic_failure);
3380+
3381+
auto v8_resolver = maybe.ToLocalChecked();
3382+
auto v8_deferred = new v8::Persistent<v8::Value>();
3383+
v8_deferred->Reset(env->isolate, v8_resolver);
3384+
3385+
*deferred = v8impl::JsDeferredFromV8Persistent(v8_deferred);
3386+
*promise = v8impl::JsValueFromV8LocalValue(v8_resolver->GetPromise());
3387+
return GET_RETURN_STATUS(env);
3388+
}
3389+
3390+
NAPI_EXTERN napi_status napi_resolve_deferred(napi_env env,
3391+
napi_deferred deferred,
3392+
napi_value resolution) {
3393+
return v8impl::ConcludeDeferred(env, deferred, resolution, true);
3394+
}
3395+
3396+
NAPI_EXTERN napi_status napi_reject_deferred(napi_env env,
3397+
napi_deferred deferred,
3398+
napi_value resolution) {
3399+
return v8impl::ConcludeDeferred(env, deferred, resolution, false);
3400+
}
3401+
3402+
NAPI_EXTERN napi_status napi_is_promise(napi_env env,
3403+
napi_value promise,
3404+
bool* is_promise) {
3405+
CHECK_ENV(env);
3406+
CHECK_ARG(env, promise);
3407+
CHECK_ARG(env, is_promise);
3408+
3409+
*is_promise = v8impl::V8LocalValueFromJsValue(promise)->IsPromise();
3410+
3411+
return napi_clear_last_error(env);
3412+
}

src/node_api.h

+14
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,20 @@ NAPI_EXTERN
543543
napi_status napi_get_node_version(napi_env env,
544544
const napi_node_version** version);
545545

546+
// Promises
547+
NAPI_EXTERN napi_status napi_create_promise(napi_env env,
548+
napi_deferred* deferred,
549+
napi_value* promise);
550+
NAPI_EXTERN napi_status napi_resolve_deferred(napi_env env,
551+
napi_deferred deferred,
552+
napi_value resolution);
553+
NAPI_EXTERN napi_status napi_reject_deferred(napi_env env,
554+
napi_deferred deferred,
555+
napi_value rejection);
556+
NAPI_EXTERN napi_status napi_is_promise(napi_env env,
557+
napi_value promise,
558+
bool* is_promise);
559+
546560
EXTERN_C_END
547561

548562
#endif // SRC_NODE_API_H_

src/node_api_types.h

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ typedef struct napi_handle_scope__ *napi_handle_scope;
1717
typedef struct napi_escapable_handle_scope__ *napi_escapable_handle_scope;
1818
typedef struct napi_callback_info__ *napi_callback_info;
1919
typedef struct napi_async_work__ *napi_async_work;
20+
typedef struct napi_deferred__ *napi_deferred;
2021

2122
typedef enum {
2223
napi_default = 0,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"targets": [
3+
{
4+
"target_name": "test_promise",
5+
"sources": [ "test_promise.c" ]
6+
}
7+
]
8+
}

test/addons-napi/test_promise/test.js

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
'use strict';
2+
3+
const common = require('../../common');
4+
const test_promise = require(`./build/${common.buildType}/test_promise`);
5+
const assert = require('assert');
6+
7+
let expected_result, promise;
8+
9+
// A resolution
10+
expected_result = 42;
11+
promise = test_promise.createPromise();
12+
promise.then(
13+
common.mustCall(function(result) {
14+
assert.strictEqual(result, expected_result,
15+
'promise resolved as expected');
16+
}),
17+
common.mustNotCall());
18+
test_promise.concludeCurrentPromise(expected_result, true);
19+
20+
// A rejection
21+
expected_result = 'It\'s not you, it\'s me.';
22+
promise = test_promise.createPromise();
23+
promise.then(
24+
common.mustNotCall(),
25+
common.mustCall(function(result) {
26+
assert.strictEqual(result, expected_result,
27+
'promise rejected as expected');
28+
}));
29+
test_promise.concludeCurrentPromise(expected_result, false);
30+
31+
// Chaining
32+
promise = test_promise.createPromise();
33+
promise.then(
34+
common.mustCall(function(result) {
35+
assert.strictEqual(result, 'chained answer',
36+
'resolving with a promise chains properly');
37+
}),
38+
common.mustNotCall());
39+
test_promise.concludeCurrentPromise(Promise.resolve('chained answer'), true);
40+
41+
assert.strictEqual(test_promise.isPromise(promise), true,
42+
'natively created promise is recognized as a promise');
43+
44+
assert.strictEqual(test_promise.isPromise(Promise.reject(-1)), true,
45+
'Promise created with JS is recognized as a promise');
46+
47+
assert.strictEqual(test_promise.isPromise(2.4), false,
48+
'Number is recognized as not a promise');
49+
50+
assert.strictEqual(test_promise.isPromise('I promise!'), false,
51+
'String is recognized as not a promise');
52+
53+
assert.strictEqual(test_promise.isPromise(undefined), false,
54+
'undefined is recognized as not a promise');
55+
56+
assert.strictEqual(test_promise.isPromise(null), false,
57+
'null is recognized as not a promise');
58+
59+
assert.strictEqual(test_promise.isPromise({}), false,
60+
'an object is recognized as not a promise');

0 commit comments

Comments
 (0)