Skip to content

Commit aba041f

Browse files
shixishclaude
andcommitted
Make the core package runtime-neutral for Cloudflare Workers
The main entry statically imported node:fs/os/path (a credentials-file token lookup, an ontology file cache) purely as historical accidents of a Node-only SDK, which meant importing this package anywhere needed nodejs_compat even for pure-fetch usage. Split those into a new @diffbot/typescript/node subpath and redesigned ontology caching as an OntologyStore/KVOntologyStore pair so the core barrel needs zero node builtins and zero import-time side effects. Verified in fixtures/worker/ against real workerd with an empty compatibility_flags array. Breaking: resolveToken (env + file) is now resolveTokenFromEnv (env only) in core, with the file-backed version moved to @diffbot/typescript/node. dqlRefreshOntology and Ontology.fromPath are replaced by OntologyStore/KVOntologyStore and the node entry's FileOntologyStore. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent d82c159 commit aba041f

34 files changed

Lines changed: 3876 additions & 132 deletions

.github/workflows/tests.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,25 @@ jobs:
2525
- run: pnpm typecheck
2626
- run: pnpm test
2727
- run: pnpm build
28+
29+
workers:
30+
name: cloudflare worker fixture
31+
runs-on: ubuntu-latest
32+
steps:
33+
- uses: actions/checkout@v4
34+
- uses: pnpm/action-setup@v4
35+
with:
36+
version: 9
37+
- uses: actions/setup-node@v4
38+
with:
39+
node-version: 22
40+
cache: pnpm
41+
- run: pnpm install --frozen-lockfile
42+
- run: pnpm build
43+
- run: pnpm install --frozen-lockfile
44+
working-directory: fixtures/worker
45+
# --dry-run needs no Cloudflare credentials, so this runs on fork PRs too.
46+
- run: pnpm exec wrangler deploy --dry-run --outdir .wrangler-build
47+
working-directory: fixtures/worker
48+
- run: pnpm exec vitest run
49+
working-directory: fixtures/worker

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ node_modules/
22
dist/
33
*.tsbuildinfo
44
.DS_Store
5+
fixtures/*/.wrangler*
6+
fixtures/*/node_modules

README.md

