feat(object-browser): preserve object list on return via stale cache

This commit is contained in:
Abeautifulsnow authored and GitHub committed 2026-09-08 11:51:50 +08:00
1 parent a6cd501a78
commit 8642e1a599
6 files changed
+189 -19

No files matched your search

@@ -36,7 +36,7 @@ describe("ContentArea object browser refresh wiring", () => {
});
it("exposes object and active table-info reloads as refresh", () => {
expect(objectBrowserSource).toMatch(/function refresh\(\): boolean \{\s+void reload\(\);\s+void refreshActiveTableInfo\(\);\s+return true;\s+\}/);
expect(objectBrowserSource).toMatch(/function refresh\(\): boolean \{\s+void reload\(\{ preserveExistingRows: true \}\);\s+void refreshActiveTableInfo\(\);\s+return true;\s+\}/);
expect(objectBrowserSource).toContain("defineExpose({ focusSearch, refresh });");
});
@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { ObjectBrowserRow } from "@/lib/table/objectBrowserRows";
import { cacheObjectBrowserRows, clearObjectBrowserRowsCache, createObjectBrowserRowsCacheWriteToken, getCachedObjectBrowserRows, invalidateObjectBrowserRowsCache } from "@/lib/table/objectBrowserRowsCache";
import { cacheObjectBrowserRows, clearObjectBrowserRowsCache, createObjectBrowserRowsCacheWriteToken, getCachedObjectBrowserRows, getCachedObjectBrowserRowsForScaffold, invalidateObjectBrowserRowsCache } from "@/lib/table/objectBrowserRowsCache";
const row: ObjectBrowserRow = {
id: "table:users",
@@ -127,4 +127,43 @@ describe("objectBrowserRowsCache", () => {
expect(invalidateObjectBrowserRowsCache({ ...scope, tableName: "users" })).toBe(1);
expect(getCachedObjectBrowserRows(scope)).toBeUndefined();
});
describe("scaffold reads (allowStale)", () => {
it("returns a fresh hit marked stale=false before the TTL", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-07-28T00:00:00Z"));
const scope = { connectionId: "c1", database: "db", schema: "public" };
cacheRows(scope);
const hit = getCachedObjectBrowserRowsForScaffold(scope);
expect(hit).toBeDefined();
expect(hit!.rows).toEqual([row]);
expect(hit!.stale).toBe(false);
expect(hit!.ageMs).toBe(0);
expect(hit!.cachedAt).toBe(Date.now());
});
it("surfaces an expired entry as a stale scaffold instead of refusing it", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-07-28T00:00:00Z"));
const scope = { connectionId: "c1", database: "db", schema: "public" };
cacheRows(scope);
vi.advanceTimersByTime(30_001);
const hit = getCachedObjectBrowserRowsForScaffold(scope);
expect(hit).toBeDefined();
expect(hit!.rows).toEqual([row]);
expect(hit!.stale).toBe(true);
expect(hit!.ageMs).toBeGreaterThan(30_000);
});
it("returns undefined when no scaffold is available (uncached or invalidated)", () => {
const scope = { connectionId: "c1", database: "db", schema: "public" };
expect(getCachedObjectBrowserRowsForScaffold(scope)).toBeUndefined();
cacheRows(scope);
invalidateObjectBrowserRowsCache({ connectionId: "c1", database: "db", schema: "public" });
expect(getCachedObjectBrowserRowsForScaffold(scope)).toBeUndefined();
});
});
});
@@ -78,6 +78,28 @@ export function getCachedObjectBrowserRows(scope: ObjectBrowserRowsCacheScope):
return hit ? cloneRows(hit.value) : undefined;
}
export interface ObjectBrowserRowsCacheHit {
rows: ObjectBrowserRow[];
stale: boolean;
ageMs: number;
cachedAt: number;
}
/**
* Reads the cached rows for a scope as a **scaffold**, allowing stale entries
* (beyond the TTL) to be surfaced and refreshed in the background. This never
* adds a new cache entry or expands capacity — a stale hit is the same Map entry
* the TTL would otherwise refuse to return, only now with its freshness metadata.
*
* Carries `stale`/`ageMs`/`cachedAt` so the caller can decide: keep an entry
* under TTL as-is, or present it immediately then revalidate in the background.
*/
export function getCachedObjectBrowserRowsForScaffold(scope: ObjectBrowserRowsCacheScope): ObjectBrowserRowsCacheHit | undefined {
const hit = objectBrowserRowsCache.get(objectBrowserRowsScope(scope), { allowStale: true });
if (!hit) return undefined;
return { rows: cloneRows(hit.value), stale: hit.stale, ageMs: hit.ageMs, cachedAt: hit.cachedAt };
}
export function createObjectBrowserRowsCacheWriteToken(scope: ObjectBrowserRowsCacheScope): ObjectBrowserRowsCacheWriteToken {
const frozenScope = Object.freeze({ ...scope });
return Object.freeze({ generation: objectBrowserRowsCacheGeneration(frozenScope).generation, scope: frozenScope });