diff --git a/doc/async_context.md b/doc/async_context.md index 06a606cd9..fc61adf61 100644 --- a/doc/async_context.md +++ b/doc/async_context.md @@ -11,6 +11,14 @@ correct async execution context. ### Constructor +Creates a new uninitialized `Napi::AsyncContext`. + +```cpp +Napi::AsyncContext::AsyncContext(); +``` + +### Constructor + Creates a new `Napi::AsyncContext`. ```cpp diff --git a/doc/async_worker.md b/doc/async_worker.md index 2250d541d..9e4b55980 100644 --- a/doc/async_worker.md +++ b/doc/async_worker.md @@ -111,6 +111,13 @@ The default implementation runs the `Callback` optionally provided when the arguments. The arguments to the `Callback` can be provided by overriding the `GetResult()` method. +> [!NOTE] +> `Napi::AsyncWorker` is backed by Node-API's `napi_create_async_work`. Node.js +> automatically wraps the completion phase in an asynchronous callback scope +> (`node::CallbackScope`), ensuring that microtasks (e.g. Promises) and +> `process.nextTick` handlers scheduled inside `OnOK` or `OnError` are +> deterministically drained upon completion. + ```cpp virtual void Napi::AsyncWorker::OnOK(); ``` diff --git a/doc/promises.md b/doc/promises.md index b4ab83389..ee8ff042d 100644 --- a/doc/promises.md +++ b/doc/promises.md @@ -65,6 +65,13 @@ Resolves the `Napi::Promise` object held by the `Napi::Promise::Deferred` object * `[in] value`: The Node-API primitive value with which to resolve the `Napi::Promise`. +> [!NOTE] +> If resolving or rejecting a `Promise::Deferred` asynchronously outside of a +> JavaScript callback execution context (such as from a custom background thread +> or libuv async handle), wrap the call in a `Napi::CallbackScope` with a +> `Napi::AsyncContext` to ensure microtasks (e.g. `.then()` handlers) are +> properly drained by the runtime. + ### Reject ```cpp diff --git a/napi-inl.h b/napi-inl.h index 59f1ca14e..9cc8d637a 100644 --- a/napi-inl.h +++ b/napi-inl.h @@ -5585,6 +5585,8 @@ inline Napi::Env CallbackScope::Env() const { // AsyncContext class //////////////////////////////////////////////////////////////////////////////// +inline AsyncContext::AsyncContext() : _env(nullptr), _context(nullptr) {} + inline AsyncContext::AsyncContext(napi_env env, const char* resource_name) : AsyncContext(env, resource_name, Object::New(env)) {} diff --git a/napi.h b/napi.h index 6b4692c7d..0804a9a66 100644 --- a/napi.h +++ b/napi.h @@ -2783,6 +2783,7 @@ class CallbackScope { class AsyncContext { public: + AsyncContext(); explicit AsyncContext(napi_env env, const char* resource_name); explicit AsyncContext(napi_env env, const char* resource_name, diff --git a/test/async_context.cc b/test/async_context.cc index be2e1d917..c4646b658 100644 --- a/test/async_context.cc +++ b/test/async_context.cc @@ -23,6 +23,14 @@ static Boolean AssertAsyncContextReturnCorrectEnv(const CallbackInfo& info) { AsyncContext context(info.Env(), "empty_context_test"); return Boolean::New(info.Env(), context.Env() == info.Env()); } + +static Boolean AssertDefaultConstructor(const CallbackInfo& info) { + AsyncContext context; + AsyncContext move_target = std::move(context); + AsyncContext initialized(info.Env(), "test"); + move_target = std::move(initialized); + return Boolean::New(info.Env(), move_target.Env() == info.Env()); +} } // end anonymous namespace Object InitAsyncContext(Env env) { @@ -32,5 +40,6 @@ Object InitAsyncContext(Env env) { Function::New(env, MakeCallbackNoResource); exports["asyncCxtReturnCorrectEnv"] = Function::New(env, AssertAsyncContextReturnCorrectEnv); + exports["defaultConstructor"] = Function::New(env, AssertDefaultConstructor); return exports; } diff --git a/test/async_context.js b/test/async_context.js index 6cf0418f6..82efb56cd 100644 --- a/test/async_context.js +++ b/test/async_context.js @@ -109,6 +109,7 @@ async function makeCallbackWithoutResource (binding) { function assertAsyncContextReturnsCorrectEnv (binding) { assert.strictEqual(binding.asynccontext.asyncCxtReturnCorrectEnv(), true); + assert.strictEqual(binding.asynccontext.defaultConstructor(), true); } async function test (binding) { diff --git a/test/async_worker.js b/test/async_worker.js index 16e2a8e96..b2774feb0 100644 --- a/test/async_worker.js +++ b/test/async_worker.js @@ -261,4 +261,24 @@ async function test (binding) { ]); }).catch(common.mustNotCall()); } + + // Verify that microtasks (Promise reactions) and process.nextTick scheduled + // in AsyncWorker completion callbacks are drained deterministically upon completion. + { + let microtaskRan = false; + let nextTickRan = false; + await new Promise((resolve) => { + binding.asyncworker.doWork(true, {}, function () { + Promise.resolve().then(() => { + microtaskRan = true; + }); + process.nextTick(() => { + nextTickRan = true; + }); + resolve(); + }, 'test data'); + }); + assert.strictEqual(microtaskRan, true, 'Microtask should have run'); + assert.strictEqual(nextTickRan, true, 'process.nextTick should have run'); + } }