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