mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 06:31:27 -07:00
[match] better invites
This commit is contained in:
@@ -6,6 +6,7 @@ export * from './password'
|
||||
export * from './rooms-db'
|
||||
export * from './room-instance-db'
|
||||
export * from './room-comments-db'
|
||||
export * from './room-invites-db'
|
||||
export * from './presence-db'
|
||||
export * from './gifts-db'
|
||||
export * from './inventory-invention-db'
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Room invites on the shared `recflare` D1 database — one row per game invite a player
|
||||
* sends another ("come join me in this room"), as `POST /invite` on the `match` worker
|
||||
* creates them.
|
||||
*
|
||||
* The invite that reaches the invitee is a live notification, not a row: `match` pushes a
|
||||
* `MessageReceived` frame the moment the invite is created, and the client renders the join
|
||||
* prompt straight off it. This table exists so the invite has an ID OF ITS OWN —
|
||||
* `RoomInviteId`, which the create response hands back — and so an invite can be looked up
|
||||
* or expired after the fact rather than vanishing with the socket frame.
|
||||
*
|
||||
* The `match` worker owns this schema/migration
|
||||
* (`apps/match/migrations/0001_room_invite.sql`, applied under its own `migrations_table`
|
||||
* so it doesn't clash with the other workers' migrations that share the database).
|
||||
* `ROOM_INVITE_SCHEMA_DDL` mirrors that migration so tests can build the table directly.
|
||||
*/
|
||||
|
||||
/** Schema DDL (mirror of apps/match/migrations/0001_room_invite.sql). */
|
||||
export const ROOM_INVITE_SCHEMA_DDL: string[] = [
|
||||
// `room_invite_id` is AUTOINCREMENT rather than a bare rowid alias: the id is handed to
|
||||
// the client, and expiring old invites deletes rows, so a reused id would point a
|
||||
// client's stale invite at somebody else's.
|
||||
//
|
||||
// `room_id` is nullable because the invite is: the caller names a room INSTANCE, and one
|
||||
// that has already died (or was never real) leaves the invite with nothing to resolve —
|
||||
// `match` sends it anyway, with a null RoomId, so the row records the same thing.
|
||||
//
|
||||
// `created_at` is epoch SECONDS, like `presence.expires_at` on the same database and for
|
||||
// the same reason: the sweep that will expire these compares it against `Date.now()/1000`
|
||||
// in SQL, and an integer compare needs no parsing. Indexed for that sweep.
|
||||
`CREATE TABLE IF NOT EXISTS room_invite (
|
||||
room_invite_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
from_player_id INTEGER NOT NULL,
|
||||
to_player_id INTEGER NOT NULL,
|
||||
room_id INTEGER,
|
||||
created_at INTEGER NOT NULL
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_room_invite_created ON room_invite (created_at)`,
|
||||
]
|
||||
|
||||
/**
|
||||
* One invite as the client reads it — PascalCase, and the whole of what `POST /invite`
|
||||
* answers with.
|
||||
*/
|
||||
export interface RoomInvite {
|
||||
RoomInviteId: number
|
||||
FromPlayerId: number
|
||||
ToPlayerId: number
|
||||
RoomId: number | null
|
||||
}
|
||||
|
||||
interface RoomInviteRow {
|
||||
room_invite_id: number
|
||||
from_player_id: number
|
||||
to_player_id: number
|
||||
room_id: number | null
|
||||
}
|
||||
|
||||
const SELECT_COLUMNS = `room_invite_id, from_player_id, to_player_id, room_id`
|
||||
|
||||
const nowSeconds = () => Math.floor(Date.now() / 1000)
|
||||
|
||||
/**
|
||||
* Record an invite from `fromPlayerId` to `toPlayerId` for a room, returning it as the
|
||||
* client reads it back. `roomId` is null when the caller's room instance didn't resolve.
|
||||
*
|
||||
* The caller still has to deliver the invite (the `MessageReceived` frame); this only
|
||||
* mints the row and its id.
|
||||
*/
|
||||
export async function createRoomInvite(
|
||||
db: D1Database,
|
||||
fromPlayerId: number,
|
||||
toPlayerId: number,
|
||||
roomId: number | null
|
||||
): Promise<RoomInvite | null> {
|
||||
const row = await db
|
||||
.prepare(
|
||||
`INSERT INTO room_invite (from_player_id, to_player_id, room_id, created_at)
|
||||
VALUES (?1, ?2, ?3, ?4)
|
||||
RETURNING ${SELECT_COLUMNS}`
|
||||
)
|
||||
.bind(fromPlayerId, toPlayerId, roomId, nowSeconds())
|
||||
.first<RoomInviteRow>()
|
||||
|
||||
if (!row) return null
|
||||
return {
|
||||
RoomInviteId: row.room_invite_id,
|
||||
FromPlayerId: row.from_player_id,
|
||||
ToPlayerId: row.to_player_id,
|
||||
RoomId: row.room_id,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user