@crawlee/core 4.0.0-beta.150 → 4.0.0-beta.152

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlee/core",
3
- "version": "4.0.0-beta.150",
3
+ "version": "4.0.0-beta.152",
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"
@@ -52,10 +52,10 @@
52
52
  "@apify/log": "^2.5.18",
53
53
  "@apify/timeout": "^0.4.4",
54
54
  "@apify/utilities": "^2.15.5",
55
- "@crawlee/fs-storage": "4.0.0-beta.150",
56
- "@crawlee/http-client": "4.0.0-beta.150",
57
- "@crawlee/types": "4.0.0-beta.150",
58
- "@crawlee/utils": "4.0.0-beta.150",
55
+ "@crawlee/fs-storage": "4.0.0-beta.152",
56
+ "@crawlee/http-client": "4.0.0-beta.152",
57
+ "@crawlee/types": "4.0.0-beta.152",
58
+ "@crawlee/utils": "4.0.0-beta.152",
59
59
  "@sapphire/async-queue": "^1.5.5",
60
60
  "@standard-schema/spec": "^1.0.0",
61
61
  "@vladfrangu/async_event_emitter": "^2.4.6",
@@ -78,5 +78,5 @@
78
78
  }
79
79
  }
80
80
  },
81
- "gitHead": "ef7e9fb2e2fcaa0f74e60a30fa8ae61c60c64626"
81
+ "gitHead": "f4980b3e55ed279e9cecd2956cc9771c9b60ef92"
82
82
  }
@@ -1,6 +1,7 @@
1
1
  import type { Dictionary } from '@crawlee/types';
2
2
  import type { Configuration } from '../configuration.js';
3
- import type { Request, Source } from '../request.js';
3
+ import type { Source } from '../request.js';
4
+ import { Request } from '../request.js';
4
5
  import type { RequestSourceStatus } from './request_loader.js';
5
6
  import type { IRequestManager, PacingSignal, RequestsLike } from './request_manager.js';
6
7
  import type { AddRequestsBatchedOptions, AddRequestsBatchedResult, RequestQueueOperationInfo, RequestQueueOperationOptions } from './request_queue.js';
@@ -2,6 +2,7 @@ import { URL } from 'node:url';
2
2
  import { getDomain } from 'tldts';
3
3
  import { z } from 'zod';
4
4
  import { asyncifyIterable } from '../iterables.js';
5
+ import { Request } from '../request.js';
5
6
  import { serviceLocator } from '../service_locator.js';
6
7
  import { normalizeHostname } from '../url.js';
7
8
  import { parseArgument, schemas } from '../validators.js';
@@ -44,6 +45,11 @@ function newDomainState(domain) {
44
45
  };
45
46
  }
46
47
  const DEFAULT_PERSIST_STATE_KEY = 'CRAWLEE_THROTTLED_DOMAINS';
48
+ /**
49
+ * How many requests one fetch may move out of the wrapped manager before giving up on finding a dispatchable
50
+ * one. The next fetch resumes where it left off, and a migrated request is never walked again.
51
+ */
52
+ const MAX_INNER_MIGRATIONS = 1000;
47
53
  /**
48
54
  * A request manager that wraps another one and paces requests per domain.
49
55
  *
@@ -133,6 +139,8 @@ export class ThrottlingRequestManager {
133
139
  #subManagersReady;
134
140
  /** Batches still being added in the background; keeps {@link ThrottlingRequestManager.checkReadiness} honest. */
135
141
  #inProgressBatchCount = 0;
142
+ /** Requests moved into a sub-queue, which both managers count. Subtracted below; not persisted. */
143
+ #migratedFromInner = 0;
136
144
  /** The latest {@link setExpectedRequestProcessingTimeSecs} hint, kept for a wrapped manager resolved later. */
137
145
  #expectedRequestProcessingSecs;
138
146
  #warnedAbout = new Set();
@@ -588,13 +596,13 @@ export class ThrottlingRequestManager {
588
596
  return this.#selectManagerOrThrow(request.url);
589
597
  }
