util: add callbackify
Add `util.callbackify(function)` for creating callback style functions from functions returning a `Thenable` PR-URL: https://github.com/nodejs/node/pull/12712 Fixes: https://github.com/nodejs/CTC/issues/109 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Teddy Katz <teddy.katz@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Timothy Gu <timothygu99@gmail.com> Reviewed-By: Anna Henningsen <anna@addaleax.net>
This commit is contained in:
parent
780acc2208
commit
af3aa682ac
@ -10,6 +10,64 @@ module developers as well. It can be accessed using:
|
||||
const util = require('util');
|
||||
```
|
||||
|
||||
## util.callbackify(original)
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* `original` {Function} An `async` function
|
||||
* Returns: {Function} a callback style function
|
||||
|
||||
Takes an `async` function (or a function that returns a Promise) and returns a
|
||||
function following the Node.js error first callback style. In the callback, the
|
||||
first argument will be the rejection reason (or `null` if the Promise resolved),
|
||||
and the second argument will be the resolved value.
|
||||
|
||||
For example:
|
||||
|
||||
```js
|
||||
const util = require('util');
|
||||
|
||||
async function fn() {
|
||||
return await Promise.resolve('hello world');
|
||||
}
|
||||
const callbackFunction = util.callbackify(fn);
|
||||
|
||||
callbackFunction((err, ret) => {
|
||||
if (err) throw err;
|
||||
console.log(ret);
|
||||
});
|
||||
```
|
||||
|
||||
Will print:
|
||||
|
||||
```txt
|
||||
hello world
|
||||
```
|
||||
|
||||
*Note*:
|
||||
|
||||
* The callback is executed asynchronously, and will have a limited stack trace.
|
||||
If the callback throws, the process will emit an [`'uncaughtException'`][]
|
||||
event, and if not handled will exit.
|
||||
|
||||
* Since `null` has a special meaning as the first argument to a callback, if a
|
||||
wrapped function rejects a `Promise` with a falsy value as a reason, the value
|
||||
is wrapped in an `Error` with the original value stored in a field named
|
||||
`reason`.
|
||||
```js
|
||||
function fn() {
|
||||
return Promise.reject(null);
|
||||
}
|
||||
const callbackFunction = util.callbackify(fn);
|
||||
|
||||
callbackFunction((err, ret) => {
|
||||
// When the Promise was rejected with `null` it is wrapped with an Error and
|
||||
// the original value is stored in `reason`.
|
||||
err && err.hasOwnProperty('reason') && err.reason === null; // true
|
||||
});
|
||||
```
|
||||
|
||||
## util.debuglog(section)
|
||||
<!-- YAML
|
||||
added: v0.11.3
|
||||
@ -955,6 +1013,7 @@ Deprecated predecessor of `console.log`.
|
||||
[`Object.assign()`]: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
|
||||
[`console.error()`]: console.html#console_console_error_data_args
|
||||
[`console.log()`]: console.html#console_console_log_data_args
|
||||
[`'uncaughtException'`]: process.html#process_event_uncaughtexception
|
||||
[`util.inspect()`]: #util_util_inspect_object_options
|
||||
[`util.promisify()`]: #util_util_promisify_original
|
||||
[Custom inspection functions on Objects]: #util_custom_inspection_functions_on_objects
|
||||
|
@ -164,6 +164,7 @@ E('ERR_SOCKET_BAD_PORT', 'Port should be > 0 and < 65536');
|
||||
E('ERR_SOCKET_DGRAM_NOT_RUNNING', 'Not running');
|
||||
E('ERR_V8BREAKITERATOR', 'full ICU data not installed. ' +
|
||||
'See https://github.com/nodejs/node/wiki/Intl');
|
||||
E('FALSY_VALUE_REJECTION', 'Promise was rejected with falsy value');
|
||||
// Add new errors from here...
|
||||
|
||||
function invalidArgType(name, expected, actual) {
|
||||
|
50
lib/util.js
50
lib/util.js
@ -1047,3 +1047,53 @@ process.versions[exports.inspect.custom] =
|
||||
(depth) => exports.format(JSON.parse(JSON.stringify(process.versions)));
|
||||
|
||||
exports.promisify = internalUtil.promisify;
|
||||
|
||||
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) {
|
||||
const newReason = new errors.Error('FALSY_VALUE_REJECTION');
|
||||
newReason.reason = reason;
|
||||
reason = newReason;
|
||||
Error.captureStackTrace(reason, callbackifyOnRejected);
|
||||
}
|
||||
return cb(reason);
|
||||
}
|
||||
|
||||
|
||||
function callbackify(original) {
|
||||
if (typeof original !== 'function') {
|
||||
throw new errors.TypeError(
|
||||
'ERR_INVALID_ARG_TYPE',
|
||||
'original',
|
||||
'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(...args) {
|
||||
const maybeCb = args.pop();
|
||||
if (typeof maybeCb !== 'function') {
|
||||
throw new errors.TypeError(
|
||||
'ERR_INVALID_ARG_TYPE',
|
||||
'last argument',
|
||||
'function');
|
||||
}
|
||||
const cb = (...args) => { Reflect.apply(maybeCb, this, args); };
|
||||
// In true node style we process the callback on `nextTick` with all the
|
||||
// implications (stack, `uncaughtException`, `async_hooks`)
|
||||
Reflect.apply(original, this, args)
|
||||
.then((ret) => process.nextTick(cb, null, ret),
|
||||
(rej) => process.nextTick(callbackifyOnRejected, rej, cb));
|
||||
}
|
||||
|
||||
Object.setPrototypeOf(callbackified, Object.getPrototypeOf(original));
|
||||
Object.defineProperties(callbackified,
|
||||
Object.getOwnPropertyDescriptors(original));
|
||||
return callbackified;
|
||||
}
|
||||
|
||||
exports.callbackify = callbackify;
|
||||
|
15
test/fixtures/uncaught-exceptions/callbackify1.js
vendored
Normal file
15
test/fixtures/uncaught-exceptions/callbackify1.js
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
// Used to test that `uncaughtException` is emitted
|
||||
|
||||
const { callbackify } = require('util');
|
||||
|
||||
{
|
||||
async function fn() { }
|
||||
|
||||
const cbFn = callbackify(fn);
|
||||
|
||||
cbFn((err, ret) => {
|
||||
throw new Error(__filename);
|
||||
});
|
||||
}
|
22
test/fixtures/uncaught-exceptions/callbackify2.js
vendored
Normal file
22
test/fixtures/uncaught-exceptions/callbackify2.js
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
// Used to test the `uncaughtException` err object
|
||||
|
||||
const assert = require('assert');
|
||||
const { callbackify } = require('util');
|
||||
|
||||
{
|
||||
const sentinel = new Error(__filename);
|
||||
process.once('uncaughtException', (err) => {
|
||||
assert.strictEqual(err, sentinel);
|
||||
// Calling test will use `stdout` to assert value of `err.message`
|
||||
console.log(err.message);
|
||||
});
|
||||
|
||||
async function fn() {
|
||||
return await Promise.reject(sentinel);
|
||||
}
|
||||
|
||||
const cbFn = callbackify(fn);
|
||||
cbFn((err, ret) => assert.ifError(err));
|
||||
}
|
227
test/parallel/test-util-callbackify.js
Normal file
227
test/parallel/test-util-callbackify.js
Normal file
@ -0,0 +1,227 @@
|
||||
'use strict';
|
||||
const common = require('../common');
|
||||
|
||||
// This test checks that the semantics of `util.callbackify` are as described in
|
||||
// the API docs
|
||||
|
||||
const assert = require('assert');
|
||||
const { callbackify } = require('util');
|
||||
const { join } = require('path');
|
||||
const { execFile } = require('child_process');
|
||||
const fixtureDir = join(common.fixturesDir, 'uncaught-exceptions');
|
||||
const 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
|
||||
for (const value of values) {
|
||||
// Test and `async function`
|
||||
async function asyncFn() {
|
||||
return await Promise.resolve(value);
|
||||
}
|
||||
|
||||
const cbAsyncFn = callbackify(asyncFn);
|
||||
cbAsyncFn(common.mustCall((err, ret) => {
|
||||
assert.ifError(err);
|
||||
assert.strictEqual(ret, value);
|
||||
}));
|
||||
|
||||
// Test Promise factory
|
||||
function promiseFn() {
|
||||
return Promise.resolve(value);
|
||||
}
|
||||
|
||||
const cbPromiseFn = callbackify(promiseFn);
|
||||
cbPromiseFn(common.mustCall((err, ret) => {
|
||||
assert.ifError(err);
|
||||
assert.strictEqual(ret, value);
|
||||
}));
|
||||
|
||||
// Test Thenable
|
||||
function thenableFn() {
|
||||
return {
|
||||
then(onRes, onRej) {
|
||||
onRes(value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const cbThenableFn = callbackify(thenableFn);
|
||||
cbThenableFn(common.mustCall((err, ret) => {
|
||||
assert.ifError(err);
|
||||
assert.strictEqual(ret, value);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// Test that rejection reason is passed as first argument to callback
|
||||
for (const value of values) {
|
||||
// Test an `async function`
|
||||
async function asyncFn() {
|
||||
return await Promise.reject(value);
|
||||
}
|
||||
|
||||
const cbAsyncFn = callbackify(asyncFn);
|
||||
cbAsyncFn(common.mustCall((err, ret) => {
|
||||
assert.strictEqual(ret, undefined);
|
||||
if (err instanceof Error) {
|
||||
if ('reason' in err) {
|
||||
assert(!value);
|
||||
assert.strictEqual(err.code, 'FALSY_VALUE_REJECTION');
|
||||
assert.strictEqual(err.reason, value);
|
||||
} else {
|
||||
assert.strictEqual(String(value).endsWith(err.message), true);
|
||||
}
|
||||
} else {
|
||||
assert.strictEqual(err, value);
|
||||
}
|
||||
}));
|
||||
|
||||
// test a Promise factory
|
||||
function promiseFn() {
|
||||
return Promise.reject(value);
|
||||
}
|
||||
|
||||
const cbPromiseFn = callbackify(promiseFn);
|
||||
cbPromiseFn(common.mustCall((err, ret) => {
|
||||
assert.strictEqual(ret, undefined);
|
||||
if (err instanceof Error) {
|
||||
if ('reason' in err) {
|
||||
assert(!value);
|
||||
assert.strictEqual(err.code, 'FALSY_VALUE_REJECTION');
|
||||
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(onRes, onRej) {
|
||||
onRej(value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const cbThenableFn = callbackify(thenableFn);
|
||||
cbThenableFn(common.mustCall((err, ret) => {
|
||||
assert.strictEqual(ret, undefined);
|
||||
if (err instanceof Error) {
|
||||
if ('reason' in err) {
|
||||
assert(!value);
|
||||
assert.strictEqual(err.code, 'FALSY_VALUE_REJECTION');
|
||||
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
|
||||
for (const value of values) {
|
||||
async function asyncFn(arg) {
|
||||
assert.strictEqual(arg, value);
|
||||
return await Promise.resolve(arg);
|
||||
}
|
||||
|
||||
const cbAsyncFn = callbackify(asyncFn);
|
||||
cbAsyncFn(value, common.mustCall((err, ret) => {
|
||||
assert.ifError(err);
|
||||
assert.strictEqual(ret, value);
|
||||
}));
|
||||
|
||||
function promiseFn(arg) {
|
||||
assert.strictEqual(arg, value);
|
||||
return Promise.resolve(arg);
|
||||
}
|
||||
|
||||
const cbPromiseFn = callbackify(promiseFn);
|
||||
cbPromiseFn(value, common.mustCall((err, ret) => {
|
||||
assert.ifError(err);
|
||||
assert.strictEqual(ret, value);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// Test that `this` binding is the same for callbackified and original
|
||||
for (const value of values) {
|
||||
const iAmThis = {
|
||||
fn(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);
|
||||
}));
|
||||
|
||||
const iAmThat = {
|
||||
async fn(arg) {
|
||||
assert.strictEqual(this, iAmThat);
|
||||
return await Promise.resolve(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);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// Test that callback that throws emits an `uncaughtException` event
|
||||
const fixture = join(fixtureDir, 'callbackify1.js');
|
||||
execFile(
|
||||
process.execPath,
|
||||
[fixture],
|
||||
common.mustCall((err, stdout, stderr) => {
|
||||
assert.strictEqual(err.code, 1);
|
||||
assert.strictEqual(Object.getPrototypeOf(err).name, 'Error');
|
||||
assert.strictEqual(stdout, '');
|
||||
const errLines = stderr.trim().split(/[\r\n]+/g);
|
||||
const errLine = errLines.find((l) => /^Error/.exec(l));
|
||||
assert.strictEqual(errLine, `Error: ${fixture}`);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
// Test that handled `uncaughtException` works and passes rejection reason
|
||||
const fixture = join(fixtureDir, 'callbackify2.js');
|
||||
execFile(
|
||||
process.execPath,
|
||||
[fixture],
|
||||
common.mustCall((err, stdout, stderr) => {
|
||||
assert.ifError(err);
|
||||
assert.strictEqual(stdout.trim(), fixture);
|
||||
assert.strictEqual(stderr, '');
|
||||
})
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user