| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495 |
- "use client";
- import { useCallback } from "react";
- import type { RealtimePostgresChangesPayload } from "@supabase/supabase-js";
- import { useQueryClient, type InfiniteData } from "@tanstack/react-query";
- import type { Database } from "@/types/database";
- import { useRealtimeChannel } from "./use-realtime-channel";
- import { buildEqFilter } from "@/lib/realtime/subscription-manager";
- import { useCurrentUser } from "@/hooks/use-current-user";
- type MovieRow = Database["public"]["Tables"]["movies"]["Row"];
- function moviesQueryKey(groupId: string): readonly [string, string] {
- return ["group-movies", groupId] as const;
- }
- /**
- * Hook that subscribes to Supabase Realtime changes on the movies table
- * for a specific group_id. Updates the TanStack Query cache directly
- * on INSERT, UPDATE, and DELETE events.
- */
- export function useRealtimeMovies(groupId: string | null) {
- const queryClient = useQueryClient();
- // Use the current user id so realtime events can dual-invalidate
- // the cross-list ["all-user-movies", userId] query key alongside the
- // per-group ["group-movies", groupId] cache update.
- const { data: currentUser } = useCurrentUser();
- const userId = currentUser?.id ?? null;
- const handlePayload = useCallback(
- (payload: RealtimePostgresChangesPayload<MovieRow>) => {
- if (!groupId) return;
- const key = moviesQueryKey(groupId);
- if (payload.eventType === "INSERT") {
- const newMovie = payload.new;
- queryClient.setQueryData<InfiniteData<MovieRow[]>>(key, (old) => {
- if (!old) return undefined;
- // Avoid duplicates across all pages
- if (old.pages.some((page) => page.some((m) => m.id === newMovie.id))) return old;
- // Prepend to first page (ordered by added_at DESC)
- return {
- pages: [[newMovie, ...old.pages[0]], ...old.pages.slice(1)],
- pageParams: old.pageParams,
- };
- });
- if (userId) {
- void queryClient.invalidateQueries({ queryKey: ["all-user-movies", userId] });
- }
- } else if (payload.eventType === "UPDATE") {
- const updated = payload.new;
- queryClient.setQueryData<InfiniteData<MovieRow[]>>(key, (old) => {
- if (!old) return undefined;
- return {
- pages: old.pages.map((page) => page.map((m) => (m.id === updated.id ? updated : m))),
- pageParams: old.pageParams,
- };
- });
- if (userId) {
- void queryClient.invalidateQueries({ queryKey: ["all-user-movies", userId] });
- }
- } else if (payload.eventType === "DELETE") {
- const deleted = payload.old;
- if (deleted && "id" in deleted) {
- queryClient.setQueryData<InfiniteData<MovieRow[]>>(key, (old) => {
- if (!old) return undefined;
- return {
- pages: old.pages.map((page) => page.filter((m) => m.id !== deleted.id)),
- pageParams: old.pageParams,
- };
- });
- if (userId) {
- void queryClient.invalidateQueries({ queryKey: ["all-user-movies", userId] });
- }
- }
- }
- },
- [groupId, queryClient, userId],
- );
- const { status } = useRealtimeChannel<MovieRow>({
- channelName: groupId ? `movies:${groupId}` : null,
- config: {
- event: "*",
- schema: "public",
- table: "movies",
- filter: groupId ? buildEqFilter("group_id", groupId) : undefined,
- onPayload: handlePayload,
- },
- enabled: !!groupId,
- });
- return { status };
- }
|