[match] add developer tachyon override

This commit is contained in:
Devin Zuczek
2026-09-18 13:08:43 -04:00
parent fd4264d982
commit 04554b0826
3 changed files with 114 additions and 9 deletions
+47 -5
View File
@@ -229,6 +229,40 @@ function tachyonServerFor(env: Env, roomInstanceId: number): TachyonServer {
return pool[roomInstanceId % pool.length] ?? 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.
*/
const DEVELOPER_TACHYON_PORT = '7778'
/**
* 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.
*/
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,
}
}
/** /**
* Networking feature flags the client reads off its connection info. Verbatim from * Networking feature flags the client reads off its connection info. Verbatim from
* the reference server — the client changes how it replicates based on these, so they * the reference server — the client changes how it replicates based on these, so they
@@ -2872,7 +2906,9 @@ const app = new Hono<App>()
'fields name the Tachyon server that instance was assigned — one entry out of the', 'fields name the Tachyon server that instance was assigned — one entry out of the',
'`TACHYON_HOST_PORT` pool, chosen by instance id so every player in a session is', '`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`,', '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.', '…). 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`.',
'`experiments` carries the client’s networking flags.', '`experiments` carries the client’s networking flags.',
].join(' '), ].join(' '),
security: AUTHED, security: AUTHED,
@@ -2911,8 +2947,13 @@ const app = new Hono<App>()
photonRoomId = instance?.photonRoomId ?? '' photonRoomId = instance?.photonRoomId ?? ''
} }
} }
// The instance's server, the same one every other player in it is handed. const account = await getAccount(c.env.DB, id)
const tachyon = tachyonServerFor(c.env, roomInstanceId) // 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
// Identifies the player to Photon. Signed with the shared JWT secret; the token's // 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 // `aud` is the realtime app it's for. Nothing verifies it while Photon is
@@ -2920,7 +2961,7 @@ const app = new Hono<App>()
const photonAuthToken = await generatePhotonAuthToken( const photonAuthToken = await generatePhotonAuthToken(
id, id,
{ {
platformId: (await getAccount(c.env.DB, id))?.platformId ?? '', platformId: account?.platformId ?? '',
platform: presence?.platform ?? 0, platform: presence?.platform ?? 0,
deviceClass: presence?.deviceClass ?? 0, deviceClass: presence?.deviceClass ?? 0,
audience: apps.photonRealtimeAppId, audience: apps.photonRealtimeAppId,
@@ -2936,7 +2977,8 @@ const app = new Hono<App>()
photonRoomId, photonRoomId,
// The Tachyon server this instance runs on, picked out of the operator's // The Tachyon server this instance runs on, picked out of the operator's
// pool by {@link tachyonServerFor} — empty strings when the pool is empty // pool by {@link tachyonServerFor} — empty strings when the pool is empty
// or the caller is in no instance. Empty rather than null: the client's // or the caller is in no instance, and on the dev port (id `dev`) for a
// developer. Empty rather than null: the client's
// decoder is likelier to accept a missing-value string than a null on a // 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 // string field. The presence payload's NULL_CONNECTION_INFO keeps its
// nulls — that one never carries credentials. // nulls — that one never carries credentials.
+9 -4
View File
@@ -216,8 +216,9 @@ export const ConnectionExperiments = z.object({
* and `photonRoomId`, the Photon room of the instance their presence says they're in * and `photonRoomId`, the Photon room of the instance their presence says they're in
* — the same name every other player in that instance is handed. The voice fields name * — the same name every other player in that instance is handed. The voice fields name
* the Tachyon server that instance was assigned out of the `TACHYON_HOST_PORT` pool — * the Tachyon server that instance was assigned out of the `TACHYON_HOST_PORT` pool —
* likewise the same for everyone in the session — and are empty when the pool is unset * likewise the same for everyone in the session, except that a caller holding the
* or the caller is in no instance. `photonRegion` matches the one stamped * DEVELOPER role is sent to that host's port 7778 as `dev` — and are empty when the pool
* is unset or the caller is in no instance. `photonRegion` matches the one stamped
* on every room instance, so the two can't disagree. * on every room instance, so the two can't disagree.
*/ */
export const ConnectionInfo = z.object({ export const ConnectionInfo = z.object({
@@ -229,10 +230,14 @@ export const ConnectionInfo = z.object({
photonRoomId: z.string().describe('The caller’s current instance; empty when they’re in none'), photonRoomId: z.string().describe('The caller’s current instance; empty when they’re in none'),
voiceConnectionInfo: z voiceConnectionInfo: z
.string() .string()
.describe('The instance’s Tachyon server, `host:port`; empty when none is configured'), .describe(
'The instance’s Tachyon server, `host:port` (port 7778 for a developer); empty when none is configured'
),
voiceServerId: z voiceServerId: z
.string() .string()
.describe('That server’s generated id (`tachyon-1`, …); cosmetic, empty when there is none'), .describe(
'That server’s generated id (`tachyon-1`, …; `dev` for a developer); cosmetic, empty when there is none'
),
experiments: ConnectionExperiments, experiments: ConnectionExperiments,
}) })
@@ -1491,6 +1491,64 @@ describe('auth-gated endpoints', () => {
} }
}) })
test('a developer is sent to the same Tachyon host on the dev port', async () => {
// Developers run against a Tachyon build listening on 7778 beside the live one, so
// only the host carries over from the instance's assignment — the dev build is not an
// entry in the pool, so it is named `dev` rather than borrowing that entry's
// positional `tachyon-N`.
await env.DB.prepare('INSERT OR IGNORE INTO account (data) VALUES (?1)')
.bind(JSON.stringify({ accountId: 974, username: 'Devver', isDeveloper: true }))
.run()
const original = env.TACHYON_HOST_PORT
try {
// One entry, so the assignment can't happen to land on 7778 by itself.
env.TACHYON_HOST_PORT = '198.51.100.12:7777'
const voiceFor = async (player: string, roomInstanceId: number) => {
const res = await exports.default.fetch(
`${ORIGIN}/player/connection-info?roomInstanceId=${roomInstanceId}`,
{ headers: await bearer(player) }
)
const body = (await res.json()) as {
value: { voiceConnectionInfo: string; voiceServerId: string }
}
return {
voiceConnectionInfo: body.value.voiceConnectionInfo,
voiceServerId: body.value.voiceServerId,
}
}
const instance = await createRoomInstance(env.DB, {
ownerAccountId: 974,
roomId: 2,
photonRoomId: 'tachyon-instance-dev',
maxCapacity: 12,
})
expect(await voiceFor('974', instance.roomInstanceId)).toEqual({
voiceConnectionInfo: '198.51.100.12:7778',
voiceServerId: 'dev',
})
// Everyone else in that instance keeps the pool entry's own port and id.
expect(await voiceFor('975', instance.roomInstanceId)).toEqual({
voiceConnectionInfo: '198.51.100.12:7777',
voiceServerId: 'tachyon-1',
})
// In no instance there is no server to move the port of: a bare ':7778' would be
// an address that answers nothing, named `dev`.
const none = await exports.default.fetch(`${ORIGIN}/player/connection-info`, {
headers: await bearer('974'),
})
const body = (await none.json()) as {
value: { voiceConnectionInfo: string; voiceServerId: string }
}
expect(body.value.voiceConnectionInfo).toBe('')
expect(body.value.voiceServerId).toBe('')
} finally {
env.TACHYON_HOST_PORT = original
}
})
test('re-matchmaking into your current room returns a different instance (id must change)', async () => { test('re-matchmaking into your current room returns a different instance (id must change)', async () => {
// The client keys the room transition off a changing roomInstanceId; handing back // The client keys the room transition off a changing roomInstanceId; handing back
// the instance the player is already in hangs their join. RecCenter (cap 12) so // the instance the player is already in hangs their join. RecCenter (cap 12) so