mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-25 23:18:51 -07:00
[rooms] fix slow engagement counters
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
-- Cheer and favorite counters on `room`, as columns — the cheer/favorite twins of
|
||||
-- `visits` (0011).
|
||||
--
|
||||
-- Generated from packages/domain/src/rooms-db.ts (ROOM_SCHEMA_DDL) — keep in sync.
|
||||
--
|
||||
-- Until now `Stats.CheerCount`/`FavoriteCount` were DERIVED on every read by aggregating
|
||||
-- the per-player `interaction` rows (`SELECT room_id, SUM(cheered), SUM(favorited) …
|
||||
-- GROUP BY room_id`). 0021's covering index made the per-room form cheap, but the browse
|
||||
-- and discovery feeds rank EVERY listable room, so they ran the unfiltered form — a walk
|
||||
-- of the whole index per call — and the discovery page runs the hot feed once per
|
||||
-- category row. That was the bulk of the database's reads.
|
||||
--
|
||||
-- Now each counter is kept current by the toggles themselves: the same batch that flips a
|
||||
-- player's `interaction.cheered`/`favorited` applies a `cheers = cheers ± 1` to the room,
|
||||
-- so a cheer still shows immediately and concurrent toggles can't lose each other. The
|
||||
-- per-player rows stay the record of WHO cheered; the columns are the count of them.
|
||||
--
|
||||
-- The backfill below is the one remaining full aggregate, run once here. `room_id` is a
|
||||
-- virtual generated column, so the correlated subquery resolves it per row; the sums come
|
||||
-- off `idx_interaction_room` without touching the table.
|
||||
|
||||
ALTER TABLE room ADD COLUMN cheers INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE room ADD COLUMN favorites INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
UPDATE room SET
|
||||
cheers = (SELECT COALESCE(SUM(cheered), 0) FROM interaction WHERE interaction.room_id = room.room_id),
|
||||
favorites = (SELECT COALESCE(SUM(favorited), 0) FROM interaction WHERE interaction.room_id = room.room_id);
|
||||
@@ -149,11 +149,12 @@ export const RoomTagDto = z.object({
|
||||
})
|
||||
|
||||
/**
|
||||
* A room's engagement counters. `CheerCount`/`FavoriteCount` are aggregated from the
|
||||
* per-player `interaction` rows on every read. `VisitCount` is the room's lifetime
|
||||
* visits — the `room.visits` column, bumped by the `match` worker on every successful
|
||||
* matchmake into the room. Nothing records distinct visitors, so `VisitorCount` stays
|
||||
* at 0.
|
||||
* A room's engagement counters. `CheerCount`/`FavoriteCount` are the `room.cheers`/
|
||||
* `favorites` columns, moved by the cheer/favorite toggles as they flip the per-player
|
||||
* `interaction` row (so a count is how many players currently have the flag set).
|
||||
* `VisitCount` is the room's lifetime visits — the `room.visits` column, bumped by the
|
||||
* `match` worker on every successful matchmake into the room. Nothing records distinct
|
||||
* visitors, so `VisitorCount` stays at 0.
|
||||
*/
|
||||
export const RoomStatsDto = z.object({
|
||||
CheerCount: z.int(),
|
||||
|
||||
@@ -4769,7 +4769,7 @@ describe('rooms endpoints', () => {
|
||||
expect(otherGet).toMatchObject({ Cheered: false, Favorited: false })
|
||||
})
|
||||
|
||||
it('room Stats aggregate cheers/favorites from the interaction table', async () => {
|
||||
it('room Stats count cheers/favorites on the room row, kept by the toggles', async () => {
|
||||
type Stats = {
|
||||
CheerCount: number
|
||||
FavoriteCount: number
|
||||
@@ -4818,6 +4818,21 @@ describe('rooms endpoints', () => {
|
||||
VisitCount: 0,
|
||||
})
|
||||
|
||||
// The counts are columns on the room, not an aggregate taken on read — the toggles
|
||||
// keep them. A repeated clear changes nothing (the flag was already off), and neither
|
||||
// does clearing a flag a player never set, so neither counts down again.
|
||||
await interact('562', 'cheer', 'DELETE')
|
||||
await interact('563', 'favorite', 'DELETE')
|
||||
expect(await direct()).toMatchObject({ CheerCount: 1, FavoriteCount: 1 })
|
||||
expect(
|
||||
await env.DB.prepare('SELECT cheers, favorites FROM room WHERE room_id = 15').first()
|
||||
).toEqual({ cheers: 1, favorites: 1 })
|
||||
// Toggling off and on again lands back where it started.
|
||||
await interact('561', 'cheer', 'PUT')
|
||||
expect(await direct()).toMatchObject({ CheerCount: 0 })
|
||||
await interact('561', 'cheer', 'PUT')
|
||||
expect(await direct()).toMatchObject({ CheerCount: 1 })
|
||||
|
||||
// VisitCount is the `room.visits` column (what match bumps on each matchmake),
|
||||
// served on every read of the room — here and in the search results — and it
|
||||
// survives the cheer/favorite aggregation rather than being zeroed by it.
|
||||
|
||||
Reference in New Issue
Block a user