feat(diigo-dump): fluent WebApp over RPC + bound fetch + logged errors
Restore the fluent client from mm-diigo's WebApp.js as RpcTarget classes (src/webapp.ts): WebApp with pathjoin/query/withHeaders/cookies/get/post, flower-style unions (NuxiNL/flower v0.11, Jan 2019 — merge only if no shared key contradicts), pathjoin containment guard; RateLimitedWebApp shares one gate across all derived handles, enforced at get/post only. src/diigo.ts ports makeDiigoAPI and the dumpBookmarks generator. RPC surface: DiigoService (WorkerEntrypoint) with open() handing a rate-limited stub whose cookie is attached server-side only, and dumpNdjson. Promise pipelining makes a whole chain a single round trip. Divergences from the original for the RPC setting: - url is a private field: a public property on an RpcTarget is readable by stub holders, and query strings can carry credentials - Error messages carry no header/param values; URLs have query stripped (structured details via @endo/errors deferred until node_modules) - fetch default is fetch.bind(globalThis): a detached fetch throws 'Illegal invocation' in workerd (found live, fixed, verified 200) - dumpPage/open log failures via console.error for observability README documents config, RPC use, and each divergence. Generated-by: opencode (GLM)
This commit is contained in:
1 parent
4a8828086e
commit
309f003fa3
6 files changed
+404
-35
No files matched your search
+64
-32
@@ -1,14 +1,54 @@
|
||||
// diigo-dump as a worker: one page-number field + button.
|
||||
// Port of mm-diigo/projects/diigo-bak/diigo-dump.mjs —
|
||||
// browser-session-cookie access to diigo's interact_api/load_user_items
|
||||
// (the API-key path is bkmkeep.py / the EmDash plugin; this one is the cookie path).
|
||||
// Secret: DIIGO_COOKIE (copy the "Cookie" header from diigo.com devtools).
|
||||
/**
|
||||
* @file diigo-dump worker: one page-number field + button, plus the
|
||||
* {@link DiigoService} RPC entrypoint. Browser-session-cookie access to
|
||||
* diigo's interact_api/load_user_items — the API-key path is bkmkeep.py /
|
||||
* the EmDash plugin; this one is the cookie path.
|
||||
*
|
||||
* Secret: DIIGO_COOKIE (copy the "Cookie" header from diigo.com devtools).
|
||||
* The fluent WebApp client lives in ./webapp.ts and ./diigo.ts; the cookie
|
||||
* is attached only here, inside this worker's address space.
|
||||
* Failures reach both surfaces: console.error for observability (wrapped
|
||||
* workers never see exceptions the fetch handler renders itself), plus the
|
||||
* rendered status/detail on the page.
|
||||
* @see {@link DiigoService}
|
||||
*/
|
||||
|
||||
const DIIGO_BASE = "https://www.diigo.com/interact_api/";
|
||||
const COUNT = 24;
|
||||
import { WorkerEntrypoint } from "cloudflare:workers";
|
||||
import { makeDiigoAPI, dumpBookmarks, rateLimit } from "./diigo.js";
|
||||
|
||||
type Env = { DIIGO_COOKIE?: string };
|
||||
|
||||
const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
||||
|
||||
/**
|
||||
* RPC entrypoint for other workers on the account. Public methods shape the
|
||||
* ABI: `open()` hands out a rate-limited stub with the cookie attached
|
||||
* server-side (the caller can pathjoin/query/get but never sees the cookie
|
||||
* or fetch authority); `dumpNdjson` runs a whole dump here and returns ndjson.
|
||||
*
|
||||
* @see https://developers.cloudflare.com/workers/runtime-apis/rpc/ — stub
|
||||
* lifecycle; use `using` / `Symbol.dispose` on the caller side
|
||||
*/
|
||||
export class DiigoService extends WorkerEntrypoint {
|
||||
open(): ReturnType<typeof rateLimit> {
|
||||
const { DIIGO_COOKIE: cookie } = this.env as Env;
|
||||
if (!cookie) {
|
||||
console.error({ event: "open", error: "Missing DIIGO_COOKIE in the diigo-dump worker." });
|
||||
throw new Error("Missing DIIGO_COOKIE in the diigo-dump worker.");
|
||||
}
|
||||
return rateLimit(makeDiigoAPI().cookies(cookie), () => sleep(500));
|
||||
}
|
||||
|
||||
async dumpNdjson(pages: number, count = 24): Promise<string> {
|
||||
const diigoAPI = this.open();
|
||||
const lines: string[] = [];
|
||||
for await (const item of dumpBookmarks({ diigoAPI, pages, count })) {
|
||||
lines.push(JSON.stringify(item));
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
@@ -27,38 +67,30 @@ export default {
|
||||
},
|
||||
};
|
||||
|
||||
/** the HTTP form handler's dump path: one page via the same fluent chain an
|
||||
* RPC caller would drive (see {@link DiigoService.open}); shows raw JSON. */
|
||||
async function dumpPage(env: Env, page: number): Promise<{ status: string; detail: string }> {
|
||||
const cookie = env.DIIGO_COOKIE;
|
||||
if (!cookie) {
|
||||
return { status: "no-creds", detail: "Missing DIIGO_COOKIE.\nSet it: npx wrangler secret put DIIGO_COOKIE (or .dev.vars locally)." };
|
||||
return {
|
||||
status: "no-creds",
|
||||
detail: "Missing DIIGO_COOKIE.\nSet it: npx wrangler secret put DIIGO_COOKIE (or .dev.vars locally).",
|
||||
};
|
||||
}
|
||||
|
||||
const target = new URL(DIIGO_BASE + "load_user_items");
|
||||
target.searchParams.set("sort", "updated");
|
||||
target.searchParams.set("count", String(COUNT));
|
||||
target.searchParams.set("page_num", String(page));
|
||||
|
||||
const diigoAPI = rateLimit(makeDiigoAPI().cookies(cookie), () => sleep(500));
|
||||
try {
|
||||
const response = await fetch(target.toString(), {
|
||||
headers: {
|
||||
cookie,
|
||||
// same attenuated-request shape diigo-dump.mjs / diigo-interact.mjs use
|
||||
"user-agent": "Mozilla/5.0 (X11; Linux x86_64) Gecko/20100101 Firefox/141.0",
|
||||
"x-requested-with": "XMLHttpRequest",
|
||||
},
|
||||
});
|
||||
const text = await response.text();
|
||||
let detail = text;
|
||||
if (response.ok) {
|
||||
try {
|
||||
const data = JSON.parse(text) as { total?: number; items?: unknown[] };
|
||||
detail = `total: ${data.total}\nitems: ${data.items?.length ?? 0}\n\n${JSON.stringify(data.items ?? data, null, 2)}`;
|
||||
} catch {
|
||||
// non-JSON; show raw
|
||||
}
|
||||
}
|
||||
return { status: String(response.status), detail };
|
||||
const text = await diigoAPI
|
||||
.pathjoin("load_user_items")
|
||||
.query({ sort: "updated", count: 24, page_num: page })
|
||||
.get();
|
||||
const data = JSON.parse(text) as { total?: number; items?: Record<string, unknown>[] };
|
||||
return {
|
||||
status: "200",
|
||||
detail: `total: ${data.total}\nitems: ${data.items?.length ?? 0}\n\n${JSON.stringify(data.items ?? data, null, 2)}`,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error({ event: "dump", page, error: String(err) });
|
||||
return { status: "error", detail: String(err) };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user