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
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
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.log()`]: console.html#console_console_log_data_args
|
||||
[`util.inspect()`]: #util_util_inspect_object_options
|
||||
[`util.promisify()`]: #util_util_promisify_original
|
||||
[Custom inspection functions on Objects]: #util_custom_inspection_functions_on_objects
|
||||
[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
|
||||
[semantically incompatible]: https://github.com/nodejs/node/issues/4179
|
||||
|
@ -4,6 +4,8 @@ const errors = require('internal/errors');
|
||||
const binding = process.binding('util');
|
||||
const signals = process.binding('constants').os.signals;
|
||||
|
||||
const { createPromise, promiseResolve, promiseReject } = binding;
|
||||
|
||||
const kArrowMessagePrivateSymbolIndex = binding['arrow_message_private_symbol'];
|
||||
const kDecoratedPrivateSymbolIndex = binding['decorated_private_symbol'];
|
||||
const noCrypto = !process.versions.openssl;
|
||||
@ -217,3 +219,62 @@ module.exports = exports = {
|
||||
// default isEncoding implementation, just in case userland overrides it.
|
||||
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[exports.inspect.custom] =
|
||||
(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) \
|
||||
V(isAsyncFunction, IsAsyncFunction) \
|
||||
V(isDataView, IsDataView) \
|
||||
V(isDate, IsDate) \
|
||||
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