feat(redis): improve key detail auto-refresh and TTL polling

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-07-31 02:41:26 +08:00
1 parent a662d12f57
commit c697d0c7ad
15 files changed
+540 -106

No files matched your search

@@ -234,7 +234,7 @@ describe("native RedisJSON editor", () => {
});
it("keeps retained drafts out of background refresh and exposes word wrap for every JSON editor", () => {
const autoRefresh = findFunction("startAutoRefresh");
const refreshTtl = findFunction("refreshTtl");
const hashSearch = findFunction("onHashSearch");
const viewMember = findFunction("viewMember");
const setMemberValueFormat = findFunction("setMemberValueFormat");
@@ -243,10 +243,9 @@ describe("native RedisJSON editor", () => {
const labels = templateElements(parsedViewer.descriptor.template!.ast as unknown as TemplateElement);
const stringTextarea = findTemplateElement((element) => element.tag === "textarea" && directiveExpression(element, "model") === "editValue");
const memberTextarea = findTemplateElement((element) => element.tag === "textarea" && directiveExpression(element, "model") === "memberEditValue");
const refreshButton = findTemplateElement((element) => element.tag === "Button" && directiveExpression(element, "on", "click") === "refreshValueAndStreamGroups");
const refreshItem = findTemplateElement((element) => element.tag === "DropdownMenuItem" && directiveExpression(element, "on", "select") === "refreshValueAndStreamGroups");
expect(autoRefresh.getText()).toContain("if (hasUnsavedRedisDraft.value) return;");
expect(autoRefresh.getText()).toContain("load({ preserveDraft: true })");
expect(refreshTtl.getText().match(/hasUnsavedRedisDraft\.value/g)).toHaveLength(2);
expect(hashSearch.getText()).toContain("if (!hasRetainedMemberDraft.value) clearSelectedMember();");
expect(viewMember.getText()).toContain("hasRetainedMemberDraft.value");
// Clean JSON → other format must clear memberDraftFormat so rawText is not compared to the pretty baseline.
@@ -259,6 +258,6 @@ describe("native RedisJSON editor", () => {
expect(labels.some((element) => element.tag === "label" && directiveExpression(element, "if") === "isTextRedisFormat(memberValueView)")).toBe(true);
expect(directiveExpression(stringTextarea, "bind", "readonly")).toBe("!canEditCurrentStringFormat || savingString");
expect(directiveExpression(memberTextarea, "bind", "readonly")).toBe("savingMember");
expect(directiveExpression(refreshButton, "bind", "disabled")).toBe("hasUnsavedRedisDraft");
expect(directiveExpression(refreshItem, "bind", "disabled")).toBe("hasUnsavedRedisDraft");
});
});
+16 -33
View File
@@ -1,48 +1,31 @@
import { describe, expect, it } from "vitest";
import { computeAutoRefreshTick, computeDisplayTtl, computeTtlForExpiryEdit, shouldStopAutoRefresh } from "@/lib/redis/redisAutoRefresh";
import { computeAutoRefreshTick, computeDisplayTtl, computeTtlForExpiryEdit, DEFAULT_REDIS_AUTO_REFRESH_INTERVAL_SECONDS, normalizeRedisAutoRefreshInterval } from "@/lib/redis/redisAutoRefresh";
describe("computeAutoRefreshTick", () => {
it("returns idle when auto-refresh is disabled", () => {
expect(computeAutoRefreshTick(false, 10, false)).toEqual({ type: "idle" });
// countdown value and loading flag don't matter when disabled
expect(computeAutoRefreshTick(false, 0, false)).toEqual({ type: "idle" });
expect(computeAutoRefreshTick(false, 5, true)).toEqual({ type: "idle" });
expect(computeAutoRefreshTick(false, 10)).toEqual({ type: "idle" });
expect(computeAutoRefreshTick(false, 0)).toEqual({ type: "idle" });
expect(computeAutoRefreshTick(false, 5)).toEqual({ type: "idle" });
});
it("returns decrement while more than one second remains", () => {
expect(computeAutoRefreshTick(true, 10, false)).toEqual({ type: "decrement" });
// decrement even when loading — countdown should keep ticking
expect(computeAutoRefreshTick(true, 3, true)).toEqual({ type: "decrement" });
it("returns decrement while a positive TTL remains", () => {
expect(computeAutoRefreshTick(true, 10)).toEqual({ type: "decrement" });
expect(computeAutoRefreshTick(true, 1)).toEqual({ type: "decrement" });
});
it("refreshes on the expiry tick instead of exposing a zero countdown", () => {
expect(computeAutoRefreshTick(true, 1, false)).toEqual({ type: "refresh" });
expect(computeAutoRefreshTick(true, 0, false)).toEqual({ type: "refresh" });
expect(computeAutoRefreshTick(true, -1, false)).toEqual({ type: "refresh" });
});
it("does not start another refresh when a load is already in flight", () => {
expect(computeAutoRefreshTick(true, 1, true)).toEqual({ type: "decrement" });
// This prevents concurrent load() calls
expect(computeAutoRefreshTick(true, 0, true)).toEqual({ type: "idle" });
expect(computeAutoRefreshTick(true, -1, true)).toEqual({ type: "idle" });
it("stops decrementing after the TTL has reached zero", () => {
expect(computeAutoRefreshTick(true, 0)).toEqual({ type: "idle" });
expect(computeAutoRefreshTick(true, -1)).toEqual({ type: "idle" });
});
});
describe("shouldStopAutoRefresh", () => {
it("returns true when TTL is zero (key expired)", () => {
expect(shouldStopAutoRefresh(0)).toBe(true);
});
it("returns true when TTL is negative (no expiry or key deleted)", () => {
expect(shouldStopAutoRefresh(-1)).toBe(true);
expect(shouldStopAutoRefresh(-2)).toBe(true);
});
it("returns false when TTL is positive (key still alive)", () => {
expect(shouldStopAutoRefresh(1)).toBe(false);
expect(shouldStopAutoRefresh(3600)).toBe(false);
describe("normalizeRedisAutoRefreshInterval", () => {
it("uses the default for invalid values and clamps arbitrary input to safe seconds", () => {
expect(normalizeRedisAutoRefreshInterval("invalid")).toBe(DEFAULT_REDIS_AUTO_REFRESH_INTERVAL_SECONDS);
expect(normalizeRedisAutoRefreshInterval(0)).toBe(1);
expect(normalizeRedisAutoRefreshInterval(1.9)).toBe(1);
expect(normalizeRedisAutoRefreshInterval(9_999)).toBe(3600);
});
});
+1
View File
@@ -392,6 +392,7 @@ export const redisScanKeys = forward("redisScanKeys");
export const redisScanKeysBatch = forward("redisScanKeysBatch");
export const redisScanValues = forward("redisScanValues");
export const redisGetValue = forward("redisGetValue");
export const redisGetTtl = forward("redisGetTtl");
export const redisGetStreamEntries = forward("redisGetStreamEntries");
export const redisGetStreamGroups = forward("redisGetStreamGroups");
export const redisGetStreamConsumers = forward("redisGetStreamConsumers");
+4
View File
@@ -2259,6 +2259,10 @@ export async function redisGetValue(connectionId: string, db: number, keyRaw: st
return post("/api/redis/get-value", { connectionId, db, keyRaw });
}
export async function redisGetTtl(connectionId: string, db: number, keyRaw: string): Promise<number> {
return post("/api/redis/get-ttl", { connectionId, db, keyRaw });
}
export async function redisGetStreamEntries(connectionId: string, db: number, keyRaw: string, cursor?: string): Promise<RedisStreamPage> {
return post("/api/redis/get-stream-entries", { connectionId, db, keyRaw, cursor });
}
+4
View File
@@ -2090,6 +2090,10 @@ export async function redisGetValue(connectionId: string, db: number, keyRaw: st
return invoke("redis_get_value", { connectionId, db, keyRaw });
}
export async function redisGetTtl(connectionId: string, db: number, keyRaw: string): Promise<number> {
return invoke("redis_get_ttl", { connectionId, db, keyRaw });
}
export async function redisGetStreamEntries(connectionId: string, db: number, keyRaw: string, cursor?: string): Promise<RedisStreamPage> {
return invoke("redis_get_stream_entries", { connectionId, db, keyRaw, cursor });
}
+17 -19
View File
@@ -5,31 +5,29 @@
* state-machine logic without mounting a Vue component.
*/
/** Action computed after evaluating one auto-refresh tick. */
export type AutoRefreshTickAction = { type: "idle" } | { type: "decrement" } | { type: "refresh" };
export const MIN_REDIS_AUTO_REFRESH_INTERVAL_SECONDS = 1;
export const MAX_REDIS_AUTO_REFRESH_INTERVAL_SECONDS = 3600;
export const DEFAULT_REDIS_AUTO_REFRESH_INTERVAL_SECONDS = 5;
/**
* Evaluate one tick of the auto-refresh interval.
*
* @param enabled Whether auto-refresh is currently toggled on.
* @param countdownTtl Current countdown value in seconds.
* @param isLoading Whether a data load is already in flight.
* @returns The action the caller should take this tick.
*/
export function computeAutoRefreshTick(enabled: boolean, countdownTtl: number, isLoading: boolean): AutoRefreshTickAction {
if (!enabled) return { type: "idle" };
if (isLoading) return countdownTtl > 0 ? { type: "decrement" } : { type: "idle" };
return countdownTtl <= 1 ? { type: "refresh" } : { type: "decrement" };
/** Action computed after evaluating one visible TTL countdown tick. */
export type AutoRefreshTickAction = { type: "idle" } | { type: "decrement" };
/** Keep a user-entered polling frequency within a safe, whole-second range. */
export function normalizeRedisAutoRefreshInterval(value: unknown): number {
const seconds = typeof value === "number" ? value : Number(value);
if (!Number.isFinite(seconds)) return DEFAULT_REDIS_AUTO_REFRESH_INTERVAL_SECONDS;
return Math.min(MAX_REDIS_AUTO_REFRESH_INTERVAL_SECONDS, Math.max(MIN_REDIS_AUTO_REFRESH_INTERVAL_SECONDS, Math.floor(seconds)));
}
/**
* Determine whether auto-refresh should be automatically disabled
* after a server load completes (e.g. key expired or deleted).
* Evaluate one one-second visible TTL countdown tick.
*
* @param ttl The TTL value returned by the server (seconds, -1 = no expiry).
* @param enabled Whether auto-refresh is currently toggled on.
* @param countdownTtl Current countdown value in seconds.
* @returns The action the caller should take this tick.
*/
export function shouldStopAutoRefresh(ttl: number): boolean {
return ttl <= 0;
export function computeAutoRefreshTick(enabled: boolean, countdownTtl: number): AutoRefreshTickAction {
return enabled && countdownTtl > 0 ? { type: "decrement" } : { type: "idle" };
}
/**