feat(redis): add auto-refresh countdown for key TTL display

* @
feat(redis): add auto-refresh countdown for Redis key TTL display

在 Redis 值查看器的 TTL 徽章旁增加自动刷新按钮,选中时显示 TTL 实时倒计时
(每秒递减),倒计时归零自动刷新数据,刷新按钮持续旋转。TTL 到期或网络异常
时自动关闭自动刷新避免死循环。

改动:
- RedisValueViewer.vue:新增 autoRefresh 状态、定时器与倒计时逻辑,模板增加
  Clock 切换按钮,RefreshCw 按钮支持 animate-spin 旋转
- load() 内复位倒计时;onBeforeUnmount 清理定时器

修复(review):
- #I01 TTL 到期/连接断开后自动关闭 autoRefresh 避免无限重试
- #C01 load() reject 增加 .catch() 处理 UnhandledPromiseRejection

i18n:zh-CN / zh-TW / en / ja / es / it / pt-BR 7 种语言补充 autoRefresh key
@

* @
test(redis): extract auto-refresh logic and add unit tests

将自动刷新核心决策逻辑提取为 lib/redis/redisAutoRefresh.ts 纯函数模块,
并编写 11 个单元测试覆盖状态机转换、TTL 显示计算和自动停止条件。

新增:
- lib/redis/redisAutoRefresh.ts:computeAutoRefreshTick / shouldStopAutoRefresh /
  computeDisplayTtl 纯函数
- lib/__tests__/redis/redisAutoRefresh.spec.ts:11 个测试用例

重构:
- RedisValueViewer.vue 改用提取后的纯函数,确保测试覆盖即实际运行逻辑
@
This commit is contained in:
Abeautifulsnow authored and GitHub committed 2026-07-08 15:37:27 +08:00
1 parent 73bd133886
commit a5359798dc
10 files changed
+190 -3

No files matched your search

@@ -0,0 +1,68 @@
import { describe, expect, it } from "vitest";
import { computeAutoRefreshTick, computeDisplayTtl, shouldStopAutoRefresh } 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" });
});
it("returns decrement when countdown is above zero", () => {
expect(computeAutoRefreshTick(true, 10, false)).toEqual({ type: "decrement" });
expect(computeAutoRefreshTick(true, 1, false)).toEqual({ type: "decrement" });
// decrement even when loading — countdown should keep ticking
expect(computeAutoRefreshTick(true, 3, true)).toEqual({ type: "decrement" });
});
it("returns refresh when countdown reaches zero and not loading", () => {
expect(computeAutoRefreshTick(true, 0, false)).toEqual({ type: "refresh" });
expect(computeAutoRefreshTick(true, -1, false)).toEqual({ type: "refresh" });
});
it("returns idle when countdown is zero but a load is already in flight", () => {
// This prevents concurrent load() calls
expect(computeAutoRefreshTick(true, 0, true)).toEqual({ type: "idle" });
expect(computeAutoRefreshTick(true, -1, true)).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("computeDisplayTtl", () => {
it("returns server TTL when auto-refresh is disabled", () => {
expect(computeDisplayTtl(false, 3, 10)).toBe(10);
});
it("returns server TTL when countdown has not started (zero)", () => {
// countdownTtl starts at 0 before first tick; show server TTL
expect(computeDisplayTtl(true, 0, 10)).toBe(10);
});
it("returns live countdown when auto-refresh is active and counting", () => {
expect(computeDisplayTtl(true, 5, 10)).toBe(5);
expect(computeDisplayTtl(true, 1, 10)).toBe(1);
});
it("returns server TTL when countdown would go below zero", () => {
// Edge case: countdownTtl shouldn't be negative in practice,
// but if it is, fall back to server TTL
expect(computeDisplayTtl(true, -1, 10)).toBe(10);
});
});
@@ -0,0 +1,47 @@
/**
* Pure decision functions for Redis key auto-refresh countdown.
*
* Extracted from RedisValueViewer.vue to enable unit testing of the
* 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" };
/**
* 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 (countdownTtl > 0) return { type: "decrement" };
if (countdownTtl <= 0 && !isLoading) return { type: "refresh" };
return { type: "idle" };
}
/**
* Determine whether auto-refresh should be automatically disabled
* after a server load completes (e.g. key expired or deleted).
*
* @param ttl The TTL value returned by the server (seconds, -1 = no expiry).
*/
export function shouldStopAutoRefresh(ttl: number): boolean {
return ttl <= 0;
}
/**
* Compute the TTL value that should be displayed in the badge.
*
* When auto-refresh is active and countdown is running, the live
* countdown value is shown. Otherwise the last known server TTL is used.
*/
export function computeDisplayTtl(autoRefreshEnabled: boolean, countdownTtl: number, serverTtl: number): number {
if (autoRefreshEnabled && countdownTtl > 0) {
return countdownTtl;
}
return serverTtl;
}