diff options
| author | Adam Malczewski <[email protected]> | 2026-06-02 19:02:43 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-02 19:02:43 +0900 |
| commit | 94ce3401cd65ff3f58c134c73cb3f816b7142093 (patch) | |
| tree | 4b3119ba5a783b0dfba152c6953e832857785933 | |
| parent | 062d01bd2f5c3ab6de7747dc5028e66b81dac6f5 (diff) | |
| download | dispatch-94ce3401cd65ff3f58c134c73cb3f816b7142093.tar.gz dispatch-94ce3401cd65ff3f58c134c73cb3f816b7142093.zip | |
feat(api): fall back to next port (3000→3010) when the port is in use
The API server bound a fixed port (3000) and died with EADDRINUSE when it
was taken — painful when running multiple dispatch instances (e.g. testing
several feature branches at once). Replace the static default export with an
explicit Bun.serve retry loop that increments the port by one on EADDRINUSE,
from START_PORT (PORT env or 3000) up to MAX_PORT (3010), logging the chosen
port and a hint to repoint the frontend's API URL on a bump.
Guarded by import.meta.main so importing the module (for `app`) never binds a
port. Frontend unchanged — set its API URL manually when a bump occurs.
| -rw-r--r-- | packages/api/src/index.ts | 61 |
1 files changed, 55 insertions, 6 deletions
diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index a0ad025..478abe0 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -70,9 +70,58 @@ app.get( export { app }; -export default { - port: Number(process.env.PORT) || 3000, - idleTimeout: 60, - fetch: app.fetch, - websocket, -}; +// Starting port (overridable via PORT) and the inclusive ceiling we will bump +// up to when a port is already in use. If 3000 is taken we try 3001, 3002, … +// up to MAX_PORT, so multiple dispatch instances (e.g. testing several +// features at once) can coexist without manually juggling ports. The frontend +// defaults to :3000 — point it at the chosen port via the in-app API-URL +// field / VITE_API_URL when a bump happens. +const START_PORT = Number(process.env.PORT) || 3000; +const MAX_PORT = 3010; + +/** + * Bind the server to `START_PORT`, incrementing by one on EADDRINUSE until a + * free port is found or MAX_PORT is exceeded. Bun's `Bun.serve` throws + * synchronously when the port is taken, so we can catch and retry. Returns the + * live server (whose `.port` reflects the port actually bound). + */ +function serveWithPortFallback() { + let lastError: unknown; + for (let port = START_PORT; port <= MAX_PORT; port++) { + try { + const server = Bun.serve({ + port, + idleTimeout: 60, + fetch: app.fetch, + websocket, + }); + if (port !== START_PORT) { + console.warn( + `dispatch: port ${START_PORT} in use — bound to ${port} instead. ` + + `Set the frontend's API URL to http://localhost:${port}.`, + ); + } + console.log(`dispatch: API listening on http://localhost:${server.port}`); + return server; + } catch (err) { + const code = (err as NodeJS.ErrnoException)?.code; + if (code === "EADDRINUSE") { + lastError = err; + continue; + } + throw err; + } + } + console.error( + `dispatch: no free port in range ${START_PORT}-${MAX_PORT}. ` + + `Free one up or set PORT to an open port.`, + ); + throw lastError ?? new Error(`No free port in range ${START_PORT}-${MAX_PORT}`); +} + +// Only start the server when run as the entry point — importing this module +// (e.g. for `app`) must not bind a port. This preserves the prior +// default-export behavior where Bun served only the entry file. +if (import.meta.main) { + serveWithPortFallback(); +} |
