feat(redis): add TTL metadata support
This commit is contained in:
1 parent
680358aff8
commit
39ce50b681
15 files changed
+551
-140
No files matched your search
+77
-98
@@ -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", () => {
|
||||
|
||||
@@ -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(" ");
|
||||
}
|
||||
Reference in new issue
Block a user