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
114 lines
3.8 KiB
JavaScript
114 lines
3.8 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. 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 = 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
|
|
* 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, 47 pages of 128): 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", "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));
|
|
}
|
|
return JSON.stringify({ total, items });
|
|
}
|
|
}
|
|
|
|
export class WebAppFactory extends WorkerEntrypoint {
|
|
async at(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);
|
|
}
|
|
/** how many diigo calls so far (test hook) */
|
|
async calls() {
|
|
return calls;
|
|
}
|
|
}
|
|
|
|
export default {
|
|
fetch() {
|
|
return new Response("fake webapp", { status: 404 });
|
|
},
|
|
};
|