Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 23 additions & 0 deletions async-iterator/node-addon-api/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
project (example)
include_directories(${CMAKE_JS_INC} node_modules/node-addon-api/)
cmake_minimum_required(VERSION 3.18)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include_directories(${CMAKE_JS_INC})
file(GLOB SOURCE_FILES "*.cc")
add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES} ${CMAKE_JS_SRC})
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node")
target_link_libraries(${PROJECT_NAME} ${CMAKE_JS_LIB})

# Include Node-API wrappers
execute_process(COMMAND node -p "require('node-addon-api').include"
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE NODE_ADDON_API_DIR
)
string(REGEX REPLACE "[\r\n\"]" "" NODE_ADDON_API_DIR ${NODE_ADDON_API_DIR})

target_include_directories(${PROJECT_NAME} PRIVATE ${NODE_ADDON_API_DIR})

# define NAPI_VERSION
add_definitions(-DNAPI_VERSION=6)
84 changes: 84 additions & 0 deletions async-iterator/node-addon-api/example.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#include <napi.h>
#include <memory>

using namespace Napi;

class AsyncIteratorExample : public ObjectWrap<AsyncIteratorExample> {
public:
AsyncIteratorExample(const CallbackInfo& info)
: ObjectWrap<AsyncIteratorExample>(info),
_from(info[0].As<Number>()),
_to(info[1].As<Number>()) {}

static Object Init(Napi::Env env, Napi::Object exports) {
Napi::Function func = DefineClass(
env,
"AsyncIteratorExample",
{InstanceMethod(Napi::Symbol::WellKnown(env, "asyncIterator"),
&AsyncIteratorExample::Iterator)});

exports.Set("AsyncIteratorExample", func);
return exports;
}

Napi::Value Iterator(const CallbackInfo& info) {
auto env = info.Env();
auto iteratorObject = Napi::Object::New(env);

iteratorObject["current"] = Number::New(env, _from);
iteratorObject["last"] = Number::New(env, _to);
auto next = Function::New(env, [](const CallbackInfo& info) {
auto env = info.Env();
auto deferred =
std::make_shared<Promise::Deferred>(Promise::Deferred::New(env));
auto iteratorObject = info.This().As<Object>();
auto callback = Function::New(
env,
[=](const CallbackInfo& info) {
auto env = info.Env();
auto value = Object::New(env);
auto iteratorObject = info.This().As<Object>();

auto current =
iteratorObject.Get("current").As<Number>().Int32Value();
auto last = iteratorObject.Get("last").As<Number>().Int32Value();
auto done = current > last;

if (done) {
value["done"] = Boolean::New(env, true);
} else {
value["done"] = Boolean::New(env, false);
value["value"] = Number::New(env, current);
iteratorObject["current"] = Number::New(env, current + 1);
}
deferred->Resolve(value);
},
"next");

env.Global()
.Get("setTimeout")
.As<Function>()
.Call({callback.Get("bind").As<Function>().Call(callback,
{iteratorObject}),
Number::New(env, 1000)});

return deferred->Promise();
});

iteratorObject["next"] =
next.Get("bind").As<Function>().Call(next, {iteratorObject});

return iteratorObject;
}

private:
int _from;
Copy link
Copy Markdown
Member

@legendecas legendecas May 27, 2022

Choose a reason for hiding this comment

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

Nit: we may prefer to suffix with the underscore.

Suggested change
int _from;
int from_;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@KevinEady let us know what you think on this one.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hi @legendecas / @mhdawson ,

It looks like the standard used in napi.h for non-public members is to prefix with underscore (46 occurrences) versus suffix (1 occurrence), so I think I will stick with the prefix.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

+1

int _to;
};

Napi::Object Init(Napi::Env env, Object exports) {
AsyncIteratorExample::Init(env, exports);
return exports;
}

NODE_API_MODULE(example, Init)
49 changes: 49 additions & 0 deletions async-iterator/node-addon-api/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const { AsyncIteratorExample } = require('bindings')('example');

