Skip to content

Commit

Permalink
Implement Promises.
Browse files Browse the repository at this point in the history
PR-URL: nodejs/node-addon-api#137
Reviewed-By: Anna Henningsen <[email protected]>
Reviewed-By: Michael Dawson <[email protected]>
Reviewed-By: Jason Ginchereau <[email protected]>
  • Loading branch information
kevindavies8 committed Oct 27, 2017
1 parent 3462b4b commit 938fde4
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 0 deletions.
41 changes: 41 additions & 0 deletions napi-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,17 @@ inline bool Value::IsFunction() const {
return Type() == napi_function;
}

inline bool Value::IsPromise() const {
if (_value == nullptr) {
return false;
}

bool result;
napi_status status = napi_is_promise(_env, _value, &result);
NAPI_THROW_IF_FAILED(_env, status, false);
return result;
}

inline bool Value::IsBuffer() const {
if (_value == nullptr) {
return false;
Expand Down Expand Up @@ -1379,6 +1390,36 @@ inline Object Function::New(size_t argc, const napi_value* args) const {
return Object(_env, result);
}

////////////////////////////////////////////////////////////////////////////////
// Promise class
////////////////////////////////////////////////////////////////////////////////

inline Promise::Deferred Promise::Deferred::New(napi_env env) {
return Promise::Deferred(env);
}

inline Promise::Deferred::Deferred(napi_env env) : _env(env) {
napi_status status = napi_create_promise(_env, &_deferred, &_promise);
NAPI_THROW_IF_FAILED(_env, status);
}

inline Promise Promise::Deferred::Promise() const {
return Napi::Promise(_env, _promise);
}

inline void Promise::Deferred::Resolve(napi_value value) const {
napi_status status = napi_resolve_deferred(_env, _deferred, value);
NAPI_THROW_IF_FAILED(_env, status);
}

inline void Promise::Deferred::Reject(napi_value value) const {
napi_status status = napi_reject_deferred(_env, _deferred, value);
NAPI_THROW_IF_FAILED(_env, status);
}

inline Promise::Promise(napi_env env, napi_value value) : Object(env, value) {
}

////////////////////////////////////////////////////////////////////////////////
// Buffer<T> class
////////////////////////////////////////////////////////////////////////////////
Expand Down
22 changes: 22 additions & 0 deletions napi.h
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ namespace Napi {
bool IsTypedArray() const; ///< Tests if a value is a JavaScript typed array.
bool IsObject() const; ///< Tests if a value is a JavaScript object.
bool IsFunction() const; ///< Tests if a value is a JavaScript function.
bool IsPromise() const; ///< Tests if a value is a JavaScript promise.
bool IsBuffer() const; ///< Tests if a value is a Node buffer.

/// Casts to another type of `Napi::Value`, when the actual type is known or assumed.
Expand Down Expand Up @@ -792,6 +793,27 @@ namespace Napi {
Object New(size_t argc, const napi_value* args) const;
};

class Promise : public Object {
public:
class Deferred {
public:
static Deferred New(napi_env env);
Deferred(napi_env env);

Napi::Promise Promise() const;

void Resolve(napi_value value) const;
void Reject(napi_value value) const;

private:
napi_env _env;
napi_deferred _deferred;
napi_value _promise;
};

Promise(napi_env env, napi_value value);
};

template <typename T>
class Buffer : public Object {
public:
Expand Down
2 changes: 2 additions & 0 deletions test/binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Object InitExternal(Env env);
Object InitFunction(Env env);
Object InitName(Env env);
Object InitObject(Env env);
Object InitPromise(Env env);
Object InitTypedArray(Env env);
Object InitObjectWrap(Env env);

Expand All @@ -22,6 +23,7 @@ Object Init(Env env, Object exports) {
exports.Set("function", InitFunction(env));
exports.Set("name", InitName(env));
exports.Set("object", InitObject(env));
exports.Set("promise", InitPromise(env));
exports.Set("typedarray", InitTypedArray(env));
exports.Set("objectwrap", InitObjectWrap(env));
return exports;
Expand Down
1 change: 1 addition & 0 deletions test/binding.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
'function.cc',
'name.cc',
'object.cc',
'promise.cc',
'typedarray.cc',
'objectwrap.cc',
],
Expand Down
70 changes: 70 additions & 0 deletions test/common/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/* Test helpers ported from test/common/index.js in Node.js project. */
'use strict';
const assert = require('assert');

const noop = () => {};

const mustCallChecks = [];

function runCallChecks(exitCode) {
if (exitCode !== 0) return;

const failed = mustCallChecks.filter(function(context) {
if ('minimum' in context) {
context.messageSegment = `at least ${context.minimum}`;
return context.actual < context.minimum;
} else {
context.messageSegment = `exactly ${context.exact}`;
return context.actual !== context.exact;
}
});

failed.forEach(function(context) {
console.log('Mismatched %s function calls. Expected %s, actual %d.',
context.name,
context.messageSegment,
context.actual);
console.log(context.stack.split('\n').slice(2).join('\n'));
});

if (failed.length) process.exit(1);
}

exports.mustCall = function(fn, exact) {
return _mustCallInner(fn, exact, 'exact');
};

function _mustCallInner(fn, criteria = 1, field) {
if (typeof fn === 'number') {
criteria = fn;
fn = noop;
} else if (fn === undefined) {
fn = noop;
}

if (typeof criteria !== 'number')
throw new TypeError(`Invalid ${field} value: ${criteria}`);

const context = {
[field]: criteria,
actual: 0,
stack: (new Error()).stack,
name: fn.name || '<anonymous>'
};

// add the exit listener only once to avoid listener leak warnings
if (mustCallChecks.length === 0) process.on('exit', runCallChecks);

mustCallChecks.push(context);

return function() {
context.actual++;
return fn.apply(this, arguments);
};
}

exports.mustNotCall = function(msg) {
return function mustNotCall() {
assert.fail(msg || 'function should not have been called');
};
};
1 change: 1 addition & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ let testModules = [
'function',
'name',
'object',
'promise',
'typedarray',
'objectwrap'
];
Expand Down
29 changes: 29 additions & 0 deletions test/promise.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#include "napi.h"

using namespace Napi;

Value IsPromise(const CallbackInfo& info) {
return Boolean::New(info.Env(), info[0].IsPromise());
}

Value ResolvePromise(const CallbackInfo& info) {
auto deferred = Promise::Deferred::New(info.Env());
deferred.Resolve(info[0]);
return deferred.Promise();
}

Value RejectPromise(const CallbackInfo& info) {
auto deferred = Promise::Deferred::New(info.Env());
deferred.Reject(info[0]);
return deferred.Promise();
}

Object InitPromise(Env env) {
Object exports = Object::New(env);

exports["isPromise"] = Function::New(env, IsPromise);
exports["resolvePromise"] = Function::New(env, ResolvePromise);
exports["rejectPromise"] = Function::New(env, RejectPromise);

return exports;
}
19 changes: 19 additions & 0 deletions test/promise.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use strict';
const buildType = process.config.target_defaults.default_configuration;
const assert = require('assert');
const common = require('./common');

test(require(`./build/${buildType}/binding.node`));
test(require(`./build/${buildType}/binding_noexcept.node`));

function test(binding) {
assert.strictEqual(binding.promise.isPromise({}), false);

const resolving = binding.promise.resolvePromise('resolved');
assert.strictEqual(binding.promise.isPromise(resolving), true);
resolving.then(common.mustCall()).catch(common.mustNotCall());

const rejecting = binding.promise.rejectPromise('error');
assert.strictEqual(binding.promise.isPromise(rejecting), true);
rejecting.then(common.mustNotCall()).catch(common.mustCall());
}

0 comments on commit 938fde4

Please sign in to comment.