mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
net: add experimental net/promises API
Add an experimental `net/promises` namespace, accessible via
`require('node:net/promises')` or `net.promises`, mirroring the existing
`fs/promises` and `dns/promises` modules.
It provides promise-based variants of net's one-shot lifecycle
operations:
- `connect()` returns a promise that fulfills with a connected Socket
once the 'connect' event fires, and rejects on connection failure or
when an optional AbortSignal is aborted, destroying the socket.
- `listen()` creates a server, begins listening, and returns a promise
that fulfills with the listening Server once the 'listening' event
fires. It rejects if the server fails to bind or an optional
AbortSignal is aborted, closing the server.
The functions are named for the actions they await, forming a parallel
`connect()`/`listen()` pair. This is intentionally not the callback
API's factory taxonomy: there `createConnection()` is canonical because
it both creates a socket and initiates connecting, whereas
`createServer()` is a pure factory that does not listen. A
`createConnection`/`createServer` pairing would be inconsistent here
because `createServer` has no completion to await, so no
`createConnection` alias is provided.
Refs: https://github.com/nodejs/node/issues/21482
Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Ethan Arrowood <ethan@arrowood.dev>
PR-URL: https://github.com/nodejs/node/pull/63965
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
1 parent
853039ccc5
commit
6d8baeabb0
9 files changed
+435
-6
No files matched your search
+119
@@ -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
|
||||
@@ -2282,6 +2324,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.
|
||||
* 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
|
||||
@@ -2316,6 +2434,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
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
'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);
|
||||
} catch (err) {
|
||||
socket.destroy();
|
||||
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 {
|
||||
// Default to an ephemeral port when no listen target is supplied, matching
|
||||
// `server.listen()` with no arguments; passing a target-less options object
|
||||
// (the `{}` default, or e.g. `{ signal }`) straight to listen() would throw
|
||||
// ERR_INVALID_ARG_VALUE. `signal` is passed through so net installs its own
|
||||
// close-on-abort handler: the signal aborts the server for its entire
|
||||
// lifetime, not just the pending listen.
|
||||
const hasListenTarget = options.port !== undefined ||
|
||||
options.path !== undefined ||
|
||||
options.fd !== undefined ||
|
||||
options.handle !== undefined;
|
||||
server.listen(hasListenTarget ? options : { ...options, port: 0 });
|
||||
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;
|
||||
}
|
||||
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; },
|
||||
};
|
||||
+15
@@ -37,6 +37,7 @@ const {
|
||||
ObjectSetPrototypeOf,
|
||||
Symbol,
|
||||
SymbolAsyncDispose,
|
||||
SymbolAsyncIterator,
|
||||
SymbolDispose,
|
||||
} = primordials;
|
||||
|
||||
@@ -156,6 +157,8 @@ let cluster;
|
||||
let dns;
|
||||
let BlockList;
|
||||
let SocketAddress;
|
||||
let netPromises;
|
||||
let kFirstEventParam;
|
||||
let autoSelectFamilyDefault = getOptionValue('--network-family-autoselection');
|
||||
let autoSelectFamilyAttemptTimeoutDefault = getOptionValue('--network-family-autoselection-attempt-timeout');
|
||||
|
||||
@@ -2854,6 +2857,14 @@ Server.prototype[SymbolAsyncDispose] = async function() {
|
||||
await FunctionPrototypeCall(promisify(this.close), this);
|
||||
};
|
||||
|
||||
Server.prototype[SymbolAsyncIterator] = function() {
|
||||
kFirstEventParam ??= require('internal/events/symbols').kFirstEventParam;
|
||||
return EventEmitter.on(this, 'connection', {
|
||||
close: ['close'],
|
||||
[kFirstEventParam]: true,
|
||||
});
|
||||
};
|
||||
|
||||
Server.prototype._emitCloseIfDrained = function() {
|
||||
debug('SERVER _emitCloseIfDrained');
|
||||
|
||||
@@ -2945,6 +2956,10 @@ module.exports = {
|
||||
connect,
|
||||
createConnection: connect,
|
||||
createServer,
|
||||
get promises() {
|
||||
netPromises ??= require('internal/net/promises');
|
||||
return netPromises;
|
||||
},
|
||||
isIP: isIP,
|
||||
isIPv4: isIPv4,
|
||||
isIPv6: isIPv6,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = require('internal/net/promises');
|
||||
@@ -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());
|
||||
@@ -0,0 +1,90 @@
|
||||
'use strict';
|
||||
const common = require('../common');
|
||||
const assert = require('assert');
|
||||
const net = require('net');
|
||||
const { once } = require('events');
|
||||
const { listen } = require('net/promises');
|
||||
|
||||
(async () => {
|
||||
// Resolves with a listening server.
|
||||
{
|
||||
const server = await listen({ port: 0 });
|
||||
assert.strictEqual(server.listening, true);
|
||||
assert.strictEqual(typeof server.address().port, 'number');
|
||||
server.close();
|
||||
}
|
||||
|
||||
// With no listen target, defaults to an ephemeral port (like
|
||||
// `server.listen()` with no arguments) rather than throwing.
|
||||
{
|
||||
const server = await listen();
|
||||
assert.strictEqual(server.listening, true);
|
||||
assert.strictEqual(typeof server.address().port, 'number');
|
||||
server.close();
|
||||
}
|
||||
|
||||
// A target-less options bag still defaults to an ephemeral port while
|
||||
// honoring other options such as `host`.
|
||||
{
|
||||
const server = await listen({ host: '127.0.0.1' });
|
||||
assert.strictEqual(server.listening, true);
|
||||
assert.strictEqual(server.address().address, '127.0.0.1');
|
||||
assert.strictEqual(typeof server.address().port, 'number');
|
||||
server.close();
|
||||
}
|
||||
|
||||
// The signal aborts the server for its entire lifetime, not just the
|
||||
// pending listen: aborting after it is listening closes the server.
|
||||
{
|
||||
const controller = new AbortController();
|
||||
const server = await listen({ port: 0, signal: controller.signal });
|
||||
assert.strictEqual(server.listening, true);
|
||||
const closed = once(server, 'close');
|
||||
controller.abort();
|
||||
await closed;
|
||||
assert.strictEqual(server.listening, false);
|
||||
}
|
||||
|
||||
// The connectionListener option receives incoming connections.
|
||||
{
|
||||
const server = await listen({
|
||||
port: 0,
|
||||
connectionListener: common.mustCall((socket) => {
|
||||
socket.end();
|
||||
server.close();
|
||||
}),
|
||||
});
|
||||
const client = net.connect(server.address().port);
|
||||
client.resume();
|
||||
}
|
||||
|
||||
// A pre-aborted signal rejects with an AbortError.
|
||||
{
|
||||
await assert.rejects(
|
||||
listen({ port: 0, signal: AbortSignal.abort() }),
|
||||
{ name: 'AbortError' });
|
||||
}
|
||||
|
||||
// Aborting while binding rejects with an AbortError and closes the server.
|
||||
{
|
||||
const controller = new AbortController();
|
||||
const promise = listen({ port: 0, signal: controller.signal });
|
||||
controller.abort();
|
||||
await assert.rejects(promise, { name: 'AbortError' });
|
||||
}
|
||||
|
||||
// An invalid signal throws.
|
||||
{
|
||||
await assert.rejects(
|
||||
listen({ port: 0, signal: 'INVALID_SIGNAL' }),
|
||||
{ code: 'ERR_INVALID_ARG_TYPE' });
|
||||
}
|
||||
|
||||
// Rejects when the address is already in use.
|
||||
{
|
||||
const first = await listen({ port: 0 });
|
||||
const { port } = first.address();
|
||||
await assert.rejects(listen({ port }), { code: 'EADDRINUSE' });
|
||||
first.close();
|
||||
}
|
||||
})().then(common.mustCall());
|
||||
@@ -0,0 +1,50 @@
|
||||
'use strict';
|
||||
const common = require('../common');
|
||||
const assert = require('assert');
|
||||
const net = require('net');
|
||||
const { once } = require('events');
|
||||
|
||||
(async () => {
|
||||
// `for await...of` yields each incoming connection as a net.Socket.
|
||||
{
|
||||
const server = net.createServer().listen(0);
|
||||
await once(server, 'listening');
|
||||
const { port } = server.address();
|
||||
|
||||
const total = 3;
|
||||
const clients = [];
|
||||
for (let i = 0; i < total; i++) {
|
||||
clients.push(net.connect(port));
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
for await (const socket of server) {
|
||||
assert.ok(socket instanceof net.Socket);
|
||||
socket.end();
|
||||
if (++count === total) break;
|
||||
}
|
||||
assert.strictEqual(count, total);
|
||||
|
||||
// Breaking out of the loop stops iterating but does not close the server.
|
||||
assert.strictEqual(server.listening, true);
|
||||
|
||||
server.close();
|
||||
for (const client of clients) {
|
||||
client.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// Iteration ends cleanly when the server closes.
|
||||
{
|
||||
const server = net.createServer().listen(0);
|
||||
await once(server, 'listening');
|
||||
process.nextTick(() => server.close());
|
||||
|
||||
let count = 0;
|
||||
for await (const socket of server) {
|
||||
count++;
|
||||
socket.end();
|
||||
}
|
||||
assert.strictEqual(count, 0);
|
||||
}
|
||||
})().then(common.mustCall());
|
||||
@@ -52,11 +52,12 @@ replServer.complete("import\t( 'n", common.mustSucceed((data) => {
|
||||
assert.notStrictEqual(lastIndex, -1);
|
||||
});
|
||||
assert.strictEqual(completions[lastIndex + 1], '');
|
||||
// There is only one Node.js module that starts with n:
|
||||
// The Node.js modules that start with n:
|
||||
assert.strictEqual(completions[lastIndex + 2], 'net');
|
||||
assert.strictEqual(completions[lastIndex + 3], '');
|
||||
assert.strictEqual(completions[lastIndex + 3], 'net/promises');
|
||||
assert.strictEqual(completions[lastIndex + 4], '');
|
||||
// It's possible to pick up non-core modules too
|
||||
for (const completion of completions.slice(lastIndex + 4)) {
|
||||
for (const completion of completions.slice(lastIndex + 5)) {
|
||||
assert.match(completion, /^n/);
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -67,11 +67,12 @@ const { startNewREPLServer } = require('../common/repl');
|
||||
assert.notStrictEqual(lastIndex, -1);
|
||||
}
|
||||
assert.strictEqual(data[0][lastIndex + 1], '');
|
||||
// There is only one Node.js module that starts with n:
|
||||
// The Node.js modules that start with n:
|
||||
assert.strictEqual(data[0][lastIndex + 2], 'net');
|
||||
assert.strictEqual(data[0][lastIndex + 3], '');
|
||||
assert.strictEqual(data[0][lastIndex + 3], 'net/promises');
|
||||
assert.strictEqual(data[0][lastIndex + 4], '');
|
||||
// It's possible to pick up non-core modules too
|
||||
for (const completion of data[0].slice(lastIndex + 4)) {
|
||||
for (const completion of data[0].slice(lastIndex + 5)) {
|
||||
assert.match(completion, /^n/);
|
||||
}
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user