async function main(from, to) {
const iterator = new AsyncIteratorExample(from, to);
for await (const value of iterator) {
console.log(value);
}
}

/*
// The JavaScript equivalent of the node-addon-api C++ code for reference
async function main(from, to) {
class AsyncIteratorExample {
constructor(from, to) {
this.from = from;
this.to = to;
}

[Symbol.asyncIterator]() {
return {
current: this.from,
last: this.to,
next() {
return new Promise(resolve => {
setTimeout(() => {
if (this.current <= this.last) {
resolve({ done: false, value: this.current++ });
} else {
resolve({ done: true });
}
}, 1000)
});
}
}
}
}
const iterator = new AsyncIteratorExample(from, to);

for await (const value of iterator) {
console.log(value);
}
}
*/

main(0, 5)
.catch(e => {
console.error(e);
process.exit(1);
});
16 changes: 16 additions & 0 deletions async-iterator/node-addon-api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "async-iterator-example",
"version": "0.0.0",
"description": "Async iterator example using node-addon-api",
"main": "index.js",
"private": true,
"dependencies": {
"bindings": "^1.5.0",
"cmake-js": "^6.3.0",
"node-addon-api": "^5.0.0"
},
"scripts": {
"test": "node index.js",
"install": "cmake-js compile"
}
}
23 changes: 23 additions & 0 deletions threadsafe-async-iterator/node-addon-api/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
project (example)
include_directories(${CMAKE_JS_INC} node_modules/node-addon-api/)
cmake_minimum_required(VERSION 3.18)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include_directories(${CMAKE_JS_INC})
file(GLOB SOURCE_FILES "*.cc")
add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES} ${CMAKE_JS_SRC})
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node")
target_link_libraries(${PROJECT_NAME} ${CMAKE_JS_LIB})

# Include Node-API wrappers
execute_process(COMMAND node -p "require('node-addon-api').include"
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE NODE_ADDON_API_DIR
)
string(REGEX REPLACE "[\r\n\"]" "" NODE_ADDON_API_DIR ${NODE_ADDON_API_DIR})

target_include_directories(${PROJECT_NAME} PRIVATE ${NODE_ADDON_API_DIR})

# define NAPI_VERSION
add_definitions(-DNAPI_VERSION=6)
163 changes: 163 additions & 0 deletions threadsafe-async-iterator/node-addon-api/example.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
#include <napi.h>
#include <condition_variable>
#include <functional>
#include <memory>
#include <mutex>
#include <optional>
#include <thread>

using namespace Napi;

class ThreadSafeAsyncIteratorExample
: public ObjectWrap<ThreadSafeAsyncIteratorExample> {
public:
ThreadSafeAsyncIteratorExample(const CallbackInfo& info)
: ObjectWrap<ThreadSafeAsyncIteratorExample>(info),
_current(info[0].As<Number>()),
_last(info[1].As<Number>()) {}

static Object Init(Napi::Env env, Napi::Object exports);

Napi::Value Iterator(const CallbackInfo& info);

private:
using Context = ThreadSafeAsyncIteratorExample;

struct DataType {
std::unique_ptr<Promise::Deferred> deferred;
bool done;
std::optional<int> value;
};

static void CallJs(Napi::Env env,
Function callback,
Context* context,
DataType* data);

using TSFN = TypedThreadSafeFunction<Context, DataType, CallJs>;

using FinalizerDataType = void;

int _current;
int _last;
TSFN _tsfn;
std::thread _thread;
std::unique_ptr<Promise::Deferred> _deferred;

// Thread-safety
std::mutex _mtx;
std::condition_variable _cv;

void threadEntry();

static void FinalizerCallback(Napi::Env env,
void*,
ThreadSafeAsyncIteratorExample* context);
};

