| "use client"; |
|
|
| import { createClient } from "@midday/supabase/client"; |
| import type { Database } from "@midday/supabase/types"; |
| import type { |
| RealtimeChannel, |
| RealtimePostgresChangesPayload, |
| } from "@supabase/supabase-js"; |
| import { useEffect, useRef } from "react"; |
|
|
| type PublicSchema = Database[Extract<keyof Database, "public">]; |
| type Tables = PublicSchema["Tables"]; |
| type TableName = keyof Tables; |
|
|
| type EventType = "INSERT" | "UPDATE" | "DELETE"; |
|
|
| interface UseRealtimeProps<TN extends TableName> { |
| channelName: string; |
| |
| events?: EventType[]; |
| table: TN; |
| filter?: string; |
| onEvent: (payload: RealtimePostgresChangesPayload<Tables[TN]["Row"]>) => void; |
| } |
|
|
| |
| let supabaseClient: ReturnType<typeof createClient> | null = null; |
|
|
| function getSupabaseClient() { |
| if (!supabaseClient) { |
| supabaseClient = createClient(); |
| } |
| return supabaseClient; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function useRealtime<TN extends TableName>({ |
| channelName, |
| events = ["INSERT", "UPDATE"], |
| table, |
| filter, |
| onEvent, |
| }: UseRealtimeProps<TN>) { |
| const onEventRef = useRef(onEvent); |
| const channelRef = useRef<RealtimeChannel | null>(null); |
|
|
| |
| useEffect(() => { |
| onEventRef.current = onEvent; |
| }, [onEvent]); |
|
|
| useEffect(() => { |
| if (!filter) { |
| return; |
| } |
|
|
| const supabase = getSupabaseClient(); |
| let channel: RealtimeChannel | null = null; |
| let isCleanedUp = false; |
|
|
| const setupChannel = () => { |
| if (isCleanedUp) return; |
|
|
| channel = supabase.channel(channelName); |
| channelRef.current = channel; |
|
|
| |
| if (events.includes("INSERT")) { |
| channel.on( |
| "postgres_changes", |
| { event: "INSERT", schema: "public", table, filter }, |
| (payload) => |
| onEventRef.current( |
| payload as RealtimePostgresChangesPayload<Tables[TN]["Row"]>, |
| ), |
| ); |
| } |
| if (events.includes("UPDATE")) { |
| channel.on( |
| "postgres_changes", |
| { event: "UPDATE", schema: "public", table, filter }, |
| (payload) => |
| onEventRef.current( |
| payload as RealtimePostgresChangesPayload<Tables[TN]["Row"]>, |
| ), |
| ); |
| } |
| if (events.includes("DELETE")) { |
| channel.on( |
| "postgres_changes", |
| { event: "DELETE", schema: "public", table, filter }, |
| (payload) => |
| onEventRef.current( |
| payload as RealtimePostgresChangesPayload<Tables[TN]["Row"]>, |
| ), |
| ); |
| } |
|
|
| channel.subscribe((status, err) => { |
| if (status === "CHANNEL_ERROR") { |
| console.error(`[Realtime] Channel error for ${channelName}:`, err); |
| } else if (status === "TIMED_OUT") { |
| console.warn(`[Realtime] Subscription timed out for ${channelName}`); |
| } |
| }); |
| }; |
|
|
| |
| if (channelRef.current) { |
| supabase.removeChannel(channelRef.current).then(() => { |
| channelRef.current = null; |
| setupChannel(); |
| }); |
| } else { |
| setupChannel(); |
| } |
|
|
| return () => { |
| isCleanedUp = true; |
| if (channel) { |
| supabase.removeChannel(channel); |
| } |
| channelRef.current = null; |
| }; |
| |
| |
| }, [channelName, table, filter]); |
| } |
|
|