66 ENV_LOCAL_BASE_URL ,
77 ENV_SERVER_BASE_URL ,
88 ENV_STATE_PATH ,
9+ METRO_COMPANION_LEASE_CHECK_INTERVAL_MS ,
910 METRO_COMPANION_RECONNECT_DELAY_MS ,
1011 METRO_COMPANION_RUN_ARG ,
1112 WS_READY_STATE_OPEN ,
@@ -76,6 +77,12 @@ function normalizeCloseCode(code: number | undefined): number {
7677 return 1011 ;
7778}
7879
80+ function normalizeOutgoingCloseCode ( code : number ) : number {
81+ if ( code === 1000 ) return code ;
82+ if ( code >= 3000 && code <= 4999 ) return code ;
83+ return 3001 ;
84+ }
85+
7986function sendJson ( socket : WebSocket , payload : object ) : void {
8087 if ( socket . readyState !== WS_READY_STATE_OPEN ) return ;
8188 socket . send ( JSON . stringify ( payload ) ) ;
@@ -128,20 +135,14 @@ async function waitForSocketShutdown(socket: WebSocket): Promise<void> {
128135
129136function closeSocketQuietly ( socket : WebSocket , code : number , reason : string ) : void {
130137 try {
131- socket . close ( code , reason ) ;
138+ socket . close ( normalizeOutgoingCloseCode ( code ) , reason ) ;
132139 } catch {
133140 // ignore shutdown races
134141 }
135142}
136143
137144function shouldKeepWorkerRunning ( options : CompanionOptions ) : boolean {
138- if ( ! options . statePath ) return true ;
139- try {
140- fs . accessSync ( options . statePath , fs . constants . F_OK ) ;
141- return true ;
142- } catch {
143- return false ;
144- }
145+ return ! options . statePath || fs . existsSync ( options . statePath ) ;
145146}
146147
147148async function handleBridgeMessage (
@@ -268,9 +269,12 @@ export async function runMetroCompanionWorker(options: CompanionOptions): Promis
268269 const upstreamSockets = new Map < string , WebSocket > ( ) ;
269270 const lifetimeHandle = setInterval ( ( ) => {
270271 if ( ! shouldKeepWorkerRunning ( options ) ) {
272+ // Node's built-in WebSocket client does not expose a force-close API. If the peer never
273+ // answers the close handshake, a detached worker can linger indefinitely, so lease expiry
274+ // uses a hard exit to guarantee teardown.
271275 process . exit ( 0 ) ;
272276 }
273- } , METRO_COMPANION_RECONNECT_DELAY_MS ) ;
277+ } , METRO_COMPANION_LEASE_CHECK_INTERVAL_MS ) ;
274278 lifetimeHandle . unref ( ) ;
275279 while ( shouldKeepWorkerRunning ( options ) ) {
276280 try {
@@ -290,6 +294,9 @@ export async function runMetroCompanionWorker(options: CompanionOptions): Promis
290294 upstreamSockets . forEach ( ( socket ) => closeSocketQuietly ( socket , 1012 , 'bridge disconnected' ) ) ;
291295 upstreamSockets . clear ( ) ;
292296 } catch ( error ) {
297+ if ( ! shouldKeepWorkerRunning ( options ) ) {
298+ break ;
299+ }
293300 console . error ( error instanceof Error ? error . message : String ( error ) ) ;
294301 }
295302 if ( ! shouldKeepWorkerRunning ( options ) ) {
0 commit comments