From f18b7271c4f2861a4e9db02eeaee67f1bb3e5e8a Mon Sep 17 00:00:00 2001 From: Pratik Bodkhe Date: Sat, 6 Jun 2026 12:52:04 +0900 Subject: [PATCH] fix(realtime): drop redundant 2s meeting poll already covered by realtime useMeetingRealtime opened a Supabase realtime channel on postgres_changes for meetings, issues, and decisions, then also ran setInterval(refreshMeeting, 2000). The timer re-invalidated five query keys every two seconds, producing a storm of REST refetches on every open meeting page (visible as endless repeating meetings/issues/meeting_series/series_participants requests in the network tab). All three tables are already in the supabase_realtime publication with REPLICA IDENTITY FULL (20260531140000_realtime_series_participants.sql), so realtime delivers every change the poll was catching. Remove the interval; keep the channel. Add a regression test asserting an idle meeting page issues no timer-driven /rest/v1/meetings refetches. --- e2e/regression/realtime-collaboration.spec.ts | 43 ++++++++++++++++++- src/lib/hooks/use-meetings.ts | 2 - 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/e2e/regression/realtime-collaboration.spec.ts b/e2e/regression/realtime-collaboration.spec.ts index af1aa78..7b7b40d 100644 --- a/e2e/regression/realtime-collaboration.spec.ts +++ b/e2e/regression/realtime-collaboration.spec.ts @@ -1,5 +1,5 @@ import { randomUUID } from "node:crypto"; -import { test, expect, type APIRequestContext, type Browser } from "@playwright/test"; +import { test, expect, type APIRequestContext, type Browser, type Request } from "@playwright/test"; const APP_URL = process.env.PLAYWRIGHT_BASE_URL ?? "http://localhost:3000"; const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL ?? process.env.SUPABASE_URL ?? "http://127.0.0.1:54321"; @@ -293,4 +293,45 @@ test.describe("Realtime collaboration and series participation", () => { await deleteAuthUser(request, participantId); } }); + + test("meeting page relies on realtime, not a 2s polling timer", async ({ + browser, + request, + }) => { + const orgId = await getCurrentOrgId(request); + const seriesId = await createSeries(request, orgId, `No-poll meeting ${Date.now()}`); + const meetingId = await createMeeting(request, seriesId, "live"); + + try { + await addSeriesParticipant(request, seriesId, TEST_USER_ID, "owner"); + const { context, page } = await newAuthedPage(browser, request, TEST_USER_EMAIL); + + try { + await page.goto(`${APP_URL}/series/${seriesId}/meetings/${meetingId}`); + await expect(page.getByText("Live").first()).toBeVisible(); + + // Let the initial load and any refetch-on-mount settle. + await page.waitForTimeout(1500); + + // Count meeting refetches during a quiet, idle window. The removed + // setInterval(refreshMeeting, 2000) fired roughly twice in 5s, each + // invalidating the meeting detail and list queries, so the old code + // produced several /rest/v1/meetings refetches here. Realtime is + // event-driven, so an idle page must issue none. + let meetingFetches = 0; + const onRequest = (req: Request) => { + if (req.url().includes("/rest/v1/meetings?select=")) meetingFetches += 1; + }; + page.on("request", onRequest); + await page.waitForTimeout(5000); + page.off("request", onRequest); + + expect(meetingFetches).toBeLessThan(2); + } finally { + await context.close(); + } + } finally { + await rest(request, `meeting_series?id=eq.${seriesId}`, { method: "DELETE", headers: serviceHeaders("return=minimal") }).catch(() => undefined); + } + }); }); diff --git a/src/lib/hooks/use-meetings.ts b/src/lib/hooks/use-meetings.ts index 01ecebd..fa8a25a 100644 --- a/src/lib/hooks/use-meetings.ts +++ b/src/lib/hooks/use-meetings.ts @@ -231,10 +231,8 @@ export function useMeetingRealtime(meetingId: string, seriesId: string) { refreshMeeting ) .subscribe(); - const interval = window.setInterval(refreshMeeting, 2000); return () => { - window.clearInterval(interval); void supabase.removeChannel(channel); }; }, [meetingId, queryClient, seriesId]);