diigo-cache: bookmark cache in Durable Objects (goal 1c)

The dump worker becomes a page cache: a hit is served from DO SQLite
storage with no diigo call; a miss asks the webapp instance in the
cache's wiring and etches the page.

- two independent caches, one behavior (PagedCache): DiigoCache keeps
  every item; PublicCache keeps shared items only, dropping private
  rows before they are stored; served at /public/<username> (HTML page
  form, or JSON)
- create flow on one port: /_connect relays webapp's mint form, /_mint
  proxies the mint, /_wire wires a private or public cache; /_publish
  adds a public cache later
- ids are webkeys: kept out of URLs (POST bodies), shown and logged
  only as a hash fingerprint; saved to the password manager under the
  holder's petname (never sent to the server)
- items keyed on diigo's link_id; username from u_name; unkeyable
  items and every failure path are logged; each page read logs hit/miss
- tests run the real worker + DOs in the vitest pool against a fake
  webapp (tests/fake-webapp.js)

Generated-by: opencode (GLM)
Generated-by: Claude Opus 5.5
This commit is contained in:
dckc committed 2026-10-07 04:19:54 -05:00
1 parent c5e433cc9c
commit 6f409fc83a
15 files changed
+1664 -211

No files matched your search

+72
View File
@@ -0,0 +1,72 @@
import { describe, expect, it } from "vitest";
import { publicOnly, toRows, usernameOf } from "../src/items.js";
import type { Item } from "../src/items.js";
/** a load_user_items row: diigo's actual field names (observed
* 2026-10-07), made-up values */
function diigoItem(linkId: number, url: string): Item {
return {
link_id: linkId,
url,
title: `title ${linkId}`,
description: "",
private: false,
u_name: "alice_99",
first_by: "bob-the-first",
user_id: 4242,
real_name: "Alice A.",
tags: "",
created_at: 1700000000,
updated_at: 1700000100 + linkId,
annotations: [],
comments: [],
};
}
function makeLog() {
const entries: { level: string; args: unknown[] }[] = [];
const at = (level: string) => (...args: unknown[]) => entries.push({ level, args });
return { log: { warn: at("warn") }, entries };
}
describe("toRows", () => {
it("keys each item on diigo's link_id, so a page of distinct items stays distinct", () => {
const { log } = makeLog();
const rows = toRows([diigoItem(11, "https://a.example/"), diigoItem(12, "https://b.example/")], log);
expect(rows.map((r) => r.itemId)).toEqual(["11", "12"]);
});
it("logs, rather than silently drops or merges, items it cannot key", () => {
const { log, entries } = makeLog();
const noId = diigoItem(13, "https://c.example/");
delete noId.link_id;
const noUrl = diigoItem(14, "");
const rows = toRows([noId, noUrl, diigoItem(15, "https://d.example/")], log);
expect(rows.map((r) => r.itemId)).toEqual(["15"]);
expect(entries.map((e) => e.level)).toEqual(["warn", "warn"]);
// diagnostics name the fields present, never their values
expect(JSON.stringify(entries)).not.toContain("https://c.example/");
});
});
describe("usernameOf", () => {
it("reads the owner's username from diigo's u_name", () => {
expect(usernameOf(diigoItem(1, "https://a.example/"))).toBe("alice_99");
});
it("answers empty for an item without one", () => {
expect(usernameOf(undefined)).toBe("");
expect(usernameOf({ u_name: "has spaces" })).toBe("");
});
});
describe("publicOnly", () => {
it("keeps shared rows and drops private ones, before anything is stored", () => {
const { log } = makeLog();
const shared = diigoItem(21, "https://shared.example/");
const secret = { ...diigoItem(22, "https://secret.example/"), private: true };
const rows = publicOnly(toRows([shared, secret], log));
expect(rows.map((r) => r.itemId)).toEqual(["21"]);
expect(JSON.stringify(rows)).not.toContain("secret.example");
});
});