fix(object-browser): avoid refresh on tab restore

This commit is contained in:
zipg authored and GitHub committed 2026-09-10 17:44:06 +08:00
1 parent f7afa8dca7
commit e7ea2e92f2
2 files changed
+11 -21

No files matched your search

@@ -2859,14 +2859,13 @@ async function loadObjects(options?: { allowCached?: boolean; preserveExistingRo
const cached = options?.allowCached ? getCachedObjectBrowserRowsForScaffold(request.scope) : undefined;
if (cached) {
// Scaffold the last-known rows so returning to this tab shows them instantly
// instead of flashing an empty list. A fresh entry (< TTL) is authoritative on
// its own; a stale one is presented and then revalidated in the background.
// Restore the last-known rows when remounting this tab. The cached list is
// authoritative for navigation restores, including entries older than the
// freshness TTL; re-querying here makes every tab switch look like a refresh.
// Explicit refresh and metadata invalidation still bypass this branch.
applyObjectBrowserRows(cached.rows);
finishOnce();
if (!cached.stale) return;
scaffoldRefresh = true;
refreshingObjects.value = true;
return;
} else {
// No scaffold: first load in this scope, cache invalidated by a DDL mutation,
// or the caller wants a true reload. If the caller explicitly asked to keep the
@@ -17,25 +17,16 @@ function functionBody(name: string): string {
}
describe("ObjectBrowser scaffold refresh race", () => {
// Scope A hits stale cache and starts a background revalidate (refreshingObjects = true);
// before it settles, the user switches to scope B which hits fresh cache and early-returns.
// A's finally() can no longer run (the load guard's epoch moved on), so the flag must be
// cleared by the newest request (B) at entry — otherwise the toolbar icon spins forever.
it("resets the transient refresh flags at entry, before the cache decision", () => {
it("restores cached rows without revalidating on a tab remount", () => {
const body = functionBody("loadObjects");
const loadingReset = body.indexOf("loadingObjects.value = false;");
const refreshingReset = body.indexOf("refreshingObjects.value = false;");
const cachedBranch = body.indexOf("const cached =");
const cachedReturn = body.indexOf("finishOnce();\n return;", cachedBranch);
expect(loadingReset).toBeGreaterThanOrEqual(0);
expect(refreshingReset).toBeGreaterThanOrEqual(0);
// Both resets must precede the cached fresh/stale decision so the newest request
// owns the spinner state even when a superseded request's finally() cannot run.
expect(Math.min(loadingReset, refreshingReset)).toBeLessThan(cachedBranch);
// And neither flag may be turned back on before the branch decides the indicator.
expect(body.indexOf("refreshingObjects.value = true;")).toBeGreaterThan(cachedBranch);
expect(body.indexOf("loadingObjects.value = true;")).toBeGreaterThan(cachedBranch);
expect(cachedBranch).toBeGreaterThanOrEqual(0);
expect(cachedReturn).toBeGreaterThan(cachedBranch);
expect(body.slice(cachedBranch, cachedReturn)).not.toContain("cached.stale");
expect(body.slice(cachedBranch, cachedReturn)).not.toContain("refreshingObjects.value = true;");
});
it("exposes a non-blocking refresh flag distinct from the blocking full-area load", () => {