@@ -23,25 +23,64 @@ import {
2323import { getSocketMetadataTags } from './socket-metadata' ;
2424import { normalizeIP } from './ip-utils' ;
2525
26- // Test if a local port for a given interface (IPv4/6) is currently in use
26+ // Test if a local port for a given interface (IPv4/6) is currently in use, by attempting
27+ // to bind to it. We reuse a single server instance and cache results to avoid creating
28+ // a new server on every request. Concurrent checks for the same port share one probe.
29+ const probeServer = net . createServer ( ) ;
30+ const portActiveCache = new Map < string , { result : boolean , expires : number } > ( ) ;
31+ const portActiveInFlight = new Map < string , Promise < boolean > > ( ) ;
32+ const PORT_ACTIVE_CACHE_TTL = 5000 ; // 5 seconds
33+
2734export async function isLocalPortActive ( interfaceIp : '::1' | '127.0.0.1' , port : number ) {
2835 if ( interfaceIp === '::1' && ! isLocalIPv6Available ) return false ;
2936
30- return new Promise ( ( resolve ) => {
31- const server = net . createServer ( ) ;
32- server . listen ( {
33- host : interfaceIp ,
34- port,
35- ipv6Only : interfaceIp === '::1'
36- } ) ;
37- server . once ( 'listening' , ( ) => {
38- resolve ( false ) ;
39- server . close ( ( ) => { } ) ;
40- } ) ;
41- server . once ( 'error' , ( e ) => {
42- resolve ( true ) ;
37+ const cacheKey = `${ interfaceIp } :${ port } ` ;
38+
39+ const cached = portActiveCache . get ( cacheKey ) ;
40+ if ( cached && cached . expires > Date . now ( ) ) {
41+ return cached . result ;
42+ }
43+
44+ // Deduplicate concurrent checks for the same address:port
45+ let probe = portActiveInFlight . get ( cacheKey ) ;
46+ if ( ! probe ) {
47+ probe = probePort ( interfaceIp , port ) ;
48+ portActiveInFlight . set ( cacheKey , probe ) ;
49+ probe . then ( ( result ) => {
50+ portActiveCache . set ( cacheKey , { result, expires : Date . now ( ) + PORT_ACTIVE_CACHE_TTL } ) ;
51+ portActiveInFlight . delete ( cacheKey ) ;
4352 } ) ;
44- } ) ;
53+ }
54+
55+ return probe ;
56+ }
57+
58+ // Serialized via the in-flight map above — only one probe runs at a time per key,
59+ // and different keys won't collide because they share the single probeServer sequentially
60+ // via the listen/close cycle.
61+ let probeServerReady = Promise . resolve ( ) ;
62+
63+ function probePort ( interfaceIp : string , port : number ) : Promise < boolean > {
64+ const result = probeServerReady . then ( ( ) =>
65+ new Promise < boolean > ( ( resolve ) => {
66+ probeServer . listen ( {
67+ host : interfaceIp ,
68+ port,
69+ ipv6Only : interfaceIp === '::1'
70+ } ) ;
71+ probeServer . once ( 'listening' , ( ) => {
72+ probeServer . close ( ( ) => resolve ( false ) ) ;
73+ } ) ;
74+ probeServer . once ( 'error' , ( ) => {
75+ resolve ( true ) ;
76+ } ) ;
77+ } )
78+ ) ;
79+
80+ // Chain so the next probe waits for this one to fully complete
81+ probeServerReady = result . then ( ( ) => { } ) ;
82+
83+ return result ;
4584}
4685
4786// This file imported in browsers etc as it's used in handlers, but none of these methods are used
0 commit comments