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;
+}