Skip to content

Commit 8979194

Browse files
authored
Add node-addon-api async iterator example (#195)
* Add async iterator example * Add example for threadsafe-async-iterator
1 parent ee220d4 commit 8979194

8 files changed

Lines changed: 388 additions & 0 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
project (example)
2+
include_directories(${CMAKE_JS_INC} node_modules/node-addon-api/)
3+
cmake_minimum_required(VERSION 3.18)
4+
5+
set(CMAKE_CXX_STANDARD 20)
6+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
7+
include_directories(${CMAKE_JS_INC})
8+
file(GLOB SOURCE_FILES "*.cc")
9+
add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES} ${CMAKE_JS_SRC})
10+
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node")
11+
target_link_libraries(${PROJECT_NAME} ${CMAKE_JS_LIB})
12+
13+
# Include Node-API wrappers
14+
execute_process(COMMAND node -p "require('node-addon-api').include"
15+
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
16+
OUTPUT_VARIABLE NODE_ADDON_API_DIR
17+
)
18+
string(REGEX REPLACE "[\r\n\"]" "" NODE_ADDON_API_DIR ${NODE_ADDON_API_DIR})
19+
20+
target_include_directories(${PROJECT_NAME} PRIVATE ${NODE_ADDON_API_DIR})
21+
22+
# define NAPI_VERSION
23+
add_definitions(-DNAPI_VERSION=6)
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#include <napi.h>
2+
#include <memory>
3+
4+
using namespace Napi;
5+
6+
class AsyncIteratorExample : public ObjectWrap<AsyncIteratorExample> {
7+
public:
8+
AsyncIteratorExample(const CallbackInfo& info)
9+
: ObjectWrap<AsyncIteratorExample>(info),
10+
_from(info[0].As<Number>()),
11+
_to(info[1].As<Number>()) {}
12+
13+
static Object Init(Napi::Env env, Napi::Object exports) {
14+
Napi::Function func = DefineClass(
15+
env,
16+
"AsyncIteratorExample",
17+
{InstanceMethod(Napi::Symbol::WellKnown(env, "asyncIterator"),
18+
&AsyncIteratorExample::Iterator)});
19+
20+
exports.Set("AsyncIteratorExample", func);
21+
return exports;
22+
}
23+
24+
Napi::Value Iterator(const CallbackInfo& info) {
25+
auto env = info.Env();
26+
auto iteratorObject = Napi::Object::New(env);
27+
28+
iteratorObject["current"] = Number::New(env, _from);
29+
iteratorObject["last"] = Number::New(env, _to);
30+
auto next = Function::New(env, [](const CallbackInfo& info) {
31+
auto env = info.Env();
32+
auto deferred =
33+
std::make_shared<Promise::Deferred>(Promise::Deferred::New(env));
34+
auto iteratorObject = info.This().As<Object>();
35+
auto callback = Function::New(
36+
env,
37+
[=](const CallbackInfo& info) {
38+
auto env = info.Env();
39+
auto value = Object::New(env);
40+
auto iteratorObject = info.This().As<Object>();
41+
42+
auto current =
43+
iteratorObject.Get("current").As<Number>().Int32Value();
44+
auto last = iteratorObject.Get("last").As<Number>().Int32Value();
45+
auto done = current > last;
46+
47+
if (done) {
48+
value["done"] = Boolean::New(env, true);
49+
} else {
50+
value["done"] = Boolean::New(env, false);
51+
value["value"] = Number::New(env, current);
52+
iteratorObject["current"] = Number::New(env, current + 1);
53+
}
54+
deferred->Resolve(value);
55+
},
56+
"next");
57+
58+
env.Global()
59+
.Get("setTimeout")
60+
.As<Function>()
61+
.Call({callback.Get("bind").As<Function>().Call(callback,
62+
{iteratorObject}),
63+
Number::New(env, 1000)});
64+
65+
return deferred->Promise();
66+
});
67+
68+
iteratorObject["next"] =
69+
next.Get("bind").As<Function>().Call(next, {iteratorObject});
70+
71+
return iteratorObject;
72+
}
73+
74+
private:
75+
int _from;
76+
int _to;
77+
};
78+
79+
Napi::Object Init(Napi::Env env, Object exports) {
80+
AsyncIteratorExample::Init(env, exports);
81+
return exports;
82+
}
83+
84+
NODE_API_MODULE(example, Init)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
const { AsyncIteratorExample } = require('bindings')('example');
2+
3+
async function main(from, to) {
4+
const iterator = new AsyncIteratorExample(from, to);
5+
for await (const value of iterator) {
6+
console.log(value);
7+
}
8+
}
9+
10+
/*
11+
// The JavaScript equivalent of the node-addon-api C++ code for reference
12+
async function main(from, to) {
13+
class AsyncIteratorExample {
14+
constructor(from, to) {
15+
this.from = from;
16+
this.to = to;
17+
}
18+
19+
[Symbol.asyncIterator]() {
20+
return {
21+
current: this.from,
22+
last: this.to,
23+
next() {
24+
return new Promise(resolve => {
25+
setTimeout(() => {
26+
if (this.current <= this.last) {
27+
resolve({ done: false, value: this.current++ });
28+
} else {
29+
resolve({ done: true });
30+
}
31+
}, 1000)
32+
});
33+
}
34+
}
35+
}
36+
}
37+
const iterator = new AsyncIteratorExample(from, to);
38+
39+
for await (const value of iterator) {
40+
console.log(value);
41+
}
42+
}
43+
*/
44+
45+
main(0, 5)
46+
.catch(e => {
47+
console.error(e);
48+
process.exit(1);
49+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "async-iterator-example",
3+
"version": "0.0.0",
4+
"description": "Async iterator example using node-addon-api",
5+
"main": "index.js",
6+
"private": true,
7+
"dependencies": {
8+
"bindings": "^1.5.0",
9+
"cmake-js": "^6.3.0",
10+
"node-addon-api": "^5.0.0"
11+
},
12+
"scripts": {
13+
"test": "node index.js",
14+
"install": "cmake-js compile"
15+
}
16+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
project (example)
2+
include_directories(${CMAKE_JS_INC} node_modules/node-addon-api/)
3+
cmake_minimum_required(VERSION 3.18)
4+
5+
set(CMAKE_CXX_STANDARD 20)
6+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
7+
include_directories(${CMAKE_JS_INC})
8+
file(GLOB SOURCE_FILES "*.cc")
9+
add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES} ${CMAKE_JS_SRC})
10+
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node")
11+
target_link_libraries(${PROJECT_NAME} ${CMAKE_JS_LIB})
12+
13+
# Include Node-API wrappers
14+
execute_process(COMMAND node -p "require('node-addon-api').include"
15+
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
16+
OUTPUT_VARIABLE NODE_ADDON_API_DIR
17+
)
18+
string(REGEX REPLACE "[\r\n\"]" "" NODE_ADDON_API_DIR ${NODE_ADDON_API_DIR})
19+
20+
target_include_directories(${PROJECT_NAME} PRIVATE ${NODE_ADDON_API_DIR})
21+
22+
# define NAPI_VERSION
23+
add_definitions(-DNAPI_VERSION=6)
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
#include <napi.h>
2+
#include <condition_variable>
3+
#include <functional>
4+
#include <memory>
5+
#include <mutex>
6+
#include <optional>
7+
#include <thread>
8+
9+
using namespace Napi;
10+
11+
class ThreadSafeAsyncIteratorExample
12+
: public ObjectWrap<ThreadSafeAsyncIteratorExample> {
13+
public:
14+
ThreadSafeAsyncIteratorExample(const CallbackInfo& info)
15+
: ObjectWrap<ThreadSafeAsyncIteratorExample>(info),
16+
_current(info[0].As<Number>()),
17+
_last(info[1].As<Number>()) {}
18+
19+
static Object Init(Napi::Env env, Napi::Object exports);
20+
21+
Napi::Value Iterator(const CallbackInfo& info);
22+
23+
private:
24+
using Context = ThreadSafeAsyncIteratorExample;
25+
26+
struct DataType {
27+
std::unique_ptr<Promise::Deferred> deferred;
28+
bool done;
29+
std::optional<int> value;
30+
};
31+
32+
static void CallJs(Napi::Env env,
33+
Function callback,
34+
Context* context,
35+
DataType* data);
36+
37+
using TSFN = TypedThreadSafeFunction<Context, DataType, CallJs>;
38+
39+
using FinalizerDataType = void;
40+
41+
int _current;
42+
int _last;
43+
TSFN _tsfn;
44+
std::thread _thread;
45+
std::unique_ptr<Promise::Deferred> _deferred;
46+
47+
// Thread-safety
48+
std::mutex _mtx;
49+
std::condition_variable _cv;
50+
51+
void threadEntry();
52+
53+
static void FinalizerCallback(Napi::Env env,
54+
void*,
55+
ThreadSafeAsyncIteratorExample* context);
56+
};
57+
58+
Object ThreadSafeAsyncIteratorExample::Init(Napi::Env env,
59+
Napi::Object exports) {
60+
Napi::Function func =
61+
DefineClass(env,
62+
"ThreadSafeAsyncIteratorExample",
63+
{InstanceMethod(Napi::Symbol::WellKnown(env, "asyncIterator"),
64+
&ThreadSafeAsyncIteratorExample::Iterator)});
65+
66+
exports.Set("ThreadSafeAsyncIteratorExample", func);
67+
return exports;
68+
}
69+
70+
Napi::Value ThreadSafeAsyncIteratorExample::Iterator(const CallbackInfo& info) {
71+
auto env = info.Env();
72+
73+
if (_thread.joinable()) {
74+
Napi::Error::New(env, "Concurrent iterations not implemented.")
75+
.ThrowAsJavaScriptException();
76+
return Napi::Value();
77+
}
78+
79+
_tsfn =
80+
TSFN::New(info.Env(),
81+
"tsfn",
82+
0,
83+
1,
84+
this,
85+
std::function<decltype(FinalizerCallback)>(FinalizerCallback));
86+
87+
// To prevent premature garbage collection; Unref in TFSN finalizer
88+
Ref();
89+
90+
// Create thread
91+
_thread = std::thread(&ThreadSafeAsyncIteratorExample::threadEntry, this);
92+
93+
// Create iterable
94+
auto iterable = Napi::Object::New(env);
95+
96+
iterable["next"] =
97+
Function::New(env, [this](const CallbackInfo& info) -> Napi::Value {
98+
std::lock_guard<std::mutex> lk(_mtx);
99+
auto env = info.Env();
100+
if (_deferred) {
101+
Napi::Error::New(env, "Concurrent iterations not implemented.")
102+
.ThrowAsJavaScriptException();
103+
return Napi::Value();
104+
}
105+
_deferred = std::make_unique<Promise::Deferred>(env);
106+
_cv.notify_all();
107+
return _deferred->Promise();
108+
});
109+
110+
return iterable;
111+
}
112+
113+
void ThreadSafeAsyncIteratorExample::threadEntry() {
114+
while (true) {
115+
std::unique_lock<std::mutex> lk(_mtx);
116+
_cv.wait(lk, [this] { return this->_deferred != nullptr; });
117+
auto done = _current > _last;
118+
if (done) {
119+
_tsfn.BlockingCall(new DataType{std::move(this->_deferred), true, {}});
120+
break;
121+
} else {
122+
std::this_thread::sleep_for(
123+
std::chrono::seconds(1)); // Simulate CPU-intensive work
124+
_tsfn.BlockingCall(
125+
new DataType{std::move(this->_deferred), false, _current++});
126+
}
127+
}
128+
_tsfn.Release();
129+
}
130+
131+
void ThreadSafeAsyncIteratorExample::CallJs(Napi::Env env,
132+
Function callback,
133+
Context* context,
134+
DataType* data) {
135+
if (env != nullptr) {
136+
auto value = Object::New(env);
137+
138+
if (data->done) {
139+
value["done"] = Boolean::New(env, true);
140+
} else {
141+
value["done"] = Boolean::New(env, false);
142+
value["value"] = Number::New(env, data->value.value());
143+
}
144+
data->deferred->Resolve(value);
145+
}
146+
147+
if (data != nullptr) {
148+
delete data;
149+
}
150+
}
151+
152+
void ThreadSafeAsyncIteratorExample::FinalizerCallback(
153+
Napi::Env env, void*, ThreadSafeAsyncIteratorExample* context) {
154+
context->_thread.join();
155+
context->Unref();
156+
}
157+
158+
Napi::Object Init(Napi::Env env, Object exports) {
159+
ThreadSafeAsyncIteratorExample::Init(env, exports);
160+
return exports;
161+
}
162+
163+
NODE_API_MODULE(example, Init)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const { ThreadSafeAsyncIteratorExample } = require('bindings')('example');
2+
3+
async function main(from, to) {
4+
const iterator = new ThreadSafeAsyncIteratorExample(from, to);
5+
for await (const value of iterator) {
6+
console.log(value);
7+
}
8+
}
9+
10+
main(0, 5)
11+
.catch(e => {
12+
console.error(e);
13+
process.exit(1);
14+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "threadsafe-async-iterator-example",
3+
"version": "0.0.0",
4+
"description": "Async iterator example with threadsafe functions using node-addon-api",
5+
"main": "index.js",
6+
"private": true,
7+
"dependencies": {
8+
"bindings": "^1.5.0",
9+
"cmake-js": "^6.3.0",
10+
"node-addon-api": "^5.0.0"
11+
},
12+
"scripts": {
13+
"test": "node index.js",
14+
"install": "cmake-js compile"
15+
}
16+
}

0 commit comments

Comments
 (0)