Skip to content

Commit e324dd3

Browse files
committed
fix(devframe): publish a shared server's WS endpoint once it listens
1 parent 7222527 commit e324dd3

2 files changed

Lines changed: 59 additions & 17 deletions

File tree

‎packages/devframe/src/adapters/__tests__/initiate.test.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,33 @@ describe('adapters/handler', () => {
232232
}
233233
})
234234

235+
it('shared-server tier: publishes the endpoint once a not-yet-listening TLS server listens', async () => {
236+
const host = '127.0.0.1'
237+
const server = createSecureServer({ allowHTTP1: true })
238+
const devtools = initDevframe(defineTestDef('handler-tls-late'), {
239+
base: '/__handler-tls-late/',
240+
auth: false,
241+
host,
242+
server: server as any,
243+
})
244+
245+
try {
246+
await devtools.ready
247+
const internal = getInternalContext(await devtools.context)
248+
expect(internal.wsEndpoint).toBeUndefined()
249+
250+
await new Promise<void>(resolve => server.listen(0, host, resolve))
251+
const { port } = server.address() as { port: number }
252+
expect(internal.wsEndpoint).toEqual({
253+
url: `wss://localhost:${port}/__handler-tls-late/__ws`,
254+
})
255+
}
256+
finally {
257+
await devtools.close()
258+
await new Promise<void>(resolve => server.close(() => resolve()))
259+
}
260+
})
261+
235262
it('ws.url tier: advertises the external endpoint verbatim, owns no transport', async () => {
236263
const devtools = initDevframe(defineTestDef('handler-remote'), { base: '/__handler-remote/', ws: { url: 'wss://devtools.example.com/relay/__ws' } })
237264

‎packages/devframe/src/node/instance-shell.ts‎

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { DevframeRpcConnection, WsOriginRegistry, WsRpcTransport } from 'de
55
import type { H3, H3Event } from 'h3'
66
import type { Buffer } from 'node:buffer'
77
import type { IncomingMessage, Server as NodeHttpServer, ServerResponse } from 'node:http'
8+
import type { AddressInfo } from 'node:net'
89
import type { Duplex } from 'node:stream'
910
import type { ConnectionMeta, DevframeNodeContext, DevframeNodeRpcSession, DevframeNodeRpcSessionMeta, DevframeRpcClientFunctions, DevframeRpcServerFunctions } from '../types'
1011
import type { DevframeSseOptions, DevframeWsOptions } from '../types/devframe'
@@ -144,14 +145,16 @@ async function bindHttpAndWs(options: BindHttpAndWsOptions): Promise<StartedServ
144145

145146
const address = httpServer.address()
146147
const resolvedPort = typeof address === 'object' && address ? address.port : port
147-
const secure = httpServer instanceof TlsServer
148-
const origin = secure
148+
const origin = httpServer instanceof TlsServer
149149
? `https://${formatHostForUrl(bindHost)}:${resolvedPort}`
150150
: normalizeHttpServerUrl(bindHost, resolvedPort)
151-
const internal = getInternalContext(context)
152-
const wsUrl = `${secure ? 'wss' : 'ws'}://${formatHostForUrl(bindHost)}:${resolvedPort}${options.path ?? ''}`
153-
if (websocket)
154-
internal.setWsEndpoint({ url: wsUrl })
151+
let wsUrl: string | undefined
152+
const cancelPublish = websocket
153+
? whenListening(httpServer, (boundPort) => {
154+
wsUrl = `${wsScheme(httpServer)}://${formatHostForUrl(bindHost)}:${boundPort}${options.path ?? ''}`
155+
getInternalContext(context).setWsEndpoint({ url: wsUrl })
156+
})
157+
: () => {}
155158

156159
return {
157160
origin,
@@ -161,15 +164,34 @@ async function bindHttpAndWs(options: BindHttpAndWsOptions): Promise<StartedServ
161164
rpcGroup: core.rpcGroup,
162165
connectionMeta: () => websocketConnectionMeta(rpcHost, options.path),
163166
async close() {
167+
cancelPublish()
164168
await closeWs()
165169
if (ownsHttpServer)
166170
await new Promise<void>(r => httpServer.close(() => r()))
167-
if (websocket && getInternalContext(context).wsEndpoint?.url === wsUrl)
171+
if (wsUrl && getInternalContext(context).wsEndpoint?.url === wsUrl)
168172
getInternalContext(context).setWsEndpoint(undefined)
169173
},
170174
}
171175
}
172176

177+
function wsScheme(server: NodeHttpServer): 'ws' | 'wss' {
178+
return server instanceof TlsServer ? 'wss' : 'ws'
179+
}
180+
181+
function whenListening(server: NodeHttpServer, fn: (port: number, address: AddressInfo) => void): () => void {
182+
const run = (): void => {
183+
const address = server.address()
184+
if (typeof address === 'object' && address)
185+
fn(address.port, address)
186+
}
187+
if (server.listening) {
188+
run()
189+
return () => {}
190+
}
191+
server.once('listening', run)
192+
return () => server.off('listening', run)
193+
}
194+
173195
/**
174196
* Structural view of `Bun.serve`, typed loosely so devframe carries no
175197
* dependency on Bun's own types (`tsc` runs with Node lib only).
@@ -943,19 +965,12 @@ export function createInstanceShell<TContext extends DevframeNodeContext>(
943965
* {@link bindHttpAndWs} does the same for the tiers it owns.
944966
*/
945967
function publishWsEndpoint(server: NodeHttpServer): void {
946-
const record = (): void => {
947-
const address = server.address()
948-
if (typeof address !== 'object' || !address)
949-
return
968+
whenListening(server, (boundPort, address) => {
950969
const host = options.host ?? (address.address === '::' || address.address === '0.0.0.0' ? 'localhost' : address.address)
951970
getInternalContext(ctx).setWsEndpoint({
952-
url: `ws://${formatHostForUrl(host)}:${address.port}${routePath}`,
971+
url: `${wsScheme(server)}://${formatHostForUrl(host)}:${boundPort}${routePath}`,
953972
})
954-
}
955-
if (server.listening)
956-
record()
957-
else
958-
server.once('listening', record)
973+
})
959974
}
960975

961976
function handleUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer): void {

0 commit comments

Comments
 (0)