Skip to content

Commit

Permalink
util: add util.promisify()
Browse files Browse the repository at this point in the history
Add `util.promisify(function)` for creating promisified functions.
Includes documentation and tests.

Fixes: nodejs/CTC#12
PR-URL: nodejs#12442
Reviewed-By: Benjamin Gruenbaum <[email protected]>
Reviewed-By: Matteo Collina <[email protected]>
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Myles Borins <[email protected]>
Reviewed-By: Evan Lucas <[email protected]>
Reviewed-By: William Kapke <[email protected]>
Reviewed-By: Timothy Gu <[email protected]>
Reviewed-By: Teddy Katz <[email protected]>
  • Loading branch information
addaleax committed May 9, 2017
1 parent 059f296 commit 99da8e8
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 0 deletions.
82 changes: 82 additions & 0 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 changes: 61 additions & 0 deletions lib/internal/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 changes: 2 additions & 0 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 change: 1 addition & 0 deletions src/node_util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ using v8::Value;


#define VALUE_METHOD_MAP(V) \
V(isAsyncFunction, IsAsyncFunction) \
V(isDataView, IsDataView) \
V(isDate, IsDate) \
V(isExternal, IsExternal) \
Expand Down
76 changes: 76 additions & 0 deletions test/parallel/test-util-promisify.js
Original file line number Diff line number Diff line change
@@ -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);
}));
}

0 comments on commit 99da8e8

Please sign in to comment.