diff --git a/doc/api/child_process.markdown b/doc/api/child_process.markdown index 65d7be99c6fad4..0b27d408243497 100644 --- a/doc/api/child_process.markdown +++ b/doc/api/child_process.markdown @@ -182,6 +182,21 @@ terminated. *Note: Unlike the `exec()` POSIX system call, `child_process.exec()` does not replace the existing process and uses a shell to execute the command.* +### child_process.exec(command[, options]) + +A [`Promise`][def-promise]-returning variant of [`child_process.exec()`][]. +Resolves to a two-element array of [Strings][def-string], representing +`stdout` output and `stderr` respectively. + +```js +child_process.exec('cat *.js bad_file | wc -l').then((output) => { + console.log(`stdout: ${output[0]}`); + console.log(`stderr: ${output[1]}`); +}).catch((err) => { + console.error(`something went wrong: ${err}`); +}); +``` + ### child_process.execFile(file[, args][, options][, callback]) * `file` {String} A path to an executable file @@ -220,6 +235,20 @@ const child = execFile('node', ['--version'], (error, stdout, stderr) => { }); ``` +### child_process.execFileAsync(file[, args][, options]) + +A [`Promise`][def-promise]-returning variant of [`child_process.execFile()`][]. +Resolves to a two-element array of [Strings][def-string], representing +`stdout` output and `stderr` respectively. + +```js +const execFile = require('child_process').execFile; +const child = execFile('node', ['--version']).then((output) => { + console.log(output[0]); // stdout + console.log(output[1]); // stderr +}); +``` + ### child_process.fork(modulePath[, args][, options]) * `modulePath` {String} The module to run in the child @@ -755,7 +784,7 @@ grep.stdin.end(); * `message` {Object} * `sendHandle` {Handle object} * `callback` {Function} -* Return: Boolean +* Return: Boolean | Promise When an IPC channel has been established between the parent and child ( i.e. when using [`child_process.fork()`][]), the `child.send()` method can be @@ -766,7 +795,7 @@ For example, in the parent script: ```js const cp = require('child_process'); -const n = cp.fork(`${__dirname}/sub.js`); +const n = cp.fork(`${\_\_dirname}/sub.js`); n.on('message', (m) => { console.log('PARENT got message:', m); @@ -801,18 +830,22 @@ passing a TCP server or socket object to the child process. The child will receive the object as the second argument passed to the callback function registered on the `process.on('message')` event. -The optional `callback` is a function that is invoked after the message is -sent but before the child may have received it. The function is called with a -single argument: `null` on success, or an [`Error`][] object on failure. +The optional `callback` is a function that is invoked after the message is sent +but before the child may have received it. The function is called with a single +argument: `null` on success, or an [`Error`][] object on failure. + +If used with a callback, `child.send()` will return `false` if the channel has +closed or when the backlog of unsent messages exceeds a threshold that makes it +unwise to send more. Otherwise, the method returns `true`. The `callback` +function can be used to implement flow control. -If no `callback` function is provided and the message cannot be sent, an -`'error'` event will be emitted by the `ChildProcess` object. This can happen, -for instance, when the child process has already exited. +When no `callback` is provided, a [`Promise`][def-promise] will be returned. +This promise will resolve after the message has been sent but before the +child has received it. -`child.send()` will return `false` if the channel has closed or when the -backlog of unsent messages exceeds a threshold that makes it unwise to send -more. Otherwise, the method returns `true`. The `callback` function can be -used to implement flow control. +If no `callback` function is provided and the message cannot be sent, the +returned [`Promise`][def-promise] will reject with an Error object. This can +happen, for instance, when the child process has already exited. #### Example: sending a server object @@ -986,4 +1019,6 @@ to the same value. [`options.detached`]: #child_process_options_detached [`options.stdio`]: #child_process_options_stdio [`stdio`]: #child_process_options_stdio +[def-promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise +[def-string]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String [synchronous counterparts]: #child_process_synchronous_process_creation diff --git a/doc/api/crypto.markdown b/doc/api/crypto.markdown index 9d42cebb2a49f8..1d19311a86b5bb 100644 --- a/doc/api/crypto.markdown +++ b/doc/api/crypto.markdown @@ -907,7 +907,7 @@ The `key` is the raw key used by the `algorithm` and `iv` is an [initialization vector][]. Both arguments must be `'binary'` encoded strings or [buffers][]. -## crypto.createDiffieHellman(prime[, prime_encoding][, generator][, generator_encoding]) +### crypto.createDiffieHellman(prime[, prime_encoding][, generator][, generator_encoding]) Creates a `DiffieHellman` key exchange object using the supplied `prime` and an optional specific `generator`. @@ -1074,7 +1074,7 @@ const hashes = crypto.getHashes(); console.log(hashes); // ['sha', 'sha1', 'sha1WithRSAEncryption', ...] ``` -### crypto.pbkdf2(password, salt, iterations, keylen, digest, callback) +### crypto.pbkdf2(password, salt, iterations, keylen, digest[, callback]) Provides an asynchronous Password-Based Key Derivation Function 2 (PBKDF2) implementation. A selected HMAC digest algorithm specified by `digest` is @@ -1084,6 +1084,8 @@ applied to derive a key of the requested byte length (`keylen`) from the The supplied `callback` function is called with two arguments: `err` and `derivedKey`. If an error occurs, `err` will be set; otherwise `err` will be null. The successfully generated `derivedKey` will be passed as a [`Buffer`][]. +If `callback` is not given, a [`Promise`][def-promise] will be returned that +will resolve to a [`Buffer`][] on success or reject with `err`. The `iterations` argument must be a number set as high as possible. The higher the number of iterations, the more secure the derived key will be, @@ -1229,7 +1231,8 @@ const crypto = require('crypto'); crypto.randomBytes(256, (err, buf) => { if (err) throw err; console.log( - `${buf.length}` bytes of random data: ${buf.toString('hex')}); + `${buf.length} bytes of random data: ${buf.toString('hex')}` + ); }); ``` @@ -1241,7 +1244,8 @@ there is a problem generating the bytes. // Synchronous const buf = crypto.randomBytes(256); console.log( - `${buf.length}` bytes of random data: ${buf.toString('hex')}); + `${buf.length} bytes of random data: ${buf.toString('hex')}` +); ``` The `crypto.randomBytes()` method will block until there is sufficient entropy. @@ -1249,6 +1253,24 @@ This should normally never take longer than a few milliseconds. The only time when generating the random bytes may conceivably block for a longer period of time is right after boot, when the whole system is still low on entropy. +### crypto.randomBytesAsync(size) + +A [`Promise`][def-promise]-returning variant of [`crypto.randomBytes`][]. +The return value is always asynchronously generated, resolves to a `Buffer` +on success, or an `Error` object otherwise. + +Example: + +```js +crypto.randomBytesAsync(256).then((buf) => { + console.log( + `${buf.length} bytes of random data: ${buf.toString('hex')}` + ); +}).catch((err) => { + console.error('Something went wrong: ${err.stack}'); +}); +``` + ### crypto.setEngine(engine[, flags]) Load and set the `engine` for some or all OpenSSL functions (selected by flags). @@ -1333,6 +1355,7 @@ See the reference for other recommendations and details. [`crypto.createDiffieHellman()`]: #crypto_crypto_creatediffiehellman_prime_prime_encoding_generator_generator_encoding [`crypto.getHashes()`]: #crypto_crypto_gethashes [`crypto.pbkdf2`]: #crypto_crypto_pbkdf2_password_salt_iterations_keylen_digest_callback +[`crypto.randomBytes`]: #crypto_crypto_randombytes_size_callback [`decipher.update`]: #crypto_decipher_update_data_input_encoding_output_encoding [`diffieHellman.setPublicKey()`]: #crypto_diffiehellman_setpublickey_public_key_encoding [`EVP_BytesToKey`]: https://www.openssl.org/docs/crypto/EVP_BytesToKey.html @@ -1341,6 +1364,7 @@ See the reference for other recommendations and details. [`Buffer`]: buffer.html [buffers]: buffer.html [Caveats]: #crypto_support_for_weak_or_compromised_algorithms +[def-promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise [initialization vector]: https://en.wikipedia.org/wiki/Initialization_vector [NIST SP 800-131A]: http://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-131Ar1.pdf [NIST SP 800-132]: http://csrc.nist.gov/publications/nistpubs/800-132/nist-sp800-132.pdf diff --git a/doc/api/dgram.markdown b/doc/api/dgram.markdown index 8408321c384e07..d75463c38c9ac4 100644 --- a/doc/api/dgram.markdown +++ b/doc/api/dgram.markdown @@ -157,6 +157,9 @@ underlying socket handle allowing connection handling duties to be shared. When `exclusive` is `true`, however, the handle is not shared and attempted port sharing results in an error. +If `callback` is not given, a [`Promise`][def-promise] will be returned, which +will resolve to `undefined` once the socket emits a `'listening'` event. + An example socket listening on an exclusive port is shown below. ```js @@ -170,7 +173,9 @@ socket.bind({ ### socket.close([callback]) Close the underlying socket and stop listening for data on it. If a callback is -provided, it is added as a listener for the [`'close'`][] event. +provided, it is added as a listener for the [`'close'`][] event. If not provided, +a [`Promise`][def-promise] will be returned, resolving to `undefined` when the +socket is closed. ### socket.dropMembership(multicastAddress[, multicastInterface]) @@ -215,15 +220,9 @@ If the socket has not been previously bound with a call to `bind`, the socket is assigned a random port number and is bound to the "all interfaces" address (`'0.0.0.0'` for `udp4` sockets, `'::0'` for `udp6` sockets.) -An optional `callback` function may be specified to as a way of reporting -DNS errors or for determining when it is safe to reuse the `buf` object. -Note that DNS lookups delay the time to send for at least one tick of the -Node.js event loop. - -The only way to know for sure that the datagram has been sent is by using a -`callback`. If an error occurs and a `callback` is given, the error will be -passed as the first argument to the `callback`. If a `callback` is not given, -the error is emitted as an `'error'` event on the `socket` object. +The `callback` parameter is optional. If not provided, `.send()` will return +a [`Promise`][def-promise] resolving to `undefined` that will forward any +errors encountered during `send` as a rejection. Offset and length are optional, but if you specify one you would need to specify the other. Also, they are supported only when the first @@ -372,9 +371,9 @@ s.bind(1234, () => { ## `dgram` module functions -### dgram.createSocket(options[, callback]) +### dgram.createSocket(options[, onmessage]) * `options` Object -* `callback` Function. Attached as a listener to `'message'` events. +* `onmessage` Function. Attached as a listener to `'message'` events. * Returns: Socket object Creates a `dgram.Socket` object. The `options` argument is an object that @@ -393,10 +392,10 @@ interfaces" address on a random port (it does the right thing for both `udp4` and `udp6` sockets). The bound address and port can be retrieved using [`socket.address().address`][] and [`socket.address().port`][]. -## dgram.createSocket(type[, callback]) +### dgram.createSocket(type[, onmessage]) * `type` String. Either 'udp4' or 'udp6' -* `callback` Function. Attached as a listener to `'message'` events. +* `onmessage` Function. Attached as a listener to `'message'` events. Optional * Returns: Socket object @@ -423,3 +422,4 @@ and `udp6` sockets). The bound address and port can be retrieved using [`socket.address().port`]: #dgram_socket_address [`socket.bind()`]: #dgram_socket_bind_port_address_callback [byte length]: buffer.html#buffer_class_method_buffer_bytelength_string_encoding +[def-promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise diff --git a/doc/api/http.markdown b/doc/api/http.markdown index fc55777acf6ada..d3d45fb3ef696a 100644 --- a/doc/api/http.markdown +++ b/doc/api/http.markdown @@ -1046,6 +1046,20 @@ http.get('http://www.google.com/index.html', (res) => { }); ``` +## http.getAsync(options) + +A [`Promise`][def-promise]-returning version of [`http.get()`][]. Returns an +object containing a `Promise` resolving to a [`http.IncomingMessage`][] Parses +`options` the same as [`http.get()`][]. Request errors will reject the promise. + +Example: + +```js +http.getAsync('http://www.google.com/index.html').then((res) => { + res.pipe(process.stdout); +}); +``` + ## http.globalAgent Global instance of Agent which is used as the default for all http client @@ -1157,6 +1171,37 @@ There are a few special headers that should be noted. * Sending an Authorization header will override using the `auth` option to compute basic authentication. +## http.requestAsync(options) + +* Returns: {Object} with attributes: + * `request` - a [`http.ClientRequest`][]. + * `response` - a [`Promise`][def-promise] for a [`http.IncomingMessage`][]. + +A [`Promise`][def-promise]-returning version of [`http.request()`][]. Returns +an object containing a [`http.ClientRequest`][] and a `Promise` resolving to +a [`http.IncomingMessage`][] as `request` and `response`, respectively. Parses +`options` the same as [`http.request()`][]. + +Example: + +```js +const http = require('http'); +const url = require('url'); + +const pair = http.requestAsync(url.parse('http://localhost:8080/')) + +pair.request.end(); +pair.response.then((resp) => { + const accumulator = []; + resp.on('data', (chunk) => { + accumulator.push(chunk); + }); + resp.on('end', () => { + console.log(Buffer.concat(accumulator).toString('utf8')); + }); +}); +``` + [`'checkContinue'`]: #http_event_checkcontinue [`'listening'`]: net.html#net_event_listening [`'response'`]: #http_event_response diff --git a/doc/api/https.markdown b/doc/api/https.markdown index 39401de8c5135c..a19fd61baf565c 100644 --- a/doc/api/https.markdown +++ b/doc/api/https.markdown @@ -98,6 +98,30 @@ https.get('https://encrypted.google.com/', (res) => { }); ``` +## https.getAsync(options) + +Like [`http.getAsync()`][] but for HTTPS. If + +`options` can be an object or a string. If `options` is a string, it is +automatically parsed with [`url.parse()`][]. + +Example: + +```js +const https = require('https'); + +https.getAsync('https://encrypted.google.com/').then((res) => { + console.log('statusCode: ', res.statusCode); + console.log('headers: ', res.headers); + + res.on('data', (d) => { + process.stdout.write(d); + }); +}).catch((err) => { + console.error(err); +}); +``` + ## https.globalAgent Global instance of [`https.Agent`][] for all HTTPS client requests. @@ -227,6 +251,17 @@ var req = https.request(options, (res) => { } ``` +## https.requestAsync(options) + +* Returns: {Object} with attributes: + * `request` - a [`http.ClientRequest`][]. + * `response` - a [`Promise`][def-promise] for a [`http.IncomingMessage`][]. + +A [`Promise`][def-promise]-returning version of [`https.request()`][]. Returns +an object containing a [`http.ClientRequest`][] and a `Promise` resolving to +a [`http.IncomingMessage`][] as `request` and `response`, respectively. Parses +`options` the same as [`http.request()`][]. + [`Agent`]: #https_class_https_agent [`globalAgent`]: #https_https_globalagent [`http.Agent`]: http.html#http_class_http_agent diff --git a/doc/api/net.markdown b/doc/api/net.markdown index 1cd14b563fc439..f1bdf8de0f9820 100644 --- a/doc/api/net.markdown +++ b/doc/api/net.markdown @@ -492,7 +492,9 @@ If `timeout` is 0, then the existing idle timeout is disabled. The optional `callback` parameter will be added as a one time listener for the [`'timeout'`][] event. -Returns `socket`. +Returns `socket` if `callback` is provided, otherwise returns a +[`Promise`][def-promise] that resolves to `undefined` once the timeout has +fired. ### socket.unref() @@ -568,6 +570,11 @@ If `host` is omitted, `'localhost'` will be assumed. The `connectListener` parameter will be added as a listener for the [`'connect'`][] event once. +## net.connectAsync(options) + +Returns a [`Promise`][def-promise] for a new [`net.Socket`][] that resolves +when the socket has connected. + ## net.createConnection(options[, connectListener]) A factory function, which returns a new [`net.Socket`][] and automatically @@ -728,4 +735,5 @@ Returns true if input is a version 6 IP address, otherwise returns false. [`socket.connect`]: #net_socket_connect_options_connectlistener [`socket.setTimeout()`]: #net_socket_settimeout_timeout_callback [`stream.setEncoding()`]: stream.html#stream_readable_setencoding_encoding +[def-promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise [Readable Stream]: stream.html#stream_class_stream_readable diff --git a/doc/api/process.markdown b/doc/api/process.markdown index 4f739c0dc70567..27fe864671cbaf 100644 --- a/doc/api/process.markdown +++ b/doc/api/process.markdown @@ -908,6 +908,29 @@ need to be root or have the `CAP_SETGID` capability. The list can contain group IDs, group names or both. +## process.setPromiseImplementation(impl) + +Sets the [`Promise`][def-promises] implementation for the process. All promises +returned by core APIs will be resolved by this implementation, giving third +party promise implementations an opportunity to cast native promises into a +different type. + +**Only top-level applications should use this API.** If this API is called more +than once, subsequent calls will issue deprecation warnings so authors can +track down offending modules. + +```javascript +process.setPromiseImplementation(require('bluebird')); + +const fs = require('fs'); + +fs.readFile('/usr/share/dict/words', 'utf8').then((words) => { + console.log(`a picture is worth ${words.split('\n').length} words.`); +}, (err) => { + console.error('there are no words.'); +}); +``` + ## process.setuid(id) Note: this function is only available on POSIX platforms (i.e. not Windows, diff --git a/lib/_tls_wrap.js b/lib/_tls_wrap.js index dd293121cbc782..d4e0f9fc6f9f71 100644 --- a/lib/_tls_wrap.js +++ b/lib/_tls_wrap.js @@ -1000,8 +1000,11 @@ exports.connect = function(/* [port, host], options, cb */) { requestOCSP: options.requestOCSP }); - if (cb) - socket.once('secureConnect', cb); + if (cb) { + socket.once('secureConnect', () => { + cb.call(socket, null, socket); + }); + } if (!options.socket) { var connect_opt; diff --git a/lib/child_process.js b/lib/child_process.js index e682aed5aede07..60ec80158b836d 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -2,6 +2,7 @@ const util = require('util'); const internalUtil = require('internal/util'); +const promisify = require('internal/promisify'); const debug = util.debuglog('child_process'); const constants = require('constants'); @@ -102,6 +103,9 @@ exports.exec = function(command /*, options, callback*/) { }; +promisify.multiple(exports, 'exec', ['stdout', 'stderr']); + + exports.execFile = function(file /*, args, options, callback*/) { var args = [], callback; var options = { @@ -288,6 +292,10 @@ exports.execFile = function(file /*, args, options, callback*/) { return child; }; + +promisify.multiple(exports, 'execFile', ['stdout', 'stderr']); + + var _deprecatedCustomFds = internalUtil.deprecate(function(options) { options.stdio = options.customFds.map(function(fd) { return fd === -1 ? 'pipe' : fd; diff --git a/lib/cluster.js b/lib/cluster.js index c8bf658d5b32a8..1c62ab99fdf600 100644 --- a/lib/cluster.js +++ b/lib/cluster.js @@ -1,5 +1,6 @@ 'use strict'; +const promisify = require('internal/promisify'); const EventEmitter = require('events'); const assert = require('assert'); const dgram = require('dgram'); @@ -406,6 +407,7 @@ function masterInit() { } if (cb) intercom.once('disconnect', cb); }; + promisify.normal(cluster, 'disconnect'); Worker.prototype.disconnect = function() { this.suicide = true; diff --git a/lib/crypto.js b/lib/crypto.js index 4b0539406e5ac6..228f45eaf36fd9 100644 --- a/lib/crypto.js +++ b/lib/crypto.js @@ -21,6 +21,7 @@ const stream = require('stream'); const util = require('util'); const internalUtil = require('internal/util'); const LazyTransform = require('internal/streams/lazy_transform'); +const promisify = require('internal/promisify'); const DH_GENERATOR = 2; @@ -553,6 +554,7 @@ exports.pbkdf2 = function(password, return pbkdf2(password, salt, iterations, keylen, digest, callback); }; +promisify.normal(exports, 'pbkdf2'); exports.pbkdf2Sync = function(password, salt, iterations, keylen, digest) { @@ -628,9 +630,13 @@ exports.setEngine = function setEngine(id, flags) { }; exports.randomBytes = exports.pseudoRandomBytes = randomBytes; - exports.rng = exports.prng = randomBytes; +promisify.normal(exports, 'randomBytes'); +promisify.normal(exports, 'pseudoRandomBytes'); +promisify.normal(exports, 'rng'); +promisify.normal(exports, 'prng'); + exports.getCiphers = function() { return filterDuplicates(getCiphers()); }; diff --git a/lib/dgram.js b/lib/dgram.js index 279f59526974b9..c184471d21ec34 100644 --- a/lib/dgram.js +++ b/lib/dgram.js @@ -1,6 +1,7 @@ 'use strict'; const assert = require('assert'); +const promisify = require('internal/promisify'); const Buffer = require('buffer').Buffer; const util = require('util'); const EventEmitter = require('events'); @@ -224,6 +225,7 @@ Socket.prototype.bind = function(port_ /*, address, callback*/) { return self; }; +promisify.normal(Socket.prototype, 'bind'); // thin wrapper around `send`, here for compatibility with dgram_legacy.js @@ -241,6 +243,7 @@ Socket.prototype.sendto = function(buffer, this.send(buffer, offset, length, port, address, callback); }; +promisify.normal(Socket.prototype, 'sendto'); function sliceBuffer(buffer, offset, length) { @@ -340,7 +343,7 @@ Socket.prototype.send = function(buffer, doSend(ex, self, ip, buffer, address, port, callback); }); }; - +promisify.normal(Socket.prototype, 'send'); function doSend(ex, self, ip, buffer, address, port, callback) { if (ex) { @@ -396,6 +399,7 @@ Socket.prototype.close = function(callback) { return this; }; +promisify.normal(Socket.prototype, 'close'); function socketCloseNT(self) { diff --git a/lib/dns.js b/lib/dns.js index 6c9795384fbf51..24e866ed29fce7 100644 --- a/lib/dns.js +++ b/lib/dns.js @@ -2,6 +2,7 @@ const net = require('net'); const util = require('util'); +const promisify = require('internal/promisify'); const cares = process.binding('cares_wrap'); const uv = process.binding('uv'); @@ -171,6 +172,7 @@ exports.lookup = function lookup(hostname, options, callback) { callback.immediately = true; return req; }; +promisify.normal(exports, 'lookup'); function onlookupservice(err, host, service) { @@ -206,6 +208,7 @@ exports.lookupService = function(host, port, callback) { callback.immediately = true; return req; }; +promisify.normal(exports, 'lookupService'); function onresolve(err, result) { @@ -252,6 +255,17 @@ exports.resolveNaptr = resolveMap.NAPTR = resolver('queryNaptr'); exports.resolveSoa = resolveMap.SOA = resolver('querySoa'); exports.reverse = resolveMap.PTR = resolver('getHostByAddr'); +promisify.normal(exports, 'resolve4'); +promisify.normal(exports, 'resolve6'); +promisify.normal(exports, 'resolveCname'); +promisify.normal(exports, 'resolveMx'); +promisify.normal(exports, 'resolveNs'); +promisify.normal(exports, 'resolveTxt'); +promisify.normal(exports, 'resolveSrv'); +promisify.normal(exports, 'resolveNaptr'); +promisify.normal(exports, 'resolveSoa'); +promisify.normal(exports, 'reverse'); + exports.resolve = function(hostname, type_, callback_) { var resolver, callback; @@ -271,6 +285,7 @@ exports.resolve = function(hostname, type_, callback_) { throw new Error('Unknown type "' + type_ + '"'); } }; +promisify.normal(exports, 'resolve'); exports.getServers = function() { diff --git a/lib/domain.js b/lib/domain.js index 41b4859361d5f3..33f9f06049244b 100644 --- a/lib/domain.js +++ b/lib/domain.js @@ -7,6 +7,7 @@ const util = require('util'); const EventEmitter = require('events'); +const promise = require('internal/promise'); const inherits = util.inherits; // communicate with events module, but don't require that @@ -35,6 +36,13 @@ exports._stack = stack; // let the process know we're using domains const _domain_flag = process._setupDomainUse(_domain, stack); +promise.addDomainHook(function(func) { + if (process.domain) { + return process.domain.bind(func); + } + return func; +}); + exports.Domain = Domain; exports.create = exports.createDomain = function() { diff --git a/lib/fs.js b/lib/fs.js index af8001bb181698..88403ac1d5611f 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -5,6 +5,7 @@ const SlowBuffer = require('buffer').SlowBuffer; const util = require('util'); +const promisify = require('internal/promisify'); const pathModule = require('path'); const binding = process.binding('fs'); @@ -195,6 +196,7 @@ fs.access = function(path, mode, callback) { req.oncomplete = makeCallback(callback); binding.access(pathModule._makeLong(path), mode, req); }; +promisify.normal(fs, 'access'); fs.accessSync = function(path, mode) { nullCheck(path); @@ -216,6 +218,7 @@ fs.exists = function(path, callback) { if (callback) callback(err ? false : true); } }; +promisify.single(fs, 'exists'); fs.existsSync = function(path) { try { @@ -264,6 +267,7 @@ fs.readFile = function(path, options, callback_) { 0o666, req); }; +promisify.normal(fs, 'readFile'); const kReadFileBufferLength = 8 * 1024; @@ -548,6 +552,7 @@ fs.close = function(fd, callback) { req.oncomplete = makeCallback(callback); binding.close(fd, req); }; +promisify.normal(fs, 'close'); fs.closeSync = function(fd) { return binding.close(fd); @@ -577,6 +582,7 @@ fs.open = function(path, flags, mode, callback_) { mode, req); }; +promisify.normal(fs, 'open'); fs.openSync = function(path, flags, mode) { mode = modeNum(mode, 0o666); @@ -736,6 +742,7 @@ fs.rename = function(oldPath, newPath, callback) { pathModule._makeLong(newPath), req); }; +promisify.normal(fs, 'rename'); fs.renameSync = function(oldPath, newPath) { nullCheck(oldPath); @@ -799,6 +806,7 @@ fs.ftruncate = function(fd, len, callback) { req.oncomplete = makeCallback(callback); binding.ftruncate(fd, len, req); }; +promisify.normal(fs, 'ftruncate'); fs.ftruncateSync = function(fd, len) { if (len === undefined) { @@ -814,6 +822,7 @@ fs.rmdir = function(path, callback) { req.oncomplete = callback; binding.rmdir(pathModule._makeLong(path), req); }; +promisify.normal(fs, 'rmdir'); fs.rmdirSync = function(path) { nullCheck(path); @@ -825,6 +834,7 @@ fs.fdatasync = function(fd, callback) { req.oncomplete = makeCallback(callback); binding.fdatasync(fd, req); }; +promisify.normal(fs, 'fdatasync'); fs.fdatasyncSync = function(fd) { return binding.fdatasync(fd); @@ -835,6 +845,7 @@ fs.fsync = function(fd, callback) { req.oncomplete = makeCallback(callback); binding.fsync(fd, req); }; +promisify.normal(fs, 'fsync'); fs.fsyncSync = function(fd) { return binding.fsync(fd); @@ -850,6 +861,7 @@ fs.mkdir = function(path, mode, callback) { modeNum(mode, 0o777), req); }; +promisify.normal(fs, 'mkdir'); fs.mkdirSync = function(path, mode) { nullCheck(path); @@ -864,6 +876,7 @@ fs.readdir = function(path, callback) { req.oncomplete = callback; binding.readdir(pathModule._makeLong(path), req); }; +promisify.normal(fs, 'readdir'); fs.readdirSync = function(path) { nullCheck(path); @@ -875,6 +888,7 @@ fs.fstat = function(fd, callback) { req.oncomplete = makeCallback(callback); binding.fstat(fd, req); }; +promisify.normal(fs, 'fstat'); fs.lstat = function(path, callback) { callback = makeCallback(callback); @@ -883,6 +897,7 @@ fs.lstat = function(path, callback) { req.oncomplete = callback; binding.lstat(pathModule._makeLong(path), req); }; +promisify.normal(fs, 'lstat'); fs.stat = function(path, callback) { callback = makeCallback(callback); @@ -891,6 +906,7 @@ fs.stat = function(path, callback) { req.oncomplete = callback; binding.stat(pathModule._makeLong(path), req); }; +promisify.normal(fs, 'stat'); fs.fstatSync = function(fd) { return binding.fstat(fd); @@ -913,6 +929,7 @@ fs.readlink = function(path, callback) { req.oncomplete = callback; binding.readlink(pathModule._makeLong(path), req); }; +promisify.normal(fs, 'readlink'); fs.readlinkSync = function(path) { nullCheck(path); @@ -949,6 +966,7 @@ fs.symlink = function(target, path, type_, callback_) { type, req); }; +promisify.normal(fs, 'symlink'); fs.symlinkSync = function(target, path, type) { type = (typeof type === 'string' ? type : null); @@ -973,6 +991,7 @@ fs.link = function(srcpath, dstpath, callback) { pathModule._makeLong(dstpath), req); }; +promisify.normal(fs, 'link'); fs.linkSync = function(srcpath, dstpath) { nullCheck(srcpath); @@ -988,6 +1007,7 @@ fs.unlink = function(path, callback) { req.oncomplete = callback; binding.unlink(pathModule._makeLong(path), req); }; +promisify.normal(fs, 'unlink'); fs.unlinkSync = function(path) { nullCheck(path); @@ -999,6 +1019,7 @@ fs.fchmod = function(fd, mode, callback) { req.oncomplete = makeCallback(callback); binding.fchmod(fd, modeNum(mode), req); }; +promisify.normal(fs, 'fchmod'); fs.fchmodSync = function(fd, mode) { return binding.fchmod(fd, modeNum(mode)); @@ -1021,6 +1042,7 @@ if (constants.hasOwnProperty('O_SYMLINK')) { }); }); }; + promisify.normal(fs, 'lchmod'); fs.lchmodSync = function(path, mode) { var fd = fs.openSync(path, constants.O_WRONLY | constants.O_SYMLINK); @@ -1053,6 +1075,7 @@ fs.chmod = function(path, mode, callback) { modeNum(mode), req); }; +promisify.normal(fs, 'chmod'); fs.chmodSync = function(path, mode) { nullCheck(path); @@ -1070,6 +1093,7 @@ if (constants.hasOwnProperty('O_SYMLINK')) { fs.fchown(fd, uid, gid, callback); }); }; + promisify.normal(fs, 'lchown'); fs.lchownSync = function(path, uid, gid) { var fd = fs.openSync(path, constants.O_WRONLY | constants.O_SYMLINK); @@ -1082,6 +1106,7 @@ fs.fchown = function(fd, uid, gid, callback) { req.oncomplete = makeCallback(callback); binding.fchown(fd, uid, gid, req); }; +promisify.normal(fs, 'fchown'); fs.fchownSync = function(fd, uid, gid) { return binding.fchown(fd, uid, gid); @@ -1094,6 +1119,7 @@ fs.chown = function(path, uid, gid, callback) { req.oncomplete = callback; binding.chown(pathModule._makeLong(path), uid, gid, req); }; +promisify.normal(fs, 'chown'); fs.chownSync = function(path, uid, gid) { nullCheck(path); @@ -1131,6 +1157,7 @@ fs.utimes = function(path, atime, mtime, callback) { toUnixTimestamp(mtime), req); }; +promisify.normal(fs, 'utimes'); fs.utimesSync = function(path, atime, mtime) { nullCheck(path); @@ -1146,6 +1173,7 @@ fs.futimes = function(fd, atime, mtime, callback) { req.oncomplete = makeCallback(callback); binding.futimes(fd, atime, mtime, req); }; +promisify.normal(fs, 'futimes'); fs.futimesSync = function(fd, atime, mtime) { atime = toUnixTimestamp(atime); @@ -1221,6 +1249,7 @@ fs.writeFile = function(path, data, options, callback_) { writeAll(fd, isUserFd, buffer, 0, buffer.length, position, callback); } }; +promisify.normal(fs, 'writeFile'); fs.writeFileSync = function(path, data, options) { if (!options) { @@ -1277,6 +1306,7 @@ fs.appendFile = function(path, data, options, callback_) { fs.writeFile(path, data, options, callback); }; +promisify.normal(fs, 'appendFile'); fs.appendFileSync = function(path, data, options) { if (!options) { @@ -1691,6 +1721,7 @@ fs.realpath = function realpath(p, cache, cb) { start(); } }; +promisify.normal(fs, 'realpath'); var pool; diff --git a/lib/https.js b/lib/https.js index 7008a79131c663..bcb0057ea5d63b 100644 --- a/lib/https.js +++ b/lib/https.js @@ -183,7 +183,7 @@ exports.request = function(options, cb) { }; exports.get = function(options, cb) { - var req = exports.request(options, cb); + const req = exports.request(options, cb); req.end(); return req; }; diff --git a/lib/internal/callbackify.js b/lib/internal/callbackify.js new file mode 100644 index 00000000000000..3da4cc923579e7 --- /dev/null +++ b/lib/internal/callbackify.js @@ -0,0 +1,24 @@ +'use strict'; + +module.exports = callbackify; + +function callbackify(fn) { + Object.defineProperty(inner, 'name', { + value: fn.name + 'Callback' + }); + return inner; + + function inner() { + const cb = arguments[arguments.length - 1]; + if (typeof cb !== 'function') { + cb = (err) => { throw err; }; + } + + new Promise((resolve) => { + resolve(fn.apply(this, arguments)); + }).then( + (result) => cb(null, result), + (error) => cb(error) + ); + } +} diff --git a/lib/internal/child_process.js b/lib/internal/child_process.js index 6487de2e9efa08..2a7a19fc4bab5b 100644 --- a/lib/internal/child_process.js +++ b/lib/internal/child_process.js @@ -1,5 +1,6 @@ 'use strict'; +const promisify = require('internal/promisify'); const StringDecoder = require('string_decoder').StringDecoder; const Buffer = require('buffer').Buffer; const EventEmitter = require('events'); @@ -503,6 +504,10 @@ function setupChannel(target, channel) { callback = handle; handle = undefined; } + if (typeof message === 'function') { + callback = message; + message = undefined; + } if (this.connected) { return this._send(message, handle, false, callback); } @@ -514,6 +519,7 @@ function setupChannel(target, channel) { } return false; }; + promisify.normal(target, 'send'); target._send = function(message, handle, swallowErrors, callback) { assert(this.connected || this._channel); diff --git a/lib/internal/promise.js b/lib/internal/promise.js new file mode 100644 index 00000000000000..a294471f1f8ab3 --- /dev/null +++ b/lib/internal/promise.js @@ -0,0 +1,45 @@ +'use strict'; + +// This module patches the global Promise before any other code has access to +// it. It does this so that promises can be instrumented with domains as +// expected. It is a temporary API - once V8 offers a way for us to interact +// with the MicrotaskQueue it can fall off. +module.exports = { + addDomainHook +}; + +const originalThen = Promise.prototype.then; +Promise.prototype.then = function(success, failure) { + var promise; + switch (arguments.length) { + case 2: + if (typeof success === 'function') { + success = domainWrapHook(success); + } + if (typeof failure === 'function') { + failure = domainWrapHook(failure); + } + promise = originalThen.call(this, success, failure); + break; + case 1: + if (typeof success === 'function') { + success = domainWrapHook(success); + } + promise = originalThen.call(this, success); + break; + case 0: + promise = originalThen.call(this); + break; + } + return promise; +}; + +var domainWrapHook = _noopDomainWrap; + +function addDomainHook(hook) { + domainWrapHook = hook; +} + +function _noopDomainWrap(func) { + return func; +} diff --git a/lib/internal/promisify.js b/lib/internal/promisify.js new file mode 100644 index 00000000000000..6dfad224a236c4 --- /dev/null +++ b/lib/internal/promisify.js @@ -0,0 +1,90 @@ +'use strict'; + +const SUFFIX = 'Promise'; +const SHORTCUT_NAME = 'promised'; +const install = process._enablePromises ? _install : _noop; + +module.exports = { + normal, + single, + multiple +}; + +function normal(object, name) { + return _promisify(object, name, (lastArg, err, val) => { + if (err) { + if (lastArg && + typeof lastArg === 'object' && + err.code && + typeof lastArg[err.code] === 'function') { + return lastArg[err.code]() + } + throw err; + } + return val; + }); +} + +function single(object, name) { + return _promisify(object, name, (lastArg, val) => { + return val; + }); +} + +function multiple(object, name, mapped) { + return _promisify(object, name, (lastArg, err, arg0, arg1) => { + if (err) { + if (lastArg && + typeof lastArg === 'object' && + err.code && + typeof lastArg[err.code] === 'function') { + return lastArg[err.code]() + } + throw err; + } + return { + [mapped[0]]: arg0, + [mapped[1]]: arg1 + }; + }); +} + +function _promisify(object, name, handler) { + const fn = install(object, name, inner); + + function inner() { + const args = Array.from(arguments); + const lastArg = args[args.length - 1]; + return new Promise((resolve, reject) => { + fn.call(this, ...args, resolver); + + function resolver(arg0, arg1, arg2) { + try { + resolve(handler(lastArg, arg0, arg1, arg2)); + } catch (err) { + reject(err); + } + } + }); + } +} + +function _install(object, name, inner) { + const fn = object[name]; + Object.defineProperty(inner, 'name', { + value: (fn.name || name) + 'Promisified' + }); + Object.defineProperty(object, `${name}${SUFFIX}`, { + enumerable: false, + value: inner + }); + + if (!object[SHORTCUT_NAME]) { + object[SHORTCUT_NAME] = {}; + } + object[SHORTCUT_NAME][name] = inner; + return fn; +} + +function _noop(object, name, inner) { +} diff --git a/lib/net.js b/lib/net.js index fca70fb51fa054..39f15227d3b5a6 100644 --- a/lib/net.js +++ b/lib/net.js @@ -5,6 +5,7 @@ const stream = require('stream'); const timers = require('timers'); const util = require('util'); const internalUtil = require('internal/util'); +const promisify = require('internal/promisify'); const internalNet = require('internal/net'); const assert = require('assert'); const cares = process.binding('cares_wrap'); @@ -299,6 +300,10 @@ Socket.prototype.listen = function() { Socket.prototype.setTimeout = function(msecs, callback) { + if (typeof msecs === 'function') { + callback = msecs; + msecs = 0; + } if (msecs === 0) { timers.unenroll(this); if (callback) { @@ -313,6 +318,7 @@ Socket.prototype.setTimeout = function(msecs, callback) { } return this; }; +promisify.normal(Socket.prototype, 'setTimeout'); Socket.prototype._onTimeout = function() { @@ -874,7 +880,6 @@ Socket.prototype.connect = function(options, cb) { this._sockname = null; } - var self = this; var pipe = !!options.path; debug('pipe', pipe, options.path); @@ -884,21 +889,22 @@ Socket.prototype.connect = function(options, cb) { } if (typeof cb === 'function') { - self.once('connect', cb); + this.once('connect', () => { + return cb.call(this, null, this); + }); } this._unrefTimer(); - self._connecting = true; - self.writable = true; + this._connecting = true; + this.writable = true; if (pipe) { - connect(self, options.path); - + connect(this, options.path); } else { - lookupAndConnect(self, options); + lookupAndConnect(this, options); } - return self; + return this; }; diff --git a/lib/readline.js b/lib/readline.js index 065cc62807c689..b4f2c561b746a9 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -11,6 +11,8 @@ const kHistorySize = 30; const util = require('util'); const debug = util.debuglog('readline'); const internalUtil = require('internal/util'); +const callbackify = require('internal/callbackify'); +const promisify = require('internal/promisify'); const inherits = util.inherits; const Buffer = require('buffer').Buffer; const EventEmitter = require('events'); @@ -79,9 +81,9 @@ function Interface(input, output, completer, terminal) { // Check arity, 2 - for async, 1 for sync if (typeof completer === 'function') { - this.completer = completer.length === 2 ? completer : function(v, cb) { - cb(null, completer(v)); - }; + this.completer = completer.length === 2 + ? completer + : callbackify(completer); } this.setPrompt('> '); @@ -204,6 +206,7 @@ Interface.prototype.question = function(query, cb) { } } }; +promisify.normal(Interface.prototype, 'question'); Interface.prototype._onLine = function(line) { diff --git a/lib/repl.js b/lib/repl.js index f80555dba41afc..e78ef9174ba810 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -22,6 +22,7 @@ 'use strict'; const internalModule = require('internal/module'); +const promisify = require('internal/promisify'); const internalUtil = require('internal/util'); const util = require('util'); const inherits = util.inherits; @@ -885,6 +886,7 @@ REPLServer.prototype.complete = function(line, callback) { callback(null, [completions || [], completeOn]); } }; +promisify.normal(REPLServer.prototype, 'complete'); /** diff --git a/lib/zlib.js b/lib/zlib.js index 3e6f7b187632f2..64674d65bff1cf 100644 --- a/lib/zlib.js +++ b/lib/zlib.js @@ -8,6 +8,7 @@ const assert = require('assert').ok; const kMaxLength = require('buffer').kMaxLength; const kRangeErrorMessage = 'Cannot create final Buffer. ' + 'It would be larger than 0x' + kMaxLength.toString(16) + ' bytes'; +const promisify = require('internal/promisify'); // zlib doesn't provide these, so kludge them in following the same // const naming scheme zlib uses. @@ -110,6 +111,7 @@ exports.deflate = function(buffer, opts, callback) { } return zlibBuffer(new Deflate(opts), buffer, callback); }; +promisify.normal(exports, 'deflate'); exports.deflateSync = function(buffer, opts) { return zlibBufferSync(new Deflate(opts), buffer); @@ -122,6 +124,7 @@ exports.gzip = function(buffer, opts, callback) { } return zlibBuffer(new Gzip(opts), buffer, callback); }; +promisify.normal(exports, 'gzip'); exports.gzipSync = function(buffer, opts) { return zlibBufferSync(new Gzip(opts), buffer); @@ -134,6 +137,7 @@ exports.deflateRaw = function(buffer, opts, callback) { } return zlibBuffer(new DeflateRaw(opts), buffer, callback); }; +promisify.normal(exports, 'deflateRaw'); exports.deflateRawSync = function(buffer, opts) { return zlibBufferSync(new DeflateRaw(opts), buffer); @@ -146,6 +150,7 @@ exports.unzip = function(buffer, opts, callback) { } return zlibBuffer(new Unzip(opts), buffer, callback); }; +promisify.normal(exports, 'unzip'); exports.unzipSync = function(buffer, opts) { return zlibBufferSync(new Unzip(opts), buffer); @@ -158,6 +163,7 @@ exports.inflate = function(buffer, opts, callback) { } return zlibBuffer(new Inflate(opts), buffer, callback); }; +promisify.normal(exports, 'inflate'); exports.inflateSync = function(buffer, opts) { return zlibBufferSync(new Inflate(opts), buffer); @@ -170,6 +176,7 @@ exports.gunzip = function(buffer, opts, callback) { } return zlibBuffer(new Gunzip(opts), buffer, callback); }; +promisify.normal(exports, 'gunzip'); exports.gunzipSync = function(buffer, opts) { return zlibBufferSync(new Gunzip(opts), buffer); @@ -182,6 +189,7 @@ exports.inflateRaw = function(buffer, opts, callback) { } return zlibBuffer(new InflateRaw(opts), buffer, callback); }; +promisify.normal(exports, 'inflateRaw'); exports.inflateRawSync = function(buffer, opts) { return zlibBufferSync(new InflateRaw(opts), buffer); diff --git a/node.gyp b/node.gyp index a65f76e4ce68db..9c0243e674bbd6 100644 --- a/node.gyp +++ b/node.gyp @@ -69,10 +69,13 @@ 'lib/v8.js', 'lib/vm.js', 'lib/zlib.js', + 'lib/internal/callbackify.js', 'lib/internal/child_process.js', 'lib/internal/cluster.js', 'lib/internal/freelist.js', 'lib/internal/linkedlist.js', + 'lib/internal/promise.js', + 'lib/internal/promisify.js', 'lib/internal/net.js', 'lib/internal/module.js', 'lib/internal/readline.js', diff --git a/src/node.cc b/src/node.cc index 29127fbfc620ca..e2caedea1bf40a 100644 --- a/src/node.cc +++ b/src/node.cc @@ -131,6 +131,7 @@ using v8::Uint32Array; using v8::V8; using v8::Value; +static bool enable_promises = false; static bool print_eval = false; static bool force_repl = false; static bool syntax_check_only = false; @@ -2988,6 +2989,11 @@ void SetupProcessObject(Environment* env, READONLY_PROPERTY(process, "_forceRepl", True(env->isolate())); } + // --enable-promises + if (enable_promises) { + READONLY_PROPERTY(process, "_enablePromises", True(env->isolate())); + } + if (preload_module_count) { CHECK(preload_modules); Local array = Array::New(env->isolate()); @@ -3271,6 +3277,8 @@ static void PrintHelp() { " present.\n" #endif #endif + " --enable-promises EXPERIMENTAL: enable promisified API\n" + " (see nodejs/node#5020)\n" "\n" "Environment variables:\n" #ifdef _WIN32 @@ -3378,6 +3386,8 @@ static void ParseArgs(int* argc, } args_consumed += 1; local_preload_modules[preload_module_count++] = module; + } else if (strcmp(arg, "--enable-promises") == 0) { + enable_promises = true; } else if (strcmp(arg, "--check") == 0 || strcmp(arg, "-c") == 0) { syntax_check_only = true; } else if (strcmp(arg, "--interactive") == 0 || strcmp(arg, "-i") == 0) { diff --git a/src/node.js b/src/node.js index 1f29386a87714f..8728145e3a1998 100644 --- a/src/node.js +++ b/src/node.js @@ -44,6 +44,7 @@ if (process.argv[1] !== '--debug-agent') startup.processChannel(); + NativeModule.require('internal/promise'); startup.processRawDebug(); process.argv[0] = process.execPath; diff --git a/test/parallel/test-async-wrap-check-providers.js b/test/parallel/test-async-wrap-check-providers.js index 38302be2cd0b5d..5f15f28df9b5c1 100644 --- a/test/parallel/test-async-wrap-check-providers.js +++ b/test/parallel/test-async-wrap-check-providers.js @@ -1,4 +1,5 @@ 'use strict'; +// Flags: --expose-internals const common = require('../common'); const assert = require('assert'); diff --git a/test/parallel/test-net-socket-timeout.js b/test/parallel/test-net-socket-timeout.js index 5ab11909b4c306..352a7a189060e6 100644 --- a/test/parallel/test-net-socket-timeout.js +++ b/test/parallel/test-net-socket-timeout.js @@ -6,7 +6,7 @@ var assert = require('assert'); // Verify that invalid delays throw var noop = function() {}; var s = new net.Socket(); -var nonNumericDelays = ['100', true, false, undefined, null, '', {}, noop, []]; +var nonNumericDelays = ['100', true, false, undefined, null, '', {}, []]; var badRangeDelays = [-0.001, -1, -Infinity, Infinity, NaN]; var validDelays = [0, 0.001, 1, 1e6]; diff --git a/test/parallel/test-promise-domain-interaction.js b/test/parallel/test-promise-domain-interaction.js new file mode 100644 index 00000000000000..83c050a9cdd6c0 --- /dev/null +++ b/test/parallel/test-promise-domain-interaction.js @@ -0,0 +1,40 @@ +'use strict'; + +require('../common'); +const domain = require('domain'); +const assert = require('assert'); +const fs = require('fs'); +const d = domain.create(); + +const acc = []; + +d.on('error', function(err) { + acc.push(err.message); +}); +d.run(function() { + const p = fs.openAsync(__filename, 'r'); + + p.then(function(fd) { + fs.readFile$(fd, function() { + throw new Error('one'); + }); + return fd; + }, function() { + setTimeout(function() { + throw new Error('should not reach this point.'); + }); + }).then(function(fd) { + setTimeout(function() { + throw new Error('two'); + }); + return fd; + }).then(function(fd) { + return fs.closeAsync(fd); + }); +}); + +process.on('exit', function() { + assert.ok(acc.indexOf('one') !== -1); + assert.ok(acc.indexOf('two') !== -1); + assert.equal(acc.length, 2); +});