Seen live: "filled: 168 of 1 pages". diigo's total overstated by 100
(16744 vs 16644), so the walk went on to page 168, which diigo answered
empty with a total ≤ 100; the fill recomputed its page count from that,
ceil(total/100) = 1.
Now a page whose own total says the data ended before it ends the walk,
with the count set to the last page with items ("167 of 167"). Judged
by total, not by items, since the public cache may keep none of a page
that isn't the end. The known-pages estimate ignores zero totals. The
fake answers past-the-end pages as diigo does (no items, total 0).
Generated-by: Claude Opus 5.5
129 lines
4.3 KiB
JavaScript
129 lines
4.3 KiB
JavaScript
/**
|
|
* @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. 250 items (3
|
|
* pages of 100, the last short), 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 = 250;
|
|
/** the plain account, capped below PAGE_SIZE (as diigo caps at 100) */
|
|
const CAPPED_WEBAPP = "fake-capped-webapp";
|
|
const CAP = 64;
|
|
/** the plain account, but its `total` claims 60 more items than it has
|
|
* (as diigo's does, observed 2026-10-07) */
|
|
const OVERSTATED_WEBAPP = "fake-overstated-webapp";
|
|
const OVERSTATEMENT = 60;
|
|
|
|
/** 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
|
|
* order; tagged ocap, plus weekly every 7th; every 10th private; every
|
|
* 5th an old bookmark edited recently (created 100 days before updated) */
|
|
const DATED_WEBAPP = "fake-dated-webapp";
|
|
const DATED_TOTAL = 60;
|
|
/** the same shape, 6000 deep (~16 years, 60 pages of 100): for walk cost */
|
|
const DEEP_WEBAPP = "fake-deep-webapp";
|
|
const DEEP_TOTAL = 6000;
|
|
const HOUR = 60 * 60 * 1000;
|
|
|
|
function datedItem(n, now, user = "dated_dan") {
|
|
return {
|
|
link_id: 5000 + n,
|
|
url: `https://dated.example/${n}`,
|
|
title: `dated ${n}`,
|
|
description: n % 7 === 0 ? "a week of fragments" : "",
|
|
private: n % 10 === 0,
|
|
u_name: user,
|
|
user_id: 777,
|
|
tags: n % 7 === 0 ? "ocap,weekly" : "ocap",
|
|
created_at: Math.floor((now - (n - 1) * 24 * HOUR - HOUR - (n % 5 === 0 ? 100 * 24 * HOUR : 0)) / 1000),
|
|
updated_at: Math.floor((now - (n - 1) * 24 * HOUR - HOUR) / 1000),
|
|
};
|
|
}
|
|
|
|
/** 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 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) % 307),
|
|
url: `https://example.com/${n}`,
|
|
title: `bookmark ${n}`,
|
|
description: "",
|
|
private: n % 3 === 0,
|
|
u_name: "alice_99",
|
|
first_by: "someone-else",
|
|
user_id: 4242,
|
|
tags: "",
|
|
created_at: 1700000000,
|
|
updated_at: 1700001000 + ((n * 11) % 307),
|
|
};
|
|
}
|
|
|
|
let calls = 0;
|
|
|
|
class FakeGet extends RpcTarget {
|
|
#path;
|
|
#params;
|
|
/** null (the plain account), "capped", "overstated", "dated", or "deep" */
|
|
#dated;
|
|
constructor(path, params, dated) {
|
|
super();
|
|
this.#path = path;
|
|
this.#params = params;
|
|
this.#dated = dated;
|
|
}
|
|
pathjoin(ref) {
|
|
return new FakeGet(ref, this.#params, this.#dated);
|
|
}
|
|
query(params) {
|
|
return new FakeGet(this.#path, { ...this.#params, ...params }, this.#dated);
|
|
}
|
|
async get() {
|
|
calls += 1;
|
|
if (this.#path !== "load_user_items") throw new Error(`fake webapp: no route ${this.#path}`);
|
|
const count = Math.min(Number(this.#params.count), this.#dated === "capped" ? CAP : Infinity);
|
|
const page = Number(this.#params.page_num);
|
|
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(dated ? datedItem(n, now, this.#dated === "deep" ? "deep_dee" : "dated_dan") : item(n));
|
|
}
|
|
// past the end, diigo answers no items and a total of 0 (observed
|
|
// 2026-10-07: the page after the last reported total ≤ 100)
|
|
if (items.length === 0) return JSON.stringify({ total: 0, items });
|
|
return JSON.stringify({ total: this.#dated === "overstated" ? total + OVERSTATEMENT : total, items });
|
|
}
|
|
}
|
|
|
|
export class WebAppFactory extends WorkerEntrypoint {
|
|
async at(id) {
|
|
const kind = {
|
|
[KNOWN_WEBAPP]: null,
|
|
[CAPPED_WEBAPP]: "capped",
|
|
[OVERSTATED_WEBAPP]: "overstated",
|
|
[DATED_WEBAPP]: "dated",
|
|
[DEEP_WEBAPP]: "deep",
|
|
}[id];
|
|
if (kind === undefined) throw new Error("fake webapp: unknown id");
|
|
return new FakeGet("", {}, kind);
|
|
}
|
|
/** how many diigo calls so far (test hook) */
|
|
async calls() {
|
|
return calls;
|
|
}
|
|
}
|
|
|
|
export default {
|
|
fetch() {
|
|
return new Response("fake webapp", { status: 404 });
|
|
},
|
|
};
|