-
-
Notifications
You must be signed in to change notification settings - Fork 36.2k
net: add experimental net/promises API #63965
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -452,6 +452,48 @@ changes: | |
| Calls [`server.close()`][] and returns a promise that fulfills when the | ||
| server has closed. | ||
|
|
||
| ### `server[Symbol.asyncIterator]()` | ||
|
|
||
| <!-- YAML | ||
| added: REPLACEME | ||
| --> | ||
|
|
||
| > Stability: 1 - Experimental | ||
|
|
||
| * Returns: {AsyncIterator} An async iterator that yields each incoming | ||
| [`net.Socket`][]. | ||
|
|
||
| Returns an async iterator over the server's incoming connections, allowing them | ||
| to be consumed with `for await...of` as an alternative to the [`'connection'`][] | ||
| event. Iteration ends when the server emits [`'close'`][], and rejects if the | ||
| server emits [`'error'`][]. | ||
|
|
||
| The loop only advances to the next connection once the current iteration's body | ||
| has finished awaiting, so connection handling should be dispatched to a separate | ||
| async task rather than awaited inline. Otherwise connections are serialized: | ||
| each one waits for the previous to be fully handled. | ||
|
|
||
| ```mjs | ||
| import { createServer } from 'node:net'; | ||
|
|
||
| const server = createServer().listen(8124); | ||
|
|
||
| async function handleConnection(socket) { | ||
| // ...handle the connection, awaiting as needed. | ||
| socket.end('hello world!'); | ||
| } | ||
|
|
||
| for await (const socket of server) { | ||
| // Dispatch handling to a separate task so the loop keeps accepting | ||
| // connections instead of serializing them. | ||
| handleConnection(socket); | ||
| } | ||
| ``` | ||
|
|
||
| The server does not stop accepting connections while the loop body runs, so a | ||
| consumer slower than the connection rate can buffer them without bound. Use | ||
| [`server.maxConnections`][] to bound concurrency. | ||
|
|
||
| ### `server.getConnections(callback)` | ||
|
|
||
| <!-- YAML | ||
|
|
@@ -2241,6 +2283,82 @@ net.isIPv6('::1'); // returns true | |
| net.isIPv6('fhqwhgads'); // returns false | ||
| ``` | ||
|
|
||
| ## `net/promises` API | ||
|
|
||
| <!-- YAML | ||
| added: REPLACEME | ||
| --> | ||
|
|
||
| > Stability: 1 - Experimental | ||
|
|
||
| The `net/promises` API provides a set of `net` functions that return `Promise` | ||
| objects rather than relying on events. The API is accessible via | ||
| `require('node:net').promises` or `require('node:net/promises')`. | ||
|
|
||
| ### `netPromises.connect(options)` | ||
|
|
||
| ### `netPromises.connect(path)` | ||
|
|
||
| ### `netPromises.connect(port[, host])` | ||
|
|
||
| <!-- YAML | ||
| added: REPLACEME | ||
| --> | ||
|
|
||
| * `options` {Object} Accepts the same arguments as [`net.connect()`][]. May | ||
| include a `signal` {AbortSignal} that can be used to abort an in-progress | ||
| connection attempt. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know this is a pattern we use elsewhere but I'd much prefer just duplicating the options list rather than referencing it.. unless the reference is to a shared location like a |
||
| * Returns: {Promise} Fulfills with a connected [`net.Socket`][]. | ||
|
|
||
| A promise-based alternative to [`net.connect()`][]. The returned promise is | ||
| fulfilled with the socket once its [`'connect'`][] event fires, and is rejected | ||
| if the connection fails or the `signal` is aborted. When the promise rejects, | ||
| the underlying socket is destroyed. | ||
|
|
||
| This API is named for the action it performs and awaits — connecting — to | ||
| parallel [`netPromises.listen()`][]. It is not named `createConnection()`, | ||
| because that name belongs to the socket-factory taxonomy of the callback API, | ||
| which has no counterpart here. | ||
|
|
||
| ```mjs | ||
| import { connect } from 'node:net/promises'; | ||
|
|
||
| const socket = await connect({ port: 8124 }); | ||
| socket.write('hello world!'); | ||
| socket.end(); | ||
| ``` | ||
|
|
||
| ### `netPromises.listen([options])` | ||
|
|
||
| <!-- YAML | ||
| added: REPLACEME | ||
| --> | ||
|
|
||
| * `options` {Object} Accepts the same options as [`net.createServer()`][] and | ||
| [`server.listen()`][], plus: | ||
| * `connectionListener` {Function} Automatically set as a listener for the | ||
| [`'connection'`][] event. | ||
| * `signal` {AbortSignal} An `AbortSignal` that may be used to abort the | ||
| server. Aborting before the server is listening rejects the returned | ||
| promise with an `AbortError`; aborting at any later point closes the | ||
| server, matching the `signal` option of [`server.listen()`][]. | ||
| * Returns: {Promise} Fulfills with a listening [`net.Server`][]. | ||
|
|
||
| Creates a [`net.Server`][] and begins listening. The returned promise is | ||
| fulfilled with the server once its [`'listening'`][] event fires, and is | ||
| rejected if the server fails to bind or the `signal` is aborted before it is | ||
| listening. When the promise rejects, the server is closed. | ||
|
|
||
| The resolved server is async iterable, so incoming connections can be consumed | ||
| with `for await...of` (see `server[Symbol.asyncIterator]()`). | ||
|
|
||
| ```mjs | ||
| import { listen } from 'node:net/promises'; | ||
|
|
||
| const server = await listen({ port: 8124 }); | ||
| console.log('listening on', server.address().port); | ||
| ``` | ||
|
|
||
| [IPC]: #ipc-support | ||
| [Identifying paths for IPC connections]: #identifying-paths-for-ipc-connections | ||
| [RFC 8305]: https://www.rfc-editor.org/rfc/rfc8305.txt | ||
|
|
@@ -2274,6 +2392,7 @@ net.isIPv6('fhqwhgads'); // returns false | |
| [`net.createServer()`]: #netcreateserveroptions-connectionlistener | ||
| [`net.getDefaultAutoSelectFamily()`]: #netgetdefaultautoselectfamily | ||
| [`net.getDefaultAutoSelectFamilyAttemptTimeout()`]: #netgetdefaultautoselectfamilyattempttimeout | ||
| [`netPromises.listen()`]: #netpromiseslistenoptions | ||
| [`new net.Socket(options)`]: #new-netsocketoptions | ||
| [`readable.setEncoding()`]: stream.md#readablesetencodingencoding | ||
| [`server.address()`]: #serveraddress | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| 'use strict'; | ||
|
|
||
| const { once } = require('events'); | ||
| const { | ||
| validateAbortSignal, | ||
| validateObject, | ||
| } = require('internal/validators'); | ||
| const { kEmptyObject } = require('internal/util'); | ||
|
|
||
| // Lazily loaded to avoid a require cycle with the `net` module, which exposes | ||
| // this namespace through its `promises` getter. | ||
| let net; | ||
| function lazyNet() { | ||
| net ??= require('net'); | ||
| return net; | ||
| } | ||
|
|
||
| // Resolves with a connected `net.Socket` once the `'connect'` event fires, and | ||
| // rejects if the connection fails or the optional `signal` is aborted. | ||
| async function connect(...args) { | ||
| const lazy = lazyNet(); | ||
| const options = lazy._normalizeArgs(args)[0]; | ||
| const { signal } = options; | ||
| if (signal !== undefined) { | ||
| validateAbortSignal(signal, 'options.signal'); | ||
| signal.throwIfAborted(); | ||
| } | ||
|
|
||
| // Strip the signal so the socket does not also install its own abort | ||
| // handling; rejecting and destroying below fully tears the socket down. | ||
| const socket = lazy.connect({ ...options, signal: undefined }); | ||
|
|
||
| try { | ||
| await once(socket, 'connect', signal !== undefined ? { signal } : kEmptyObject); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No I believe this correctly rejects. The |
||
| } catch (err) { | ||
| socket.destroy(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should |
||
| throw err; | ||
| } | ||
| return socket; | ||
| } | ||
|
|
||
| // Creates a server and resolves with it once it is listening, rejecting if it | ||
| // fails to bind or the optional `signal` is aborted. | ||
| async function listen(options = kEmptyObject) { | ||
| validateObject(options, 'options'); | ||
| const { signal, connectionListener } = options; | ||
| if (signal !== undefined) { | ||
| validateAbortSignal(signal, 'options.signal'); | ||
| signal.throwIfAborted(); | ||
| } | ||
|
|
||
| const lazy = lazyNet(); | ||
| const server = lazy.createServer(options, connectionListener); | ||
|
|
||
| try { | ||
| // Pass `signal` through to listen() so net installs its own | ||
| // close-on-abort handler: the signal aborts the server for its entire | ||
| // lifetime, not just the pending listen. | ||
| server.listen(options); | ||
| await once(server, 'listening', signal !== undefined ? { signal } : kEmptyObject); | ||
| } catch (err) { | ||
| // On abort, net's signal handler already closes the server, so closing | ||
| // again would be redundant; on other failures (e.g. a bind error) there | ||
| // is no such handler, so close it here. | ||
| if (!signal?.aborted) { | ||
| server.close(); | ||
| } | ||
| throw err; | ||
|
Comment on lines
+65
to
+68
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit... feel free to ignore but I think this reads better if using throwIfAborted signal.throwIfAborted();
server.close();? |
||
| } | ||
| return server; | ||
| } | ||
|
|
||
| module.exports = { | ||
| connect, | ||
| listen, | ||
| get isIP() { return lazyNet().isIP; }, | ||
| get isIPv4() { return lazyNet().isIPv4; }, | ||
| get isIPv6() { return lazyNet().isIPv6; }, | ||
| get BlockList() { return lazyNet().BlockList; }, | ||
| get SocketAddress() { return lazyNet().SocketAddress; }, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| 'use strict'; | ||
|
|
||
| module.exports = require('internal/net/promises'); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| 'use strict'; | ||
| const common = require('../common'); | ||
| const assert = require('assert'); | ||
| const net = require('net'); | ||
| const { once } = require('events'); | ||
| const { connect } = require('net/promises'); | ||
|
|
||
| (async () => { | ||
| // Resolves with a connected socket and round-trips data. | ||
| { | ||
| const server = net.createServer((socket) => { | ||
| socket.end('hello'); | ||
| }).listen(0); | ||
| await once(server, 'listening'); | ||
| const socket = await connect({ port: server.address().port }); | ||
| assert.strictEqual(socket.connecting, false); | ||
| const chunks = []; | ||
| for await (const chunk of socket) { | ||
| chunks.push(chunk); | ||
| } | ||
| assert.strictEqual(Buffer.concat(chunks).toString(), 'hello'); | ||
| server.close(); | ||
| } | ||
|
|
||
| // net.promises is the same object as require('net/promises'). | ||
| assert.strictEqual(net.promises, require('net/promises')); | ||
|
|
||
| // Rejects when the connection is refused. | ||
| { | ||
| const server = net.createServer().listen(0); | ||
| await once(server, 'listening'); | ||
| const { port } = server.address(); | ||
| server.close(); | ||
| await once(server, 'close'); | ||
| await assert.rejects(connect({ port }), { code: 'ECONNREFUSED' }); | ||
| } | ||
|
|
||
| // A pre-aborted signal rejects with an AbortError. | ||
| { | ||
| await assert.rejects( | ||
| connect({ port: 0, signal: AbortSignal.abort() }), | ||
| { name: 'AbortError' }); | ||
| } | ||
|
|
||
| // Aborting while connecting rejects with an AbortError. | ||
| { | ||
| const server = net.createServer().listen(0); | ||
| await once(server, 'listening'); | ||
| const controller = new AbortController(); | ||
| const promise = connect({ port: server.address().port, signal: controller.signal }); | ||
| controller.abort(); | ||
| await assert.rejects(promise, { name: 'AbortError' }); | ||
| server.close(); | ||
| } | ||
|
|
||
| // An invalid signal throws. | ||
| { | ||
| await assert.rejects( | ||
| connect({ port: 0, signal: 'INVALID_SIGNAL' }), | ||
| { code: 'ERR_INVALID_ARG_TYPE' }); | ||
| } | ||
| })().then(common.mustCall()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: not sure of the exact wording I'd recommend, but might be better to explicitly say to NOT use
awaitwithin the body of the loop.