bake canonical build harness (deterministic /ship for forks) [photography]

This commit is contained in:
vibeasite-bot
2026-08-22 06:22:02 +01:00
parent fe4e8590c4
commit 55a09ffd20
77 changed files with 6553 additions and 284 deletions
+161
View File
@@ -0,0 +1,161 @@
// ---------------------------------------------------------------------------
// 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<void>;
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<ChannelMessage[]>([]);
const [error, setError] = useState<string | null>(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<string, any>) => Promise<void>;
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<PresenceMember[]>([]);
const [error, setError] = useState<string | null>(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<string, any>) => {
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<void>;
}
/**
* 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<ChannelMessage[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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 };
}