feat(redis): add local expiration modes

This commit is contained in:
onenewcode authored and GitHub committed 2026-07-26 23:33:56 +08:00
1 parent 3a2dc4225a
commit 51fa058edb
35 files changed
+2918 -98

No files matched your search

@@ -0,0 +1,185 @@
import { readFileSync } from "node:fs";
import { parse } from "vue/compiler-sfc";
import ts from "typescript";
import { describe, expect, it } from "vitest";
const browserSource = readFileSync(new URL("../../../components/redis/RedisKeyBrowser.vue", import.meta.url), "utf8");
const viewerSource = readFileSync(new URL("../../../components/redis/RedisValueViewer.vue", import.meta.url), "utf8");
const localeSources = {
en: readFileSync(new URL("../../../i18n/locales/en.ts", import.meta.url), "utf8"),
es: readFileSync(new URL("../../../i18n/locales/es.ts", import.meta.url), "utf8"),
it: readFileSync(new URL("../../../i18n/locales/it.ts", import.meta.url), "utf8"),
ja: readFileSync(new URL("../../../i18n/locales/ja.ts", import.meta.url), "utf8"),
"pt-BR": readFileSync(new URL("../../../i18n/locales/pt-BR.ts", import.meta.url), "utf8"),
"zh-CN": readFileSync(new URL("../../../i18n/locales/zh-CN.ts", import.meta.url), "utf8"),
"zh-TW": readFileSync(new URL("../../../i18n/locales/zh-TW.ts", import.meta.url), "utf8"),
};
function findFunction(source: string, name: string): ts.FunctionDeclaration {
const parsed = parse(source, { filename: `${name}.vue` });
expect(parsed.errors).toEqual([]);
const script = parsed.descriptor.scriptSetup;
expect(script).toBeDefined();
const program = ts.createSourceFile(`${name}.ts`, script!.content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
const declaration = program.statements.find((statement): statement is ts.FunctionDeclaration => ts.isFunctionDeclaration(statement) && statement.name?.text === name);
expect(declaration).toBeDefined();
return declaration!;
}
function callsIn(node: ts.Node): ts.CallExpression[] {
const calls: ts.CallExpression[] = [];
const visit = (child: ts.Node) => {
if (ts.isCallExpression(child)) calls.push(child);
ts.forEachChild(child, visit);
};
visit(node);
return calls;
}
function callName(call: ts.CallExpression): string | undefined {
if (ts.isIdentifier(call.expression)) return call.expression.text;
if (ts.isPropertyAccessExpression(call.expression)) return `${call.expression.expression.getText()}.${call.expression.name.text}`;
return undefined;
}
describe("Redis expiry mode wiring", () => {
it("writes every create type before applying exactly one shared expiration policy", () => {
const create = findFunction(browserSource, "createRedisKey");
const calls = callsIn(create);
const expectedWriterArgumentCounts: Record<string, number> = {
"api.redisSetString": 4,
"api.redisJsonSet": 4,
"api.redisHashSet": 5,
"api.redisListPush": 4,
"api.redisSetAdd": 4,
"api.redisZadd": 5,
"api.redisStreamAdd": 5,
};
for (const [name, argumentCount] of Object.entries(expectedWriterArgumentCounts)) {
const writerCalls = calls.filter((call) => callName(call) === name);
expect(writerCalls.length, `${name} should be used by the create flow`).toBeGreaterThan(0);
expect(
writerCalls.every((call) => call.arguments.length === argumentCount),
`${name} must not receive a per-write TTL`,
).toBe(true);
}
expect(calls.map(callName)).toContain("applyRedisExpiryPolicy");
expect(create.getText()).toContain("if (creatingKey.value) return;");
expect(calls.map(callName)).not.toContain("api.redisSetTtl");
expect(calls.map(callName)).not.toContain("api.redisSetExpireAt");
expect(create.getText().lastIndexOf("applyRedisExpiryPolicy")).toBeGreaterThan(create.getText().lastIndexOf("api.redisStreamAdd"));
expect(findFunction(browserSource, "syncWrittenKey").getText()).toContain('if (created.redis_type === "none")');
expect(create.getText()).toContain('toast(t("redis.keyExpiredBeforeDisplay"), 3000)');
expect(create.getText()).toContain("if (wroteValue && writtenKeyRaw)");
expect(findFunction(browserSource, "reflectWrittenKey").getText()).toContain("removeKnownKey(keyRaw)");
});
it("keeps only unwritten structured entries after a later writer fails", () => {
const create = findFunction(browserSource, "createRedisKey").getText();
const partialRecoveryStart = create.indexOf("if (writingStructuredEntries)");
const genericRecoveryStart = create.lastIndexOf("if (wroteValue && writtenKeyRaw)");
const partialRecovery = create.slice(partialRecoveryStart, genericRecoveryStart);
expect(create).toContain("let writingStructuredEntries = false;");
expect(create).toContain("const pendingEntries = createKeyEntries.value.slice();");
expect(create.match(/for \(const entry of pendingEntries\)/g)).toHaveLength(4);
expect(create.match(/removeWrittenCreateKeyEntry\(entry\.id\)/g)).toHaveLength(4);
expect(create).toContain("createKeyPartiallyWritten.value = true;");
expect(partialRecovery).toContain("await reflectWrittenKey(writtenKeyRaw);");
expect(partialRecovery).toContain('t("redis.createKeyPartialWrite")');
expect(partialRecovery).not.toContain("showCreateKeyDialog.value = false;");
expect(browserSource).toContain(':disabled="creatingKey || createKeyPartiallyWritten"');
expect(findFunction(browserSource, "onCreateKeyDialogOpenChange").getText()).toContain("if (!open && creatingKey.value) return;");
expect(findFunction(browserSource, "onCreateKeyTypeChange").getText()).toContain("if (creatingKey.value || createKeyPartiallyWritten.value) return;");
});
it("defaults TTL editing by server state, supports absolute prefill, and keeps teleported controls inside the editor", () => {
const start = findFunction(viewerSource, "startEditTtl").getText();
const save = findFunction(viewerSource, "saveTtl").getText();
const currentTtl = findFunction(viewerSource, "currentEditableTtl").getText();
expect(start).toContain("redisExpiryModeForTtl(ttl)");
expect(currentTtl).toContain("computeTtlForExpiryEdit(autoRefreshEnabled.value, countdownTtl.value, data.value.ttl)");
expect(viewerSource).toContain("unixSecondsToCalendarDateTime(Math.ceil(Date.now() / 1_000) + ttl)");
expect(save).toContain("validateRedisExpiry");
expect(save).toContain("applyRedisExpiryPolicy");
expect(save).toContain("toast(errorMessage(error), 3000)");
expect(viewerSource).toContain('"[data-date-time-picker-content]"');
expect(viewerSource).toContain('"[data-redis-expiry-mode-content]"');
expect(viewerSource).toContain('v-model="ttlExpireAt" compact');
expect(viewerSource.match(/as="button"/g) ?? []).toHaveLength(2);
expect(viewerSource).toContain(":aria-label=\"t('redis.expiry')\"");
expect(viewerSource).toContain(":aria-label=\"t('grid.save')\"");
});
it("moves keyboard focus to the active TTL editor control", () => {
const start = findFunction(viewerSource, "startEditTtl").getText();
const focus = findFunction(viewerSource, "focusTtlExpiryControl").getText();
expect(start).toContain("focusTtlExpiryControl()");
expect(focus).toContain('mode === "ttl"');
expect(focus).toContain("ttlInputEl.value?.$el?.focus()");
expect(focus).toContain("[data-slot='select-trigger']");
expect(focus).toContain("[data-date-time-picker-trigger]");
expect(viewerSource).toContain("if (editingTtl.value && mode !== previousMode) focusTtlExpiryControl(mode);");
});
it("serializes expiry saves and preserves unrelated value drafts", () => {
const save = findFunction(viewerSource, "saveTtl").getText();
const load = findFunction(viewerSource, "load").getText();
expect(save).toContain("if (savingTtl.value) return;");
expect(save).toContain("savingTtl.value = true;");
expect(save).toContain("await refreshTtlState(error);");
expect(save).toContain("const refreshError = await refreshTtlState();");
expect(save).toContain("savingTtl.value = false;");
expect(load).toContain("const preservedValue = { ...currentValue, ttl: loadedValue.ttl };");
expect(viewerSource).toContain(':disabled="savingTtl"');
});
it("keeps expiry controls compact and inline in the value header", () => {
expect(viewerSource).toContain("flex min-w-0 max-w-full flex-wrap items-center gap-1");
expect(viewerSource).toContain('SelectTrigger size="sm"');
expect(viewerSource).toContain("max-w-[min(100%,14rem)]");
expect(viewerSource).toContain('v-model="ttlExpireAt" compact');
expect(viewerSource).not.toContain("full-width");
expect(viewerSource).not.toContain("basis-full");
expect(viewerSource).not.toContain("redis-expiry-mode-trigger");
expect(viewerSource).not.toContain('class="h-6 w-30 text-xs"');
});
it("uses the positive-seconds TTL placeholder in both expiration editors", () => {
const obsoletePlaceholders = {
en: "empty = no expiry",
es: "vacío = sin expiración",
it: "vuoto = senza scadenza",
ja: "空 = 期限なし",
"pt-BR": "vazio = sem expiração",
"zh-CN": "留空表示永不过期",
"zh-TW": "留空表示永不過期",
};
for (const [locale, source] of Object.entries(localeSources)) {
const ttlPlaceholder = source.match(/createKeyTtlPlaceholder: "([^"]+)"/)?.[1];
expect(ttlPlaceholder, locale).toBeDefined();
expect(source, locale).toContain("createKeyPartialWrite:");
expect(ttlPlaceholder, locale).not.toBe(obsoletePlaceholders[locale as keyof typeof obsoletePlaceholders]);
}
expect(browserSource).toContain("t('redis.createKeyTtlPlaceholder')");
expect(viewerSource).toContain("t('redis.createKeyTtlPlaceholder')");
});
it("removes a key that expires while its detail view is refreshing", () => {
const load = findFunction(viewerSource, "load").getText();
const deleted = findFunction(browserSource, "onKeyDeleted").getText();
const loaded = findFunction(browserSource, "onKeyLoaded").getText();
expect(load).toContain('if (loadedValue.redis_type === "none")');
expect(load).toContain('emit("deleted", props.keyRaw)');
expect(deleted).toContain("removeKnownKey(keyRaw)");
expect(loaded).toContain('if (value.redis_type === "none")');
});
});
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { computeAutoRefreshTick, computeDisplayTtl, shouldStopAutoRefresh } from "@/lib/redis/redisAutoRefresh";
import { computeAutoRefreshTick, computeDisplayTtl, computeTtlForExpiryEdit, shouldStopAutoRefresh } from "@/lib/redis/redisAutoRefresh";
describe("computeAutoRefreshTick", () => {
it("returns idle when auto-refresh is disabled", () => {
@@ -64,3 +64,17 @@ describe("computeDisplayTtl", () => {
expect(computeDisplayTtl(true, -1, 10)).toBe(0);
});
});
describe("computeTtlForExpiryEdit", () => {
it("keeps a last-confirmed positive TTL during the in-flight zero-countdown window", () => {
expect(computeTtlForExpiryEdit(true, 0, 5)).toBe(5);
expect(computeTtlForExpiryEdit(true, -1, 5)).toBe(5);
});
it("uses the live positive countdown and preserves non-expiring server states", () => {
expect(computeTtlForExpiryEdit(true, 3, 10)).toBe(3);
expect(computeTtlForExpiryEdit(false, 3, 10)).toBe(10);
expect(computeTtlForExpiryEdit(true, 0, -1)).toBe(-1);
expect(computeTtlForExpiryEdit(true, 0, -2)).toBe(-2);
});
});
@@ -0,0 +1,99 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
invoke: vi.fn(),
}));
vi.mock("@tauri-apps/api/core", () => ({
invoke: mocks.invoke,
}));
vi.mock("@tauri-apps/api/event", () => ({
listen: vi.fn(),
}));
describe("Redis expiry Tauri API", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.unstubAllGlobals();
});
it("invokes EXPIREAT with the Unix timestamp", async () => {
mocks.invoke.mockResolvedValue(undefined);
const { redisSetExpireAt } = await import("@/lib/backend/tauri");
await redisSetExpireAt("redis-1", 2, "c2Vzc2lvbg==", 1_735_689_600);
expect(mocks.invoke).toHaveBeenCalledWith("redis_set_expire_at", {
connectionId: "redis-1",
db: 2,
keyRaw: "c2Vzc2lvbg==",
expireAt: 1_735_689_600,
});
});
it("forwards relative TTL and persist values unchanged", async () => {
mocks.invoke.mockResolvedValue(undefined);
const { redisSetTtl } = await import("@/lib/backend/tauri");
await redisSetTtl("redis-1", 2, "c2Vzc2lvbg==", 90);
await redisSetTtl("redis-1", 2, "c2Vzc2lvbg==", -1);
expect(mocks.invoke).toHaveBeenNthCalledWith(1, "redis_set_ttl", {
connectionId: "redis-1",
db: 2,
keyRaw: "c2Vzc2lvbg==",
ttl: 90,
});
expect(mocks.invoke).toHaveBeenNthCalledWith(2, "redis_set_ttl", {
connectionId: "redis-1",
db: 2,
keyRaw: "c2Vzc2lvbg==",
ttl: -1,
});
});
});
describe("Redis expiry HTTP API", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.unstubAllGlobals();
});
function stubFetch() {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue(undefined),
});
vi.stubGlobal("fetch", fetchMock);
return fetchMock;
}
function lastCall(fetchMock: ReturnType<typeof stubFetch>): { url: string; body: Record<string, unknown> } {
const [url, init] = fetchMock.mock.calls.at(-1) as [string, RequestInit];
return { url, body: JSON.parse(String(init.body)) as Record<string, unknown> };
}
it("posts relative TTL, persist, and absolute expiration requests", async () => {
const fetchMock = stubFetch();
const { redisSetExpireAt, redisSetTtl } = await import("@/lib/backend/http");
await redisSetTtl("redis-1", 2, "c2Vzc2lvbg==", 90);
expect(lastCall(fetchMock)).toEqual({
url: "/api/redis/set-ttl",
body: { connectionId: "redis-1", db: 2, keyRaw: "c2Vzc2lvbg==", ttl: 90 },
});
await redisSetTtl("redis-1", 2, "c2Vzc2lvbg==", -1);
expect(lastCall(fetchMock)).toEqual({
url: "/api/redis/set-ttl",
body: { connectionId: "redis-1", db: 2, keyRaw: "c2Vzc2lvbg==", ttl: -1 },
});
await redisSetExpireAt("redis-1", 2, "c2Vzc2lvbg==", 1_735_689_600);
expect(lastCall(fetchMock)).toEqual({
url: "/api/redis/set-expire-at",
body: { connectionId: "redis-1", db: 2, keyRaw: "c2Vzc2lvbg==", expireAt: 1_735_689_600 },
});
});
});
+1
View File
@@ -395,6 +395,7 @@ export const redisStreamAdd = forward("redisStreamAdd");
export const redisJsonSet = forward("redisJsonSet");
export const redisCheckJsonModule = forward("redisCheckJsonModule");
export const redisSetTtl = forward("redisSetTtl");
export const redisSetExpireAt = forward("redisSetExpireAt");
export const redisDeleteKeys = forward("redisDeleteKeys");
export const redisFlushDb = forward("redisFlushDb");
export const redisExecuteCommand = forward("redisExecuteCommand");
+4
View File
@@ -2012,6 +2012,10 @@ export async function redisSetTtl(connectionId: string, db: number, keyRaw: stri
return post("/api/redis/set-ttl", { connectionId, db, keyRaw, ttl });
}
export async function redisSetExpireAt(connectionId: string, db: number, keyRaw: string, expireAt: number): Promise<void> {
return post("/api/redis/set-expire-at", { connectionId, db, keyRaw, expireAt });
}
export async function redisDeleteKeys(connectionId: string, db: number, keyRaws: string[]): Promise<number> {
return post("/api/redis/delete-keys", { connectionId, db, keyRaws });
}
+4
View File
@@ -1731,6 +1731,10 @@ export async function redisSetTtl(connectionId: string, db: number, keyRaw: stri
return invoke("redis_set_ttl", { connectionId, db, keyRaw, ttl });
}
export async function redisSetExpireAt(connectionId: string, db: number, keyRaw: string, expireAt: number): Promise<void> {
return invoke("redis_set_expire_at", { connectionId, db, keyRaw, expireAt });
}
export async function redisDeleteKeys(connectionId: string, db: number, keyRaws: string[]): Promise<number> {
return invoke("redis_delete_keys", { connectionId, db, keyRaws });
}
@@ -41,3 +41,16 @@ export function shouldStopAutoRefresh(ttl: number): boolean {
export function computeDisplayTtl(autoRefreshEnabled: boolean, countdownTtl: number, serverTtl: number): number {
return autoRefreshEnabled ? Math.max(countdownTtl, 0) : serverTtl;
}
/**
* Choose the TTL used to initialize an expiry edit.
*
* A zero countdown can be a transient state while the final background refresh
* is still in flight. Do not turn a last-confirmed positive Redis TTL into
* PERSIST during that window.
*/
export function computeTtlForExpiryEdit(autoRefreshEnabled: boolean, countdownTtl: number, serverTtl: number): number {
if (serverTtl <= 0) return serverTtl;
const displayedTtl = computeDisplayTtl(autoRefreshEnabled, countdownTtl, serverTtl);
return displayedTtl > 0 ? displayedTtl : serverTtl;
}
+73
View File
@@ -0,0 +1,73 @@
import { CalendarDateTime, resetLocalTimeZone, setLocalTimeZone } from "@internationalized/date";
import { afterEach, describe, expect, it, vi } from "vitest";
import { calendarDateTimeToUnixSeconds, formatLocalDateTime, parseLocalDateTime, unixSecondsToCalendarDateTime } from "@/components/ui/date-time-picker/dateTimePicker";
import { applyRedisExpiryPolicy, parseRedisTtl, redisExpiryModeForTtl, validateRedisExpiry } from "./redisExpiry";
afterEach(() => {
resetLocalTimeZone();
});
describe("Redis expiry helpers", () => {
it("only accepts safe positive whole-second TTLs", () => {
expect(parseRedisTtl("")).toBeNull();
expect(parseRedisTtl("0")).toBeNull();
expect(parseRedisTtl("-1")).toBeNull();
expect(parseRedisTtl("1.5")).toBeNull();
expect(parseRedisTtl("1seconds")).toBeNull();
expect(parseRedisTtl(String(Number.MAX_SAFE_INTEGER + 1))).toBeNull();
expect(parseRedisTtl(" 60 ")).toBe(60);
});
it("maps Redis TTL metadata to the expected initial mode", () => {
expect(redisExpiryModeForTtl(-2)).toBe("none");
expect(redisExpiryModeForTtl(-1)).toBe("none");
expect(redisExpiryModeForTtl(1)).toBe("ttl");
});
it("validates empty, invalid, past, and future absolute expiry times", () => {
const now = new Date(2024, 1, 29, 12, 0, 0).getTime();
const future = new CalendarDateTime(2024, 2, 29, 12, 0, 1);
const past = new CalendarDateTime(2024, 2, 29, 11, 59, 59);
expect(validateRedisExpiry("none", "", null, now)).toEqual({ valid: true, policy: { mode: "none" } });
expect(validateRedisExpiry("ttl", "0", null, now)).toEqual({ valid: false, reason: "ttl" });
expect(validateRedisExpiry("at", "", null, now)).toEqual({ valid: false, reason: "date" });
expect(validateRedisExpiry("at", "", past, now)).toEqual({ valid: false, reason: "past" });
expect(validateRedisExpiry("at", "", future, now)).toEqual({
valid: true,
policy: { mode: "at", expireAt: calendarDateTimeToUnixSeconds(future) },
});
});
it("treats DST gaps and overlaps as invalid absolute expiry times", () => {
setLocalTimeZone("America/Los_Angeles");
const now = new Date("2024-01-01T00:00:00Z").getTime();
expect(validateRedisExpiry("at", "", new CalendarDateTime(2024, 3, 10, 2, 30, 0), now)).toEqual({ valid: false, reason: "date" });
expect(validateRedisExpiry("at", "", new CalendarDateTime(2024, 11, 3, 1, 30, 0), now)).toEqual({ valid: false, reason: "date" });
});
it("keeps local calendar fields when parsing, formatting, and converting", () => {
const leap = parseLocalDateTime("2024-02-29 23:45:06");
expect(leap).not.toBeNull();
expect(formatLocalDateTime(leap!)).toBe("2024-02-29 23:45:06");
expect(parseLocalDateTime("2024-02-30 23:45:06")).toBeNull();
expect(parseLocalDateTime("2024-02-29T23:45:06")?.toString()).toBe("2024-02-29T23:45:06");
const roundTrip = unixSecondsToCalendarDateTime(calendarDateTimeToUnixSeconds(leap!));
expect(formatLocalDateTime(roundTrip)).toBe("2024-02-29 23:45:06");
});
it("uses PERSIST, EXPIRE, or EXPIREAT once according to the selected policy", async () => {
const transport = { setTtl: vi.fn().mockResolvedValue(undefined), setExpireAt: vi.fn().mockResolvedValue(undefined) };
await applyRedisExpiryPolicy(transport, "connection", 3, "key", { mode: "none" });
await applyRedisExpiryPolicy(transport, "connection", 3, "key", { mode: "ttl", ttl: 45 });
await applyRedisExpiryPolicy(transport, "connection", 3, "key", { mode: "at", expireAt: 1_735_689_600 });
expect(transport.setTtl).toHaveBeenNthCalledWith(1, "connection", 3, "key", -1);
expect(transport.setTtl).toHaveBeenNthCalledWith(2, "connection", 3, "key", 45);
expect(transport.setExpireAt).toHaveBeenCalledOnce();
expect(transport.setExpireAt).toHaveBeenCalledWith("connection", 3, "key", 1_735_689_600);
});
});
+57
View File
@@ -0,0 +1,57 @@
import type { CalendarDateTime } from "@internationalized/date";
import { calendarDateTimeToUnixSeconds } from "@/components/ui/date-time-picker/dateTimePicker";
export type RedisExpiryMode = "none" | "ttl" | "at";
export type RedisExpiryPolicy = { mode: "none" } | { mode: "ttl"; ttl: number } | { mode: "at"; expireAt: number };
export type RedisExpiryValidation = { valid: true; policy: RedisExpiryPolicy } | { valid: false; reason: "ttl" | "date" | "past" };
export interface RedisExpiryTransport {
setTtl: (connectionId: string, db: number, keyRaw: string, ttl: number) => Promise<void>;
setExpireAt: (connectionId: string, db: number, keyRaw: string, expireAt: number) => Promise<void>;
}
/** Parse the EXPIRE argument without accepting partial, negative, or unsafe values. */
export function parseRedisTtl(value: string): number | null {
const trimmed = value.trim();
if (!/^[1-9]\d*$/.test(trimmed)) return null;
const ttl = Number(trimmed);
return Number.isSafeInteger(ttl) ? ttl : null;
}
export function redisExpiryModeForTtl(ttl: number): RedisExpiryMode {
return ttl > 0 ? "ttl" : "none";
}
export function validateRedisExpiry(mode: RedisExpiryMode, ttlInput: string, expireAt: CalendarDateTime | null, now = Date.now()): RedisExpiryValidation {
if (mode === "none") return { valid: true, policy: { mode } };
if (mode === "ttl") {
const ttl = parseRedisTtl(ttlInput);
return ttl === null ? { valid: false, reason: "ttl" } : { valid: true, policy: { mode, ttl } };
}
if (!expireAt) return { valid: false, reason: "date" };
let timestamp: number;
try {
timestamp = calendarDateTimeToUnixSeconds(expireAt);
} catch {
return { valid: false, reason: "date" };
}
return timestamp * 1_000 <= now ? { valid: false, reason: "past" } : { valid: true, policy: { mode, expireAt: timestamp } };
}
/** Applies exactly one post-write Redis expiration command for a validated policy. */
export async function applyRedisExpiryPolicy(transport: RedisExpiryTransport, connectionId: string, db: number, keyRaw: string, policy: RedisExpiryPolicy): Promise<void> {
if (policy.mode === "none") {
await transport.setTtl(connectionId, db, keyRaw, -1);
return;
}
if (policy.mode === "ttl") {
await transport.setTtl(connectionId, db, keyRaw, policy.ttl);
return;
}
await transport.setExpireAt(connectionId, db, keyRaw, policy.expireAt);
}