Lines changed: 150 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ TypeScript client library for [Diffbot](https://www.diffbot.com) APIs. This is a
55
## Installation
66

77
```bash
8-
pnpm add diffbot-typescript
8+
pnpm add @diffbot/typescript
99
```
1010

11-
Requires Node.js 18+ (native `fetch`).
11+
Requires Node.js 18+ (native `fetch`) to build and install. The main entry point itself
12+
runs anywhere `fetch` exists, including Cloudflare Workers with no compatibility flags —
13+
see [Cloudflare Workers](#cloudflare-workers) below.
1214

1315
## Usage
1416

@@ -19,23 +21,71 @@ Create a client once, then pass it to the API functions you need. Import only th
1921
### Authentication
2022

2123
```typescript
22-
import { DiffbotClient, extract, resolveToken } from "diffbot-typescript";
24+
import { DiffbotClient, extract, resolveTokenFromEnv } from "@diffbot/typescript";
2325

24-
const db = new DiffbotClient({ token: resolveToken() });
26+
const db = new DiffbotClient({ token: resolveTokenFromEnv() });
2527
const data = await extract(db, "https://www.example.com");
2628
await db.close();
2729
```
2830

2931
Token resolution order:
3032

31-
1. Explicit argument to `resolveToken(token)`
32-
2. `DIFFBOT_API_TOKEN` environment variable
33-
3. `DIFFBOT_API_TOKEN=...` line in `~/.diffbot/credentials`
33+
1. Explicit argument to `resolveTokenFromEnv(token)`
34+
2. An `env`-style object passed as the second argument — the Workers convention: `resolveTokenFromEnv(undefined, env)`
35+
3. `DIFFBOT_API_TOKEN` environment variable (`process.env`)
36+
37+
Node users who want a `~/.diffbot/credentials` file as a final fallback should import
38+
`resolveToken` from `@diffbot/typescript/node` instead — same first three steps, plus the
39+
file. See [Runtime support](#runtime-support-node-vs-everywhere-else).
40+
41+
### Client configuration
42+
43+
`DiffbotClient` is the single place to configure the SDK — every API function takes its
44+
settings from the client you hand it.
45+
46+
| Option | Default | Used by |
47+
|--------|---------|---------|
48+
| `token` | — (required) | all |
49+
| `timeout` | `30000` (ms) | all |
50+
| `fetch` | global `fetch` | all |
51+
| `analyzeUrl` | `https://api.diffbot.com/v3` | `extract` |
52+
| `crawlerUrl` | `https://api.diffbot.com/v3/crawl` | `crawl`, `crawlListJobs`, `crawlGetJob`, `crawlDeleteJob` |
53+
| `llmUrl` | `https://llm.diffbot.com/rag/v1/chat/completions` | `ask` |
54+
| `webSearchUrl` | `https://llm.diffbot.com/api/v1/web_search` | `webSearch` |
55+
| `nlpUrl` | `https://nl.diffbot.com/v1/` | `entities` |
56+
| `dqlUrl` | `https://kg.diffbot.com/kg/v3/dql` | `dql`, `dqlParallel` |
57+
| `ontologyUrl` | `https://kg.diffbot.com/kg/ontology` | `dqlFetchOntology`, `dqlFetchOntologyText`, `OntologyStore` |
58+
59+
```typescript
60+
import { DiffbotClient } from "@diffbot/typescript";
61+
62+
const db = new DiffbotClient({
63+
token: "YOUR_TOKEN",
64+
timeout: 60_000,
65+
dqlUrl: "http://localhost:8080/kg/v3/dql",
66+
});
67+
```
68+
69+
`analyzeUrl` and `crawlerUrl` are bases that the SDK appends to (`/${api}` and `/data`
70+
respectively); the rest are complete endpoints, used as given.
71+
72+
Passing `fetch` replaces the transport, which is the hook for retries, logging, extra
73+
headers, or mocking in tests:
74+
75+
```typescript
76+
const db = new DiffbotClient({
77+
token: "YOUR_TOKEN",
78+
fetch: async (input, init) => {
79+
console.log("", input); // the SDK always calls fetch with a URL string
80+
return fetch(input, init);
81+
},
82+
});
83+
```
3484

3585
### Extract structured content
3686

3787
```typescript
38-
import { DiffbotClient, extract } from "diffbot-typescript";
88+
import { DiffbotClient, extract } from "@diffbot/typescript";
3989

4090
const db = new DiffbotClient({ token: "YOUR_TOKEN" });
4191
const data = await extract(db, "https://www.example.com");
@@ -44,7 +94,7 @@ const data = await extract(db, "https://www.example.com");
4494
### Ask Diffbot LLM
4595

4696
```typescript
47-
import { ask } from "diffbot-typescript";
97+
import { ask } from "@diffbot/typescript";
4898

4999
for await (const chunk of ask(db, [{ role: "user", content: "What's the capital of France?" }])) {
50100
process.stdout.write(chunk);
@@ -54,7 +104,7 @@ for await (const chunk of ask(db, [{ role: "user", content: "What's the capital
54104
### Crawl a site
55105

56106
```typescript
57-
import { crawl } from "diffbot-typescript";
107+
import { crawl } from "@diffbot/typescript";
58108

59109
for await (const event of crawl(db, "https://www.example.com", { hops: 1 })) {
60110
console.log(event);
@@ -64,26 +114,50 @@ for await (const event of crawl(db, "https://www.example.com", { hops: 1 })) {
64114
### Query the Knowledge Graph
65115

66116
```typescript
67-
import { dql } from "diffbot-typescript";
117+
import { dql } from "@diffbot/typescript";
68118

69119
const results = await dql(db, 'type:Organization name:"Diffbot"');
70120
```
71121

72122
### Web Search
73123

74124
```typescript
75-
import { webSearch } from "diffbot-typescript";
125+
import { webSearch } from "@diffbot/typescript";
76126

77127
const results = await webSearch(db, "diffbot knowledge graph");
78128
for (const r of results.search_results as Array<Record<string, unknown>>) {
79129
console.log(r.score, r.title, r.pageUrl);
80130
}
81131
```
82132

133+
### Caching the Knowledge Graph ontology
134+
135+
`dqlFetchOntology` fetches on every call. For anything that calls it more than once,
136+
`OntologyStore` caches the parsed `Ontology`, dedupes concurrent fetches into one request,
137+
and clears its memo on a rejection so a failed fetch doesn't get stuck:
138+
139+
```typescript
140+
import { DiffbotClient, OntologyStore } from "@diffbot/typescript";
141+
142+
const db = new DiffbotClient({ token: "YOUR_TOKEN" });
143+
const store = new OntologyStore(db);
144+
145+
const ontology = await store.load();
146+
console.log(ontology.types());
147+
148+
// Force a refetch, e.g. after `dqlRefreshOntology`'s old role:
149+
await store.load({ refresh: true });
150+
```
151+
152+
The base `OntologyStore` caches only in memory, for the life of the instance.
153+
`KVOntologyStore` (below) persists across instances via Cloudflare KV; Node users
154+
wanting a filesystem-backed cache should use `FileOntologyStore` from
155+
`@diffbot/typescript/node` — see [Runtime support](#runtime-support-node-vs-everywhere-else).
156+
83157
### Entities (NLP)
84158

85159
```typescript
86-
import { entities } from "diffbot-typescript";
160+
import { entities } from "@diffbot/typescript";
87161

88162
const result = await entities(db, "Apple CEO Tim Cook announced record quarterly earnings.");
89163
```
@@ -93,23 +167,72 @@ const result = await entities(db, "Apple CEO Tim Cook announced record quarterly
93167
`DiffbotClient` supports explicit resource management for automatic cleanup:
94168

95169
```typescript
96-
import { DiffbotClient, extract } from "diffbot-typescript";
170+
import { DiffbotClient, extract } from "@diffbot/typescript";
97171

98172
await using db = new DiffbotClient({ token: "YOUR_TOKEN" });
99173
const data = await extract(db, "https://www.example.com");
100174
```
101175

176+
## Runtime support: Node vs. everywhere else
177+
178+
The main entry point (`@diffbot/typescript`) imports no Node builtins and has no
179+
import-time side effects — it runs in Node, browsers, and edge runtimes including
180+
Cloudflare Workers, with **no compatibility flags required**. It is enforced, not just
181+
tested: the build target is `platform: "neutral"`, so an accidental `node:fs` import
182+
anywhere in the main entry fails the build, and a post-build script scans the emitted
183+
bundle for node builtins as a second check.
184+
185+
Two things genuinely need a filesystem and live in a separate `@diffbot/typescript/node`
186+
entry point instead:
187+
188+
```typescript
189+
import { resolveToken, FileOntologyStore } from "@diffbot/typescript/node";
190+
191+
const token = resolveToken(); // env, then ~/.diffbot/credentials
192+
const store = new FileOntologyStore(db, "/path/to/ontology.json");
193+
```
194+
195+
| | Main entry (`@diffbot/typescript`) | Node entry (`@diffbot/typescript/node`) |
196+
|---|---|---|
197+
| Token resolution | `resolveTokenFromEnv(token?, env?)` — arg, `env`, `process.env` | `resolveToken(token?, env?)` — same, plus `~/.diffbot/credentials` |
198+
| Ontology cache | `OntologyStore` (memory), `KVOntologyStore` (Cloudflare KV) | `FileOntologyStore` (filesystem) |
199+
200+
## Cloudflare Workers
201+
202+
```typescript
203+
import { DiffbotClient, dql, resolveTokenFromEnv, KVOntologyStore } from "@diffbot/typescript";
204+
205+
export default {
206+
async fetch(request: Request, env: { DIFFBOT_API_TOKEN: string; ONTOLOGY: KVNamespace }) {
207+
const db = new DiffbotClient({ token: resolveTokenFromEnv(undefined, env) });
208+
const ontology = await new KVOntologyStore(db, env.ONTOLOGY).load();
209+
const results = await dql(db, 'type:Organization name:"Diffbot"');
210+
return Response.json({ results, types: ontology.types() });
211+
},
212+
};
213+
```
214+
215+
No `compatibility_flags` entry is needed for this package. Tokens come from `env`, not a
216+
credentials file — there is no filesystem in a Worker, so `resolveToken` (the Node-only,
217+
file-backed version) is not importable here; use `resolveTokenFromEnv`. For the ontology
218+
cache, pass a `KVNamespace` binding straight to `KVOntologyStore` — it satisfies the
219+
store's `KVLike` interface directly, with a lock against concurrent refresh stampedes and
220+
optional stale-while-revalidate via `staleAfterSeconds` + `waitUntil`.
221+
222+
`fixtures/worker/` in this repo is a working Worker exercising exactly this path, verified
223+
in CI against workerd with `compatibility_flags: []`.
224+
102225
## Python ↔ TypeScript API mapping
103226

104227
| Python | TypeScript |
105228
|--------|------------|
106-
| `resolve_token()` | `resolveToken()` |
229+
| `resolve_token()` | `resolveTokenFromEnv()` (or `resolveToken()` from `@diffbot/typescript/node`) |
107230
| `crawl_list_jobs()` | `crawlListJobs()` |
108231
| `crawl_get_job()` | `crawlGetJob()` |
109232
| `crawl_delete_job()` | `crawlDeleteJob()` |
110233
| `dql_parallel()` | `dqlParallel()` |
111234
| `dql_fetch_ontology()` | `dqlFetchOntology()` |
112-
| `dql_refresh_ontology()` | `dqlRefreshOntology()` |
235+
| `dql_refresh_ontology()` | `new OntologyStore(db).load({ refresh: true })` (or `FileOntologyStore` from `@diffbot/typescript/node`) |
113236
| `web_search()` | `webSearch()` |
114237

115238
## Development
@@ -126,6 +249,17 @@ Live integration tests (requires `DIFFBOT_API_TOKEN`):
126249
pnpm test:live
127250
```
128251

252+
Cloudflare Worker fixture (proves the main entry needs no compat flags — see
253+
`fixtures/worker/`):
254+
255+
```bash
256+
pnpm build # fixture resolves the package through dist/
257+
cd fixtures/worker
258+
pnpm install
259+
pnpm exec wrangler deploy --dry-run # bundle check, no Cloudflare credentials needed
260+
pnpm exec vitest run # runs in workerd via @cloudflare/vitest-pool-workers
261+
```
262+
129263
## License
130264

131265
MIT

fixtures/worker/package.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "diffbot-typescript-worker-fixture",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"check": "wrangler deploy --dry-run --outdir .wrangler-build",
7+
"test": "vitest run"
8+
},
9+
"dependencies": {
10+
"@diffbot/typescript": "link:../.."
11+
},
12+
"devDependencies": {
13+
"@cloudflare/vitest-pool-workers": "^0.8.0",
14+
"@cloudflare/workers-types": "^4.20250101.0",
15+
"typescript": "^5.8.3",
16+
"vitest": "^3.2.1",
17+
"wrangler": "^4.0.0"
18+
},
19+
"pnpm": {
20+
"onlyBuiltDependencies": ["esbuild", "sharp", "workerd"]
21+
}
22+
}

0 commit comments

Comments
 (0)