diff --git a/doc/node_misc.md b/doc/node_misc.md index 17578e34..1dfd9752 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. +`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: ```c++ @@ -39,6 +41,8 @@ class AsyncResource { const char* method, int argc, v8::Local* argv); + + inline operator node::async_context(); }; ``` diff --git a/nan.h b/nan.h index 9253dbde..07d4344b 100644 --- a/nan.h +++ b/nan.h @@ -660,6 +660,12 @@ class AsyncResource { #endif } +#if NODE_MODULE_VERSION >= NODE_9_0_MODULE_VERSION + inline operator node::async_context() const { + return 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..5fdfbada --- /dev/null +++ b/test/cpp/asyncresource-promise.cpp @@ -0,0 +1,74 @@ +/********************************************************************* + * 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)).ToChecked(); + + v8::Local target = New(); + + // Run the callback in the async context. + node::CallbackScope callback_scope(v8::Isolate::GetCurrent(), target, + static_cast(*delay_request)); + + 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'); + }) +});