From 8a2547477018a0ed931f9dcd4777c5af6626e2da Mon Sep 17 00:00:00 2001 From: devthejo Date: Sun, 8 Mar 2026 00:38:52 +0100 Subject: [PATCH] fix: network reset navigation drift --- src/hooks/useLatestWithSubscription.js | 15 +++++-- src/hooks/useStreamQueryWithSubscription.js | 15 +++++-- src/network/NetworkProviders.js | 44 ++++++++++++++++++--- src/network/apollo.js | 3 +- src/stores/network.js | 12 +++++- 5 files changed, 73 insertions(+), 16 deletions(-) diff --git a/src/hooks/useLatestWithSubscription.js b/src/hooks/useLatestWithSubscription.js index 42c3fe0..c3c5b15 100644 --- a/src/hooks/useLatestWithSubscription.js +++ b/src/hooks/useLatestWithSubscription.js @@ -128,7 +128,7 @@ export default function useLatestWithSubscription( // Some devices keep the WS transport "connected" after a lock/unlock, but the // per-operation subscription stops delivering. Trigger a controlled resubscribe. - const FOREGROUND_KICK_MIN_INACTIVE_MS = 3_000; + const FOREGROUND_KICK_MIN_INACTIVE_MS = 30_000; const FOREGROUND_KICK_MIN_INTERVAL_MS = 15_000; if ( @@ -239,6 +239,15 @@ export default function useLatestWithSubscription( if (age < livenessStaleMs) return; const now = Date.now(); + const becameInactiveAt = lastBecameInactiveAtRef.current; + const inactiveWindowMs = becameInactiveAt ? now - becameInactiveAt : null; + if ( + typeof inactiveWindowMs === "number" && + inactiveWindowMs < livenessStaleMs + 15_000 + ) { + return; + } + if (now - lastLivenessKickAtRef.current < livenessStaleMs) return; lastLivenessKickAtRef.current = now; @@ -276,7 +285,7 @@ export default function useLatestWithSubscription( // Escalation policy for repeated consecutive stale kicks. if ( - consecutiveStaleKicksRef.current >= STALE_KICKS_BEFORE_RELOAD && + consecutiveStaleKicksRef.current >= STALE_KICKS_BEFORE_RELOAD + 2 && now - lastReloadAtRef.current >= MIN_ESCALATION_INTERVAL_MS ) { const lastRecovery = wsLastRecoveryDateRef.current @@ -310,7 +319,7 @@ export default function useLatestWithSubscription( // ignore } - networkActions.triggerReload(); + networkActions.triggerReload("transport"); } else if ( consecutiveStaleKicksRef.current >= STALE_KICKS_BEFORE_WS_RESTART && now - lastWsRestartAtRef.current >= MIN_ESCALATION_INTERVAL_MS diff --git a/src/hooks/useStreamQueryWithSubscription.js b/src/hooks/useStreamQueryWithSubscription.js index 48e10ba..4b123d4 100644 --- a/src/hooks/useStreamQueryWithSubscription.js +++ b/src/hooks/useStreamQueryWithSubscription.js @@ -131,7 +131,7 @@ export default function useStreamQueryWithSubscription( // Some devices keep the WS transport "connected" after a lock/unlock, but the // per-operation subscription stops delivering. Trigger a controlled resubscribe. - const FOREGROUND_KICK_MIN_INACTIVE_MS = 3_000; + const FOREGROUND_KICK_MIN_INACTIVE_MS = 30_000; const FOREGROUND_KICK_MIN_INTERVAL_MS = 15_000; if ( @@ -281,6 +281,15 @@ export default function useStreamQueryWithSubscription( }); } // Avoid spamming resubscribe triggers. + const becameInactiveAt = lastBecameInactiveAtRef.current; + const inactiveWindowMs = becameInactiveAt ? now - becameInactiveAt : null; + if ( + typeof inactiveWindowMs === "number" && + inactiveWindowMs < livenessStaleMs + 15_000 + ) { + return; + } + if (now - lastLivenessKickAtRef.current < livenessStaleMs) return; lastLivenessKickAtRef.current = now; @@ -318,7 +327,7 @@ export default function useStreamQueryWithSubscription( // Escalation policy for repeated consecutive stale kicks. if ( - consecutiveStaleKicksRef.current >= STALE_KICKS_BEFORE_RELOAD && + consecutiveStaleKicksRef.current >= STALE_KICKS_BEFORE_RELOAD + 2 && now - lastReloadAtRef.current >= MIN_ESCALATION_INTERVAL_MS ) { const lastRecovery = wsLastRecoveryDateRef.current @@ -352,7 +361,7 @@ export default function useStreamQueryWithSubscription( // ignore } - networkActions.triggerReload(); + networkActions.triggerReload("transport"); } else if ( consecutiveStaleKicksRef.current >= STALE_KICKS_BEFORE_WS_RESTART && now - lastWsRestartAtRef.current >= MIN_ESCALATION_INTERVAL_MS diff --git a/src/network/NetworkProviders.js b/src/network/NetworkProviders.js index 9bd701e..77f8b27 100644 --- a/src/network/NetworkProviders.js +++ b/src/network/NetworkProviders.js @@ -20,6 +20,7 @@ import getRetryMaxAttempts from "./getRetryMaxAttemps"; import { createLogger } from "~/lib/logger"; import { NETWORK_SCOPES } from "~/lib/logger/scopes"; +import createCache from "./cache"; const { useNetworkState, networkActions } = store; @@ -28,11 +29,15 @@ const networkProvidersLogger = createLogger({ feature: "NetworkProviders", }); +const sharedApolloCache = createCache(); + const initializeNewApolloClient = (reload) => { if (reload) { const { apolloClient } = network; apolloClient.stop(); - apolloClient.clearStore(); + if (apolloClient.cache !== sharedApolloCache) { + apolloClient.clearStore(); + } } network.apolloClient = createApolloClient({ @@ -40,6 +45,7 @@ const initializeNewApolloClient = (reload) => { GRAPHQL_URL: env.GRAPHQL_URL, GRAPHQL_WS_URL: env.GRAPHQL_WS_URL, getRetryMaxAttempts, + cache: sharedApolloCache, }); }; initializeNewApolloClient(); @@ -51,34 +57,60 @@ network.oaFilesKy = oaFilesKy; export default function NetworkProviders({ children }) { const [key, setKey] = useState(0); + const [transportClient, setTransportClient] = useState(() => network.apolloClient); - const networkState = useNetworkState(["initialized", "triggerReload"]); + const networkState = useNetworkState([ + "initialized", + "triggerReload", + "reloadKind", + "transportGeneration", + ]); useEffect(() => { if (networkState.triggerReload) { networkProvidersLogger.debug("Network triggerReload received", { + reloadKind: networkState.reloadKind, reloadId: store.getAuthState()?.reloadId, hasUserToken: !!store.getAuthState()?.userToken, }); + + const isFullReload = networkState.reloadKind !== "transport"; initializeNewApolloClient(true); - setKey((prevKey) => prevKey + 1); + + if (isFullReload) { + setTransportClient(network.apolloClient); + setKey((prevKey) => prevKey + 1); + } else { + setTransportClient(network.apolloClient); + networkProvidersLogger.debug("Network transport recovered in place", { + reloadId: store.getAuthState()?.reloadId, + hasUserToken: !!store.getAuthState()?.userToken, + transportGeneration: networkState.transportGeneration, + }); + networkActions.onReload(); + } } - }, [networkState.triggerReload]); + }, [ + networkState.triggerReload, + networkState.reloadKind, + networkState.transportGeneration, + ]); useEffect(() => { if (key > 0) { networkProvidersLogger.debug("Network reloaded", { + reloadKind: networkState.reloadKind, reloadId: store.getAuthState()?.reloadId, hasUserToken: !!store.getAuthState()?.userToken, }); networkActions.onReload(); } - }, [key]); + }, [key, networkState.reloadKind]); if (!networkState.initialized) { return ; } - const providers = [[ApolloProvider, { client: network.apolloClient }]]; + const providers = [[ApolloProvider, { client: transportClient }]]; return ( diff --git a/src/network/apollo.js b/src/network/apollo.js index d9df11b..cf9ca45 100644 --- a/src/network/apollo.js +++ b/src/network/apollo.js @@ -19,6 +19,7 @@ if (__DEV__ || process.env.NODE_ENV !== "production") { } export default function createApolloClient(options) { + const cache = options.cache || createCache(); const errorLink = createErrorLink(options); const authLink = createAuthLink(options); const cancelLink = createCancelLink(); @@ -50,8 +51,6 @@ export default function createApolloClient(options) { httpLink: httpChain, }); - const cache = createCache(); - const apolloClient = new ApolloClient({ cache, // connectToDevTools: true, // Enable dev tools for better debugging diff --git a/src/stores/network.js b/src/stores/network.js index 2113f08..0835c2d 100644 --- a/src/stores/network.js +++ b/src/stores/network.js @@ -9,20 +9,28 @@ export default createAtom(({ merge, get }) => { wsLastHeartbeatDate: null, wsLastRecoveryDate: null, triggerReload: false, + reloadKind: null, initialized: true, hasInternetConnection: true, + transportGeneration: 0, }, actions: { - triggerReload: () => { + triggerReload: (reloadKind = "full") => { merge({ - initialized: false, triggerReload: true, + reloadKind, + initialized: reloadKind === "transport" ? true : false, + transportGeneration: + reloadKind === "transport" + ? get("transportGeneration") + 1 + : get("transportGeneration"), }); }, onReload: () => { merge({ initialized: true, triggerReload: false, + reloadKind: null, }); }, WSConnected: () => {