diff --git a/Makefile b/Makefile
index fc164f88..1b97e3f6 100644
--- a/Makefile
+++ b/Makefile
@@ -35,6 +35,7 @@ LINT_SOURCES = \
nan_weak.h \
test/cpp/accessors.cpp \
test/cpp/accessors2.cpp \
+ test/cpp/asyncresource.cpp \
test/cpp/asyncworker.cpp \
test/cpp/asyncprogressworker.cpp \
test/cpp/asyncprogressworkerstream.cpp \
diff --git a/doc/node_misc.md b/doc/node_misc.md
index 8aa080f5..01343520 100644
--- a/doc/node_misc.md
+++ b/doc/node_misc.md
@@ -1,16 +1,69 @@
## Miscellaneous Node Helpers
+ - Nan::AsyncResource
- Nan::MakeCallback()
- NAN_MODULE_INIT()
- Nan::Export()
+
+### Nan::AsyncResource
+
+This class is analogous to the `AsyncResource` JavaScript class exposed by Node's [async_hooks][] API.
+
+When calling back into JavaScript asynchornously, special care must be taken to ensure that the runtime can properly track
+async hops. `Nan::AsyncResource` is a class that provides an RAII wrapper around `node::EmitAsyncInit`, `node::EmitAsyncDestroy`,
+and `node::MakeCallback`. Using this mechanism to call back into JavaScript, as opposed to `Nan::MakeCallback` or
+`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.
+
+Definition:
+
+```c++
+class AsyncResource {
+ public:
+ AsyncResource(MaybeLocal maybe_resource, v8::Local name);
+ AsyncResource(MaybeLocal maybe_resource, const char* name);
+ ~AsyncResource();
+
+ v8::MaybeLocal runInAsyncScope(v8::Local target,
+ v8::Local func,
+ int argc,
+ v8::Local* argv,
+ Nan::async_context async_context);
+ v8::MaybeLocal runInAsyncScope(v8::Local target,
+ v8::Local symbol,
+ int argc,
+ v8::Local* argv,
+ Nan::async_context async_context);
+ v8::MaybeLocal runInAsyncScope(v8::Local target,
+ const char* method,
+ int argc,
+ v8::Local* argv,
+ Nan::async_context async_context);
+};
+```
+
+* `maybe_resource`: An optional object associated with the async work that will be passed to the possible [async_hooks][]
+ `init` hook.
+* `name`: Identified for the kind of resource that is being provided for diagnostics information exposed by the [async_hooks][]
+ API. This will be passed to the possible `init` hook as the `type`. To avoid name collisions with other modules we recommend
+ that the name include the name of the owning module as a prefix. For example `mysql` module could use something like
+ `mysql:batch-db-query-resource`.
+* When calling JS on behalf of this resource, one can use `runInAsyncScope`. This will ensure that the callback runs in the
+ correct async execution context.
+* `AsyncDestroy` is automatically called when an AsyncResource object is destroyed.
+
+For more details, see the Node [async_hooks][] documentation. You might also want to take a look at the documentation for the
+[N-API counterpart][napi]. For example usage, see the `asyncresource.cpp` example in the `test/cpp` directory.
### Nan::MakeCallback()
-Wrappers around `node::MakeCallback()` providing a consistent API across all supported versions of Node.
+Wrappers around the legacy `node::MakeCallback()` APIs.
-Use `MakeCallback()` rather than using `v8::Function#Call()` directly in order to properly process internal Node functionality including domains, async hooks, the microtask queue, and other debugging functionality.
+We recommend that you use the `AsyncResource` class and `AsyncResource::runInAsyncScope` instead of using `Nan::MakeCallback` or
+`v8::Function#Call()` directly. `AsyncResource` properly takes care of running the callback in the correct async execution
+context – something that is essential for functionality like domains, async_hooks and async debugging.
Signatures:
@@ -61,3 +114,6 @@ NAN_MODULE_INIT(Init) {
NAN_EXPORT(target, Foo);
}
```
+
+[async_hooks]: https://nodejs.org/dist/latest-v9.x/docs/api/async_hooks.html
+[napi]: https://nodejs.org/dist/latest-v9.x/docs/api/n-api.html#n_api_custom_asynchronous_operations
diff --git a/nan.h b/nan.h
index 7c7699ff..ae4894f3 100644
--- a/nan.h
+++ b/nan.h
@@ -1273,6 +1273,104 @@ class Utf8String {
#endif // NODE_MODULE_VERSION
+//=== async_context ============================================================
+
+#if NODE_MODULE_VERSION >= NODE_8_0_MODULE_VERSION
+ typedef node::async_context async_context;
+#else
+ struct async_context {};
+#endif
+
+// === AsyncResource ===========================================================
+
+ class AsyncResource {
+ public:
+ AsyncResource(
+ MaybeLocal maybe_resource
+ , v8::Local resource_name) {
+#if NODE_MODULE_VERSION >= NODE_8_0_MODULE_VERSION
+ v8::Isolate* isolate = v8::Isolate::GetCurrent();
+
+ v8::Local resource =
+ maybe_resource.IsEmpty() ? New()
+ : maybe_resource.ToLocalChecked();
+
+ node::async_context context =
+ node::EmitAsyncInit(isolate, resource, resource_name);
+ asyncContext = static_cast(context);
+#endif
+ }
+
+ AsyncResource(MaybeLocal maybe_resource, const char* name) {
+#if NODE_MODULE_VERSION >= NODE_8_0_MODULE_VERSION
+ v8::Isolate* isolate = v8::Isolate::GetCurrent();
+
+ v8::Local resource =
+ maybe_resource.IsEmpty() ? New()
+ : maybe_resource.ToLocalChecked();
+ v8::Local name_string =
+ New(name).ToLocalChecked();
+ node::async_context context =
+ node::EmitAsyncInit(isolate, resource, name_string);
+ asyncContext = static_cast(context);
+#endif
+ }
+
+ ~AsyncResource() {
+#if NODE_MODULE_VERSION >= NODE_8_0_MODULE_VERSION
+ v8::Isolate* isolate = v8::Isolate::GetCurrent();
+ node::async_context node_context =
+ static_cast(asyncContext);
+ node::EmitAsyncDestroy(isolate, node_context);
+#endif
+ }
+
+ inline MaybeLocal runInAsyncScope(
+ v8::Local target
+ , v8::Local func
+ , int argc
+ , v8::Local* argv) {
+#if NODE_MODULE_VERSION < NODE_8_0_MODULE_VERSION
+ return MakeCallback(target, func, argc, argv);
+#else
+ return node::MakeCallback(
+ v8::Isolate::GetCurrent(), target, func, argc, argv,
+ static_cast(asyncContext));
+#endif
+ }
+
+ inline MaybeLocal runInAsyncScope(
+ v8::Local target
+ , v8::Local symbol
+ , int argc
+ , v8::Local* argv) {
+#if NODE_MODULE_VERSION < NODE_8_0_MODULE_VERSION
+ return MakeCallback(target, symbol, argc, argv);
+#else
+ return node::MakeCallback(
+ v8::Isolate::GetCurrent(), target, symbol, argc, argv,
+ static_cast(asyncContext));
+#endif
+ }
+
+ inline MaybeLocal runInAsyncScope(
+ v8::Local target
+ , const char* method
+ , int argc
+ , v8::Local* argv) {
+#if NODE_MODULE_VERSION < NODE_8_0_MODULE_VERSION
+ return MakeCallback(target, method, argc, argv);
+#else
+ return node::MakeCallback(
+ v8::Isolate::GetCurrent(), target, method, argc, argv,
+ static_cast(asyncContext));
+#endif
+ }
+
+ private:
+ async_context asyncContext;
+ };
+
typedef void (*FreeCallback)(char *data, void *hint);
typedef const FunctionCallbackInfo& NAN_METHOD_ARGS_TYPE;
diff --git a/test/binding.gyp b/test/binding.gyp
index 2c70cb9e..28d51a4a 100644
--- a/test/binding.gyp
+++ b/test/binding.gyp
@@ -90,6 +90,10 @@
"target_name" : "makecallback"
, "sources" : [ "cpp/makecallback.cpp" ]
}
+ , {
+ "target_name" : "asyncresource"
+ , "sources" : [ "cpp/asyncresource.cpp" ]
+ }
, {
"target_name" : "isolatedata"
, "sources" : [ "cpp/isolatedata.cpp" ]
diff --git a/test/cpp/asyncresource.cpp b/test/cpp/asyncresource.cpp
new file mode 100644
index 00000000..8777e661
--- /dev/null
+++ b/test/cpp/asyncresource.cpp
@@ -0,0 +1,69 @@
+/*********************************************************************
+ * NAN - Native Abstractions for Node.js
+ *
+ * Copyright (c) 2018 NAN contributors
+ *
+ * MIT License
+ ********************************************************************/
+
+#include
+#include
+
+using namespace Nan; // NOLINT(build/namespaces)
+
+class DelayRequest : public AsyncResource {
+ public:
+ DelayRequest(int milliseconds_, v8::Local callback_)
+ : AsyncResource(MaybeLocal(), "nan:test.DelayRequest"),
+ milliseconds(milliseconds_) {
+ callback.Reset(callback_);
+ request.data = this;
+ }
+ ~DelayRequest() {
+ callback.Reset();
+ }
+
+ Persistent callback;
+ uv_work_t request;
+ int milliseconds;
+};
+
+void Delay(uv_work_t* req) {
+ DelayRequest *delay_request = static_cast(req->data);
+ sleep(delay_request->milliseconds / 1000);
+}
+
+void AfterDelay(uv_work_t* req, int status) {
+ HandleScope scope;
+
+ DelayRequest *delay_request = static_cast(req->data);
+ v8::Local callback = New(delay_request->callback);
+ v8::Local argv[0] = {};
+
+ v8::Local target = New();
+
+ // Run the callback in the async context.
+ delay_request->runInAsyncScope(target, callback, 0, argv);
+
+ delete delay_request;
+}
+
+NAN_METHOD(Delay) {
+ int delay = To(info[0]).FromJust();
+ v8::Local cb = To(info[1]).ToLocalChecked();
+
+ DelayRequest* delay_request = new DelayRequest(delay, cb);
+
+ uv_queue_work(
+ uv_default_loop()
+ , &delay_request->request
+ , Delay
+ , reinterpret_cast(AfterDelay));
+}
+
+NAN_MODULE_INIT(Init) {
+ Set(target, New("delay").ToLocalChecked(),
+ GetFunction(New(Delay)).ToLocalChecked());
+}
+
+NODE_MODULE(asyncresource, Init)
diff --git a/test/js/asyncresource-test.js b/test/js/asyncresource-test.js
new file mode 100644
index 00000000..5e093fa7
--- /dev/null
+++ b/test/js/asyncresource-test.js
@@ -0,0 +1,70 @@
+/*********************************************************************
+ * NAN - Native Abstractions for Node.js
+ *
+ * Copyright (c) 2018 NAN contributors
+ *
+ * MIT License
+ ********************************************************************/
+
+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' }).delay
+ , asyncHooks = require('async_hooks');
+
+test('asyncresource', function (t) {
+ t.plan(7);
+
+ var resourceAsyncId;
+ var originalExecutionAsyncId;
+ var beforeCalled = false;
+ var afterCalled = false;
+ var destroyCalled = false;
+
+ var hooks = asyncHooks.createHook({
+ init: function(asyncId, type, triggerAsyncId, resource) {
+ if (type === 'nan:test.DelayRequest') {
+ resourceAsyncId = asyncId;
+ }
+ },
+ before: function(asyncId) {
+ if (asyncId === resourceAsyncId) {
+ beforeCalled = true;
+ }
+ },
+ after: function(asyncId) {
+ if (asyncId === resourceAsyncId) {
+ afterCalled = true;
+ }
+ },
+ destroy: function(asyncId) {
+ if (asyncId === resourceAsyncId) {
+ destroyCalled = true;
+ }
+ }
+
+ });
+ hooks.enable();
+
+ originalExecutionAsyncId = asyncHooks.executionAsyncId();
+ delay(1000, function() {
+ t.equal(asyncHooks.executionAsyncId(), resourceAsyncId,
+ 'callback should have the correct execution context');
+ t.equal(asyncHooks.triggerAsyncId(), originalExecutionAsyncId,
+ 'callback should have the correct trigger context');
+ t.ok(beforeCalled, 'before should have been called');
+ t.notOk(afterCalled, 'after should not have been called yet');
+ setTimeout(function() {
+ t.ok(afterCalled, 'after should have been called');
+ t.ok(destroyCalled, 'destroy should have been called');
+ t.equal(asyncHooks.triggerAsyncId(), resourceAsyncId,
+ 'setTimeout should have been triggered by the async resource');
+ hooks.disable();
+ }, 1);
+ });
+});