@crawlee/core 3.17.1-beta.4 → 3.17.1-beta.40

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.
@@ -22,6 +22,14 @@ class GotScrapingHttpClient {
22
22
  });
23
23
  return {
24
24
  ...gotResult,
25
+ complete: gotResult.complete,
26
+ headers: gotResult.headers,
27
+ ip: gotResult.ip,
28
+ redirectUrls: gotResult.redirectUrls,
29
+ statusCode: gotResult.statusCode,
30
+ statusMessage: gotResult.statusMessage,
31
+ trailers: gotResult.trailers,
32
+ url: gotResult.url,
25
33
  body: gotResult.body,
26
34
  request: { url: request.url, ...gotResult.request },
27
35
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlee/core",
3
- "version": "3.17.1-beta.4",
3
+ "version": "3.17.1-beta.40",
4
4
  "description": "The scalable web crawling and scraping library for JavaScript/Node.js. Enables development of data extraction and web automation jobs (not only) with headless Chrome and Puppeteer.",
5
5
  "engines": {
6
6
  "node": ">=16.0.0"
@@ -59,9 +59,9 @@
59
59
  "@apify/pseudo_url": "^2.0.30",
60
60
  "@apify/timeout": "^0.3.0",
61
61
  "@apify/utilities": "^2.7.10",
62
- "@crawlee/memory-storage": "3.17.1-beta.4",
63
- "@crawlee/types": "3.17.1-beta.4",
64
- "@crawlee/utils": "3.17.1-beta.4",
62
+ "@crawlee/memory-storage": "3.17.1-beta.40",
63
+ "@crawlee/types": "3.17.1-beta.40",
64
+ "@crawlee/utils": "3.17.1-beta.40",
65
65
  "@sapphire/async-queue": "^1.5.1",
66
66
  "@vladfrangu/async_event_emitter": "^2.2.2",
67
67
  "csv-stringify": "^6.2.0",
@@ -83,5 +83,5 @@
83
83
  }
84
84
  }
85
85
  },
86
- "gitHead": "3feb562069d7f3cdd4923fbc6dc2c65857580735"
86
+ "gitHead": "67b3b3eb7d8d0716bfd458aa1a55c16fdb154858"
87
87
  }
package/router.d.ts CHANGED
@@ -2,13 +2,26 @@ import type { Dictionary } from '@crawlee/types';
2
2
  import type { CrawlingContext, LoadedRequest, RestrictedCrawlingContext } from './crawlers/crawler_commons';
3
3
  import type { Request } from './request';
4
4
  import type { Awaitable } from './typedefs';
