diff --git a/frontend/index.html b/frontend/index.html index 7cffd1b..a6c1bbc 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -20,8 +20,14 @@ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b2f1d94..200a232 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -9,6 +9,7 @@ import { Suspense, } from 'react'; import { api } from './api'; +import { takePrefetch } from './prefetch'; import { useWebSocket } from './useWebSocket'; import { useRepeaterMode, @@ -308,7 +309,7 @@ export function App() { // Fetch radio config (not sent via WebSocket) const fetchConfig = useCallback(async () => { try { - const data = await api.getRadioConfig(); + const data = await (takePrefetch('config') ?? api.getRadioConfig()); setConfig(data); } catch (err) { console.error('Failed to fetch config:', err); @@ -318,7 +319,7 @@ export function App() { // Fetch app settings const fetchAppSettings = useCallback(async () => { try { - const data = await api.getSettings(); + const data = await (takePrefetch('settings') ?? api.getSettings()); setAppSettings(data); // Initialize in-memory cache with server data initLastMessageTimes(data.last_message_times ?? {}); @@ -330,7 +331,7 @@ export function App() { // Fetch undecrypted packet count const fetchUndecryptedCount = useCallback(async () => { try { - const data = await api.getUndecryptedPacketCount(); + const data = await (takePrefetch('undecryptedCount') ?? api.getUndecryptedPacketCount()); setUndecryptedCount(data.count); } catch (err) { console.error('Failed to fetch undecrypted count:', err); @@ -340,7 +341,7 @@ export function App() { // Fetch all contacts, paginating if >1000 const fetchAllContacts = useCallback(async (): Promise => { const pageSize = 1000; - const first = await api.getContacts(pageSize, 0); + const first = await (takePrefetch('contacts') ?? api.getContacts(pageSize, 0)); if (first.length < pageSize) return first; let all = [...first]; let offset = pageSize; @@ -360,7 +361,7 @@ export function App() { fetchUndecryptedCount(); // Fetch contacts and channels via REST (parallel, faster than WS serial push) - api.getChannels().then(setChannels).catch(console.error); + (takePrefetch('channels') ?? api.getChannels()).then(setChannels).catch(console.error); fetchAllContacts() .then((data) => { setContacts(data); diff --git a/frontend/src/hooks/useUnreadCounts.ts b/frontend/src/hooks/useUnreadCounts.ts index efb3c96..b4bb06a 100644 --- a/frontend/src/hooks/useUnreadCounts.ts +++ b/frontend/src/hooks/useUnreadCounts.ts @@ -7,12 +7,7 @@ import { type ConversationTimes, } from '../utils/conversationState'; import type { Channel, Contact, Conversation, Message, UnreadCounts } from '../types'; - -// Consume the prefetched unreads promise started in index.html (if available). -// This lets the fetch run while React JS is still downloading/parsing. -const prefetchedUnreads: Promise | undefined = ( - window as unknown as { __prefetch?: { unreads?: Promise } } -).__prefetch?.unreads; +import { takePrefetch } from '../prefetch'; export interface UseUnreadCountsResult { unreadCounts: Record; @@ -63,8 +58,9 @@ export function useUnreadCounts( const contactsLen = contacts.length; const prevLens = useRef({ channels: 0, contacts: 0 }); useEffect(() => { - if (prefetchedUnreads) { - prefetchedUnreads.then(applyUnreads).catch(() => fetchUnreads()); + const prefetched = takePrefetch('unreads'); + if (prefetched) { + prefetched.then(applyUnreads).catch(() => fetchUnreads()); } else { fetchUnreads(); } diff --git a/frontend/src/prefetch.ts b/frontend/src/prefetch.ts new file mode 100644 index 0000000..d437852 --- /dev/null +++ b/frontend/src/prefetch.ts @@ -0,0 +1,26 @@ +/** + * Consume prefetched API promises started in index.html before React loaded. + * + * Each key is consumed at most once — the first caller gets the promise, + * subsequent callers get undefined and should fall back to a normal fetch. + */ + +import type { AppSettings, Channel, Contact, RadioConfig, UnreadCounts } from './types'; + +interface PrefetchMap { + config?: Promise; + settings?: Promise; + channels?: Promise; + contacts?: Promise; + unreads?: Promise; + undecryptedCount?: Promise<{ count: number }>; +} + +const store: PrefetchMap = (window as unknown as { __prefetch?: PrefetchMap }).__prefetch ?? {}; + +/** Take a prefetched promise (consumed once, then gone). */ +export function takePrefetch(key: K): PrefetchMap[K] { + const p = store[key]; + delete store[key]; + return p; +}