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,72 @@
const CACHE_NAME = "dada-public-assets-v1";
const DATABASE_NAME = "dada-public-asset-cache-v1";
const LRU_STORE = "public_asset_lru";
const POLICY_STORE = "client_cache_policy";
const PUBLIC_ROUTE = /^\/api\/v1\/assets\/public\/([a-z0-9][a-z0-9._-]{0,79})\/([0-9a-f-]{36})$/i;
function openDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DATABASE_NAME, 1);
request.onupgradeneeded = () => {
const database = request.result;
if (!database.objectStoreNames.contains(LRU_STORE)) database.createObjectStore(LRU_STORE, { keyPath: "resource_id" });
if (!database.objectStoreNames.contains(POLICY_STORE)) database.createObjectStore(POLICY_STORE, { keyPath: "policy_id" });
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error ?? new Error("public_cache_database_open_failed"));
});
}
async function readMetadata(resourceId) {
const database = await openDatabase();
try {
return await new Promise((resolve, reject) => {
const request = database.transaction(LRU_STORE, "readonly").objectStore(LRU_STORE).get(resourceId);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error ?? new Error("public_cache_metadata_read_failed"));
});
} finally {
database.close();
}
}
async function touchMetadata(resourceId, releaseVersion) {
const database = await openDatabase();
try {
await new Promise((resolve, reject) => {
const transaction = database.transaction(LRU_STORE, "readwrite");
const store = transaction.objectStore(LRU_STORE);
const request = store.get(resourceId);
request.onsuccess = () => {
const entry = request.result;
if (entry?.release_version === releaseVersion) store.put({ ...entry, last_accessed_at: Date.now() });
};
transaction.oncomplete = () => resolve();
transaction.onabort = () => reject(transaction.error ?? new Error("public_cache_touch_aborted"));
transaction.onerror = () => reject(transaction.error ?? new Error("public_cache_touch_failed"));
});
} finally {
database.close();
}
}
self.addEventListener("install", () => self.skipWaiting());
self.addEventListener("activate", (event) => event.waitUntil(self.clients.claim()));
self.addEventListener("fetch", (event) => {
if (event.request.method !== "GET") return;
const url = new URL(event.request.url);
if (url.origin !== self.location.origin || url.search || url.hash) return;
const route = PUBLIC_ROUTE.exec(url.pathname);
if (!route) return;
event.respondWith((async () => {
const releaseVersion = route[1];
const resourceId = route[2];
const metadata = await readMetadata(resourceId);
if (!metadata || metadata.release_version !== releaseVersion) return fetch(event.request);
const cached = await (await caches.open(CACHE_NAME)).match(event.request);
if (!cached) return fetch(event.request);
event.waitUntil(touchMetadata(resourceId, releaseVersion));
return cached;
})());
});