@crawlora-org/sdk 1.2.0-sdk.19 → 1.4.0-sdk.1
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/CHANGELOG.md +23 -0
- package/README.md +17 -0
- package/docs/recipes.md +45 -0
- package/package.json +1 -1
- package/src/client.js +175 -23
- package/src/index.d.ts +57 -1
- package/src/index.js +9 -2
- package/src/operations.js +455 -0
- package/src/types.d.ts +333 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.4.0-sdk.1
|
|
4
|
+
|
|
5
|
+
- Configurable retries: `maxRetryDelay`, `retryStatuses`, and an `isRetryable`
|
|
6
|
+
predicate; added an `onRetry` hook, opt-in `requestId` (x-request-id, also on
|
|
7
|
+
`error.requestId`), and a `logger` event sink.
|
|
8
|
+
- Pagination: cursor/token mode (`cursorParam` + `nextCursor`) and a
|
|
9
|
+
`paginateItems` per-item async iterator; operation metadata now exposes
|
|
10
|
+
`paginatable` and `cursorParams`.
|
|
11
|
+
- Streaming: `responseType: "stream"` returns the raw `Response` on success.
|
|
12
|
+
- Config: `CRAWLORA_API_KEY` / `CRAWLORA_BASE_URL` environment fallback.
|
|
13
|
+
|
|
14
|
+
## v1.3.0-sdk.1
|
|
15
|
+
|
|
16
|
+
- Added `CrawloraClientError`, `CrawloraServerError`, and `CrawloraNetworkError`
|
|
17
|
+
subclasses of `CrawloraError` so callers can branch on 4xx vs 5xx vs transport
|
|
18
|
+
failures.
|
|
19
|
+
- Added `client.paginate(operationId, params, options?)`, an async iterator that
|
|
20
|
+
advances the page/offset query parameter and stops on an empty page.
|
|
21
|
+
- Added the generated `OperationIds` map for typo-safe dynamic operation ids,
|
|
22
|
+
e.g. `client.request(OperationIds.BingSearch, { q: "coffee" })`.
|
|
23
|
+
- The generator now shares a single language-neutral core with the Go and Python
|
|
24
|
+
SDKs; generated output is unchanged.
|
|
25
|
+
|
|
3
26
|
## v1.2.0-sdk.19
|
|
4
27
|
|
|
5
28
|
- Regenerated the public SDK contract with the promoted Shopify endpoint family.
|
package/README.md
CHANGED
|
@@ -142,6 +142,23 @@ The error includes `status`, optional API `code`, parsed `body`, response
|
|
|
142
142
|
headers, capped at 30 seconds. Externally aborted requests fail with
|
|
143
143
|
`Crawlora request aborted` and are not retried.
|
|
144
144
|
|
|
145
|
+
`CrawloraError` has three subclasses so you can branch on the failure kind:
|
|
146
|
+
`CrawloraClientError` (4xx, request rejected), `CrawloraServerError` (5xx), and
|
|
147
|
+
`CrawloraNetworkError` (transport failure, timeout, or abort before a response).
|
|
148
|
+
|
|
149
|
+
## Pagination
|
|
150
|
+
|
|
151
|
+
`client.paginate` is an async iterator that advances the page/offset query
|
|
152
|
+
parameter and stops when a page returns no data:
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
for await (const page of crawlora.paginate("ebay-seller-feedback", { seller: "acme" })) {
|
|
156
|
+
for (const review of page.data) console.log(review);
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Override detection with `{ pageParam, start, step, maxPages }`.
|
|
161
|
+
|
|
145
162
|
## Examples
|
|
146
163
|
|
|
147
164
|
Runnable examples live under `examples/` and skip cleanly when required
|
package/docs/recipes.md
CHANGED
|
@@ -74,6 +74,51 @@ try {
|
|
|
74
74
|
}
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
## Custom Retries And Observability
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
const crawlora = new CrawloraClient({
|
|
81
|
+
retries: 3,
|
|
82
|
+
maxRetryDelay: 10000,
|
|
83
|
+
retryStatuses: [429, 503], // or:
|
|
84
|
+
isRetryable: (status, error) => status >= 500,
|
|
85
|
+
onRetry: (attempt, error, delay) => console.warn(`retry ${attempt} after ${delay}ms`, error.status),
|
|
86
|
+
requestId: true, // sets x-request-id; available as error.requestId
|
|
87
|
+
logger: (event) => console.debug(event)
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Branch on `CrawloraClientError` (4xx), `CrawloraServerError` (5xx), and
|
|
92
|
+
`CrawloraNetworkError` (transport).
|
|
93
|
+
|
|
94
|
+
## Pagination
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
// page/offset (auto-detected)
|
|
98
|
+
for await (const page of crawlora.paginate("ebay-seller-feedback", { seller: "acme" })) { /* ... */ }
|
|
99
|
+
|
|
100
|
+
// per-item iteration
|
|
101
|
+
for await (const item of crawlora.paginateItems("ebay-seller-feedback", { seller: "acme" })) { /* ... */ }
|
|
102
|
+
|
|
103
|
+
// cursor/token pagination
|
|
104
|
+
for await (const page of crawlora.paginate("producthunt-leaderboard", {}, {
|
|
105
|
+
cursorParam: "cursor",
|
|
106
|
+
nextCursor: (p) => p.next_cursor
|
|
107
|
+
})) { /* ... */ }
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Streaming Responses
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
const res = await crawlora.request("bing-search", { q: "coffee" }, { responseType: "stream" });
|
|
114
|
+
for await (const chunk of res.body) { /* ... */ }
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Environment Variables
|
|
118
|
+
|
|
119
|
+
`CRAWLORA_API_KEY` and `CRAWLORA_BASE_URL` are used when not set explicitly
|
|
120
|
+
(precedence: option > env > default).
|
|
121
|
+
|
|
77
122
|
## Optional Live Smoke Tests
|
|
78
123
|
|
|
79
124
|
```sh
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { operations, groups } from "./operations.js";
|
|
2
2
|
|
|
3
3
|
const DEFAULT_BASE_URL = "https://api.crawlora.net/api/v1";
|
|
4
|
-
export const VERSION = "1.
|
|
4
|
+
export const VERSION = "1.4.0-sdk.1";
|
|
5
5
|
const DEFAULT_USER_AGENT = `crawlora-js-sdk/${VERSION}`;
|
|
6
6
|
|
|
7
7
|
export class CrawloraError extends Error {
|
|
8
|
-
constructor(message, { status = 0, code, body, headers = {}, response, cause, retryable } = {}) {
|
|
8
|
+
constructor(message, { status = 0, code, body, headers = {}, response, cause, retryable, requestId } = {}) {
|
|
9
9
|
super(message);
|
|
10
10
|
this.name = "CrawloraError";
|
|
11
11
|
this.status = status;
|
|
@@ -15,17 +15,54 @@ export class CrawloraError extends Error {
|
|
|
15
15
|
this.response = response;
|
|
16
16
|
this.cause = cause;
|
|
17
17
|
this.retryable = retryable;
|
|
18
|
+
this.requestId = requestId;
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
// Response status 4xx: the request was rejected by the API (bad params, auth,
|
|
23
|
+
// not found). Usually not retryable.
|
|
24
|
+
export class CrawloraClientError extends CrawloraError {
|
|
25
|
+
constructor(message, options) {
|
|
26
|
+
super(message, options);
|
|
27
|
+
this.name = "CrawloraClientError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Response status 5xx: the API failed to handle a valid request. Retryable.
|
|
32
|
+
export class CrawloraServerError extends CrawloraError {
|
|
33
|
+
constructor(message, options) {
|
|
34
|
+
super(message, options);
|
|
35
|
+
this.name = "CrawloraServerError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Transport failure, timeout, or abort before a response was received.
|
|
40
|
+
export class CrawloraNetworkError extends CrawloraError {
|
|
41
|
+
constructor(message, options) {
|
|
42
|
+
super(message, options);
|
|
43
|
+
this.name = "CrawloraNetworkError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function apiErrorClass(status) {
|
|
48
|
+
return status >= 500 ? CrawloraServerError : CrawloraClientError;
|
|
49
|
+
}
|
|
50
|
+
|
|
21
51
|
export class CrawloraClient {
|
|
22
52
|
constructor(options = {}) {
|
|
23
|
-
|
|
53
|
+
// Precedence: explicit option > environment variable > default.
|
|
54
|
+
this.apiKey = options.apiKey || envVar("CRAWLORA_API_KEY") || "";
|
|
24
55
|
this.jwtToken = options.jwtToken || "";
|
|
25
|
-
this.baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
56
|
+
this.baseUrl = (options.baseUrl || envVar("CRAWLORA_BASE_URL") || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
26
57
|
this.timeout = options.timeout ?? 30000;
|
|
27
58
|
this.retries = normalizeNonNegativeInteger(options.retries ?? 0);
|
|
28
59
|
this.retryDelay = normalizeNonNegativeNumber(options.retryDelay ?? 250);
|
|
60
|
+
this.maxRetryDelay = normalizeNonNegativeNumber(options.maxRetryDelay ?? 30000);
|
|
61
|
+
this.retryStatuses = options.retryStatuses ? new Set(options.retryStatuses) : null;
|
|
62
|
+
this.isRetryable = typeof options.isRetryable === "function" ? options.isRetryable : null;
|
|
63
|
+
this.onRetry = typeof options.onRetry === "function" ? options.onRetry : null;
|
|
64
|
+
this.requestId = options.requestId === true;
|
|
65
|
+
this.logger = typeof options.logger === "function" ? options.logger : null;
|
|
29
66
|
this.headers = { ...(options.headers || {}) };
|
|
30
67
|
this.userAgent = options.userAgent === false ? "" : (options.userAgent || DEFAULT_USER_AGENT);
|
|
31
68
|
this.fetch = options.fetch || globalThis.fetch;
|
|
@@ -55,21 +92,40 @@ export class CrawloraClient {
|
|
|
55
92
|
params = params ?? {};
|
|
56
93
|
options = options ?? {};
|
|
57
94
|
const responseType = validateResponseType(options.responseType || "auto");
|
|
95
|
+
this.#log({ event: "request", operation: operationId });
|
|
58
96
|
|
|
59
97
|
let attempt = 0;
|
|
60
98
|
for (;;) {
|
|
61
99
|
try {
|
|
62
100
|
return await this.#send(operation, params, options, responseType);
|
|
63
101
|
} catch (error) {
|
|
64
|
-
if (!(error instanceof CrawloraError) || error.retryable === false || attempt >= this.retries || !
|
|
102
|
+
if (!(error instanceof CrawloraError) || error.retryable === false || attempt >= this.retries || !this.#isRetryable(error.status, error)) {
|
|
65
103
|
throw error;
|
|
66
104
|
}
|
|
67
105
|
attempt++;
|
|
68
|
-
|
|
106
|
+
const delay = this.#retryDelayFor(error, attempt);
|
|
107
|
+
this.#log({ event: "retry", operation: operationId, attempt, status: error.status, delay });
|
|
108
|
+
if (this.onRetry) this.onRetry(attempt, error, delay);
|
|
109
|
+
await sleep(delay, options.signal);
|
|
69
110
|
}
|
|
70
111
|
}
|
|
71
112
|
}
|
|
72
113
|
|
|
114
|
+
#isRetryable(status, error) {
|
|
115
|
+
if (this.isRetryable) return !!this.isRetryable(status, error);
|
|
116
|
+
if (this.retryStatuses) return status === 0 || this.retryStatuses.has(status);
|
|
117
|
+
return shouldRetry(status);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#retryDelayFor(error, attempt) {
|
|
121
|
+
const retryAfter = parseRetryAfter(error.headers, this.maxRetryDelay);
|
|
122
|
+
return retryAfter ?? retryDelay(this.retryDelay, attempt);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#log(event) {
|
|
126
|
+
if (this.logger) this.logger(event);
|
|
127
|
+
}
|
|
128
|
+
|
|
73
129
|
async #send(operation, params, options, responseType) {
|
|
74
130
|
const { url, body, bodyHeaders } = buildRequest(operation, this.baseUrl, params);
|
|
75
131
|
const headers = mergeHeaders(
|
|
@@ -79,6 +135,7 @@ export class CrawloraClient {
|
|
|
79
135
|
bodyHeaders,
|
|
80
136
|
options.headers
|
|
81
137
|
);
|
|
138
|
+
const requestId = this.requestId ? ensureRequestId(headers) : (headerValue(headers, "x-request-id") || undefined);
|
|
82
139
|
|
|
83
140
|
const controller = new AbortController();
|
|
84
141
|
const timeoutMs = options.timeout ?? this.timeout;
|
|
@@ -98,31 +155,117 @@ export class CrawloraClient {
|
|
|
98
155
|
});
|
|
99
156
|
} catch (error) {
|
|
100
157
|
if (options.signal?.aborted) {
|
|
101
|
-
throw new
|
|
158
|
+
throw new CrawloraNetworkError("Crawlora request aborted", { cause: error, retryable: false, requestId });
|
|
102
159
|
}
|
|
103
160
|
if (timedOut) {
|
|
104
|
-
throw new
|
|
161
|
+
throw new CrawloraNetworkError("Crawlora request timed out", { cause: error, retryable: false, requestId });
|
|
105
162
|
}
|
|
106
|
-
throw new
|
|
163
|
+
throw new CrawloraNetworkError("Crawlora transport error", { cause: error, requestId });
|
|
107
164
|
} finally {
|
|
108
165
|
if (timeout) clearTimeout(timeout);
|
|
109
166
|
}
|
|
110
167
|
|
|
111
168
|
const headersObject = responseHeaders(response);
|
|
112
|
-
|
|
169
|
+
// Streaming success returns the raw Response; the caller reads response.body.
|
|
170
|
+
if (responseType === "stream" && response.ok) {
|
|
171
|
+
return response;
|
|
172
|
+
}
|
|
173
|
+
const parsed = await parseResponse(response, responseType === "stream" ? "auto" : responseType, headersObject, requestId);
|
|
113
174
|
if (!response.ok) {
|
|
114
175
|
const code = parsed && typeof parsed === "object" ? parsed.code : undefined;
|
|
115
176
|
const message = parsed && typeof parsed === "object" && parsed.msg ? parsed.msg : response.statusText;
|
|
116
|
-
|
|
177
|
+
const ApiError = apiErrorClass(response.status);
|
|
178
|
+
throw new ApiError(message || `Crawlora request failed with status ${response.status}`, {
|
|
117
179
|
status: response.status,
|
|
118
180
|
code,
|
|
119
181
|
body: parsed,
|
|
120
182
|
headers: headersObject,
|
|
121
|
-
response
|
|
183
|
+
response,
|
|
184
|
+
requestId
|
|
122
185
|
});
|
|
123
186
|
}
|
|
124
187
|
return parsed;
|
|
125
188
|
}
|
|
189
|
+
|
|
190
|
+
// Async iterator over pages of a paginated operation. Numeric mode advances
|
|
191
|
+
// the page/offset query parameter and stops on an empty page; cursor mode
|
|
192
|
+
// (cursorParam + nextCursor) sends the cursor and stops when nextCursor is falsy.
|
|
193
|
+
// for await (const page of client.paginate("ebay-seller-feedback", { seller })) { ... }
|
|
194
|
+
async *paginate(operationId, params = {}, options = {}) {
|
|
195
|
+
const operation = operations[operationId];
|
|
196
|
+
if (!operation) {
|
|
197
|
+
throw new TypeError(`Unknown Crawlora operation: ${operationId}`);
|
|
198
|
+
}
|
|
199
|
+
const maxPages = options.maxPages ?? Infinity;
|
|
200
|
+
|
|
201
|
+
if (options.cursorParam || options.nextCursor) {
|
|
202
|
+
if (!(options.cursorParam && options.nextCursor)) {
|
|
203
|
+
throw new TypeError("cursor pagination requires both cursorParam and nextCursor");
|
|
204
|
+
}
|
|
205
|
+
if (!operation.queryParams.some((parameter) => parameter.name === options.cursorParam)) {
|
|
206
|
+
throw new TypeError(`cursorParam ${options.cursorParam} is not a query parameter of ${operationId}`);
|
|
207
|
+
}
|
|
208
|
+
let cursor = options.start;
|
|
209
|
+
for (let i = 0; i < maxPages; i++) {
|
|
210
|
+
const pageParams = { ...params };
|
|
211
|
+
if (cursor !== undefined && cursor !== null) pageParams[options.cursorParam] = cursor;
|
|
212
|
+
const response = await this.request(operationId, pageParams, options);
|
|
213
|
+
yield response;
|
|
214
|
+
cursor = options.nextCursor(response);
|
|
215
|
+
if (!cursor) break;
|
|
216
|
+
}
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const pageParam = options.pageParam || detectPageParam(operation);
|
|
221
|
+
if (!pageParam) {
|
|
222
|
+
throw new TypeError(`Operation ${operationId} has no page or offset query parameter to paginate`);
|
|
223
|
+
}
|
|
224
|
+
const step = options.step ?? 1;
|
|
225
|
+
let pageValue = options.start ?? (pageParam === "offset" ? 0 : 1);
|
|
226
|
+
for (let i = 0; i < maxPages; i++) {
|
|
227
|
+
const response = await this.request(operationId, { ...params, [pageParam]: pageValue }, options);
|
|
228
|
+
yield response;
|
|
229
|
+
if (pageIsEmpty(response)) break;
|
|
230
|
+
pageValue += step;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Async iterator over individual items across pages. `items` extracts the list
|
|
235
|
+
// from a page (default: the Crawlora `data` array).
|
|
236
|
+
async *paginateItems(operationId, params = {}, options = {}) {
|
|
237
|
+
const extract = options.items || defaultItems;
|
|
238
|
+
for await (const page of this.paginate(operationId, params, options)) {
|
|
239
|
+
for (const item of extract(page)) yield item;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const PAGE_PARAM_NAMES = ["page", "offset"];
|
|
245
|
+
|
|
246
|
+
function defaultItems(response) {
|
|
247
|
+
if (response && typeof response === "object" && Array.isArray(response.data)) return response.data;
|
|
248
|
+
if (Array.isArray(response)) return response;
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function detectPageParam(operation) {
|
|
253
|
+
for (const name of PAGE_PARAM_NAMES) {
|
|
254
|
+
if (operation.queryParams.some((parameter) => parameter.name === name)) return name;
|
|
255
|
+
}
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function pageIsEmpty(response) {
|
|
260
|
+
if (response === undefined || response === null) return true;
|
|
261
|
+
let data = response;
|
|
262
|
+
if (typeof response === "object" && !Array.isArray(response) && "data" in response) {
|
|
263
|
+
data = response.data;
|
|
264
|
+
}
|
|
265
|
+
if (data === undefined || data === null) return true;
|
|
266
|
+
if (Array.isArray(data)) return data.length === 0;
|
|
267
|
+
if (typeof data === "object") return Object.keys(data).length === 0;
|
|
268
|
+
return !data;
|
|
126
269
|
}
|
|
127
270
|
|
|
128
271
|
function buildRequest(operation, baseUrl, params) {
|
|
@@ -242,13 +385,13 @@ function composeSignal(controller, signal) {
|
|
|
242
385
|
}
|
|
243
386
|
|
|
244
387
|
function validateResponseType(responseType) {
|
|
245
|
-
if (responseType === "auto" || responseType === "json" || responseType === "text") {
|
|
388
|
+
if (responseType === "auto" || responseType === "json" || responseType === "text" || responseType === "stream") {
|
|
246
389
|
return responseType;
|
|
247
390
|
}
|
|
248
|
-
throw new TypeError("Invalid responseType: expected one of auto, json, text");
|
|
391
|
+
throw new TypeError("Invalid responseType: expected one of auto, json, text, stream");
|
|
249
392
|
}
|
|
250
393
|
|
|
251
|
-
async function parseResponse(response, responseType, headers) {
|
|
394
|
+
async function parseResponse(response, responseType, headers, requestId) {
|
|
252
395
|
if (responseType === "text") return response.text();
|
|
253
396
|
const contentType = response.headers.get("content-type") || "";
|
|
254
397
|
if (responseType === "json" || contentType.toLowerCase().includes("application/json")) {
|
|
@@ -261,7 +404,8 @@ async function parseResponse(response, responseType, headers) {
|
|
|
261
404
|
body: text,
|
|
262
405
|
headers,
|
|
263
406
|
response,
|
|
264
|
-
cause: error
|
|
407
|
+
cause: error,
|
|
408
|
+
requestId
|
|
265
409
|
});
|
|
266
410
|
}
|
|
267
411
|
}
|
|
@@ -279,25 +423,33 @@ function retryDelay(baseDelay, attempt) {
|
|
|
279
423
|
return delay + jitter;
|
|
280
424
|
}
|
|
281
425
|
|
|
282
|
-
function
|
|
283
|
-
return parseRetryAfter(error.headers) ?? retryDelay(baseDelay, attempt);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function parseRetryAfter(headers) {
|
|
426
|
+
function parseRetryAfter(headers, cap = 30000) {
|
|
287
427
|
const value = headerValue(headers, "retry-after");
|
|
288
428
|
if (!value) return undefined;
|
|
289
429
|
const seconds = Number(value);
|
|
290
430
|
if (Number.isFinite(seconds) && seconds > 0) {
|
|
291
|
-
return Math.min(seconds * 1000,
|
|
431
|
+
return Math.min(seconds * 1000, cap);
|
|
292
432
|
}
|
|
293
433
|
const date = Date.parse(value);
|
|
294
434
|
if (Number.isFinite(date)) {
|
|
295
435
|
const delay = date - Date.now();
|
|
296
|
-
if (delay > 0) return Math.min(delay,
|
|
436
|
+
if (delay > 0) return Math.min(delay, cap);
|
|
297
437
|
}
|
|
298
438
|
return undefined;
|
|
299
439
|
}
|
|
300
440
|
|
|
441
|
+
function envVar(name) {
|
|
442
|
+
return (typeof process !== "undefined" && process.env && process.env[name]) || undefined;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function ensureRequestId(headers) {
|
|
446
|
+
const existing = headerValue(headers, "x-request-id");
|
|
447
|
+
if (existing) return existing;
|
|
448
|
+
const id = (typeof crypto !== "undefined" && crypto.randomUUID) ? crypto.randomUUID() : `req-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
449
|
+
headers["x-request-id"] = id;
|
|
450
|
+
return id;
|
|
451
|
+
}
|
|
452
|
+
|
|
301
453
|
function headerValue(headers, name) {
|
|
302
454
|
for (const [key, value] of Object.entries(headers || {})) {
|
|
303
455
|
if (key.toLowerCase() === name.toLowerCase()) return value;
|
package/src/index.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
CrawloraGeneratedGroups,
|
|
3
3
|
OperationId,
|
|
4
|
+
OperationParamsMap,
|
|
4
5
|
OperationRequestArgs,
|
|
5
6
|
OperationResponseMap
|
|
6
7
|
} from "./types.js";
|
|
7
8
|
|
|
8
9
|
export type CrawloraParams = Record<string, unknown>;
|
|
9
10
|
|
|
11
|
+
export type CrawloraLogEvent = { event: string; [key: string]: unknown };
|
|
12
|
+
|
|
10
13
|
export interface CrawloraClientOptions {
|
|
11
14
|
apiKey?: string;
|
|
12
15
|
jwtToken?: string;
|
|
@@ -14,6 +17,18 @@ export interface CrawloraClientOptions {
|
|
|
14
17
|
timeout?: number;
|
|
15
18
|
retries?: number;
|
|
16
19
|
retryDelay?: number;
|
|
20
|
+
/** Cap on backoff and Retry-After delays in ms (default 30000). */
|
|
21
|
+
maxRetryDelay?: number;
|
|
22
|
+
/** Override the retryable HTTP status set (network status 0 stays retryable). */
|
|
23
|
+
retryStatuses?: Iterable<number>;
|
|
24
|
+
/** Full predicate; supersedes retryStatuses when set. */
|
|
25
|
+
isRetryable?: (status: number, error: CrawloraError) => boolean;
|
|
26
|
+
/** Called before each retry sleep with (attempt, error, delayMs). */
|
|
27
|
+
onRetry?: (attempt: number, error: CrawloraError, delay: number) => void;
|
|
28
|
+
/** Generate an x-request-id header when absent. */
|
|
29
|
+
requestId?: boolean;
|
|
30
|
+
/** Structured event sink (request/retry); never logs on its own. */
|
|
31
|
+
logger?: (event: CrawloraLogEvent) => void;
|
|
17
32
|
headers?: Record<string, string>;
|
|
18
33
|
userAgent?: string | false;
|
|
19
34
|
fetch?: typeof globalThis.fetch;
|
|
@@ -21,7 +36,7 @@ export interface CrawloraClientOptions {
|
|
|
21
36
|
|
|
22
37
|
export interface CrawloraRequestOptions {
|
|
23
38
|
headers?: Record<string, string>;
|
|
24
|
-
responseType?: "auto" | "json" | "text";
|
|
39
|
+
responseType?: "auto" | "json" | "text" | "stream";
|
|
25
40
|
timeout?: number;
|
|
26
41
|
signal?: AbortSignal;
|
|
27
42
|
}
|
|
@@ -43,6 +58,8 @@ export interface OperationDefinition {
|
|
|
43
58
|
consumes: string[];
|
|
44
59
|
produces: string[];
|
|
45
60
|
security: string[];
|
|
61
|
+
paginatable?: boolean;
|
|
62
|
+
cursorParams?: string[];
|
|
46
63
|
}
|
|
47
64
|
|
|
48
65
|
export class CrawloraError extends Error {
|
|
@@ -53,6 +70,34 @@ export class CrawloraError extends Error {
|
|
|
53
70
|
response?: Response;
|
|
54
71
|
cause?: unknown;
|
|
55
72
|
retryable?: boolean;
|
|
73
|
+
requestId?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Thrown for 4xx API responses (request rejected by the API).
|
|
77
|
+
export class CrawloraClientError extends CrawloraError {}
|
|
78
|
+
// Thrown for 5xx API responses (API failed to handle a valid request).
|
|
79
|
+
export class CrawloraServerError extends CrawloraError {}
|
|
80
|
+
// Thrown for transport failures, timeouts, and aborts before a response.
|
|
81
|
+
export class CrawloraNetworkError extends CrawloraError {}
|
|
82
|
+
|
|
83
|
+
export interface CrawloraPaginateOptions extends CrawloraRequestOptions {
|
|
84
|
+
// Numeric query parameter to advance. Auto-detected as "page" or "offset".
|
|
85
|
+
pageParam?: string;
|
|
86
|
+
// Cursor/token query parameter (cursor mode). Requires nextCursor.
|
|
87
|
+
cursorParam?: string;
|
|
88
|
+
// Extracts the next cursor from a page; iteration stops when it returns falsy.
|
|
89
|
+
nextCursor?: (page: unknown) => unknown;
|
|
90
|
+
// First page value (numeric) or initial cursor value.
|
|
91
|
+
start?: unknown;
|
|
92
|
+
// Amount added to the page parameter after each page. Defaults to 1.
|
|
93
|
+
step?: number;
|
|
94
|
+
// Maximum number of pages to fetch. Defaults to unbounded.
|
|
95
|
+
maxPages?: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface CrawloraPaginateItemsOptions extends CrawloraPaginateOptions {
|
|
99
|
+
// Extracts the item list from a page (default: the `data` array).
|
|
100
|
+
items?: (page: unknown) => Iterable<unknown>;
|
|
56
101
|
}
|
|
57
102
|
|
|
58
103
|
export class CrawloraClient {
|
|
@@ -65,6 +110,16 @@ export class CrawloraClient {
|
|
|
65
110
|
operationId: I,
|
|
66
111
|
...args: OperationRequestArgs<I>
|
|
67
112
|
): Promise<OperationResponseMap[I]>;
|
|
113
|
+
paginate<I extends OperationId>(
|
|
114
|
+
operationId: I,
|
|
115
|
+
params?: OperationParamsMap[I],
|
|
116
|
+
options?: CrawloraPaginateOptions
|
|
117
|
+
): AsyncGenerator<OperationResponseMap[I], void, unknown>;
|
|
118
|
+
paginateItems<I extends OperationId>(
|
|
119
|
+
operationId: I,
|
|
120
|
+
params?: OperationParamsMap[I],
|
|
121
|
+
options?: CrawloraPaginateItemsOptions
|
|
122
|
+
): AsyncGenerator<unknown, void, unknown>;
|
|
68
123
|
[group: string]: unknown;
|
|
69
124
|
}
|
|
70
125
|
|
|
@@ -73,5 +128,6 @@ export interface CrawloraClient extends CrawloraGeneratedGroups {}
|
|
|
73
128
|
export const operations: Record<string, OperationDefinition>;
|
|
74
129
|
export const groups: Record<string, Record<string, string>>;
|
|
75
130
|
export const operationCount: number;
|
|
131
|
+
export const OperationIds: Readonly<Record<string, OperationId>>;
|
|
76
132
|
export const VERSION: string;
|
|
77
133
|
export * from "./types.js";
|
package/src/index.js
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
|
-
export {
|
|
2
|
-
|
|
1
|
+
export {
|
|
2
|
+
CrawloraClient,
|
|
3
|
+
CrawloraError,
|
|
4
|
+
CrawloraClientError,
|
|
5
|
+
CrawloraServerError,
|
|
6
|
+
CrawloraNetworkError,
|
|
7
|
+
VERSION
|
|
8
|
+
} from "./client.js";
|
|
9
|
+
export { operations, groups, operationCount, OperationIds } from "./operations.js";
|