fix(redis): keep key browser filters across tab switches
This commit is contained in:
5 files changed
+486
-5
No files matched your search
@@ -0,0 +1,58 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { clearRedisKeyBrowserState, restoreRedisKeyBrowserState, saveRedisKeyBrowserState, type RedisKeyBrowserStateSnapshot } from "@/lib/tabs/redisKeyBrowserStateCache";
|
||||
|
||||
function snapshot(suffix: number): RedisKeyBrowserStateSnapshot {
|
||||
return {
|
||||
searchPattern: `user:${suffix}*`,
|
||||
searchMode: suffix % 2 === 0 ? "key" : "value",
|
||||
fuzzyKeySearch: suffix % 3 === 0,
|
||||
noExpiryOnly: suffix % 2 === 1,
|
||||
};
|
||||
}
|
||||
|
||||
describe("redisKeyBrowserStateCache", () => {
|
||||
it("round-trips a snapshot for the same key", () => {
|
||||
saveRedisKeyBrowserState("round-trip", snapshot(3));
|
||||
|
||||
expect(restoreRedisKeyBrowserState("round-trip")).toEqual(snapshot(3));
|
||||
});
|
||||
|
||||
it("returns undefined for an unknown key", () => {
|
||||
expect(restoreRedisKeyBrowserState("missing")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps the most recent write for a key", () => {
|
||||
saveRedisKeyBrowserState("overwrite", snapshot(1));
|
||||
saveRedisKeyBrowserState("overwrite", snapshot(2));
|
||||
|
||||
expect(restoreRedisKeyBrowserState("overwrite")).toEqual(snapshot(2));
|
||||
});
|
||||
|
||||
it("evicts the oldest entry beyond 32 keys", () => {
|
||||
for (let index = 0; index < 33; index += 1) {
|
||||
saveRedisKeyBrowserState(`evict-${index}`, snapshot(index));
|
||||
}
|
||||
|
||||
expect(restoreRedisKeyBrowserState("evict-0")).toBeUndefined();
|
||||
expect(restoreRedisKeyBrowserState("evict-1")).toBeDefined();
|
||||
expect(restoreRedisKeyBrowserState("evict-32")).toBeDefined();
|
||||
});
|
||||
|
||||
it("treats a restore as recency so a touched entry survives eviction", () => {
|
||||
for (let index = 0; index < 32; index += 1) {
|
||||
saveRedisKeyBrowserState(`touch-${index}`, snapshot(index));
|
||||
}
|
||||
expect(restoreRedisKeyBrowserState("touch-0")).toEqual(snapshot(0));
|
||||
saveRedisKeyBrowserState("touch-new", snapshot(99));
|
||||
|
||||
expect(restoreRedisKeyBrowserState("touch-0")).toBeDefined();
|
||||
expect(restoreRedisKeyBrowserState("touch-1")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("clears a key on demand", () => {
|
||||
saveRedisKeyBrowserState("clear-me", snapshot(4));
|
||||
clearRedisKeyBrowserState("clear-me");
|
||||
|
||||
expect(restoreRedisKeyBrowserState("clear-me")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
export type RedisKeyBrowserSearchMode = "key" | "value" | "all";
|
||||
|
||||
export interface RedisKeyBrowserStateSnapshot {
|
||||
/** Key / value / all search input text. */
|
||||
searchPattern: string;
|
||||
/** Which Redis search mode the toolbar is in. */
|
||||
searchMode: RedisKeyBrowserSearchMode;
|
||||
/** Fuzzy MATCH wrapping for key-mode searches. */
|
||||
fuzzyKeySearch: boolean;
|
||||
/** Local filter: only show keys with TTL = -1 in the loaded result set. */
|
||||
noExpiryOnly: boolean;
|
||||
}
|
||||
|
||||
// ContentArea renders only the active tab, so RedisKeyBrowser is unmounted on
|
||||
// every tab switch and remounted when the user comes back. Search conditions
|
||||
// survive that round trip through this session-scoped cache keyed by tab id —
|
||||
// the same role documentBrowserStateCache plays for Mongo/ES collection tabs.
|
||||
// The bound keeps state for long tab sessions from accumulating forever.
|
||||
const MAX_ENTRIES = 32;
|
||||
const cache = new Map<string, RedisKeyBrowserStateSnapshot>();
|
||||
|
||||
export function restoreRedisKeyBrowserState(stateKey: string): RedisKeyBrowserStateSnapshot | undefined {
|
||||
const snapshot = cache.get(stateKey);
|
||||
if (!snapshot) return undefined;
|
||||
// Refresh LRU recency.
|
||||
cache.delete(stateKey);
|
||||
cache.set(stateKey, snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
export function saveRedisKeyBrowserState(stateKey: string, snapshot: RedisKeyBrowserStateSnapshot): void {
|
||||
cache.delete(stateKey);
|
||||
cache.set(stateKey, snapshot);
|
||||
while (cache.size > MAX_ENTRIES) {
|
||||
const oldest = cache.keys().next().value;
|
||||
if (oldest === undefined) break;
|
||||
cache.delete(oldest);
|
||||
}
|
||||
}
|
||||
|
||||
export function clearRedisKeyBrowserState(stateKey: string): void {
|
||||
cache.delete(stateKey);
|
||||
}
|
||||
Reference in new issue
Block a user