diff --git a/doc/api/util.md b/doc/api/util.md index 9de5abd7a85304..7a9a779cbab935 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -399,6 +399,86 @@ util.inspect.defaultOptions.maxArrayLength = null; console.log(arr); // logs the full array ``` +## util.promisify(original) + + +* `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 + + +* {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 diff --git a/lib/internal/util.js b/lib/internal/util.js index 1633a920d55209..4dfcf81837b912 100644 --- a/lib/internal/util.js +++ b/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; diff --git a/lib/util.js b/lib/util.js index 4e626a3accc3f8..d73b12dcfd59b3 100644 --- a/lib/util.js +++ b/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; diff --git a/src/node_util.cc b/src/node_util.cc index 3424739cc96688..50de94bfb2bf3a 100644 --- a/src/node_util.cc +++ b/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) \ diff --git a/test/parallel/test-util-promisify.js b/test/parallel/test-util-promisify.js new file mode 100644 index 00000000000000..23b3d400375239 --- /dev/null +++ b/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); + })); +}