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:
1 parent
efaf74023d
commit
826153e35f
5 files changed
+111
-47
No files matched your search
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user