feat(redis): improve key detail auto-refresh and TTL polling
This commit is contained in:
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
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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" };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user