mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 22:51:30 -07:00
moving some more things to domains
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
export { RoomInstanceType, Accessibility, Role } from './enums'
|
||||
export * from './accounts-db'
|
||||
export * from './rooms-db'
|
||||
export * from './room-instance-db'
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* Room instances — live sessions of a room. Stored with the same JSON-blob pattern
|
||||
* as the rooms/accounts tables: the full instance is a JSON blob in `data`, and
|
||||
* every field is a SQLite generated (virtual) column extracted from it (snake_case
|
||||
* per the C# `[Column]` names). `id` is a sequential key held in the blob.
|
||||
*
|
||||
* The `rooms` worker owns the schema (migrations/0004_room_instance.sql); the
|
||||
* `match` worker finds/creates instances here at matchmake time. This module is the
|
||||
* single source of truth for the helpers — both workers import it from
|
||||
* `@repo/domain`. Columns marked `[JsonIgnore]` in the C# (owner_account_id,
|
||||
* data_blob, allow_new_users, join_disabled) live in the blob but are dropped from
|
||||
* the client DTO (`toDto`).
|
||||
*/
|
||||
|
||||
/** Schema DDL (mirror of migrations/0004_room_instance.sql). */
|
||||
export const ROOM_INSTANCE_SCHEMA_DDL: string[] = [
|
||||
`CREATE TABLE IF NOT EXISTS room_instance (
|
||||
data TEXT NOT NULL,
|
||||
id INTEGER GENERATED ALWAYS AS (json_extract(data, '$.roomInstanceId')) VIRTUAL,
|
||||
owner_account_id INTEGER GENERATED ALWAYS AS (json_extract(data, '$.ownerAccountId')) VIRTUAL,
|
||||
room_id INTEGER GENERATED ALWAYS AS (json_extract(data, '$.roomId')) VIRTUAL,
|
||||
sub_room_id INTEGER GENERATED ALWAYS AS (json_extract(data, '$.subRoomId')) VIRTUAL,
|
||||
location TEXT GENERATED ALWAYS AS (json_extract(data, '$.location')) VIRTUAL,
|
||||
data_blob TEXT GENERATED ALWAYS AS (json_extract(data, '$.dataBlob')) VIRTUAL,
|
||||
event_id INTEGER GENERATED ALWAYS AS (json_extract(data, '$.eventId')) VIRTUAL,
|
||||
photon_region_id TEXT GENERATED ALWAYS AS (json_extract(data, '$.photonRegionId')) VIRTUAL,
|
||||
photon_room_id TEXT GENERATED ALWAYS AS (json_extract(data, '$.photonRoomId')) VIRTUAL,
|
||||
name TEXT GENERATED ALWAYS AS (json_extract(data, '$.name')) VIRTUAL,
|
||||
max_capacity INTEGER GENERATED ALWAYS AS (json_extract(data, '$.maxCapacity')) VIRTUAL,
|
||||
is_full INTEGER GENERATED ALWAYS AS (json_extract(data, '$.isFull')) VIRTUAL,
|
||||
is_private INTEGER GENERATED ALWAYS AS (json_extract(data, '$.isPrivate')) VIRTUAL,
|
||||
is_in_progress INTEGER GENERATED ALWAYS AS (json_extract(data, '$.isInProgress')) VIRTUAL,
|
||||
room_code TEXT GENERATED ALWAYS AS (json_extract(data, '$.roomCode')) VIRTUAL,
|
||||
room_instance_type INTEGER GENERATED ALWAYS AS (json_extract(data, '$.roomInstanceType')) VIRTUAL,
|
||||
club_id INTEGER GENERATED ALWAYS AS (json_extract(data, '$.clubId')) VIRTUAL,
|
||||
encrypt_voice_chat INTEGER GENERATED ALWAYS AS (json_extract(data, '$.EncryptVoiceChat')) VIRTUAL,
|
||||
matchmaking_policy INTEGER GENERATED ALWAYS AS (json_extract(data, '$.matchmakingPolicy')) VIRTUAL,
|
||||
allow_new_users INTEGER GENERATED ALWAYS AS (json_extract(data, '$.allowNewUsers')) VIRTUAL,
|
||||
join_disabled INTEGER GENERATED ALWAYS AS (json_extract(data, '$.joinDisabled')) VIRTUAL,
|
||||
created_at TEXT GENERATED ALWAYS AS (json_extract(data, '$.createdAt')) VIRTUAL
|
||||
)`,
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS idx_room_instance_id ON room_instance (id)`,
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS idx_room_instance_photon_room_id ON room_instance (photon_room_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_room_instance_room_id ON room_instance (room_id)`,
|
||||
]
|
||||
|
||||
/** Client-facing RoomInstance JSON (JsonPropertyName keys; JsonIgnore omitted). */
|
||||
export interface RoomInstanceDto {
|
||||
roomInstanceId: number
|
||||
roomId: number
|
||||
subRoomId: number
|
||||
location: string
|
||||
eventId: number
|
||||
photonRegionId: string
|
||||
photonRoomId: string
|
||||
name: string
|
||||
maxCapacity: number
|
||||
isFull: boolean
|
||||
isPrivate: boolean
|
||||
isInProgress: boolean
|
||||
roomCode: string
|
||||
roomInstanceType: number
|
||||
clubId: number
|
||||
// PascalCase JSON key, per the C# `[JsonPropertyName("EncryptVoiceChat")]`.
|
||||
EncryptVoiceChat: boolean
|
||||
matchmakingPolicy: number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
/** The full stored instance — the DTO plus the JsonIgnore fields (in the blob). */
|
||||
interface StoredRoomInstance extends RoomInstanceDto {
|
||||
ownerAccountId: number
|
||||
dataBlob: string
|
||||
allowNewUsers: boolean
|
||||
joinDisabled: boolean
|
||||
}
|
||||
|
||||
/** Fields for a new instance; `roomInstanceId` and `createdAt` are assigned here. */
|
||||
export interface NewRoomInstance {
|
||||
ownerAccountId: number
|
||||
roomId: number
|
||||
photonRoomId: string
|
||||
subRoomId?: number
|
||||
location?: string
|
||||
dataBlob?: string
|
||||
eventId?: number
|
||||
photonRegionId?: string
|
||||
name?: string
|
||||
maxCapacity?: number
|
||||
isFull?: boolean
|
||||
isPrivate?: boolean
|
||||
isInProgress?: boolean
|
||||
roomCode?: string
|
||||
roomInstanceType?: number
|
||||
clubId?: number
|
||||
encryptVoiceChat?: boolean
|
||||
matchmakingPolicy?: number
|
||||
allowNewUsers?: boolean
|
||||
joinDisabled?: boolean
|
||||
}
|
||||
|
||||
/** Project a stored instance to the client DTO (JsonIgnore fields dropped). */
|
||||
function toDto(s: StoredRoomInstance): RoomInstanceDto {
|
||||
return {
|
||||
roomInstanceId: s.roomInstanceId,
|
||||
roomId: s.roomId,
|
||||
subRoomId: s.subRoomId,
|
||||
location: s.location,
|
||||
eventId: s.eventId,
|
||||
photonRegionId: s.photonRegionId,
|
||||
photonRoomId: s.photonRoomId,
|
||||
name: s.name,
|
||||
maxCapacity: s.maxCapacity,
|
||||
isFull: s.isFull,
|
||||
isPrivate: s.isPrivate,
|
||||
isInProgress: s.isInProgress,
|
||||
roomCode: s.roomCode,
|
||||
roomInstanceType: s.roomInstanceType,
|
||||
clubId: s.clubId,
|
||||
EncryptVoiceChat: s.EncryptVoiceChat,
|
||||
matchmakingPolicy: s.matchmakingPolicy,
|
||||
createdAt: s.createdAt,
|
||||
}
|
||||
}
|
||||
|
||||
const parse = (data: string): StoredRoomInstance => JSON.parse(data) as StoredRoomInstance
|
||||
|
||||
/**
|
||||
* Ids start high (above 1_000_000) so an instance id never collides with the
|
||||
* dorm's fixed roomInstanceId of 1 — the client keys room transitions off the id,
|
||||
* so a room instance that returned 1 would look like "still in the dorm".
|
||||
*/
|
||||
const ID_BASE = 1_000_000
|
||||
|
||||
/** Insert a new room instance, returning it as a client DTO. */
|
||||
export async function createRoomInstance(
|
||||
db: D1Database,
|
||||
input: NewRoomInstance
|
||||
): Promise<RoomInstanceDto> {
|
||||
const idRow = await db
|
||||
.prepare(`SELECT COALESCE(MAX(id), ${ID_BASE}) + 1 AS next FROM room_instance`)
|
||||
.first<{ next: number }>()
|
||||
const stored: StoredRoomInstance = {
|
||||
roomInstanceId: idRow?.next ?? ID_BASE + 1,
|
||||
ownerAccountId: input.ownerAccountId,
|
||||
roomId: input.roomId,
|
||||
subRoomId: input.subRoomId ?? 0,
|
||||
location: input.location ?? '',
|
||||
dataBlob: input.dataBlob ?? '',
|
||||
eventId: input.eventId ?? 0,
|
||||
photonRegionId: input.photonRegionId ?? 'us',
|
||||
photonRoomId: input.photonRoomId,
|
||||
name: input.name ?? '',
|
||||
maxCapacity: input.maxCapacity ?? 0,
|
||||
isFull: input.isFull ?? false,
|
||||
isPrivate: input.isPrivate ?? false,
|
||||
isInProgress: input.isInProgress ?? false,
|
||||
roomCode: input.roomCode ?? '',
|
||||
roomInstanceType: input.roomInstanceType ?? 0,
|
||||
clubId: input.clubId ?? 0,
|
||||
EncryptVoiceChat: input.encryptVoiceChat ?? false,
|
||||
matchmakingPolicy: input.matchmakingPolicy ?? 0,
|
||||
allowNewUsers: input.allowNewUsers ?? true,
|
||||
joinDisabled: input.joinDisabled ?? false,
|
||||
createdAt: new Date().toISOString(),
|
||||
}
|
||||
await db.prepare('INSERT INTO room_instance (data) VALUES (?1)').bind(JSON.stringify(stored)).run()
|
||||
return toDto(stored)
|
||||
}
|
||||
|
||||
/** Look up a room instance by its id (roomInstanceId). */
|
||||
export async function getRoomInstance(db: D1Database, id: number): Promise<RoomInstanceDto | null> {
|
||||
const row = await db
|
||||
.prepare('SELECT data FROM room_instance WHERE id = ?1')
|
||||
.bind(id)
|
||||
.first<{ data: string }>()
|
||||
return row ? toDto(parse(row.data)) : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Flip an instance's `isInProgress` flag, rewriting the JSON blob (the generated
|
||||
* `is_in_progress` column follows it). Returns the updated DTO, or null when the
|
||||
* instance doesn't exist.
|
||||
*/
|
||||
export async function setRoomInstanceInProgress(
|
||||
db: D1Database,
|
||||
id: number,
|
||||
isInProgress: boolean
|
||||
): Promise<RoomInstanceDto | null> {
|
||||
const row = await db
|
||||
.prepare('SELECT data FROM room_instance WHERE id = ?1')
|
||||
.bind(id)
|
||||
.first<{ data: string }>()
|
||||
if (!row) return null
|
||||
const stored = parse(row.data)
|
||||
stored.isInProgress = isInProgress
|
||||
await db
|
||||
.prepare('UPDATE room_instance SET data = ?1 WHERE id = ?2')
|
||||
.bind(JSON.stringify(stored), id)
|
||||
.run()
|
||||
return toDto(stored)
|
||||
}
|
||||
|
||||
/**
|
||||
* The oldest joinable public instance of a room (not private, not full, joins
|
||||
* enabled, not already in progress), or null when there's none to join. Used by
|
||||
* matchmaking to reuse an existing instance before creating a new one.
|
||||
*/
|
||||
export async function getJoinableInstance(
|
||||
db: D1Database,
|
||||
roomId: number
|
||||
): Promise<RoomInstanceDto | null> {
|
||||
const row = await db
|
||||
.prepare(
|
||||
`SELECT data FROM room_instance
|
||||
WHERE room_id = ?1 AND is_private = 0 AND is_full = 0 AND join_disabled = 0
|
||||
AND is_in_progress = 0
|
||||
ORDER BY id LIMIT 1`
|
||||
)
|
||||
.bind(roomId)
|
||||
.first<{ data: string }>()
|
||||
return row ? toDto(parse(row.data)) : null
|
||||
}
|
||||
|
||||
/** All instances of a given room. */
|
||||
export async function getRoomInstancesByRoom(
|
||||
db: D1Database,
|
||||
roomId: number
|
||||
): Promise<RoomInstanceDto[]> {
|
||||
const { results } = await db
|
||||
.prepare('SELECT data FROM room_instance WHERE room_id = ?1')
|
||||
.bind(roomId)
|
||||
.all<{ data: string }>()
|
||||
return results.map((r) => toDto(parse(r.data)))
|
||||
}
|
||||
@@ -233,6 +233,83 @@ export async function saveSubRoomData(
|
||||
return room
|
||||
}
|
||||
|
||||
/** Fields from the client's subroom `modify` form (each applied only when supplied). */
|
||||
export interface ModifySubRoomInput {
|
||||
name?: string
|
||||
accessibility?: number
|
||||
maxPlayers?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a subroom's settings in place — its Name, Accessibility, and MaxPlayers
|
||||
* (the fields the client's subroom `modify` form carries). Only the supplied
|
||||
* fields are changed; the whole room JSON is rewritten (subrooms live in it).
|
||||
* Returns the updated room, or null when the room or subroom doesn't exist.
|
||||
*/
|
||||
export async function modifySubRoom(
|
||||
db: D1Database,
|
||||
roomId: number,
|
||||
subRoomId: number,
|
||||
input: ModifySubRoomInput
|
||||
): Promise<Room | null> {
|
||||
const room = await getRoomById(db, roomId)
|
||||
if (!room) return null
|
||||
const sub = findSubRoom(room, subRoomId)
|
||||
if (!sub) return null
|
||||
|
||||
if (input.name !== undefined) sub.Name = input.name
|
||||
if (input.accessibility !== undefined) sub.Accessibility = input.accessibility
|
||||
if (input.maxPlayers !== undefined) sub.MaxPlayers = input.maxPlayers
|
||||
|
||||
await db
|
||||
.prepare('UPDATE room SET data = ?2 WHERE room_id = ?1')
|
||||
.bind(roomId, JSON.stringify(room))
|
||||
.run()
|
||||
return room
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone an existing subroom into a new subroom of the same room, owned by
|
||||
* `accountId`. The copy keeps the source's scene/settings (and its saved data
|
||||
* blobs, so it loads identical content) but gets a fresh SubRoomId — the next
|
||||
* integer above the room's current subrooms. Returns the updated room and the new
|
||||
* subroom, or null when the room or source subroom doesn't exist.
|
||||
*/
|
||||
export async function cloneSubRoom(
|
||||
db: D1Database,
|
||||
roomId: number,
|
||||
subRoomId: number,
|
||||
accountId: number
|
||||
): Promise<{ room: Room; subRoom: Record<string, unknown> } | null> {
|
||||
const room = await getRoomById(db, roomId)
|
||||
if (!room) return null
|
||||
const source = findSubRoom(room, subRoomId)
|
||||
if (!source) return null
|
||||
|
||||
const subRooms = Array.isArray(room.SubRooms)
|
||||
? (room.SubRooms as Array<Record<string, unknown>>)
|
||||
: []
|
||||
const nextSubRoomId =
|
||||
subRooms.reduce((max, s) => {
|
||||
const id = typeof s.SubRoomId === 'number' ? s.SubRoomId : 0
|
||||
return id > max ? id : max
|
||||
}, 0) + 1
|
||||
|
||||
const subRoom: Record<string, unknown> = {
|
||||
...source,
|
||||
SubRoomId: nextSubRoomId,
|
||||
RoomId: room.RoomId,
|
||||
CreatorAccountId: accountId,
|
||||
}
|
||||
|
||||
room.SubRooms = [...subRooms, subRoom]
|
||||
await db
|
||||
.prepare('UPDATE room SET data = ?2 WHERE room_id = ?1')
|
||||
.bind(roomId, JSON.stringify(room))
|
||||
.run()
|
||||
return { room, subRoom }
|
||||
}
|
||||
|
||||
interface RoomRow {
|
||||
data: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user