diigo-cache: the public page only reads

/public/<username> reads through the cache's viewer() facet
(PageViewer: stored pages, knownTotal, fill progress — the reader
alone), so it can't fetch from diigo or write storage.

Behavior change, agreed beforehand: an unfetched page no longer fills
itself on a miss; it says "not fetched yet" (JSON: stored: false), and
the owner fills via the webkey page. The HIT/MISS line becomes "stored
<when>". Strangers can no longer make the cache spend the owner's diigo
session; the README's deployment concern shrinks to /api.

Tests: viewing an unfetched page makes no webapp call; the page appears
once stored; fill progress shows after one fill step.

Generated-by: Claude Opus 5.5
This commit is contained in:
dckc committed 2026-10-07 09:26:09 -05:00
1 parent a17d287e1d
commit d7190abaf8
5 files changed
+54 -33

No files matched your search

+5
View File
@@ -417,6 +417,11 @@ export class PageViewer extends RpcTarget {
return stored && { ...stored, cached: true };
}
/** diigo's total, as last stored, or null if nothing is stored */
knownTotal(): number | null {
return this.#reader.knownTotal();
}
/** the cache's fill job, if any */
fillStatus(): Promise<FillStatus | undefined> {
return this.#fillStatus();
+13 -9
View File
@@ -479,24 +479,28 @@ async function publicPage(request: Request, env: Env, username: string): Promise
const raw = new URL(request.url).searchParams.get("page");
const pageN = raw === null ? 1 : Number.parseInt(raw, 10);
const valid = !Number.isNaN(pageN) && pageN >= 1 && pageN <= 10000;
let page;
// read-only: the viewer facet serves stored pages and cannot fetch
// from diigo or write storage; filling is the owner's (webkey page)
const viewer = await cache.viewer();
let page, total, fill;
try {
// a hit is storage-only; a miss asks the webapp (private rows are
// dropped before etching)
page = await cache.page(valid ? pageN : 1, PUBLIC_PER_PAGE);
page = await viewer.page(valid ? pageN : 1, PUBLIC_PER_PAGE);
total = page ? page.total : await viewer.knownTotal();
fill = await viewer.fillStatus();
} catch (err) {
console.error({ event: "public-page", page: pageN, error: String(err) });
return new Response(`could not load page ${valid ? pageN : 1}: ${String(err)}`, { status: 502 });
} finally {
viewer[Symbol.dispose]();
}
// total and pages count all of diigo's items: public page n is diigo
// page n with the private rows dropped
const pages = Math.max(1, Math.ceil(page.total / PUBLIC_PER_PAGE));
const items = page.items.map((j) => JSON.parse(j) as Item);
const fill = await cache.fillStatus();
const pages = total === null ? 1 : Math.max(1, Math.ceil(total / PUBLIC_PER_PAGE));
const items = page ? page.items.map((j) => JSON.parse(j) as Item) : [];
const accept = request.headers.get("accept") ?? "";
if (accept.includes("application/json") && !accept.includes("text/html")) {
return Response.json({ username, page: pageN, pages, total: page.total, items });
return Response.json({ username, page: pageN, pages, total, stored: page !== null, etchedAt: page?.etchedAt ?? null, items });
}
let notice = "";
@@ -527,7 +531,7 @@ ${items
<body>
<h1>${esc(username)}'s shared bookmarks</h1>
<p>${pages} page${pages === 1 ? "" : "s"} (diigo's, ${PUBLIC_PER_PAGE} bookmarks each; private ones are left out, so a page may show fewer).</p>
<p><strong>cache: ${page.cached ? "HIT (from storage)" : "MISS (fetched from diigo now)"}</strong> — page ${valid ? pageN : 1}, ${items.length} shared</p>
<p>${page ? `<strong>stored ${esc(page.etchedAt ?? "")}</strong> — page ${valid ? pageN : 1}, ${items.length} shared` : `<strong>not fetched yet</strong> — page ${valid ? pageN : 1}; the cache's owner fills it`}</p>
${fill ? fillLine(fill) : ""}
<form method="get" action="${here}">
<p>${nav(pageN - 1, "← prev")}