feat: complete TASK-WP0-06 public cache

This commit is contained in:
suyx
2026-07-27 23:05:23 +08:00
parent e12a83d6d3
commit 2dd6f5c2c2
11 changed files with 775 additions and 2 deletions
@@ -0,0 +1,5 @@
<!doctype html>
<html lang="zh-CN">
<head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>Dada public cache probe</title></head>
<body><main><h1>Public cache probe</h1><output id="status">starting</output></main><script type="module" src="/tests/e2e/fixtures/public-asset-cache.ts"></script></body>
</html>
+25
View File
@@ -0,0 +1,25 @@
import {
createPublicAssetCacheForTest,
registerPublicAssetServiceWorker,
} from "../../../apps/web/src/public-asset-cache.js";
declare global {
interface Window {
dadaCacheProbe: {
cache: ReturnType<typeof createPublicAssetCacheForTest>;
register(): Promise<void>;
};
}
}
let tick = 1_000;
const cache = createPublicAssetCacheForTest({ maxBytes: 20, now: () => ++tick });
window.dadaCacheProbe = {
cache,
async register() {
await registerPublicAssetServiceWorker();
await navigator.serviceWorker.ready;
document.querySelector("#status")!.textContent = "ready";
},
};
void window.dadaCacheProbe.register();
+121
View File
@@ -0,0 +1,121 @@
import { mkdirSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { expect, test } from "@playwright/test";
import { createServer, type ViteDevServer } from "vite";
const resources = new Map([
["123e4567-e89b-42d3-a456-426614174001", "aaaaaaaa"],
["123e4567-e89b-42d3-a456-426614174002", "bbbbbbbb"],
["123e4567-e89b-42d3-a456-426614174003", "cccccccc"],
]);
let vite: ViteDevServer;
let webUrl: string;
let publicFetchCount = 0;
test.beforeAll(async () => {
vite = await createServer({
configFile: false,
plugins: [{
name: "wp0-06-public-assets",
configureServer(server) {
server.middlewares.use((request, response, next) => {
const match = request.url?.match(/^\/api\/v1\/assets\/public\/[^/]+\/([0-9a-f-]+)$/i);
if (!match) return next();
const body = resources.get(match[1] ?? "");
if (!body) { response.statusCode = 404; response.end(); return; }
publicFetchCount += 1;
response.setHeader("Cache-Control", "public, max-age=31536000, immutable");
response.setHeader("Content-Type", "application/octet-stream");
response.end(body);
});
},
}],
publicDir: resolve("apps/web/public"),
root: process.cwd(),
server: { host: "127.0.0.1", port: 0 },
});
await vite.listen();
const address = vite.httpServer?.address();
if (!address || typeof address === "string") throw new Error("Vite did not expose a test port.");
webUrl = `http://127.0.0.1:${address.port}`;
});
test.afterAll(async () => vite.close());
test("TDD-WP0-CACHE-001-public-lru enumerates isolated browser persistence", async ({ page }) => {
publicFetchCount = 0;
await page.goto(`${webUrl}/tests/e2e/fixtures/public-asset-cache.html`);
await expect(page.locator("#status")).toHaveText("ready");
await page.reload();
await expect(page.locator("#status")).toHaveText("ready");
const result = await page.evaluate(async () => {
const ids = [
"123e4567-e89b-42d3-a456-426614174001",
"123e4567-e89b-42d3-a456-426614174002",
"123e4567-e89b-42d3-a456-426614174003",
];
const descriptor = (resource_id: string, release_version: string) => ({
access_class: "public_release_asset" as const,
cache_kind: "thumbnail" as const,
release_version,
resource_id,
});
const cache = window.dadaCacheProbe.cache;
await cache.clear();
const first = await cache.cache(descriptor(ids[0]!, "asset-20260727.1"));
const second = await cache.cache(descriptor(ids[1]!, "asset-20260727.1"));
await fetch(`/api/v1/assets/public/asset-20260727.1/${ids[0]}`);
const third = await cache.cache(descriptor(ids[2]!, "asset-20260727.2"));
const rejected = [];
for (const [access_class, cache_kind] of [
["internal_preview_asset", "thumbnail"], ["private_user_asset", "thumbnail"],
["public_release_asset", "project"], ["public_release_asset", "prompt"],
["public_release_asset", "verification_code"], ["public_release_asset", "session"],
["public_release_asset", "unsaved_edit"], ["public_release_asset", "user_download"],
]) {
rejected.push(await cache.cache({ access_class, cache_kind, release_version: "asset-20260727.2", resource_id: ids[0] }));
}
const enumeration = await cache.inspect();
const registrations = await navigator.serviceWorker.getRegistrations();
return {
enumeration,
local_storage_keys: Object.keys(localStorage),
rejected,
results: [first, second, third],
service_workers: registrations.map((item) => ({ active: item.active?.state, scope: item.scope, script_url: item.active?.scriptURL })),
session_storage_keys: Object.keys(sessionStorage),
};
});
expect(result.results[2]).toMatchObject({ evicted_resource_ids: ["123e4567-e89b-42d3-a456-426614174002"], status: "cached" });
expect(result.enumeration.policy.current_public_cache_bytes).toBe(16);
expect(result.enumeration.policy.max_public_cache_bytes).toBe(20);
expect(result.enumeration.entries.map((item: { resource_id: string }) => item.resource_id).sort()).toEqual([
"123e4567-e89b-42d3-a456-426614174001",
"123e4567-e89b-42d3-a456-426614174003",
]);
expect(result.enumeration.cache_keys).toHaveLength(2);
expect(result.enumeration.indexed_db_names).toEqual(["dada-public-asset-cache-v1"]);
expect(result.rejected).toEqual(Array.from({ length: 8 }, () => ({ status: "rejected_not_allowlisted" })));
expect(result.local_storage_keys).toEqual([]);
expect(result.session_storage_keys).toEqual([]);
expect(result.service_workers).toHaveLength(1);
expect(publicFetchCount).toBe(3);
const evidenceDirectory = process.env.DADA_EVIDENCE_DIR_CACHE;
if (evidenceDirectory) {
mkdirSync(evidenceDirectory, { recursive: true });
writeFileSync(resolve(evidenceDirectory, "cache-enumeration.json"), `${JSON.stringify({
business_database_calls: 0,
public_network_fetches: publicFetchCount,
...result,
}, null, 2)}\n`);
writeFileSync(resolve(evidenceDirectory, "lru-trace.json"), `${JSON.stringify({
capacity_driver: { production_max_bytes: 157_286_400, test_max_bytes: 20 },
prewrite_eviction: true,
results: result.results,
}, null, 2)}\n`);
}
});
+119
View File
@@ -0,0 +1,119 @@
import { createHash } from "node:crypto";
import { describe, expect, it } from "vitest";
import { createApp } from "../../apps/api/src/app.js";
import {
MAX_PUBLIC_CACHE_BYTES,
buildPublicAssetUrl,
isPublicCacheDescriptor,
isPublicServiceWorkerRoute,
planPublicCacheWrite,
type PublicAssetLruEntry,
} from "../../apps/web/src/public-asset-cache.js";
const resourceIds = [
"123e4567-e89b-42d3-a456-426614174001",
"123e4567-e89b-42d3-a456-426614174002",
"123e4567-e89b-42d3-a456-426614174003",
"123e4567-e89b-42d3-a456-426614174004",
] as const;
describe("TDD-WP0-CACHE-001 public allowlist LRU", () => {
it("freezes the 150 MiB policy and allows exact equality", () => {
expect(MAX_PUBLIC_CACHE_BYTES).toBe(157_286_400);
const plan = planPublicCacheWrite([], {
bytes: MAX_PUBLIC_CACHE_BYTES,
last_accessed_at: 4,
release_version: "asset-20260727.2",
resource_id: resourceIds[0],
}, MAX_PUBLIC_CACHE_BYTES);
expect(plan).toEqual({ accepted: true, evict: [], resulting_bytes: MAX_PUBLIC_CACHE_BYTES });
expect(planPublicCacheWrite([], {
bytes: MAX_PUBLIC_CACHE_BYTES + 1,
last_accessed_at: 4,
release_version: "asset-20260727.2",
resource_id: resourceIds[0],
}, MAX_PUBLIC_CACHE_BYTES)).toEqual({ accepted: false, evict: [], resulting_bytes: 0 });
});
it("evicts old releases first and then the least recently used entry before writing", () => {
const entries: PublicAssetLruEntry[] = [
{ bytes: 40, last_accessed_at: 30, release_version: "asset-20260727.1", resource_id: resourceIds[0] },
{ bytes: 40, last_accessed_at: 10, release_version: "asset-20260727.1", resource_id: resourceIds[1] },
{ bytes: 40, last_accessed_at: 1, release_version: "asset-20260727.2", resource_id: resourceIds[2] },
];
const plan = planPublicCacheWrite(entries, {
bytes: 40,
last_accessed_at: 40,
release_version: "asset-20260727.2",
resource_id: resourceIds[3],
}, 120);
expect(plan).toEqual({ accepted: true, evict: [resourceIds[1]], resulting_bytes: 120 });
});
it.each([
{ access_class: "internal_preview_asset", cache_kind: "thumbnail" },
{ access_class: "private_user_asset", cache_kind: "thumbnail" },
{ access_class: "public_release_asset", cache_kind: "sticker_original" },
{ access_class: "public_release_asset", cache_kind: "generation" },
{ access_class: "public_release_asset", cache_kind: "export" },
{ access_class: "public_release_asset", cache_kind: "project" },
{ access_class: "public_release_asset", cache_kind: "prompt" },
{ access_class: "public_release_asset", cache_kind: "verification_code" },
{ access_class: "public_release_asset", cache_kind: "session" },
{ access_class: "public_release_asset", cache_kind: "unsaved_edit" },
{ access_class: "public_release_asset", cache_kind: "user_download" },
])("rejects non-allowlisted candidate $access_class/$cache_kind", (candidate) => {
expect(isPublicCacheDescriptor({
...candidate,
release_version: "asset-20260727.2",
resource_id: resourceIds[0],
})).toBe(false);
});
it.each(["thumbnail", "template_conversion", "font"] as const)("allows public %s only", (cacheKind) => {
expect(isPublicCacheDescriptor({
access_class: "public_release_asset",
cache_kind: cacheKind,
release_version: "asset-20260727.2",
resource_id: resourceIds[0],
})).toBe(true);
});
it("constructs and intercepts only same-origin versioned public asset routes", () => {
const url = buildPublicAssetUrl("asset-20260727.2", resourceIds[0]);
expect(url).toBe(`/api/v1/assets/public/asset-20260727.2/${resourceIds[0]}`);
expect(isPublicServiceWorkerRoute(new URL(url, "http://127.0.0.1:43121"), "http://127.0.0.1:43121")).toBe(true);
for (const path of [
`/api/v1/assets/preview/asset-20260727.2/${resourceIds[0]}`,
`/api/v1/private-assets/${resourceIds[0]}`,
`/api/v1/projects/${resourceIds[0]}`,
"/api/v1/support/check",
"file:///C:/private.png",
]) {
expect(isPublicServiceWorkerRoute(new URL(path, "http://127.0.0.1:43121"), "http://127.0.0.1:43121")).toBe(false);
}
});
it("serves existing public binaries as immutable versioned responses with a strong ETag", { timeout: 15_000 }, async () => {
const bytes = Buffer.from("public-thumbnail");
const sha256 = createHash("sha256").update(bytes).digest("hex");
const app = await createApp({
browserGate: false,
publicAssets: {
read: () => ({ assetId: resourceIds[0], bytes, mimeType: "image/png", resourceVersion: "asset-20260727.2", sha256 }),
},
});
const response = await app.inject({
headers: { host: "127.0.0.1:43121" },
method: "GET",
url: `/api/v1/assets/public/asset-20260727.2/${resourceIds[0]}`,
});
expect(response.statusCode).toBe(200);
expect(response.headers["cache-control"]).toBe("public, max-age=31536000, immutable");
expect(response.headers.etag).toBe(`"sha256-${sha256}"`);
expect(response.headers["x-content-type-options"]).toBe("nosniff");
await app.close();
});
});