feat(redis): add TTL metadata support

This commit is contained in:
dadong authored and GitHub committed 2026-08-20 01:51:11 +08:00
1 parent 680358aff8
commit 39ce50b681
15 files changed
+551 -140

No files matched your search

+77 -98
View File
@@ -2,48 +2,38 @@ import { describe, expect, it } from "vitest";
import { formatTtl } from "@/lib/common/ttlFormat";
// ---------------------------------------------------------------------------
// Mock `t()` helpers — only the day unit needs localization
// Mock `t()` helpers — day/hour/minute/second units are all localized
// ---------------------------------------------------------------------------
function enT(key: string, options?: Record<string, unknown>): string {
const messages: Record<string, string> = {
"redis.ttlDay": "{count}d",
function makeT(messages: Record<string, string>): (key: string, options?: Record<string, unknown>) => string {
return (key, options) => {
const msg = messages[key] ?? key;
const count = (options as { count?: number })?.count;
if (count != null) return msg.replace("{count}", String(count));
return msg;
};
const msg = messages[key] ?? key;
const count = (options as { count?: number })?.count;
if (count != null) return msg.replace("{count}", String(count));
return msg;
}
function zhT(key: string, options?: Record<string, unknown>): string {
const messages: Record<string, string> = {
"redis.ttlDay": "{count}天",
};
const msg = messages[key] ?? key;
const count = (options as { count?: number })?.count;
if (count != null) return msg.replace("{count}", String(count));
return msg;
}
const enT = makeT({
"redis.ttlDay": "{count}d",
"redis.ttlHour": "{count}h",
"redis.ttlMinute": "{count}m",
"redis.ttlSecond": "{count}s",
});
function esT(key: string, options?: Record<string, unknown>): string {
const messages: Record<string, string> = {
"redis.ttlDay": "{count}d",
};
const msg = messages[key] ?? key;
const count = (options as { count?: number })?.count;
if (count != null) return msg.replace("{count}", String(count));
return msg;
}
const zhT = makeT({
"redis.ttlDay": "{count}天",
"redis.ttlHour": "{count}小时",
"redis.ttlMinute": "{count}分钟",
"redis.ttlSecond": "{count}秒",
});
function itT(key: string, options?: Record<string, unknown>): string {
const messages: Record<string, string> = {
"redis.ttlDay": "{count}g",
};
const msg = messages[key] ?? key;
const count = (options as { count?: number })?.count;
if (count != null) return msg.replace("{count}", String(count));
return msg;
}
const itT = makeT({
"redis.ttlDay": "{count}g",
"redis.ttlHour": "{count}h",
"redis.ttlMinute": "{count}m",
"redis.ttlSecond": "{count}s",
});
// ---------------------------------------------------------------------------
// Tests
@@ -64,93 +54,82 @@ describe("formatTtl — special values", () => {
});
});
describe("formatTtl — sub-day, HH:mm:ss (no i18n)", () => {
it("formats 45s as 00:00:45", () => {
expect(formatTtl(45, enT)).toBe("00:00:45");
describe("formatTtl — sub-hour, single unit or minute+second", () => {
it("formats 45s with only the seconds unit", () => {
expect(formatTtl(45, enT)).toBe("45s");
expect(formatTtl(45, zhT)).toBe("45秒");
});
it("formats 1m as 00:01:00", () => {
expect(formatTtl(60, enT)).toBe("00:01:00");
it("formats 1m with only the minutes unit", () => {
expect(formatTtl(60, enT)).toBe("1m");
expect(formatTtl(60, zhT)).toBe("1分钟");
});
it("formats 1m 30s as 00:01:30", () => {
expect(formatTtl(90, enT)).toBe("00:01:30");
it("formats 1m 30s as two units", () => {
expect(formatTtl(90, enT)).toBe("1m 30s");
expect(formatTtl(90, zhT)).toBe("1分钟 30秒");
});
it("formats 2m 5s as 00:02:05", () => {
expect(formatTtl(125, enT)).toBe("00:02:05");
});
it("formats 1h as 01:00:00", () => {
expect(formatTtl(3600, enT)).toBe("01:00:00");
});
it("formats 1h 1m 1s as 01:01:01", () => {
expect(formatTtl(3661, enT)).toBe("01:01:01");
});
it("formats 23h 59m 59s as 23:59:59", () => {
expect(formatTtl(86399, enT)).toBe("23:59:59");
it("formats 2m 5s as two units", () => {
expect(formatTtl(125, enT)).toBe("2m 5s");
expect(formatTtl(125, zhT)).toBe("2分钟 5秒");
});
});
describe("formatTtl — multi-day, {n}d HH:mm:ss (day unit localized)", () => {
it("formats exactly 1 day as 1d 00:00:00", () => {
expect(formatTtl(86400, enT)).toBe("1d 00:00:00");
expect(formatTtl(86400, zhT)).toBe("1天 00:00:00");
describe("formatTtl — sub-day, keeps only the two largest non-zero units", () => {
it("formats 1h with only the hours unit", () => {
expect(formatTtl(3600, enT)).toBe("1h");
expect(formatTtl(3600, zhT)).toBe("1小时");
});
it("formats 7 days as 7d 00:00:00", () => {
expect(formatTtl(604800, enT)).toBe("7d 00:00:00");
expect(formatTtl(604800, zhT)).toBe("7天 00:00:00");
it("formats 1h 1m 1s by dropping the seconds unit", () => {
expect(formatTtl(3661, enT)).toBe("1h 1m");
expect(formatTtl(3661, zhT)).toBe("1小时 1分钟");
});
it("formats 365 days as 365d 00:00:00", () => {
expect(formatTtl(31536000, enT)).toBe("365d 00:00:00");
expect(formatTtl(31536000, zhT)).toBe("365天 00:00:00");
it("formats 23h 59m 59s by dropping the seconds unit", () => {
expect(formatTtl(86399, enT)).toBe("23h 59m");
expect(formatTtl(86399, zhT)).toBe("23小时 59分钟");
});
});
describe("formatTtl — multi-day, day unit plus hours when present", () => {
it("formats exactly 1 day with only the day unit", () => {
expect(formatTtl(86400, enT)).toBe("1d");
expect(formatTtl(86400, zhT)).toBe("1天");
});
it("formats 1d 1h 1m 1s", () => {
expect(formatTtl(90061, enT)).toBe("1d 01:01:01");
expect(formatTtl(90061, zhT)).toBe("1天 01:01:01");
it("formats 7 days with only the day unit", () => {
expect(formatTtl(604800, enT)).toBe("7d");
expect(formatTtl(604800, zhT)).toBe("7天");
});
it("formats 2d 3h 4m 5s", () => {
it("formats 365 days with only the day unit", () => {
expect(formatTtl(31536000, enT)).toBe("365d");
expect(formatTtl(31536000, zhT)).toBe("365天");
});
it("formats 1d 1h 1m 1s by keeping only day and hour", () => {
expect(formatTtl(90061, enT)).toBe("1d 1h");
expect(formatTtl(90061, zhT)).toBe("1天 1小时");
});
it("formats 2d 3h 4m 5s by keeping only day and hour", () => {
const ttl = 2 * 86400 + 3 * 3600 + 4 * 60 + 5; // 183845
expect(formatTtl(ttl, enT)).toBe("2d 03:04:05");
expect(formatTtl(ttl, zhT)).toBe("2天 03:04:05");
});
});
describe("formatTtl — Spanish locale", () => {
it("formats sub-day values with no i18n", () => {
expect(formatTtl(45, esT)).toBe("00:00:45");
expect(formatTtl(3661, esT)).toBe("01:01:01");
});
it("formats multi-day with localized d", () => {
expect(formatTtl(86400, esT)).toBe("1d 00:00:00");
expect(formatTtl(604800, esT)).toBe("7d 00:00:00");
});
it("returns null for -1", () => {
expect(formatTtl(-1, esT)).toBeNull();
});
it("returns null for 0", () => {
expect(formatTtl(0, esT)).toBeNull();
expect(formatTtl(ttl, enT)).toBe("2d 3h");
expect(formatTtl(ttl, zhT)).toBe("2天 3小时");
});
});
describe("formatTtl — Italian locale", () => {
it("formats sub-day values with no i18n", () => {
expect(formatTtl(45, itT)).toBe("00:00:45");
expect(formatTtl(3661, itT)).toBe("01:01:01");
it("formats multi-day with localized g", () => {
expect(formatTtl(86400, itT)).toBe("1g");
expect(formatTtl(604800, itT)).toBe("7g");
});
it("formats multi-day with localized g", () => {
expect(formatTtl(86400, itT)).toBe("1g 00:00:00");
expect(formatTtl(604800, itT)).toBe("7g 00:00:00");
it("formats sub-day with localized units", () => {
expect(formatTtl(3661, itT)).toBe("1h 1m");
expect(formatTtl(45, itT)).toBe("45s");
});
it("returns null for -1", () => {
+17 -8
View File
@@ -1,7 +1,9 @@
/**
* Formats Redis TTL (seconds) as a compact string:
* < 1 day → HH:mm:ss
* >= 1 day → {n}d HH:mm:ss (day unit localized)
* Formats Redis TTL (seconds) as a compact human-readable remaining time:
* 从大到小取前两个非零单位,例如:
* 45 → 45s / 45秒
* 3661 → 1h 1m / 1小时 1分钟
* 93784 → 1d 2h / 1天 2小时
*
* Returns null for ttl === -1 (no expiry) or ttl <= 0.
*/
@@ -17,10 +19,17 @@ export function formatTtl(ttl: number, t: TranslateFn): string | null {
const minutes = Math.floor((ttl % 3_600) / 60);
const seconds = ttl % 60;
const time = [String(hours).padStart(2, "0"), String(minutes).padStart(2, "0"), String(seconds).padStart(2, "0")].join(":");
// 按从大到小排列四个单位,只保留前两个非零单位,避免展示过长
const units: Array<[number, string]> = [
[days, "redis.ttlDay"],
[hours, "redis.ttlHour"],
[minutes, "redis.ttlMinute"],
[seconds, "redis.ttlSecond"],
];
if (days > 0) {
return `${t("redis.ttlDay", { count: days })} ${time}`;
}
return time;
return units
.filter(([value]) => value > 0)
.slice(0, 2)
.map(([value, key]) => t(key, { count: value }))
.join(" ");
}