diigo-cache: fetch 128 items per diigo page; refuse short pages

PAGE_SIZE = 128 (was 24), one constant for the fill job, public pages,
dumps, and DiigoBridge, so a cache never mixes sizes: ~131 pages instead
of ~698 for 16.7k bookmarks.

Cache invalidation, agreed beforehand: pages are cached by (page number,
size), so pages cached at 24 become misses and refetch at 128. Items are
keyed by link_id and just re-upsert; nothing is lost.

Unknown whether diigo honors count=128, so a short page that isn't the
last is refused loudly (logged, thrown; a fill fails with the reason)
rather than silently skipping items. The fake gains a capped account to
test it; the plain account grows to 300 (3 pages), the deep one to 6000.

Generated-by: Claude Opus 5.5
This commit is contained in:
dckc committed 2026-10-07 04:56:56 -05:00
1 parent efaf74023d
commit 826153e35f
5 files changed
+111 -47

No files matched your search

+17 -13
View File
@@ -1,14 +1,17 @@
/**
* @file a stand-in for webapp's `WebAppFactory` entrypoint, for tests: an
* auxiliary worker (plain JS — the vitest pool doesn't transform aux
* workers) serving diigo-shaped `load_user_items` pages. 30 items, every
* third private; one webapp id is known. Counts its diigo calls so tests
* workers) serving diigo-shaped `load_user_items` pages. 300 items (3
* pages of 128), every third private; a few webapp ids are known. Counts its diigo calls so tests
* can tell a cache hit (no call) from a miss.
*/
import { RpcTarget, WorkerEntrypoint } from "cloudflare:workers";
const KNOWN_WEBAPP = "fake-webapp-id";
const TOTAL = 30;
const TOTAL = 300;
/** the plain account, but diigo-style capped at 100 items per page */
const CAPPED_WEBAPP = "fake-capped-webapp";
const CAP = 100;
/** a second account, for date filters (goal 1d): 60 bookmarks, item n
* updated n-1 days + 1 hour before the call, in diigo's updated-desc
@@ -16,9 +19,9 @@ const TOTAL = 30;
* 5th an old bookmark edited recently (created 100 days before updated) */
const DATED_WEBAPP = "fake-dated-webapp";
const DATED_TOTAL = 60;
/** the same shape, 1200 deep (~3.3 years, 50 pages): for walk cost */
/** the same shape, 6000 deep (~16 years, 47 pages of 128): for walk cost */
const DEEP_WEBAPP = "fake-deep-webapp";
const DEEP_TOTAL = 1200;
const DEEP_TOTAL = 6000;
const HOUR = 60 * 60 * 1000;
function datedItem(n, now, user = "dated_dan") {
@@ -38,11 +41,11 @@ function datedItem(n, now, user = "dated_dan") {
/** item n (1-based, in diigo's order): diigo's field names, made-up
* values. link_id and updated_at are scrambled (distinct; 7 and 11 are
* coprime to 31), so neither column sorts back into diigo's order — a
* coprime to 307, a prime above TOTAL), so neither column sorts back into diigo's order — a
* cache that replays by column rather than by fetch order is caught. */
function item(n) {
return {
link_id: 1000 + ((n * 7) % 31),
link_id: 1000 + ((n * 7) % 307),
url: `https://example.com/${n}`,
title: `bookmark ${n}`,
description: "",
@@ -52,7 +55,7 @@ function item(n) {
user_id: 4242,
tags: "",
created_at: 1700000000,
updated_at: 1700001000 + ((n * 11) % 31),
updated_at: 1700001000 + ((n * 11) % 307),
};
}
@@ -61,7 +64,7 @@ let calls = 0;
class FakeGet extends RpcTarget {
#path;
#params;
/** null (the plain account), "dated", or "deep" */
/** null (the plain account), "capped", "dated", or "deep" */
#dated;
constructor(path, params, dated) {
super();
@@ -78,13 +81,14 @@ class FakeGet extends RpcTarget {
async get() {
calls += 1;
if (this.#path !== "load_user_items") throw new Error(`fake webapp: no route ${this.#path}`);
const count = Number(this.#params.count);
const count = Math.min(Number(this.#params.count), this.#dated === "capped" ? CAP : Infinity);
const page = Number(this.#params.page_num);
const total = this.#dated === "deep" ? DEEP_TOTAL : this.#dated ? DATED_TOTAL : TOTAL;
const dated = this.#dated === "dated" || this.#dated === "deep";
const total = this.#dated === "deep" ? DEEP_TOTAL : dated ? DATED_TOTAL : TOTAL;
const now = Date.now();
const items = [];
for (let n = (page - 1) * count + 1; n <= Math.min(page * count, total); n += 1) {
items.push(this.#dated ? datedItem(n, now, this.#dated === "deep" ? "deep_dee" : "dated_dan") : item(n));
items.push(dated ? datedItem(n, now, this.#dated === "deep" ? "deep_dee" : "dated_dan") : item(n));
}
return JSON.stringify({ total, items });
}
@@ -92,7 +96,7 @@ class FakeGet extends RpcTarget {
export class WebAppFactory extends WorkerEntrypoint {
async at(id) {
const kind = { [KNOWN_WEBAPP]: null, [DATED_WEBAPP]: "dated", [DEEP_WEBAPP]: "deep" }[id];
const kind = { [KNOWN_WEBAPP]: null, [CAPPED_WEBAPP]: "capped", [DATED_WEBAPP]: "dated", [DEEP_WEBAPP]: "deep" }[id];
if (kind === undefined) throw new Error("fake webapp: unknown id");
return new FakeGet("", {}, kind);
}