mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-11-02 02:19:43 +00:00
perf(app): reduce redundant data fetching
Share bounded database pools across API repositories, consolidate sync and chat queries, and make mobile refreshes cache-aware and foreground-aware. Add focused regression coverage, client freshness documentation, and repository agent role configuration.
This commit is contained in:
1 parent
9957fa43f2
commit
c7ee8a4851
81 files changed
+4456
-1400
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { ActivityIndicator, Pressable, RefreshControl, ScrollView, StyleSheet, Text, View } from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useIsFocused } from "@react-navigation/native";
|
||||
import Animated, { FadeIn } from "react-native-reanimated";
|
||||
import { GradientBackground } from "../../components/ui";
|
||||
import { Paywall } from "../../components/paywall";
|
||||
@@ -20,7 +21,8 @@ const TABS: { key: Tab; label: string }[] = [
|
||||
];
|
||||
|
||||
export function DaresScreen() {
|
||||
const dares = useDares();
|
||||
const isFocused = useIsFocused();
|
||||
const dares = useDares({ isFocused });
|
||||
const [tab, setTab] = useState<Tab>("active");
|
||||
const [sheetPack, setSheetPack] = useState<DarePack | null>(null);
|
||||
const [sheetOpen, setSheetOpen] = useState(false);
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { daresApi } from "../../../lib/daresApi";
|
||||
import { ApiError } from "../../../lib/apiClient";
|
||||
import { usePolling } from "../../../hooks/usePolling";
|
||||
import type { DareCatalog, DareStats, SendDarePayload, SentDare } from "../types";
|
||||
|
||||
/** Matches the chat poll cadence; the API has no realtime channel. */
|
||||
const POLL_INTERVAL_MS = 5_000;
|
||||
|
||||
export interface UseDaresConfig {
|
||||
/**
|
||||
* Supplied by the screen rather than read from navigation here, so the hook
|
||||
* stays renderable without a navigator. Polling stops when the screen is not
|
||||
* on top; `usePolling` additionally stops it when the app backgrounds.
|
||||
*/
|
||||
isFocused?: boolean;
|
||||
}
|
||||
|
||||
export interface UseDaresReturn {
|
||||
active: SentDare[];
|
||||
history: SentDare[];
|
||||
@@ -25,7 +35,19 @@ export interface UseDaresReturn {
|
||||
cancel: (dareId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useDares(): UseDaresReturn {
|
||||
/**
|
||||
* Identity of the active set as the user experiences it. History, stats and the
|
||||
* remaining-send allowance only move when a dare enters or leaves this set or
|
||||
* changes status, so this is the signal that says the slower endpoints are worth
|
||||
* re-reading.
|
||||
*/
|
||||
const activeSignature = (dares: SentDare[]): string =>
|
||||
dares
|
||||
.map(dare => `${dare.id}:${dare.status}`)
|
||||
.sort()
|
||||
.join("|");
|
||||
|
||||
export function useDares({ isFocused = true }: UseDaresConfig = {}): UseDaresReturn {
|
||||
const [active, setActive] = useState<SentDare[]>([]);
|
||||
const [history, setHistory] = useState<SentDare[]>([]);
|
||||
const [catalog, setCatalog] = useState<DareCatalog | null>(null);
|
||||
@@ -36,42 +58,63 @@ export function useDares(): UseDaresReturn {
|
||||
const [paywallReason, setPaywallReason] = useState<string | null>(null);
|
||||
const fetchingRef = useRef(false);
|
||||
const mountedRef = useRef(true);
|
||||
const signatureRef = useRef<string | null>(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* `full` reads everything; the polling path reads only the active list and
|
||||
* follows up on the rest when that list actually changed. A failure updates
|
||||
* the UI's error state either way, but is only rethrown for the poll path
|
||||
* (`throwOnError`): `usePolling` needs the rejection to drive its backoff,
|
||||
* while pull-to-refresh and post-mutation callers already surface the error
|
||||
* themselves and must not turn it into an unhandled rejection.
|
||||
*/
|
||||
const load = useCallback(async (full: boolean, throwOnError = false) => {
|
||||
if (fetchingRef.current) return;
|
||||
fetchingRef.current = true;
|
||||
try {
|
||||
const [activeResult, historyResult, catalogResult, statsResult] = await Promise.all([
|
||||
daresApi.list("active"),
|
||||
daresApi.list("history"),
|
||||
daresApi.catalog(),
|
||||
daresApi.stats(),
|
||||
]);
|
||||
const activeResult = await daresApi.list("active");
|
||||
if (!mountedRef.current) return;
|
||||
setActive(activeResult.dares);
|
||||
setHistory(historyResult.dares);
|
||||
setCatalog(catalogResult);
|
||||
setStats(statsResult);
|
||||
|
||||
const signature = activeSignature(activeResult.dares);
|
||||
const changed = signature !== signatureRef.current;
|
||||
signatureRef.current = signature;
|
||||
|
||||
if (full || changed) {
|
||||
const [historyResult, catalogResult, statsResult] = await Promise.all([
|
||||
daresApi.list("history"),
|
||||
daresApi.catalog(),
|
||||
daresApi.stats(),
|
||||
]);
|
||||
if (!mountedRef.current) return;
|
||||
setHistory(historyResult.dares);
|
||||
setCatalog(catalogResult);
|
||||
setStats(statsResult);
|
||||
}
|
||||
setError(null);
|
||||
} catch (cause) {
|
||||
if (mountedRef.current) {
|
||||
setError(cause instanceof ApiError ? cause.message : "Could not load dares");
|
||||
}
|
||||
if (throwOnError) throw cause;
|
||||
} finally {
|
||||
fetchingRef.current = false;
|
||||
if (mountedRef.current) setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
void refresh();
|
||||
const timer = setInterval(() => void refresh(), POLL_INTERVAL_MS);
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, [refresh]);
|
||||
/** Pull-to-refresh and post-mutation reloads always read the slow endpoints too. */
|
||||
const refresh = useCallback(() => load(true), [load]);
|
||||
const poll = useCallback(() => load(false, true), [load]);
|
||||
|
||||
usePolling(poll, { intervalMs: POLL_INTERVAL_MS, enabled: isFocused });
|
||||
|
||||
/** Premium refusals steer to the paywall; everything else surfaces inline. */
|
||||
const handleFailure = useCallback((cause: unknown) => {
|
||||
|
||||
Reference in new issue
Block a user