// --------------------------------------------------------------------------- // useChannel + usePresence + useHistory — pre-built OddSockets realtime hooks // --------------------------------------------------------------------------- // PRE-INSTALLED in every React project (realtime leg of the tyga backend // bundle). Same stub->real-client-on-connect pattern as use-anydb/use-tygarun: // // import { useChannel, usePresence, useHistory } from "@/hooks/use-oddsockets" // const { messages, publish, connected } = useChannel("room:lobby") // const { members, updateState } = usePresence("room:lobby") // const { history, loading } = useHistory("room:lobby", { count: 50 }) // // Until OddSockets is connected, hooks run against the local demo bus in // lib/oddsockets.ts so realtime UIs render live. `connected` reflects // isConfigured() and `error` surfaces a broken real connect — the demo state // never silently masks a bad key. // --------------------------------------------------------------------------- import { useState, useEffect, useCallback, useRef } from "react"; import oddsockets, { isConfigured, type ChannelMessage, type PresenceMember } from "@/lib/oddsockets"; interface ChannelResult { messages: ChannelMessage[]; publish: (data: any) => Promise; connected: boolean; error: string | null; } /** * Subscribe to a realtime channel. Accumulates incoming messages and exposes a * publish() for sending. `connected` is true only when OddSockets is really * connected (false in demo mode). */ export function useChannel(channelName: string): ChannelResult { const [messages, setMessages] = useState([]); const [error, setError] = useState(null); const channelRef = useRef(oddsockets.channel(channelName)); useEffect(() => { const ch = oddsockets.channel(channelName); channelRef.current = ch; let off = () => {}; try { off = ch.subscribe((msg) => setMessages((prev) => [...prev, msg])); } catch (err: any) { setError(err.message || "Failed to subscribe"); console.error(`[useChannel] ${channelName}:`, err); } return () => { try { off(); } catch { /* noop */ } }; }, [channelName]); const publish = useCallback(async (data: any) => { setError(null); try { await channelRef.current.publish(data); } catch (err: any) { setError(err.message || "Failed to publish"); throw err; } }, []); return { messages, publish, connected: isConfigured(), error }; } interface PresenceResult { members: PresenceMember[]; updateState: (state: Record) => Promise; connected: boolean; error: string | null; } /** * Track presence on a channel (who's here + their state), with updateState() * to publish this client's own state (e.g. { typing: true }). */ export function usePresence(channelName: string): PresenceResult { const [members, setMembers] = useState([]); const [error, setError] = useState(null); const channelRef = useRef(oddsockets.channel(channelName)); const refresh = useCallback(async () => { try { const list = await channelRef.current.getPresence(); setMembers(list || []); } catch (err: any) { setError(err.message || "Failed to read presence"); } }, []); useEffect(() => { const ch = oddsockets.channel(channelName); channelRef.current = ch; let off = () => {}; try { // re-read presence on every channel message (join/leave/state changes) off = ch.subscribe(() => { refresh(); }); } catch (err: any) { setError(err.message || "Failed to subscribe to presence"); } refresh(); return () => { try { off(); } catch { /* noop */ } }; }, [channelName, refresh]); const updateState = useCallback(async (state: Record) => { setError(null); try { await channelRef.current.updateState(state); await refresh(); } catch (err: any) { setError(err.message || "Failed to update state"); throw err; } }, [refresh]); return { members, updateState, connected: isConfigured(), error }; } interface HistoryResult { history: ChannelMessage[]; loading: boolean; error: string | null; refetch: () => Promise; } /** * Fetch recent messages for a channel (backlog), with loading/error states. */ export function useHistory( channelName: string, options: { count?: number; start?: string; end?: string } = {} ): HistoryResult { const { count = 50, start, end } = options; const [history, setHistory] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const fetchHistory = useCallback(async () => { setLoading(true); setError(null); try { const ch = oddsockets.channel(channelName); const result = await ch.getHistory({ count, start, end }); setHistory(result || []); } catch (err: any) { setError(err.message || "Failed to fetch history"); console.error(`[useHistory] ${channelName}:`, err); } finally { setLoading(false); } }, [channelName, count, start, end]); useEffect(() => { fetchHistory(); }, [fetchHistory]); return { history, loading, error, refetch: fetchHistory }; }