util: add util.promisify()
Add `util.promisify(function)` for creating promisified functions. Includes documentation and tests. Fixes: https://github.com/nodejs/CTC/issues/12 PR-URL: https://github.com/nodejs/node/pull/12442 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Myles Borins <myles.borins@gmail.com> Reviewed-By: Evan Lucas <evanlucas@me.com> Reviewed-By: William Kapke <william.kapke@gmail.com> Reviewed-By: Timothy Gu <timothygu99@gmail.com> Reviewed-By: Teddy Katz <teddy.katz@gmail.com>
This commit is contained in:
parent
059f296050
commit
99da8e8e02
@ -399,6 +399,86 @@ util.inspect.defaultOptions.maxArrayLength = null;
|
|||||||
console.log(arr); // logs the full array
|
console.log(arr); // logs the full array
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## util.promisify(original)
|
||||||
|
<!-- YAML
|
||||||
|
added: REPLACEME
|
||||||
|
-->
|
||||||
|
|
||||||
|
* `original` {Function}
|
||||||
|
|
||||||
|
Takes a function following the common Node.js callback style, i.e. taking a
|
||||||
|
`(err, value) => ...` callback as the last argument, and returns a version
|
||||||
|
that returns promises.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const util = require('util');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const stat = util.promisify(fs.stat);
|
||||||
|
stat('.').then((stats) => {
|
||||||
|
// Do something with `stats`
|
||||||
|
}).catch((error) => {
|
||||||
|
// Handle the error.
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Or, equivalently using `async function`s:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const util = require('util');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const stat = util.promisify(fs.stat);
|
||||||
|
|
||||||
|
async function callStat() {
|
||||||
|
const stats = await stat('.');
|
||||||
|
console.log(`This directory is owned by ${stats.uid}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If there is an `original[util.promisify.custom]` property present, `promisify`
|
||||||
|
will return its value, see [Custom promisified functions][].
|
||||||
|
|
||||||
|
`promisify()` assumes that `original` is a function taking a callback as its
|
||||||
|
final argument in all cases, and the returned function will result in undefined
|
||||||
|
behaviour if it does not.
|
||||||
|
|
||||||
|
### Custom promisified functions
|
||||||
|
|
||||||
|
Using the `util.promisify.custom` symbol one can override the return value of
|
||||||
|
[`util.promisify()`][]:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const util = require('util');
|
||||||
|
|
||||||
|
function doSomething(foo, callback) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
doSomething[util.promisify.custom] = function(foo) {
|
||||||
|
return getPromiseSomehow();
|
||||||
|
};
|
||||||
|
|
||||||
|
const promisified = util.promisify(doSomething);
|
||||||
|
console.log(promisified === doSomething[util.promisify.custom]);
|
||||||
|
// prints 'true'
|
||||||
|
```
|
||||||
|
|
||||||
|
This can be useful for cases where the original function does not follow the
|
||||||
|
standard format of taking an error-first callback as the last argument.
|
||||||
|
|
||||||
|
### util.promisify.custom
|
||||||
|
<!-- YAML
|
||||||
|
added: REPLACEME
|
||||||
|
-->
|
||||||
|
|
||||||
|
* {symbol}
|
||||||
|
|
||||||
|
A Symbol that can be used to declare custom promisified variants of functions,
|
||||||
|
see [Custom promisified functions][].
|
||||||
|
|
||||||
## Deprecated APIs
|
## Deprecated APIs
|
||||||
|
|
||||||
The following APIs have been deprecated and should no longer be used. Existing
|
The following APIs have been deprecated and should no longer be used. Existing
|
||||||
@ -878,7 +958,9 @@ Deprecated predecessor of `console.log`.
|
|||||||
[`console.error()`]: console.html#console_console_error_data_args
|
[`console.error()`]: console.html#console_console_error_data_args
|
||||||
[`console.log()`]: console.html#console_console_log_data_args
|
[`console.log()`]: console.html#console_console_log_data_args
|
||||||
[`util.inspect()`]: #util_util_inspect_object_options
|
[`util.inspect()`]: #util_util_inspect_object_options
|
||||||
|
[`util.promisify()`]: #util_util_promisify_original
|
||||||
[Custom inspection functions on Objects]: #util_custom_inspection_functions_on_objects
|
[Custom inspection functions on Objects]: #util_custom_inspection_functions_on_objects
|
||||||
[Customizing `util.inspect` colors]: #util_customizing_util_inspect_colors
|
[Customizing `util.inspect` colors]: #util_customizing_util_inspect_colors
|
||||||
|
[Custom promisified functions]: #util_custom_promisified_functions
|
||||||
[constructor]: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/constructor
|
[constructor]: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/constructor
|
||||||
[semantically incompatible]: https://github.com/nodejs/node/issues/4179
|
[semantically incompatible]: https://github.com/nodejs/node/issues/4179
|
||||||
|
@ -4,6 +4,8 @@ const errors = require('internal/errors');
|
|||||||
const binding = process.binding('util');
|
const binding = process.binding('util');
|
||||||
const signals = process.binding('constants').os.signals;
|
const signals = process.binding('constants').os.signals;
|
||||||
|
|
||||||
|
const { createPromise, promiseResolve, promiseReject } = binding;
|
||||||
|
|
||||||
const kArrowMessagePrivateSymbolIndex = binding['arrow_message_private_symbol'];
|
const kArrowMessagePrivateSymbolIndex = binding['arrow_message_private_symbol'];
|
||||||
const kDecoratedPrivateSymbolIndex = binding['decorated_private_symbol'];
|
const kDecoratedPrivateSymbolIndex = binding['decorated_private_symbol'];
|
||||||
const noCrypto = !process.versions.openssl;
|
const noCrypto = !process.versions.openssl;
|
||||||
@ -217,3 +219,62 @@ module.exports = exports = {
|
|||||||
// default isEncoding implementation, just in case userland overrides it.
|
// default isEncoding implementation, just in case userland overrides it.
|
||||||
kIsEncodingSymbol: Symbol('node.isEncoding')
|
kIsEncodingSymbol: Symbol('node.isEncoding')
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const kCustomPromisifiedSymbol = Symbol('util.promisify.custom');
|
||||||
|
const kCustomPromisifyArgsSymbol = Symbol('customPromisifyArgs');
|
||||||
|
|
||||||
|
function promisify(orig) {
|
||||||
|
if (typeof orig !== 'function') {
|
||||||
|
const errors = require('internal/errors');
|
||||||
|
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'original', 'function');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orig[kCustomPromisifiedSymbol]) {
|
||||||
|
const fn = orig[kCustomPromisifiedSymbol];
|
||||||
|
if (typeof fn !== 'function') {
|
||||||
|
throw new TypeError('The [util.promisify.custom] property must be ' +
|
||||||
|
'a function');
|
||||||
|
}
|
||||||
|
Object.defineProperty(fn, kCustomPromisifiedSymbol, {
|
||||||
|
value: fn, enumerable: false, writable: false, configurable: true
|
||||||
|
});
|
||||||
|
return fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Names to create an object from in case the callback receives multiple
|
||||||
|
// arguments, e.g. ['stdout', 'stderr'] for child_process.exec.
|
||||||
|
const argumentNames = orig[kCustomPromisifyArgsSymbol];
|
||||||
|
|
||||||
|
function fn(...args) {
|
||||||
|
const promise = createPromise();
|
||||||
|
try {
|
||||||
|
orig.call(this, ...args, (err, ...values) => {
|
||||||
|
if (err) {
|
||||||
|
promiseReject(promise, err);
|
||||||
|
} else if (argumentNames !== undefined && values.length > 1) {
|
||||||
|
const obj = {};
|
||||||
|
for (var i = 0; i < argumentNames.length; i++)
|
||||||
|
obj[argumentNames[i]] = values[i];
|
||||||
|
promiseResolve(promise, obj);
|
||||||
|
} else {
|
||||||
|
promiseResolve(promise, values[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
promiseReject(promise, err);
|
||||||
|
}
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.setPrototypeOf(fn, Object.getPrototypeOf(orig));
|
||||||
|
|
||||||
|
Object.defineProperty(fn, kCustomPromisifiedSymbol, {
|
||||||
|
value: fn, enumerable: false, writable: false, configurable: true
|
||||||
|
});
|
||||||
|
return Object.defineProperties(fn, Object.getOwnPropertyDescriptors(orig));
|
||||||
|
}
|
||||||
|
|
||||||
|
promisify.custom = kCustomPromisifiedSymbol;
|
||||||
|
|
||||||
|
exports.promisify = promisify;
|
||||||
|
exports.customPromisifyArgs = kCustomPromisifyArgsSymbol;
|
||||||
|
@ -1057,3 +1057,5 @@ exports._exceptionWithHostPort = function(err,
|
|||||||
// process.versions needs a custom function as some values are lazy-evaluated.
|
// process.versions needs a custom function as some values are lazy-evaluated.
|
||||||
process.versions[exports.inspect.custom] =
|
process.versions[exports.inspect.custom] =
|
||||||
(depth) => exports.format(JSON.parse(JSON.stringify(process.versions)));
|
(depth) => exports.format(JSON.parse(JSON.stringify(process.versions)));
|
||||||
|
|
||||||
|
exports.promisify = internalUtil.promisify;
|
||||||
|
@ -21,6 +21,7 @@ using v8::Value;
|
|||||||
|
|
||||||
|
|
||||||
#define VALUE_METHOD_MAP(V) \
|
#define VALUE_METHOD_MAP(V) \
|
||||||
|
V(isAsyncFunction, IsAsyncFunction) \
|
||||||
V(isDataView, IsDataView) \
|
V(isDataView, IsDataView) \
|
||||||
V(isDate, IsDate) \
|
V(isDate, IsDate) \
|
||||||
V(isExternal, IsExternal) \
|
V(isExternal, IsExternal) \
|
||||||
|
76
test/parallel/test-util-promisify.js
Normal file
76
test/parallel/test-util-promisify.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
'use strict';
|
||||||
|
const common = require('../common');
|
||||||
|
const assert = require('assert');
|
||||||
|
const fs = require('fs');
|
||||||
|
const vm = require('vm');
|
||||||
|
const { promisify } = require('util');
|
||||||
|
|
||||||
|
common.crashOnUnhandledRejection();
|
||||||
|
|
||||||
|
const stat = promisify(fs.stat);
|
||||||
|
|
||||||
|
{
|
||||||
|
const promise = stat(__filename);
|
||||||
|
assert(promise instanceof Promise);
|
||||||
|
promise.then(common.mustCall((value) => {
|
||||||
|
assert.deepStrictEqual(value, fs.statSync(__filename));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const promise = stat('/dontexist');
|
||||||
|
promise.catch(common.mustCall((error) => {
|
||||||
|
assert(error.message.includes('ENOENT: no such file or directory, stat'));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
function fn() {}
|
||||||
|
function promisifedFn() {}
|
||||||
|
fn[promisify.custom] = promisifedFn;
|
||||||
|
assert.strictEqual(promisify(fn), promisifedFn);
|
||||||
|
assert.strictEqual(promisify(promisify(fn)), promisifedFn);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
function fn() {}
|
||||||
|
fn[promisify.custom] = 42;
|
||||||
|
assert.throws(
|
||||||
|
() => promisify(fn),
|
||||||
|
(err) => err instanceof TypeError &&
|
||||||
|
err.message === 'The [util.promisify.custom] property must ' +
|
||||||
|
'be a function');
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const fn = vm.runInNewContext('(function() {})');
|
||||||
|
assert.notStrictEqual(Object.getPrototypeOf(promisify(fn)),
|
||||||
|
Function.prototype);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
function fn(callback) {
|
||||||
|
callback(null, 'foo', 'bar');
|
||||||
|
}
|
||||||
|
promisify(fn)().then(common.mustCall((value) => {
|
||||||
|
assert.deepStrictEqual(value, 'foo');
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
function fn(callback) {
|
||||||
|
callback(null);
|
||||||
|
}
|
||||||
|
promisify(fn)().then(common.mustCall((value) => {
|
||||||
|
assert.strictEqual(value, undefined);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
function fn(callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
promisify(fn)().then(common.mustCall((value) => {
|
||||||
|
assert.strictEqual(value, undefined);
|
||||||
|
}));
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user