Context
Split out from a cubic review finding on #24 (single-process launcher scripts/emulate-all.ts).
scripts/emulate-all.ts was hardened so that startVercel awaits the HTTP server's 'listening' event and rejects on 'error' — an occupied Vercel-service port now rejects into main()'s try/catch, which closes the services already started instead of crashing the launcher on an asynchronous listen error.
The this-repo side (startPlease) has the same latent issue, but the readiness lives one layer down in @pleaseai/emulate's createEmulator, which is outside the launcher PR's scope.
Problem
packages/emulate/src/api.ts — createEmulator calls serve({ fetch, port }) and returns immediately, without awaiting 'listening':
const httpServer = serve({ fetch: app.fetch, port })
return { url: baseUrl, /* ... */ }
serve() (from @emulators/core) returns a Node http.Server that binds asynchronously. If the port is occupied, the failure surfaces on the server's 'error' event (EADDRINUSE) on a later tick — not as a throw from createEmulator. So an awaited createEmulator(...) resolves successfully even though the port never bound, and the EADDRINUSE becomes an uncaught exception that crashes the process.
Proposed fix
Make createEmulator await readiness before resolving, mirroring the launcher fix:
const httpServer = serve({ fetch: app.fetch, port })
await new Promise<void>((resolve, reject) => {
function onListening() { httpServer.off('error', onError); resolve() }
function onError(err: Error) { httpServer.off('listening', onListening); reject(err) }
httpServer.once('listening', onListening)
httpServer.once('error', onError)
})
Then startPlease in the launcher inherits correct port-in-use handling automatically.
Notes
- Low severity: affects a dev-time emulator launcher on a port collision, not production paths.
- Verify
serve() doesn't already attach its own conflicting 'error' handler in @emulators/core.
Context
Split out from a cubic review finding on #24 (single-process launcher
scripts/emulate-all.ts).scripts/emulate-all.tswas hardened so thatstartVercelawaits the HTTP server's'listening'event and rejects on'error'— an occupied Vercel-service port now rejects intomain()'stry/catch, which closes the services already started instead of crashing the launcher on an asynchronous listen error.The this-repo side (
startPlease) has the same latent issue, but the readiness lives one layer down in@pleaseai/emulate'screateEmulator, which is outside the launcher PR's scope.Problem
packages/emulate/src/api.ts—createEmulatorcallsserve({ fetch, port })and returns immediately, without awaiting'listening':serve()(from@emulators/core) returns a Nodehttp.Serverthat binds asynchronously. If the port is occupied, the failure surfaces on the server's'error'event (EADDRINUSE) on a later tick — not as a throw fromcreateEmulator. So an awaitedcreateEmulator(...)resolves successfully even though the port never bound, and theEADDRINUSEbecomes an uncaught exception that crashes the process.Proposed fix
Make
createEmulatorawait readiness before resolving, mirroring the launcher fix:Then
startPleasein the launcher inherits correct port-in-use handling automatically.Notes
serve()doesn't already attach its own conflicting'error'handler in@emulators/core.