@crawlee/core 4.0.0-beta.97 → 4.0.0-beta.99

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.
@@ -17,7 +17,6 @@ export type IsAny<T> = 0 extends 1 & T ? true : false;
17
17
  * When the route map is open (the default `Record<string, ...>`), this is just the regular loose
18
18
  * {@link Source} input. When the map declares concrete labels, providing a `label` requires the matching
19
19
  * `userData` shape and rejects labels not present in the map; unlabeled requests keep loose `userData`.
20
- * @internal
21
20
  */
22
21
  export type LabeledSource<Routes extends Record<keyof Routes, Dictionary>> = string extends keyof Routes ? string | Source : string | Request | ({
23
22
  requestsFromUrl?: string;
@@ -38,7 +37,6 @@ export type TypedRequestsLike<Routes extends Record<keyof Routes, Dictionary>> =
38
37
  /**
39
38
  * The label-aware `addRequests` method signature exposed on a request handler's context when the crawler is
40
39
  * bound to a typed router. Mirrors {@link RestrictedCrawlingContext.addRequests} with typed sources.
41
- * @internal
42
40
  */
43
41
  export type TypedContextAddRequests<Routes extends Record<keyof Routes, Dictionary>> = (requestsLike: ReadonlyDeep<LabeledSource<Routes>[]>, options?: ReadonlyDeep<RequestQueueOperationOptions>) => Promise<void>;
44
42
  /**
@@ -59,10 +57,8 @@ type TypedEnqueueLinksOptions<Options, Routes extends Record<keyof Routes, Dicti
59
57
  * Transforms a context's existing `enqueueLinks` method so that the `label`/`userData` in its options follow
60
58
  * the router's route map, while preserving everything else about the signature (argument optionality and
61
59
  * return type, which differ between crawler types).
62
- * @internal
63
60
  */
64
61
  export type TypedContextEnqueueLinks<EnqueueLinks, Routes extends Record<keyof Routes, Dictionary>> = EnqueueLinks extends (options?: infer Options) => infer Result ? (options?: TypedEnqueueLinksOptions<Options, Routes>) => Result : EnqueueLinks extends (options: infer Options) => infer Result ? (options: TypedEnqueueLinksOptions<Options, Routes>) => Result : EnqueueLinks;
65
- /** @internal */
66
62
  export type WithRequired<T, K extends keyof T> = T & {
67
63
  [P in K]-?: T[P];
68
64
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlee/core",
3
- "version": "4.0.0-beta.97",
3
+ "version": "4.0.0-beta.99",
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": ">=22.0.0"
@@ -53,9 +53,9 @@
53
53
  "@apify/pseudo_url": "^2.0.59",
54
54
  "@apify/timeout": "^0.4.4",
55
55
  "@apify/utilities": "^2.15.5",
56
- "@crawlee/fs-storage": "4.0.0-beta.97",
57
- "@crawlee/types": "4.0.0-beta.97",
58
- "@crawlee/utils": "4.0.0-beta.97",
56
+ "@crawlee/fs-storage": "4.0.0-beta.99",
57
+ "@crawlee/types": "4.0.0-beta.99",
58
+ "@crawlee/utils": "4.0.0-beta.99",
59
59
  "@sapphire/async-queue": "^1.5.5",
60
60
  "@sapphire/shapeshift": "^4.0.0",
61
61
  "@vladfrangu/async_event_emitter": "^2.4.6",
@@ -79,5 +79,5 @@
79
79
  }
80
80
  }
81
81
  },
82
- "gitHead": "43008dd43f4832d083353ba1b731c0b4607c9cfa"
82
+ "gitHead": "ad2748380941842bb10cff100f4b4caad92049e3"
83
83
  }
