@diffbot/typescript 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,7 +8,9 @@ TypeScript client library for [Diffbot](https://www.diffbot.com) APIs. This is a
8
8
  pnpm add @diffbot/typescript
9
9
  ```
10
10
 
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.
12
14
 
13
15
  ## Usage
14
16
 
@@ -19,18 +21,66 @@ Create a client once, then pass it to the API functions you need. Import only th
19
21
  ### Authentication
20
22
 
21
23
  ```typescript
22
- import { DiffbotClient, extract, resolveToken } from "@diffbot/typescript";
24
+ import { DiffbotClient, extract, resolveTokenFromEnv } from "@diffbot/typescript";
23
25
 
24
- const db = new DiffbotClient({ token: resolveToken() });
26
+ const db = new DiffbotClient({ token: resolveTokenFromEnv() });
25
27
  const data = await extract(db, "https://www.example.com");
26
28
  await db.close();
27
29
  ```
28
30
 
29
31
  Token resolution order:
30
32
 
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
+ ```
34
84
 
35
85
  ### Extract structured content
36
86
 
@@ -80,6 +130,30 @@ for (const r of results.search_results as Array<Record<string, unknown>>) {
80
130
  }
81
131
  ```
82
132
 
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
+
83
157
  ### Entities (NLP)
84
158
 
85
159
  ```typescript
@@ -99,17 +173,66 @@ await using db = new DiffbotClient({ token: "YOUR_TOKEN" });
99
173
  const data = await extract(db, "https://www.example.com");
100
174
  ```
101
175
 
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
+
102
225
  ## Python ↔ TypeScript API mapping
103
226
 
104
227
  | Python | TypeScript |
105
228
  |--------|------------|
106
- | `resolve_token()` | `resolveToken()` |
229
+ | `resolve_token()` | `resolveTokenFromEnv()` (or `resolveToken()` from `@diffbot/typescript/node`) |
107
230
  | `crawl_list_jobs()` | `crawlListJobs()` |
108
231
  | `crawl_get_job()` | `crawlGetJob()` |
109
232
  | `crawl_delete_job()` | `crawlDeleteJob()` |
110
233
  | `dql_parallel()` | `dqlParallel()` |
111
234
  | `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`) |
113
236
  | `web_search()` | `webSearch()` |
114
237
 
115
238
  ## Development
@@ -126,6 +249,17 @@ Live integration tests (requires `DIFFBOT_API_TOKEN`):
126
249
  pnpm test:live
127
250
  ```
128
251
 
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
+
129
263
  ## License
130
264
 
131
265
  MIT
package/dist/index.cjs CHANGED
@@ -26,7 +26,9 @@ __export(index_exports, {
26
26
  DiffbotClient: () => DiffbotClient,
27
27
  DiffbotError: () => DiffbotError,
28
28
  ExtractionError: () => ExtractionError,
29
+ KVOntologyStore: () => KVOntologyStore,
29
30
  Ontology: () => Ontology,
31
+ OntologyStore: () => OntologyStore,
30
32
  RateLimitError: () => RateLimitError,
31
33
  VERSION: () => VERSION,
32
34
  ValidationError: () => ValidationError,
@@ -37,17 +39,17 @@ __export(index_exports, {
37
39
  crawlListJobs: () => crawlListJobs,
38
40
  dql: () => dql,
39
41
  dqlFetchOntology: () => dqlFetchOntology,
42
+ dqlFetchOntologyText: () => dqlFetchOntologyText,
40
43
  dqlParallel: () => dqlParallel,
41
- dqlRefreshOntology: () => dqlRefreshOntology,
42
44
  entities: () => entities,
43
45
  extract: () => extract,
44
- resolveToken: () => resolveToken,
46
+ resolveTokenFromEnv: () => resolveTokenFromEnv,
45
47
  webSearch: () => webSearch
46
48
  });
47
49
  module.exports = __toCommonJS(index_exports);
48
50
 
49
51
  // src/constants.ts
50
- var VERSION = "0.1.0";
52
+ var VERSION = "0.2.0";
51
53
  var DEFAULT_TIMEOUT = 3e4;
52
54
  var EXTRACT_BASE = "https://api.diffbot.com/v3";
53
55
  var CRAWL_BASE = "https://api.diffbot.com/v3/crawl";
@@ -188,6 +190,8 @@ var DiffbotClient = class {
188
190
  crawlerUrl;
189
191
  webSearchUrl;
190
192
  nlpUrl;
193
+ dqlUrl;
194
+ ontologyUrl;
191
195
  http;
192
196
  constructor(options) {
193
197
  if (!options.token) {
@@ -199,6 +203,8 @@ var DiffbotClient = class {
199
203
  this.crawlerUrl = options.crawlerUrl ?? CRAWL_BASE;
200
204
  this.webSearchUrl = options.webSearchUrl ?? WEB_SEARCH_BASE;
201
205
  this.nlpUrl = options.nlpUrl ?? NLP_BASE;
206
+ this.dqlUrl = options.dqlUrl ?? KG_DQL_ENDPOINT;
207
+ this.ontologyUrl = options.ontologyUrl ?? KG_ONTOLOGY_ENDPOINT;
202
208
  this.http = new HttpClient(
203
209
  options.fetch ?? fetch,
204
210
  options.timeout ?? DEFAULT_TIMEOUT,
@@ -214,34 +220,20 @@ var DiffbotClient = class {
214
220
  };
215
221
 
216
222
  // src/auth.ts
217
- var import_node_fs = require("fs");
218
- var import_node_os = require("os");
219
- var import_node_path = require("path");
220
- var authConfig = {
221
- credentialsPath: (0, import_node_path.join)((0, import_node_os.homedir)(), ".diffbot", "credentials")
222
- };
223
- function readCredentialsFile(path = authConfig.credentialsPath) {
224
- if (!(0, import_node_fs.existsSync)(path)) {
225
- return "";
226
- }
227
- const text = (0, import_node_fs.readFileSync)(path, "utf8");
228
- for (const line of text.split("\n")) {
229
- const trimmed = line.trim();
230
- if (trimmed.startsWith(`${TOKEN_ENV_VAR}=`)) {
231
- return trimmed.slice(TOKEN_ENV_VAR.length + 1).trim();
232
- }
233
- }
234
- return "";
223
+ function envToken(env) {
224
+ const fromEnv = env?.[TOKEN_ENV_VAR];
225
+ if (typeof fromEnv === "string" && fromEnv.trim()) {
226
+ return fromEnv.trim();
227
+ }
228
+ const proc = globalThis.process;
229
+ const fromProcess = proc?.env?.[TOKEN_ENV_VAR];
230
+ return fromProcess?.trim() ?? "";
235
231
  }
236
- function resolveToken(token) {
232
+ function resolveTokenFromEnv(token, env) {
237
233
  if (token?.trim()) {
238
234
  return token.trim();
239
235
  }
240
- const envToken = process.env[TOKEN_ENV_VAR]?.trim();
241
- if (envToken) {
242
- return envToken;
243
- }
244
- return readCredentialsFile();
236
+ return envToken(env);
245
237
  }
246
238
 
247
239
  // src/extract.ts
@@ -470,12 +462,7 @@ async function crawlDeleteJob(client, jobName) {
470
462
  raiseForStatus(response, body);
471
463
  }
472
464
 
473
- // src/kg.ts
474
- var import_promises2 = require("fs/promises");
475
- var import_node_path2 = require("path");
476
-
477
465
  // src/ontology.ts
478
- var import_promises = require("fs/promises");
479
466
  var Ontology = class _Ontology {
480
467
  data;
481
468
  constructor(data) {
@@ -485,10 +472,6 @@ var Ontology = class _Ontology {
485
472
  const text = typeof raw === "string" ? raw : new TextDecoder().decode(raw);
486
473
  return new _Ontology(JSON.parse(text));
487
474
  }
488
- static async fromPath(path) {
489
- const text = await (0, import_promises.readFile)(path, "utf8");
490
- return _Ontology.fromJson(text);
491
- }
492
475
  types() {
493
476
  const types = this.data.types;
494
477
  return Object.keys(types ?? {}).sort();
@@ -601,7 +584,7 @@ function buildDqlParams(client, query, options) {
601
584
  }
602
585
  async function dql(client, query, options = {}) {
603
586
  const params = buildDqlParams(client, query, options);
604
- const response = await client.http.get(KG_DQL_ENDPOINT, { params });
587
+ const response = await client.http.get(client.dqlUrl, { params });
605
588
  const body = await readBodyText(response);
606
589
  raiseForStatus(response, body);
607
590
  if (options.raw) {
@@ -625,18 +608,14 @@ async function dqlParallel(client, queries, workers = 8) {
625
608
  await Promise.all(Array.from({ length: limit }, () => worker()));
626
609
  return results;
627
610
  }
628
- async function dqlRefreshOntology(client, dest) {
629
- const response = await client.http.get(KG_ONTOLOGY_ENDPOINT);
611
+ async function dqlFetchOntologyText(client) {
612
+ const response = await client.http.get(client.ontologyUrl);
630
613
  const body = await readBodyText(response);
631
614
  raiseForStatus(response, body);
632
- await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(dest), { recursive: true });
633
- await (0, import_promises2.writeFile)(dest, body);
615
+ return body;
634
616
  }
635
617
  async function dqlFetchOntology(client) {
636
- const response = await client.http.get(KG_ONTOLOGY_ENDPOINT);
637
- const body = await readBodyText(response);
638
- raiseForStatus(response, body);
639
- return Ontology.fromJson(body);
618
+ return Ontology.fromJson(await dqlFetchOntologyText(client));
640
619
  }
641
620
 
642
621
  // src/nlp.ts
@@ -651,6 +630,123 @@ async function entities(client, text, lang = "auto") {
651
630
  return Array.isArray(data) ? data[0] ?? {} : data;
652
631
  }
653
632
 
633
+ // src/ontology-store.ts
634
+ var OntologyStore = class {
635
+ client;
636
+ ttlSeconds;
637
+ inFlight = null;
638
+ constructor(client, options = {}) {
639
+ this.client = client;
640
+ this.ttlSeconds = options.ttlSeconds ?? 86400;
641
+ }
642
+ /**
643
+ * Resolve the ontology, from cache when possible. Concurrent calls share
644
+ * one in-flight fetch rather than issuing duplicate requests; a rejected
645
+ * fetch clears the in-flight memo so the next call retries instead of
646
+ * replaying the same failure forever.
647
+ */
648
+ load(options = {}) {
649
+ if (options.refresh) {
650
+ this.inFlight = null;
651
+ }
652
+ if (!this.inFlight) {
653
+ this.inFlight = this.resolve(options.refresh ?? false).catch((error) => {
654
+ this.inFlight = null;
655
+ throw error;
656
+ });
657
+ }
658
+ return this.inFlight;
659
+ }
660
+ async resolve(refresh) {
661
+ if (!refresh) {
662
+ const cached = await this.read();
663
+ if (cached !== void 0) {
664
+ try {
665
+ return Ontology.fromJson(cached);
666
+ } catch {
667
+ }
668
+ }
669
+ }
670
+ const text = await dqlFetchOntologyText(this.client);
671
+ await this.write(text);
672
+ return Ontology.fromJson(text);
673
+ }
674
+ /** Read the cached ontology text, or `undefined` on a miss. Memory-only by default. */
675
+ read() {
676
+ return Promise.resolve(void 0);
677
+ }
678
+ /** Persist ontology text after a fetch. No-op by default. */
679
+ write(_text) {
680
+ return Promise.resolve();
681
+ }
682
+ };
683
+ var KVOntologyStore = class extends OntologyStore {
684
+ kv;
685
+ key;
686
+ refreshedAtKey;
687
+ lockKey;
688
+ staleAfterSeconds;
689
+ lockTtlSeconds;
690
+ waitUntil;
691
+ constructor(client, kv, options = {}) {
692
+ super(client, options);
693
+ this.kv = kv;
694
+ this.key = options.key ?? "diffbot:ontology:v1";
695
+ this.refreshedAtKey = `${this.key}:refreshed_at`;
696
+ this.lockKey = `${this.key}:lock`;
697
+ this.staleAfterSeconds = options.staleAfterSeconds ?? 0;
698
+ this.lockTtlSeconds = options.lockTtlSeconds ?? 60;
699
+ this.waitUntil = options.waitUntil;
700
+ }
701
+ load(options = {}) {
702
+ if (!options.refresh && this.staleAfterSeconds > 0) {
703
+ return this.loadWithStaleWhileRevalidate();
704
+ }
705
+ return super.load(options);
706
+ }
707
+ async loadWithStaleWhileRevalidate() {
708
+ const [cached, refreshedAt] = await Promise.all([
709
+ this.kv.get(this.key),
710
+ this.kv.get(this.refreshedAtKey)
711
+ ]);
712
+ if (cached === null) {
713
+ return super.load();
714
+ }
715
+ const age = refreshedAt ? (Date.now() - Number(refreshedAt)) / 1e3 : Infinity;
716
+ const isStale = age > this.staleAfterSeconds;
717
+ const ontology = Ontology.fromJson(cached);
718
+ if (isStale) {
719
+ const refresh = super.load({ refresh: true }).catch(() => {
720
+ });
721
+ if (this.waitUntil) {
722
+ this.waitUntil(refresh);
723
+ } else {
724
+ await refresh;
725
+ return super.load();
726
+ }
727
+ }
728
+ return ontology;
729
+ }
730
+ async read() {
731
+ const cached = await this.kv.get(this.key);
732
+ return cached ?? void 0;
733
+ }
734
+ async write(text) {
735
+ if (await this.kv.get(this.lockKey)) {
736
+ return;
737
+ }
738
+ await this.kv.put(this.lockKey, "1", { expirationTtl: this.lockTtlSeconds });
739
+ try {
740
+ await this.kv.put(this.key, text, { expirationTtl: this.ttlSeconds });
741
+ await this.kv.put(this.refreshedAtKey, String(Date.now()), {
742
+ expirationTtl: this.ttlSeconds
743
+ });
744
+ } finally {
745
+ await this.kv.delete(this.lockKey);
746
+ }
747
+ }
748
+ };
749
+
654
750
  // src/web-search.ts
655
751
  async function webSearch(client, text, options = {}) {
656
752
  const headers = { Authorization: `Bearer ${client.token}` };
@@ -662,30 +758,4 @@ async function webSearch(client, text, options = {}) {
662
758
  raiseForStatus(response, body);
663
759
  return JSON.parse(body);
664
760
  }
665
- // Annotate the CommonJS export names for ESM import in node:
666
- 0 && (module.exports = {
667
- APIError,
668
- AuthError,
669
- CrawlEventType,
670
- DiffbotClient,
671
- DiffbotError,
672
- ExtractionError,
673
- Ontology,
674
- RateLimitError,
675
- VERSION,
676
- ValidationError,
677
- ask,
678
- crawl,
679
- crawlDeleteJob,
680
- crawlGetJob,
681
- crawlListJobs,
682
- dql,
683
- dqlFetchOntology,
684
- dqlParallel,
685
- dqlRefreshOntology,
686
- entities,
687
- extract,
688
- resolveToken,
689
- webSearch
690
- });
691
761
  //# sourceMappingURL=index.cjs.map