Skip to content
Open
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
8 changes: 8 additions & 0 deletions doc/async_context.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions doc/async_worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -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();
```
Expand Down
7 changes: 7 additions & 0 deletions doc/promises.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions napi-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {}

Expand Down
1 change: 1 addition & 0 deletions napi.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions test/async_context.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
}
1 change: 1 addition & 0 deletions test/async_context.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
20 changes: 20 additions & 0 deletions test/async_worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}