diigo-cache: fill from a webkey page over Cap'n Web
Starting a fill no longer POSTs the cache id to /_fill. Instead: - the created page links the cache's webkey, /cache/#<cache id>: a skeleton page, the same for every key, that reads the key from the fragment (never sent to the server or in a Referer) - its script trades the key in-band for a capability over Cap'n Web at /api: CacheApi.cache(key) returns a CacheCap facet (fill, status, fingerprint, the public companion's fill) and never the DO's wiring methods; malformed and unknown keys get one refusal that never echoes the key, and each refusal is logged without it - then calls startFill(): trade and call pipeline into one round trip - the worker serves capnweb's browser build itself at /_capnweb.js (a wrangler Text rule), so no third-party script sits beside the key The /_fill route and its forms are gone. Tests run the real capnweb client against the worker (fetch routed to worker.fetch). Generated-by: Claude Opus 5.5
This commit is contained in:
1 parent
fbe2538345
commit
8c221abe6f
9 files changed
+397
-112
No files matched your search
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* @file the webkey page at /cache/ — a skeleton, identical for every key
|
||||
* (docs/webkeys.md): the key rides the URL fragment, which browsers never
|
||||
* send to the server or in a Referer. The inline module reads it, trades
|
||||
* it for a capability (`api.cache(key)`, Cap'n Web over /api, in-band),
|
||||
* and drives the fill job. Each round trip is its own HTTP batch session:
|
||||
* calls made before an await travel together.
|
||||
*/
|
||||
|
||||
export const CACHE_PAGE_HTML = `<!doctype html>
|
||||
<html><head><meta charset="utf-8"><meta name="referrer" content="no-referrer">
|
||||
<title>diigo cache</title></head>
|
||||
<body>
|
||||
<h1>diigo cache <code id="fingerprint"></code></h1>
|
||||
<p id="problem" hidden></p>
|
||||
<section id="private" hidden>
|
||||
<h2>private cache</h2>
|
||||
<p id="private-status">no fill job yet</p>
|
||||
<button type="button" data-target="private" data-action="start">fill</button>
|
||||
<button type="button" data-target="private" data-action="stop">stop</button>
|
||||
</section>
|
||||
<section id="public" hidden>
|
||||
<h2>public cache</h2>
|
||||
<p id="public-status">no fill job yet</p>
|
||||
<button type="button" data-target="public" data-action="start">fill</button>
|
||||
<button type="button" data-target="public" data-action="stop">stop</button>
|
||||
</section>
|
||||
<p><a href="/">← home</a></p>
|
||||
<script type="module">
|
||||
import { newHttpBatchRpcSession } from "/_capnweb.js";
|
||||
|
||||
const key = location.hash.slice(1);
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const problem = (text) => {
|
||||
$("problem").textContent = text;
|
||||
$("problem").hidden = false;
|
||||
};
|
||||
/** a fresh batch session, traded for this key's capability */
|
||||
const cap = () => newHttpBatchRpcSession("/api").cache(key);
|
||||
/** the fill-job stub for a target */
|
||||
const job = (target) => (target === "public" ? cap().publicCache() : cap());
|
||||
|
||||
const describe = (s) => {
|
||||
if (!s) return "no fill job yet";
|
||||
const of = Math.max(0, s.nextPage - 1) + " of " + (s.pages ?? "?") + " pages";
|
||||
return {
|
||||
running: "filling: " + of,
|
||||
done: "filled: " + of,
|
||||
stopped: "fill stopped: " + of,
|
||||
failed: "fill failed at page " + s.nextPage + ": " + (s.error ?? "unknown error"),
|
||||
}[s.state];
|
||||
};
|
||||
|
||||
let polling = false;
|
||||
async function refresh(hasPublic) {
|
||||
const targets = hasPublic ? ["private", "public"] : ["private"];
|
||||
const statuses = await Promise.all(targets.map((t) => job(t).fillStatus()));
|
||||
targets.forEach((t, i) => ($(t + "-status").textContent = describe(statuses[i])));
|
||||
const running = statuses.some((s) => s?.state === "running");
|
||||
if (running && !polling) {
|
||||
polling = true;
|
||||
setTimeout(() => {
|
||||
polling = false;
|
||||
refresh(hasPublic).catch(report);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
function report(err) {
|
||||
console.error({ event: "cache-page", error: String(err) });
|
||||
problem(String(err));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!key) return problem("no key: this page's address should end in #<cache id>");
|
||||
const c = cap();
|
||||
const [fingerprint, hasPublic] = await Promise.all([c.fingerprint(), c.hasPublic()]);
|
||||
$("fingerprint").textContent = fingerprint;
|
||||
$("private").hidden = false;
|
||||
$("public").hidden = !hasPublic;
|
||||
for (const button of document.querySelectorAll("button[data-target]")) {
|
||||
button.addEventListener("click", () => {
|
||||
const { target, action } = button.dataset;
|
||||
const j = job(target);
|
||||
(action === "start" ? j.startFill() : j.stopFill())
|
||||
.then(() => refresh(hasPublic))
|
||||
.catch(report);
|
||||
});
|
||||
}
|
||||
await refresh(hasPublic);
|
||||
}
|
||||
main().catch(report);
|
||||
</script>
|
||||
</body></html>`;
|
||||
Reference in new issue
Block a user