From 25f30d2bce7d3ef0a59aa8d3f69d39bedab824f0 Mon Sep 17 00:00:00 2001 From: Michael Hayes Date: Sat, 31 Jan 2015 12:40:41 -0800 Subject: [PATCH] support Native Promises --- index.js | 85 +++++ test/native-promises.tap.js | 619 ++++++++++++++++++++++++++++++++++++ 2 files changed, 704 insertions(+) create mode 100644 test/native-promises.tap.js diff --git a/index.js b/index.js index 0762e30..aa3ca1e 100644 --- a/index.js +++ b/index.js @@ -221,3 +221,88 @@ if (crypto) { activator ); } + +var instrumentPromise = !!global.Promise; + +// Check that global Promise is native +if (instrumentPromise) { + // shoult not use any methods that have already been wrapped + var promiseListener = process.addAsyncListener({ + create: function create() { + instrumentPromise = false; + } + }); + + // should not resolve synchronously + global.Promise.resolve(true).then(function notSync() { + instrumentPromise = false; + }); + + process.removeAsyncListener(promiseListener); +} + +if (instrumentPromise) { + var Promise = global.Promise; + + global.Promise = function wrappedPromise(executor) { + if (!(this instanceof global.Promise)) { + return Promise(executor); + } + + var promise = new Promise(wrappedExecutor); + var context, args; + executor.apply(context, args); + + + return promise; + + function wrappedExecutor(accept, reject) { + context = this; + args = [wrappedAccept, wrappedReject]; + + // These wrappers create a function that can be passed a function and an argument to + // call as a continuation from the accept or reject. + function wrappedAccept(val) { + if (promise.__asl_wrapper) return accept(val); + promise.__asl_wrapper = wrapCallback(function(ctx, fn, result) { + return fn.call(ctx, result); + }); + return accept(val); + } + + function wrappedReject(val) { + if (promise.__asl_wrapper) return reject(val); + promise.__asl_wrapper = wrapCallback(function(ctx, fn, result) { + return fn.call(ctx, result); + }); + return reject(val); + } + } + } + + wrap(Promise.prototype, 'then', wrapThen); + wrap(Promise.prototype, 'chain', wrapThen); + + var PromiseMethods = ['accept', 'all', 'defer', 'race', 'reject', 'resolve']; + + PromiseMethods.forEach(function(key) { + global.Promise[key] = Promise[key]; + }); +} + +function wrapThen(original) { + return function wrappedThen() { + var promise = this; + return original.apply(this, [].map.call(arguments, bind)); + + // wrap callbacks (success, error) so that the callbacks will be called as a + // continuations of the accept or reject call using the __asl__wrapper created above. + function bind(fn) { + if (typeof fn !== 'function') return fn; + return function(val) { + if (!promise.__asl_wrapper) return fn.call(this, val); + return promise.__asl_wrapper(this, fn, val); + }; + } + } +} diff --git a/test/native-promises.tap.js b/test/native-promises.tap.js new file mode 100644 index 0000000..a088e5f --- /dev/null +++ b/test/native-promises.tap.js @@ -0,0 +1,619 @@ +if (!global.Promise) return; + +var test = require('tap').test; +require('../index.js'); + + +test('then', function(t) { + t.plan(4); + + var listener = addListner(); + + var promise = new Promise(function(accept, reject) { + listener.currentName = 'accept'; + accept(10); + }); + + promise.then(function(val) { + listener.currentName = 'nextTick in first then'; + process.nextTick(function() { + t.strictEqual(val, 10); + }); + }); + + listener.currentName = 'setImmediate in root'; + setImmediate(function() { + promise.then(function(val) { + t.strictEqual(val, 10); + t.strictEqual(this, global); + listener.currentName = 'setTimeout in 2nd then'; + setTimeout(function() { + t.deepEqual(listener.root, expected); + t.end(); + }); + }); + }); + + process.removeAsyncListener(listener.listener); + + var expected = { + name: 'root', + children: [ + { + name: 'accept', + children: [ + { + name: 'nextTick in first then', + children: [], + before: 1, + after: 1, + error: 0 + }, + { + name: 'setTimeout in 2nd then', + children: [], + before: 1, + after: 0, + error: 0 + } + ], + before: 2, + after: 2, + error: 0 + }, + { + name: 'setImmediate in root', + children: [], + before: 1, + after: 1, + error: 0 + } + ], + before: 0, + after: 0, + error: 0 + } +}); + +test('chain', function chainTest(t) { + t.plan(4); + + var listener = addListner(); + + var promise = new Promise(function(accept, reject) { + listener.currentName = 'accept'; + accept(10); + }); + + promise.chain(function(val) { + listener.currentName = 'nextTick in first chain'; + process.nextTick(function() { + t.strictEqual(val, 10); + }); + }); + + listener.currentName = 'setImmediate in root'; + setImmediate(function() { + promise.chain(function(val) { + t.strictEqual(val, 10); + t.strictEqual(this, global); + listener.currentName = 'setTimeout in 2nd chain'; + setTimeout(function() { + t.deepEqual(listener.root, expected); + t.end(); + }); + }); + }); + + process.removeAsyncListener(listener.listener); + + var expected = { + name: 'root', + children: [ + { + name: 'accept', + children: [ + { + name: 'nextTick in first chain', + children: [], + before: 1, + after: 1, + error: 0 + }, + { + name: 'setTimeout in 2nd chain', + children: [], + before: 1, + after: 0, + error: 0 + } + ], + before: 2, + after: 2, + error: 0 + }, + { + name: 'setImmediate in root', + children: [], + before: 1, + after: 1, + error: 0 + } + ], + before: 0, + after: 0, + error: 0 + } +}); + +test('catch', function(t) { + t.plan(4) + var listener = addListner(); + + var promise = new Promise(function(accept, reject) { + listener.currentName = 'reject'; + reject(15); + }); + + promise.catch(function(val) { + listener.currentName = 'nextTick in catch'; + process.nextTick(function() { + t.strictEqual(val, 15); + }); + }); + + listener.currentName = 'setImmediate in root'; + setImmediate(function() { + promise.then( + function fullfilled() { + throw new Error('should not be called on reject'); + }, + function rejected(val) { + t.strictEqual(val, 15); + t.strictEqual(this, global); + listener.currentName = 'setTimeout in then'; + setTimeout(function() { + t.deepEqual(listener.root, expected); + t.end(); + }); + } + ) + }); + + process.removeAsyncListener(listener.listener); + + var expected = { + name: 'root', + children: [ + { + name: 'reject', + children: [ + { + name: 'nextTick in catch', + children: [], + before: 1, + after: 1, + error: 0 + }, + { + name: 'setTimeout in then', + children: [], + before: 1, + after: 0, + error: 0 + } + ], + before: 2, + after: 2, + error: 0 + }, + { + name: 'setImmediate in root', + children: [], + before: 1, + after: 1, + error: 0 + } + ], + before: 0, + after: 0, + error: 0 + }; +}); + +test('Promise.resolve', function resolveTest(t) { + var listener = addListner(); + + listener.currentName = 'resolve'; + var p = Promise.resolve(123); + + p.then(function then(value) { + listener.currentName = 'nextTick'; + process.nextTick(function next() { + t.equal(value, 123) + t.deepEqual(listener.root, { + name: 'root', + children: [{ + name: 'resolve', + children: [{ + name: 'nextTick', + children: [], + before: 1, + after: 0, + error: 0 + }], + before: 1, + after: 1, + error: 0 + }], + before: 0, + after: 0, + error: 0 + }); + t.end(); + }); + process.removeAsyncListener(listener.listener); + }); +}); + +test('Promise.accept', function acceptTest(t) { + var listener = addListner(); + + listener.currentName = 'accept'; + var p = Promise.accept(123); + + p.then(function then(value) { + listener.currentName = 'nextTick'; + process.nextTick(function next() { + t.equal(value, 123) + t.deepEqual(listener.root, { + name: 'root', + children: [{ + name: 'accept', + children: [{ + name: 'nextTick', + children: [], + before: 1, + after: 0, + error: 0 + }], + before: 1, + after: 1, + error: 0 + }], + before: 0, + after: 0, + error: 0 + }); + t.end(); + }); + process.removeAsyncListener(listener.listener); + }); +}); + +test('Promise.reject', function rejectTest(t) { + var listener = addListner(); + + listener.currentName = 'reject'; + var p = Promise.reject(123); + + p.catch(function then(value) { + listener.currentName = 'nextTick'; + process.nextTick(function next() { + t.equal(value, 123); + t.deepEqual(listener.root, { + name: 'root', + children: [{ + name: 'reject', + children: [{ + name: 'nextTick', + children: [], + before: 1, + after: 0, + error: 0 + }], + before: 1, + after: 1, + error: 0 + }], + before: 0, + after: 0, + error: 0 + }); + t.end(); + }); + process.removeAsyncListener(listener.listener); + }); +}); + +test('Promise.all', function allTest(t) { + var listener = addListner(); + + listener.currentName = 'resolve 1'; + var a = Promise.resolve(123); + listener.currentName = 'resolve 2'; + var b = Promise.resolve(456); + listener.currentName = 'all'; + var p = Promise.all([a, b]) + + p.then(function then(value) { + listener.currentName = 'nextTick'; + process.nextTick(function next() { + process.removeAsyncListener(listener.listener); + t.deepEqual(value, [123, 456]) + t.deepEqual(listener.root, { + name: 'root', + children: [{ + name: 'resolve 1', + children: [], + before: 1, + after: 1, + error: 0 + }, { + name: 'resolve 2', + children: [{ + name: 'all', + children: [{ + name: 'nextTick', + children: [], + before: 1, + after: 0, + error: 0 + }], + before: 1, + after: 1, + error: 0 + }], + before: 1, + after: 1, + error: 0 + }], + before: 0, + after: 0, + error: 0 + }) + t.end(); + }); + }); +}); + +test('Promise.all reject', function allTest(t) { + var listener = addListner(); + + listener.currentName = 'resolve'; + var a = Promise.resolve(123); + listener.currentName = 'reject'; + var b = Promise.reject(456); + listener.currentName = 'all'; + var p = Promise.all([a, b]) + + p.catch(function then(value) { + listener.currentName = 'nextTick'; + process.nextTick(function next() { + process.removeAsyncListener(listener.listener); + t.equal(value, 456) + t.deepEqual(listener.root, { + name: 'root', + children: [{ + name: 'resolve', + children: [], + before: 1, + after: 1, + error: 0 + }, { + name: 'reject', + children: [{ + name: 'all', + children: [{ + name: 'nextTick', + children: [], + before: 1, + after: 0, + error: 0 + }], + before: 1, + after: 1, + error: 0 + }], + before: 1, + after: 1, + error: 0 + }], + before: 0, + after: 0, + error: 0 + }) + t.end(); + }); + }); +}); + +test('Promise.race', function raceTest(t) { + var listener = addListner(); + + listener.currentName = 'resolve 1'; + var a = Promise.resolve(123); + listener.currentName = 'resolve 2'; + var b = Promise.resolve(456); + listener.currentName = 'race'; + var p = Promise.race([a, b]) + + p.then(function then(value) { + listener.currentName = 'nextTick'; + process.nextTick(function next() { + process.removeAsyncListener(listener.listener); + t.equal(value, 123) + t.deepEqual(listener.root, { + name: 'root', + children: [{ + name: 'resolve 1', + children: [{ + name: 'race', + children: [{ + name: 'nextTick', + children: [], + before: 1, + after: 0, + error: 0 + }], + before: 1, + after: 1, + error: 0 + }], + before: 1, + after: 1, + error: 0 + }, { + name: 'resolve 2', + children: [], + before: 1, + after: 1, + error: 0 + }], + before: 0, + after: 0, + error: 0 + }) + t.end(); + }); + }); +}); + +test('Promise.race - reject', function raceTest(t) { + var listener = addListner(); + + listener.currentName = 'reject'; + var a = Promise.reject(123); + listener.currentName = 'resolve'; + var b = Promise.resolve(456); + listener.currentName = 'race'; + var p = Promise.race([a, b]) + + p.catch(function then(value) { + listener.currentName = 'nextTick'; + process.nextTick(function next() { + process.removeAsyncListener(listener.listener); + t.equal(value, 123) + t.deepEqual(listener.root, { + name: 'root', + children: [{ + name: 'reject', + children: [{ + name: 'race', + children: [{ + name: 'nextTick', + children: [], + before: 1, + after: 0, + error: 0 + }], + before: 1, + after: 1, + error: 0 + }], + before: 1, + after: 1, + error: 0 + }, { + name: 'resolve', + children: [], + before: 1, + after: 1, + error: 0 + }], + before: 0, + after: 0, + error: 0 + }) + t.end(); + }); + }); +}); + +test('Promise.defer', function diferTest(t) { + var listener = addListner(); + + listener.currentName = 'defer'; + var p = Promise.defer() + listener.currentName = 'resolve'; + p.resolve(123) + listener.currentName = 'reject'; + p.reject(456) + + p.promise.then(function then(value) { + listener.currentName = 'nextTick'; + process.nextTick(function next() { + process.removeAsyncListener(listener.listener); + t.equal(value, 123) + t.deepEqual(listener.root, { + name: 'root', + children: [{ + name: 'resolve', + children: [{ + name: 'nextTick', + children: [], + before: 1, + after: 0, + error: 0 + }], + before: 1, + after: 1, + error: 0 + }], + before: 0, + after: 0, + error: 0 + }) + t.end(); + }); + }); +}); + +function addListner() { + var listener = process.addAsyncListener({ + create: create, + before: before, + after: after, + error: error + }); + + + var state = { + listener: listener, + currentName: 'root', + }; + + state.root = create(); + state.current = state.root; + + return state; + + function create () { + var node = { + name: state.currentName, + children: [], + before: 0, + after: 0, + error: 0 + }; + + if(state.current) state.current.children.push(node); + return node; + } + + function before(ctx, node) { + state.current = node; + state.current.before++; + } + + function after(ctx, node) { + node.after++; + state.current = null; + } + + function error(ctx, node) { + node.error++; + state.current = null; + return false; + } +}