mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-01 20:53:25 +00:00
Mint game ids on the server, randomly route create-game across workers (#4393)
## What Game creation no longer requires the caller to pick the `gameID` or compute its owning worker. The client POSTs to a prefix-less `/api/create_game`; **nginx (prod) and the vite dev proxy randomly route it to a worker**, which **mints an id that hashes back to itself** and returns it along with its `workerIndex`. ## Why it stays correct The minted id still hashes to the creating worker (via the existing `generateGameIdForWorker`), so everything downstream that derives the worker from the gameID — websocket connect, share URL, join flow — keeps working unchanged. The only thing that moved is *who picks the id and worker*. ## Changes - **`src/server/Worker.ts`** — factor create into a shared `createGameForId`; add `POST /api/create_game` (no id) that mints a self-owned id and returns `gameInfo` + `workerIndex`/`workerPath`. The existing `POST /api/create_game/:id` stays. - **`nginx.conf`** — `location = /api/create_game` proxies to a `random` worker upstream. - **`generate-nginx-upstream.sh` + `Dockerfile`** — the entrypoint generates that upstream from `NUM_WORKERS` at container **start** time. `NUM_WORKERS` isn't known at image build time (the image is built once and deployed with different env), so it can't be baked into `nginx.conf` — hence runtime generation of exactly the live worker ports (no dead-server padding). - **`vite.config.ts`** — dev-only middleware forwards `POST /api/create_game` to a random worker. Vite's `http-proxy` can't pick a per-request random target, so this is a small middleware plugin (same pattern as the existing `serveProprietaryDir`), registered before the `/api` proxy. - **`src/client/HostLobbyModal.ts`** — stop generating the id client-side; use the server's. ## Behavior change to note The host's share link used to be copied **instantly** from a client-generated id. Now the id comes from the server, so the copy waits one create round-trip — I moved the URL build/copy into the create `.then` (and kept the failure path that clears the clipboard). Brief empty-link state in the modal until create resolves. ## Verification - tsc + eslint clean; full suite green (1543 tests). - nginx additions validated with `nginx -t` in isolation (the full file references container-only paths like `/etc/nginx/mime.types`); upstream + `proxy_pass` resolve. - `generate-nginx-upstream.sh` tested with `NUM_WORKERS` set and unset (defaults to 1). Not yet exercised live end-to-end (needs a dev-server restart — `vite.config.ts` + `Worker.ts` changes aren't hot-reloaded). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,6 @@ import {
|
||||
TeamCountConfig,
|
||||
isValidGameID,
|
||||
} from "../core/Schemas";
|
||||
import { generateID } from "../core/Util";
|
||||
import { getPlayToken } from "./Auth";
|
||||
import "./components/baseComponents/Modal";
|
||||
import { BaseModal } from "./components/BaseModal";
|
||||
@@ -487,26 +486,24 @@ export class HostLobbyModal extends BaseModal {
|
||||
|
||||
protected onOpen(): void {
|
||||
this.startLobbyUpdates();
|
||||
this.lobbyId = generateID();
|
||||
// Note: clientID will be assigned by server when we join the lobby
|
||||
// lobbyCreatorClientID stays empty until then
|
||||
|
||||
// Copy immediately so the host can share the link without waiting for the
|
||||
// server. If lobby creation fails, clear the clipboard to avoid a dead link.
|
||||
void this.constructUrl().then(async (url) => {
|
||||
this.updateLobbyHistory(url);
|
||||
await this.updateComplete;
|
||||
void (this.querySelector("copy-button") as CopyButton)?.handleCopy();
|
||||
});
|
||||
// The server mints the game id, so we don't know it until createLobby
|
||||
// resolves. clientID is assigned by the server when we join the lobby.
|
||||
|
||||
// Pass auth token for creator identification (server extracts persistentID from it)
|
||||
createLobby(this.lobbyId)
|
||||
createLobby()
|
||||
.then(async (lobby) => {
|
||||
this.lobbyId = lobby.gameID;
|
||||
if (!isValidGameID(this.lobbyId)) {
|
||||
throw new Error(`Invalid lobby ID format: ${this.lobbyId}`);
|
||||
}
|
||||
crazyGamesSDK.showInviteButton(this.lobbyId);
|
||||
|
||||
// Now that we have the id, build and copy the share link. If lobby
|
||||
// creation fails, the catch below clears the clipboard.
|
||||
const url = await this.constructUrl();
|
||||
this.updateLobbyHistory(url);
|
||||
await this.updateComplete;
|
||||
void (this.querySelector("copy-button") as CopyButton)?.handleCopy();
|
||||
})
|
||||
.then(() => {
|
||||
this.dispatchEvent(
|
||||
@@ -1149,21 +1146,20 @@ export class HostLobbyModal extends BaseModal {
|
||||
}
|
||||
}
|
||||
|
||||
async function createLobby(gameID: string): Promise<GameInfo> {
|
||||
async function createLobby(): Promise<GameInfo> {
|
||||
// Send JWT token for creator identification - server extracts persistentID from it
|
||||
// persistentID should never be exposed to other clients
|
||||
const token = await getPlayToken();
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/${ClientEnv.workerPath(gameID)}/api/create_game/${gameID}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
// No worker prefix and no id: nginx (prod) / the vite dev proxy randomly
|
||||
// routes to a worker, which mints a self-owned id and returns it.
|
||||
const response = await fetch(`/api/create_game`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
|
||||
Reference in New Issue
Block a user