Browse Source

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>
v6
Anna Henningsen 8 years ago
parent
commit
99da8e8e02
No known key found for this signature in database GPG Key ID: D8B9F5AEAE84E4CF
  1. 82
      doc/api/util.md
  2. 61
      lib/internal/util.js
  3. 2
      lib/util.js
  4. 1
      src/node_util.cc
  5. 76
      test/parallel/test-util-promisify.js

82
doc/api/util.md

@ -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

61
lib/internal/util.js

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

2
lib/util.js

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

1
src/node_util.cc

@ -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

@ -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…
Cancel
Save