5
- export interface RouterHandler<Context extends Omit<RestrictedCrawlingContext, 'enqueueLinks'> = CrawlingContext> extends Router<Context> {
5
+ /**
6
+ * The crawling context received by a route handler, with `request.userData` narrowed to `UserData`.
7
+ */
8
+ export type RouterHandlerContext<Context, UserData extends Dictionary> = Omit<Context, 'request'> & {
9
+ request: LoadedRequest<Request<UserData>>;
10
+ };
11
+ /**
12
+ * The set of labels accepted by {@link Router.addHandler}. When the router declares a concrete
13
+ * route map (e.g. `{ PRODUCT: ...; CATEGORY: ... }`), only those labels (plus symbols) are
14
+ * allowed — unknown labels become a compile-time error. When the map is left open (the default
15
+ * `Record<string, ...>`), any string or symbol label is accepted, preserving the original behaviour.
16
+ */
17
+ export type RouterLabel<Routes extends Record<keyof Routes, Dictionary>> = string extends keyof Routes ? string | symbol : (keyof Routes & string) | symbol;
18
+ export interface RouterHandler<Context extends Omit<RestrictedCrawlingContext, 'enqueueLinks'> = CrawlingContext, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>> extends Router<Context, Routes> {
6
19
  (ctx: Context): Awaitable<void>;
7
20
  }
8
21
  export type GetUserDataFromRequest<T> = T extends Request<infer Y> ? Y : never;
9
- export type RouterRoutes<Context, UserData extends Dictionary> = {
10
- [label in string | symbol]: (ctx: Omit<Context, 'request'> & {
11
- request: Request<UserData>;
22
+ export type RouterRoutes<Context, Routes extends Record<keyof Routes, Dictionary>> = {
23
+ [Label in keyof Routes]: (ctx: Omit<Context, 'request'> & {
24
+ request: Request<Routes[Label]>;
12
25
  }) => Awaitable<void>;
13
26
  };
14
27
  /**
@@ -75,8 +88,30 @@ export type RouterRoutes<Context, UserData extends Dictionary> = {
75
88
  * ctx.log.info('...');
76
89
  * });
77
90
  * ```
91
+ *
92
+ * To get `request.userData` typed per label, declare a route map and pass it as the second
93
+ * type argument. The label passed to {@link Router.addHandler} then drives the type of
94
+ * `request.userData`, and unknown labels are rejected at compile time:
95
+ *
96
+ * ```ts
97
+ * import { createCheerioRouter, CheerioCrawlingContext } from 'crawlee';
98
+ *
99
+ * interface Routes {
100
+ * PRODUCT: { sku: string; price: number };
101
+ * CATEGORY: { categoryId: string };
102
+ * }
103
+ *
104
+ * const router = createCheerioRouter<CheerioCrawlingContext, Routes>();
105
+ *
106
+ * router.addHandler('PRODUCT', async ({ request }) => {
107
+ * request.userData.sku; // string
108
+ * request.userData.price; // number
109
+ * });
110
+ *
111
+ * router.addHandler('TYPO', async () => {}); // compile error: not a known label
112
+ * ```
78
113
  */
79
- export declare class Router<Context extends Omit<RestrictedCrawlingContext, 'enqueueLinks'>> {
114
+ export declare class Router<Context extends Omit<RestrictedCrawlingContext, 'enqueueLinks'>, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>> {
80
115
  private readonly routes;
81
116
  private readonly middlewares;
82
117
  /**
@@ -85,17 +120,22 @@ export declare class Router<Context extends Omit<RestrictedCrawlingContext, 'enq
85
120
  */
86
121
  protected constructor();
87
122
  /**
88
- * Registers new route handler for given label.
123
+ * Registers new route handler for given label. When the router declares a route map, the
124
+ * `label` is restricted to the declared labels and `request.userData` is typed accordingly.
125
+ */
126
+ addHandler<Label extends keyof Routes & string>(label: Label, handler: (ctx: RouterHandlerContext<Context, Routes[Label]>) => Awaitable<void>): void;
127
+ /**
128
+ * Registers new route handler for given label, explicitly typing `request.userData` via the
129
+ * `UserData` type argument. Useful when the router has no declared route map (the open default)
130
+ * and you want to type a single handler, or to register a handler under a `symbol` label.
89
131
  */
90
- addHandler<UserData extends Dictionary = GetUserDataFromRequest<Context['request']>>(label: string | symbol, handler: (ctx: Omit<Context, 'request'> & {
91
- request: LoadedRequest<Request<UserData>>;
92
- }) => Awaitable<void>): void;
132
+ addHandler<UserData extends Dictionary = GetUserDataFromRequest<Context['request']>>(label: RouterLabel<Routes>, handler: (ctx: RouterHandlerContext<Context, UserData>) => Awaitable<void>): void;
93
133
  /**
94
- * Registers default route handler.
134
+ * Registers default route handler. As a fallback it can receive any request (including labels not
135
+ * declared in the route map), so `request.userData` defaults to the context's `userData` type
136
+ * (loosely typed by default). Pass an explicit `UserData` type argument to narrow it.
95
137
  */
96
- addDefaultHandler<UserData extends Dictionary = GetUserDataFromRequest<Context['request']>>(handler: (ctx: Omit<Context, 'request'> & {
97
- request: LoadedRequest<Request<UserData>>;
98
- }) => Awaitable<void>): void;
138
+ addDefaultHandler<UserData extends Dictionary = GetUserDataFromRequest<Context['request']>>(handler: (ctx: RouterHandlerContext<Context, UserData>) => Awaitable<void>): void;
99
139
  /**
100
140
  * Registers a middleware that will be fired before the matching route handler.
101
141
  * Multiple middlewares can be registered, they will be fired in the same order.
@@ -129,5 +169,6 @@ export declare class Router<Context extends Omit<RestrictedCrawlingContext, 'enq
129
169
  * await crawler.run();
130
170
  * ```
131
171
  */
132
- static create<Context extends Omit<RestrictedCrawlingContext, 'enqueueLinks'> = CrawlingContext, UserData extends Dictionary = GetUserDataFromRequest<Context['request']>>(routes?: RouterRoutes<Context, UserData>): RouterHandler<Context>;
172
+ static create<Context extends Omit<RestrictedCrawlingContext, 'enqueueLinks'> = CrawlingContext, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>>(routes?: RouterRoutes<Context, Routes>): RouterHandler<Context, Routes>;
173
+ static create<Context extends Omit<RestrictedCrawlingContext, 'enqueueLinks'> = CrawlingContext, UserData extends Dictionary = GetUserDataFromRequest<Context['request']>>(routes?: RouterRoutes<Context, Record<string, UserData>>): RouterHandler<Context, Record<string, UserData>>;
133
174
  }
package/router.js CHANGED
@@ -67,6 +67,28 @@ const defaultRoute = Symbol('default-route');
67
67
  * ctx.log.info('...');
68
68
  * });
69
69
  * ```
70
+ *
71
+ * To get `request.userData` typed per label, declare a route map and pass it as the second
72
+ * type argument. The label passed to {@link Router.addHandler} then drives the type of
73
+ * `request.userData`, and unknown labels are rejected at compile time:
74
+ *
75
+ * ```ts
76
+ * import { createCheerioRouter, CheerioCrawlingContext } from 'crawlee';
77
+ *
78
+ * interface Routes {
79
+ * PRODUCT: { sku: string; price: number };
80
+ * CATEGORY: { categoryId: string };
81
+ * }
82
+ *
83
+ * const router = createCheerioRouter<CheerioCrawlingContext, Routes>();
84
+ *
85
+ * router.addHandler('PRODUCT', async ({ request }) => {
86
+ * request.userData.sku; // string
87
+ * request.userData.price; // number
88
+ * });
89
+ *
90
+ * router.addHandler('TYPO', async () => {}); // compile error: not a known label
91
+ * ```
70
92
  */
71
93
  class Router {
72
94
  /**
@@ -87,15 +109,14 @@ class Router {
87
109
  value: []
88
110
  });
89
111
  }
90
- /**
91
- * Registers new route handler for given label.
92
- */
93
112
  addHandler(label, handler) {
94
113
  this.validate(label);
95
114
  this.routes.set(label, handler);
96
115
  }
97
116
  /**
98
- * Registers default route handler.
117
+ * Registers default route handler. As a fallback it can receive any request (including labels not
118
+ * declared in the route map), so `request.userData` defaults to the context's `userData` type
119
+ * (loosely typed by default). Pass an explicit `UserData` type argument to narrow it.
99
120
  */
100
121
  addDefaultHandler(handler) {
101
122
  this.validate(defaultRoute);
@@ -133,26 +154,6 @@ class Router {
133
154
  throw new Error(message);
134
155
  }
135
156
  }
136
- /**
137
- * Creates new router instance. This instance can then serve as a `requestHandler` of your crawler.
138
- *
139
- * ```ts
140
- * import { Router, CheerioCrawler, CheerioCrawlingContext } from 'crawlee';
141
- *
142
- * const router = Router.create<CheerioCrawlingContext>();
143
- * router.addHandler('label-a', async (ctx) => {
144
- * ctx.log.info('...');
145
- * });
146
- * router.addDefaultHandler(async (ctx) => {
147
- * ctx.log.info('...');
148
- * });
149
- *
150
- * const crawler = new CheerioCrawler({
151
- * requestHandler: router,
152
- * });
153
- * await crawler.run();
154
- * ```
155
- */
156
157
  static create(routes) {
157
158
  const router = new Router();
158
159
  const obj = Object.create(Function.prototype);
@@ -324,9 +324,11 @@ class RequestList {
324
324
  const sourcesFromFunction = await this.sourcesFunction();
325
325
  const sourcesFromFunctionCount = sourcesFromFunction.length;
326
326
  for (let i = 0; i < sourcesFromFunctionCount; i++) {
327
- const source = sourcesFromFunction.shift();
327
+ const source = sourcesFromFunction[i];
328
+ delete sourcesFromFunction[i];
328
329
  this._addRequest(source);
329
330
  }
331
+ sourcesFromFunction.length = 0;
330
332
  }
331
333
  catch (e) {
332
334
  const err = e;
@@ -13,6 +13,11 @@ const request_1 = require("../request");
13
13
  const access_checking_1 = require("./access_checking");
14
14
  const storage_manager_1 = require("./storage_manager");
15
15
  const utils_2 = require("./utils");
16
+ /**
17
+ * Maximum number of consecutive batch-add attempts that make no progress before the remaining
18
+ * unprocessed requests are skipped, so permanently rejected requests don't retry forever.
19
+ */
20
+ const MAX_UNPROCESSED_REQUESTS_RETRIES = 3;
16
21
  class RequestProvider {
17
22
  constructor(options, config = configuration_1.Configuration.getGlobalConfig()) {
18
23
  Object.defineProperty(this, "config", {
@@ -385,13 +390,20 @@ class RequestProvider {
385
390
  const requestIterator = generateRequests();
386
391
  const chunks = (0, utils_1.peekableAsyncIterable)((0, utils_1.chunkedAsyncIterable)(requestIterator, effectiveChunkSize));
387
392
  const chunksIterator = chunks[Symbol.asyncIterator]();
388
- const attemptToAddToQueueAndAddAnyUnprocessed = async (providedRequests, cache = true) => {
393
+ const attemptToAddToQueueAndAddAnyUnprocessed = async (providedRequests, cache = true, unsuccessfulAttempts = 0) => {
389
394
  const resultsToReturn = [];
390
395
  const apiResult = await this.addRequests(providedRequests, { forefront: options.forefront, cache });
391
396
  resultsToReturn.push(...apiResult.processedRequests);
392
397
  if (apiResult.unprocessedRequests.length) {
398
+ // Count attempts that make no progress, so permanently rejected requests (e.g. a malformed
399
+ // `userData` shape causing a 400) don't loop forever. Any progress resets the counter.
400
+ const attempts = apiResult.processedRequests.length ? 0 : unsuccessfulAttempts + 1;
401
+ if (attempts >= MAX_UNPROCESSED_REQUESTS_RETRIES) {
402
+ this.log.warning(`Some requests were consistently rejected by the request queue and will be skipped after ${MAX_UNPROCESSED_REQUESTS_RETRIES} attempts. This usually means the request data is malformed (e.g. an invalid 'userData' shape).`, { unprocessedRequests: apiResult.unprocessedRequests });
403
+ return resultsToReturn;
404
+ }
393
405
  await (0, utils_1.sleep)(waitBetweenBatchesMillis);
394
- resultsToReturn.push(...(await attemptToAddToQueueAndAddAnyUnprocessed(providedRequests.filter((r) => !apiResult.processedRequests.some((pr) => pr.uniqueKey === r.uniqueKey)), false)));
406
+ resultsToReturn.push(...(await attemptToAddToQueueAndAddAnyUnprocessed(providedRequests.filter((r) => !apiResult.processedRequests.some((pr) => pr.uniqueKey === r.uniqueKey)), false, attempts)));
395
407
  }
396
408
  return resultsToReturn;
397
409
  };