move presence to d1

This commit is contained in:
Devin Zuczek
2026-07-11 12:37:22 -04:00
parent aad184181b
commit fb553c2fd0
16 changed files with 452 additions and 131 deletions
+5 -8
View File
@@ -10,6 +10,7 @@ import {
RoomInstanceType,
setLastLoginTime,
setPasswordHash,
setPresence,
} from '@repo/domain'
import { logger, withNotFound, withOnError } from '@repo/hono-helpers'
import { generateToken, TOKEN_TTL_SECONDS, validateAndGetAccountId } from '@repo/jwt'
@@ -48,16 +49,14 @@ const ORIENTATION_ROOM_ID = 13
* client treats presence as out-of-sync and bounces the player to the dorm.
*/
const ORIENTATION_INSTANCE_ID = -2
/** Presence TTL (s) — matches the match worker; refreshed by each heartbeat. */
const PRESENCE_TTL = 900
/**
* Seed a freshly created account's match presence to the Orientation room. The
* client is placed into Orientation by its new-user flow without a matchmake
* call, so the match heartbeat would otherwise report no/stale (dorm) presence
* and bounce the player out. We write the Orientation instance (built from the
* shared rooms D1, matching the match worker's `roomInstanceFromRoom` shape) so
* the heartbeat keeps them there.
* shared rooms D1, matching the match worker's `roomInstanceFromRoom` shape) into
* the shared `presence` table (see @repo/domain) so the heartbeat keeps them there.
*/
async function placeNewPlayerInOrientation(env: App['Bindings'], accountId: number): Promise<void> {
const row = await env.DB.prepare('SELECT data FROM room WHERE room_id = ?1')
@@ -92,16 +91,14 @@ async function placeNewPlayerInOrientation(env: App['Bindings'], accountId: numb
isInProgress: false,
EncryptVoiceChat: false,
}
const presence = {
await setPresence(env.DB, {
accountId,
roomInstance,
statusVisibility: 0,
deviceClass: 0,
vrMovementMode: 1,
platform: 0,
appVersion: '20230302',
}
await env.RECFLARE_MATCH_PRESENCE.put(`presence:${accountId}`, JSON.stringify(presence), {
expirationTtl: PRESENCE_TTL,
})
}
+3 -5
View File
@@ -3,12 +3,10 @@ import type { SharedHonoEnv, SharedHonoVariables } from '@repo/hono-helpers/src/
export type Env = SharedHonoEnv & {
// Shared rooms/accounts D1 database. The `auth` worker owns the `accounts`
// table (creates accounts on signup, seeds the system + Coach accounts).
// table (creates accounts on signup, seeds the system + Coach accounts). Also
// seeds new players' presence to the Orientation room (the shared `presence`
// table, see @repo/domain) so the match heartbeat keeps them there.
DB: D1Database
// Shared match-presence KV (owned by the `match` worker). On account creation
// the new player's presence is seeded to the Orientation room so the match
// heartbeat keeps them there instead of bouncing them to the dorm.
RECFLARE_MATCH_PRESENCE: KVNamespace
// Shared Secrets Store binding for the HS256 signing key. Resolve the value with
// `await env.JWT_SECRET.get()`. Every worker binds the same store, so tokens
// signed here verify in all of them. Provisioned via `wrangler secrets-store`;
+11 -5
View File
@@ -4,7 +4,7 @@ import { beforeAll, describe, expect, test } from 'vitest'
import '../../auth.app'
import { SCHEMA_DDL } from '@repo/domain'
import { PRESENCE_SCHEMA_DDL, SCHEMA_DDL } from '@repo/domain'
import { hashPassword } from '../../password'
import { REFRESH_SCHEMA_DDL } from '../../refresh-db'
@@ -32,6 +32,8 @@ beforeAll(async () => {
await adminSecretsStore(env.JWT_SECRET).create('test-signing-key')
for (const stmt of SCHEMA_DDL) await env.DB.prepare(stmt).run()
for (const stmt of REFRESH_SCHEMA_DDL) await env.DB.prepare(stmt).run()
// Presence table (owned by the rooms worker) — signup seeds the Orientation row.
for (const stmt of PRESENCE_SCHEMA_DDL) await env.DB.prepare(stmt).run()
// Seed the accounts the credential-login tests use, each with LOGIN_PASSWORD set.
const hash = await hashPassword(LOGIN_PASSWORD)
@@ -312,11 +314,15 @@ describe('auth worker routes', () => {
test('POST /connect/token create_account seeds the new player into Orientation', async () => {
const payload = await tokenFor('grant_type=create_account&platform_id=steam-456')
const sub = payload.sub as string
const presence = await env.RECFLARE_MATCH_PRESENCE.get<{
// Presence is written to the shared `presence` D1 table (account_id keyed).
const row = await env.DB.prepare('SELECT data FROM presence WHERE account_id = ?1')
.bind(Number(sub))
.first<{ data: string }>()
expect(row).not.toBeNull()
const presence = JSON.parse(row!.data) as {
roomInstance: { roomInstanceId: number; roomId: number; location: string; name: string }
}>(`presence:${sub}`, 'json')
expect(presence).not.toBeNull()
expect(presence!.roomInstance).toMatchObject({
}
expect(presence.roomInstance).toMatchObject({
roomInstanceId: -2,
roomId: 13,
location: ORIENTATION_SCENE,
-8
View File
@@ -18,14 +18,6 @@
"migrations_table": "d1_migrations_auth"
}
],
// Shared match-presence KV (owned by the `match` worker) — used to place new
// accounts into the Orientation room on signup.
"kv_namespaces": [
{
"binding": "RECFLARE_MATCH_PRESENCE",
"id": "local"
}
],
"logpush": false,
// Shared Secrets Store holding the HS256 JWT signing key. Every worker binds the
// same store as JWT_SECRET so tokens signed by `auth` verify here. The "local"