summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/api/domain.md50
-rw-r--r--src/node.cc56
-rw-r--r--test/parallel/test-domain-promise.js128
3 files changed, 234 insertions, 0 deletions
diff --git a/doc/api/domain.md b/doc/api/domain.md
index eb04987f08..4ada1c2ca0 100644
--- a/doc/api/domain.md
+++ b/doc/api/domain.md
@@ -1,4 +1,11 @@
# Domain
+<!-- YAML
+changes:
+ - version: REPLACEME
+ pr-url: https://github.com/nodejs/node/pull/12489
+ description: Handlers for `Promise`s are now invoked in the domain in which
+ the first promise of a chain was created.
+-->
> Stability: 0 - Deprecated
@@ -444,6 +451,49 @@ d.run(() => {
In this example, the `d.on('error')` handler will be triggered, rather
than crashing the program.
+## Domains and Promises
+
+As of Node REPLACEME, the handlers of Promises are run inside the domain in
+which the call to `.then` or `.catch` itself was made:
+
+```js
+const d1 = domain.create();
+const d2 = domain.create();
+
+let p;
+d1.run(() => {
+ p = Promise.resolve(42);
+});
+
+d2.run(() => {
+ p.then((v) => {
+ // running in d2
+ });
+});
+```
+
+A callback may be bound to a specific domain using [`domain.bind(callback)`][]:
+
+```js
+const d1 = domain.create();
+const d2 = domain.create();
+
+let p;
+d1.run(() => {
+ p = Promise.resolve(42);
+});
+
+d2.run(() => {
+ p.then(p.domain.bind((v) => {
+ // running in d1
+ }));
+});
+```
+
+Note that domains will not interfere with the error handling mechanisms for
+Promises, i.e. no `error` event will be emitted for unhandled Promise
+rejections.
+
[`domain.add(emitter)`]: #domain_domain_add_emitter
[`domain.bind(callback)`]: #domain_domain_bind_callback
[`domain.dispose()`]: #domain_domain_dispose
diff --git a/src/node.cc b/src/node.cc
index abb570fb9e..8b061d7f40 100644
--- a/src/node.cc
+++ b/src/node.cc
@@ -143,6 +143,7 @@ using v8::Number;
using v8::Object;
using v8::ObjectTemplate;
using v8::Promise;
+using v8::PromiseHookType;
using v8::PromiseRejectMessage;
using v8::PropertyCallbackInfo;
using v8::ScriptOrigin;
@@ -1114,6 +1115,58 @@ bool ShouldAbortOnUncaughtException(Isolate* isolate) {
}
+void DomainPromiseHook(PromiseHookType type,
+ Local<Promise> promise,
+ Local<Value> parent,
+ void* arg) {
+ Environment* env = static_cast<Environment*>(arg);
+ Local<Context> context = env->context();
+
+ if (type == PromiseHookType::kResolve) return;
+ if (type == PromiseHookType::kInit && env->in_domain()) {
+ promise->Set(context,
+ env->domain_string(),
+ env->domain_array()->Get(context,
+ 0).ToLocalChecked()).FromJust();
+ return;
+ }
+
+ // Loosely based on node::MakeCallback().
+ Local<Value> domain_v =
+ promise->Get(context, env->domain_string()).ToLocalChecked();
+ if (!domain_v->IsObject())
+ return;
+
+ Local<Object> domain = domain_v.As<Object>();
+ if (domain->Get(context, env->disposed_string())
+ .ToLocalChecked()->IsTrue()) {
+ return;
+ }
+
+ if (type == PromiseHookType::kBefore) {
+ Local<Value> enter_v =
+ domain->Get(context, env->enter_string()).ToLocalChecked();
+ if (enter_v->IsFunction()) {
+ if (enter_v.As<Function>()->Call(context, domain, 0, nullptr).IsEmpty()) {
+ FatalError("node::PromiseHook",
+ "domain enter callback threw, please report this "
+ "as a bug in Node.js");
+ }
+ }
+ } else {
+ Local<Value> exit_v =
+ domain->Get(context, env->exit_string()).ToLocalChecked();
+ if (exit_v->IsFunction()) {
+ if (exit_v.As<Function>()->Call(context, domain, 0, nullptr).IsEmpty()) {
+ FatalError("node::MakeCallback",
+ "domain exit callback threw, please report this "
+ "as a bug in Node.js");
+ }
+ }
+ }
+}
+
+
void SetupDomainUse(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
@@ -1153,9 +1206,12 @@ void SetupDomainUse(const FunctionCallbackInfo<Value>& args) {
Local<ArrayBuffer> array_buffer =
ArrayBuffer::New(env->isolate(), fields, sizeof(*fields) * fields_count);
+ env->AddPromiseHook(DomainPromiseHook, static_cast<void*>(env));
+
args.GetReturnValue().Set(Uint32Array::New(array_buffer, 0, fields_count));
}
+
void RunMicrotasks(const FunctionCallbackInfo<Value>& args) {
args.GetIsolate()->RunMicrotasks();
}
diff --git a/test/parallel/test-domain-promise.js b/test/parallel/test-domain-promise.js
new file mode 100644
index 0000000000..8bae75eb63
--- /dev/null
+++ b/test/parallel/test-domain-promise.js
@@ -0,0 +1,128 @@
+'use strict';
+const common = require('../common');
+const assert = require('assert');
+const domain = require('domain');
+const fs = require('fs');
+const vm = require('vm');
+
+common.crashOnUnhandledRejection();
+
+{
+ const d = domain.create();
+
+ d.run(common.mustCall(() => {
+ Promise.resolve().then(common.mustCall(() => {
+ assert.strictEqual(process.domain, d);
+ }));
+ }));
+}
+
+{
+ const d = domain.create();
+
+ d.run(common.mustCall(() => {
+ Promise.resolve().then(() => {}).then(() => {}).then(common.mustCall(() => {
+ assert.strictEqual(process.domain, d);
+ }));
+ }));
+}
+
+{
+ const d = domain.create();
+
+ d.run(common.mustCall(() => {
+ vm.runInNewContext(`Promise.resolve().then(common.mustCall(() => {
+ assert.strictEqual(process.domain, d);
+ }));`, { common, assert, process, d });
+ }));
+}
+
+{
+ const d1 = domain.create();
+ const d2 = domain.create();
+ let p;
+ d1.run(common.mustCall(() => {
+ p = Promise.resolve(42);
+ }));
+
+ d2.run(common.mustCall(() => {
+ p.then(common.mustCall((v) => {
+ assert.strictEqual(process.domain, d2);
+ assert.strictEqual(p.domain, d1);
+ }));
+ }));
+}
+
+{
+ const d1 = domain.create();
+ const d2 = domain.create();
+ let p;
+ d1.run(common.mustCall(() => {
+ p = Promise.resolve(42);
+ }));
+
+ d2.run(common.mustCall(() => {
+ p.then(p.domain.bind(common.mustCall((v) => {
+ assert.strictEqual(process.domain, d1);
+ assert.strictEqual(p.domain, d1);
+ })));
+ }));
+}
+
+{
+ const d1 = domain.create();
+ const d2 = domain.create();
+ let p;
+ d1.run(common.mustCall(() => {
+ p = Promise.resolve(42);
+ }));
+
+ d1.run(common.mustCall(() => {
+ d2.run(common.mustCall(() => {
+ p.then(common.mustCall((v) => {
+ assert.strictEqual(process.domain, d2);
+ assert.strictEqual(p.domain, d1);
+ }));
+ }));
+ }));
+}
+
+{
+ const d1 = domain.create();
+ const d2 = domain.create();
+ let p;
+ d1.run(common.mustCall(() => {
+ p = Promise.reject(new Error('foobar'));
+ }));
+
+ d2.run(common.mustCall(() => {
+ p.catch(common.mustCall((v) => {
+ assert.strictEqual(process.domain, d2);
+ assert.strictEqual(p.domain, d1);
+ }));
+ }));
+}
+
+{
+ const d = domain.create();
+
+ d.run(common.mustCall(() => {
+ Promise.resolve().then(common.mustCall(() => {
+ setTimeout(common.mustCall(() => {
+ assert.strictEqual(process.domain, d);
+ }), 0);
+ }));
+ }));
+}
+
+{
+ const d = domain.create();
+
+ d.run(common.mustCall(() => {
+ Promise.resolve().then(common.mustCall(() => {
+ fs.readFile(__filename, common.mustCall(() => {
+ assert.strictEqual(process.domain, d);
+ }));
+ }));
+ }));
+}