Skip to content

Commit 6706f96

Browse files
committed
clean up optional callback implementation
1 parent cc8de12 commit 6706f96

File tree

6 files changed

+259
-9
lines changed

6 files changed

+259
-9
lines changed

test/threadsafe_function_ex/call.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ module.exports = Promise.all([
88
test(require(`../build/${buildType}/binding_noexcept.node`))
99
]);
1010

11+
/**
12+
* This test ensures the data sent to the NonBlockingCall and the data received
13+
* in the JavaScript callback are the same.
14+
* - Creates a contexted threadsafe function with callback.
15+
* - Makes one call, and waits for call to complete.
16+
* - The callback forwards the item's data to the given JavaScript function in
17+
* the test.
18+
* - Asserts the data is the same.
19+
*/
1120
async function test(binding) {
1221
const data = {};
1322
const tsfn = new binding.threadsafe_function_ex_call.TSFNWrap(tsfnData => {

test/threadsafe_function_ex/context.cc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ class TSFNWrap : public ObjectWrap<TSFNWrap> {
3636

3737
Napi::Value GetContextByCall(const CallbackInfo &info) {
3838
Napi::Env env = info.Env();
39-
auto* callData = new TSFNData(env);
40-
_tsfn.NonBlockingCall( callData );
39+
auto *callData = new TSFNData(env);
40+
_tsfn.NonBlockingCall(callData);
4141
return callData->Promise();
4242
};
4343

test/threadsafe_function_ex/context.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ module.exports = Promise.all([
88
test(require(`../build/${buildType}/binding_noexcept.node`))
99
]);
1010

11+
/**
12+
* The context provided to the threadsafe function's constructor is accessible
13+
* on both the threadsafe function's callback as well the threadsafe function
14+
* itself. This test ensures the context across all three are the same.
15+
* - Creates a contexted threadsafe function with callback.
16+
* - The callback forwards the item's data to the given JavaScript function in
17+
* the test.
18+
* - Makes one call, and waits for call to complete.
19+
* - Asserts the contexts are the same.
20+
*/
1121
async function test(binding) {
1222
const ctx = {};
1323
const tsfn = new binding.threadsafe_function_ex_context.TSFNWrap(ctx);

test/threadsafe_function_ex/simple.cc

Lines changed: 187 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
using namespace Napi;
66

7-
namespace {
7+
namespace simple {
88

99
// Full type of our ThreadSafeFunctionEx
1010
using TSFN = ThreadSafeFunctionEx<>;
@@ -62,10 +62,194 @@ TSFNWrap::TSFNWrap(const CallbackInfo &info)
6262
);
6363
#endif
6464
}
65-
} // namespace
65+
} // namespace simple
6666

67+
namespace existing {
68+
69+
struct DataType {
70+
Promise::Deferred deferred;
71+
bool reject;
72+
};
73+
74+
// CallJs callback function provided to `napi_create_threadsafe_function`. It is
75+
// _NOT_ used by `Napi::ThreadSafeFunctionEx<>`.
76+
static void CallJs(napi_env env, napi_value jsCallback, void * /*context*/,
77+
void *data) {
78+
DataType *casted = static_cast<DataType *>(data);
79+
if (env != nullptr) {
80+
if (data != nullptr) {
81+
napi_value undefined;
82+
napi_status status = napi_get_undefined(env, &undefined);
83+
NAPI_THROW_IF_FAILED(env, status);
84+
if (casted->reject) {
85+
casted->deferred.Reject(undefined);
86+
} else {
87+
casted->deferred.Resolve(undefined);
88+
}
89+
}
90+
}
91+
if (casted != nullptr) {
92+
delete casted;
93+
}
94+
}
95+
96+
// Full type of our ThreadSafeFunctionEx
97+
using TSFN = ThreadSafeFunctionEx<void, DataType>;
98+
99+
// A JS-accessible wrap that holds a TSFN.
100+
class ExistingTSFNWrap : public ObjectWrap<ExistingTSFNWrap> {
101+
public:
102+
static Object Init(Napi::Env env, Object exports);
103+
ExistingTSFNWrap(const CallbackInfo &info);
104+
105+
Napi::Value Release(const CallbackInfo &) {
106+
_tsfn.Release();
107+
return _deferred.Promise();
108+
};
109+
110+
Napi::Value Call(const CallbackInfo &info) {
111+
auto *data =
112+
new DataType{Promise::Deferred::New(info.Env()), info[0].ToBoolean()};
113+
_tsfn.NonBlockingCall(data);
114+
return data->deferred.Promise();
115+
};
116+
117+
private:
118+
TSFN _tsfn;
119+
Promise::Deferred _deferred;
120+
};
121+
122+
Object ExistingTSFNWrap::Init(Napi::Env env, Object exports) {
123+
Function func =
124+
DefineClass(env, "ExistingTSFNWrap",
125+
{InstanceMethod("call", &ExistingTSFNWrap::Call),
126+
InstanceMethod("release", &ExistingTSFNWrap::Release)});
127+
128+
exports.Set("ExistingTSFNWrap", func);
129+
return exports;
130+
}
131+
132+
ExistingTSFNWrap::ExistingTSFNWrap(const CallbackInfo &info)
133+
: ObjectWrap<ExistingTSFNWrap>(info),
134+
_deferred(Promise::Deferred::New(info.Env())) {
135+
136+
auto env = info.Env();
137+
#if NAPI_VERSION == 4
138+
napi_threadsafe_function napi_tsfn;
139+
auto status = napi_create_threadsafe_function(
140+
info.Env(), TSFN::DefaultFunctionFactory(env), nullptr,
141+
String::From(info.Env(), "Test"), 0, 1, nullptr, nullptr, nullptr, CallJs,
142+
&napi_tsfn);
143+
if (status != napi_ok) {
144+
NAPI_THROW_IF_FAILED(env, status);
145+
}
146+
// A threadsafe function on N-API 4 still requires a callback function.
147+
_tsfn = TSFN(napi_tsfn);
148+
#else
149+
napi_threadsafe_function napi_tsfn;
150+
auto status = napi_create_threadsafe_function(
151+
info.Env(), nullptr, nullptr, String::From(info.Env(), "Test"), 0, 1,
152+
nullptr, nullptr, nullptr, CallJs, &napi_tsfn);
153+
if (status != napi_ok) {
154+
NAPI_THROW_IF_FAILED(env, status);
155+
}
156+
_tsfn = TSFN(napi_tsfn);
157+
#endif
158+
}
159+
} // namespace existing
160+
161+
namespace empty {
162+
#if NAPI_VERSION > 4
163+
164+
using Context = void;
165+
166+
struct DataType {
167+
Promise::Deferred deferred;
168+
bool reject;
169+
};
170+
171+
// CallJs callback function
172+
static void CallJs(Napi::Env env, Function jsCallback, Context * /*context*/,
173+
DataType *data) {
174+
if (env != nullptr) {
175+
if (data != nullptr) {
176+
if (data->reject) {
177+
data->deferred.Reject(env.Undefined());
178+
} else {
179+
data->deferred.Resolve(env.Undefined());
180+
}
181+
}
182+
}
183+
if (data != nullptr) {
184+
delete data;
185+
}
186+
}
187+
188+
// Full type of our ThreadSafeFunctionEx
189+
using EmptyTSFN = ThreadSafeFunctionEx<void, DataType, CallJs>;
190+
191+
// A JS-accessible wrap that holds a TSFN.
192+
class EmptyTSFNWrap : public ObjectWrap<EmptyTSFNWrap> {
193+
public:
194+
static Object Init(Napi::Env env, Object exports);
195+
EmptyTSFNWrap(const CallbackInfo &info);
196+
197+
Napi::Value Release(const CallbackInfo &) {
198+
_tsfn.Release();
199+
return _deferred.Promise();
200+
};
201+
202+
Napi::Value Call(const CallbackInfo &info) {
203+
if (info.Length() == 0 || !info[0].IsBoolean()) {
204+
NAPI_THROW(
205+
Napi::TypeError::New(info.Env(), "Expected argument 0 to be boolean"),
206+
Value());
207+
}
208+
209+
auto *data =
210+
new DataType{Promise::Deferred::New(info.Env()), info[0].ToBoolean()};
211+
_tsfn.NonBlockingCall(data);
212+
return data->deferred.Promise();
213+
};
214+
215+
private:
216+
EmptyTSFN _tsfn;
217+
Promise::Deferred _deferred;
218+
};
219+
220+
Object EmptyTSFNWrap::Init(Napi::Env env, Object exports) {
221+
Function func =
222+
DefineClass(env, "EmptyTSFNWrap",
223+
{InstanceMethod("call", &EmptyTSFNWrap::Call),
224+
InstanceMethod("release", &EmptyTSFNWrap::Release)});
225+
226+
exports.Set("EmptyTSFNWrap", func);
227+
return exports;
228+
}
229+
230+
EmptyTSFNWrap::EmptyTSFNWrap(const CallbackInfo &info)
231+
: ObjectWrap<EmptyTSFNWrap>(info),
232+
_deferred(Promise::Deferred::New(info.Env())) {
233+
234+
auto env = info.Env();
235+
_tsfn = EmptyTSFN::New(env, // napi_env env,
236+
"Test", // ResourceString resourceName,
237+
0, // size_t maxQueueSize,
238+
1 // size_t initialThreadCount
239+
);
240+
}
241+
#endif
242+
} // namespace empty
67243
Object InitThreadSafeFunctionExSimple(Env env) {
68-
return TSFNWrap::Init(env, Object::New(env));
244+
245+
#if NAPI_VERSION > 4
246+
return empty::EmptyTSFNWrap::Init(
247+
env, existing::ExistingTSFNWrap::Init(
248+
env, simple::TSFNWrap::Init(env, Object::New(env))));
249+
#else
250+
return existing::ExistingTSFNWrap::Init(
251+
env, simple::TSFNWrap::Init(env, Object::New(env)));
252+
#endif
69253
}
70254

71255
#endif

test/threadsafe_function_ex/simple.js

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,59 @@
11
'use strict';
22

3+
const assert = require('assert');
34
const buildType = process.config.target_defaults.default_configuration;
45

56
module.exports = Promise.all([
67
test(require(`../build/${buildType}/binding.node`)),
78
test(require(`../build/${buildType}/binding_noexcept.node`))
8-
]);
9+
].reduce((p, c) => p.concat(c)), []);
910

10-
async function test(binding) {
11-
const ctx = {};
12-
const tsfn = new binding.threadsafe_function_ex_simple.TSFNWrap(ctx);
11+
function test(binding) {
12+
return [
13+
// testSimple(binding),
14+
testEmpty(binding)
15+
];
16+
}
17+
18+
/**
19+
* A simple, fire-and-forget test.
20+
* - Creates a threadsafe function with no context or callback.
21+
* - The node-addon-api 'no callback' feature is implemented by passing either a
22+
* no-op `Function` on N-API 4 or `std::nullptr` on N-API 5+ to the underlying
23+
* `napi_create_threadsafe_function` call.
24+
* - Makes one call, releases, then waits for finalization.
25+
* - Inherently ignores the state of the item once it has been added to the
26+
* queue. Since there are no callbacks or context, it is impossible to capture
27+
* the state.
28+
*/
29+
async function testSimple(binding) {
30+
const tsfn = new binding.threadsafe_function_ex_simple.TSFNWrap();
1331
tsfn.call();
1432
await tsfn.release();
1533
}
34+
35+
/**
36+
* **ONLY ON N-API 5+**. The optional JavaScript function callback feature is
37+
* not available in N-API <= 4.
38+
* - Creates a threadsafe function with no JavaScript context or callback.
39+
* - Makes two calls, expecting the first to resolve and the second to reject.
40+
* - Waits for Node to process the items on the queue prior releasing the
41+
* threadsafe function.
42+
*/
43+
async function testEmpty(binding) {
44+
const { EmptyTSFNWrap } = binding.threadsafe_function_ex_simple;
45+
46+
if (typeof EmptyTSFNWrap === 'function') {
47+
const tsfn = new binding.threadsafe_function_ex_simple.EmptyTSFNWrap();
48+
await tsfn.call(false /* reject */);
49+
let caught = false;
50+
try {
51+
await tsfn.call(true /* reject */);
52+
} catch (ex) {
53+
caught = true;
54+
}
55+
56+
assert.ok(caught, 'The promise rejection was not caught');
57+
await tsfn.release();
58+
}
59+
}

test/threadsafe_function_ex/threadsafe.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ const common = require('../common');
77
test(require(`../build/${buildType}/binding.node`));
88
test(require(`../build/${buildType}/binding_noexcept.node`));
99

10+
/**
11+
* This spec replicates the non-`Ex` multi-threaded spec using the `Ex` API.
12+
*/
1013
function test(binding) {
1114
const expectedArray = (function(arrayLength) {
1215
const result = [];

0 commit comments

Comments
 (0)