Object ThreadSafeAsyncIteratorExample::Init(Napi::Env env,
Napi::Object exports) {
Napi::Function func =
DefineClass(env,
"ThreadSafeAsyncIteratorExample",
{InstanceMethod(Napi::Symbol::WellKnown(env, "asyncIterator"),
&ThreadSafeAsyncIteratorExample::Iterator)});

exports.Set("ThreadSafeAsyncIteratorExample", func);
return exports;
}

Napi::Value ThreadSafeAsyncIteratorExample::Iterator(const CallbackInfo& info) {
auto env = info.Env();

if (_thread.joinable()) {
Napi::Error::New(env, "Concurrent iterations not implemented.")
.ThrowAsJavaScriptException();
return Napi::Value();
}

_tsfn =
TSFN::New(info.Env(),
"tsfn",
0,
1,
this,
std::function<decltype(FinalizerCallback)>(FinalizerCallback));

// To prevent premature garbage collection; Unref in TFSN finalizer
Ref();

// Create thread
_thread = std::thread(&ThreadSafeAsyncIteratorExample::threadEntry, this);

// Create iterable
auto iterable = Napi::Object::New(env);

iterable["next"] =
Function::New(env, [this](const CallbackInfo& info) -> Napi::Value {
std::lock_guard<std::mutex> lk(_mtx);
auto env = info.Env();
if (_deferred) {
Napi::Error::New(env, "Concurrent iterations not implemented.")
.ThrowAsJavaScriptException();
return Napi::Value();
}
_deferred = std::make_unique<Promise::Deferred>(env);
_cv.notify_all();
return _deferred->Promise();
});

return iterable;
}

void ThreadSafeAsyncIteratorExample::threadEntry() {
while (true) {
std::unique_lock<std::mutex> lk(_mtx);
_cv.wait(lk, [this] { return this->_deferred != nullptr; });
auto done = _current > _last;
if (done) {
_tsfn.BlockingCall(new DataType{std::move(this->_deferred), true, {}});
break;
} else {
std::this_thread::sleep_for(
std::chrono::seconds(1)); // Simulate CPU-intensive work
_tsfn.BlockingCall(
new DataType{std::move(this->_deferred), false, _current++});
}
}
_tsfn.Release();
}

void ThreadSafeAsyncIteratorExample::CallJs(Napi::Env env,
Function callback,
Context* context,
DataType* data) {
if (env != nullptr) {
auto value = Object::New(env);

if (data->done) {
value["done"] = Boolean::New(env, true);
} else {
value["done"] = Boolean::New(env, false);
value["value"] = Number::New(env, data->value.value());
}
data->deferred->Resolve(value);
}

if (data != nullptr) {
delete data;
}
}

void ThreadSafeAsyncIteratorExample::FinalizerCallback(
Napi::Env env, void*, ThreadSafeAsyncIteratorExample* context) {
context->_thread.join();
context->Unref();
}

Napi::Object Init(Napi::Env env, Object exports) {
ThreadSafeAsyncIteratorExample::Init(env, exports);
return exports;
}

NODE_API_MODULE(example, Init)
14 changes: 14 additions & 0 deletions threadsafe-async-iterator/node-addon-api/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const { ThreadSafeAsyncIteratorExample } = require('bindings')('example');

async function main(from, to) {
const iterator = new ThreadSafeAsyncIteratorExample(from, to);
for await (const value of iterator) {
console.log(value);
}
}

main(0, 5)
.catch(e => {
console.error(e);
process.exit(1);
});
16 changes: 16 additions & 0 deletions threadsafe-async-iterator/node-addon-api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "threadsafe-async-iterator-example",
"version": "0.0.0",
"description": "Async iterator example with threadsafe functions using node-addon-api",
"main": "index.js",
"private": true,
"dependencies": {
"bindings": "^1.5.0",
"cmake-js": "^6.3.0",
"node-addon-api": "^5.0.0"
},
"scripts": {
"test": "node index.js",
"install": "cmake-js compile"
}
}