From 73b3f769111467d42eb10349d2890945367a4062 Mon Sep 17 00:00:00 2001 From: Momtchil Momtchev Date: Wed, 23 Sep 2026 14:39:44 +0200 Subject: [PATCH 1/5] add AsyncResource::makeCallbackScope --- doc/node_misc.md | 5 ++ nan.h | 6 +++ test/binding.gyp | 4 ++ test/cpp/asyncresource-promise.cpp | 73 +++++++++++++++++++++++++++ test/js/asyncresource-promise-test.js | 54 ++++++++++++++++++++ 5 files changed, 142 insertions(+) create mode 100644 test/cpp/asyncresource-promise.cpp create mode 100644 test/js/asyncresource-promise-test.js diff --git a/doc/node_misc.md b/doc/node_misc.md index 17578e34..fa1ceeb2 100644 --- a/doc/node_misc.md +++ b/doc/node_misc.md @@ -16,6 +16,8 @@ and `node::MakeCallback`. Using this mechanism to call back into JavaScript, as `v8::Function::Call` ensures that the callback is executed in the correct async context. This ensures that async mechanisms such as domains and [async_hooks][] function correctly. +`makeCallBackScope` is a factory for `node::callbackScope`. A `callbackScope` ensures that micro-tasks such as `Pormise` handlers and `nextTick` callbacks will be executed after the function returns. It is a RAII guard and must always be allocated on the stack. This must be used when an async worker does an action with an effect on the event loop such as resolving a `Promise` without calling JavaScript - otherwise `makeCallback` already takes care of the micro-tasks queue. + Definition: ```c++ @@ -39,6 +41,9 @@ class AsyncResource { const char* method, int argc, v8::Local* argv); + + node::CallbackScope node::CallbackScope makeCallbackScope( + v8::Local target); }; ``` diff --git a/nan.h b/nan.h index 9253dbde..a0a18613 100644 --- a/nan.h +++ b/nan.h @@ -660,6 +660,12 @@ class AsyncResource { #endif } +#if NODE_MODULE_VERSION >= NODE_9_0_MODULE_VERSION + inline node::CallbackScope makeCallbackScope(v8::Local target) { + return {v8::Isolate::GetCurrent(), target, context}; + } +#endif + private: NAN_DISALLOW_ASSIGN_COPY_MOVE(AsyncResource) #if NODE_MODULE_VERSION >= NODE_9_0_MODULE_VERSION diff --git a/test/binding.gyp b/test/binding.gyp index caddcae3..1bdbc64d 100644 --- a/test/binding.gyp +++ b/test/binding.gyp @@ -98,6 +98,10 @@ "target_name" : "asyncresource" , "sources" : [ "cpp/asyncresource.cpp" ] } + , { + "target_name" : "asyncresource-promise" + , "sources" : [ "cpp/asyncresource-promise.cpp" ] + } , { "target_name" : "callbackcontext" , "sources" : [ "cpp/callbackcontext.cpp" ] diff --git a/test/cpp/asyncresource-promise.cpp b/test/cpp/asyncresource-promise.cpp new file mode 100644 index 00000000..91ed96f5 --- /dev/null +++ b/test/cpp/asyncresource-promise.cpp @@ -0,0 +1,73 @@ +/********************************************************************* + * NAN - Native Abstractions for Node.js + * + * Copyright (c) 2018 NAN contributors + * + * MIT License + ********************************************************************/ + +#include + +#include "sleep.h" // NOLINT(build/include_subdir) + +using namespace Nan; // NOLINT(build/namespaces) + +class DelayRequest : public AsyncResource { + public: + DelayRequest(int milliseconds_, v8::Local resolver_) + : AsyncResource("nan:test.DelayPromise"), + milliseconds(milliseconds_) { + resolver.Reset(resolver_); + request.data = this; + } + ~DelayRequest() { + resolver.Reset(); + } + + Persistent resolver; + uv_work_t request; + int milliseconds; +}; + +void Delay(uv_work_t* req) { + DelayRequest *delay_request = static_cast(req->data); + Sleep(delay_request->milliseconds); +} + +void AfterDelay(uv_work_t* req, int status) { + HandleScope scope; + + DelayRequest *delay_request = static_cast(req->data); + v8::Local resolver = New(delay_request->resolver); + resolver->Resolve(Nan::GetCurrentContext(), New(true)); + + v8::Local target = New(); + + // Run the callback in the async context. + node::CallbackScope callback_scope = delay_request->makeCallbackScope(target); + + delete delay_request; +} + +NAN_METHOD(Delay) { + int delay = To(info[0]).FromJust(); + v8::Local resolver = + v8::Promise::Resolver::New(Nan::GetCurrentContext()).ToLocalChecked(); + + DelayRequest* delay_request = new DelayRequest(delay, resolver); + + info.GetReturnValue().Set(resolver->GetPromise()); + + uv_queue_work( + GetCurrentEventLoop() + , &delay_request->request + , Delay + , reinterpret_cast(AfterDelay)); +} + +NAN_MODULE_INIT(Init) { + Set(target, New("delay").ToLocalChecked(), + GetFunction(New(Delay)).ToLocalChecked()); +} + +NODE_MODULE(asyncresource_promise, Init) diff --git a/test/js/asyncresource-promise-test.js b/test/js/asyncresource-promise-test.js new file mode 100644 index 00000000..6dfb71e4 --- /dev/null +++ b/test/js/asyncresource-promise-test.js @@ -0,0 +1,54 @@ +/********************************************************************* + * NAN - Native Abstractions for Node.js + * + * Copyright (c) 2026 NAN contributors + * + * MIT License + ********************************************************************/ + +const version = process.versions.node.split('.'); +if (version[0] < 9) { + process.exit(0); +} + +try { + require('async_hooks'); +} catch (e) { + process.exit(0); +} + +const test = require('tap').test + , testRoot = require('path').resolve(__dirname, '..') + , delay = require('bindings')({ module_root: testRoot, bindings: 'asyncresource-promise' }).delay + , asyncHooks = require('async_hooks'); + +test('asyncresource-promise', function (t) { + t.plan(2); + + var beforeCalled = false; + var promiseResolveCalled = false; + + var hooks = asyncHooks.createHook({ + init: function (asyncId, type, triggerAsyncId, resource) { + if (type === 'nan:test.DelayPromise') { + resourceAsyncId = asyncId; + } + }, + before: function (asyncId) { + if (asyncId === resourceAsyncId) { + beforeCalled = true; + } + }, + promiseResolve: function (asyncId) { + promiseResolveCalled = true; + } + + }); + hooks.enable(); + + originalExecutionAsyncId = asyncHooks.executionAsyncId(); + delay(1000).then(function() { + t.ok(beforeCalled, 'before should have been called'); + t.ok(promiseResolveCalled, 'promiseResolve should have been called'); + }) +}); From 2204f256058f7f6f6e9905586d6cc4565203c67e Mon Sep 17 00:00:00 2001 From: Momtchil Momtchev Date: Thu, 15 Oct 2026 14:52:48 +0200 Subject: [PATCH 2/5] replace the awkward factory method with an operator to match the node-addon-api design --- doc/node_misc.md | 5 ++--- nan.h | 4 ++-- test/cpp/asyncresource-promise.cpp | 3 ++- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/node_misc.md b/doc/node_misc.md index fa1ceeb2..1dfd9752 100644 --- a/doc/node_misc.md +++ b/doc/node_misc.md @@ -16,7 +16,7 @@ and `node::MakeCallback`. Using this mechanism to call back into JavaScript, as `v8::Function::Call` ensures that the callback is executed in the correct async context. This ensures that async mechanisms such as domains and [async_hooks][] function correctly. -`makeCallBackScope` is a factory for `node::callbackScope`. A `callbackScope` ensures that micro-tasks such as `Pormise` handlers and `nextTick` callbacks will be executed after the function returns. It is a RAII guard and must always be allocated on the stack. This must be used when an async worker does an action with an effect on the event loop such as resolving a `Promise` without calling JavaScript - otherwise `makeCallback` already takes care of the micro-tasks queue. +`operator node::async_context` allows to retrieve the underlying async context in a form that can be used to construct a `node::CallbackScope`. A `node::CallbackScope` ensures that micro-tasks such as `Promise` handlers and `nextTick` callbacks will be executed after the function returns. It is a RAII guard and must always be allocated on the stack. This must be used when an async worker does an action with an effect on the event loop such as resolving a `Promise` without calling JavaScript - otherwise `makeCallback` already takes care of the micro-tasks queue. Refer to the `asyncresource-promise.cpp` unit test for an example that resolves a `Promise` from an async context. Definition: @@ -42,8 +42,7 @@ class AsyncResource { int argc, v8::Local* argv); - node::CallbackScope node::CallbackScope makeCallbackScope( - v8::Local target); + inline operator node::async_context(); }; ``` diff --git a/nan.h b/nan.h index a0a18613..1a8e94ef 100644 --- a/nan.h +++ b/nan.h @@ -661,8 +661,8 @@ class AsyncResource { } #if NODE_MODULE_VERSION >= NODE_9_0_MODULE_VERSION - inline node::CallbackScope makeCallbackScope(v8::Local target) { - return {v8::Isolate::GetCurrent(), target, context}; + inline operator node::async_context() { + return (node::async_context)context; } #endif diff --git a/test/cpp/asyncresource-promise.cpp b/test/cpp/asyncresource-promise.cpp index 91ed96f5..6795f024 100644 --- a/test/cpp/asyncresource-promise.cpp +++ b/test/cpp/asyncresource-promise.cpp @@ -44,7 +44,8 @@ void AfterDelay(uv_work_t* req, int status) { v8::Local target = New(); // Run the callback in the async context. - node::CallbackScope callback_scope = delay_request->makeCallbackScope(target); + node::CallbackScope callback_scope(v8::Isolate::GetCurrent(), target, + (node::async_context)*delay_request); delete delay_request; } From 234b300aef642d3655ee7cf708b8c8e9c5814a47 Mon Sep 17 00:00:00 2001 From: Momtchil Momtchev Date: Thu, 15 Oct 2026 15:14:37 +0200 Subject: [PATCH 3/5] do not ignore the returned value of Resolver::Resolve() --- test/cpp/asyncresource-promise.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cpp/asyncresource-promise.cpp b/test/cpp/asyncresource-promise.cpp index 6795f024..1e33f580 100644 --- a/test/cpp/asyncresource-promise.cpp +++ b/test/cpp/asyncresource-promise.cpp @@ -39,7 +39,7 @@ void AfterDelay(uv_work_t* req, int status) { DelayRequest *delay_request = static_cast(req->data); v8::Local resolver = New(delay_request->resolver); - resolver->Resolve(Nan::GetCurrentContext(), New(true)); + resolver->Resolve(Nan::GetCurrentContext(), New(true)).ToChecked(); v8::Local target = New(); From 460e5545fa279d15ad44a05b9cb59b5c058b5895 Mon Sep 17 00:00:00 2001 From: Momtchil Momtchev Date: Thu, 15 Oct 2026 16:45:58 +0200 Subject: [PATCH 4/5] remove the superfluous typecast --- nan.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nan.h b/nan.h index 1a8e94ef..07d4344b 100644 --- a/nan.h +++ b/nan.h @@ -661,8 +661,8 @@ class AsyncResource { } #if NODE_MODULE_VERSION >= NODE_9_0_MODULE_VERSION - inline operator node::async_context() { - return (node::async_context)context; + inline operator node::async_context() const { + return context; } #endif From f9237f688e3f6c04a349097515efdffbdad39601 Mon Sep 17 00:00:00 2001 From: Momtchil Momtchev Date: Fri, 16 Oct 2026 13:36:35 +0200 Subject: [PATCH 5/5] replace the C-style cast with static_cast --- test/cpp/asyncresource-promise.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cpp/asyncresource-promise.cpp b/test/cpp/asyncresource-promise.cpp index 1e33f580..5fdfbada 100644 --- a/test/cpp/asyncresource-promise.cpp +++ b/test/cpp/asyncresource-promise.cpp @@ -45,7 +45,7 @@ void AfterDelay(uv_work_t* req, int status) { // Run the callback in the async context. node::CallbackScope callback_scope(v8::Isolate::GetCurrent(), target, - (node::async_context)*delay_request); + static_cast(*delay_request)); delete delay_request; }