fix(native): sync React Query online state with server reachability

Probe the health endpoint while unreachable, refetch errored queries when the server
returns, and keep gcTime at least as long as the persister maxAge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jakeandClaude Opus 5.5 committed 2026-10-05 02:51:59 -04:00
1 parent 07ea8f68f2
commit feb41e3149
6 files changed
+43 -8

No files matched your search

+2 -1
View File
@@ -25,6 +25,7 @@ import { useWidgetRefresh } from "@/hooks/use-widget-refresh";
import { initLocale } from "@/lib/i18n";
import { initAnalytics, posthog } from "@/lib/posthog";
import { queryClient } from "@/lib/query-client";
import { QUERY_PERSIST_MAX_AGE } from "@/lib/query-config";
import { initSentry, Sentry } from "@/lib/sentry";
import { getScopeKey, initSession, onStorageScopeChange, queryPersister } from "@/lib/server";
import { sofaTheme } from "@/lib/theme";
@@ -170,7 +171,7 @@ function QueryProvider({ children }: { children: React.ReactNode }) {
if (!scopeKey) return;
const options = { queryClient, persister: queryPersister };
const options = { queryClient, persister: queryPersister, maxAge: QUERY_PERSIST_MAX_AGE };
let unsubscribe: (() => void) | undefined;
let aborted = false;
@@ -87,6 +87,10 @@ export function useServerConnection() {
onServerReachabilityChange((reachable) => {
if (reachable) {
authClient.$store.atoms.session.get().refetch?.();
void queryClient.refetchQueries({
type: "active",
predicate: (q) => q.state.status === "error",
});
}
}),
[],
+2 -1
View File
@@ -4,6 +4,7 @@ import { MutationCache, QueryCache, QueryClient } from "@tanstack/react-query";
import { isClientError, isUnauthorizedError } from "@/lib/error-messages";
import { posthog } from "@/lib/posthog";
import { QUERY_GC_TIME } from "@/lib/query-config";
import { authClient, getIsReachable, isNetworkError, markSessionRejected } from "@/lib/server";
import { toast } from "@/lib/toast";
import { i18n } from "@sofa/i18n";
@@ -23,7 +24,7 @@ export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60_000,
gcTime: 300_000,
gcTime: QUERY_GC_TIME,
networkMode: "online",
retry: (failureCount, error) => {
// 4xx errors can't succeed on retry.
+9
View File
@@ -0,0 +1,9 @@
import { describe, expect, test } from "vitest";
import { QUERY_GC_TIME, QUERY_PERSIST_MAX_AGE } from "@/lib/query-config";
describe("query cache lifetimes", () => {
test("gcTime is at least the persister maxAge", () => {
expect(QUERY_GC_TIME).toBeGreaterThanOrEqual(QUERY_PERSIST_MAX_AGE);
});
});
+4
View File
@@ -0,0 +1,4 @@
/** How long persisted query data is kept (and restored) — 24 h. */
export const QUERY_PERSIST_MAX_AGE = 24 * 60 * 60 * 1000;
/** gcTime must be ≥ the persister's maxAge, or GC'd queries vanish from the persisted cache. */
export const QUERY_GC_TIME = QUERY_PERSIST_MAX_AGE;
+22 -6
View File
@@ -206,6 +206,7 @@ export async function validateServerUrl(url: string): Promise<ValidationResult>
// ---------------------------------------------------------------------------
let isReachable = true;
let lastNetworkState: Network.NetworkState | null = null;
const reachabilityListeners: Array<(reachable: boolean) => void> = [];
function notifyReachability() {
@@ -218,14 +219,28 @@ function setReachable(nextReachable: boolean) {
if (isReachable === nextReachable) return;
isReachable = nextReachable;
notifyReachability();
syncOnlineState();
}
function isDeviceOnline(state: Network.NetworkState): boolean {
return !!state.isConnected && state.isInternetReachable !== false;
}
function syncOnlineState(state: Network.NetworkState) {
onlineManager.setOnline(isDeviceOnline(state) || isReachable);
function syncOnlineState(state: Network.NetworkState | null = lastNetworkState) {
if (state) lastNetworkState = state;
const deviceOnline = lastNetworkState ? isDeviceOnline(lastNetworkState) : true;
onlineManager.setOnline(deviceOnline || isReachable);
}
// While the server is unreachable, queries are paused and never hit serverFetch, so probe the
// health endpoint directly; a success flips reachability (and the online flag) back.
function probeServerIfUnreachable() {
if (isReachable) return;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
serverFetch(`${getServerUrl()}/api/health`, { method: "GET", signal: controller.signal })
.catch(() => {})
.finally(() => clearTimeout(timer));
}
function syncAppFocus(state: AppStateStatus) {
@@ -252,9 +267,6 @@ export async function serverFetch(input: RequestInfo | URL, init?: RequestInit):
try {
const response = await fetch(input, init);
setReachable(true);
if (!onlineManager.isOnline()) {
onlineManager.setOnline(true);
}
return response;
} catch (error) {
if (isNetworkError(error)) {
@@ -280,12 +292,16 @@ export function startReachabilityMonitor(): () => void {
syncAppFocus(AppState.currentState);
void Network.getNetworkStateAsync().then(syncOnlineState);
const networkSubscription = Network.addNetworkStateListener(syncOnlineState);
const networkSubscription = Network.addNetworkStateListener((state) => {
syncOnlineState(state);
probeServerIfUnreachable();
});
const appStateSubscription = AppState.addEventListener("change", (nextState) => {
syncAppFocus(nextState);
if (nextState === "active") {
void Network.getNetworkStateAsync().then(syncOnlineState);
probeServerIfUnreachable();
}
});