feat(cache): share public snapshots through Redis

This commit is contained in:
2026-08-28 19:53:40 +00:00 Unverified
parent 56460dcf13
commit cc822e6aef
6 changed files with 306 additions and 8 deletions
+42
View File
@@ -0,0 +1,42 @@
import "server-only";
import { cacheLife, cacheTag } from "next/cache";
import { getPublicPage, listPublicCharacters } from "@/lib/content/queries";
import {
characterCacheTag,
dataSourceCacheTag,
DIRECTORY_CACHE_TAG,
pageCacheTag,
vectorCacheTag,
} from "@/lib/cache/tags";
export async function getCachedPublicDirectory() {
"use cache: remote";
cacheLife({ stale: 30, revalidate: 60, expire: 60 * 60 });
cacheTag(DIRECTORY_CACHE_TAG);
return listPublicCharacters();
}
export async function getCachedPublicPage(
characterSlug: string,
pageSlug?: string,
) {
"use cache: remote";
cacheLife({ stale: 15, revalidate: 60, expire: 60 * 60 });
const result = await getPublicPage(characterSlug, pageSlug);
cacheTag(DIRECTORY_CACHE_TAG);
if (result.type === "page") {
cacheTag(
pageCacheTag(result.snapshot.page.id),
characterCacheTag(result.snapshot.character.id),
vectorCacheTag(
result.snapshot.page.id,
result.snapshot.page.version,
result.snapshot.versionVector,
),
...Object.keys(result.snapshot.versionVector).map(dataSourceCacheTag),
);
}
return result;
}
+22
View File
@@ -0,0 +1,22 @@
import { describe, expect, it } from "vitest";
import {
cacheTagsForOutboxEvent,
DIRECTORY_CACHE_TAG,
vectorCacheTag,
} from "./tags";
describe("public cache tags", () => {
it("uses a stable ordered page/data-source version vector", () => {
expect(vectorCacheTag("page", 4, { beta: 2, alpha: 8 })).toBe(
"buzz:vector:page:4:alpha@8,beta@2",
);
});
it("maps outbox events to dependency-aware tags", () => {
expect(cacheTagsForOutboxEvent({ eventType: "directory.updated", aggregateId: "id" })).toEqual([DIRECTORY_CACHE_TAG]);
expect(cacheTagsForOutboxEvent({ eventType: "page.updated", aggregateId: "page" })).toEqual(["buzz:page:page"]);
expect(cacheTagsForOutboxEvent({ eventType: "data-source.updated", aggregateId: "source" })).toEqual(["buzz:data-source:source"]);
expect(cacheTagsForOutboxEvent({ eventType: "admin.updated", aggregateId: "id" })).toEqual([]);
});
});
+39
View File
@@ -0,0 +1,39 @@
export const DIRECTORY_CACHE_TAG = "buzz:directory";
export function pageCacheTag(pageId: string): string {
return `buzz:page:${pageId}`;
}
export function characterCacheTag(characterId: string): string {
return `buzz:character:${characterId}`;
}
export function dataSourceCacheTag(dataSourceId: string): string {
return `buzz:data-source:${dataSourceId}`;
}
export function vectorCacheTag(
pageId: string,
pageVersion: number,
vector: Record<string, number>,
): string {
const serialized = Object.entries(vector)
.sort(([left], [right]) => left.localeCompare(right))
.map(([id, version]) => `${id}@${version}`)
.join(",");
return `buzz:vector:${pageId}:${pageVersion}:${serialized}`;
}
export type OutboxCacheEvent = {
eventType: string;
aggregateId: string;
};
export function cacheTagsForOutboxEvent(event: OutboxCacheEvent): string[] {
if (event.eventType === "directory.updated") return [DIRECTORY_CACHE_TAG];
if (event.eventType === "page.updated") return [pageCacheTag(event.aggregateId)];
if (event.eventType === "data-source.updated") {
return [dataSourceCacheTag(event.aggregateId)];
}
return [];
}