@crawlee/core 4.0.0-beta.150 → 4.0.0-beta.151
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.
|
|
3
|
+
"version": "4.0.0-beta.151",
|
|
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.
|
|
56
|
-
"@crawlee/http-client": "4.0.0-beta.
|
|
57
|
-
"@crawlee/types": "4.0.0-beta.
|
|
58
|
-
"@crawlee/utils": "4.0.0-beta.
|
|
55
|
+
"@crawlee/fs-storage": "4.0.0-beta.151",
|
|
56
|
+
"@crawlee/http-client": "4.0.0-beta.151",
|
|
57
|
+
"@crawlee/types": "4.0.0-beta.151",
|
|
58
|
+
"@crawlee/utils": "4.0.0-beta.151",
|
|
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": "
|
|
81
|
+
"gitHead": "1865bf2b241da81d8ed6f1215c59f9d0b88ec553"
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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();
|