fix: batch room meeting status queries via prop-drilling

Alternative to the batcher approach (#848): parent fetches all room
meeting statuses in a single bulk POST and passes data down as props.
No extra dependency (@yornaath/batshit), no implicit batching magic.

Backend: POST /v1/rooms/meetings/bulk-status + bulk DB methods.
Frontend: useRoomsBulkMeetingStatus hook in RoomList, MeetingStatus
receives data as props instead of calling per-room hooks.
CI: fix pnpm 8→10 auto-detect, add concurrency group.
Tests: Jest+jsdom+testing-library for bulk hook.
This commit is contained in:
Igor Loskutov
2026-02-05 20:04:31 -05:00
parent 1ce1c7a910
commit 083a50cbcd
13 changed files with 1253 additions and 54 deletions

View File

@@ -1,8 +1,8 @@
"use client";
import { $api } from "./apiClient";
import { $api, client } from "./apiClient";
import { useError } from "../(errors)/errorContext";
import { QueryClient, useQueryClient } from "@tanstack/react-query";
import { QueryClient, useQuery, useQueryClient } from "@tanstack/react-query";
import type { components } from "../reflector-api";
import { useAuth } from "./AuthProvider";
import { MeetingId } from "./types";
@@ -641,16 +641,21 @@ export function useMeetingDeactivate() {
setError(error as Error, "Failed to end meeting");
},
onSuccess: () => {
return queryClient.invalidateQueries({
predicate: (query) => {
const key = query.queryKey;
return key.some(
(k) =>
typeof k === "string" &&
!!MEETING_LIST_PATH_PARTIALS.find((e) => k.includes(e)),
);
},
});
return Promise.all([
queryClient.invalidateQueries({
predicate: (query) => {
const key = query.queryKey;
return key.some(
(k) =>
typeof k === "string" &&
!!MEETING_LIST_PATH_PARTIALS.find((e) => k.includes(e)),
);
},
}),
queryClient.invalidateQueries({
queryKey: ["bulk-meeting-status"],
}),
]);
},
});
}
@@ -707,6 +712,9 @@ export function useRoomsCreateMeeting() {
},
).queryKey,
}),
queryClient.invalidateQueries({
queryKey: ["bulk-meeting-status"],
}),
]);
},
onError: (error) => {
@@ -772,6 +780,32 @@ export function useRoomActiveMeetings(roomName: string | null) {
);
}
type RoomMeetingStatus = components["schemas"]["RoomMeetingStatus"];
export type BulkMeetingStatusMap = Record<string, RoomMeetingStatus>;
export function useRoomsBulkMeetingStatus(roomNames: string[]) {
const { isAuthenticated } = useAuthReady();
return useQuery({
queryKey: ["bulk-meeting-status", roomNames],
queryFn: async (): Promise<BulkMeetingStatusMap> => {
if (roomNames.length === 0) return {};
const { data, error } = await client.POST(
"/v1/rooms/meetings/bulk-status",
{ body: { room_names: roomNames } },
);
if (error || !data) {
throw new Error(
`bulk-status fetch failed: ${JSON.stringify(error ?? "no data")}`,
);
}
return data;
},
enabled: roomNames.length > 0 && isAuthenticated,
});
}
export function useRoomGetMeeting(
roomName: string | null,
meetingId: MeetingId | null,