BATSHIT_REPORT.md: full context on bulk query batching — business goal, approach, all changes, verification status, and how to run. FRONTEND_TEST_RESEARCH.md: research on unit testing react-query hooks with jest.mock, renderHook, and batcher testing patterns.
7.3 KiB
Batch Room Meeting Status Queries — Handoff Report
Business Goal
The rooms list page (/rooms) fires 2N+2 HTTP requests for N rooms. Each room card renders a MeetingStatus component that independently calls two hooks:
useRoomActiveMeetings(roomName)→GET /v1/rooms/{room_name}/meetings/activeuseRoomUpcomingMeetings(roomName)→GET /v1/rooms/{room_name}/meetings/upcoming
Each of those endpoints internally does a room lookup by name (LIMIT 1) plus a data query. For 10 rooms, that's 22 HTTP requests and 20 DB queries on page load. This is a classic N+1 problem at the API layer.
Goal: collapse all per-room meeting status queries into a single bulk POST request.
Approach: DataLoader-style Batching
We use @yornaath/batshit — a lightweight DataLoader pattern for JS. It collects individual .fetch(roomName) calls within a 10ms window, deduplicates them, and dispatches one bulk request. Each caller gets back only their slice of the response.
This is isomorphic: removing the batcher and reverting the hooks to direct API calls would still work. The backend bulk endpoint is additive, the individual endpoints remain untouched.
What Was Built
Branch
fix-room-query-batching (worktree at .worktrees/fix-room-query-batching/)
Backend Changes
New bulk DB methods (3 files):
| File | Method | Purpose |
|---|---|---|
server/reflector/db/rooms.py |
RoomController.get_by_names(names) |
Fetch rooms by name list using IN clause |
server/reflector/db/meetings.py |
MeetingController.get_all_active_for_rooms(room_ids, current_time) |
Active meetings for multiple rooms, one query |
server/reflector/db/calendar_events.py |
CalendarEventController.get_upcoming_for_rooms(room_ids, minutes_ahead) |
Upcoming events for multiple rooms, one query |
New endpoint in server/reflector/views/rooms.py:
POST /v1/rooms/meetings/bulk-status
Body: { "room_names": ["room-a", "room-b", ...] }
Response: { "room-a": { "active_meetings": [...], "upcoming_events": [...] }, ... }
- 3 total DB queries regardless of room count (rooms lookup, active meetings, upcoming events)
- Auth masking applied: non-owners get
host_room_url=""for Whereby,description=null/attendees=nullfor calendar events - Registered before
/{room_id}route to avoid path conflict - Request/response models:
BulkStatusRequest,RoomMeetingStatus
Frontend Changes
New dependency: @yornaath/batshit (added to www/package.json)
New file: www/app/lib/meetingStatusBatcher.ts
export const meetingStatusBatcher = create({
fetcher: async (roomNames: string[]) => {
const unique = [...new Set(roomNames)];
const { data } = await client.POST("/v1/rooms/meetings/bulk-status", {
body: { room_names: unique },
});
return roomNames.map((name) => ({
roomName: name,
active_meetings: data?.[name]?.active_meetings ?? [],
upcoming_events: data?.[name]?.upcoming_events ?? [],
}));
},
resolver: keyResolver("roomName"),
scheduler: windowScheduler(10), // 10ms batching window
});
Modified hooks in www/app/lib/apiHooks.ts:
useRoomActiveMeetings— changed from$api.useQuery("get", "/v1/rooms/{room_name}/meetings/active", ...)touseQuery+meetingStatusBatcher.fetch(roomName)useRoomUpcomingMeetings— same patternuseRoomsCreateMeeting— cache invalidation updated from$api.queryOptions(...)tomeetingStatusKeys.active(roomName)- Added
meetingStatusKeysquery key factory:{ active: (name) => ["rooms", name, "meetings/active"], upcoming: (name) => ["rooms", name, "meetings/upcoming"] }
Cache invalidation compatibility: the new query keys contain "meetings/active" and "meetings/upcoming" as string elements, which the existing useMeetingDeactivate predicate matches via .includes(). No changes needed there.
Regenerated: www/app/reflector-api.d.ts from OpenAPI spec.
Files Modified
| File | Change |
|---|---|
server/reflector/db/rooms.py |
+get_by_names() |
server/reflector/db/meetings.py |
+get_all_active_for_rooms() |
server/reflector/db/calendar_events.py |
+get_upcoming_for_rooms() |
server/reflector/views/rooms.py |
+endpoint, +BulkStatusRequest, +RoomMeetingStatus, +imports (asyncio, defaultdict) |
www/package.json |
+@yornaath/batshit |
www/pnpm-lock.yaml |
Updated |
www/app/lib/meetingStatusBatcher.ts |
New file |
www/app/lib/apiHooks.ts |
Rewrote 2 hooks, added key factory, updated 1 invalidation |
www/app/reflector-api.d.ts |
Regenerated |
Verification Status
Tested:
- Backend lint (ruff): clean
- Backend tests: 351 passed, 8 skipped (2 pre-existing flaky tests unrelated to this work:
test_transcript_rtc_and_websocket,test_transcript_upload_file) - TypeScript type check (
tsc --noEmit): clean - OpenAPI spec: bulk-status endpoint present and correctly typed
- Pre-commit hooks: all passed
NOT tested (requires manual browser verification):
- Open rooms list page → Network tab shows single
POST /v1/rooms/meetings/bulk-statusinstead of 2N GETs - Active meeting badges render correctly per room
- Upcoming meeting indicators render correctly per room
- Single room page (
MeetingSelection.tsx) still works (batcher handles batch-of-1) - Meeting deactivation → cache invalidates and meeting status refreshes
- Creating a meeting → active meetings badge updates
Frontend Testing (Next Steps)
See FRONTEND_TEST_RESEARCH.md for a full research document on how to write unit tests for these hooks. Summary:
- Approach:
jest.mock()on module-levelapiClientandmeetingStatusBatcher,renderHook/waitForfrom@testing-library/react - Batcher testing: unit test batcher directly with mock
client.POST; test hooks with mock batcher module - New deps needed:
@testing-library/react,@testing-library/jest-dom,@testing-library/dom,jest-environment-jsdom - Key gotcha:
openapi-react-queryreconstructed from mock client to test actual integration, or mock$apimethods directly - Potential issues:
ts-jestv29 / Jest 30 compatibility, ESM handling foropenapi-react-query
How to Run
Backend (Docker, from worktree)
cd .worktrees/fix-room-query-batching
# Symlink env files (not in git)
ln -sf /path/to/main/server/.env server/.env
ln -sf /path/to/main/www/.env.local www/.env.local
ln -sf /path/to/main/www/.env www/.env
# Start services
docker compose up -d redis postgres server
Frontend (manual, per project convention)
cd .worktrees/fix-room-query-batching/www
pnpm install # if not done
pnpm dev
Backend Tests
cd .worktrees/fix-room-query-batching/server
REDIS_HOST=localhost CELERY_BROKER_URL=redis://localhost:6379/1 CELERY_RESULT_BACKEND=redis://localhost:6379/1 uv run pytest tests/ -q
Regenerate OpenAPI Types
Requires the backend server running on port 1250:
cd .worktrees/fix-room-query-batching/server
REDIS_HOST=localhost CELERY_BROKER_URL=redis://localhost:6379/1 CELERY_RESULT_BACKEND=redis://localhost:6379/1 \
uv run python -c "import json; from reflector.app import app; json.dump(app.openapi(), open('/tmp/openapi.json','w'))" 2>/dev/null
cd ../www
npx openapi-typescript /tmp/openapi.json -o ./app/reflector-api.d.ts