Repository navigation
v0.44.0
What's changed
Read the PR list and Commits through TanStack Query
Lists felt sluggish because every load hook dropped back to shimmer rows
whenever it ran again: coming back from a PR, switching tab, paging,
changing a filter or pressing Try again all blanked the list, even when
the rows were in the 30 s cache.
This adds @tanstack/react-query (v4: v5 needs React 18, and
azure-devops-ui pins React 16) with one QueryClient per page, and moves
the two lists that flash most onto it:
- Pull requests: useQuery under prListKey. Back from a PR, or back to a
tab, shows the rows at once and refreshes them in the background.
A new page or filter keeps the current rows until its own arrive; the
pager waits for the page it last asked for. - Commits: useInfiniteQuery, a page per "Load more". A filter change
keeps the rows; another branch starts from shimmer rows, so one
branch's commits never show under another's name. A failed refresh or
"Load more" keeps what is shown (Load more says so in a MessageCard).
Fresh for 30 s as before, kept 10 minutes, refetched on focus when stale.
Only a network or 5xx failure is retried, once; sign-in, permission and
rate-limit errors show at once as before. invalidatePrs and
invalidateRefs now also refetch what is on screen and drop the rest
(dropQueries), so a merged PR is not shown under Active afterwards;
sign-out clears the whole cache. listPrs and listCommitsRest no longer
cache themselves.
No hand-rolled markup: the existing Table, Card, ZeroData and
MessageCard are reused.
Add the shared query hook, key roots and IndexedDB persistence
useGh wraps useQuery in the loading/error/ready shape the hubs already
render, so each hub converts the same way: loading only while there is
nothing to show, an error only while there is still nothing to show.
Every query key starts with a root (prs, pr, refs, browse, actions,
security, misc), and invalidateRefs, invalidateBrowse, invalidatePr,
invalidatePrPage and invalidateForks now drop their root too.
queryPersist.tsx keeps opted-in reads (meta.persist) in IndexedDB, one
record per organisation, ADO user and hub, so reopening a hub shows its
last lists at once while they refresh. Only lists and metadata opt in;
records last a day, a new extension version drops them, and Sign out
clears them all. Signing in refetches every read, so sign-in state is
no longer part of the keys (it made a restored cache miss on load).
The PR list and Commits opt in.
Test the query persister; keep the mock's scenarios apart
Unit tests (fake-indexeddb) for idbPersister: one debounced write a
second after the last change, Dates restored intact, one record per
id, and Sign out clearing every hub's record; and for shouldPersist,
which keeps only successful reads that opted in.
In the mock harness the user id now carries the scenario (and live
mode), so a list saved under "ok" can't hide a failure scenario's
error, and Reset storage deletes the saved reads too.
Read Pushes, Branches, Tags, Releases and commits through queries
The ref hubs dropped to shimmer rows on every revisit, Try again and
write. Their reads now go through useGh under the "refs" root, so they
show what they had while refreshing, and invalidateRefs (Try again, and
every branch or tag write via useRefWrites) refetches what is on screen.
- Pushes: useInfiniteQuery, a page per "Load more"; a failed page keeps
the rows and says so in a MessageCard instead of replacing the list. - Branches, Tags, Releases, policies, the compare pickers' names, the
commit header, the viewer and admin flags are kept between visits.
Compare results, commit file patches, locked branches and the policy
editor's own ruleset are not (they go stale or must start fresh). - invalidateReleases drops ["refs", "releases"] too, since the release
caches are not among the ref caches. - Reload counters and signedIn are gone from the loaders: sign-in now
refetches everything.
No hand-rolled markup: the new warning is a MessageCard.
Read the Files hub through queries
Files re-fetched the tree, folder, last changes and README on every
visit, and dropped to shimmer on Try again. Its reads now go through
useGh under the "browse" root (forks, the default branch and the latest
release under "refs"), so going back up a folder or reopening the hub
shows what it had at once.
- Kept between visits: repo info, the project's connected repos (per
project), the default branch, the latest release, folder listings,
last change per entry and History. Never trees, file contents, the
README, markdown source, blame or compare. - The tree, a file, the README, blame and compare are fresh for five
minutes, so refocusing the frame doesn't re-download a large tree or
file (they were never refreshed during a visit before). - The view reads the tree through the cache, so after a commit (which
drops "browse") it reads the new tree. - History's "Load more" is a useInfiniteQuery; a failed page keeps the
rows and says so in a MessageCard. - Compare builds Monaco from the query's stable value, so a background
refresh does not rebuild the editor. - Opening a file with edit=true waits for the write context.
No hand-rolled markup: the new warning is a MessageCard.
Share saved reads per project and merge them, not replace them
One record per hub meant two widgets of one kind (or one hub in two
projects) overwrote each other's saved reads, last writer winning. The
record is now per organisation, ADO user and project, shared by every
hub and widget of the project, and each save merges into it inside one
IndexedDB transaction: the newer read of a key wins, reads older than a
day go, and a record from another extension version is replaced.
Because saved reads now outlive the in-memory cache, cacheTime goes
back to ten minutes (it was a day, which held file contents and logs in
memory for that long); useGh takes a cacheTime for large reads.
dropQueries also forgets the saved copies under its key, so the next
visit after a write doesn't start from them.
invalidatePrPage drops only the PR's page and edit reads: dropping the
whole PR refetched every part of it after each action (18 requests in
the mock), and the rest already follow the action store's version.
Read the pull request page, New pull request and hover cards through queries
Switching a PR's tabs, going back to the list and reopening a PR all
started from a spinner. The PR page's reads now go through useGh under
["pr", repo, number, …], so tabs and revisits show what they had while
refreshing; the header is kept between visits.
- The header still re-reads on an action or feed version bump; edit
data, deployments and work items re-read on the action store's
version (useRefetchOn) and keep what they show meanwhile. actionStore
and feedStore are unchanged. - Files, updates, ranges, viewed states and annotations are cached for
the visit but never saved (they hold patches). A viewed tick writes
into the cached answer and is put back if GitHub refuses. - New pull request reads under ["prs", "new", …]; the preview and the
form's file count share one compare read. - Hover cards read the client directly (fetchQuery), since they also
open in Monaco zones and widgets with no provider; an answer is fresh
for ten minutes. The Monaco zone roots in PrFiles get a provider. - My PRs (the page and the My work tab) is kept between visits; being
signed out is an error, so it is never saved. The My work tab's own
entry renders inside QueryProvider.
Diff file contents, blobs and rendered markdown stay as they were.
No hand-rolled markup.
Read Actions, Advanced Security and the widgets through queries
The runs list, run pages, environments, library and alert lists
re-fetched from scratch on every revisit and filter change. Their reads
now go through useGh under the "actions" and "security" roots.
- Polling is refetchInterval: the runs list every 10 s while any loaded
run is active, a run page every 5 s while it or a job is; both stop
when nothing is running and while the frame is hidden (2 reads in 21 s
visible, none in 22 s hidden in the mock). - Writes (re-run, approve, delete logs, Run workflow, enable/disable,
library and alert changes) drop the reads they affect; deleting a run
removes it from every cached runs list at once. - Runs and alert lists are infinite queries: filters keep the rows
until the new ones arrive, and a failed "Load more" keeps the rows and
says so in a MessageCard. - Kept between visits: runs, workflows, environments, deployments,
alert lists and counts, widget data. Never logs, artifacts, test
results, run detail or permissions. Job logs leave memory after five
minutes. - The widgets render inside QueryProvider; being signed out is an error
there, so it is never saved.
No hand-rolled markup: the new warning is a MessageCard.
Show sign-in and permission errors over saved data; save one page per list
Three things found reviewing the move to the query cache, before release:
- The pages show an error only when there is nothing else to show, so a
sign-in that lapsed overnight left yesterday's saved rows on screen
with no way to sign in. A read GitHub refuses for sign-in ("auth") or
permission ("access") now clears what it showed, so every page falls
through to its error view (with Sign in), and forgets the saved copy:
every saved read for "auth", that read's for "access". Rate-limit and
network failures still keep what is shown. Done once, in the query
cache's onError, so it covers useGh and the direct useQuery pages. - On a revisit TanStack re-reads every page an infinite list holds, one
after another: five "Load more" pages saved meant five requests (each
with GraphQL extras) every time the hub opened. Saved infinite lists
now keep their first page only. - Every read of a project shares one IndexedDB record, so one value the
browser can't clone would fail the whole write and silently stop
every hub's reads being kept. Such a read is now left out (with a
console warning) and the rest are saved. A crawl of every hub and
widget in the mock saved them all into one record with no warnings.
Release 0.44.0
Every hub now reads GitHub through TanStack Query, and keeps its lists
between visits, so pages stop dropping to shimmer rows on revisits,
tab switches and Try again, and a hub opened again shows its last lists
at once while they refresh.
Let a publish re-run finish a release whose upload went through
0.44.0 uploaded to the Marketplace, then the validation poll got a
request timeout from the Marketplace, read it as an empty status and
failed the job, so the tag and GitHub release were never made. A re-run
couldn't recover: it would upload the same version again and fail on
"Version number must increase".
- A publish refused because the version is already there carries on to
the validation check instead of failing. - A validation poll with no answer counts as pending and is asked
again; only a real non-pending status fails the job.
Full changelog: v0.43.0...v0.44.0