fix(redis): keep key browser filters across tab switches

This commit is contained in:
一颗红心 authored and GitHub committed 2026-09-10 11:23:57 +08:00
1 parent b2d2507aec
commit dc1b651949
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);
}