feat(object-browser): preserve object list on return via stale cache
This commit is contained in:
1 parent
a6cd501a78
commit
8642e1a599
6 files changed
+189
-19
No files matched your search
+1
-1
@@ -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 });
|
||||
|
||||
Reference in new issue
Block a user