@scrapa/sdk 0.1.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/LICENSE +21 -0
- package/README.md +232 -0
- package/dist/client.d.ts +47 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +182 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +50 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +56 -0
- package/dist/errors.js.map +1 -0
- package/dist/generated/contract.d.ts +470 -0
- package/dist/generated/contract.d.ts.map +1 -0
- package/dist/generated/contract.js +161 -0
- package/dist/generated/contract.js.map +1 -0
- package/dist/generated/operations.d.ts +213 -0
- package/dist/generated/operations.d.ts.map +1 -0
- package/dist/generated/operations.js +220 -0
- package/dist/generated/operations.js.map +1 -0
- package/dist/generated/params.d.ts +420 -0
- package/dist/generated/params.d.ts.map +1 -0
- package/dist/generated/params.js +7 -0
- package/dist/generated/params.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
- package/src/client.ts +227 -0
- package/src/errors.ts +83 -0
- package/src/generated/contract.ts +564 -0
- package/src/generated/operations.ts +293 -0
- package/src/generated/params.ts +437 -0
- package/src/index.ts +9 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 scrapa.app
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# @scrapa/sdk
|
|
2
|
+
|
|
3
|
+
The official TypeScript client for [scrapa.app](https://scrapa.app) — read any web page as
|
|
4
|
+
markdown, HTML, text, links, a screenshot or a PDF, through a plain HTTP request or a real
|
|
5
|
+
browser, whichever the page turns out to need. Search the web through the same key, and hold
|
|
6
|
+
a real Chromium your own Playwright or Puppeteer drives.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm i @scrapa/sdk
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { Scrapa } from "@scrapa/sdk";
|
|
14
|
+
|
|
15
|
+
const scrapa = new Scrapa(process.env.SCRAPA_API_KEY!);
|
|
16
|
+
|
|
17
|
+
const page = await scrapa.scrape({ url: "https://example.com" });
|
|
18
|
+
console.log(page.markdown);
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Every method, every argument and every field is generated from the API's own definitions, so
|
|
22
|
+
what your editor autocompletes is what the server accepts. Node 20+, Bun, Deno, workers and
|
|
23
|
+
the browser — anywhere `fetch` exists.
|
|
24
|
+
|
|
25
|
+
## Reading a page
|
|
26
|
+
|
|
27
|
+
`scrape()` takes a URL and gives you the page. `engine` defaults to `auto`: a plain HTTP
|
|
28
|
+
request first, re-read in a real Chromium only when the body turns out to be a JavaScript
|
|
29
|
+
shell — which is the difference between 1 credit and 5.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
const page = await scrapa.scrape({
|
|
33
|
+
url: "https://news.ycombinator.com",
|
|
34
|
+
formats: ["markdown", "links"],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
page.engine; // "fetch" — this one did not need a browser
|
|
38
|
+
page.links; // every absolute link, in document order
|
|
39
|
+
page.metadata.title;
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Ask for a screenshot or a PDF and the browser is used whatever `engine` says. Both come back
|
|
43
|
+
base64-encoded, and asking for both costs no more than asking for one.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
const shot = await scrapa.scrape({ url: "https://stripe.com", formats: ["screenshot"] });
|
|
47
|
+
await writeFile("stripe.png", Buffer.from(shot.screenshot!, "base64"));
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Searching the web
|
|
51
|
+
|
|
52
|
+
`search()` runs a real search and hands back results we read ourselves. That is not a
|
|
53
|
+
detail — every search vendor forbids redistributing their results through another API, so
|
|
54
|
+
our own fetch is the only kind you are allowed to build on.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
const found = await scrapa.search({
|
|
58
|
+
query: "melhores frameworks typescript 2026",
|
|
59
|
+
limit: 10,
|
|
60
|
+
proxy: { country: "br" }, // picks the exit AND the market: Brazilian results, in Portuguese
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
found.results[0].url; // array order IS the ranking — no score to sort by, because none is real
|
|
64
|
+
found.cached; // the same question inside the window costs nothing and says so
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`topic: "news"` is the one topic that reliably carries `publishedAt`. `includeDomains` is
|
|
68
|
+
asked of the front *and* enforced on the answer, so what comes back is on your list whatever
|
|
69
|
+
the front did with the operator. `answer: true` adds a paragraph written over the hits. To
|
|
70
|
+
read any result, hand its URL straight to `scrape()`.
|
|
71
|
+
|
|
72
|
+
A search every front refused costs nothing. A search that ran and found nothing is charged —
|
|
73
|
+
the index answered, and "nothing published that" is an answer.
|
|
74
|
+
|
|
75
|
+
## Driving a real browser
|
|
76
|
+
|
|
77
|
+
A session is one Chromium held for you, with a CDP URL your own tools connect to. We hand
|
|
78
|
+
back a URL rather than a client, so everything you already know how to do with Playwright or
|
|
79
|
+
Puppeteer keeps working.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { chromium } from "playwright";
|
|
83
|
+
|
|
84
|
+
const session = await scrapa.sessions.create({ timeoutMin: 10 });
|
|
85
|
+
const browser = await chromium.connectOverCDP(session.connectUrl);
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const page = await browser.newPage();
|
|
89
|
+
await page.goto("https://example.com");
|
|
90
|
+
await page.getByRole("button", { name: "Accept" }).click();
|
|
91
|
+
} finally {
|
|
92
|
+
await browser.close();
|
|
93
|
+
await scrapa.sessions.close({ id: session.id });
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`create()` returns only once Chromium is actually up, so `connectUrl` is a required field and
|
|
98
|
+
needs no `!`. It carries the session's token, is shown once and cannot be rebuilt from the id
|
|
99
|
+
— which is why `get()` does not repeat it.
|
|
100
|
+
|
|
101
|
+
Minutes are reserved for the whole window and settled by what you used, so closing early
|
|
102
|
+
hands the rest back. A browser that never came up costs nothing at all, and if you forget to
|
|
103
|
+
close one, it is reaped at `expiresAt` and settled the same way.
|
|
104
|
+
|
|
105
|
+
## Reading a list of pages
|
|
106
|
+
|
|
107
|
+
A batch is N scrapes on our schedule instead of your connection. Same options, same engines,
|
|
108
|
+
same meter — what you buy is not holding a socket open for an hour.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const batch = await scrapa.batches.create({
|
|
112
|
+
urls: sitemap.map((entry) => entry.loc),
|
|
113
|
+
formats: ["markdown"],
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
let status = await scrapa.batches.get({ id: batch.id });
|
|
117
|
+
while (status.completed < status.total) {
|
|
118
|
+
await new Promise((resolve) => setTimeout(resolve, 5_000));
|
|
119
|
+
status = await scrapa.batches.get({ id: batch.id });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (let from = 0; from < status.total; ) {
|
|
123
|
+
const page = await scrapa.batches.get({ id: batch.id, items: true, from });
|
|
124
|
+
for (const item of page.items ?? []) {
|
|
125
|
+
if (item.status === "done") save(item.url, item.result!.markdown);
|
|
126
|
+
}
|
|
127
|
+
from += page.items?.length ?? status.total;
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Nothing is charged when you submit and nothing is reserved: each URL then costs what the same
|
|
132
|
+
call would have cost alone, and `cost` on the batch is what it has spent so far. Because
|
|
133
|
+
nothing is held up front, a batch can outlive the credits that started it — those URLs come
|
|
134
|
+
back `skipped` rather than `failed`, which is not a verdict about the URL, and you can submit
|
|
135
|
+
exactly those positions again.
|
|
136
|
+
|
|
137
|
+
`completed` counts every URL that reached an end, so it is what you compare against `total`;
|
|
138
|
+
`failed` and `skipped` both live inside it. Results are readable for 24 hours, up to two
|
|
139
|
+
hundred items per read.
|
|
140
|
+
|
|
141
|
+
Rather not poll? Pass a `webhook` and we will POST when it is done. The signing secret comes
|
|
142
|
+
back once, on the create — verify a delivery with HMAC-SHA256 over `<timestamp>.<raw body>`
|
|
143
|
+
against the `v1=` value in the `scrapa-signature` header.
|
|
144
|
+
|
|
145
|
+
## What it costs, per call
|
|
146
|
+
|
|
147
|
+
Every metered answer carries what it cost and what is left. Methods hand you the page itself,
|
|
148
|
+
so the meter arrives through a callback — per call, rather than a mutable field two concurrent
|
|
149
|
+
scrapes would fight over.
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
await scrapa.scrape(
|
|
153
|
+
{ url: "https://example.com" },
|
|
154
|
+
{ onUsage: ({ cost, creditsRemaining }) => console.log(cost, creditsRemaining) },
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// …or price the call without making it:
|
|
158
|
+
const { cost, maxCost } = await scrapa.quote({ url: "https://example.com" });
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`maxCost` is the ceiling: with `auto` it is the browser's price, charged only if the page
|
|
162
|
+
turns out to need one.
|
|
163
|
+
|
|
164
|
+
## When something says no
|
|
165
|
+
|
|
166
|
+
A refusal tells you *who* said no, and that decides what you do next.
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
import { ScrapaError } from "@scrapa/sdk";
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
await scrapa.scrape({ url });
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (!(error instanceof ScrapaError)) throw error;
|
|
175
|
+
switch (error.outcome) {
|
|
176
|
+
case "blocked": // the site said no — try another pool, country or engine
|
|
177
|
+
return scrapa.scrape({ url, proxy: { pool: "residential", country: "de" } });
|
|
178
|
+
case "denied": // our policy said no — retrying cannot help, change the URL
|
|
179
|
+
return null;
|
|
180
|
+
case "broken": // we broke it, and already refunded it — worth one retry
|
|
181
|
+
return scrapa.scrape({ url });
|
|
182
|
+
}
|
|
183
|
+
throw error; // your own mistake: a bad field, a missing key, a spent quota
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`error.cost` is `0` on every one of those, because a page you did not get is a page you do not
|
|
188
|
+
pay for. `error.code` carries the exact reason, `error.retryAfter` says when a limit clears,
|
|
189
|
+
and `error.requestId` is what support needs to find the call.
|
|
190
|
+
|
|
191
|
+
The client repeats a call only when a repeat can fix it: something we broke (already refunded)
|
|
192
|
+
or a rate limit that says when it clears. It never repeats a call that timed out on your side,
|
|
193
|
+
because that one may already have read the page — you get the error and decide.
|
|
194
|
+
|
|
195
|
+
## Bringing your own proxy
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
await scrapa.scrape({
|
|
199
|
+
url: "https://example.com",
|
|
200
|
+
proxy: { pool: "custom", server: "http://user:pass@proxy.example:8080" },
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Free on every plan, including the free one, and the bytes are never billed.
|
|
205
|
+
|
|
206
|
+
## Reading the meter
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
const usage = await scrapa.usage.get();
|
|
210
|
+
usage.creditsUsed; // this billing month
|
|
211
|
+
usage.creditsIncluded;
|
|
212
|
+
usage.creditBalance; // purchased credits, which never expire
|
|
213
|
+
usage.resetAt;
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Options
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
new Scrapa({
|
|
220
|
+
apiKey: "sc_…",
|
|
221
|
+
baseUrl: "https://api.scrapa.app/v1", // point at another deployment
|
|
222
|
+
timeout: 65_000, // per attempt, above the API's own ceiling
|
|
223
|
+
maxRetries: 2, // 0 disables
|
|
224
|
+
fetch: myFetch, // a proxy agent, a test double
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Full reference: [docs.scrapa.app](https://docs.scrapa.app).
|
|
229
|
+
|
|
230
|
+
## Licence
|
|
231
|
+
|
|
232
|
+
MIT
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type UsageMeta } from "./generated/contract.ts";
|
|
2
|
+
import { GeneratedOperations } from "./generated/operations.ts";
|
|
3
|
+
export interface ScrapaOptions {
|
|
4
|
+
/** An `sc_` key from the console. */
|
|
5
|
+
apiKey: string;
|
|
6
|
+
/** Defaults to the hosted API. */
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
/** Per-attempt timeout in ms. Default 65000 — above the API's own 60s ceiling for a
|
|
9
|
+
* scrape, so this fires only when something is genuinely wrong. */
|
|
10
|
+
timeout?: number;
|
|
11
|
+
/** Extra attempts for the failures a repeat can fix. Default 2, 0 disables. */
|
|
12
|
+
maxRetries?: number;
|
|
13
|
+
/** Swap the fetch implementation (tests, a proxy, a custom agent). */
|
|
14
|
+
fetch?: typeof globalThis.fetch;
|
|
15
|
+
}
|
|
16
|
+
/** What a generated method hands the transport. */
|
|
17
|
+
export interface RequestSpec {
|
|
18
|
+
method: "GET" | "POST" | "DELETE";
|
|
19
|
+
/** `/sessions/{id}` — a `{name}` is filled from the params and dropped from the rest. */
|
|
20
|
+
path: string;
|
|
21
|
+
/** Safe to repeat: a refunded failure can be retried without charging twice. */
|
|
22
|
+
idempotent: boolean;
|
|
23
|
+
}
|
|
24
|
+
/** Per-call overrides. */
|
|
25
|
+
export interface RequestOptions {
|
|
26
|
+
/** Abort this call — yours to control, and the API stops paying for egress when it fires. */
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
/** Override the client's timeout for one call. */
|
|
29
|
+
timeout?: number;
|
|
30
|
+
/** What the call cost and what is left, as the answer carries it. A method returns the
|
|
31
|
+
* page itself, so this is where the meter goes for the callers who want it — per call,
|
|
32
|
+
* rather than a mutable `lastUsage` that two concurrent scrapes would fight over. */
|
|
33
|
+
onUsage?: (usage: UsageMeta) => void;
|
|
34
|
+
}
|
|
35
|
+
export declare class Scrapa extends GeneratedOperations {
|
|
36
|
+
private readonly apiKey;
|
|
37
|
+
private readonly baseUrl;
|
|
38
|
+
private readonly timeout;
|
|
39
|
+
private readonly maxRetries;
|
|
40
|
+
private readonly fetchImpl;
|
|
41
|
+
constructor(options: ScrapaOptions | string);
|
|
42
|
+
protected request<T>(spec: RequestSpec, params?: object, opts?: RequestOptions): Promise<T>;
|
|
43
|
+
/** How long to wait before repeating a call, or undefined to give up now. */
|
|
44
|
+
private retryDelay;
|
|
45
|
+
private attempt;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAUA,OAAO,EAA2B,KAAK,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAClF,OAAO,EAAoB,mBAAmB,EAAe,MAAM,2BAA2B,CAAC;AAE/F,MAAM,WAAW,aAAa;IAC1B,qCAAqC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,kCAAkC;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;wEACoE;IACpE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CACnC;AAED,mDAAmD;AACnD,MAAM,WAAW,WAAW;IACxB,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,QAAQ,CAAC;IAClC,yFAAyF;IACzF,IAAI,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,UAAU,EAAE,OAAO,CAAC;CACvB;AAED,0BAA0B;AAC1B,MAAM,WAAW,cAAc;IAC3B,6FAA6F;IAC7F,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;0FAEsF;IACtF,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;CACxC;AAkCD,qBAAa,MAAO,SAAQ,mBAAmB;IAC3C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0B;gBAExC,OAAO,EAAE,aAAa,GAAG,MAAM;cAqB3B,OAAO,CAAC,CAAC,EACrB,IAAI,EAAE,WAAW,EACjB,MAAM,GAAE,MAAW,EACnB,IAAI,GAAE,cAAmB,GAC1B,OAAO,CAAC,CAAC,CAAC;IAcb,6EAA6E;IAC7E,OAAO,CAAC,UAAU;YAcJ,OAAO;CAqFxB"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// The transport under every generated method: one request, one `{ data }` unwrap, one error
|
|
2
|
+
// taxonomy.
|
|
3
|
+
//
|
|
4
|
+
// The retry policy is the opinionated part, and it is narrow on purpose. A scrape that we
|
|
5
|
+
// broke was already refunded, so repeating it costs nothing; a rate limit says when it
|
|
6
|
+
// clears. But a call that timed out on YOUR side may well have answered on ours — repeating
|
|
7
|
+
// it reads the page twice and charges twice — so the client hands that one back with its
|
|
8
|
+
// code and lets you decide. The default timeout sits above the API's own ceiling, which is
|
|
9
|
+
// what makes a client-side timeout genuinely exceptional rather than routine.
|
|
10
|
+
import { ScrapaError } from "./errors.js";
|
|
11
|
+
import { HEADERS } from "./generated/contract.js";
|
|
12
|
+
import { DEFAULT_BASE_URL, GeneratedOperations, SDK_VERSION } from "./generated/operations.js";
|
|
13
|
+
/** A Retry-After we would honour beyond this is not a retry, it is a hang: the caller gets
|
|
14
|
+
* the error (with `retryAfter` on it) and schedules it themselves. */
|
|
15
|
+
const MAX_RETRY_DELAY_MS = 10_000;
|
|
16
|
+
/** What the API answers with when it never got to answer at all. */
|
|
17
|
+
function statusCode(status) {
|
|
18
|
+
if (status === 503)
|
|
19
|
+
return "SERVICE_UNAVAILABLE";
|
|
20
|
+
if (status === 404)
|
|
21
|
+
return "NOT_FOUND";
|
|
22
|
+
if (status === 401)
|
|
23
|
+
return "UNAUTHORIZED";
|
|
24
|
+
return "INTERNAL_ERROR";
|
|
25
|
+
}
|
|
26
|
+
/** Seconds from `Retry-After`, or from the error's own details. */
|
|
27
|
+
function retryAfterOf(header, details) {
|
|
28
|
+
const fromDetails = typeof details === "object" && details !== null
|
|
29
|
+
? details.retryAfterSecs
|
|
30
|
+
: undefined;
|
|
31
|
+
if (typeof fromDetails === "number" && Number.isFinite(fromDetails))
|
|
32
|
+
return fromDetails;
|
|
33
|
+
const seconds = header === null ? NaN : Number(header);
|
|
34
|
+
return Number.isFinite(seconds) ? seconds : undefined;
|
|
35
|
+
}
|
|
36
|
+
function numberHeader(response, name) {
|
|
37
|
+
const raw = response.headers.get(name);
|
|
38
|
+
if (raw === null)
|
|
39
|
+
return undefined;
|
|
40
|
+
const value = Number(raw);
|
|
41
|
+
return Number.isFinite(value) ? value : undefined;
|
|
42
|
+
}
|
|
43
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
44
|
+
export class Scrapa extends GeneratedOperations {
|
|
45
|
+
apiKey;
|
|
46
|
+
baseUrl;
|
|
47
|
+
timeout;
|
|
48
|
+
maxRetries;
|
|
49
|
+
fetchImpl;
|
|
50
|
+
constructor(options) {
|
|
51
|
+
super();
|
|
52
|
+
const opts = typeof options === "string" ? { apiKey: options } : options;
|
|
53
|
+
if (!opts.apiKey)
|
|
54
|
+
throw new TypeError("scrapa: an API key is required — create one at https://console.scrapa.app/keys");
|
|
55
|
+
this.apiKey = opts.apiKey;
|
|
56
|
+
// A trailing slash would double up against a path's leading one.
|
|
57
|
+
this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
58
|
+
this.timeout = opts.timeout ?? 65_000;
|
|
59
|
+
this.maxRetries = opts.maxRetries ?? 2;
|
|
60
|
+
const impl = opts.fetch ?? globalThis.fetch;
|
|
61
|
+
if (typeof impl !== "function")
|
|
62
|
+
throw new TypeError("scrapa: no global fetch — use Node 20+, or pass `fetch` in the options.");
|
|
63
|
+
// Bound: an unbound global fetch throws "Illegal invocation" in browsers.
|
|
64
|
+
this.fetchImpl = opts.fetch ?? impl.bind(globalThis);
|
|
65
|
+
}
|
|
66
|
+
async request(spec, params = {}, opts = {}) {
|
|
67
|
+
const retries = spec.idempotent ? this.maxRetries : 0;
|
|
68
|
+
for (let attempt = 0;; attempt++) {
|
|
69
|
+
try {
|
|
70
|
+
return await this.attempt(spec, params, opts);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (!(error instanceof ScrapaError) || attempt >= retries)
|
|
74
|
+
throw error;
|
|
75
|
+
const delay = this.retryDelay(error, attempt);
|
|
76
|
+
if (delay === undefined)
|
|
77
|
+
throw error;
|
|
78
|
+
await sleep(delay);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** How long to wait before repeating a call, or undefined to give up now. */
|
|
83
|
+
retryDelay(error, attempt) {
|
|
84
|
+
// A timeout on OUR side may have answered on theirs; repeating it would read the
|
|
85
|
+
// page — and charge for it — twice. The caller gets the error and decides.
|
|
86
|
+
const worthRepeating = error.retryable && error.code !== "TIMEOUT";
|
|
87
|
+
if (!worthRepeating)
|
|
88
|
+
return undefined;
|
|
89
|
+
if (error.retryAfter !== undefined) {
|
|
90
|
+
const ms = error.retryAfter * 1_000;
|
|
91
|
+
return ms > MAX_RETRY_DELAY_MS ? undefined : ms;
|
|
92
|
+
}
|
|
93
|
+
// Exponential with jitter, so a fleet of clients does not resynchronise onto the
|
|
94
|
+
// same second after an outage.
|
|
95
|
+
return Math.min(250 * 2 ** attempt, 4_000) * (0.5 + Math.random());
|
|
96
|
+
}
|
|
97
|
+
async attempt(spec, params, opts) {
|
|
98
|
+
const rest = { ...params };
|
|
99
|
+
// A templated segment is spent on the URL, not sent again as a query parameter.
|
|
100
|
+
const path = spec.path.replace(/\{(\w+)\}/g, (_, key) => {
|
|
101
|
+
const value = rest[key];
|
|
102
|
+
delete rest[key];
|
|
103
|
+
return encodeURIComponent(String(value ?? ""));
|
|
104
|
+
});
|
|
105
|
+
const url = new URL(this.baseUrl + path);
|
|
106
|
+
const hasBody = spec.method === "POST";
|
|
107
|
+
if (!hasBody)
|
|
108
|
+
for (const [key, value] of Object.entries(rest))
|
|
109
|
+
if (value !== undefined && value !== null)
|
|
110
|
+
url.searchParams.set(key, String(value));
|
|
111
|
+
const headers = {
|
|
112
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
113
|
+
accept: "application/json",
|
|
114
|
+
[HEADERS.client]: `scrapa-js/${SDK_VERSION}`,
|
|
115
|
+
};
|
|
116
|
+
if (hasBody)
|
|
117
|
+
headers["content-type"] = "application/json";
|
|
118
|
+
const timeout = AbortSignal.timeout(opts.timeout ?? this.timeout);
|
|
119
|
+
let response;
|
|
120
|
+
try {
|
|
121
|
+
response = await this.fetchImpl(url.toString(), {
|
|
122
|
+
method: spec.method,
|
|
123
|
+
headers,
|
|
124
|
+
...(hasBody ? { body: JSON.stringify(rest) } : undefined),
|
|
125
|
+
signal: opts.signal ? AbortSignal.any([opts.signal, timeout]) : timeout,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch (cause) {
|
|
129
|
+
// The caller's own abort is theirs, not a failure of ours to describe.
|
|
130
|
+
if (opts.signal?.aborted)
|
|
131
|
+
throw cause;
|
|
132
|
+
const timedOut = cause instanceof Error && cause.name === "TimeoutError";
|
|
133
|
+
throw new ScrapaError({
|
|
134
|
+
code: (timedOut ? "TIMEOUT" : "NETWORK_ERROR"),
|
|
135
|
+
status: 0,
|
|
136
|
+
message: timedOut
|
|
137
|
+
? `scrapa: no answer within ${opts.timeout ?? this.timeout}ms — the page may still have been read and charged.`
|
|
138
|
+
: `scrapa: could not reach ${url.host}.`,
|
|
139
|
+
cause,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const requestId = response.headers.get(HEADERS.requestId) ?? undefined;
|
|
143
|
+
const text = await response.text();
|
|
144
|
+
let payload;
|
|
145
|
+
try {
|
|
146
|
+
payload = text ? JSON.parse(text) : undefined;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
payload = undefined;
|
|
150
|
+
}
|
|
151
|
+
if (response.ok) {
|
|
152
|
+
const answer = payload;
|
|
153
|
+
if (opts.onUsage && answer.meta)
|
|
154
|
+
opts.onUsage(answer.meta);
|
|
155
|
+
return answer.data;
|
|
156
|
+
}
|
|
157
|
+
const envelope = typeof payload === "object" && payload !== null
|
|
158
|
+
? payload.error
|
|
159
|
+
: undefined;
|
|
160
|
+
const details = envelope?.details;
|
|
161
|
+
throw new ScrapaError({
|
|
162
|
+
code: envelope?.code ?? statusCode(response.status),
|
|
163
|
+
status: response.status,
|
|
164
|
+
// A proxy or a cold start can answer HTML; say what actually came back rather
|
|
165
|
+
// than pretending the body was ours.
|
|
166
|
+
message: typeof envelope?.message === "string"
|
|
167
|
+
? envelope.message
|
|
168
|
+
: `scrapa: ${spec.method} ${spec.path} failed with ${response.status}.`,
|
|
169
|
+
messageKey: typeof envelope?.messageKey === "string" ? envelope.messageKey : undefined,
|
|
170
|
+
params: envelope?.params,
|
|
171
|
+
details: envelope ? details : text.slice(0, 500) || undefined,
|
|
172
|
+
retryAfter: retryAfterOf(response.headers.get(HEADERS.retryAfter), details),
|
|
173
|
+
requestId,
|
|
174
|
+
// Every metered answer says what it cost, refusals included: a blocked page
|
|
175
|
+
// reports 0, and that is how "blocked pages cost nothing" is verifiable rather
|
|
176
|
+
// than asserted.
|
|
177
|
+
cost: numberHeader(response, HEADERS.cost),
|
|
178
|
+
creditsRemaining: numberHeader(response, HEADERS.creditsRemaining),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,4FAA4F;AAC5F,YAAY;AACZ,EAAE;AACF,0FAA0F;AAC1F,uFAAuF;AACvF,4FAA4F;AAC5F,yFAAyF;AACzF,2FAA2F;AAC3F,8EAA8E;AAC9E,OAAO,EAAE,WAAW,EAAwB,MAAM,aAAa,CAAC;AAChE,OAAO,EAAE,OAAO,EAAkC,MAAM,yBAAyB,CAAC;AAClF,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAqC/F;uEACuE;AACvE,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAElC,oEAAoE;AACpE,SAAS,UAAU,CAAC,MAAc;IAC9B,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,qBAAqB,CAAC;IACjD,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,WAAW,CAAC;IACvC,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,cAAc,CAAC;IAC1C,OAAO,gBAAgB,CAAC;AAC5B,CAAC;AAED,mEAAmE;AACnE,SAAS,YAAY,CAAC,MAAqB,EAAE,OAAgB;IACzD,MAAM,WAAW,GACb,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI;QAC3C,CAAC,CAAE,OAAwC,CAAC,cAAc;QAC1D,CAAC,CAAC,SAAS,CAAC;IACpB,IAAI,OAAO,WAAW,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC;QAAE,OAAO,WAAW,CAAC;IACxF,MAAM,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACvD,OAAO,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;AAC1D,CAAC;AAED,SAAS,YAAY,CAAC,QAAkB,EAAE,IAAY;IAClD,MAAM,GAAG,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACvC,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IACnC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IAC1B,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AACtD,CAAC;AAED,MAAM,KAAK,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAEhF,MAAM,OAAO,MAAO,SAAQ,mBAAmB;IAC1B,MAAM,CAAS;IACf,OAAO,CAAS;IAChB,OAAO,CAAS;IAChB,UAAU,CAAS;IACnB,SAAS,CAA0B;IAEpD,YAAY,OAA+B;QACvC,KAAK,EAAE,CAAC;QACR,MAAM,IAAI,GAAG,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;QACzE,IAAI,CAAC,IAAI,CAAC,MAAM;YACZ,MAAM,IAAI,SAAS,CACf,gFAAgF,CACnF,CAAC;QACN,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,iEAAiE;QACjE,IAAI,CAAC,OAAO,GAAG,CAAC,IAAI,CAAC,OAAO,IAAI,gBAAgB,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACtE,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,MAAM,CAAC;QACtC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,CAAC,CAAC;QACvC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,CAAC;QAC5C,IAAI,OAAO,IAAI,KAAK,UAAU;YAC1B,MAAM,IAAI,SAAS,CACf,yEAAyE,CAC5E,CAAC;QACN,0EAA0E;QAC1E,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACzD,CAAC;IAES,KAAK,CAAC,OAAO,CACnB,IAAiB,EACjB,SAAiB,EAAE,EACnB,OAAuB,EAAE;QAEzB,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;QACtD,KAAK,IAAI,OAAO,GAAG,CAAC,GAAI,OAAO,EAAE,EAAE,CAAC;YAChC,IAAI,CAAC;gBACD,OAAO,MAAM,IAAI,CAAC,OAAO,CAAI,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;YACrD,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACb,IAAI,CAAC,CAAC,KAAK,YAAY,WAAW,CAAC,IAAI,OAAO,IAAI,OAAO;oBAAE,MAAM,KAAK,CAAC;gBACvE,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;gBAC9C,IAAI,KAAK,KAAK,SAAS;oBAAE,MAAM,KAAK,CAAC;gBACrC,MAAM,KAAK,CAAC,KAAK,CAAC,CAAC;YACvB,CAAC;QACL,CAAC;IACL,CAAC;IAED,6EAA6E;IACrE,UAAU,CAAC,KAAkB,EAAE,OAAe;QAClD,iFAAiF;QACjF,2EAA2E;QAC3E,MAAM,cAAc,GAAG,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC;QACnE,IAAI,CAAC,cAAc;YAAE,OAAO,SAAS,CAAC;QACtC,IAAI,KAAK,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;YACjC,MAAM,EAAE,GAAG,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC;YACpC,OAAO,EAAE,GAAG,kBAAkB,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;QACpD,CAAC;QACD,iFAAiF;QACjF,+BAA+B;QAC/B,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,IAAI,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IACvE,CAAC;IAEO,KAAK,CAAC,OAAO,CAAI,IAAiB,EAAE,MAAc,EAAE,IAAoB;QAC5E,MAAM,IAAI,GAA4B,EAAE,GAAG,MAAM,EAAE,CAAC;QACpD,gFAAgF;QAChF,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC,EAAE,GAAW,EAAE,EAAE;YAC5D,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;YACxB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;YACjB,OAAO,kBAAkB,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC;QACnD,CAAC,CAAC,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;QACzC,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,KAAK,MAAM,CAAC;QACvC,IAAI,CAAC,OAAO;YACR,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC;gBAC3C,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;oBAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QAE5F,MAAM,OAAO,GAA2B;YACpC,aAAa,EAAE,UAAU,IAAI,CAAC,MAAM,EAAE;YACtC,MAAM,EAAE,kBAAkB;YAC1B,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,aAAa,WAAW,EAAE;SAC/C,CAAC;QACF,IAAI,OAAO;YAAE,OAAO,CAAC,cAAc,CAAC,GAAG,kBAAkB,CAAC;QAE1D,MAAM,OAAO,GAAG,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC;QAClE,IAAI,QAAkB,CAAC;QACvB,IAAI,CAAC;YACD,QAAQ,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE;gBAC5C,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,OAAO;gBACP,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;gBACzD,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO;aAC1E,CAAC,CAAC;QACP,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACb,uEAAuE;YACvE,IAAI,IAAI,CAAC,MAAM,EAAE,OAAO;gBAAE,MAAM,KAAK,CAAC;YACtC,MAAM,QAAQ,GAAG,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc,CAAC;YACzE,MAAM,IAAI,WAAW,CAAC;gBAClB,IAAI,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,eAAe,CAA2B;gBACxE,MAAM,EAAE,CAAC;gBACT,OAAO,EAAE,QAAQ;oBACb,CAAC,CAAC,4BAA4B,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,OAAO,qDAAqD;oBAC/G,CAAC,CAAC,2BAA2B,GAAG,CAAC,IAAI,GAAG;gBAC5C,KAAK;aACR,CAAC,CAAC;QACP,CAAC;QAED,MAAM,SAAS,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,SAAS,CAAC;QACvE,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnC,IAAI,OAAgB,CAAC;QACrB,IAAI,CAAC;YACD,OAAO,GAAG,IAAI,CAAC,CAAC,CAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAa,CAAC,CAAC,CAAC,SAAS,CAAC;QAC/D,CAAC;QAAC,MAAM,CAAC;YACL,OAAO,GAAG,SAAS,CAAC;QACxB,CAAC;QAED,IAAI,QAAQ,CAAC,EAAE,EAAE,CAAC;YACd,MAAM,MAAM,GAAG,OAAwC,CAAC;YACxD,IAAI,IAAI,CAAC,OAAO,IAAI,MAAM,CAAC,IAAI;gBAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAC3D,OAAO,MAAM,CAAC,IAAI,CAAC;QACvB,CAAC;QAED,MAAM,QAAQ,GACV,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI;YAC3C,CAAC,CAAE,OAA+C,CAAC,KAAK;YACxD,CAAC,CAAC,SAAS,CAAC;QACpB,MAAM,OAAO,GAAG,QAAQ,EAAE,OAAO,CAAC;QAClC,MAAM,IAAI,WAAW,CAAC;YAClB,IAAI,EAAG,QAAQ,EAAE,IAA8B,IAAI,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC;YAC9E,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,8EAA8E;YAC9E,qCAAqC;YACrC,OAAO,EACH,OAAO,QAAQ,EAAE,OAAO,KAAK,QAAQ;gBACjC,CAAC,CAAC,QAAQ,CAAC,OAAO;gBAClB,CAAC,CAAC,WAAW,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI,gBAAgB,QAAQ,CAAC,MAAM,GAAG;YAC/E,UAAU,EAAE,OAAO,QAAQ,EAAE,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS;YACtF,MAAM,EAAE,QAAQ,EAAE,MAAqD;YACvE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,SAAS;YAC7D,UAAU,EAAE,YAAY,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;YAC3E,SAAS;YACT,4EAA4E;YAC5E,+EAA+E;YAC/E,iBAAiB;YACjB,IAAI,EAAE,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,CAAC;YAC1C,gBAAgB,EAAE,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,gBAAgB,CAAC;SACrE,CAAC,CAAC;IACP,CAAC;CACJ"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type ErrorCode } from "./generated/contract.ts";
|
|
2
|
+
/** Failures that never reached the API, so they carry no server code. */
|
|
3
|
+
export type ClientErrorCode = "NETWORK_ERROR" | "TIMEOUT";
|
|
4
|
+
export interface ScrapaErrorInit {
|
|
5
|
+
code: ErrorCode | ClientErrorCode;
|
|
6
|
+
message: string;
|
|
7
|
+
/** HTTP status, or 0 when no answer arrived. */
|
|
8
|
+
status: number;
|
|
9
|
+
messageKey?: string;
|
|
10
|
+
params?: Record<string, string | number>;
|
|
11
|
+
details?: unknown;
|
|
12
|
+
/** Seconds until the call is worth making again, when the API said. */
|
|
13
|
+
retryAfter?: number;
|
|
14
|
+
/** `X-Request-ID` — quote it to support and we can find the call. */
|
|
15
|
+
requestId?: string;
|
|
16
|
+
/** What this call cost. Zero on every refusal the API charges nothing for, which is
|
|
17
|
+
* every refusal except a page that answered. */
|
|
18
|
+
cost?: number;
|
|
19
|
+
creditsRemaining?: number;
|
|
20
|
+
cause?: unknown;
|
|
21
|
+
}
|
|
22
|
+
export declare class ScrapaError extends Error {
|
|
23
|
+
readonly code: ErrorCode | ClientErrorCode;
|
|
24
|
+
readonly status: number;
|
|
25
|
+
readonly messageKey?: string;
|
|
26
|
+
readonly params?: Record<string, string | number>;
|
|
27
|
+
readonly details?: unknown;
|
|
28
|
+
readonly retryAfter?: number;
|
|
29
|
+
readonly requestId?: string;
|
|
30
|
+
readonly cost?: number;
|
|
31
|
+
readonly creditsRemaining?: number;
|
|
32
|
+
constructor(init: ScrapaErrorInit);
|
|
33
|
+
/**
|
|
34
|
+
* Who said no — the one thing that decides what to do next.
|
|
35
|
+
*
|
|
36
|
+
* "blocked" the site refused the page. It cost you nothing: try another engine, pool
|
|
37
|
+
* or country. The same call again will be refused again.
|
|
38
|
+
* "denied" our policy refused this URL (a private address, the acceptable-use list).
|
|
39
|
+
* Retrying cannot help; change the URL.
|
|
40
|
+
* "broken" we broke it, and already refunded it. Worth one retry as-is.
|
|
41
|
+
*
|
|
42
|
+
* `undefined` for the refusals that were the caller's own doing — a bad field, a missing
|
|
43
|
+
* key, a spent quota — which are not outcomes of a page at all.
|
|
44
|
+
*/
|
|
45
|
+
get outcome(): "blocked" | "denied" | "broken" | undefined;
|
|
46
|
+
/** True when repeating the identical call could work. A limit that clears by waiting
|
|
47
|
+
* and anything we broke, never a verdict on the page itself. */
|
|
48
|
+
get retryable(): boolean;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAOA,OAAO,EAAc,KAAK,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAErE,yEAAyE;AACzE,MAAM,MAAM,eAAe,GAAG,eAAe,GAAG,SAAS,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC5B,IAAI,EAAE,SAAS,GAAG,eAAe,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;IACzC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;qDACiD;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,qBAAa,WAAY,SAAQ,KAAK;IAClC,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,eAAe,CAAC;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;IAClD,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;gBAEvB,IAAI,EAAE,eAAe;IAcjC;;;;;;;;;;;OAWG;IACH,IAAI,OAAO,IAAI,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAEzD;IAED;qEACiE;IACjE,IAAI,SAAS,IAAI,OAAO,CAOvB;CACJ"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Every failure this client can raise, as one class.
|
|
2
|
+
//
|
|
3
|
+
// The API answers a refusal with a machine-readable code, and acting on the code is the
|
|
4
|
+
// whole point: it says WHO said no, which is what decides the caller's next move. The site
|
|
5
|
+
// blocked the page (try another engine, pool or country), our policy refused the URL
|
|
6
|
+
// (retrying cannot help), a limit needs waiting out, or we broke it (retry as-is, it was
|
|
7
|
+
// refunded). Collapse that into `Error("Request failed")` and the caller is left guessing.
|
|
8
|
+
import { OUTCOME_OF } from "./generated/contract.js";
|
|
9
|
+
export class ScrapaError extends Error {
|
|
10
|
+
code;
|
|
11
|
+
status;
|
|
12
|
+
messageKey;
|
|
13
|
+
params;
|
|
14
|
+
details;
|
|
15
|
+
retryAfter;
|
|
16
|
+
requestId;
|
|
17
|
+
cost;
|
|
18
|
+
creditsRemaining;
|
|
19
|
+
constructor(init) {
|
|
20
|
+
super(init.message, init.cause !== undefined ? { cause: init.cause } : undefined);
|
|
21
|
+
this.name = "ScrapaError";
|
|
22
|
+
this.code = init.code;
|
|
23
|
+
this.status = init.status;
|
|
24
|
+
this.messageKey = init.messageKey;
|
|
25
|
+
this.params = init.params;
|
|
26
|
+
this.details = init.details;
|
|
27
|
+
this.retryAfter = init.retryAfter;
|
|
28
|
+
this.requestId = init.requestId;
|
|
29
|
+
this.cost = init.cost;
|
|
30
|
+
this.creditsRemaining = init.creditsRemaining;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Who said no — the one thing that decides what to do next.
|
|
34
|
+
*
|
|
35
|
+
* "blocked" the site refused the page. It cost you nothing: try another engine, pool
|
|
36
|
+
* or country. The same call again will be refused again.
|
|
37
|
+
* "denied" our policy refused this URL (a private address, the acceptable-use list).
|
|
38
|
+
* Retrying cannot help; change the URL.
|
|
39
|
+
* "broken" we broke it, and already refunded it. Worth one retry as-is.
|
|
40
|
+
*
|
|
41
|
+
* `undefined` for the refusals that were the caller's own doing — a bad field, a missing
|
|
42
|
+
* key, a spent quota — which are not outcomes of a page at all.
|
|
43
|
+
*/
|
|
44
|
+
get outcome() {
|
|
45
|
+
return OUTCOME_OF[this.code];
|
|
46
|
+
}
|
|
47
|
+
/** True when repeating the identical call could work. A limit that clears by waiting
|
|
48
|
+
* and anything we broke, never a verdict on the page itself. */
|
|
49
|
+
get retryable() {
|
|
50
|
+
return (this.code === "NETWORK_ERROR" ||
|
|
51
|
+
this.code === "TIMEOUT" ||
|
|
52
|
+
this.code === "RATE_LIMIT_EXCEEDED" ||
|
|
53
|
+
this.outcome === "broken");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,qDAAqD;AACrD,EAAE;AACF,wFAAwF;AACxF,2FAA2F;AAC3F,qFAAqF;AACrF,yFAAyF;AACzF,2FAA2F;AAC3F,OAAO,EAAE,UAAU,EAAkB,MAAM,yBAAyB,CAAC;AAwBrE,MAAM,OAAO,WAAY,SAAQ,KAAK;IACzB,IAAI,CAA8B;IAClC,MAAM,CAAS;IACf,UAAU,CAAU;IACpB,MAAM,CAAmC;IACzC,OAAO,CAAW;IAClB,UAAU,CAAU;IACpB,SAAS,CAAU;IACnB,IAAI,CAAU;IACd,gBAAgB,CAAU;IAEnC,YAAY,IAAqB;QAC7B,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QAClF,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;QAC1B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;QAClC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QAC5B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;QAClC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;QAChC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,gBAAgB,CAAC;IAClD,CAAC;IAED;;;;;;;;;;;OAWG;IACH,IAAI,OAAO;QACP,OAAO,UAAU,CAAC,IAAI,CAAC,IAAiB,CAAC,CAAC;IAC9C,CAAC;IAED;qEACiE;IACjE,IAAI,SAAS;QACT,OAAO,CACH,IAAI,CAAC,IAAI,KAAK,eAAe;YAC7B,IAAI,CAAC,IAAI,KAAK,SAAS;YACvB,IAAI,CAAC,IAAI,KAAK,qBAAqB;YACnC,IAAI,CAAC,OAAO,KAAK,QAAQ,CAC5B,CAAC;IACN,CAAC;CACJ"}
|