diff --git a/package.json b/package.json index 48f42c2..89053c7 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "license": "MIT", "devDependencies": { "airtap": "0.0.6", + "is-async-supported": "~1.2.0", "run-series": "~1.1.4", "tape": "~4.9.0" }, diff --git a/test/browser/callbackify.js b/test/browser/callbackify.js new file mode 100644 index 0000000..ea05089 --- /dev/null +++ b/test/browser/callbackify.js @@ -0,0 +1,169 @@ +'use strict'; + +var test = require('tape'); +var callbackify = require('../../').callbackify; + +if (typeof Promise === 'undefined') { + console.log('no global Promise found, skipping callbackify tests'); + return; +} + +function after (n, cb) { + var i = 0; + return function () { + if (++i === n) cb(); + } +} + +var values = [ + 'hello world', + null, + undefined, + false, + 0, + {}, + { key: 'value' }, + function ok() {}, + ['array', 'with', 4, 'values'], + new Error('boo') +]; +if (typeof Symbol !== 'undefined') { + values.push(Symbol('I am a symbol')); +} + +test('util.callbackify resolution value is passed as second argument to callback', function (t) { + var end = after(values.length * 2, t.end); + // Test that the resolution value is passed as second argument to callback + values.forEach(function(value) { + // Test Promise factory + function promiseFn() { + return Promise.resolve(value); + } + + var cbPromiseFn = callbackify(promiseFn); + cbPromiseFn(function(err, ret) { + t.ifError(err); + t.strictEqual(ret, value, 'cb ' + typeof value); + end(); + }); + + // Test Thenable + function thenableFn() { + return { + then: function(onRes, onRej) { + onRes(value); + } + }; + } + + var cbThenableFn = callbackify(thenableFn); + cbThenableFn(function(err, ret) { + t.ifError(err); + t.strictEqual(ret, value, 'thenable ' + typeof value); + end(); + }); + }); +}); + +test('util.callbackify rejection reason is passed as first argument to callback', function (t) { + var end = after(values.length * 2, t.end); + // Test that rejection reason is passed as first argument to callback + values.forEach(function(value) { + // test a Promise factory + function promiseFn() { + return Promise.reject(value); + } + + var cbPromiseFn = callbackify(promiseFn); + cbPromiseFn(function(err, ret) { + t.strictEqual(ret, undefined, 'cb ' + typeof value); + if (err instanceof Error) { + if ('reason' in err) { + t.ok(!value); + t.strictEqual(err.message, 'Promise was rejected with a falsy value'); + t.strictEqual(err.reason, value); + } else { + t.strictEqual(String(value).slice(-err.message.length), err.message); + } + } else { + t.strictEqual(err, value); + } + end(); + }); + + // Test Thenable + function thenableFn() { + return { + then: function (onRes, onRej) { + onRej(value); + } + }; + } + + var cbThenableFn = callbackify(thenableFn); + cbThenableFn(function(err, ret) { + t.strictEqual(ret, undefined, 'thenable ' + typeof value); + if (err instanceof Error) { + if ('reason' in err) { + t.ok(!value); + t.strictEqual(err.message, 'Promise was rejected with a falsy value'); + t.strictEqual(err.reason, value); + } else { + t.strictEqual(String(value).slice(-err.message.length), err.message); + } + } else { + t.strictEqual(err, value); + } + end(); + }); + }); +}); + +test('util.callbackify arguments passed to callbackified function are passed to original', function (t) { + var end = after(values.length, t.end); + // Test that arguments passed to callbackified function are passed to original + values.forEach(function(value) { + function promiseFn(arg) { + t.strictEqual(arg, value); + return Promise.resolve(arg); + } + + var cbPromiseFn = callbackify(promiseFn); + cbPromiseFn(value, function(err, ret) { + t.ifError(err); + t.strictEqual(ret, value); + end(); + }); + }); +}); + +test('util.callbackify `this` binding is the same for callbackified and original', function (t) { + var end = after(values.length, t.end); + // Test that `this` binding is the same for callbackified and original + values.forEach(function(value) { + var iAmThis = { + fn: function(arg) { + t.strictEqual(this, iAmThis); + return Promise.resolve(arg); + }, + }; + iAmThis.cbFn = callbackify(iAmThis.fn); + iAmThis.cbFn(value, function(err, ret) { + t.ifError(err); + t.strictEqual(ret, value); + t.strictEqual(this, iAmThis); + end(); + }); + }); +}); + +test('util.callbackify non-function inputs throw', function (t) { + // Verify that non-function inputs throw. + ['foo', null, undefined, false, 0, {}, typeof Symbol !== 'undefined' ? Symbol() : undefined, []].forEach(function(value) { + t.throws( + function() { callbackify(value); }, + 'The "original" argument must be of type Function' + ); + }); + t.end(); +}); diff --git a/test/browser/index.js b/test/browser/index.js index 78e0fb3..28b2e5a 100644 --- a/test/browser/index.js +++ b/test/browser/index.js @@ -1,2 +1,4 @@ require('./inspect'); require('./is'); +require('./promisify'); +require('./callbackify'); diff --git a/test/browser/promisify.js b/test/browser/promisify.js new file mode 100644 index 0000000..44165c1 --- /dev/null +++ b/test/browser/promisify.js @@ -0,0 +1,205 @@ +var promisify = require('../../').promisify; +var test = require('tape'); + +var hasSymbol = typeof Symbol !== 'undefined'; + +if (typeof Promise === 'undefined') { + console.log('no global Promise found, skipping promisify tests'); + return; +} + +var callbacker = function (arg, cb) { + setTimeout(function () { + if (typeof arg === 'string') + cb(null, arg.toUpperCase()); + else cb(new TypeError('incorrect type')); + }, 5); +} +var promiser = promisify(callbacker); + +test('util.promisify resolves', function (t) { + var promise = promiser(__filename); + t.ok(promise instanceof Promise); + promise.then(function (value) { + t.deepEqual(value, __filename.toUpperCase()); + t.end(); + }); +}); + +test('util.promisify rejects', function (t) { + var promise = promiser(42); + promise.catch(function (error) { + t.equal(error.message, 'incorrect type'); + t.end(); + }); +}); + +test('util.promisify custom', { skip: !hasSymbol }, function (t) { + function fn() {} + function promisifedFn() {} + fn[promisify.custom] = promisifedFn; + t.strictEqual(promisify(fn), promisifedFn); + t.strictEqual(promisify(promisify(fn)), promisifedFn); + t.end(); +}); + +test('util.promisify custom of invalid type', { skip: !hasSymbol }, function (t) { + function fn2() {} + fn2[promisify.custom] = 42; + t.throws( + function () { promisify(fn2); }, + /must be of type Function/ + ); + t.end(); +}); + +test('util.promisify multiple callback values', function (t) { + function fn5(callback) { + callback(null, 'foo', 'bar'); + } + promisify(fn5)().then(function (value) { + t.strictEqual(value, 'foo'); + t.end(); + }); +}); + +test('util.promisify no callback success value', function (t) { + function fn6(callback) { + callback(null); + } + promisify(fn6)().then(function (value) { + t.strictEqual(value, undefined); + t.end(); + }); +}); + +test('util.promisify no callback arguments at all', function (t) { + function fn7(callback) { + callback(); + } + promisify(fn7)().then(function (value) { + t.strictEqual(value, undefined); + t.end(); + }); +}); + +test('util.promisify passing arguments', function (t) { + function fn8(err, val, callback) { + callback(err, val); + } + promisify(fn8)(null, 42).then(function (value) { + t.strictEqual(value, 42); + t.end(); + }); +}); + +test('util.promisify passing arguments (rejects)', function (t) { + function fn9(err, val, callback) { + callback(err, val); + } + promisify(fn9)(new Error('oops'), null).catch(function (err) { + t.strictEqual(err.message, 'oops'); + t.end(); + }); +}); + +test('util.promisify chain', function (t) { + function fn9(err, val, callback) { + callback(err, val); + } + + + Promise.resolve() + .then(function () { return promisify(fn9)(null, 42); }) + .then(function (value) { + t.strictEqual(value, 42); + t.end(); + }); +}); + +test('util.promisify keeps correct `this`', function (t) { + var o = {}; + var fn10 = promisify(function(cb) { + + cb(null, this === o); + }); + + o.fn = fn10; + + o.fn().then(function(val) { + t.ok(val); + t.end(); + }); +}); + +test('util.promisify calling callback multiple times', function (t) { + var err = new Error('Should not have called the callback with the error.'); + var stack = err.stack; + + var fn11 = promisify(function(cb) { + cb(null); + cb(err); + }); + + Promise.resolve() + .then(function () { return fn11(); }) + .then(function () { return Promise.resolve(); }) + .then(function () { + t.strictEqual(stack, err.stack); + t.end(); + }); +}); + +// Can't do this without Symbol() unfortunately +test('util.promisify promisifying a promisified function', { skip: !hasSymbol }, function (t) { + function c() { } + var a = promisify(function() { }); + var b = promisify(a); + t.notStrictEqual(c, a); + t.strictEqual(a, b); + t.end(); +}); + +test('util.promisify sync throw becomes rejection', function (t) { + var errToThrow; + var thrower = promisify(function(a, b, c, cb) { + errToThrow = new Error(); + throw errToThrow; + }); + thrower(1, 2, 3) + .then(t.fail) + .then(t.fail, function (e) { + t.strictEqual(e, errToThrow); + t.end(); + }); +}); + +test('util.promisify callback and sync throw', function (t) { + var err = new Error(); + + var a = promisify(function (cb) { cb(err) })(); + var b = promisify(function () { throw err; })(); + + Promise.all([ + a.then(t.fail, function(e) { + t.strictEqual(err, e); + }), + b.then(t.fail, function(e) { + t.strictEqual(err, e); + }) + ]).then(function () { + t.end(); + }); +}); + +test('util.promisify throws for incorrect argument types', function (t) { + var array = [undefined, null, true, 0, 'str', {}, []]; + if (typeof Symbol !== 'undefined') array.push(Symbol()); + array.forEach(function (input) { + t.throws( + function () { promisify(input); }, + 'The "original" argument must be of type Function' + ); + }); + t.end(); +}); diff --git a/test/node/callbackify-async.js b/test/node/callbackify-async.js new file mode 100644 index 0000000..bb8f92c --- /dev/null +++ b/test/node/callbackify-async.js @@ -0,0 +1,120 @@ +'use strict'; + +// Separate test file for tests using new syntax (async/await). + +var common = require('./common'); +var assert = require('assert'); +var callbackify = require('../../').callbackify; +var execFile = require('child_process').execFile; + +var values = [ + 'hello world', + null, + undefined, + false, + 0, + {}, + { key: 'value' }, + Symbol('I am a symbol'), + function ok() {}, + ['array', 'with', 4, 'values'], + new Error('boo') +]; + +{ + // Test that the resolution value is passed as second argument to callback + values.forEach(function(value) { + // Test and `async function` + async function asyncFn() { + return value; + } + + var cbAsyncFn = callbackify(asyncFn); + cbAsyncFn(common.mustCall(function(err, ret) { + assert.ifError(err); + assert.strictEqual(ret, value); + })); + }); +} + +{ + // Test that rejection reason is passed as first argument to callback + values.forEach(function(value) { + // Test an `async function` + async function asyncFn() { + return Promise.reject(value); + } + + var cbAsyncFn = callbackify(asyncFn); + cbAsyncFn(common.mustCall(function (err, ret) { + assert.strictEqual(ret, undefined); + if (err instanceof Error) { + if ('reason' in err) { + assert(!value); + assert.strictEqual(err.message, 'Promise was rejected with a falsy value'); + assert.strictEqual(err.reason, value); + } else { + assert.strictEqual(String(value).endsWith(err.message), true); + } + } else { + assert.strictEqual(err, value); + } + })); + }); +} + +{ + // Test that arguments passed to callbackified function are passed to original + values.forEach(function(value) { + async function asyncFn(arg) { + assert.strictEqual(arg, value); + return arg; + } + + var cbAsyncFn = callbackify(asyncFn); + cbAsyncFn(value, common.mustCall(function(err, ret) { + assert.ifError(err); + assert.strictEqual(ret, value); + })); + }); +} + +{ + // Test that `this` binding is the same for callbackified and original + values.forEach(function(value) { + var iAmThat = { + async fn(arg) { + assert.strictEqual(this, iAmThat); + return arg; + }, + }; + iAmThat.cbFn = callbackify(iAmThat.fn); + iAmThat.cbFn(value, common.mustCall(function(err, ret) { + assert.ifError(err); + assert.strictEqual(ret, value); + assert.strictEqual(this, iAmThat); + })); + }); +} + +{ + async function asyncFn() { + return 42; + } + + var cb = callbackify(asyncFn); + var args = []; + + // Verify that the last argument to the callbackified function is a function. + ['foo', null, undefined, false, 0, {}, Symbol(), []].forEach(function(value) { + args.push(value); + common.expectsError(function() { + cb(...args); + }, { + code: 'ERR_INVALID_ARG_TYPE', + type: TypeError, + message: 'The last argument must be of type Function' + }); + }); +} + diff --git a/test/node/callbackify.js b/test/node/callbackify.js new file mode 100644 index 0000000..a3828ab --- /dev/null +++ b/test/node/callbackify.js @@ -0,0 +1,190 @@ +'use strict'; + +// This test checks that the semantics of `util.callbackify` are as described in +// the API docs + +var common = require('./common'); +var assert = require('assert'); +var callbackify = require('../../').callbackify; +var execFile = require('child_process').execFile; + +var values = [ + 'hello world', + null, + undefined, + false, + 0, + {}, + { key: 'value' }, + function ok() {}, + ['array', 'with', 4, 'values'], + new Error('boo') +]; +if (typeof Symbol !== 'undefined') { + values.push(Symbol('I am a symbol')); +} + +{ + // Test that the resolution value is passed as second argument to callback + values.forEach(function(value) { + // Test Promise factory + function promiseFn() { + return Promise.resolve(value); + } + + var cbPromiseFn = callbackify(promiseFn); + cbPromiseFn(common.mustCall(function(err, ret) { + assert.ifError(err); + assert.strictEqual(ret, value); + })); + + // Test Thenable + function thenableFn() { + return { + then: function(onRes, onRej) { + onRes(value); + } + }; + } + + var cbThenableFn = callbackify(thenableFn); + cbThenableFn(common.mustCall(function(err, ret) { + assert.ifError(err); + assert.strictEqual(ret, value); + })); + }); +} + +{ + // Test that rejection reason is passed as first argument to callback + values.forEach(function(value) { + // test a Promise factory + function promiseFn() { + return Promise.reject(value); + } + + var cbPromiseFn = callbackify(promiseFn); + cbPromiseFn(common.mustCall(function(err, ret) { + assert.strictEqual(ret, undefined); + if (err instanceof Error) { + if ('reason' in err) { + assert(!value); + assert.strictEqual(err.message, 'Promise was rejected with a falsy value'); + assert.strictEqual(err.reason, value); + } else { + assert.strictEqual(String(value).endsWith(err.message), true); + } + } else { + assert.strictEqual(err, value); + } + })); + + // Test Thenable + function thenableFn() { + return { + then: function (onRes, onRej) { + onRej(value); + } + }; + } + + var cbThenableFn = callbackify(thenableFn); + cbThenableFn(common.mustCall(function(err, ret) { + assert.strictEqual(ret, undefined); + if (err instanceof Error) { + if ('reason' in err) { + assert(!value); + assert.strictEqual(err.message, 'Promise was rejected with a falsy value'); + assert.strictEqual(err.reason, value); + } else { + assert.strictEqual(String(value).endsWith(err.message), true); + } + } else { + assert.strictEqual(err, value); + } + })); + }); +} + +{ + // Test that arguments passed to callbackified function are passed to original + values.forEach(function(value) { + function promiseFn(arg) { + assert.strictEqual(arg, value); + return Promise.resolve(arg); + } + + var cbPromiseFn = callbackify(promiseFn); + cbPromiseFn(value, common.mustCall(function(err, ret) { + assert.ifError(err); + assert.strictEqual(ret, value); + })); + }); +} + +{ + // Test that `this` binding is the same for callbackified and original + values.forEach(function(value) { + var iAmThis = { + fn: function(arg) { + assert.strictEqual(this, iAmThis); + return Promise.resolve(arg); + }, + }; + iAmThis.cbFn = callbackify(iAmThis.fn); + iAmThis.cbFn(value, common.mustCall(function(err, ret) { + assert.ifError(err); + assert.strictEqual(ret, value); + assert.strictEqual(this, iAmThis); + })); + }); +} + +// These tests are not necessary in the browser. +if (false) { + // Test that callback that throws emits an `uncaughtException` event + var fixture = fixtures.path('uncaught-exceptions', 'callbackify1.js'); + execFile( + process.execPath, + [fixture], + common.mustCall(function (err, stdout, stderr) { + assert.strictEqual(err.code, 1); + assert.strictEqual(Object.getPrototypeOf(err).name, 'Error'); + assert.strictEqual(stdout, ''); + var errLines = stderr.trim().split(/[\r\n]+/); + var errLine = errLines.find(function (l) { return /^Error/.exec(l) }); + assert.strictEqual(errLine, 'Error: ' + fixture); + }) + ); +} + +if (false) { + // Test that handled `uncaughtException` works and passes rejection reason + var fixture = fixtures.path('uncaught-exceptions', 'callbackify2.js'); + execFile( + process.execPath, + [fixture], + common.mustCall(function (err, stdout, stderr) { + assert.ifError(err); + assert.strictEqual(stdout.trim(), fixture); + assert.strictEqual(stderr, ''); + }) + ); +} + +{ + // Verify that non-function inputs throw. + ['foo', null, undefined, false, 0, {}, typeof Symbol !== 'undefined' ? Symbol() : undefined, []].forEach(function(value) { + common.expectsError(function() { + callbackify(value); + }, { + code: 'ERR_INVALID_ARG_TYPE', + type: TypeError, + message: 'The "original" argument must be of type Function' + }); + }); +} + +if (require('is-async-supported')()) { + require('./callbackify-async'); +} diff --git a/test/node/common.js b/test/node/common.js new file mode 100644 index 0000000..b5b9d58 --- /dev/null +++ b/test/node/common.js @@ -0,0 +1,31 @@ +var assert = require('assert'); + +var mustCalls = []; +var common = { + expectsError: function (fn, props) { + try { fn(); } + catch (err) { + if (props.type) assert.equal(err.constructor, props.type); + if (props.message) assert.equal(err.message, props.message); + return; + } + assert.fail('expected error'); + }, + mustCall: function (fn) { + function mustCall() { + mustCall.called = true + return fn.apply(this, arguments); + } + + mustCalls.push(mustCall); + return mustCall; + } +}; + +process.on('exit', function () { + mustCalls.forEach(function (mc) { + assert(mc.called); + }); +}); + +module.exports = common; diff --git a/test/node/index.js b/test/node/index.js index 6ad5b42..a3a807f 100644 --- a/test/node/index.js +++ b/test/node/index.js @@ -15,5 +15,7 @@ series([ test(require.resolve('./debug')), test(require.resolve('./format')), test(require.resolve('./inspect')), - test(require.resolve('./log')) + test(require.resolve('./log')), + test(require.resolve('./promisify')), + test(require.resolve('./callbackify')) ]); diff --git a/test/node/promisify.js b/test/node/promisify.js new file mode 100644 index 0000000..57afcf0 --- /dev/null +++ b/test/node/promisify.js @@ -0,0 +1,201 @@ +var common = require('./common'); +var assert = require('assert'); +var fs = require('fs'); +var vm = require('vm'); +var promisify = require('../../util').promisify; + +if (typeof Promise === 'undefined') { + console.log('no global Promise found, skipping promisify tests'); + return; +} + +var stat = promisify(fs.stat); + +{ + var promise = stat(__filename); + assert(promise instanceof Promise); + promise.then(common.mustCall(function (value) { + assert.deepStrictEqual(value, fs.statSync(__filename)); + })); +} + +{ + var promise = stat('/dontexist'); + promise.catch(common.mustCall(function (error) { + assert(error.message.indexOf('ENOENT: no such file or directory, stat') !== -1); + })); +} + +{ + function fn() {} + function promisifedFn() {} + fn[promisify.custom] = promisifedFn; + assert.strictEqual(promisify(fn), promisifedFn); + assert.strictEqual(promisify(promisify(fn)), promisifedFn); +} + +{ + function fn2() {} + fn2[promisify.custom] = 42; + common.expectsError( + function () { promisify(fn2); }, + { code: 'ERR_INVALID_ARG_TYPE', type: TypeError } + ); +} + +// promisify args test disabled, it is an internal core API that is +// not used anywhere anymore and this package does not implement it. +if (false) { + var firstValue = 5; + var secondValue = 17; + + function fn3(callback) { + callback(null, firstValue, secondValue); + } + + fn3[customPromisifyArgs] = ['first', 'second']; + + promisify(fn3)().then(common.mustCall(function (obj) { + assert.deepStrictEqual(obj, { first: firstValue, second: secondValue }); + })); +} + +{ + var fn4 = vm.runInNewContext('(function() {})'); + assert.notStrictEqual(Object.getPrototypeOf(promisify(fn4)), + Function.prototype); +} + +{ + function fn5(callback) { + callback(null, 'foo', 'bar'); + } + promisify(fn5)().then(common.mustCall(function (value) { + assert.deepStrictEqual(value, 'foo'); + })); +} + +{ + function fn6(callback) { + callback(null); + } + promisify(fn6)().then(common.mustCall(function (value) { + assert.strictEqual(value, undefined); + })); +} + +{ + function fn7(callback) { + callback(); + } + promisify(fn7)().then(common.mustCall(function (value) { + assert.strictEqual(value, undefined); + })); +} + +{ + function fn8(err, val, callback) { + callback(err, val); + } + promisify(fn8)(null, 42).then(common.mustCall(function (value) { + assert.strictEqual(value, 42); + })); +} + +{ + function fn9(err, val, callback) { + callback(err, val); + } + promisify(fn9)(new Error('oops'), null).catch(common.mustCall(function (err) { + assert.strictEqual(err.message, 'oops'); + })); +} + +{ + function fn9(err, val, callback) { + callback(err, val); + } + + + Promise.resolve() + .then(function () { return promisify(fn9)(null, 42); }) + .then(function (value) { + assert.strictEqual(value, 42); + }); +} + +{ + var o = {}; + var fn10 = promisify(function(cb) { + + cb(null, this === o); + }); + + o.fn = fn10; + + o.fn().then(common.mustCall(function(val) { + assert(val); + })); +} + +(function () { + var err = new Error('Should not have called the callback with the error.'); + var stack = err.stack; + + var fn11 = promisify(function(cb) { + cb(null); + cb(err); + }); + + Promise.resolve() + .then(function () { return fn11(); }) + .then(function () { return Promise.resolve(); }) + .then(function () { + assert.strictEqual(stack, err.stack); + }); +})(); + +{ + function c() { } + var a = promisify(function() { }); + var b = promisify(a); + assert.notStrictEqual(c, a); + assert.strictEqual(a, b); +} + +{ + var errToThrow; + var thrower = promisify(function(a, b, c, cb) { + errToThrow = new Error(); + throw errToThrow; + }); + thrower(1, 2, 3) + .then(assert.fail) + .then(assert.fail, function (e) { assert.strictEqual(e, errToThrow); }); +} + +{ + var err = new Error(); + + var a = promisify(function (cb) { cb(err) })(); + var b = promisify(function () { throw err; })(); + + Promise.all([ + a.then(assert.fail, function(e) { + assert.strictEqual(err, e); + }), + b.then(assert.fail, function(e) { + assert.strictEqual(err, e); + }) + ]); +} + +[undefined, null, true, 0, 'str', {}, [], Symbol()].forEach(function (input) { + common.expectsError( + function () { promisify(input); }, + { + code: 'ERR_INVALID_ARG_TYPE', + type: TypeError, + message: 'The "original" argument must be of type Function' + }); +}); diff --git a/util.js b/util.js index e0ea321..cbc2498 100644 --- a/util.js +++ b/util.js @@ -19,6 +19,16 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. +var getOwnPropertyDescriptors = Object.getOwnPropertyDescriptors || + function getOwnPropertyDescriptors(obj) { + var keys = Object.keys(obj); + var descriptors = {}; + for (var i = 0; i < keys.length; i++) { + descriptors[keys[i]] = Object.getOwnPropertyDescriptor(obj, keys[i]); + } + return descriptors; + }; + var formatRegExp = /%[sdj%]/g; exports.format = function(f) { if (!isString(f)) { @@ -584,3 +594,110 @@ exports._extend = function(origin, add) { function hasOwnProperty(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } + +var kCustomPromisifiedSymbol = typeof Symbol !== 'undefined' ? Symbol('util.promisify.custom') : undefined; + +exports.promisify = function promisify(original) { + if (typeof original !== 'function') + throw new TypeError('The "original" argument must be of type Function'); + + if (kCustomPromisifiedSymbol && original[kCustomPromisifiedSymbol]) { + var fn = original[kCustomPromisifiedSymbol]; + if (typeof fn !== 'function') { + throw new TypeError('The "util.promisify.custom" argument must be of type Function'); + } + Object.defineProperty(fn, kCustomPromisifiedSymbol, { + value: fn, enumerable: false, writable: false, configurable: true + }); + return fn; + } + + function fn() { + var promiseResolve, promiseReject; + var promise = new Promise(function (resolve, reject) { + promiseResolve = resolve; + promiseReject = reject; + }); + + var args = []; + for (var i = 0; i < arguments.length; i++) { + args.push(arguments[i]); + } + args.push(function (err, value) { + if (err) { + promiseReject(err); + } else { + promiseResolve(value); + } + }); + + try { + original.apply(this, args); + } catch (err) { + promiseReject(err); + } + + return promise; + } + + Object.setPrototypeOf(fn, Object.getPrototypeOf(original)); + + if (kCustomPromisifiedSymbol) Object.defineProperty(fn, kCustomPromisifiedSymbol, { + value: fn, enumerable: false, writable: false, configurable: true + }); + return Object.defineProperties( + fn, + getOwnPropertyDescriptors(original) + ); +} + +exports.promisify.custom = kCustomPromisifiedSymbol + +function callbackifyOnRejected(reason, cb) { + // `!reason` guard inspired by bluebird (Ref: https://goo.gl/t5IS6M). + // Because `null` is a special error value in callbacks which means "no error + // occurred", we error-wrap so the callback consumer can distinguish between + // "the promise rejected with null" or "the promise fulfilled with undefined". + if (!reason) { + var newReason = new Error('Promise was rejected with a falsy value'); + newReason.reason = reason; + reason = newReason; + } + return cb(reason); +} + +function callbackify(original) { + if (typeof original !== 'function') { + throw new TypeError('The "original" argument must be of type Function'); + } + + // We DO NOT return the promise as it gives the user a false sense that + // the promise is actually somehow related to the callback's execution + // and that the callback throwing will reject the promise. + function callbackified() { + var args = []; + for (var i = 0; i < arguments.length; i++) { + args.push(arguments[i]); + } + + var maybeCb = args.pop(); + if (typeof maybeCb !== 'function') { + throw new TypeError('The last argument must be of type Function'); + } + var self = this; + var cb = function() { + return maybeCb.apply(self, arguments); + }; + // In true node style we process the callback on `nextTick` with all the + // implications (stack, `uncaughtException`, `async_hooks`) + original.apply(this, args) + .then(function(ret) { process.nextTick(cb, null, ret) }, + function(rej) { process.nextTick(callbackifyOnRejected, rej, cb) }); + } + + Object.setPrototypeOf(callbackified, Object.getPrototypeOf(original)); + Object.defineProperties(callbackified, + getOwnPropertyDescriptors(original)); + return callbackified; +} +exports.callbackify = callbackify;