diigo-cache: invalidate the last N days of cached pages

invalidateRecent(days) drops the cached pages holding anything diigo
updated within the last N days (on the cache's injected clock), and
every page before them, so the next read or fill refetches them; item
rows stay. Exposed on the fill capability (private and public) and on
the webkey page as "invalidate the last N days" per cache.

A user-requested refetch, not a change in what stored state means.

Generated-by: Claude Opus 5.5
This commit is contained in:
dckc committed 2026-10-07 08:27:13 -05:00
1 parent cffb1fa9b2
commit 904f620f06
5 files changed
+91 -1

No files matched your search

+9 -1
View File
@@ -28,9 +28,10 @@ type Fillable = {
startFill(opts?: { pagesPerStep?: number }): Promise<FillStatus>;
fillStatus(): Promise<FillStatus | undefined>;
stopFill(): Promise<FillStatus>;
invalidateRecent(days: number): Promise<{ pages: number }>;
};
/** a cache's fill job, and nothing else */
/** a cache's fill job and page invalidation, and nothing else */
export class FillCap extends RpcTarget {
#cache: Fillable;
@@ -55,6 +56,13 @@ export class FillCap extends RpcTarget {
async stopFill(): Promise<FillStatus> {
return this.#cache.stopFill();
}
/** drop the cached pages holding anything updated in the last `days`
* days (1–3650), so they refetch */
async invalidateRecent(days: unknown): Promise<{ pages: number }> {
if (typeof days !== "number") throw new Error("days: expected a number");
return this.#cache.invalidateRecent(days);
}
}
/** what the holder of a private cache's key may do */
+13
View File
@@ -28,12 +28,18 @@ export const CACHE_PAGE_HTML = `<!doctype html>
<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>
<p><label>invalidate the last <input type="number" id="private-days" min="1" max="3650" value="7" size="5"> days</label>
<button type="button" data-target="private" data-action="invalidate">invalidate</button>
<span id="private-invalidated"></span></p>
</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>
<p><label>invalidate the last <input type="number" id="public-days" min="1" max="3650" value="7" size="5"> days</label>
<button type="button" data-target="public" data-action="invalidate">invalidate</button>
<span id="public-invalidated"></span></p>
</section>
<p><a href="/">← home</a></p>
<script type="module">
@@ -105,6 +111,13 @@ async function main() {
button.addEventListener("click", () => {
const { target, action } = button.dataset;
const j = job(target);
if (action === "invalidate") {
const days = Number($(target + "-days").value);
j.invalidateRecent(days)
.then((r) => ($(target + "-invalidated").textContent = "dropped " + r.pages + " cached pages; fill to refetch"))
.catch(report);
return;
}
(action === "start" ? j.startFill() : j.stopFill())
.then(() => refresh(hasPublic))
.catch(report);
+31
View File
@@ -245,6 +245,37 @@ abstract class PagedCache extends DurableObject<CacheEnv> {
return stopped;
}
/**
* drop the cached pages holding anything diigo updated in the last
* `days` days, and every page before them (newer still; and new
* updates push items back), so the next read or fill refetches them.
* Item rows stay. Returns how many pages were dropped.
*/
async invalidateRecent(days: number): Promise<{ pages: number }> {
if (!Number.isInteger(days) || days < 1 || days > 3650) throw new Error("days: expected a whole number from 1 to 3650");
this.#schema();
const cutoff = Math.floor(this.#clock.now() / 1000) - days * 24 * 60 * 60;
const sql = this.ctx.storage.sql;
const deepest = sql
.exec<{ last: number | null }>(
"select max(page_members.page_num) as last from page_members join items on items.item_id = page_members.item_id where cast(items.updated as integer) >= ?",
cutoff,
)
.toArray()[0]?.last;
if (!deepest) {
console.log({ event: "cache", kind: this.kind, outcome: "invalidated", days, pages: 0 });
return { pages: 0 };
}
let pages = 0;
this.ctx.storage.transactionSync(() => {
pages = Number(sql.exec<{ n: number }>("select count(*) as n from pages where page_num <= ?", deepest).toArray()[0].n);
sql.exec("delete from page_members where page_num <= ?", deepest);
sql.exec("delete from pages where page_num <= ?", deepest);
});
console.log({ event: "cache", kind: this.kind, outcome: "invalidated", days, pages });
return { pages };
}
/** one fill step: up to `fillStep` pages, progress saved after each,
* then the next step is scheduled (or the job ends) */
async alarm(): Promise<void> {