diff --git a/src/debuggability.js b/src/debuggability.js index c716f4ef3..4bc98c67c 100644 --- a/src/debuggability.js +++ b/src/debuggability.js @@ -1,6 +1,6 @@ "use strict"; module.exports = function(Promise, Context) { -var getDomain = Promise._getDomain; +var getContext = Promise._getContext; var async = Promise._async; var Warning = require("./errors").Warning; var util = require("./util"); @@ -104,19 +104,13 @@ Promise.prototype._warn = function(message, shouldUseOwnTrace, promise) { }; Promise.onPossiblyUnhandledRejection = function (fn) { - var domain = getDomain(); - possiblyUnhandledRejection = - typeof fn === "function" ? (domain === null ? - fn : util.domainBind(domain, fn)) - : undefined; + var context = getContext(); + possiblyUnhandledRejection = util.contextBind(context, fn); }; Promise.onUnhandledRejectionHandled = function (fn) { - var domain = getDomain(); - unhandledRejectionHandled = - typeof fn === "function" ? (domain === null ? - fn : util.domainBind(domain, fn)) - : undefined; + var context = getContext(); + unhandledRejectionHandled = util.contextBind(context, fn); }; var disableLongStackTraces = function() {}; @@ -308,6 +302,9 @@ Promise.config = function(opts) { Promise.prototype._fireEvent = defaultFireEvent; } } + if ("asyncHooks" in opts) { + config.asyncHooks = opts.asyncHooks; + } return Promise; }; @@ -933,12 +930,16 @@ var config = { warnings: warnings, longStackTraces: false, cancellation: false, - monitoring: false + monitoring: false, + asyncHooks: false }; if (longStackTraces) Promise.longStackTraces(); return { + asyncHooks: function() { + return config.asyncHooks; + }, longStackTraces: function() { return config.longStackTraces; }, diff --git a/src/join.js b/src/join.js index db7b6018f..0099b1c84 100644 --- a/src/join.js +++ b/src/join.js @@ -1,7 +1,7 @@ "use strict"; module.exports = function(Promise, PromiseArray, tryConvertToPromise, INTERNAL, async, - getDomain) { + getContext) { var util = require("./util"); var canEvaluate = util.canEvaluate; var tryCatch = util.tryCatch; @@ -147,10 +147,8 @@ Promise.join = function () { if (!ret._isFateSealed()) { if (holder.asyncNeeded) { - var domain = getDomain(); - if (domain !== null) { - holder.fn = util.domainBind(domain, holder.fn); - } + var context = getContext(); + holder.fn = util.contextBind(context, holder.fn); } ret._setAsyncGuaranteed(); ret._setOnCancel(holder); diff --git a/src/map.js b/src/map.js index dd7244e6d..f5fbd4df6 100644 --- a/src/map.js +++ b/src/map.js @@ -6,7 +6,7 @@ module.exports = function(Promise, INTERNAL, debug) { var ASSERT = require("./assert"); -var getDomain = Promise._getDomain; +var getContext = Promise._getContext; var util = require("./util"); var tryCatch = util.tryCatch; var errorObj = util.errorObj; @@ -15,8 +15,8 @@ var async = Promise._async; function MappingPromiseArray(promises, fn, limit, _filter) { this.constructor$(promises); this._promise._captureStackTrace(); - var domain = getDomain(); - this._callback = domain === null ? fn : util.domainBind(domain, fn); + var context = getContext(); + this._callback = util.contextBind(context, fn); this._preservedValues = _filter === INTERNAL ? new Array(this.length()) : null; diff --git a/src/promise.js b/src/promise.js index 862cc52ad..edfef2f77 100644 --- a/src/promise.js +++ b/src/promise.js @@ -14,19 +14,32 @@ var UNDEFINED_BINDING = {}; var ASSERT = require("./assert"); var util = require("./util"); -var getDomain; +var getContext; if (util.isNode) { - getDomain = function() { - var ret = process.domain; - if (ret === undefined) ret = null; - return ret; - }; + if (util.nodeSupportsAsyncResource) { + var AsyncResource = require("async_hooks").AsyncResource; + getContext = function() { + if (!debug.asyncHooks()) { + return { domain: process.domain }; + } + return { + domain: process.domain, + async: new AsyncResource("Bluebird::Promise") + }; + }; + } else { + getContext = function() { + return { + domain: process.domain + }; + }; + } } else { - getDomain = function() { - return null; + getContext = function() { + return {}; }; } -util.notEnumerableProp(Promise, "_getDomain", getDomain); +util.notEnumerableProp(Promise, "_getContext", getContext); var es5 = require("./es5"); var Async = require("./async"); @@ -244,7 +257,7 @@ Promise.prototype._then = function ( this._fireEvent("promiseChained", this, promise); } - var domain = getDomain(); + var context = getContext(); if (!BIT_FIELD_CHECK(IS_PENDING_AND_WAITING_NEG)) { var handler, value, settler = target._settlePromiseCtx; if (BIT_FIELD_CHECK(IS_FULFILLED)) { @@ -262,15 +275,14 @@ Promise.prototype._then = function ( } async.invoke(settler, target, { - handler: domain === null ? handler - : (typeof handler === "function" && - util.domainBind(domain, handler)), + handler: util.contextBind(context, handler), promise: promise, receiver: receiver, value: value }); } else { - target._addCallbacks(didFulfill, didReject, promise, receiver, domain); + target._addCallbacks(didFulfill, didReject, promise, + receiver, context); } return promise; @@ -396,9 +408,9 @@ Promise.prototype._addCallbacks = function ( reject, promise, receiver, - domain + context ) { - ASSERT(typeof domain === "object"); + ASSERT(typeof context === "object"); ASSERT(!this._isFateSealed()); ASSERT(!this._isFollowing()); var index = this._length(); @@ -417,12 +429,10 @@ Promise.prototype._addCallbacks = function ( this._promise0 = promise; this._receiver0 = receiver; if (typeof fulfill === "function") { - this._fulfillmentHandler0 = - domain === null ? fulfill : util.domainBind(domain, fulfill); + this._fulfillmentHandler0 = util.contextBind(context, fulfill); } if (typeof reject === "function") { - this._rejectionHandler0 = - domain === null ? reject : util.domainBind(domain, reject); + this._rejectionHandler0 = util.contextBind(context, reject); } } else { ASSERT(this[base + CALLBACK_PROMISE_OFFSET] === undefined); @@ -434,11 +444,11 @@ Promise.prototype._addCallbacks = function ( this[base + CALLBACK_RECEIVER_OFFSET] = receiver; if (typeof fulfill === "function") { this[base + CALLBACK_FULFILL_OFFSET] = - domain === null ? fulfill : util.domainBind(domain, fulfill); + util.contextBind(context, fulfill); } if (typeof reject === "function") { this[base + CALLBACK_REJECT_OFFSET] = - domain === null ? reject : util.domainBind(domain, reject); + util.contextBind(context, reject); } } this._setLength(index + 1); @@ -590,7 +600,8 @@ Promise.prototype._settlePromise = function(promise, handler, receiver, value) { if (tryCatch(handler).call(receiver, value) === errorObj) { promise._reject(errorObj.e); } - } else if (handler === reflectHandler) { + } else if (handler === reflectHandler || (handler && + handler[util.wrappedSymbol] === reflectHandler)) { promise._fulfill(reflectHandler.call(receiver)); } else if (receiver instanceof Proxyable) { receiver._promiseCancelled(promise); @@ -786,7 +797,7 @@ require("./cancel")(Promise, PromiseArray, apiRejection, debug); require("./direct_resolve")(Promise); require("./synchronous_inspection")(Promise); require("./join")( - Promise, PromiseArray, tryConvertToPromise, INTERNAL, async, getDomain); + Promise, PromiseArray, tryConvertToPromise, INTERNAL, async, getContext); Promise.Promise = Promise; Promise.version = "__VERSION__"; }; diff --git a/src/reduce.js b/src/reduce.js index 4220b58c1..a9c0cdd5a 100644 --- a/src/reduce.js +++ b/src/reduce.js @@ -5,14 +5,14 @@ module.exports = function(Promise, tryConvertToPromise, INTERNAL, debug) { -var getDomain = Promise._getDomain; +var getContext = Promise._getContext; var util = require("./util"); var tryCatch = util.tryCatch; function ReductionPromiseArray(promises, fn, initialValue, _each) { this.constructor$(promises); - var domain = getDomain(); - this._fn = domain === null ? fn : util.domainBind(domain, fn); + var context = getContext(); + this._fn = util.contextBind(context, fn); if (initialValue !== undefined) { initialValue = Promise.resolve(initialValue); initialValue._attachCancellationCallback(this); @@ -32,8 +32,8 @@ function ReductionPromiseArray(promises, fn, initialValue, _each) { util.inherits(ReductionPromiseArray, PromiseArray); ReductionPromiseArray.prototype._gotAccum = function(accum) { - if (this._eachValues !== undefined && - this._eachValues !== null && + if (this._eachValues !== undefined && + this._eachValues !== null && accum !== INTERNAL) { this._eachValues.push(accum); } diff --git a/src/util.js b/src/util.js index 31d6a0f5d..d746f8a16 100644 --- a/src/util.js +++ b/src/util.js @@ -344,8 +344,25 @@ function getNativePromise() { } } -function domainBind(self, cb) { - return self.bind(cb); +function contextBind(ctx, cb) { + if (typeof cb !== "function") + return cb; + + if (ctx != null && ctx.domain != null) { + cb = ctx.domain.bind(cb); + } + + if (ctx != null && ctx.async != null) { + var old = cb; + cb = function() { + INLINE_SLICE(args, arguments); + return ctx.async.runInAsyncScope(function() { + return old.apply(this, args); + }, this); + }; + cb[ret.wrappedSymbol] = old; + } + return cb; } var ret = { @@ -382,17 +399,25 @@ var ret = { env: env, global: globalObject, getNativePromise: getNativePromise, - domainBind: domainBind + contextBind: contextBind }; ret.isRecentNode = ret.isNode && (function() { var version; - if (process.versions && process.versions.node) { + if (process.versions && process.versions.node) { version = process.versions.node.split(".").map(Number); } else if (process.version) { version = process.version.split(".").map(Number); } return (version[0] === 0 && version[1] > 10) || (version[0] > 0); })(); +ret.nodeSupportsAsyncResource = ret.isNode && (function() { + var version = process.versions.node.split(".").map(Number); + return (version[0] === 9 && version[1] >= 6) || (version[0] > 9); +})(); + +if (ret.nodeSupportsAsyncResource) { + ret.wrappedSymbol = Symbol(); +} if (ret.isNode) ret.toFastProperties(process); diff --git a/test/mocha/async_hooks.js b/test/mocha/async_hooks.js new file mode 100644 index 000000000..388f647aa --- /dev/null +++ b/test/mocha/async_hooks.js @@ -0,0 +1,135 @@ +"use strict"; + +var assert = require("assert"); + +var supportsAsync = false; +try { + require('async_hooks'); + supportsAsync = true; +} catch (e) { } + +if (supportsAsync) { + runTests(); +} + +function runTests() { + var async_hooks = require('async_hooks'); + + var tree = new Set(); + var hook = async_hooks.createHook({ + init: function(asyncId, type, triggerId) { + if (tree.has(triggerId)) { + tree.add(asyncId); + } + } + }); + + var currentId = async_hooks.executionAsyncId; + + function getAsyncPromise() { + return new Promise(function(resolve, reject) { + setTimeout(function() { + setTimeout(resolve, 1); + }, 1); + }); + } + + describe("async_hooks", function() { + beforeEach(function() { + Promise.config({ asyncHooks: true }); + }) + afterEach(function() { + tree.clear(); + hook.disable(); + Promise.config({ asyncHooks: false }); + }); + + it('should preserve async context when using fromNode', function() { + hook.enable() + tree.add(currentId()); + + return new Promise(function(resolve) { + var globalResolve; + setImmediate(function() { + hook.enable() + tree.add(currentId()); + resolve( + new Promise(function(resolve) { globalResolve = resolve; }) + .then(function() { + assert.ok(tree.has(currentId())); + }) + ); + }) + + setTimeout(function() { + globalResolve(); + }, 10); + }) + }); + + it('should preserve async context when using .map', function() { + hook.enable() + tree.add(currentId()); + var d1 = getAsyncPromise(); + + return new Promise(function(resolve, reject) { + resolve(Promise.map([d1, null, Promise.resolve(1), Promise.delay(1)], function() { + return currentId(); + }).then(function(asyncIds) { + for (var i = 0; i < asyncIds.length; ++i) { + assert.ok(tree.has(asyncIds[i])); + } + })); + }); + }); + + it('should preserve async context when using .filter', function() { + hook.enable() + tree.add(currentId()); + var d1 = getAsyncPromise(); + + return new Promise(function(resolve, reject) { + resolve(Promise.filter([d1, null, Promise.resolve(1), Promise.delay(1)], function() { + assert.ok(tree.has(currentId())); + })); + }); + }); + + it('should preserve async context when using .reduce', function() { + hook.enable() + tree.add(currentId()); + var d1 = getAsyncPromise(); + + return new Promise(function(resolve, reject) { + resolve(Promise.reduce([d1, null, Promise.resolve(1), Promise.delay(1)], function() { + assert.ok(tree.has(currentId())); + })); + }); + }); + + it('should preserve async context when using .each', function() { + hook.enable() + tree.add(currentId()); + var d1 = getAsyncPromise(); + + return new Promise(function(resolve, reject) { + resolve(Promise.each([d1, null, Promise.resolve(1), Promise.delay(1)], function() { + assert.ok(tree.has(currentId())); + })); + }); + }); + + it('should be able to disable AsyncResource usage', function() { + Promise.config({ asyncHooks: false }); + hook.enable() + tree.add(currentId()); + var d1 = getAsyncPromise(); + + return new Promise(function(resolve, reject) { + resolve(d1.then(function() { + assert.ok(!tree.has(currentId())); + })); + }); + }); + }); +} diff --git a/tools/build.js b/tools/build.js index 6ec9b463d..3cef5e266 100644 --- a/tools/build.js +++ b/tools/build.js @@ -212,7 +212,7 @@ function buildBrowser(sources, dir, tmpDir, depsRequireCode, minify, npmPackage, entries: entries, detectGlobals: false, standalone: "Promise" - }); + }).exclude('async_hooks'); return Promise.promisify(b.bundle, b)().then(function(src) { var alias = "\ ;if (typeof window !== 'undefined' && window !== null) { \