Browse Source

Add `util.callbackify`

inspect
Renée Kooi 7 years ago
parent
commit
a5e4650587
No known key found for this signature in database GPG Key ID: 8CDD5F0BC448F040
  1. 1
      package.json
  2. 120
      test/node/callbackify-async.js
  3. 190
      test/node/callbackify.js
  4. 31
      test/node/common.js
  5. 3
      test/node/index.js
  6. 29
      test/node/promisify.js
  7. 49
      util.js

1
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"
},

120
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'
});
});
}

190
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');
}

31
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;

3
test/node/index.js

@ -16,5 +16,6 @@ series([
test(require.resolve('./format')),
test(require.resolve('./inspect')),
test(require.resolve('./log')),
test(require.resolve('./promisify'))
test(require.resolve('./promisify')),
test(require.resolve('./callbackify'))
]);

29
test/node/promisify.js

@ -1,3 +1,4 @@
var common = require('./common');
var assert = require('assert');
var fs = require('fs');
var vm = require('vm');
@ -8,28 +9,6 @@ if (typeof Promise === 'undefined') {
return;
}
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;
}
};
var stat = promisify(fs.stat);
{
@ -220,9 +199,3 @@ if (false) {
message: 'The "original" argument must be of type Function'
});
});
process.on('exit', function () {
mustCalls.forEach(function (mc) {
assert(mc.called);
});
});

49
util.js

@ -642,3 +642,52 @@ exports.promisify = function promisify(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,
Object.getOwnPropertyDescriptors(original));
return callbackified;
}
exports.callbackify = callbackify;

Loading…
Cancel
Save