590
598
  async getTotalCount() {
591
- return this.#sumOverManagers((manager) => manager.getTotalCount());
599
+ return (await this.#sumOverManagers((manager) => manager.getTotalCount())) - this.#migratedFromInner;
592
600
  }
593
601
  async getPendingCount() {
594
602
  return this.#sumOverManagers((manager) => manager.getPendingCount());
595
603
  }
596
604
  async getHandledCount() {
597
- return this.#sumOverManagers((manager) => manager.getHandledCount());
605
+ return (await this.#sumOverManagers((manager) => manager.getHandledCount())) - this.#migratedFromInner;
598
606
  }
599
607
  /**
600
608
  * Reports whether anything can be dispatched right now, and if not, when — or why never.
@@ -638,8 +646,9 @@ export class ThrottlingRequestManager {
638
646
  readyAt = until;
639
647
  }
640
648
  }
649
+ const inner = await this.#getInner();
641
650
  const probed = (await Promise.all([
642
- (await this.#getInner()).checkReadiness(),
651
+ inner.checkReadiness(),
643
652
  ...dispatchable.map(async (subManager) => (await subManager).checkReadiness()),
644
653
  ])).reduce(joinRequestSourceStatuses);
645
654
  // Anything dispatchable outranks a stalled or throttled domain, so we stop here without touching them.
@@ -687,6 +696,7 @@ export class ThrottlingRequestManager {
687
696
  async #purgeDomainQueues() {
688
697
  const subManagers = await this.#getSubManagers();
689
698
  await Promise.all(subManagers.map(async (manager) => manager.purge?.()));
699
+ this.#migratedFromInner = 0;
690
700
  for (const state of this.domainStates.values()) {
691
701
  state.consecutive429Count = 0;
692
702
  state.backoffUntil = 0;
@@ -746,7 +756,7 @@ export class ThrottlingRequestManager {
746
756
  // No dispatch to pace, so the domain keeps its slot.
747
757
  state.crawlDelayUntil = crawlDelayUntilBefore;
748
758
  }
749
- const request = await (await this.#getInner()).fetchNextRequest();
759
+ const request = await this.#fetchFromInner();
750
760
  if (request !== null) {
751
761
  this.#inFlightFromInner.add(request.id ?? request.uniqueKey);
752
762
  }
@@ -761,6 +771,51 @@ export class ThrottlingRequestManager {
761
771
  }
762
772
  return request;
763
773
  }
774
+ /**
775
+ * The next request the wrapped manager can offer whose domain is not being held back.
776
+ *
777
+ * Its requests are not stored per domain, so they cannot be skipped the way a sub-queue can - the only
778
+ * way to find out which domain one belongs to is to take it out. One that turns out to be backing off is
779
+ * moved into its domain's sub-queue, where that domain's clocks hold it back like any other: dispatching
780
+ * it would have the crawler hammer the domain that just told us to wait, and putting it back would have
781
+ * every later fetch walk past it again.
782
+ */
783
+ async #fetchFromInner() {
784
+ const inner = await this.#getInner();
785
+ for (let migrated = 0; migrated < MAX_INNER_MIGRATIONS; migrated++) {
786
+ const request = await inner.fetchNextRequest();
787
+ if (request === null) {
788
+ return null;
789
+ }
790
+ const state = this.#getDomainState(request.url);
791
+ if (state === null || Date.now() >= throttledUntil(state)) {
792
+ return request;
793
+ }
794
+ await this.#migrateToSubQueue(request, inner);
795
+ }
796
+ return null;
797
+ }
798
+ /**
799
+ * Moves a request from the wrapped manager into its domain's sub-queue.
800
+ *
801
+ * Added there before it is marked handled here, so that a crash in between duplicates the request rather
802
+ * than dropping it, and `uniqueKey` absorbs the duplicate. A copy is what lands in the sub-queue: adding
803
+ * the request itself would overwrite the `id` the wrapped manager knows it by.
804
+ */
805
+ async #migrateToSubQueue(request, inner) {
806
+ try {
807
+ const subManager = await this.#selectManagerOrThrow(request.url);
808
+ await subManager.addRequest(new Request({ ...request, id: undefined }));
809
+ }
810
+ catch (error) {
811
+ // Whatever the reason, the request is out of the wrapped manager and in nobody's hands, so it
812
+ // goes back before the failure is raised.
813
+ await inner.reclaimRequest(request, { forefront: true });
814
+ throw error;
815
+ }
816
+ await inner.markRequestAsHandled(request);
817
+ this.#migratedFromInner += 1;
818
+ }
764
819
  async *[Symbol.asyncIterator]() {
765
820
  while (true) {
766
821
  const req = await this.fetchNextRequest();