From 73b3f769111467d42eb10349d2890945367a4062 Mon Sep 17 00:00:00 2001 From: Momtchil Momtchev Date: Wed, 23 Sep 2026 14:39:44 +0200 Subject: [PATCH] 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'); + }) +});