Skip to content

N-api (node-addon-api) #161

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

Merged
merged 60 commits into from
Apr 3, 2020
Merged
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
12f48ef
N-API port initial implementation (WIP)
artemp Nov 16, 2018
8e796ce
clang-format
artemp Nov 16, 2018
54e1cac
default init `name_`
artemp Nov 16, 2018
8a5fe79
default construct `name_` in initialiser list to address :
artemp Nov 16, 2018
f762f77
clang-format
artemp Nov 16, 2018
da9be93
mode clang-format
artemp Nov 16, 2018
1c8d735
remove extra init (tidy)
artemp Nov 19, 2018
5727b02
make ctor explicit
artemp Nov 19, 2018
85d952b
pass Function by const-ref
artemp Nov 19, 2018
6219f17
annotate with NOLINT + fix passing Function by const-ref in standalon…
artemp Nov 19, 2018
45da8d2
clang-format
artemp Nov 19, 2018
9843b9a
default 'in-class' init member variable `name_` to placate g++
artemp Nov 19, 2018
4df69fc
remove LTO flags on linux builds
artemp Nov 21, 2018
ac12e37
Revert "remove LTO flags on linux builds" - doh!
artemp Nov 21, 2018
4ee2e66
Merge branch 'master' into n-api
artemp Nov 23, 2018
bfe8b4c
fix .clang-format
artemp Nov 26, 2018
27b9d1d
reimplement CPU intensive task and factor out into separate header
artemp Nov 26, 2018
ae1df1d
more cleaning up
artemp Nov 26, 2018
f56151f
remove redundant `override` c++11
artemp Nov 26, 2018
8aa7626
fix comment
artemp Mar 26, 2020
b95d393
Make simpler
artemp Mar 27, 2020
6854cf2
Update npm packages
artemp Mar 30, 2020
b914a10
add link to docs
artemp Mar 30, 2020
b10c91b
Remove Nan references + update link
artemp Mar 30, 2020
17de6e2
remove node-6 builds
artemp Mar 30, 2020
6258ccf
Allocate new Buffer and copy data into it.
artemp Mar 30, 2020
2277119
Add empty dtor to avoid memory leaks + make AsyncHelloWorker non-copy…
artemp Mar 30, 2020
0e9c970
clang-format
artemp Mar 30, 2020
8f37517
clang-tidy
artemp Mar 30, 2020
ea29acc
require mode-addon-api >= 2.0.0
artemp Mar 30, 2020
d4e6284
Take ownership of underlying data (NOTE: clang-tidy doesn't like this:)
artemp Mar 30, 2020
952946f
Add missing return statements
artemp Mar 31, 2020
2accc64
Use latest (HEAD) hode-addon-api module (fixes SIGILL crash in "Contr…
artemp Mar 31, 2020
79f2507
Fix test - use exact error message match
artemp Mar 31, 2020
88f37e5
sleep for 100ms to speed up tests running time
artemp Mar 31, 2020
586e60f
Store name_ member variable by value + clang tidy
artemp Mar 31, 2020
e769844
clang-format
artemp Mar 31, 2020
2520ace
Initialize result_
artemp Mar 31, 2020
cb186eb
c++ style
artemp Mar 31, 2020
6d08efe
Return std::unique_ptr<std::vector<char>> and stop abusing std::strin…
artemp Apr 1, 2020
4f5883f
update to use std::vector<char> + add gsl::owner<T> implementation +…
artemp Apr 1, 2020
bd6bdb6
Only target node v10 and v12
artemp Apr 1, 2020
eaa4fbf
tidy .travis.yml
artemp Apr 1, 2020
9eb4ab5
Revert "tidy .travis.yml"
artemp Apr 1, 2020
0c6c1fb
Revert "Only target node v10 and v12"
artemp Apr 1, 2020
3c8dfb6
Fix (hopefully) .travis.yml syntax
artemp Apr 1, 2020
826dd48
Update node versions
artemp Apr 1, 2020
c926694
Add AsyncHelloWorker_v2 implementation which uses `GetResult` method …
artemp Apr 1, 2020
d2cb818
add node v13 targets
artemp Apr 1, 2020
885b623
clang-format
artemp Apr 1, 2020
11e9edb
Don't rely on order of args evaluation which is implemetation defined…
artemp Apr 2, 2020
65da09a
Makefile - better target name
artemp Apr 2, 2020
7c717d9
Update link [skip ci]
artemp Apr 2, 2020
5cb4747
attempt to fix coverage problem
Apr 2, 2020
e88d774
suppress OS X specific leak
Apr 2, 2020
72cc593
remove references to nan in readme
Apr 2, 2020
8cb4609
Merge remote-tracking branch 'origin/master' into n-api
artemp Apr 3, 2020
da256b3
avoid re-running npm install when no needed
Apr 3, 2020
a454a21
use correct mason-js module
Apr 3, 2020
4a013d3
update deps
Apr 3, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .clang-format
Original file line number Diff line number Diff line change
@@ -33,7 +33,7 @@ BraceWrapping:
BeforeElse: true
IndentBraces: false
BreakBeforeBinaryOperators: None
BreakBeforeBraces: Attach
BreakBeforeBraces: Custom
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: false
ColumnLimit: 0
@@ -72,7 +72,7 @@ PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 60
PointerAlignment: Left
ReflowComments: true
SortIncludes: true
SortIncludes: false
SpaceAfterCStyleCast: false
SpaceBeforeAssignmentOperators: true
SpaceBeforeParens: ControlStatements
40 changes: 32 additions & 8 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -2,11 +2,10 @@ language: node_js

sudo: false

# enable c++11/14 builds
addons:
apt:
sources: [ 'ubuntu-toolchain-r-test' ]
packages: [ 'libstdc++-4.9-dev' ]
packages: [ 'libstdc++-5-dev' ]

install:
- node -v
@@ -35,7 +34,7 @@ matrix:

## ** Builds that are published **

# linux cfi build node v6/release
# linux cfi build node v10/release
- os: linux
env: BUILDTYPE=release TOOLSET=cfi CXXFLAGS="-fsanitize=cfi -fvisibility=hidden" LDFLAGS="-fsanitize=cfi"
node_js: 10
@@ -51,6 +50,22 @@ matrix:
- os: linux
env: BUILDTYPE=debug
node_js: 10
# linux publishable node v12/release
- os: linux
env: BUILDTYPE=release
node_js: 12
# linux publishable node v12/debug
- os: linux
env: BUILDTYPE=debug
node_js: 12
# linux publishable node v13/release
- os: linux
env: BUILDTYPE=release
node_js: 13
# linux publishable node v13/debug
- os: linux
env: BUILDTYPE=debug
node_js: 13
# osx publishable node v10/release
- os: osx
osx_image: xcode9.3
@@ -61,7 +76,7 @@ matrix:
osx_image: xcode9.3
env: BUILDTYPE=release
node_js: 12
# linux sanitizer build node v6/debug
# linux sanitizer build node v10/debug
- os: linux
env: BUILDTYPE=debug TOOLSET=asan
node_js: 10
@@ -71,7 +86,8 @@ matrix:
- make sanitize
# Overrides `before_script` (tests are already run in `make sanitize`)
before_script:
# osx sanitizer build node v6/debug
- true
# osx sanitizer build node v10/debug
- os: osx
env: BUILDTYPE=debug TOOLSET=asan
node_js: 10
@@ -81,7 +97,7 @@ matrix:
- make sanitize
# Overrides `before_script` (tests are already run in `make sanitize`)
before_script:

- true
## ** Builds that do not get published **

# g++ build (default builds all use clang++)
@@ -100,6 +116,7 @@ matrix:
- make ${BUILDTYPE}
# Overrides `script` to disable publishing
script:
- true
# Coverage build
- os: linux
env: BUILDTYPE=debug CXXFLAGS="--coverage" LDFLAGS="--coverage"
@@ -108,8 +125,11 @@ matrix:
script:
- export PATH=$(pwd)/mason_packages/.link/bin/:${PATH}
- which llvm-cov
- pip install --user codecov
- codecov --gcov-exec "llvm-cov gcov -l"
- curl -S -f https://codecov.io/bash -o codecov
- chmod +x codecov
# Workaround until we can avoid problem after https://github.com/travis-ci/travis-build/pull/1263 lands
- PATH=$(echo "$PATH" | sed 's/.\/node_modules\/.bin://')
- ./codecov -x "llvm-cov gcov" -Z
# Clang format build
- os: linux
# can be generic since we don't need nodejs to run formatting
@@ -123,8 +143,10 @@ matrix:
- make format
# Overrides `before_script`, no need to run tests
before_script:
- true
# Overrides `script` to disable publishing
script:
- true
# Clang tidy build
- os: linux
env: CLANG_TIDY
@@ -138,5 +160,7 @@ matrix:
- make tidy
# Overrides `before_script`, no need to run tests
before_script:
- true
# Overrides `script` to disable publishing
script:
- true
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -24,10 +24,10 @@ export WERROR ?= true
# just typing `make` will call `make release`
default: release

node_modules/nan:
node_modules/node-addon-api:
npm install --ignore-scripts

mason_packages/headers: node_modules/nan
mason_packages/headers: node_modules/node-addon-api
node_modules/.bin/mason-js install

mason_packages/.link/include: mason_packages/headers
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
![dancing skel](https://mapbox.s3.amazonaws.com/cpp-assets/node-cpp-skel-readme_blue.png)
[![Build Status](https://travis-ci.org/mapbox/node-cpp-skel.svg?branch=master)](https://travis-ci.org/mapbox/node-cpp-skel)
[![codecov](https://codecov.io/gh/mapbox/node-cpp-skel/branch/master/graph/badge.svg)](https://codecov.io/gh/mapbox/node-cpp-skel)

A skeleton for binding C++ libraries to Node.js using [Nan](https://github.com/nodejs/nan). This is a small, helper repository that generates simple `HelloWorld` Javascript example constructors. The examples have a number of methods to demonstrate different ways to use Nan for building particular types of functionality (i.e. asynchronous functions). Use this repo as both a template for your own code as well as a learning tool if you're just starting to develop Node/C++ Addons.
A skeleton for building a C++ addon for Node.js. This is a small, helper repository that generates simple `HelloWorld` Javascript example constructors. The examples have a number of methods to demonstrate different ways to use the Node C+ API for building particular types of functionality (i.e. asynchronous functions). Use this repo as both a template for your own code as well as a learning tool if you're just starting to develop Node/C++ Addons.

**Why port C++ to Node.js?**. That's a great question! C++ is a high performance language that allows you to execute operations without clogging up the event loop. Node.js is single-threaded, which blocks execution. Even in highly optimized javascript code it may be impossible to improve performance. Passing heavy operations into C++ and subsequently into C++ workers can greatly improve the overall runtime of the code. Porting C++ code to Node.js is also referred to as creating an ["Addon"](https://github.com/mapbox/cpp/blob/master/node-cpp.md).

**Nan**: Nan is used in many C++ => Node.js port projects, such as [node-mapnik](https://github.com/mapnik/node-mapnik), [node-osrm](https://github.com/Project-OSRM/node-osrm), and [node-osmium](https://github.com/osmcode/node-osmium). More examples of how to port C++ libraries to node can be found at [nodejs.org/api/addons.html](https://nodejs.org/api/addons.html). See https://nodesource.com/blog/c-add-ons-for-nodejs-v4/ for a detailed summary of the origins of Nan.

[![Build Status](https://travis-ci.org/mapbox/node-cpp-skel.svg?branch=master)](https://travis-ci.org/mapbox/node-cpp-skel)
[![codecov](https://codecov.io/gh/mapbox/node-cpp-skel/branch/master/graph/badge.svg)](https://codecov.io/gh/mapbox/node-cpp-skel)
More examples of how to port C++ libraries to node can be found at [nodejs.org/api/addons.html](https://nodejs.org/api/addons.html).

# What's in the box? :package:

2 changes: 1 addition & 1 deletion binding.gyp
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@
# It's a variable to make easy to pass to
# cflags (linux) and xcode (mac)
'system_includes': [
"-isystem <(module_root_dir)/<!(node -e \"require('nan')\")",
"-isystem <!@(node -p \"require('node-addon-api').include.slice(1,-1)\")",
"-isystem <(module_root_dir)/mason_packages/.link/include/"
],
# Flags we pass to the compiler to ensure the compiler
8 changes: 4 additions & 4 deletions docs/extended-tour.md
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@ This skeleton includes a few examples of how you might design your application,


### When would you use a standalone function?
A standalone function is a function that exists at the top level of the module scope rather than as a member of an object that is instantiated. So if your module is `wonderful`, a standalone function would be called like `wonderful.callme()`.
A standalone function is a function that exists at the top level of the module scope rather than as a member of an object that is instantiated. So if your module is `wonderful`, a standalone function would be called like `wonderful.callme()`.

A standalone function makes sense when the only data needed by the function can be easily passed as arguments. When it is not easy or clean to pass data as arguments then you should consider encapsulation, for example, exposing a function as a member of an object. One of the benefits of creating a standalone function is that it can help [separate concerns](https://en.wikipedia.org/wiki/Separation_of_concerns), which is mainly a stylistic design decision.

@@ -32,12 +32,12 @@ Example:
### When would you use an object/class?
Create an object/class when you need to do some kind of data preprocessing before going into the thread pool. It's best to write your code so that the preprocessing happens _once_ as a separate operation, then continues through to the thread pool after the preprocessing is finished and the object is ready. Examples:
- [node-mapnik](https://github.com/mapnik/node-mapnik/blob/fe80ce5d79c0e90cfbb5a2b992bf0ae2b8f88198/src/mapnik_map.hpp#L20): we create a Map object once, then use the object multiple times for rendering each vector tile.
- [node-addon-examples](https://github.com/nodejs/node-addon-examples/tree/master/6_object_wrap/nan) for another example of what an object-focused Addon looks like.
- [node-addon-examples](https://github.com/nodejs/node-addon-examples/tree/master/6_object_wrap/node-addon-api) for another example of what an object-focused Addon looks like.

Objects/Classes are useful when you start doing more complex operations and need to consider performance more heavily, when performance constraints start to matter. Use classes to compute the value of something once, rather than every time you call a function.

One step further is using an asynchronous object or class, which enables you to pass data into the threadpool. The aim is to process data in the threadpool in the most efficient way possible. [Efficiency](https://github.com/mapbox/cpp/blob/master/glossary.md#efficiency) and high performance are the main goals of the `HelloObjectAsync` example in this skel. The `HelloObjectAsync` example demonstrates high load, when _many_ objects in _many_ threads are being allocated. This scenario is where reducing unnecessary allocations really pays off, and is also why this example uses "move semantics" for even better performance.
- **Move semantics**: move semantics avoid data being copied (allocating memory), to limit unnecessary memory allocation, and are most useful when working with big data.
- **Move semantics**: move semantics avoid data being copied (allocating memory), to limit unnecessary memory allocation, and are most useful when working with big data.

Other thoughts related to move semantics:
- Always best to use move semantics instead of passing by reference, espeically with objects like [`std::string`](http://shaharmike.com/cpp/std-string/), which can be expensive.
@@ -175,7 +175,7 @@ This binary file `../lib/binding/module.node` is what `require()` points to with

# Clang Tools

This skeleton uses two clang/llvm tools for automated formatting and static fixes.
This skeleton uses two clang/llvm tools for automated formatting and static fixes.

Each of these tools run within their own [Travis jobs](https://github.com/mapbox/node-cpp-skel/blob/master/.travis.yml). You can disable these Travis jobs if you'd like, by commenting them out in `.travis.yml`. This may be necessary if you're porting this skel into an already-existing project that triggers tons of clang-tidy errors, and you'd like to work through them gradually while still actively iterating on other parts of your code.

363 changes: 238 additions & 125 deletions package-lock.json

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@mapbox/node-cpp-skel",
"version": "0.1.0",
"description": "Skeleton for bindings to C++ libraries for Node.js using NAN",
"description": "Skeleton for bindings to C++ libraries for Node.js using N-API (node-addon-api)",
"url": "http://github.com/mapbox/node-cpp-skel",
"main": "./lib/index.js",
"repository": {
@@ -17,15 +17,15 @@
"license": "ISC",
"dependencies": {
"@mapbox/mason-js": "^0.1.5",
"nan": "^2.14.0",
"node-pre-gyp": "^0.13.0"
"node-addon-api": "nodejs/node-addon-api",
"node-pre-gyp": "^0.14.0"
},
"devDependencies": {
"aws-sdk": "^2.4.7",
"tape": "^4.5.1",
"d3-queue": "^3.0.1",
"minimist": "~1.2.0",
"bytes": "^2.4.0"
"bytes": "^3.1.0",
"d3-queue": "^3.0.7",
"minimist": "^1.2.5",
"tape": "^4.13.2"
},
"binary": {
"module_name": "module",
2 changes: 2 additions & 0 deletions scripts/sanitize.sh
Original file line number Diff line number Diff line change
@@ -26,6 +26,8 @@ echo "leak:v8::internal" >> ${SUPPRESSION_FILE}
echo "leak:node::CreateEnvironment" >> ${SUPPRESSION_FILE}
echo "leak:node::Start" >> ${SUPPRESSION_FILE}
echo "leak:node::Init" >> ${SUPPRESSION_FILE}
# Suppress leak related to https://github.com/libuv/libuv/pull/2480
echo "leak:uv__set_process_title_platform_init" >> ${SUPPRESSION_FILE}
export ASAN_SYMBOLIZER_PATH=$(pwd)/mason_packages/.link/bin/llvm-symbolizer
export MSAN_SYMBOLIZER_PATH=$(pwd)/mason_packages/.link/bin/llvm-symbolizer
export UBSAN_OPTIONS=print_stacktrace=1
24 changes: 24 additions & 0 deletions src/cpu_intensive_task.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#pragma once

#include <chrono>
#include <string>
#include <thread>
#include <vector>

namespace detail {

// simulate CPU intensive task
inline std::unique_ptr<std::vector<char>> do_expensive_work(std::string const& name, bool louder)
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::string str = "...threads are busy async bees...hello " + name;
std::unique_ptr<std::vector<char>> result = std::make_unique<std::vector<char>>(str.begin(), str.end());
if (louder)
{
std::string extra{"!!!!"};
std::copy(extra.c_str(), extra.c_str() + extra.length(), back_inserter(*result));
}
return result;
}

} // namespace detail
29 changes: 11 additions & 18 deletions src/module.cpp
Original file line number Diff line number Diff line change
@@ -2,26 +2,24 @@
#include "object_sync/hello.hpp"
#include "standalone/hello.hpp"
#include "standalone_async/hello_async.hpp"
#include <nan.h>
#include <napi.h>
// #include "your_code.hpp"

// "target" is a magic var that NAN_MODULE_INIT passes into a module's scope.
// When you write things to target, they become available to call from
// Javascript world.
NAN_MODULE_INIT(init) {

Napi::Object init(Napi::Env env, Napi::Object exports)
{
// expose hello method
Nan::SetMethod(target, "hello", standalone::hello);
exports.Set(Napi::String::New(env, "hello"), Napi::Function::New(env, standalone::hello));

// expose helloAsync method
Nan::SetMethod(target, "helloAsync", standalone_async::helloAsync);
exports.Set(Napi::String::New(env, "helloAsync"), Napi::Function::New(env, standalone_async::helloAsync));

// expose HelloObject class
object_sync::HelloObject::Init(target);
object_sync::HelloObject::Init(env, exports);

// expose HelloObjectAsync class
object_async::HelloObjectAsync::Init(target);
object_async::HelloObjectAsync::Init(env, exports);

return exports;
/**
* You may have noticed there are multiple "hello" functions as part of this
* module.
@@ -36,13 +34,8 @@ NAN_MODULE_INIT(init) {
// Include your .hpp file at the top of this file.
}

// Here we initialize the module (we only do this once)
// by attaching the init function to the module. This invokes
// a variety of magic from inside nodejs core that we don't need to
// worry about, but if you care the details are at https://github.com/nodejs/node/blob/34d1b1144e1af8382dad71c28c8d956ebf709801/src/node.h#L431-L518
// We mark this NOLINT to avoid the clang-tidy checks
// warning about code inside nodejs that we don't control and can't
// directly change to avoid the warning.
// Initialize the module (we only do this once)
// Mark this NOLINT to avoid the clang-tidy checks
// NODE_GYP_MODULE_NAME is the name of our module as defined in 'target_name'
// variable in the 'binding.gyp', which is passed along as a compiler define
NODE_MODULE(NODE_GYP_MODULE_NAME, init) // NOLINT
NODE_API_MODULE(NODE_GYP_MODULE_NAME, init) // NOLINT
45 changes: 17 additions & 28 deletions src/module_utils.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#pragma once
#include <memory>
#include <nan.h>
#include <napi.h>
#include <string>

namespace utils {
@@ -10,35 +10,24 @@ namespace utils {
* throwing errors.
* Usage:
*
* v8::Local<v8::Function> callback;
* return CallbackError("error message", callback); // "return" is important to
* prevent duplicate callbacks from being fired!
*
*
* "inline" is important here as well. See for more contex:
* - https://github.com/mapbox/cpp/blob/master/glossary.md#inline-keyword
* - https://github.com/mapbox/node-cpp-skel/pull/52#discussion_r126847394 for
* context
* Napi::CallbackInfo info;
* return CallbackError("error message", info);
*
*/
inline void CallbackError(std::string message, v8::Local<v8::Function> func) {
Nan::Callback cb(func);
v8::Local<v8::Value> argv[1] = {Nan::Error(message.c_str())};
Nan::Call(cb, 1, argv);
inline Napi::Value CallbackError(std::string const& message, Napi::CallbackInfo const& info)
{
Napi::Object obj = Napi::Object::New(info.Env());
obj.Set("message", message);
auto func = info[info.Length() - 1].As<Napi::Function>();
// ^^^ here we assume that info has a valid callback function
// TODO: consider changing either method signature or adding internal checks
return func.Call({obj});
}
} // namespace utils

inline Nan::MaybeLocal<v8::Object> NewBufferFrom(std::unique_ptr<std::string>&& ptr) {
Nan::MaybeLocal<v8::Object> res = Nan::NewBuffer(
&(*ptr)[0],
ptr->size(),
[](char*, void* hint) {
delete static_cast<std::string*>(hint);
},
ptr.get());
if (!res.IsEmpty()) {
ptr.release(); // NOLINT ignore bugprone-unused-return-value
}
return res;
}
namespace gsl {
template <typename T>
using owner = T;
} // namespace gsl

} // namespace utils
// ^^^ type alias required for clang-tidy (cppcoreguidelines-owning-memory)
418 changes: 167 additions & 251 deletions src/object_async/hello_async.cpp

Large diffs are not rendered by default.

25 changes: 8 additions & 17 deletions src/object_async/hello_async.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#pragma once
#include <nan.h>
#include <napi.h>

namespace object_async {

@@ -10,27 +10,18 @@ namespace object_async {
* Also, this class adheres to the rule of Zero because we define no custom
* destructor or copy constructor
*/
class HelloObjectAsync : public Nan::ObjectWrap {

class HelloObjectAsync : public Napi::ObjectWrap<HelloObjectAsync>
{
public:
// initializer
static void Init(v8::Local<v8::Object> target);

// methods required for the V8 constructor
static NAN_METHOD(New);
static Nan::Persistent<v8::Function>& create_once();

// helloAsync, custom async method tied to Init of this class
// method's logic lives in ./hello.cpp
static NAN_METHOD(helloAsync);

// C++ Constructor
// Passing the arg by rvalue reference (&&)
HelloObjectAsync(std::string&& name);
static Napi::Object Init(Napi::Env env, Napi::Object exports);
explicit HelloObjectAsync(Napi::CallbackInfo const& info);
Napi::Value helloAsync(Napi::CallbackInfo const& info);

private:
// member variable
// specific to each instance of the class
std::string name_;
static Napi::FunctionReference constructor;
std::string name_ = "";
};
} // namespace object_async
165 changes: 34 additions & 131 deletions src/object_sync/hello.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#include "hello.hpp"

#include <memory>

/**
@@ -28,143 +27,47 @@
// clearly organize your application.
namespace object_sync {

// Custom constructor, assigns custom name passed in from Javascript world.
// This constructor uses member init list via the semicolon, aka "direct initialization"
// which is more efficient than using assignment operators.
HelloObject::HelloObject(std::string&& name) : name_(name) {}
Napi::FunctionReference HelloObject::constructor; // NOLINT

// Triggered from Javascript world when calling "new HelloObject(name)"
NAN_METHOD(HelloObject::New) {
if (info.IsConstructCall()) {
try {
if (info.Length() >= 1) {
if (info[0]->IsString()) {
// Don't want to risk passing a null string around, which might create unpredictable behavior.
Nan::Utf8String utf8_value(info[0]);
int len = utf8_value.length();
if (len <= 0) {
return Nan::ThrowTypeError("arg must be a non-empty string");
}

/**
* This line converts a V8 string to a C++ std::string.
* In the background, it triggers memory allocation (stack allocating, but std:string is also dynamically allocating memory in the heap)
* We want to avoid heap allocation to ensure more performant code.
* See https://github.com/mapbox/cpp/blob/master/glossary.md#stack-allocation
* and https://stackoverflow.com/questions/79923/what-and-where-are-the-stack-and-heap/80113#80113
* Also, providing the length allows the std::string constructor to avoid calculating the length internally
* and should be faster since it skips an operation.
*/
std::string name(*utf8_value, static_cast<std::size_t>(len));

/**
* This line is where HelloObject takes ownership of "name" with the use of move semantics.
* Then all later usage of "name" are passed by reference (const&), but the actual home or address in memory
* will always be owned by this instance of HelloObjectAsync. Generally important to know what has ownership of an object.
* When a object/value is a member of a class (like "name"), we know the class (HelloObjectAsync) has full control of the scope of the object/value.
* This avoids the scenario of "name" being destroyed or becoming out of scope.
*
* Also, we're using "new" here to create a custom C++ class, based on node::ObjectWrap since this is a node addon.
* In this case, "new" allocates a C++ object (dynamically on the heap) and then passes ownership (control of when it gets deleted)
* to V8, the javascript engine which decides when to clean up the object based on how its’ garbage collector works.
* In other words, the memory of HelloObjectAsync is expliclty deleted via node::ObjectWrap when it's gone out of scope
* (the object needs to stay alive until the V8 garbage collector has decided it's done):
* https://github.com/nodejs/node/blob/7ec28a0a506efe9d1c03240fd028bea4a3d350da/src/node_object_wrap.h#L124
*/
auto self = std::make_unique<HelloObject>(std::move(name)); // Using unique pointer to adhere to cpp core guideline: https://clang.llvm.org/extra/clang-tidy/checks/cppcoreguidelines-owning-memory.html
self->Wrap(info.This()); // Connects C++ object to Javascript object (this)
self.release(); // NOLINT Release the ownership of self so it can be managed by wrapper
} else {
return Nan::ThrowTypeError(
"arg must be a string");
}
} else {
return Nan::ThrowTypeError(
"must provide string arg");
}
} catch (const std::exception& ex) {
return Nan::ThrowTypeError(ex.what());
}

info.GetReturnValue().Set(info.This());
} else {
return Nan::ThrowTypeError(
"Cannot call constructor as function, you need to use 'new' keyword");
HelloObject::HelloObject(Napi::CallbackInfo const& info)
: Napi::ObjectWrap<HelloObject>(info)
{
Napi::Env env = info.Env();
Napi::HandleScope scope(env);
std::size_t length = info.Length();
if (length != 1 || !info[0].IsString())
{
Napi::TypeError::New(env, "String expected").ThrowAsJavaScriptException();
return;
}
name_ = info[0].As<Napi::String>().Utf8Value();
if (name_.empty())
{
Napi::TypeError::New(env, "arg must be a non-empty string").ThrowAsJavaScriptException();
}
}

// NAN_METHOD is applicable to methods you want to expose to JS world
NAN_METHOD(HelloObject::hello) {
/**
* Note: a HandleScope is automatically included inside NAN_METHOD. See the
* docs at NAN that say:
* 'Note that an implicit HandleScope is created for you on
* JavaScript-accessible methods so you do not need to insert one yourself.'
* at
* https://github.com/nodejs/nan/blob/2dfc5c2d19c8066903a19ced6a72c06d2c825dec/doc/scopes.md#nanhandlescope
* "What is node::ObjectWrap???" The short version is that node::ObjectWrap
* and wrapping/unwrapping objects
* is the somewhat clumsy way it is possible to bind Node and C++. The main
* points to remember:
* - To access a class instance inside a C++ static method, you must unwrap
* the object.
* - The C++ methods must be static to make them available at startup across
* the language boundary (JS <-> C++).
*/
auto* h = Nan::ObjectWrap::Unwrap<HelloObject>(info.Holder());

// "info" comes from the NAN_METHOD macro, which returns differently
// according to the version of node
info.GetReturnValue().Set(
Nan::New<v8::String>("...initialized an object...hello " + h->name_)
.ToLocalChecked());
}

// This is a Singleton, which is a general programming design concept for
// creating an instance once within a process.
Nan::Persistent<v8::Function>& HelloObject::create_once() {
static Nan::Persistent<v8::Function> init;
return init;
Napi::Value HelloObject::hello(Napi::CallbackInfo const& info)
{
Napi::Env env = info.Env();
return Napi::String::New(env, name_);
}

void HelloObject::Init(v8::Local<v8::Object> target) {
// A handlescope is needed so that v8 objects created in the local memory
// space (this function in this case)
// are cleaned up when the function is done running (and the handlescope is
// destroyed)
// Fun trivia: forgetting a handlescope is one of the most common causes of
// memory leaks in node.js core
// https://www.joyent.com/blog/walmart-node-js-memory-leak
Nan::HandleScope scope;

// This is saying:
// "Node, please allocate a new Javascript string object
// inside the V8 local memory space, with the value 'HelloObject' "
v8::Local<v8::String> whoami = Nan::New("HelloObject").ToLocalChecked();

// Officially create the HelloObject
auto fnTp = Nan::New<v8::FunctionTemplate>(
HelloObject::New, v8::Local<v8::Value>()); // Passing the HelloObject::New method above
fnTp->InstanceTemplate()->SetInternalFieldCount(1); // It's 1 when holding the ObjectWrap itself and nothing else
fnTp->SetClassName(whoami); // Passing the Javascript string object above

// Add custom methods here.
// This is how hello() is exposed as part of HelloObject.
// This line is attaching the "hello" method to a JavaScript function
// prototype.
// "hello" is therefore like a property of the fnTp object
// ex: console.log(HelloObject.hello) --> [Function: hello]
SetPrototypeMethod(fnTp, "helloMethod", hello);

// Create an unique instance of the HelloObject function template,
// then set this unique instance to the target
const auto fn = Nan::GetFunction(fnTp).ToLocalChecked();
create_once().Reset(fn); // calls the static &HelloObject::create_once method
// above. This ensures the instructions in this Init
// function are retained in memory even after this
// code block ends.
Nan::Set(target, whoami, fn);
Napi::Object HelloObject::Init(Napi::Env env, Napi::Object exports)
{

Napi::Function func = DefineClass(env, "HelloObject", {InstanceMethod("helloMethod", &HelloObject::hello)});
// Create a peristent reference to the class constructor. This will allow
// a function called on a class prototype and a function
// called on instance of a class to be distinguished from each other.
constructor = Napi::Persistent(func);
// Call the SuppressDestruct() method on the static data prevent the calling
// to this destructor to reset the reference when the environment is no longer
// available.
constructor.SuppressDestruct();
exports.Set("HelloObject", func);
return exports;
}

} // namespace object_sync
37 changes: 13 additions & 24 deletions src/object_sync/hello.hpp
Original file line number Diff line number Diff line change
@@ -1,35 +1,24 @@
#pragma once

#include <nan.h>
#include <napi.h>

namespace object_sync {

/**
* HelloObject class
* This is in a header file so we can access it across other .cpp files if necessary
* Also, this class adheres to the rule of Zero because we define no custom destructor or copy constructor
*/
class HelloObject : public Nan::ObjectWrap {

* HelloObject class
* This is in a header file so we can access it across other .cpp files if necessary
* Also, this class adheres to the rule of Zero because we define no custom destructor or copy constructor
*/
class HelloObject : public Napi::ObjectWrap<HelloObject>
{
public:
// initializer
static void Init(v8::Local<v8::Object> target);

// methods required for the V8 constructor (?)
static NAN_METHOD(New);
static Nan::Persistent<v8::Function>& create_once();

// hello, custom sync method tied to Init of this class
// method's logic lives in ./hello.cpp
static NAN_METHOD(hello);

// C++ Constructor
// Passing the arg by rvalue reference (&&)
HelloObject(std::string&& name);
// initializers
static Napi::Object Init(Napi::Env env, Napi::Object exports);
explicit HelloObject(Napi::CallbackInfo const& info);
Napi::Value hello(Napi::CallbackInfo const& info);

private:
// member variable
// specific to each instance of the class
std::string name_;
static Napi::FunctionReference constructor;
std::string name_ = "";
};
} // namespace object_sync
21 changes: 6 additions & 15 deletions src/standalone/hello.cpp
Original file line number Diff line number Diff line change
@@ -10,20 +10,11 @@
* console.log(check); // => "hello world"
*/
namespace standalone {
// If this was not defined within a namespace, it would be in the global scope.
// Namespaces are used because C++ has no notion of scoped modules, so all of
// the code you write in any file could conflict with other code.
// Namespaces are generally a great idea in C++ because it helps scale and
// clearly organize your application.

// hello is a "standalone function" because it's not a class.
// If this function was not defined within a namespace, it would be in the
// global scope.
NAN_METHOD(hello) {

// "info" comes from the NAN_METHOD macro, which returns differently
// according to the version of node
info.GetReturnValue().Set(
Nan::New<v8::String>("hello world").ToLocalChecked());
Napi::Value hello(Napi::CallbackInfo const& info)
{
Napi::Env env = info.Env();
return Napi::String::New(env, "hello world");
}
} // namespace standalone

} // namespace standalone
37 changes: 5 additions & 32 deletions src/standalone/hello.hpp
Original file line number Diff line number Diff line change
@@ -1,36 +1,9 @@
#pragma once
#include <nan.h>
// specify #include with carrots, ex: <nan.h> --> look for header in global
// specify #include with quotes, ex: "hello.hpp" --> look for header in location
// relative to this file
#include <napi.h>

/*
Namespace is an organizational method that helps to clearly show where a
method is coming from.
Namespaces are generally a great idea in C++ because they help us scale
things. C++ has no notion of scoped modules,
so all of the code you write in any file could potentially conflict with other
classes/functions/etc.
Namespaces help to differentiate pieces of your code.
The convention In this skeleton is to name the namespace to match the name of
the subdirectory where it lives.
So in this case, the namespace is called "standalone" because this method
lives within the "standalone" subdirectory.
If there is another "hello" function used for another example, the compiler
will know the difference between the two:
standalone::hello
VS
potato::hello
*/
namespace standalone {

// hello, custom sync method tied to module.cpp
// method's logic lives in hello.cpp
NAN_METHOD(hello);
} // namespace standalone
// hello, custom sync method
Napi::Value hello(Napi::CallbackInfo const& info);

} // namespace standalone
186 changes: 77 additions & 109 deletions src/standalone_async/hello_async.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "hello_async.hpp"
#include "../cpu_intensive_task.hpp"
#include "../module_utils.hpp"

#include <exception>
@@ -24,166 +25,133 @@
* });
*/

// If this was not defined within a namespace, it would be in the global scope.
// Namespaces are used because C++ has no notion of scoped modules, so all of
// the code you write in any file could conflict with other code.
// Namespaces are generally a great idea in C++ because it helps scale and
// clearly organize your application.
namespace standalone_async {

// Expensive allocation of std::map, querying, and string comparison,
// therefore threads are busy
std::unique_ptr<std::string> do_expensive_work(bool louder) {

std::map<std::size_t, std::string> container;
std::size_t work_to_do = 100000;

for (std::size_t i = 0; i < work_to_do; ++i) {
container.emplace(i, std::to_string(i));
}

for (std::size_t i = 0; i < work_to_do; ++i) {
std::string const& item = container[i];
if (item != std::to_string(i)) {

// AsyncHelloWorker's Execute function will take care of this error
// and return it to js-world via callback
// Marked NOLINT to avoid clang-tidy cert-err60-cpp error which we cannot
// avoid on some linux distros where std::runtime_error is not properly
// marked noexcept. Details at https://www.securecoding.cert.org/confluence/display/cplusplus/ERR60-CPP.+Exception+objects+must+be+nothrow+copy+constructible
throw std::runtime_error("Uh oh, this should never happen"); // NOLINT
}
}

std::unique_ptr<std::string> result = std::make_unique<std::string>("...threads are busy async bees...hello world");

if (louder) {
*result += "!!!!";
}

return result;
}

// This is the worker running asynchronously and calling a user-provided
// callback when done.
// Consider storing all C++ objects you need by value or by shared_ptr to keep
// them alive until done.
// Nan AsyncWorker docs:
// https://github.com/nodejs/nan/blob/master/doc/asyncworker.md
struct AsyncHelloWorker : Nan::AsyncWorker {
using Base = Nan::AsyncWorker;

AsyncHelloWorker(bool louder, bool buffer, Nan::Callback* cb)
: Base(cb, "skel:standalone-async-worker"), louder_(louder), buffer_(buffer) {}
// Napi::AsyncWorker docs:
// https://github.com/nodejs/node-addon-api/blob/master/doc/async_worker.md
struct AsyncHelloWorker : Napi::AsyncWorker
{
using Base = Napi::AsyncWorker;
// ctor
AsyncHelloWorker(bool louder, bool buffer, Napi::Function const& cb)
: Base(cb),
louder_(louder),
buffer_(buffer) {}

// The Execute() function is getting called when the worker starts to run.
// - You only have access to member variables stored in this worker.
// - You do not have access to Javascript v8 objects here.
void Execute() override {
void Execute() override
{
// The try/catch is critical here: if code was added that could throw an
// unhandled error INSIDE the threadpool, it would be disasterous
try {
result_ = do_expensive_work(louder_);
} catch (const std::exception& e) {
SetErrorMessage(e.what());
try
{
result_ = detail::do_expensive_work("world", louder_);
}
catch (std::exception const& e)
{
SetError(e.what());
}
}

// The HandleOKCallback() is getting called when Execute() successfully
// The OnOK() is getting called when Execute() successfully
// completed.
// - In case Execute() invoked SetErrorMessage("") this function is not
// getting called.
// - You have access to Javascript v8 objects again
// - You have to translate from C++ member variables to Javascript v8 objects
// - Finally, you call the user's callback with your results
void HandleOKCallback() override {
Nan::HandleScope scope;

if (buffer_) {
const auto argc = 2u;
v8::Local<v8::Value> argv[argc] = {
Nan::Null(), utils::NewBufferFrom(std::move(result_)).ToLocalChecked()};
// Static cast done here to avoid 'cppcoreguidelines-pro-bounds-array-to-pointer-decay' warning with clang-tidy
callback->Call(argc, static_cast<v8::Local<v8::Value>*>(argv), async_resource);
} else {
const auto argc = 2u;
v8::Local<v8::Value> argv[argc] = {
Nan::Null(), Nan::New<v8::String>(*result_).ToLocalChecked()};
// Static cast done here to avoid 'cppcoreguidelines-pro-bounds-array-to-pointer-decay' warning with clang-tidy
callback->Call(argc, static_cast<v8::Local<v8::Value>*>(argv), async_resource);
void OnOK() final
{
Napi::HandleScope scope(Env());
if (!Callback().IsEmpty() && result_)
{
if (buffer_)
{
char* data = result_->data();
std::size_t size = result_->size();
auto buffer = Napi::Buffer<char>::New(Env(),
data,
size,
[](Napi::Env, char*, gsl::owner<std::vector<char>*> v) {
delete v;
},
result_.release());
Callback().Call({Env().Null(), buffer});
}
else
{
Callback().Call({Env().Null(), Napi::String::New(Env(), result_->data(), result_->size())});
}
}
}

std::unique_ptr<std::string> result_ = std::make_unique<std::string>();
std::unique_ptr<std::vector<char>> result_ = nullptr;
const bool louder_;
const bool buffer_;
};

// helloAsync is a "standalone function" because it's not a class.
// If this function was not defined within a namespace ("standalone_async"
// specified above), it would be in the global scope.
NAN_METHOD(helloAsync) {

Napi::Value helloAsync(Napi::CallbackInfo const& info)
{
bool louder = false;
bool buffer = false;

// Check second argument, should be a 'callback' function.
// This allows us to set the callback so we can use it to return errors
// instead of throwing.
// Also, "info" comes from the NAN_METHOD macro, which returns differently
// according to the version of node
if (!info[1]->IsFunction()) {
return Nan::ThrowTypeError("second arg 'callback' must be a function");
if (!info[1].IsFunction())
{
Napi::TypeError::New(info.Env(), "second arg 'callback' must be a function").ThrowAsJavaScriptException();
return info.Env().Null();
}
v8::Local<v8::Function> callback = info[1].As<v8::Function>();

Napi::Function callback = info[1].As<Napi::Function>();

// Check first argument, should be an 'options' object
if (!info[0]->IsObject()) {
return utils::CallbackError("first arg 'options' must be an object",
callback);
if (!info[0].IsObject())
{
return utils::CallbackError("first arg 'options' must be an object", info);
}
v8::Local<v8::Object> options = info[0].As<v8::Object>();
Napi::Object options = info[0].As<Napi::Object>();

// Check options object for the "louder" property, which should be a boolean
// value
if (Nan::Has(options, Nan::New("louder").ToLocalChecked()).FromMaybe(false)) {
v8::Local<v8::Value> louder_val =
Nan::Get(options, Nan::New("louder").ToLocalChecked()).ToLocalChecked();
if (!louder_val->IsBoolean()) {
return utils::CallbackError("option 'louder' must be a boolean",
callback);
}
Nan::Maybe<bool> maybe_louder = Nan::To<bool>(louder_val);
if (maybe_louder.IsNothing()) {
return utils::CallbackError("option 'louder' must be a boolean", callback);

if (options.Has(Napi::String::New(info.Env(), "louder")))
{
Napi::Value louder_val = options.Get(Napi::String::New(info.Env(), "louder"));
if (!louder_val.IsBoolean())
{
return utils::CallbackError("option 'louder' must be a boolean", info);
}
louder = maybe_louder.FromJust();
louder = louder_val.As<Napi::Boolean>().Value();
}
// Check options object for the "buffer" property, which should be a boolean
// value
if (Nan::Has(options, Nan::New("buffer").ToLocalChecked()).FromMaybe(false)) {
v8::Local<v8::Value> buffer_val =
Nan::Get(options, Nan::New("buffer").ToLocalChecked()).ToLocalChecked();
if (!buffer_val->IsBoolean()) {
return utils::CallbackError("option 'buffer' must be a boolean",
callback);
}
Nan::Maybe<bool> maybe_buffer = Nan::To<bool>(buffer_val);
if (maybe_buffer.IsNothing()) {
return utils::CallbackError("option 'buffer' must be a boolean", callback);
// Check options object for the "buffer" property, which should be a boolean value
if (options.Has(Napi::String::New(info.Env(), "buffer")))
{
Napi::Value buffer_val = options.Get(Napi::String::New(info.Env(), "buffer"));
if (!buffer_val.IsBoolean())
{
return utils::CallbackError("option 'buffer' must be a boolean", info);
}
buffer = maybe_buffer.FromJust();
buffer = buffer_val.As<Napi::Boolean>().Value();
}

// Creates a worker instance and queues it to run asynchronously, invoking the
// callback when done.
// - Nan::AsyncWorker takes a pointer to a Nan::Callback and deletes the
// - Napi::AsyncWorker takes a pointer to a Napi::FunctionReference and deletes the
// pointer automatically.
// - Nan::AsyncQueueWorker takes a pointer to a Nan::AsyncWorker and deletes
// - Napi::AsyncQueueWorker takes a pointer to a Napi::AsyncWorker and deletes
// the pointer automatically.
auto cb = std::make_unique<Nan::Callback>(callback);
auto worker = std::make_unique<AsyncHelloWorker>(louder, buffer, cb.release());
Nan::AsyncQueueWorker(worker.release());
auto* worker = new AsyncHelloWorker{louder, buffer, callback}; // NOLINT
worker->Queue();
return info.Env().Undefined(); // NOLINT
}

} // namespace standalone_async
35 changes: 5 additions & 30 deletions src/standalone_async/hello_async.hpp
Original file line number Diff line number Diff line change
@@ -1,35 +1,10 @@
#pragma once
#include <nan.h>
// carrots, ex: <nan.h> --> look for header in global
// quotes, ex: "hello.hpp" --> look for header in location relative to this file
#include <napi.h>

/*
Namespace is an organizational method that helps to clearly show where a
method is coming from.
Namespaces are generally a great idea in C++ because they help us scale
things. C++ has no notion of scoped modules,
so all of the code you write in any file could potentially conflict with other
classes/functions/etc.
Namespaces help to differentiate pieces of your code.
The convention In this skeleton is to name the namespace to match the name of
the subdirectory where it lives.
So in this case, the namespace is called "standalone" because this method
lives within the "standalone" subdirectory.
If there is another "hello" function used for another example, the compiler
will know the difference between the two:
standalone::hello
VS
potato::hello
*/
namespace standalone_async {

// hello, custom sync method tied to module.cpp
// hello, custom sync method
// method's logic lives in hello.cpp
NAN_METHOD(helloAsync);
} // namespace standalone_async
Napi::Value helloAsync(Napi::CallbackInfo const& info);

} // namespace standalone_async
12 changes: 6 additions & 6 deletions test/hello_object.test.js
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ var module = require('../lib/index.js');
test('success: prints expected string', function(t) {
var H = new module.HelloObject('carol');
var check = H.helloMethod();
t.equal(check, '...initialized an object...hello carol', 'returned expected string');
t.equal(check, 'carol', 'returned expected string');
t.end();
});

@@ -13,7 +13,7 @@ test('error: throws when passing empty string', function(t) {
var H = new module.HelloObject('');
} catch(err) {
t.ok(err, 'expected error');
t.equal(err.message, 'arg must be a non-empty string', 'expected error message')
t.equal(err.message, 'arg must be a non-empty string', 'expected error message');
t.end();
}
});
@@ -23,7 +23,7 @@ test('error: throws when missing "new"', function(t) {
var H = module.HelloObject();
} catch(err) {
t.ok(err);
t.equal(err.message, 'Cannot call constructor as function, you need to use \'new\' keyword', 'expected error message')
t.equal(err.message, 'Class constructors cannot be invoked without \'new\'', 'expected error message');
t.end();
}
});
@@ -33,7 +33,7 @@ test('error: handles non-string arg within constructor', function(t) {
var H = new module.HelloObject(24);
} catch(err) {
t.ok(err, 'expected error');
t.ok(err.message.indexOf('arg must be a string') > -1, 'expected error message');
t.ok(err.message, 'A string was expected', 'expected error message');
t.end();
}
});
@@ -43,7 +43,7 @@ test('error: handles missing arg', function(t) {
var H = new module.HelloObject();
} catch (err) {
t.ok(err, 'expected error');
t.ok(err.message.indexOf('must provide string arg') > -1, 'expected error message');
t.ok(err.message, 'must provide string arg', 'expected error message');
t.end();
}
});
});
8 changes: 4 additions & 4 deletions test/hello_object_async.test.js
Original file line number Diff line number Diff line change
@@ -45,7 +45,7 @@ test('error: throws when missing "new"', function(t) {
var H = module.HelloObjectAsync('world');
} catch(err) {
t.ok(err, 'expected error');
t.equal(err.message, 'Cannot call constructor as function, you need to use \'new\' keyword', 'expected error message')
t.equal(err.message, 'Class constructors cannot be invoked without \'new\'', 'expected error message')
t.end();
}
});
@@ -54,8 +54,8 @@ test('error: handles non-string arg within constructor', function(t) {
try {
var H = new module.HelloObjectAsync(24);
} catch(err) {
t.ok(err, 'expected error');
t.ok(err.message.indexOf('arg must be a string') > -1, 'expected error message');
console.log(err.message);
t.equal(err.message, 'String expected', 'expected error message');
t.end();
}
});
@@ -103,7 +103,7 @@ test('error: handles missing arg', function(t) {
var H = new module.HelloObjectAsync();
} catch (err) {
t.ok(err, 'expected error');
t.ok(err.message.indexOf('must provide string arg') > -1, 'expected error message');
t.equal(err.message, 'String expected', 'expected error message');
t.end();
}
});