package/router.d.ts CHANGED
@@ -48,7 +48,6 @@ export type RoutesFromSchemas<Schemas extends RouteSchemas> = {
48
48
  /**
49
49
  * The `userData` type of the default route: inferred from the {@link defaultRoute} schema when the route map
50
50
  * carries one, otherwise the provided `Fallback`.
51
- * @internal
52
51
  */
53
52
  export type DefaultRouteUserData<Routes, Fallback extends Dictionary> = Routes extends {
54
53
  [defaultRoute]: infer DefaultUserData extends Dictionary;
@@ -310,9 +310,6 @@ export interface RequestQueueOperationOptions {
310
310
  */
311
311
  cache?: boolean;
312
312
  }
313
- /**
314
- * @internal
315
- */
316
313
  export interface RequestQueueOperationInfo extends QueueOperationInfo {
317
314
  uniqueKey: string;
318
315
  forefront: boolean;
@@ -17,11 +17,6 @@ import { RequestDeduplicationCache } from './request_dedup_cache.js';
17
17
  * @internal
18
18
  */
19
19
  const MAX_CACHED_REQUESTS = 2_000_000;
20
- /**
21
- * The maximum number of consecutive no-progress retries for unprocessed requests in `addRequestsBatched()`.
22
- * @internal
23
- */
24
- const MAX_UNPROCESSED_REQUESTS_RETRIES = 3;
25
20
  /**
26
21
  * Represents a queue of URLs to crawl, which is used for deep crawling of websites
27
22
  * where you start with several URLs and then recursively
@@ -327,32 +322,27 @@ export class RequestQueue {
327
322
  const requestIterator = generateRequests();
328
323
  const chunks = peekableAsyncIterable(chunkedAsyncIterable(requestIterator, effectiveChunkSize));
329
324
  const chunksIterator = chunks[Symbol.asyncIterator]();
330
- const attemptToAddToQueueAndAddAnyUnprocessed = async (providedRequests, cache = true, unsuccessfulAttempts = 0) => {
331
- const resultsToReturn = [];
332
- const apiResult = await this.addRequests(providedRequests, { forefront: options.forefront, cache });
333
- resultsToReturn.push(...apiResult.processedRequests);
334
- if (apiResult.unprocessedRequests.length) {
335
- // Count attempts that make no progress, so permanently rejected requests (e.g. a malformed
336
- // `userData` shape causing a 400) don't loop forever. Any progress resets the counter.
337
- const attempts = apiResult.processedRequests.length ? 0 : unsuccessfulAttempts + 1;
338
- if (attempts >= MAX_UNPROCESSED_REQUESTS_RETRIES) {
339
- 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 });
340
- return resultsToReturn;
341
- }
342
- await sleep(waitBetweenBatchesMillis);
343
- resultsToReturn.push(...(await attemptToAddToQueueAndAddAnyUnprocessed(providedRequests.filter((r) => !apiResult.processedRequests.some((pr) => pr.uniqueKey === r.uniqueKey)), false, attempts)));
344
- }
345
- return resultsToReturn;
346
- };
347
325
  /**
348
326
  * Process a chunk: send it to the queue, then update the remaining budget if maxNewRequests is active.
327
+ *
328
+ * Requests the backend reports as unprocessed are warned about and skipped rather than retried:
329
+ * `unprocessedRequests` is what remains after the backend's own transient-error handling - a
330
+ * semantic rejection (e.g. a malformed `userData` shape) that re-sending would only re-poke.
331
+ * Retrying transient failures is the storage backend's job, not the frontend's.
349
332
  */
350
333
  const processChunk = async (chunk, cache = true) => {
351
- const results = await attemptToAddToQueueAndAddAnyUnprocessed(chunk, cache);
334
+ const { processedRequests, unprocessedRequests } = await this.addRequests(chunk, {
335
+ forefront: options.forefront,
336
+ cache,
337
+ });
338
+ if (unprocessedRequests.length > 0) {
339
+ this.log.warning('Some requests were rejected by the request queue and will be skipped. ' +
340
+ "This usually means the request data is malformed (e.g. an invalid 'userData' shape).", { unprocessedRequests });
341
+ }
352
342
  if (maxNewRequests !== undefined) {
353
- remainingBudget -= results.filter((r) => !r.wasAlreadyPresent).length;
343
+ remainingBudget -= processedRequests.filter((r) => !r.wasAlreadyPresent).length;
354
344
  }
355
- return results;
345
+ return processedRequests;
356
346
  };
357
347
  /**
358
348
  * Build the final result. When maxNewRequests is set, drains any remaining items
@@ -17,8 +17,6 @@ type Hashable = string;
17
17
  * The manager itself does not resolve identifiers — callers pass explicit `id`, `name`, or `alias` (at most one),
18
18
  * and a pre-bound `backendOpener` promise. When none of `id`, `name`, `alias` are provided, the manager automatically
19
19
  * assigns a reserved default alias.
20
- *
21
- * @ignore
22
20
  */
23
21
  export declare class StorageInstanceManager {
24
22
  private readonly cache;
@@ -118,8 +118,6 @@ class StorageCache {
118
118
  * The manager itself does not resolve identifiers — callers pass explicit `id`, `name`, or `alias` (at most one),
119
119
  * and a pre-bound `backendOpener` promise. When none of `id`, `name`, `alias` are provided, the manager automatically
120
120
  * assigns a reserved default alias.
121
- *
122
- * @ignore
123
121
  */
124
122
  export class StorageInstanceManager {
125
123
  cache = new StorageCache();