This document summarizes key concepts from Node-addon-api documentation for handling threads, promises, and async operations in native addons.
ThreadSafeFunction::New() with initial thread countAcquire() when starting to use itRelease() when doneThreadSafeFunction tsfn = ThreadSafeFunction::New(
env,
callback,
"Resource Name",
0, // Unlimited queue
1, // Initial thread count
[]( Napi::Env ) { // Finalizer
// Clean up after threads
nativeThread.join();
}
);
Thread Management:
Acquire() and BlockingCall()napi_closing status during shutdownRelease() is the last call from a threadShutdown Handling:
Abort() to signal no more calls can be madePromise::Deferred objectsResolve() or Reject()Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);
// Store promise for return
Napi::Promise promise = deferred.Promise();
// Later, resolve or reject
deferred.Resolve(result); // or
deferred.Reject(error);
Main Thread Only:
Napi::Env, Napi::Value, or Napi::ReferenceAny Thread:
Acquire(), Release(), BlockingCall()Cause: ThreadSafeFunction not properly released or threads still running
Solution:
Acquire() with Release()napi_closing status gracefullyCause: Async operation fails to resolve/reject promise before shutdown
Solution:
Cause: Object deleted while detached thread still running
Solution:
Execute(): Runs on worker thread (no Node.js API access)OnOK(): Called on main thread when work completes successfullyOnError(): Called on main thread if an error occursclass MyWorker : public Napi::AsyncWorker {
public:
MyWorker(Napi::Function& callback, std::string data)
: AsyncWorker(callback), data_(data) {}
void Execute() override {
// This runs on a worker thread
// Do NOT use any Napi:: methods here
result_ = ProcessData(data_);
}
void OnOK() override {
// This runs on the main thread
Callback().Call({Env().Null(), String::New(Env(), result_)});
}
void OnError(const Napi::Error& error) override {
// This runs on the main thread
Callback().Call({error.Value()});
}
private:
std::string data_;
std::string result_;
};
class ProgressWorker : public Napi::AsyncProgressWorker<int> {
public:
ProgressWorker(Napi::Function& callback, Napi::Function& progress)
: AsyncProgressWorker(callback), progress_callback_(Napi::Persistent(progress)) {}
void Execute(const ExecutionProgress& progress) override {
for (int i = 0; i < 100; i++) {
// Send progress update
progress.Send(&i, 1);
// Do work...
}
}
void OnProgress(const int* data, size_t count) override {
// Called on main thread with progress data
if (!progress_callback_.IsEmpty()) {
progress_callback_.Call({Napi::Number::New(Env(), *data)});
}
}
private:
Napi::FunctionReference progress_callback_;
};
Based on this analysis, the current BackupJob implementation has several issues:
The current implementation uses std::thread(...).detach() which means:
Anti-pattern Example:
std::thread([work]() {
// Do work
}).detach(); // BAD: Cannot join this thread
Correct Pattern:
// Store thread handle and join in destructor/finalizer
std::thread worker([work]() {
// Do work
});
// Later, in cleanup:
worker.join(); // Wait for thread to complete
Note: Adding arbitrary timeouts or forcing garbage collection in tests is NOT a solution. These are band-aids that mask the underlying design flaw.