mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
[rooms] fix friendy name and room update
This commit is contained in:
@@ -1507,7 +1507,9 @@ const app = new Hono<App>()
|
|||||||
description: [
|
description: [
|
||||||
'Owner-only (the room’s `CreatorAccountId` — co-owners cannot). An unknown room or a',
|
'Owner-only (the room’s `CreatorAccountId` — co-owners cannot). An unknown room or a',
|
||||||
'non-owner is HTTP 200 with `Success: false` and an `ErrorId`; only a missing token is',
|
'non-owner is HTTP 200 with `Success: false` and an `ErrorId`; only a missing token is',
|
||||||
'a real 401. An absent `description` field clears the description.',
|
'a real 401. An absent `description` field clears the description. Pushes a',
|
||||||
|
'`RoomUpdate` to the owner — this envelope carries no room, so the push is the only',
|
||||||
|
'thing that tells their client to redraw.',
|
||||||
].join(' '),
|
].join(' '),
|
||||||
security: AUTHED,
|
security: AUTHED,
|
||||||
parameters: [roomIdParam],
|
parameters: [roomIdParam],
|
||||||
@@ -1541,6 +1543,9 @@ const app = new Hono<App>()
|
|||||||
const body = (await c.req.parseBody().catch(() => ({}))) as Record<string, unknown>
|
const body = (await c.req.parseBody().catch(() => ({}))) as Record<string, unknown>
|
||||||
const description = typeof body.description === 'string' ? body.description : ''
|
const description = typeof body.description === 'string' ? body.description : ''
|
||||||
await setRoomDescription(c.env.DB, roomId, description)
|
await setRoomDescription(c.env.DB, roomId, description)
|
||||||
|
// Same reason as the rename below: the envelope carries no room, so without this
|
||||||
|
// the client redraws the room from what it already had — the old description.
|
||||||
|
await pushRoomUpdate(c, accountId, { ...room, Description: description })
|
||||||
return roomResult(c, { Success: true })
|
return roomResult(c, { Success: true })
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -1617,7 +1622,13 @@ const app = new Hono<App>()
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Writes `FriendlyName` too — see `setRoomName`. The two are the same string here,
|
||||||
|
// and the client labels the room from the display one.
|
||||||
await setRoomName(c.env.DB, roomId, name)
|
await setRoomName(c.env.DB, roomId, name)
|
||||||
|
// The rename answers a bare `{ Success }` with no room in it, so the client has
|
||||||
|
// nothing to re-render from and kept showing the old name until the push arrived.
|
||||||
|
// Built from the room already in hand rather than re-read, like the image route's.
|
||||||
|
await pushRoomUpdate(c, accountId, { ...room, Name: name, FriendlyName: name })
|
||||||
return roomResult(c, { Success: true })
|
return roomResult(c, { Success: true })
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -1638,7 +1649,8 @@ const app = new Hono<App>()
|
|||||||
'this call TOGGLES: it adds the tag (Type 0) when absent and removes it when',
|
'this call TOGGLES: it adds the tag (Type 0) when absent and removes it when',
|
||||||
'present. The “main” tags (`pvp`/`quest`/`game`/`hangout`/`art`) behave as radio',
|
'present. The “main” tags (`pvp`/`quest`/`game`/`hangout`/`art`) behave as radio',
|
||||||
'buttons — setting one clears the others. Answers the lowercase envelope with the',
|
'buttons — setting one clears the others. Answers the lowercase envelope with the',
|
||||||
'updated room, which the client re-renders from.',
|
'updated room, which the client re-renders from, and pushes a `RoomUpdate` to the',
|
||||||
|
'owner for their other sessions.',
|
||||||
].join(' '),
|
].join(' '),
|
||||||
security: AUTHED,
|
security: AUTHED,
|
||||||
parameters: [roomIdParam],
|
parameters: [roomIdParam],
|
||||||
@@ -1665,6 +1677,9 @@ const app = new Hono<App>()
|
|||||||
if (tag === '') return roomEnvelope(c, null, 'You must provide a tag!')
|
if (tag === '') return roomEnvelope(c, null, 'You must provide a tag!')
|
||||||
|
|
||||||
const updated = await toggleRoomTag(c.env.DB, roomId, room, tag)
|
const updated = await toggleRoomTag(c.env.DB, roomId, room, tag)
|
||||||
|
// This one DOES answer the updated room, so the caller's own client redraws from
|
||||||
|
// the response; the push is for their other sessions, as on every mutation below.
|
||||||
|
await pushRoomUpdate(c, accountId, updated)
|
||||||
return roomEnvelope(c, updated)
|
return roomEnvelope(c, updated)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
import { NotificationType } from '../../../../notify/src/notification-types'
|
import { NotificationType } from '../../../../notify/src/notification-types'
|
||||||
import importRooms from '../../../static/ImportRooms.json'
|
import importRooms from '../../../static/ImportRooms.json'
|
||||||
|
|
||||||
|
import type { Room } from '@repo/domain'
|
||||||
import type { Env } from '../../context'
|
import type { Env } from '../../context'
|
||||||
|
|
||||||
declare module 'cloudflare:test' {
|
declare module 'cloudflare:test' {
|
||||||
@@ -2677,8 +2678,68 @@ describe('rooms endpoints', () => {
|
|||||||
expect(await bodyOf(ok)).toMatchObject({ Success: true })
|
expect(await bodyOf(ok)).toMatchObject({ Success: true })
|
||||||
const room = (await (await SELF.fetch(`${ORIGIN}/rooms?name=RenamedCenter`)).json()) as {
|
const room = (await (await SELF.fetch(`${ORIGIN}/rooms?name=RenamedCenter`)).json()) as {
|
||||||
RoomId: number
|
RoomId: number
|
||||||
|
FriendlyName: string
|
||||||
}
|
}
|
||||||
expect(room.RoomId).toBe(2)
|
expect(room.RoomId).toBe(2)
|
||||||
|
// The DISPLAY name follows the rename. It is otherwise only defaulted to `Name` on
|
||||||
|
// read, so a room that had ever stored one would keep labelling itself with the old
|
||||||
|
// name while every name-keyed lookup used the new one.
|
||||||
|
expect(room.FriendlyName).toBe('RenamedCenter')
|
||||||
|
})
|
||||||
|
|
||||||
|
/** The hub stub records every notifyPlayer call — see vitest.config.ts. */
|
||||||
|
type SentUpdate = { playerId: number; notificationType: string | number; data: Room }
|
||||||
|
const notifyHub = () => env.RECFLARE_NOTIFICATIONS_HUB.getByName('global')
|
||||||
|
const resetNotifications = () => notifyHub().fetch('http://do/all', { method: 'DELETE' })
|
||||||
|
const sentNotifications = async (): Promise<SentUpdate[]> =>
|
||||||
|
(await (await notifyHub().fetch('http://do/all')).json()) as SentUpdate[]
|
||||||
|
|
||||||
|
it('a rename pushes a RoomUpdate to the owner carrying the new name', async () => {
|
||||||
|
// The rename answers a bare `{ Success }` with no room in it, so the push is the only
|
||||||
|
// thing that tells the client to redraw — without it the old name stays on screen.
|
||||||
|
await resetNotifications()
|
||||||
|
expect(
|
||||||
|
await bodyOf(await putForm('/rooms/2/name', { name: 'PushedRename' }, '1'))
|
||||||
|
).toMatchObject({ Success: true })
|
||||||
|
|
||||||
|
const sent = await sentNotifications()
|
||||||
|
expect(sent).toHaveLength(1)
|
||||||
|
// RoomUpdate, to the OWNER, carrying the room as it now stands — both names.
|
||||||
|
expect(sent[0].playerId).toBe(1)
|
||||||
|
expect(sent[0].notificationType).toBe(NotificationType.SubscriptionUpdateRoom)
|
||||||
|
expect(sent[0].data).toMatchObject({
|
||||||
|
RoomId: 2,
|
||||||
|
Name: 'PushedRename',
|
||||||
|
FriendlyName: 'PushedRename',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Put it back for the tests that read room 2 by name.
|
||||||
|
await putForm('/rooms/2/name', { name: 'RenamedCenter' }, '1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('a description edit and a tag toggle push a RoomUpdate too', async () => {
|
||||||
|
// Same reason for the description: its envelope carries no room either.
|
||||||
|
await resetNotifications()
|
||||||
|
await putForm('/rooms/2/description', { description: 'Pushed description' }, '1')
|
||||||
|
const afterDescription = await sentNotifications()
|
||||||
|
expect(afterDescription).toHaveLength(1)
|
||||||
|
expect(afterDescription[0].data).toMatchObject({
|
||||||
|
RoomId: 2,
|
||||||
|
Description: 'Pushed description',
|
||||||
|
})
|
||||||
|
|
||||||
|
// The tag toggle DOES answer the updated room, so its push is for the owner's other
|
||||||
|
// sessions rather than for the caller's own redraw.
|
||||||
|
await resetNotifications()
|
||||||
|
await putForm('/rooms/2/tags', { tag: 'pushedtag' }, '1')
|
||||||
|
const afterTag = await sentNotifications()
|
||||||
|
expect(afterTag).toHaveLength(1)
|
||||||
|
expect(afterTag[0].playerId).toBe(1)
|
||||||
|
const tags = afterTag[0].data.Tags as Array<{ Tag: string }>
|
||||||
|
expect(tags.map((t) => t.Tag)).toContain('pushedtag')
|
||||||
|
|
||||||
|
// Toggle it back off — the same call removes it.
|
||||||
|
await putForm('/rooms/2/tags', { tag: 'pushedtag' }, '1')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('room_instance: create + read round-trips and hides JsonIgnore fields', async () => {
|
it('room_instance: create + read round-trips and hides JsonIgnore fields', async () => {
|
||||||
|
|||||||
@@ -361,10 +361,23 @@ export async function setRoomDescription(
|
|||||||
.run()
|
.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Set a room's Name in place (the caller checks ownership + name uniqueness first). */
|
/**
|
||||||
|
* Set a room's Name in place (the caller checks ownership + name uniqueness first).
|
||||||
|
*
|
||||||
|
* Writes `FriendlyName` to the same string. That is the DISPLAY name — what the client
|
||||||
|
* labels the room with — and it is only defaulted to `Name` on read
|
||||||
|
* ({@link attachRoomDtoDefaults}), with `??=`, so a room whose blob has ever carried one
|
||||||
|
* keeps it. Renaming without this leaves that room displaying its old name forever while
|
||||||
|
* every name-keyed lookup uses the new one.
|
||||||
|
*
|
||||||
|
* The reference lets a creator set a display name apart from the unique `Name`; nothing
|
||||||
|
* here exposes that, so the two are kept in step rather than allowed to diverge silently.
|
||||||
|
*/
|
||||||
export async function setRoomName(db: D1Database, roomId: number, name: string): Promise<void> {
|
export async function setRoomName(db: D1Database, roomId: number, name: string): Promise<void> {
|
||||||
await db
|
await db
|
||||||
.prepare("UPDATE room SET data = json_set(data, '$.Name', ?2) WHERE room_id = ?1")
|
.prepare(
|
||||||
|
"UPDATE room SET data = json_set(data, '$.Name', ?2, '$.FriendlyName', ?2) WHERE room_id = ?1"
|
||||||
|
)
|
||||||
.bind(roomId, name)
|
.bind(roomId, name)
|
||||||
.run()
|
.run()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user