From 82437d6d4f66f019745c810eb525176693d24506 Mon Sep 17 00:00:00 2001 From: g-h-miles Date: Tue, 13 Oct 2026 17:24:01 -0500 Subject: [PATCH 1/2] fix(mobile): pulling to refresh a pull request fetches current checks The PR detail pull-to-refresh only re-read through the server's cache, which answers with the held detail and revalidates behind it, so check statuses stayed stale. Invalidate the pull request first, as the web refresh button and the mobile PR list already do. Co-Authored-By: Claude Opus 5.5 --- .../pull-requests/PullRequestRouteScreen.tsx | 26 ++++++---- apps/mobile/src/state/use-pull-to-refresh.ts | 50 ++++++++++++------- 2 files changed, 49 insertions(+), 27 deletions(-) diff --git a/apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx b/apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx index 9ff0d0b18681c..47ac4bf336932 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx @@ -44,13 +44,13 @@ import { useRoute, type StaticScreenProps, } from "@react-navigation/native"; -import { useAtomRefresh } from "@effect/atom-react"; +import { RegistryContext, useAtomRefresh } from "@effect/atom-react"; import { pullRequestReviewKey } from "./pullRequestReviewDrafts"; import { type PullRequestDetail, type PullRequestListEntry } from "@t3tools/contracts"; import { PullRequestOverviewLoading } from "./PullRequestOverviewLoading"; import { copyPullRequestLink, sharePullRequestLink } from "./pullRequestLink"; import { pullRequestStatus } from "./pullRequestStatus"; -import { useEffect, useRef, useState } from "react"; +import { use, useEffect, useRef, useState } from "react"; import { Linking, Platform, @@ -354,13 +354,21 @@ export function PullRequestRouteScreen({ route }: StaticScreenProps { - pullRefresh.onRefresh(); - if (guidesEnabled) refreshGuide(); - stack.refresh(); - refreshActivity(); - filesContent.refresh(); - }; + const registry = use(RegistryContext); + // A pull goes around the server's cache, like the web refresh button: the invalidation goes + // first so the re-reads ask the host for current checks, reviews, and state. + const refresh = () => + pullRefresh.onRefresh(async () => { + const target = requestTarget(params); + await composerPullRequests.invalidate.run(registry, { + environmentId: target.environmentId, + input: { reference: target.input }, + }); + if (guidesEnabled) refreshGuide(); + stack.refresh(); + refreshActivity(); + filesContent.refresh(); + }); const jumpToFiles = () => { scrollViews.current.overview?.scrollToOffset({ offset: bodyY.current + filesY.current - insets.top, diff --git a/apps/mobile/src/state/use-pull-to-refresh.ts b/apps/mobile/src/state/use-pull-to-refresh.ts index cf29eefa9307f..4e3b248f29aa0 100644 --- a/apps/mobile/src/state/use-pull-to-refresh.ts +++ b/apps/mobile/src/state/use-pull-to-refresh.ts @@ -3,6 +3,7 @@ import { executeAtomQuery } from "@t3tools/client-runtime/state/runtime"; import { type AsyncResult, type Atom } from "effect/reactivity"; import { use, useCallback, useEffect, useRef, useState } from "react"; +/** Only a user's pull owns the refresh indicator; query invalidation stays silent. */ export function usePullToRefresh(atom: Atom.Atom> | null) { const registry = use(RegistryContext); const request = useRef<{ atom: NonNullable; controller: AbortController } | null>( @@ -17,24 +18,37 @@ export function usePullToRefresh(atom: Atom.Atom { - if (!atom || (request.current?.atom === atom && !request.current.controller.signal.aborted)) - return; - request.current?.controller.abort(); - const controller = new AbortController(); - request.current = { atom, controller }; - setRefreshingAtom(atom); - void executeAtomQuery(registry, atom, { - refresh: true, - signal: controller.signal, - reportFailure: false, - reportDefect: false, - }).finally(() => { - if (controller.signal.aborted || request.current?.controller !== controller) return; - request.current = null; - setRefreshingAtom(null); - }); - }, [atom, registry]); + /** + * `beforeRefresh` runs first under the same indicator, e.g. to drop a server-side cache. If it + * fails, the read still runs and at worst answers from that cache. + */ + const onRefresh = useCallback( + (beforeRefresh?: () => Promise) => { + if (!atom || (request.current?.atom === atom && !request.current.controller.signal.aborted)) + return; + request.current?.controller.abort(); + const controller = new AbortController(); + request.current = { atom, controller }; + setRefreshingAtom(atom); + void Promise.resolve() + .then(beforeRefresh) + .catch(() => undefined) + .then(() => + executeAtomQuery(registry, atom, { + refresh: true, + signal: controller.signal, + reportFailure: false, + reportDefect: false, + }), + ) + .finally(() => { + if (controller.signal.aborted || request.current?.controller !== controller) return; + request.current = null; + setRefreshingAtom(null); + }); + }, + [atom, registry], + ); return { refreshing: atom !== null && refreshingAtom === atom, onRefresh }; } From b76d4e808595268e56ba4bac84b0e370a3af6ba9 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 19 Oct 2026 10:30:55 +0000 Subject: [PATCH 2/2] fix(mobile): invalidate pull request cache before refreshing --- .../pull-requests/PullRequestRouteScreen.tsx | 27 +++++++---- apps/mobile/src/state/use-pull-to-refresh.ts | 48 ++++++++++++------- 2 files changed, 48 insertions(+), 27 deletions(-) diff --git a/apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx b/apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx index 9ff0d0b18681c..2dff97d9eff9a 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx @@ -44,13 +44,13 @@ import { useRoute, type StaticScreenProps, } from "@react-navigation/native"; -import { useAtomRefresh } from "@effect/atom-react"; +import { RegistryContext, useAtomRefresh } from "@effect/atom-react"; import { pullRequestReviewKey } from "./pullRequestReviewDrafts"; import { type PullRequestDetail, type PullRequestListEntry } from "@t3tools/contracts"; import { PullRequestOverviewLoading } from "./PullRequestOverviewLoading"; import { copyPullRequestLink, sharePullRequestLink } from "./pullRequestLink"; import { pullRequestStatus } from "./pullRequestStatus"; -import { useEffect, useRef, useState } from "react"; +import { use, useEffect, useRef, useState } from "react"; import { Linking, Platform, @@ -354,13 +354,22 @@ export function PullRequestRouteScreen({ route }: StaticScreenProps { - pullRefresh.onRefresh(); - if (guidesEnabled) refreshGuide(); - stack.refresh(); - refreshActivity(); - filesContent.refresh(); - }; + const registry = use(RegistryContext); + const refresh = () => + pullRefresh.onRefresh(async (signal) => { + const target = requestTarget(params); + await composerPullRequests.invalidate + .run(registry, { + environmentId: target.environmentId, + input: { reference: target.input }, + }) + .catch(() => undefined); + if (signal.aborted) return; + if (guidesEnabled) refreshGuide(); + stack.refresh(); + refreshActivity(); + filesContent.refresh(); + }); const jumpToFiles = () => { scrollViews.current.overview?.scrollToOffset({ offset: bodyY.current + filesY.current - insets.top, diff --git a/apps/mobile/src/state/use-pull-to-refresh.ts b/apps/mobile/src/state/use-pull-to-refresh.ts index cf29eefa9307f..98686da323462 100644 --- a/apps/mobile/src/state/use-pull-to-refresh.ts +++ b/apps/mobile/src/state/use-pull-to-refresh.ts @@ -17,24 +17,36 @@ export function usePullToRefresh(atom: Atom.Atom { - if (!atom || (request.current?.atom === atom && !request.current.controller.signal.aborted)) - return; - request.current?.controller.abort(); - const controller = new AbortController(); - request.current = { atom, controller }; - setRefreshingAtom(atom); - void executeAtomQuery(registry, atom, { - refresh: true, - signal: controller.signal, - reportFailure: false, - reportDefect: false, - }).finally(() => { - if (controller.signal.aborted || request.current?.controller !== controller) return; - request.current = null; - setRefreshingAtom(null); - }); - }, [atom, registry]); + const onRefresh = useCallback( + (beforeRefresh?: (signal: AbortSignal) => Promise) => { + if (!atom || (request.current?.atom === atom && !request.current.controller.signal.aborted)) + return; + request.current?.controller.abort(); + const controller = new AbortController(); + request.current = { atom, controller }; + setRefreshingAtom(atom); + void Promise.resolve() + .then(() => { + if (!controller.signal.aborted) return beforeRefresh?.(controller.signal); + }) + .catch(() => undefined) + .then(() => { + if (controller.signal.aborted) return; + return executeAtomQuery(registry, atom, { + refresh: true, + signal: controller.signal, + reportFailure: false, + reportDefect: false, + }); + }) + .finally(() => { + if (controller.signal.aborted || request.current?.controller !== controller) return; + request.current = null; + setRefreshingAtom(null); + }); + }, + [atom, registry], + ); return { refreshing: atom !== null && refreshingAtom === atom, onRefresh }; }