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

+16 -9
View File
@@ -2,7 +2,7 @@ import { env } from "cloudflare:workers";
import { evictDurableObject, runInDurableObject } from "cloudflare:test";
import { describe, expect, it, vi } from "vitest";
import worker from "../src/index.js";
import { toWebkey } from "../src/webkeys.js";
import { idFromWebkey, toWebkey } from "../src/webkeys.js";
import { FETCH_BUDGET } from "../src/query.js";
import { PAGE_SIZE } from "../src/cache.js";
@@ -225,22 +225,28 @@ describe("worker routes", () => {
const call = (path: string, init?: RequestInit) =>
worker.fetch(new Request(`https://diigo-cache.test${path}`, init), env as never);
it("wires a public cache whose page fills itself on a miss", async () => {
it("the public page only reads: an unfetched page says so; the owner's fill makes it appear", async () => {
const wired = await call("/_wire", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ webappId: WEBAPP_ID, mode: "public" }),
});
expect(wired.status).toBe(200);
const { username } = (await wired.json()) as { username: string };
const { username, publicId } = (await wired.json()) as { username: string; publicId: string };
expect(username).toBe("alice_99");
// page 2 was never fetched by anyone: the public cache asks the webapp
// page 2 was never fetched: the public page can't fetch it
const before = await testEnv.WEBAPP.calls();
const unfetched = await (await call(`/public/${username}?page=2`)).text();
expect(unfetched).toContain('<form method="get"');
expect(unfetched).toContain("<strong>not fetched yet</strong>");
expect(await testEnv.WEBAPP.calls()).toBe(before);
// the owner's side stores it (here, directly on the public cache)
const pub = testEnv.PUBLIC_DO.get(testEnv.PUBLIC_DO.idFromString(idFromWebkey(publicId)));
await pub.page(2, PAGE_SIZE);
const html = await (await call(`/public/${username}?page=2`)).text();
expect(html).toContain('<form method="get"');
expect(html).toContain("<strong>cache: MISS");
const again = await (await call(`/public/${username}?page=2`)).text();
expect(again).toContain("<strong>cache: HIT");
expect(html).toMatch(/<strong>stored \d{4}-\d\d-\d\dT[\d:.]+Z<\/strong>/);
// PAGE_SIZE 100: page 2 is bookmarks 101..200
expect(html).toContain(">bookmark 130<");
// the date comes first, in toISOString() form
@@ -249,8 +255,9 @@ describe("worker routes", () => {
const json = (await (
await call(`/public/${username}?page=2`, { headers: { accept: "application/json" } })
).json()) as { pages: number; items: unknown[] };
).json()) as { pages: number; stored: boolean; items: unknown[] };
expect(json.pages).toBe(3);
expect(json.stored).toBe(true);
expect(json.items).toHaveLength(67); // 101..200 minus its 33 multiples of 3
});
+8 -5
View File
@@ -4,7 +4,7 @@ import { describe, expect, it, vi } from "vitest";
import worker from "../src/index.js";
import { newHttpBatchRpcSession } from "capnweb";
import type { CacheApi } from "../src/api.js";
import { toWebkey } from "../src/webkeys.js";
import { idFromWebkey, toWebkey } from "../src/webkeys.js";
import { FIXED_NOW } from "./test-worker.js";
import { PAGE_SIZE } from "../src/cache.js";
@@ -203,7 +203,7 @@ describe("fill via webkey and Cap'n Web (goal 1e)", () => {
headers: { "content-type": "application/json" },
body: JSON.stringify({ webappId: "fake-webapp-id", mode }),
});
return (await res.json()) as { cacheId: string; username?: string };
return (await res.json()) as { cacheId: string; username?: string; publicId?: string };
};
it("trading the key and starting the fill pipeline into one round trip", async () => {
@@ -332,14 +332,17 @@ describe("fill via webkey and Cap'n Web (goal 1e)", () => {
expect((await call("/_fill", { method: "POST" })).status).toBe(404);
});
it("the public page shows a running fill's progress", async () => {
const { cacheId, username } = await wireJson("public");
it("the public page shows a fill's progress", async () => {
const { cacheId, username, publicId } = (await wireJson("public")) as { cacheId: string; username: string; publicId: string };
const { api, done } = session();
try {
await api.cache(cacheId).publicCache().startFill();
await api.cache(cacheId).publicCache().startFill({ pagesPerStep: 1 });
} finally {
done();
}
// one step of the job: page 1 stored, so the page count is known
const pub = testEnv.PUBLIC_DO.get(testEnv.PUBLIC_DO.idFromString(idFromWebkey(publicId)));
await runDurableObjectAlarm(pub);
const html = await (await call(`/public/${username}`)).text();
expect(html).toMatch(/fill(ed|ing)[^<]*\d+ of \d+ pages/);
});