[match] tachyon sandbox switch

This commit is contained in:
Devin Zuczek
2026-09-21 12:49:07 -04:00
parent cf0317ab5b
commit 58a794d4e2
7 changed files with 91 additions and 73 deletions
+28 -41
View File
@@ -191,12 +191,12 @@ interface TachyonServer {
* never sends the id anywhere), but it is positional, so inserting an entry renames
* every server after it.
*/
function tachyonPool(env: Env): TachyonServer[] {
return varOr(env.TACHYON_HOST_PORT, '')
function tachyonPool(hostPorts: string, idPrefix = 'tachyon-'): TachyonServer[] {
return hostPorts
.split(',')
.map((entry) => entry.trim())
.filter((entry) => entry !== '')
.map((hostPort, i) => ({ hostPort, serverId: `tachyon-${i + 1}` }))
.map((hostPort, i) => ({ hostPort, serverId: `${idPrefix}${i + 1}` }))
}
/** What the connection info carries when there is no Tachyon server to name. */
@@ -223,44 +223,31 @@ const NO_TACHYON_SERVER: TachyonServer = { hostPort: '', serverId: '' }
* `roomInstanceId` 0 means the caller resolved to no instance at all (they're in no
* room, or named one that doesn't exist); they get no server rather than server one.
*/
function tachyonServerFor(env: Env, roomInstanceId: number): TachyonServer {
const pool = tachyonPool(env)
function tachyonServerFor(pool: TachyonServer[], roomInstanceId: number): TachyonServer {
if (pool.length === 0 || roomInstanceId <= 0) return NO_TACHYON_SERVER
return pool[roomInstanceId % pool.length] ?? NO_TACHYON_SERVER
}
/**
* The Tachyon port a DEVELOPER is handed, in place of the port on the pool entry their
* instance resolved to. Developers connect to a separate Tachyon build running beside the
* live one on the same box, so only the PORT moves: the HOST stays whichever one
* {@link tachyonServerFor} picked, rather than scattering developers off the pool.
* The prefix of a sandbox server's generated id (see {@link tachyonPoolFor}) — `dev-1`,
* `dev-2` rather than the live pool's `tachyon-N`, so the (cosmetic) id the client
* displays says which pool it is on.
*/
const DEVELOPER_TACHYON_PORT = '7778'
const SANDBOX_TACHYON_SERVER_ID_PREFIX = 'dev-'
/**
* The id displayed for that dev server. Not `tachyon-N` — the generated ids are positional
* names for entries in the pool ({@link tachyonPool}), and the dev build is not one of
* them: it is a different server that happens to share a host. The id is cosmetic (the
* client displays it and never sends it anywhere), so it only has to say which server the
* player is on, and "dev" is that.
* The pool the caller's instance is picked out of. A SANDBOX account runs against its own
* Tachyon servers, apart from the live pool, so the whole list is swapped for
* `TACHYON_HOST_PORT_SANDBOX` rather than any one entry: selection within it is the same
* instance-id round-robin {@link tachyonServerFor} applies to the live pool. When that var
* is unset a sandbox account falls back to the live pool, so an operator who runs no
* sandbox servers doesn't leave those accounts without voice.
*/
const DEVELOPER_TACHYON_SERVER_ID = 'dev'
/**
* `server` with its port replaced by {@link DEVELOPER_TACHYON_PORT} and its id by
* {@link DEVELOPER_TACHYON_SERVER_ID}. An empty address is left alone — the pool is unset or
* the caller is in no instance, so there is no server to point at another port, and writing
* a bare `:7778` would hand the client an address that answers nothing (and name a server
* that isn't there). Splits on the LAST colon so a bracketed IPv6 host survives.
*/
function developerTachyonServer(server: TachyonServer): TachyonServer {
if (server.hostPort === '') return server
const colon = server.hostPort.lastIndexOf(':')
const host = colon === -1 ? server.hostPort : server.hostPort.slice(0, colon)
return {
hostPort: `${host}:${DEVELOPER_TACHYON_PORT}`,
serverId: DEVELOPER_TACHYON_SERVER_ID,
}
function tachyonPoolFor(env: Env, isSandbox: boolean): TachyonServer[] {
const sandbox = varOr(env.TACHYON_HOST_PORT_SANDBOX, '')
return isSandbox && sandbox.trim() !== ''
? tachyonPool(sandbox, SANDBOX_TACHYON_SERVER_ID_PREFIX)
: tachyonPool(varOr(env.TACHYON_HOST_PORT, ''))
}
/**
@@ -2907,8 +2894,8 @@ const app = new Hono<App>()
'`TACHYON_HOST_PORT` pool, chosen by instance id so every player in a session is',
'handed the same one, with a generated `voiceServerId` (`tachyon-1`, `tachyon-2`,',
'…). Both are empty when the pool is unset or the caller is in no instance. A',
'caller holding the DEVELOPER role gets that same host on port 7778, the dev',
'Tachyon build, with a `voiceServerId` of `dev`.',
'sandbox account is picked out of the `TACHYON_HOST_PORT_SANDBOX` pool instead, the same way,',
'with ids `dev-1`, `dev-2`, ….',
'`experiments` carries the client’s networking flags.',
].join(' '),
security: AUTHED,
@@ -2949,11 +2936,11 @@ const app = new Hono<App>()
}
const account = await getAccount(c.env.DB, id)
// The instance's server, the same one every other player in it is handed — except
// for a DEVELOPER, who is sent to the same host on the dev port, under the id
// `dev` rather than the pool entry's positional name.
const assigned = tachyonServerFor(c.env, roomInstanceId)
const tachyon =
account?.isDeveloper === true ? developerTachyonServer(assigned) : assigned
// that a SANDBOX account is picked out of the sandbox pool instead of the live one.
const tachyon = tachyonServerFor(
tachyonPoolFor(c.env, account?.isSandbox === true),
roomInstanceId
)
// Identifies the player to Photon. Signed with the shared JWT secret; the token's
// `aud` is the realtime app it's for. Nothing verifies it while Photon is
@@ -2977,8 +2964,8 @@ const app = new Hono<App>()
photonRoomId,
// The Tachyon server this instance runs on, picked out of the operator's
// pool by {@link tachyonServerFor} — empty strings when the pool is empty
// or the caller is in no instance, and on the dev port (id `dev`) for a
// developer. Empty rather than null: the client's
// or the caller is in no instance, and out of the sandbox pool for a
// sandbox account. Empty rather than null: the client's
// decoder is likelier to accept a missing-value string than a null on a
// string field. The presence payload's NULL_CONNECTION_INFO keeps its
// nulls — that one never carries credentials.