feat(editor): move endpoint availability onto TanStack Query - #7285
Merged
Conversation
ConnorYoh
requested review from
EthanHealy01,
Frooodle,
jbrunton96 and
reecebrowne
as code owners
August 4, 2026 13:35
useEndpointConfig kept its own cache: a module-level globalFetchDone boolean, a mutable globalEndpointCache object, and a resetGlobalCache() called from the JWT listener. Which consumer mounted first decided who paid for the request, and the cache outlived nothing but a page reload. 251 lines to 101. One shared query for the whole availability map; each consumer projects the endpoints it asked for. Same return shape, so the 12 consumers are untouched. - Unknown endpoints and any failure still read as enabled. This fires before auth settles, and disabling every tool on a hiccup is worse than letting one call fail later. - retry is off for the availability map: the fallback is already the answer, so retrying only doubles a request that every logged-out visitor makes on load. - JWT change invalidates instead of mutating a module global. Desktop is untouched. It shadows this module entirely with a 482-line override whose orchestration — dependency-ready gating, backend-status and server-monitor subscriptions, timeout retries, SaaS routing optimism — is not a plain query, and it has no test coverage to convert against. Its own PR.
ConnorYoh
force-pushed
the
claude/react-query-endpoint-config
branch
from
August 4, 2026 15:10
5b85654 to
f722279
Compare
Member
Author
|
CI failures here are infrastructure, not the change:
Everything that exercises this change passed: Needs a re-run once the mirrors settle. |
Both conflicts are additive unions in the shared query layer, from #7283 landing app config on main while this branch adds endpoint availability. - core/query/keys.ts: keep appConfig alongside endpointsAvailability and endpointEnabled. - core/api/config.ts: keep fetchAppConfig and DEFAULT_APP_CONFIG alongside fetchEndpointsAvailability and fetchEndpointEnabled. No behavioural overlap between the two sides.
reecebrowne
approved these changes
Aug 10, 2026
jbrunton96
approved these changes
Aug 10, 2026
pull Bot
pushed a commit
to qtxtz/Stirling-PDF
that referenced
this pull request
Aug 29, 2026
…Tools#7726) # Description of Changes Step 5 of the TanStack Query rollout, covering the admin People, Teams and Team details screens. Follows Stirling-Tools#7264, Stirling-Tools#7283, Stirling-Tools#7285. ## The problem Two separate ones, in the same three files. **Reads.** Each section fetched and held its own copy of the same resources: People read the roster and the team list, Teams read the team list plus the roster again when its add-member modal opened, Team details read all three. Cost scaled with how many screens you visited rather than with how much data exists. **Writes.** Thirteen handlers each did the same five things by hand: set a processing flag, call the service, toast the outcome, dig a message out of an axios error, and reload their own slice. Refreshing was a convention, not a mechanism, and one handler had already forgotten it. ## The fix Three shared query keys (`adminUsers`, `teams`, `teamDetails`), and one `useAdminMutation` helper that every write is declared against: ```ts const createTeam = useAdminMutation({ write: (name: string) => teamService.createTeam(name), invalidates: ["teams"], success: t("workspace.teams.createTeam.success"), errorFallback: t("workspace.teams.createTeam.error"), onDone: () => { setNewTeamName(""); setCreateModalOpened(false); }, }); ``` Each write names the slices it disturbs, which is the part that only works when reads and writes are designed together: `createTeam` invalidates the team list, while a membership move invalidates the list, both teams' detail rows and the roster, because it genuinely changes all three. Invalidation refetches only mounted queries, so this costs nothing extra. The blanket "invalidate everything" helper survives in exactly one role: child components (invite, password change, seat update) that write through their own services, where the affected scopes are not visible from the call site. ## Why it is better, measured Request counts come from one harness driving `teams -> team details -> back -> people`, run against the branch point and against this branch. The assertion is committed, so it cannot silently regress. | | Before | After | |---|---|---| | Requests | 7 | **3** | | `getTeams` | 4 | **1** | | `getUsers` | 2 | **1** | | `getTeamDetails` | 1 | 1 | | Committed renders | 17 | **15** | Three is one per distinct resource, the floor for that sequence. The four `getTeams` were the Teams table, Team details fetching the same list for its "move to team" dropdown, the explicit refresh on the back button, and People. Renders barely move, which is expected: this changes where data lives, not how often React draws. It is reported because a caching change can quietly cost renders, and this one does not. On the code itself, across the three sections: | | | |---|---| | Net lines | **-216** | | `useState`/`useEffect` removed | **11**, none added | | Duplicated `isAxiosError` blocks | 13 to **1** | | `setProcessing` calls | 19 to **0** | `isAxiosError` is no longer imported by any of the three files. ## Bug fixed `disableMfaByAdmin` showed a success toast and never refreshed. The menu item renders only when `user.mfaEnabled` is true, so an admin disabled MFA, was told it worked, and watched the option stay on screen until a manual reload. It is covered by a test that fails if the invalidation is removed. ## Behaviour worth checking in review - A write no longer blocks its handler before closing the modal. The dialog closes when the write succeeds and the table updates when the refetch lands, rather than the button spinning through both. - Modal submit buttons now track their own mutation rather than one shared flag. Team details still derives a single busy flag, now from its five mutations rather than a `useState`, so its row actions disable together as before. - The per-handler `console.error` is kept, once, in the shared error path. ## Testing Five tests, each verified by breaking the implementation and confirming that one test, and only that one, fails: | Mutation | Caught by | |---|---| | Drop the shared stale window (`staleTime: 0`) | request-count test | | Make invalidation a no-op | write-visibility test | | Ignore the login-enabled gate | login-disabled test | | Stop invalidating after the MFA write | MFA-refresh test | | Fall back to the generic error message | server-message test | The write tests drive the real flows through their modals and menus rather than calling hooks directly. `task frontend:check` passes typecheck, lint and oxfmt, and 2383 of 2385 editor tests. The two failures, `workbenchSession.test.ts` and `notificationActions.test.tsx`, are untouched here and fail identically with this branch's changes reverted. ## Scope The three services keep their current shape; nothing outside these three sections and the new hook module changes. Child modals that write through their own services still refresh via the blanket helper, and converting those is separate work.
pull Bot
pushed a commit
to kokizzu/Stirling-PDF
that referenced
this pull request
Aug 29, 2026
…ls#7436) # Description of Changes Step 4 of the TanStack Query rollout, and the first of the polling hooks. Follows Stirling-Tools#7264, Stirling-Tools#7283, Stirling-Tools#7285. ## The problem `useSigningSessions` hand-rolled its own fetch, loading state and `setInterval`. Two consequences: - **A raw `setInterval` keeps polling a hidden tab.** Browsers throttle background timers, they do not stop them, so a backgrounded editor with Shared Sign open keeps hitting both endpoints for as long as it is open. - **No tests.** The hook had none, and its quietest behaviour (below) is the easiest thing to break without noticing. ## End state One query behind `qk.signingSessions()`, with the polling lifecycle handed to the library: - Polling stops while the tab is hidden, and refetches on return rather than leaving data up to a full interval stale. - Mounts render from cache while they revalidate, so moving between the tool picker and the signing tool no longer flashes an empty list. - 12 tests where there were none. Same return shape, so no consumer files change. ### What this is not This is not a deduplication win. The three consumers are never mounted at the same time: `ToolPanel` renders the tool picker or the active tool and never both, so the badge cannot be on screen with either of the others, and `SharedSigningLauncher` and `useSigningSessionController` sit inside two different tools. The shared key earns its keep on cache reuse across those transitions, not on concurrent fetches. ## The bit worth reviewing The hand-rolled `{ silent: true }` flag encoded three states, and no single Query flag reproduces them: | | Spinner | Toast on failure | |---|---|---| | First load | yes | yes | | Background poll | no | no | | Explicit refetch | **yes** | **yes** | `isLoading` is false during an explicit refetch when data is already on screen; `isFetching` is true during a background poll. Neither matches, so the user-initiated case is tracked with a small flag and the failure toast is gated on `isLoadingError` plus the explicit path. ## Testing Twelve tests. Rather than trust them, each claim was checked by breaking the implementation and confirming the relevant test fails: | Mutation | Caught by | |---|---| | `refetchIntervalInBackground: true` | hidden-tab test | | Drop `refetchOnWindowFocus` | returns-to-view test | | Drop the user-initiated spinner flag | manual-refresh test | | Toast on every error | background-failure-is-silent test | | Give each observer its own key | dedupe test | Three things worth knowing for the next conversion: - **`waitFor` flushes renders.** Recording an index *after* `waitFor(callCount === 2)` skips past the in-flight render, so a "did the spinner flip on" assertion passes vacuously. The marker has to go before the poll. - **Fake timers hide in-flight state.** The fetch settles inside the same `act()`, so the intermediate render never happens. That test uses real timers and a held-open promise. - **`visibilitychange` has to bubble.** query-core listens for it on `window`, and the real event bubbles from `document`. A test helper dispatching a non-bubbling event never reaches the focus manager, and the pause behaviour still appears to work because `refetchInterval` reads `document.visibilityState` directly at tick time rather than through the event. **One claim is deliberately unguarded.** `isLoading` vs `isFetching` for a background poll produces no re-render at all, so there is nothing observable for a test to assert and no user-visible difference to protect. ## Pre-existing failures `task frontend:check` passes typecheck, lint and oxfmt, and 2363 of 2365 editor tests. The two failures, `workbenchSession.test.ts` and `notificationActions.test.tsx`, fail identically with this branch's changes reverted and are untouched by it. ## Scope This is one of five pollers. The remaining four, `useLocalFolderPoller`, `WatchedFolderWorkbenchView`, `SessionDetailPanel` and cloud `TeamSection`, are separate files with their own consumers and follow separately, now that the silent-refresh pattern has a worked example. --------- Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description of Changes
The problem
useEndpointConfigkept its own cache: a module-levelglobalFetchDoneboolean, a mutableglobalEndpointCacheobject, and aresetGlobalCache()called from the JWT listener. Which consumer mounted first decided who paid for the request, and nothing invalidated it except a page reload.End state
One shared query for the whole availability map; each of the 12 consumers projects the endpoints it asked for. 251 lines to 101, same return shape, no consumer changes.
globalFetchDone+ mutable module objectresetGlobalCache()mutating that objectinvalidateQueriesuseStatetripleBehaviour kept deliberately:
retryis off for the availability map. The fallback is the answer, so retrying only doubles a request every logged-out visitor makes on load.Desktop is untouched
desktop/hooks/useEndpointConfig.tsshadows this module entirely — no shared code, so core converting doesn't affect it and there's no half-migrated state.It's 482 lines of orchestration rather than fetching: dependency-ready gating,
tauriBackendServiceandselfHostedServerMonitorsubscriptions, a 2.5s timeout retry for backend startup, a legacy?endpoints=fallback for old servers, and SaaS-routing optimism that rewrites disabled endpoints to enabled. It also has no test coverage to convert against, and it decides whether tools appear at all in the desktop app.That's a different job from this one and wants its own review. Next PR.
Testing
9 new tests: projection onto the requested subset, one request across consumers, unknown-endpoint fallback, failure fallback with no retry, empty-list no-fetch, JWT invalidation, and the three single-endpoint cases.
task frontend:checkgreen: 1677 tests across 192 files, typecheck on all five flavours, eslint, dpdm, prettier.