From c69f1352d495cdc40455c75df3821e63634c70ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E4=B8=AB=E8=AE=B2=E6=A2=B5?= Date: Fri, 31 Jul 2026 23:11:48 +0800 Subject: [PATCH] feat(redis): poll full values and track TTL countdowns --- .../redis/RedisValueViewer.expiry.spec.ts | 130 ++++++++--- .../RedisValueViewer.streamMonitoring.spec.ts | 4 - .../src/components/redis/RedisValueViewer.vue | 214 ++++++++++-------- .../__tests__/redis/RedisExpiryWiring.spec.ts | 2 +- .../redis/RedisValueViewer.redisJson.spec.ts | 8 +- .../__tests__/redis/redisAutoRefresh.spec.ts | 55 +++-- .../desktop/src/lib/redis/redisAutoRefresh.ts | 24 +- 7 files changed, 268 insertions(+), 169 deletions(-) diff --git a/apps/desktop/src/components/redis/RedisValueViewer.expiry.spec.ts b/apps/desktop/src/components/redis/RedisValueViewer.expiry.spec.ts index 65deca7df..2a3a79818 100644 --- a/apps/desktop/src/components/redis/RedisValueViewer.expiry.spec.ts +++ b/apps/desktop/src/components/redis/RedisValueViewer.expiry.spec.ts @@ -94,6 +94,24 @@ function stringValue(rawBase64 = "dmFsdWU=", ttl = 60) { }; } +function listValue(ttl = 60) { + return { + key_display: "key", + key_raw: "key", + ttl, + redis_type: "list", + data: { + kind: "list" as const, + items: [ + { index: 0, value: { raw_base64: "Zmlyc3Q=", encoding: "utf8" as const } }, + { index: 1, value: { raw_base64: "c2Vjb25k", encoding: "utf8" as const } }, + ], + total: 2, + scan_cursor: undefined, + }, + }; +} + function missingValue() { return { key_display: "key", @@ -221,7 +239,7 @@ async function setStringDraft(value: string) { } describe("RedisValueViewer expiry saving", () => { - it("defaults to manual refresh without TTL polling", async () => { + it("defaults to manual refresh without automatic value polling", async () => { vi.useFakeTimers(); mocks.redisGetValue.mockResolvedValueOnce(stringValue("dmFsdWU=", 60)); @@ -229,16 +247,16 @@ describe("RedisValueViewer expiry saving", () => { await settle(); await vi.advanceTimersByTimeAsync(10_000); + expect(mocks.redisGetValue).toHaveBeenCalledOnce(); expect(mocks.redisGetTtl).not.toHaveBeenCalled(); - expect(document.querySelector("[aria-label='grid.refresh']")?.getAttribute("aria-pressed")).toBe("false"); + expect(document.querySelector("[data-slot='badge'][aria-label='redis.expiry']")?.textContent).toContain("00:00:50"); }); - it("polls only the TTL at the configured interval", async () => { + it("polls the full value at the configured interval without refreshing the parent key tree", async () => { vi.useFakeTimers(); localStorage.setItem("dbx-redis-auto-refresh-enabled-v2", "true"); localStorage.setItem("dbx-redis-auto-refresh-interval-seconds-v2", "5"); - mocks.redisGetValue.mockResolvedValueOnce(stringValue("dmFsdWU=", 60)); - mocks.redisGetTtl.mockResolvedValueOnce(45); + mocks.redisGetValue.mockResolvedValueOnce(stringValue("dmFsdWU=", 60)).mockResolvedValueOnce(stringValue("cmVmcmVzaGVk", 45)); const loaded = vi.fn(); mountViewer(vi.fn(), loaded); @@ -246,49 +264,76 @@ describe("RedisValueViewer expiry saving", () => { await vi.advanceTimersByTimeAsync(5000); await settle(); - expect(mocks.redisGetTtl).toHaveBeenCalledWith("connection", 0, "key"); - expect(mocks.redisGetValue).toHaveBeenCalledOnce(); + expect(mocks.redisGetValue).toHaveBeenCalledTimes(2); + expect(mocks.redisGetTtl).not.toHaveBeenCalled(); expect(loaded).toHaveBeenCalledOnce(); + expect(document.querySelector("textarea")?.value).toBe("refreshed"); expect(document.querySelector("[data-slot='badge'][aria-label='redis.expiry']")?.textContent).toContain("00:00:45"); }); + it("pauses automatic value polling while a collection member is open", async () => { + vi.useFakeTimers(); + localStorage.setItem("dbx-redis-auto-refresh-enabled-v2", "true"); + localStorage.setItem("dbx-redis-auto-refresh-interval-seconds-v2", "1"); + mocks.redisGetValue.mockResolvedValue(listValue()); + + mountViewer(vi.fn()); + await settle(); + Array.from(document.querySelectorAll("[data-redis-value-row]")) + .find((row) => row.textContent?.includes("second"))! + .click(); + await settle(); + expect(document.querySelector("[data-redis-member-detail]")?.textContent).toContain("second"); + + await vi.advanceTimersByTimeAsync(3000); + await settle(); + expect(mocks.redisGetValue).toHaveBeenCalledOnce(); + + document.querySelector("[data-slot='dialog-close']")!.click(); + await settle(); + await vi.advanceTimersByTimeAsync(1000); + await settle(); + + expect(mocks.redisGetValue).toHaveBeenCalledTimes(2); + }); + it("keeps polling when the loaded TTL starts at zero", async () => { vi.useFakeTimers(); localStorage.setItem("dbx-redis-auto-refresh-enabled-v2", "true"); localStorage.setItem("dbx-redis-auto-refresh-interval-seconds-v2", "1"); - mocks.redisGetValue.mockResolvedValueOnce(stringValue("dmFsdWU=", 0)); - mocks.redisGetTtl.mockResolvedValueOnce(30); + mocks.redisGetValue.mockResolvedValueOnce(stringValue("dmFsdWU=", 0)).mockResolvedValueOnce(stringValue("dmFsdWU=", 30)); mountViewer(vi.fn()); await settle(); await vi.advanceTimersByTimeAsync(1000); await settle(); - expect(mocks.redisGetTtl).toHaveBeenCalledOnce(); + expect(mocks.redisGetValue).toHaveBeenCalledTimes(2); expect(document.querySelector("[data-slot='badge'][aria-label='redis.expiry']")?.textContent).toContain("00:00:30"); }); - it("shows auto-refresh as stopped after a TTL polling error", async () => { + it("stops auto-refresh after a full-value polling error", async () => { vi.useFakeTimers(); localStorage.setItem("dbx-redis-auto-refresh-enabled-v2", "true"); localStorage.setItem("dbx-redis-auto-refresh-interval-seconds-v2", "1"); - mocks.redisGetValue.mockResolvedValueOnce(stringValue("dmFsdWU=", 60)); - mocks.redisGetTtl.mockRejectedValueOnce(new Error("network unavailable")); + mocks.redisGetValue.mockResolvedValueOnce(stringValue("dmFsdWU=", 60)).mockRejectedValueOnce(new Error("network unavailable")); mountViewer(vi.fn()); await settle(); await vi.advanceTimersByTimeAsync(1000); await settle(); - expect(document.querySelector("[aria-label='grid.refresh']")?.getAttribute("aria-pressed")).toBe("false"); + await vi.advanceTimersByTimeAsync(5_000); + await settle(); + + expect(mocks.redisGetValue).toHaveBeenCalledTimes(2); }); it("keeps polling after an external PERSIST and picks up a later TTL", async () => { vi.useFakeTimers(); localStorage.setItem("dbx-redis-auto-refresh-enabled-v2", "true"); localStorage.setItem("dbx-redis-auto-refresh-interval-seconds-v2", "1"); - mocks.redisGetValue.mockResolvedValueOnce(stringValue("dmFsdWU=", 60)); - mocks.redisGetTtl.mockResolvedValueOnce(-1).mockResolvedValueOnce(30); + mocks.redisGetValue.mockResolvedValueOnce(stringValue("dmFsdWU=", 60)).mockResolvedValueOnce(stringValue("dmFsdWU=", -1)).mockResolvedValueOnce(stringValue("dmFsdWU=", 30)); mountViewer(vi.fn()); await settle(); @@ -299,8 +344,7 @@ describe("RedisValueViewer expiry saving", () => { await vi.advanceTimersByTimeAsync(1000); await settle(); - expect(mocks.redisGetTtl).toHaveBeenCalledTimes(2); - expect(document.querySelector("[data-redis-value-refresh]")?.getAttribute("aria-pressed")).toBe("true"); + expect(mocks.redisGetValue).toHaveBeenCalledTimes(3); expect(document.querySelector("[data-slot='badge'][aria-label='redis.expiry']")?.textContent).toContain("00:00:30"); }); @@ -308,8 +352,7 @@ describe("RedisValueViewer expiry saving", () => { vi.useFakeTimers(); localStorage.setItem("dbx-redis-auto-refresh-enabled-v2", "true"); localStorage.setItem("dbx-redis-auto-refresh-interval-seconds-v2", "1"); - mocks.redisGetValue.mockResolvedValueOnce(stringValue("dmFsdWU=", 60)); - mocks.redisGetTtl.mockResolvedValue(45); + mocks.redisGetValue.mockResolvedValue(stringValue("dmFsdWU=", 45)); let visibilityState: DocumentVisibilityState = "visible"; vi.spyOn(document, "visibilityState", "get").mockImplementation(() => visibilityState); @@ -319,38 +362,57 @@ describe("RedisValueViewer expiry saving", () => { document.dispatchEvent(new Event("visibilitychange")); await vi.advanceTimersByTimeAsync(5000); - expect(mocks.redisGetTtl).not.toHaveBeenCalled(); + expect(mocks.redisGetValue).toHaveBeenCalledOnce(); visibilityState = "visible"; document.dispatchEvent(new Event("visibilitychange")); await vi.advanceTimersByTimeAsync(1000); await settle(); - expect(mocks.redisGetTtl).toHaveBeenCalledOnce(); + expect(mocks.redisGetValue).toHaveBeenCalledTimes(2); + }); + + it("keeps elapsed TTL time while manual refresh is hidden", async () => { + vi.useFakeTimers(); + mocks.redisGetValue.mockResolvedValueOnce(stringValue("dmFsdWU=", 60)); + let visibilityState: DocumentVisibilityState = "visible"; + vi.spyOn(document, "visibilityState", "get").mockImplementation(() => visibilityState); + + mountViewer(vi.fn()); + await settle(); + await vi.advanceTimersByTimeAsync(10_000); + visibilityState = "hidden"; + document.dispatchEvent(new Event("visibilitychange")); + await vi.advanceTimersByTimeAsync(20_000); + + visibilityState = "visible"; + document.dispatchEvent(new Event("visibilitychange")); + await settle(); + + expect(document.querySelector("[data-slot='badge'][aria-label='redis.expiry']")?.textContent).toContain("00:00:30"); }); it("pauses polling while deactivated and resumes from the saved setting", async () => { vi.useFakeTimers(); localStorage.setItem("dbx-redis-auto-refresh-enabled-v2", "true"); localStorage.setItem("dbx-redis-auto-refresh-interval-seconds-v2", "1"); - mocks.redisGetValue.mockResolvedValueOnce(stringValue("dmFsdWU=", 60)); - mocks.redisGetTtl.mockResolvedValue(45); + mocks.redisGetValue.mockResolvedValue(stringValue("dmFsdWU=", 45)); const viewer = mountKeepAliveViewer(); await settle(); await viewer.setActive(false); await vi.advanceTimersByTimeAsync(5000); - expect(mocks.redisGetTtl).not.toHaveBeenCalled(); + expect(mocks.redisGetValue).toHaveBeenCalledOnce(); await viewer.setActive(true); await vi.advanceTimersByTimeAsync(1000); await settle(); - expect(mocks.redisGetTtl).toHaveBeenCalledOnce(); + expect(mocks.redisGetValue).toHaveBeenCalledTimes(2); }); - it("pauses TTL polling while a value draft is unsaved", async () => { + it("pauses value polling while a value draft is unsaved", async () => { vi.useFakeTimers(); localStorage.setItem("dbx-redis-auto-refresh-enabled-v2", "true"); localStorage.setItem("dbx-redis-auto-refresh-interval-seconds-v2", "1"); @@ -363,33 +425,33 @@ describe("RedisValueViewer expiry saving", () => { await vi.advanceTimersByTimeAsync(5000); await settle(); - expect(mocks.redisGetTtl).not.toHaveBeenCalled(); + expect(mocks.redisGetValue).toHaveBeenCalledOnce(); expect(deleted).not.toHaveBeenCalled(); expect(document.querySelector("textarea")?.value).toBe("draft"); }); - it("ignores a missing-key response when a draft is created during TTL polling", async () => { + it("ignores a missing-key response when a draft is created during value polling", async () => { vi.useFakeTimers(); localStorage.setItem("dbx-redis-auto-refresh-enabled-v2", "true"); localStorage.setItem("dbx-redis-auto-refresh-interval-seconds-v2", "1"); mocks.redisGetValue.mockResolvedValueOnce(stringValue("dmFsdWU=", 60)); - const ttlRequest = deferred(); - mocks.redisGetTtl.mockReturnValueOnce(ttlRequest.promise); + const valueRequest = deferred>(); + mocks.redisGetValue.mockReturnValueOnce(valueRequest.promise); const deleted = vi.fn(); mountViewer(deleted); await settle(); await vi.advanceTimersByTimeAsync(1000); await settle(); - expect(mocks.redisGetTtl).toHaveBeenCalledOnce(); + expect(mocks.redisGetValue).toHaveBeenCalledTimes(2); await setStringDraft("draft"); - ttlRequest.resolve(-2); + valueRequest.resolve(missingValue()); await settle(); await vi.advanceTimersByTimeAsync(5000); await settle(); - expect(mocks.redisGetTtl).toHaveBeenCalledOnce(); + expect(mocks.redisGetValue).toHaveBeenCalledTimes(2); expect(deleted).not.toHaveBeenCalled(); expect(document.querySelector("textarea")?.value).toBe("draft"); }); diff --git a/apps/desktop/src/components/redis/RedisValueViewer.streamMonitoring.spec.ts b/apps/desktop/src/components/redis/RedisValueViewer.streamMonitoring.spec.ts index a0d3c9060..efcf25463 100644 --- a/apps/desktop/src/components/redis/RedisValueViewer.streamMonitoring.spec.ts +++ b/apps/desktop/src/components/redis/RedisValueViewer.streamMonitoring.spec.ts @@ -162,10 +162,6 @@ function openGroups(host: HTMLElement) { async function refreshValue(host: HTMLElement) { host.querySelector("[data-redis-value-refresh]")!.click(); await settle(); - const refreshItem = document.querySelector("[data-slot='dropdown-menu-content'] [data-slot='dropdown-menu-item']"); - expect(refreshItem).not.toBeNull(); - refreshItem!.click(); - await settle(); } describe("RedisValueViewer stream monitoring", () => { diff --git a/apps/desktop/src/components/redis/RedisValueViewer.vue b/apps/desktop/src/components/redis/RedisValueViewer.vue index 4639764e6..f731b0997 100644 --- a/apps/desktop/src/components/redis/RedisValueViewer.vue +++ b/apps/desktop/src/components/redis/RedisValueViewer.vue @@ -4,7 +4,7 @@ import type { CalendarDateTime } from "@internationalized/date"; import { useI18n } from "vue-i18n"; import { onClickOutside } from "@vueuse/core"; import { DynamicScroller, DynamicScrollerItem, RecycleScroller } from "vue-virtual-scroller"; -import { Check, Copy, ClipboardCopy, Eye, Trash2, Save, RefreshCw, Plus, Loader2, Pencil, WrapText, ArrowUp, ArrowDown, ArrowUpDown, Search } from "@lucide/vue"; +import { Check, ChevronDown, Copy, ClipboardCopy, Eye, Trash2, Save, RefreshCw, Plus, Loader2, Pencil, WrapText, ArrowUp, ArrowDown, ArrowUpDown, Search } from "@lucide/vue"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; @@ -25,7 +25,7 @@ import { useEditorFontFamilyStyle } from "@/composables/useEditorFontFamilyStyle import { createShikiJsonHighlighter, type JsonHighlighter } from "@/lib/common/shikiJsonHighlighter"; import { copyToClipboard } from "@/lib/common/clipboard"; import { formatTtl } from "@/lib/common/ttlFormat"; -import { computeAutoRefreshTick, computeDisplayTtl, computeTtlForExpiryEdit, DEFAULT_REDIS_AUTO_REFRESH_INTERVAL_SECONDS, normalizeRedisAutoRefreshInterval } from "@/lib/redis/redisAutoRefresh"; +import { computeDisplayTtl, computeTtlCountdownTick, computeTtlCountdownValue, computeTtlForExpiryEdit, DEFAULT_REDIS_AUTO_REFRESH_INTERVAL_SECONDS, normalizeRedisAutoRefreshInterval } from "@/lib/redis/redisAutoRefresh"; import { canRenderRedisValueFormat, canEditRedisMemberDetail, @@ -164,16 +164,17 @@ const memberValueView = ref(readPreferredRedisValueFormat()); const redisJsonWordWrap = ref(readRedisJsonWordWrap()); const redisJsonHighlighter = ref(); -// Auto-refresh keeps the displayed TTL moving locally and periodically asks -// Redis for the authoritative value. The two timers stay separate so a short -// polling interval never causes a full key-value reload. +// Auto-refresh keeps the displayed TTL moving locally and periodically reloads +// the complete key detail. The full reload updates changed values as well as +// the authoritative TTL without rebuilding the parent key tree. const autoRefreshEnabled = ref(readRedisAutoRefreshEnabled()); const autoRefreshIntervalSeconds = ref(readRedisAutoRefreshInterval()); const countdownTtl = ref(0); -const refreshingTtl = ref(false); +const refreshingValue = ref(false); let autoRefreshTimer: ReturnType | null = null; let countdownTimer: ReturnType | null = null; -let ttlRefreshRequestId = 0; +let autoRefreshRequestId = 0; +let countdownTtlObservedAtMs = Date.now(); let redisValueViewerIsActive = true; function canRunAutoRefresh(): boolean { @@ -200,46 +201,54 @@ function startAutoRefresh() { stopAutoRefresh(); if (!autoRefreshEnabled.value || !data.value || !canRunAutoRefresh()) return; - countdownTtl.value = data.value.ttl; - countdownTimer = setInterval(() => { - const action = computeAutoRefreshTick(autoRefreshEnabled.value, countdownTtl.value); - if (action.type === "decrement") { - countdownTtl.value--; - } - }, 1000); - - autoRefreshTimer = setInterval(() => void refreshTtl(), autoRefreshIntervalSeconds.value * 1000); + autoRefreshTimer = setInterval(() => void refreshAutoValue(), autoRefreshIntervalSeconds.value * 1000); } -async function refreshTtl() { - if (refreshingTtl.value || loading.value || editingTtl.value || savingTtl.value || hasUnsavedRedisDraft.value || !autoRefreshEnabled.value || !data.value || !canRunAutoRefresh()) return; +function startCountdown() { + stopCountdown(); + if (!data.value || !canRunAutoRefresh()) return; - const requestId = ++ttlRefreshRequestId; - refreshingTtl.value = true; - try { - const ttl = await api.redisGetTtl(props.connectionId, props.db, props.keyRaw); - // A draft may be created while the request is in flight. Never apply even - // a missing-key response after the user has started editing. - if (requestId !== ttlRefreshRequestId || hasUnsavedRedisDraft.value || !autoRefreshEnabled.value || !data.value || !canRunAutoRefresh()) return; - - if (ttl === -2) { - data.value = null; - collectionItems.value = []; - scanCursor.value = undefined; - resetStreamEntries(); - resetStreamMonitoring(); - stopAutoRefresh(); - emit("deleted", props.keyRaw); - return; + updateCountdownTtl(); + countdownTimer = setInterval(() => { + const action = computeTtlCountdownTick(countdownTtl.value); + if (action.type === "decrement") { + updateCountdownTtl(); } + }, 1000); +} - const refreshedValue = { ...data.value, ttl }; - data.value = refreshedValue; - countdownTtl.value = ttl; +function syncCountdownTtl(serverTtl: number) { + countdownTtl.value = serverTtl; + countdownTtlObservedAtMs = Date.now(); +} + +function updateCountdownTtl() { + if (!data.value) return; + countdownTtl.value = computeTtlCountdownValue(data.value.ttl, countdownTtlObservedAtMs, Date.now()); +} + +function startRefreshTimers() { + startCountdown(); + startAutoRefresh(); +} + +async function refreshAutoValue() { + if (refreshingValue.value || loading.value || editingTtl.value || savingTtl.value || hasUnsavedRedisDraft.value || shouldPauseAutoValueRefresh() || !autoRefreshEnabled.value || !data.value || !canRunAutoRefresh()) return; + + const requestId = ++autoRefreshRequestId; + refreshingValue.value = true; + try { + const applied = await load({ + background: true, + preserveDraft: true, + notifyParent: false, + shouldApply: () => requestId === autoRefreshRequestId && !hasUnsavedRedisDraft.value && !shouldPauseAutoValueRefresh() && autoRefreshEnabled.value && canRunAutoRefresh(), + }); + if (requestId !== autoRefreshRequestId || !applied || !data.value) return; } catch { // A failed background read must not retry in a tight loop. Manual refresh // remains available and starts a fresh polling lifecycle on success. - if (requestId === ttlRefreshRequestId) { + if (requestId === autoRefreshRequestId) { stopAutoRefresh(); // The user did not turn the preference off, so do not persist this // transient failure. The visible state must still match the stopped @@ -247,29 +256,37 @@ async function refreshTtl() { autoRefreshEnabled.value = false; } } finally { - if (requestId === ttlRefreshRequestId) refreshingTtl.value = false; + if (requestId === autoRefreshRequestId) refreshingValue.value = false; } } function stopAutoRefresh() { - ttlRefreshRequestId++; - refreshingTtl.value = false; + autoRefreshRequestId++; + refreshingValue.value = false; if (autoRefreshTimer !== null) { clearInterval(autoRefreshTimer); autoRefreshTimer = null; } +} + +function stopCountdown() { if (countdownTimer !== null) { clearInterval(countdownTimer); countdownTimer = null; } } +function stopRefreshTimers() { + stopAutoRefresh(); + stopCountdown(); +} + function handleDocumentVisibilityChange() { if (document.visibilityState === "hidden") { - stopAutoRefresh(); + stopRefreshTimers(); return; } - startAutoRefresh(); + startRefreshTimers(); } const hashSortBy = ref<"field" | "value" | null>(null); @@ -476,6 +493,12 @@ let hashResizeStartWidth = 0; let zsetResizeStartX = 0; let zsetResizeStartWidth = 0; +function shouldPauseAutoValueRefresh(): boolean { + const loadedPageSize = data.value ? redisValueCollectionItems(data.value).length : 0; + const hasExpandedCollectionPage = collectionItems.value.length > loadedPageSize; + return showMemberDetail.value || valueSearchOpen.value || Boolean(hashSearchQuery.value.trim()) || Boolean(activeHashSearchQuery.value) || searchLoading.value || loadingMore.value || hasExpandedCollectionPage; +} + type PendingDelete = { kind: "key" } | { kind: "hash"; field: string } | { kind: "list"; index: number } | { kind: "set"; member: string } | { kind: "zset"; member: string }; const pendingDelete = ref(null); @@ -1023,23 +1046,29 @@ const deleteDetails = computed(() => { return t("dangerDialog.redisSetMemberDetails", { key, member: formatValue(pending.member) }); }); -async function load(options: { selectDefaultMember?: boolean; preserveDraft?: boolean } = {}): Promise { +async function load(options: { background?: boolean; notifyParent?: boolean; preserveDraft?: boolean; selectDefaultMember?: boolean; shouldApply?: () => boolean } = {}): Promise { + const background = options.background ?? false; + const notifyParent = options.notifyParent ?? true; const shouldSelectDefaultMember = options.selectDefaultMember ?? true; const requestId = ++loadRequestId; - loading.value = true; + if (!background) loading.value = true; try { const loadedValue = await api.redisGetValue(props.connectionId, props.db, props.keyRaw); - if (requestId !== loadRequestId) return false; + if (requestId !== loadRequestId || (options.shouldApply && !options.shouldApply())) return false; // Redis reports a key that expired between refreshes as a `none` value. // Tell the browser to remove it instead of rendering a stale detail shell. if (loadedValue.redis_type === "none") { + // A background read can finish after the user starts editing. Preserve + // the draft and defer even a missing-key update until the user decides + // whether to save or discard it. + if (background && options.preserveDraft && hasUnsavedRedisDraft.value) return false; data.value = null; collectionItems.value = []; scanCursor.value = undefined; resetStreamEntries(); resetStreamMonitoring(); - stopAutoRefresh(); + stopRefreshTimers(); emit("deleted", props.keyRaw); return true; } @@ -1049,7 +1078,8 @@ async function load(options: { selectDefaultMember?: boolean; preserveDraft?: bo if (currentValue) { const preservedValue = { ...currentValue, ttl: loadedValue.ttl }; data.value = preservedValue; - emit("loaded", preservedValue); + syncCountdownTtl(loadedValue.ttl); + if (notifyParent) emit("loaded", preservedValue); } return false; } @@ -1062,7 +1092,8 @@ async function load(options: { selectDefaultMember?: boolean; preserveDraft?: bo searchLoading.value = false; resetValueSearch(); data.value = loadedValue; - emit("loaded", loadedValue); + syncCountdownTtl(loadedValue.ttl); + if (notifyParent) emit("loaded", loadedValue); scanCursor.value = redisValueCollectionScanCursor(loadedValue); collectionItems.value = redisValueCollectionItems(loadedValue); replaceStreamEntries(loadedValue); @@ -1100,10 +1131,8 @@ async function load(options: { selectDefaultMember?: boolean; preserveDraft?: bo throw error; } finally { if (requestId === loadRequestId) { - loading.value = false; - if (autoRefreshEnabled.value && data.value) { - startAutoRefresh(); - } + if (!background) loading.value = false; + if (!background && data.value) startRefreshTimers(); } } } @@ -1613,7 +1642,7 @@ function requestZsetRemove(member: string | null) { // TTL function currentEditableTtl(): number { if (!data.value) return -1; - return computeTtlForExpiryEdit(autoRefreshEnabled.value, countdownTtl.value, data.value.ttl); + return computeTtlForExpiryEdit(countdownTtl.value, data.value.ttl); } function expiryValidationMessage(reason: "ttl" | "date" | "past"): string { @@ -1956,17 +1985,17 @@ onMounted(() => { }); onActivated(() => { redisValueViewerIsActive = true; - startAutoRefresh(); + startRefreshTimers(); }); onDeactivated(() => { redisValueViewerIsActive = false; - stopAutoRefresh(); + stopRefreshTimers(); }); onBeforeUnmount(() => { window.removeEventListener("pointerdown", handleValueViewerPointerDown, true); document.removeEventListener("visibilitychange", handleDocumentVisibilityChange); redisValueViewerIsActive = false; - stopAutoRefresh(); + stopRefreshTimers(); stopResizeHashColumns(); stopResizeZsetColumns(); if (hashSearchTimer) clearTimeout(hashSearchTimer); @@ -2001,39 +2030,40 @@ defineExpose({ focusSearch });
{{ formatValue(data.key_display) }} - - - - - - - - {{ t("grid.refresh") }} - - - {{ t("redis.autoRefresh") }} - - - - {{ t("serverDashboard.off") }} - - - - - {{ interval }}s - - - +
+ + + + + + + {{ t("redis.autoRefresh") }} + + + + + {{ t("serverDashboard.off") }} + + + + + {{ interval }}s + + + +
@@ -2044,7 +2074,7 @@ defineExpose({ focusSearch }); {{ t("redis.columnSize") }}: {{ metadataSizeLabel }}