@@ -4,6 +4,7 @@ import type { NodeAdapter } from 'crossws/adapters/node'
44import type { Buffer } from 'node:buffer'
55import type { Server as HttpServer , IncomingMessage } from 'node:http'
66import type { Server as HttpsServer , ServerOptions as HttpsServerOptions } from 'node:https'
7+ import type { AddressInfo } from 'node:net'
78import type { Duplex } from 'node:stream'
89import type { RpcFunctionDefinitionAny } from '../types'
910import { createServer as createHttpServer } from 'node:http'
@@ -44,9 +45,12 @@ export interface WsRpcTransportOptions {
4445 * this transport detaches the upgrade listener without closing the server.
4546 */
4647 server ?: HttpServer | HttpsServer
47- /** Port for a newly-created standalone WS server. */
48+ /**
49+ * Port for the standalone WebSocket server. Defaults to `0`, which lets the
50+ * operating system assign an available port.
51+ */
4852 port ?: number
49- /** Host for a newly-created standalone WS server. Defaults to `localhost`. */
53+ /** Host for the standalone WebSocket server. Defaults to `localhost`. */
5054 host ?: string
5155 /**
5256 * Restrict the WS endpoint to a single upgrade route (e.g. `/__devframe_ws`). When
@@ -168,6 +172,10 @@ export interface WsRpcTransport {
168172 * `peers` and pub/sub. See https://crossws.h3.dev.
169173 */
170174 ws : NodeAdapter
175+ /** Resolves when the transport-owned server is listening. */
176+ ready : Promise < void >
177+ /** Returns the bound address, or `null` when the server is not listening. */
178+ address : ( ) => AddressInfo | string | null
171179 /** Remove the upgrade listener from a shared `server` (a no-op otherwise). */
172180 detach : ( ) => void
173181 /**
@@ -184,6 +192,27 @@ const EMPTY_DEFS: ReadonlyMap<string, Pick<RpcFunctionDefinitionAny, 'jsonSerial
184192
185193function NOOP ( ) { }
186194
195+ function listen (
196+ server : HttpServer | HttpsServer ,
197+ port : number ,
198+ host : string ,
199+ ) : Promise < void > {
200+ return new Promise ( ( resolve , reject ) => {
201+ const onError = ( error : Error ) => reject ( error )
202+ server . once ( 'error' , onError )
203+ try {
204+ server . listen ( port , host , ( ) => {
205+ server . off ( 'error' , onError )
206+ resolve ( )
207+ } )
208+ }
209+ catch ( error ) {
210+ server . off ( 'error' , onError )
211+ reject ( error )
212+ }
213+ } )
214+ }
215+
187216/** Compare two URL paths ignoring a trailing slash. */
188217function pathMatches ( a : string , b : string ) : boolean {
189218 const strip = ( p : string ) => ( p . length > 1 && p . endsWith ( '/' ) ? p . slice ( 0 , - 1 ) : p )
@@ -276,9 +305,9 @@ function routeUpgrades(
276305 * `server` (sharing its port, optionally scoped to a `path`), or let this
277306 * helper create a standalone server from `port` / `host` / `https`.
278307 *
279- * Returns the crossws node adapter plus `detach` (remove the upgrade
280- * listener from a shared `server`) and `close` (full deterministic
281- * teardown).
308+ * Returns the crossws node adapter, standalone-server readiness/address
309+ * accessors, `detach` (remove the upgrade listener from a shared `server`),
310+ * and `close` (full deterministic teardown).
282311 */
283312export function attachWsRpcTransport <
284313 ClientFunctions extends object ,
@@ -388,6 +417,7 @@ export function attachWsRpcTransport<
388417 } )
389418
390419 let detach = NOOP
420+ let ready = Promise . resolve ( )
391421 // A server created (and thus owned) by this transport. Nothing else
392422 // handles its upgrades, so off-route clients are rejected promptly.
393423 let ownedServer : HttpServer | HttpsServer | undefined
@@ -399,7 +429,7 @@ export function attachWsRpcTransport<
399429 else if ( https ) {
400430 ownedServer = createHttpsServer ( https )
401431 detach = routeUpgrades ( ownedServer , ws , path , true , allowedOrigins )
402- ownedServer . listen ( port , host )
432+ ready = listen ( ownedServer , port ?? 0 , host )
403433 }
404434 else {
405435 // Standalone server on its own port. Plain HTTP requests get the
@@ -409,11 +439,15 @@ export function attachWsRpcTransport<
409439 res . end ( 'Upgrade Required' )
410440 } )
411441 detach = routeUpgrades ( ownedServer , ws , path , true , allowedOrigins )
412- ownedServer . listen ( port , host )
442+ ready = listen ( ownedServer , port ?? 0 , host )
413443 }
414444
445+ const activeServer = server ?? ownedServer
446+
415447 return {
416448 ws,
449+ ready,
450+ address : ( ) => activeServer ?. address ( ) ?? null ,
417451 detach,
418452 async close ( ) {
419453 // Detach our upgrade listener first so a shared host server stops
@@ -425,6 +459,9 @@ export function attachWsRpcTransport<
425459 ws . closeAll ( undefined , undefined , true )
426460 if ( ownedServer ) {
427461 const srv = ownedServer
462+ await ready . catch ( ( ) => { } )
463+ if ( ! srv . listening )
464+ return
428465 await new Promise < void > ( r => srv . close ( ( ) => r ( ) ) )
429466 }
430467 } ,
0 commit comments