@crawlee/core 4.0.0-beta.12 → 4.0.0-beta.121
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/README.md +17 -13
- package/autoscaling/autoscaled_pool.d.ts +67 -172
- package/autoscaling/autoscaled_pool.js +165 -320
- package/autoscaling/client_load_signal.d.ts +55 -0
- package/autoscaling/client_load_signal.js +73 -0
- package/autoscaling/concurrency_system.d.ts +268 -0
- package/autoscaling/concurrency_system.js +351 -0
- package/autoscaling/cpu_load_signal.d.ts +43 -0
- package/autoscaling/cpu_load_signal.js +47 -0
- package/autoscaling/event_loop_load_signal.d.ts +51 -0
- package/autoscaling/event_loop_load_signal.js +60 -0
- package/autoscaling/index.d.ts +6 -1
- package/autoscaling/index.js +6 -1
- package/autoscaling/load_signal.d.ts +99 -0
- package/autoscaling/load_signal.js +104 -0
- package/autoscaling/memory_load_signal.d.ts +47 -0
- package/autoscaling/memory_load_signal.js +106 -0
- package/autoscaling/snapshotter.d.ts +58 -163
- package/autoscaling/snapshotter.js +45 -263
- package/autoscaling/system_status.d.ts +62 -84
- package/autoscaling/system_status.js +92 -122
- package/autoscaling/weighted_avg.d.ts +5 -0
- package/autoscaling/weighted_avg.js +14 -0
- package/byte_utils.d.ts +17 -0
- package/byte_utils.js +42 -0
- package/configuration.d.ts +96 -223
- package/configuration.js +170 -222
- package/cookie_utils.d.ts +4 -3
- package/cookie_utils.js +22 -13
- package/crawlers/context_pipeline.d.ts +10 -1
- package/crawlers/context_pipeline.js +31 -8
- package/crawlers/crawler_commons.d.ts +90 -83
- package/crawlers/crawler_commons.js +1 -116
- package/crawlers/error_snapshotter.d.ts +2 -5
- package/crawlers/error_snapshotter.js +7 -8
- package/crawlers/error_tracker.d.ts +0 -1
- package/crawlers/error_tracker.js +0 -1
- package/crawlers/index.d.ts +1 -3
- package/crawlers/index.js +0 -3
- package/crawlers/internals/types.d.ts +0 -1
- package/crawlers/internals/types.js +0 -1
- package/crawlers/statistics.d.ts +143 -59
- package/crawlers/statistics.js +243 -153
- package/debug.d.ts +36 -0
- package/debug.js +70 -0
- package/enqueue_links/enqueue_links.d.ts +59 -68
- package/enqueue_links/enqueue_links.js +57 -62
- package/enqueue_links/index.d.ts +0 -1
- package/enqueue_links/index.js +0 -1
- package/enqueue_links/shared.d.ts +40 -27
- package/enqueue_links/shared.js +90 -68
- package/errors.d.ts +72 -4
- package/errors.js +89 -5
- package/events/event_manager.d.ts +35 -9
- package/events/event_manager.js +10 -12
- package/events/index.d.ts +0 -1
- package/events/index.js +0 -1
- package/events/local_event_manager.d.ts +15 -3
- package/events/local_event_manager.js +39 -13
- package/http.d.ts +9 -0
- package/http.js +28 -0
- package/index.d.ts +7 -4
- package/index.js +6 -3
- package/iterables.d.ts +79 -0
- package/iterables.js +134 -0
- package/log.d.ts +82 -3
- package/log.js +106 -1
- package/memory-storage/consts.d.ts +4 -0
- package/memory-storage/consts.js +4 -0
- package/memory-storage/index.d.ts +1 -0
- package/memory-storage/index.js +1 -0
- package/memory-storage/memory-storage.d.ts +38 -0
- package/memory-storage/memory-storage.js +130 -0
- package/memory-storage/resource-clients/common/base-client.d.ts +4 -0
- package/memory-storage/resource-clients/common/base-client.js +6 -0
- package/memory-storage/resource-clients/dataset.d.ts +40 -0
- package/memory-storage/resource-clients/dataset.js +114 -0
- package/memory-storage/resource-clients/key-value-store.d.ts +63 -0
- package/memory-storage/resource-clients/key-value-store.js +204 -0
- package/memory-storage/resource-clients/request-queue.d.ts +77 -0
- package/memory-storage/resource-clients/request-queue.js +422 -0
- package/memory-storage/utils.d.ts +16 -0
- package/memory-storage/utils.js +41 -0
- package/owned_or_injected.d.ts +58 -0
- package/owned_or_injected.js +98 -0
- package/package.json +13 -12
- package/proxy_configuration.d.ts +24 -132
- package/proxy_configuration.js +24 -143
- package/recoverable_state.d.ts +140 -0
- package/recoverable_state.js +212 -0
- package/request.d.ts +86 -17
- package/request.js +120 -41
- package/router.d.ts +193 -21
- package/router.js +188 -43
- package/serialization.d.ts +0 -1
- package/serialization.js +9 -11
- package/service_locator.d.ts +165 -0
- package/service_locator.js +253 -0
- package/session_pool/consts.d.ts +1 -2
- package/session_pool/consts.js +1 -2
- package/session_pool/errors.d.ts +0 -1
- package/session_pool/errors.js +0 -1
- package/session_pool/fingerprint.d.ts +9 -0
- package/session_pool/fingerprint.js +30 -0
- package/session_pool/index.d.ts +0 -2
- package/session_pool/index.js +0 -2
- package/session_pool/session.d.ts +35 -89
- package/session_pool/session.js +82 -142
- package/session_pool/session_pool.d.ts +69 -90
- package/session_pool/session_pool.js +151 -150
- package/storages/batched_adds.d.ts +37 -0
- package/storages/batched_adds.js +73 -0
- package/storages/dataset.d.ts +114 -54
- package/storages/dataset.js +285 -144
- package/storages/index.d.ts +10 -8
- package/storages/index.js +8 -8
- package/storages/key_value_store.d.ts +185 -42
- package/storages/key_value_store.js +424 -151
- package/storages/key_value_store_codec.d.ts +32 -0
- package/storages/key_value_store_codec.js +113 -0
- package/storages/request_dedup_cache.d.ts +22 -0
- package/storages/request_dedup_cache.js +48 -0
- package/storages/request_list.d.ts +52 -116
- package/storages/request_list.js +159 -133
- package/storages/request_loader.d.ts +101 -0
- package/storages/request_loader.js +1 -0
- package/storages/request_manager.d.ts +33 -0
- package/storages/request_manager.js +1 -0
- package/storages/request_manager_tandem.d.ts +97 -0
- package/storages/request_manager_tandem.js +197 -0
- package/storages/request_queue.d.ts +290 -47
- package/storages/request_queue.js +757 -216
- package/storages/{sitemap_request_list.d.ts → sitemap_request_loader.d.ts} +37 -88
- package/storages/{sitemap_request_list.js → sitemap_request_loader.js} +137 -143
- package/storages/storage_instance_manager.d.ts +87 -0
- package/storages/storage_instance_manager.js +256 -0
- package/storages/storage_stats.d.ts +48 -0
- package/storages/storage_stats.js +29 -0
- package/storages/throttling_request_manager.d.ts +216 -0
- package/storages/throttling_request_manager.js +453 -0
- package/storages/transaction.d.ts +252 -0
- package/storages/transaction.js +251 -0
- package/storages/utils.d.ts +58 -11
- package/storages/utils.js +64 -13
- package/system-info/cpu-info.d.ts +67 -0
- package/system-info/cpu-info.js +216 -0
- package/system-info/memory-info.d.ts +31 -0
- package/system-info/memory-info.js +115 -0
- package/system-info/ps-tree.d.ts +17 -0
- package/system-info/ps-tree.js +144 -0
- package/system-info/runtime.d.ts +14 -0
- package/system-info/runtime.js +80 -0
- package/typedefs.d.ts +0 -6
- package/typedefs.js +0 -1
- package/url.d.ts +9 -0
- package/url.js +11 -0
- package/validators.d.ts +8 -1
- package/validators.js +10 -3
- package/autoscaling/autoscaled_pool.d.ts.map +0 -1
- package/autoscaling/autoscaled_pool.js.map +0 -1
- package/autoscaling/index.d.ts.map +0 -1
- package/autoscaling/index.js.map +0 -1
- package/autoscaling/snapshotter.d.ts.map +0 -1
- package/autoscaling/snapshotter.js.map +0 -1
- package/autoscaling/system_status.d.ts.map +0 -1
- package/autoscaling/system_status.js.map +0 -1
- package/configuration.d.ts.map +0 -1
- package/configuration.js.map +0 -1
- package/cookie_utils.d.ts.map +0 -1
- package/cookie_utils.js.map +0 -1
- package/crawlers/context_pipeline.d.ts.map +0 -1
- package/crawlers/context_pipeline.js.map +0 -1
- package/crawlers/crawler_commons.d.ts.map +0 -1
- package/crawlers/crawler_commons.js.map +0 -1
- package/crawlers/crawler_utils.d.ts +0 -10
- package/crawlers/crawler_utils.d.ts.map +0 -1
- package/crawlers/crawler_utils.js +0 -12
- package/crawlers/crawler_utils.js.map +0 -1
- package/crawlers/error_snapshotter.d.ts.map +0 -1
- package/crawlers/error_snapshotter.js.map +0 -1
- package/crawlers/error_tracker.d.ts.map +0 -1
- package/crawlers/error_tracker.js.map +0 -1
- package/crawlers/index.d.ts.map +0 -1
- package/crawlers/index.js.map +0 -1
- package/crawlers/internals/types.d.ts.map +0 -1
- package/crawlers/internals/types.js.map +0 -1
- package/crawlers/statistics.d.ts.map +0 -1
- package/crawlers/statistics.js.map +0 -1
- package/enqueue_links/enqueue_links.d.ts.map +0 -1
- package/enqueue_links/enqueue_links.js.map +0 -1
- package/enqueue_links/index.d.ts.map +0 -1
- package/enqueue_links/index.js.map +0 -1
- package/enqueue_links/shared.d.ts.map +0 -1
- package/enqueue_links/shared.js.map +0 -1
- package/errors.d.ts.map +0 -1
- package/errors.js.map +0 -1
- package/events/event_manager.d.ts.map +0 -1
- package/events/event_manager.js.map +0 -1
- package/events/index.d.ts.map +0 -1
- package/events/index.js.map +0 -1
- package/events/local_event_manager.d.ts.map +0 -1
- package/events/local_event_manager.js.map +0 -1
- package/http_clients/base-http-client.d.ts +0 -134
- package/http_clients/base-http-client.d.ts.map +0 -1
- package/http_clients/base-http-client.js +0 -33
- package/http_clients/base-http-client.js.map +0 -1
- package/http_clients/form-data-like.d.ts +0 -67
- package/http_clients/form-data-like.d.ts.map +0 -1
- package/http_clients/form-data-like.js +0 -5
- package/http_clients/form-data-like.js.map +0 -1
- package/http_clients/got-scraping-http-client.d.ts +0 -15
- package/http_clients/got-scraping-http-client.d.ts.map +0 -1
- package/http_clients/got-scraping-http-client.js +0 -69
- package/http_clients/got-scraping-http-client.js.map +0 -1
- package/http_clients/index.d.ts +0 -3
- package/http_clients/index.d.ts.map +0 -1
- package/http_clients/index.js +0 -3
- package/http_clients/index.js.map +0 -1
- package/index.d.ts.map +0 -1
- package/index.js.map +0 -1
- package/log.d.ts.map +0 -1
- package/log.js.map +0 -1
- package/proxy_configuration.d.ts.map +0 -1
- package/proxy_configuration.js.map +0 -1
- package/request.d.ts.map +0 -1
- package/request.js.map +0 -1
- package/router.d.ts.map +0 -1
- package/router.js.map +0 -1
- package/serialization.d.ts.map +0 -1
- package/serialization.js.map +0 -1
- package/session_pool/consts.d.ts.map +0 -1
- package/session_pool/consts.js.map +0 -1
- package/session_pool/errors.d.ts.map +0 -1
- package/session_pool/errors.js.map +0 -1
- package/session_pool/events.d.ts +0 -3
- package/session_pool/events.d.ts.map +0 -1
- package/session_pool/events.js +0 -3
- package/session_pool/events.js.map +0 -1
- package/session_pool/index.d.ts.map +0 -1
- package/session_pool/index.js.map +0 -1
- package/session_pool/session.d.ts.map +0 -1
- package/session_pool/session.js.map +0 -1
- package/session_pool/session_pool.d.ts.map +0 -1
- package/session_pool/session_pool.js.map +0 -1
- package/storages/access_checking.d.ts +0 -13
- package/storages/access_checking.d.ts.map +0 -1
- package/storages/access_checking.js +0 -14
- package/storages/access_checking.js.map +0 -1
- package/storages/dataset.d.ts.map +0 -1
- package/storages/dataset.js.map +0 -1
- package/storages/index.d.ts.map +0 -1
- package/storages/index.js.map +0 -1
- package/storages/key_value_store.d.ts.map +0 -1
- package/storages/key_value_store.js.map +0 -1
- package/storages/request_list.d.ts.map +0 -1
- package/storages/request_list.js.map +0 -1
- package/storages/request_provider.d.ts +0 -308
- package/storages/request_provider.d.ts.map +0 -1
- package/storages/request_provider.js +0 -555
- package/storages/request_provider.js.map +0 -1
- package/storages/request_queue.d.ts.map +0 -1
- package/storages/request_queue.js.map +0 -1
- package/storages/request_queue_v2.d.ts +0 -87
- package/storages/request_queue_v2.d.ts.map +0 -1
- package/storages/request_queue_v2.js +0 -438
- package/storages/request_queue_v2.js.map +0 -1
- package/storages/sitemap_request_list.d.ts.map +0 -1
- package/storages/sitemap_request_list.js.map +0 -1
- package/storages/storage_manager.d.ts +0 -58
- package/storages/storage_manager.d.ts.map +0 -1
- package/storages/storage_manager.js +0 -105
- package/storages/storage_manager.js.map +0 -1
- package/storages/utils.d.ts.map +0 -1
- package/storages/utils.js.map +0 -1
- package/tsconfig.build.tsbuildinfo +0 -1
- package/typedefs.d.ts.map +0 -1
- package/typedefs.js.map +0 -1
- package/validators.d.ts.map +0 -1
- package/validators.js.map +0 -1
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { AsyncQueue } from '@sapphire/async-queue';
|
|
2
|
+
/** Reserved alias for the default (unnamed) storage. */
|
|
3
|
+
const DEFAULT_STORAGE_ALIAS = '__default__';
|
|
4
|
+
/**
|
|
5
|
+
* Three-tier cache for storage instances, modelled after crawlee-python's `_StorageCache`.
|
|
6
|
+
*
|
|
7
|
+
* Each tier maps `[storageClass][key][backendCacheKey] → instance`:
|
|
8
|
+
* - `byId` — keyed by the backend-assigned storage id
|
|
9
|
+
* - `byName` — keyed by the persistent storage name
|
|
10
|
+
* - `byAlias` — keyed by a run-scoped alias (e.g. `'__default__'` for unnamed storages)
|
|
11
|
+
*/
|
|
12
|
+
class StorageCache {
|
|
13
|
+
byId = new Map();
|
|
14
|
+
byName = new Map();
|
|
15
|
+
byAlias = new Map();
|
|
16
|
+
get(cls, { id, name, alias, backendCacheKey, }) {
|
|
17
|
+
for (const [tier, key] of [
|
|
18
|
+
[this.byId, id],
|
|
19
|
+
[this.byName, name],
|
|
20
|
+
[this.byAlias, alias],
|
|
21
|
+
]) {
|
|
22
|
+
if (key === undefined)
|
|
23
|
+
continue;
|
|
24
|
+
const cached = tier.get(cls)?.get(key)?.get(backendCacheKey);
|
|
25
|
+
if (cached) {
|
|
26
|
+
if (cached instanceof cls) {
|
|
27
|
+
return cached;
|
|
28
|
+
}
|
|
29
|
+
throw new Error('Cached storage instance type mismatch.');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
/** Write a single entry into a given tier. */
|
|
35
|
+
setInMap(tier, cls, key, instance, backendCacheKey) {
|
|
36
|
+
if (!tier.has(cls))
|
|
37
|
+
tier.set(cls, new Map());
|
|
38
|
+
const keyMap = tier.get(cls);
|
|
39
|
+
if (!keyMap.has(key))
|
|
40
|
+
keyMap.set(key, new Map());
|
|
41
|
+
keyMap.get(key).set(backendCacheKey, instance);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Cache an instance under its actual id, name, and an optional alias.
|
|
45
|
+
*/
|
|
46
|
+
set(cls, instance, backendCacheKey, alias) {
|
|
47
|
+
// Always cache by id.
|
|
48
|
+
this.setInMap(this.byId, cls, instance.id, instance, backendCacheKey);
|
|
49
|
+
// Cache by name — only for named storages.
|
|
50
|
+
if (instance.name) {
|
|
51
|
+
this.setInMap(this.byName, cls, instance.name, instance, backendCacheKey);
|
|
52
|
+
}
|
|
53
|
+
// Cache by alias — only for unnamed storages opened via alias.
|
|
54
|
+
if (alias !== undefined) {
|
|
55
|
+
this.setInMap(this.byAlias, cls, alias, instance, backendCacheKey);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
removeFromCache(instance) {
|
|
59
|
+
const storageType = instance.constructor;
|
|
60
|
+
for (const tier of [this.byId, this.byName, this.byAlias]) {
|
|
61
|
+
const classMap = tier.get(storageType);
|
|
62
|
+
if (!classMap)
|
|
63
|
+
continue;
|
|
64
|
+
for (const keyMap of classMap.values()) {
|
|
65
|
+
for (const [cacheKey, cached] of keyMap) {
|
|
66
|
+
if (cached === instance) {
|
|
67
|
+
keyMap.delete(cacheKey);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Ensure that the same string is not used as both a name and an alias for the same
|
|
75
|
+
* storage class + backend combination. Mirrors crawlee-python's `_check_name_alias_conflict`.
|
|
76
|
+
*/
|
|
77
|
+
checkNameAliasConflict(cls, { name, alias, backendCacheKey }) {
|
|
78
|
+
if (alias) {
|
|
79
|
+
const existingByName = this.byName.get(cls)?.get(alias)?.get(backendCacheKey);
|
|
80
|
+
if (existingByName) {
|
|
81
|
+
throw new Error(`Cannot open storage with alias "${alias}" because a named storage with the same identifier already exists.`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (name) {
|
|
85
|
+
const existingByAlias = this.byAlias.get(cls)?.get(name)?.get(backendCacheKey);
|
|
86
|
+
if (existingByAlias) {
|
|
87
|
+
throw new Error(`Cannot open storage with name "${name}" because an alias storage with the same identifier already exists.` +
|
|
88
|
+
` If you meant to open the alias storage, use { alias: "${name}" } instead.`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Iterate all cached instances across all storage types. */
|
|
93
|
+
*allValues() {
|
|
94
|
+
const seen = new Set();
|
|
95
|
+
for (const classMap of this.byId.values()) {
|
|
96
|
+
for (const keyMap of classMap.values()) {
|
|
97
|
+
for (const instance of keyMap.values()) {
|
|
98
|
+
if (!seen.has(instance)) {
|
|
99
|
+
seen.add(instance);
|
|
100
|
+
yield instance;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
clear() {
|
|
107
|
+
this.byId.clear();
|
|
108
|
+
this.byName.clear();
|
|
109
|
+
this.byAlias.clear();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Unified manager for opening and caching storage instances (Dataset, KeyValueStore, RequestQueue).
|
|
114
|
+
*
|
|
115
|
+
* A single instance manages all storage types. Instances are cached by
|
|
116
|
+
* `(storageClass, id/name/alias, backendCacheKey)` so the same storage is never opened twice.
|
|
117
|
+
*
|
|
118
|
+
* The manager itself does not resolve identifiers — callers pass explicit `id`, `name`, or `alias` (at most one),
|
|
119
|
+
* and a pre-bound `backendOpener` promise. When none of `id`, `name`, `alias` are provided, the manager automatically
|
|
120
|
+
* assigns a reserved default alias.
|
|
121
|
+
*/
|
|
122
|
+
export class StorageInstanceManager {
|
|
123
|
+
#cache = new StorageCache();
|
|
124
|
+
#openerLocks = new Map();
|
|
125
|
+
/**
|
|
126
|
+
* Open (or retrieve from cache) a storage instance.
|
|
127
|
+
*
|
|
128
|
+
* @param cls The storage class constructor (e.g. `Dataset`, `KeyValueStore`, `RequestQueue`).
|
|
129
|
+
* @param id Storage ID (mutually exclusive with `name` and `alias`).
|
|
130
|
+
* @param name Storage name (mutually exclusive with `id` and `alias`).
|
|
131
|
+
* @param alias Run-scoped alias (mutually exclusive with `id` and `name`).
|
|
132
|
+
* Automatically assigned when no identifier is provided.
|
|
133
|
+
* @param backendOpener A **lazy** factory that creates the sub-backend.
|
|
134
|
+
* Only called on a cache miss.
|
|
135
|
+
* @param backendCacheKey Opaque key identifying the storage backend, so that the same logical
|
|
136
|
+
* storage opened through different clients is cached separately.
|
|
137
|
+
*/
|
|
138
|
+
async openStorage(cls, { id, name, alias, backendOpener, backendCacheKey, }) {
|
|
139
|
+
// Auto-set alias='__default__' when no parameters are specified (mirrors crawlee-python).
|
|
140
|
+
if (!id && !name && !alias) {
|
|
141
|
+
alias = DEFAULT_STORAGE_ALIAS;
|
|
142
|
+
}
|
|
143
|
+
// Fast-path cache check (no lock).
|
|
144
|
+
if (alias !== undefined) {
|
|
145
|
+
const cached = this.#cache.get(cls, { alias, backendCacheKey });
|
|
146
|
+
if (cached)
|
|
147
|
+
return cached;
|
|
148
|
+
}
|
|
149
|
+
else if (id) {
|
|
150
|
+
const cached = this.#cache.get(cls, { id, backendCacheKey });
|
|
151
|
+
if (cached)
|
|
152
|
+
return cached;
|
|
153
|
+
}
|
|
154
|
+
else if (name) {
|
|
155
|
+
const cached = this.#cache.get(cls, { name, backendCacheKey });
|
|
156
|
+
if (cached)
|
|
157
|
+
return cached;
|
|
158
|
+
}
|
|
159
|
+
const identifierKey = id ?? name ?? alias ?? DEFAULT_STORAGE_ALIAS;
|
|
160
|
+
const lockKey = `${cls.name}:${identifierKey}:${backendCacheKey}`;
|
|
161
|
+
if (!this.#openerLocks.has(lockKey)) {
|
|
162
|
+
this.#openerLocks.set(lockKey, new AsyncQueue());
|
|
163
|
+
}
|
|
164
|
+
const queue = this.#openerLocks.get(lockKey);
|
|
165
|
+
await queue.wait();
|
|
166
|
+
try {
|
|
167
|
+
// Double-check cache under lock (another caller may have filled it while we waited).
|
|
168
|
+
if (alias !== undefined) {
|
|
169
|
+
const cached = this.#cache.get(cls, { alias, backendCacheKey });
|
|
170
|
+
if (cached)
|
|
171
|
+
return cached;
|
|
172
|
+
}
|
|
173
|
+
else if (id) {
|
|
174
|
+
const cached = this.#cache.get(cls, { id, backendCacheKey });
|
|
175
|
+
if (cached)
|
|
176
|
+
return cached;
|
|
177
|
+
}
|
|
178
|
+
else if (name) {
|
|
179
|
+
const cached = this.#cache.get(cls, { name, backendCacheKey });
|
|
180
|
+
if (cached)
|
|
181
|
+
return cached;
|
|
182
|
+
}
|
|
183
|
+
// Prevent the same string from being used as both a name and an alias.
|
|
184
|
+
this.#cache.checkNameAliasConflict(cls, { name, alias, backendCacheKey });
|
|
185
|
+
// Cache miss — create the sub-backend and storage instance.
|
|
186
|
+
const subBackend = await backendOpener();
|
|
187
|
+
const storageInfo = await subBackend.getMetadata();
|
|
188
|
+
// Storage frontends are thin wrappers over the backend. We hand them the resolved metadata
|
|
189
|
+
// we just fetched (so `id`/`name` etc. are available synchronously) along with the backend.
|
|
190
|
+
const instance = new cls({ metadata: storageInfo, backend: subBackend });
|
|
191
|
+
// Atomic cache writes (no awaits between these).
|
|
192
|
+
this.#cache.set(cls, instance, backendCacheKey, alias);
|
|
193
|
+
return instance;
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
queue.shift();
|
|
197
|
+
// Clean up idle locks so the map doesn't grow unboundedly
|
|
198
|
+
// (mirrors crawlee-python's WeakValueDictionary behaviour).
|
|
199
|
+
if (queue.remaining === 0) {
|
|
200
|
+
this.#openerLocks.delete(lockKey);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Remove a storage instance from the cache (called from `storage.drop()`).
|
|
206
|
+
*/
|
|
207
|
+
removeFromCache(instance) {
|
|
208
|
+
this.#cache.removeFromCache(instance);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Clear the entire cache. Also calls `clearCache()` on any cached KeyValueStore
|
|
212
|
+
* instances (duck-typed to avoid importing KeyValueStore and circular dependencies).
|
|
213
|
+
* Called during service locator reset.
|
|
214
|
+
*/
|
|
215
|
+
clearCache() {
|
|
216
|
+
for (const instance of this.#cache.allValues()) {
|
|
217
|
+
if ('clearCache' in instance && typeof instance.clearCache === 'function') {
|
|
218
|
+
instance.clearCache();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
this.#cache.clear();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Decompose a user-provided `identifier` (the `Dataset.open()` / `KeyValueStore.open()` /
|
|
226
|
+
* `RequestQueue.open()` argument) into separate `id`, `name`, and `alias` fields that
|
|
227
|
+
* the `StorageInstanceManager` and `StorageBackend.create*Client` expect.
|
|
228
|
+
*
|
|
229
|
+
* - `null` / `undefined` / `{}` → default storage alias
|
|
230
|
+
* - `string` → resolved via `storageExists` (ID-first, then name)
|
|
231
|
+
* - `{ id }` → `{ id }`
|
|
232
|
+
* - `{ name }` → `{ name }`
|
|
233
|
+
* - `{ alias }` → `{ alias }`
|
|
234
|
+
*/
|
|
235
|
+
export async function resolveStorageIdentifier(identifier, storageBackend, storageType) {
|
|
236
|
+
if (identifier === null || identifier === undefined) {
|
|
237
|
+
return { alias: DEFAULT_STORAGE_ALIAS };
|
|
238
|
+
}
|
|
239
|
+
if (typeof identifier === 'string') {
|
|
240
|
+
if (storageBackend.storageExists && (await storageBackend.storageExists(identifier, storageType))) {
|
|
241
|
+
return { id: identifier };
|
|
242
|
+
}
|
|
243
|
+
return { name: identifier };
|
|
244
|
+
}
|
|
245
|
+
if (identifier.id) {
|
|
246
|
+
return { id: identifier.id };
|
|
247
|
+
}
|
|
248
|
+
if (identifier.name) {
|
|
249
|
+
return { name: identifier.name };
|
|
250
|
+
}
|
|
251
|
+
if ('alias' in identifier && identifier.alias) {
|
|
252
|
+
return { alias: identifier.alias };
|
|
253
|
+
}
|
|
254
|
+
// Empty object — treated as default storage.
|
|
255
|
+
return { alias: DEFAULT_STORAGE_ALIAS };
|
|
256
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend-independent usage counters tracked by the storage frontend classes
|
|
3
|
+
* ({@link Dataset}, {@link KeyValueStore}, {@link RequestQueue}).
|
|
4
|
+
*
|
|
5
|
+
* These count the operations the frontend issues against its underlying storage backend, so they are
|
|
6
|
+
* meaningful for any storage backend (memory, file system, cloud). They are tallied per client call
|
|
7
|
+
* — e.g. iterating a key-value store increments `readCount` once per record fetched and `listCount`
|
|
8
|
+
* once per listed page. Backend-specific figures that the frontend cannot compute (such as the number
|
|
9
|
+
* of bytes stored) are intentionally not included here; read those from the backend's own API instead.
|
|
10
|
+
*/
|
|
11
|
+
/** Usage counters for a {@link Dataset}. */
|
|
12
|
+
export interface DatasetStats {
|
|
13
|
+
/** Number of read operations issued to the dataset client (e.g. `getData`). */
|
|
14
|
+
readCount: number;
|
|
15
|
+
/** Number of write operations issued to the dataset client (e.g. `pushData`). */
|
|
16
|
+
writeCount: number;
|
|
17
|
+
}
|
|
18
|
+
/** Usage counters for a {@link KeyValueStore}. */
|
|
19
|
+
export interface KeyValueStoreStats {
|
|
20
|
+
/** Number of read operations issued to the key-value store client (e.g. `getValue`). */
|
|
21
|
+
readCount: number;
|
|
22
|
+
/** Number of write operations issued to the key-value store client (e.g. `setValue`). */
|
|
23
|
+
writeCount: number;
|
|
24
|
+
/** Number of delete operations issued to the key-value store client (e.g. `deleteValue`). */
|
|
25
|
+
deleteCount: number;
|
|
26
|
+
/** Number of listing operations issued to the key-value store client (e.g. `listKeys`). */
|
|
27
|
+
listCount: number;
|
|
28
|
+
}
|
|
29
|
+
/** Usage counters for a {@link RequestQueue}. */
|
|
30
|
+
export interface RequestQueueStats {
|
|
31
|
+
/** Number of write operations issued to the request queue client (add / handle / reclaim). */
|
|
32
|
+
writeCount: number;
|
|
33
|
+
/** Number of queue-head reads issued to the request queue client (`fetchNextRequest`). */
|
|
34
|
+
headItemReadCount: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A tiny mutable counter that the storage frontends increment on each client call and expose through
|
|
38
|
+
* a read-only `stats` snapshot. Generic over the concrete counter shape so each storage type gets only
|
|
39
|
+
* the buckets that make sense for it.
|
|
40
|
+
*/
|
|
41
|
+
export declare class StorageStatsTracker<T extends Record<keyof T, number>> {
|
|
42
|
+
#private;
|
|
43
|
+
constructor(initial: T);
|
|
44
|
+
/** Increment a counter bucket by `by` (default `1`). */
|
|
45
|
+
add(key: keyof T, by?: number): void;
|
|
46
|
+
/** Return a snapshot of the current counters. The returned object is a copy and safe to keep. */
|
|
47
|
+
get current(): T;
|
|
48
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend-independent usage counters tracked by the storage frontend classes
|
|
3
|
+
* ({@link Dataset}, {@link KeyValueStore}, {@link RequestQueue}).
|
|
4
|
+
*
|
|
5
|
+
* These count the operations the frontend issues against its underlying storage backend, so they are
|
|
6
|
+
* meaningful for any storage backend (memory, file system, cloud). They are tallied per client call
|
|
7
|
+
* — e.g. iterating a key-value store increments `readCount` once per record fetched and `listCount`
|
|
8
|
+
* once per listed page. Backend-specific figures that the frontend cannot compute (such as the number
|
|
9
|
+
* of bytes stored) are intentionally not included here; read those from the backend's own API instead.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* A tiny mutable counter that the storage frontends increment on each client call and expose through
|
|
13
|
+
* a read-only `stats` snapshot. Generic over the concrete counter shape so each storage type gets only
|
|
14
|
+
* the buckets that make sense for it.
|
|
15
|
+
*/
|
|
16
|
+
export class StorageStatsTracker {
|
|
17
|
+
#counters;
|
|
18
|
+
constructor(initial) {
|
|
19
|
+
this.#counters = { ...initial };
|
|
20
|
+
}
|
|
21
|
+
/** Increment a counter bucket by `by` (default `1`). */
|
|
22
|
+
add(key, by = 1) {
|
|
23
|
+
this.#counters[key] += by;
|
|
24
|
+
}
|
|
25
|
+
/** Return a snapshot of the current counters. The returned object is a copy and safe to keep. */
|
|
26
|
+
get current() {
|
|
27
|
+
return { ...this.#counters };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import type { Dictionary } from '@crawlee/types';
|
|
2
|
+
import type { Configuration } from '../configuration.js';
|
|
3
|
+
import type { Request, Source } from '../request.js';
|
|
4
|
+
import type { IRequestManager, RequestsLike } from './request_manager.js';
|
|
5
|
+
import type { AddRequestsBatchedOptions, AddRequestsBatchedResult, RequestQueueOperationInfo, RequestQueueOperationOptions } from './request_queue.js';
|
|
6
|
+
import type { StorageIdentifier } from './storage_instance_manager.js';
|
|
7
|
+
import type { StorageOpenOptions } from './utils.js';
|
|
8
|
+
/**
|
|
9
|
+
* Opens a request manager, matching the shape of storage `open` methods such as
|
|
10
|
+
* {@link RequestQueue.open|`RequestQueue.open`}.
|
|
11
|
+
*
|
|
12
|
+
* {@link ThrottlingRequestManager} calls this once per configured domain, so every per-domain queue shares the
|
|
13
|
+
* concrete type and storage backend of the manager being wrapped.
|
|
14
|
+
*/
|
|
15
|
+
export type RequestManagerOpener<T extends IRequestManager = IRequestManager> = (identifier: string | StorageIdentifier, options?: StorageOpenOptions) => Promise<T>;
|
|
16
|
+
/**
|
|
17
|
+
* A request manager that can pace requests per domain, as {@link ThrottlingRequestManager} does.
|
|
18
|
+
*
|
|
19
|
+
* The crawlers detect this structurally rather than by type, so a wrapper can opt in by forwarding these three
|
|
20
|
+
* methods without {@link IRequestManager} having to know that throttling exists.
|
|
21
|
+
*/
|
|
22
|
+
export interface SupportsDomainThrottling {
|
|
23
|
+
/** @see {@link ThrottlingRequestManager.recordDomainDelay} */
|
|
24
|
+
recordDomainDelay(url: string, retryAfterMs?: number | null): boolean;
|
|
25
|
+
/** @see {@link ThrottlingRequestManager.setCrawlDelay} */
|
|
26
|
+
setCrawlDelay(url: string, delaySeconds: number): boolean;
|
|
27
|
+
/** @see {@link ThrottlingRequestManager.assertNoStalledDomains} */
|
|
28
|
+
assertNoStalledDomains(): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
/** Whether `manager` can pace requests per domain. */
|
|
31
|
+
export declare function supportsDomainThrottling(manager: unknown): manager is SupportsDomainThrottling;
|
|
32
|
+
/** Options for {@link ThrottlingRequestManager}. */
|
|
33
|
+
export interface ThrottlingRequestManagerOptions<T extends IRequestManager = IRequestManager> {
|
|
34
|
+
/**
|
|
35
|
+
* The request manager to wrap, usually a {@link RequestQueue}. Requests for domains that are not throttled
|
|
36
|
+
* are stored here.
|
|
37
|
+
*/
|
|
38
|
+
inner: T;
|
|
39
|
+
/**
|
|
40
|
+
* Hostnames to throttle. Matching is case-insensitive and exact - wildcards such as `*.example.com` are not
|
|
41
|
+
* supported, so list each subdomain you care about. Requests for any other domain bypass throttling entirely.
|
|
42
|
+
*
|
|
43
|
+
* An internationalized domain may be given in either its unicode or its punycode form, and an IPv6 address
|
|
44
|
+
* has to be bracketed (`[::1]`).
|
|
45
|
+
*/
|
|
46
|
+
domains: string[];
|
|
47
|
+
/**
|
|
48
|
+
* Opens the per-domain queues, one per entry in `domains`, each under the alias `throttled-<domain>`.
|
|
49
|
+
* @default RequestQueue.open
|
|
50
|
+
*/
|
|
51
|
+
requestManagerOpener?: RequestManagerOpener<T>;
|
|
52
|
+
/**
|
|
53
|
+
* The delay applied after a domain's first HTTP 429, doubled on each subsequent one.
|
|
54
|
+
* @default 2
|
|
55
|
+
*/
|
|
56
|
+
baseDelaySecs?: number;
|
|
57
|
+
/**
|
|
58
|
+
* Upper bound on the delay between requests to a rate-limited domain, applied to both the exponential
|
|
59
|
+
* backoff and a `Retry-After` value.
|
|
60
|
+
* @default 60
|
|
61
|
+
*/
|
|
62
|
+
maxDelaySecs?: number;
|
|
63
|
+
/**
|
|
64
|
+
* How long a domain may rate-limit us without a single request getting through before the crawl is
|
|
65
|
+
* abandoned with a {@link PersistentRateLimitError}.
|
|
66
|
+
*
|
|
67
|
+
* A domain that keeps answering 429 for this long is not going to be crawled by waiting longer - the
|
|
68
|
+
* concurrency is too high for it, or it has blocked us outright. Its requests are deliberately left in
|
|
69
|
+
* their queue, so re-running the crawl with `purgeOnStart` disabled picks them up once the domain recovers.
|
|
70
|
+
*
|
|
71
|
+
* A crawler running with `keepAlive` is exempt - outliving a domain that will not let us through is the
|
|
72
|
+
* whole point there.
|
|
73
|
+
* @default 900
|
|
74
|
+
*/
|
|
75
|
+
maxDomainStallSecs?: number;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A request manager that wraps another one and paces requests per domain.
|
|
79
|
+
*
|
|
80
|
+
* Requests for the configured {@link ThrottlingRequestManagerOptions.domains|`domains`} are routed into their own
|
|
81
|
+
* queue when they are added, so each request lives in exactly one place and deduplication keeps working. Everything
|
|
82
|
+
* else goes to the wrapped manager untouched.
|
|
83
|
+
*
|
|
84
|
+
* {@link ThrottlingRequestManager.fetchNextRequest|`fetchNextRequest()`} serves the domain that has been waiting
|
|
85
|
+
* longest and skips any that are backing off, falling back to the wrapped manager. It never blocks: while every
|
|
86
|
+
* remaining request belongs to a throttled domain it returns `null` and {@link ThrottlingRequestManager.isEmpty}
|
|
87
|
+
* reports `true`, so the crawler idles instead of holding a concurrency slot open.
|
|
88
|
+
*
|
|
89
|
+
* Delays come from two places:
|
|
90
|
+
* - HTTP 429 responses, honouring `Retry-After` and otherwise backing off exponentially. The crawlers report these
|
|
91
|
+
* automatically; a request that is throttled is retried later without counting against `maxRequestRetries` and
|
|
92
|
+
* without penalising its session.
|
|
93
|
+
* - robots.txt `Crawl-delay` directives, when `respectRobotsTxtFile` is enabled.
|
|
94
|
+
*
|
|
95
|
+
* This is opt-in: throttling only happens for a domain you list explicitly.
|
|
96
|
+
*
|
|
97
|
+
* **Example usage:**
|
|
98
|
+
*
|
|
99
|
+
* ```ts
|
|
100
|
+
* const crawler = new CheerioCrawler({
|
|
101
|
+
* requestManager: new ThrottlingRequestManager({
|
|
102
|
+
* inner: await RequestQueue.open(),
|
|
103
|
+
* domains: ['api.example.com', 'slow-site.org'],
|
|
104
|
+
* }),
|
|
105
|
+
* requestHandler: async ({ request }) => { ... },
|
|
106
|
+
* });
|
|
107
|
+
* ```
|
|
108
|
+
*
|
|
109
|
+
* @category Sources
|
|
110
|
+
*/
|
|
111
|
+
export declare class ThrottlingRequestManager<T extends IRequestManager = IRequestManager> implements IRequestManager, SupportsDomainThrottling {
|
|
112
|
+
private readonly config;
|
|
113
|
+
private readonly inner;
|
|
114
|
+
private readonly requestManagerOpener;
|
|
115
|
+
private readonly baseDelayMs;
|
|
116
|
+
private readonly maxDelayMs;
|
|
117
|
+
private readonly maxDomainStallMs;
|
|
118
|
+
private readonly domainStates;
|
|
119
|
+
private readonly subManagers;
|
|
120
|
+
private readonly log;
|
|
121
|
+
/**
|
|
122
|
+
* Sub-managers are keyed by a stable alias, so with `purgeOnStart` disabled they outlive the process. They
|
|
123
|
+
* must therefore be reopened for every configured domain rather than created on first insert - otherwise a
|
|
124
|
+
* restart sees an empty map, reports the crawl finished, and strands whatever the previous run left in them.
|
|
125
|
+
*/
|
|
126
|
+
private subManagersReady?;
|
|
127
|
+
/** Batches still being added in the background; keeps {@link ThrottlingRequestManager.isFinished} honest. */
|
|
128
|
+
private inProgressBatchCount;
|
|
129
|
+
private readonly warnedAbout;
|
|
130
|
+
private get hasThrottledDomains();
|
|
131
|
+
constructor(options: ThrottlingRequestManagerOptions<T>, config?: Configuration);
|
|
132
|
+
/** The wrapped manager, holding every request whose domain is not throttled. */
|
|
133
|
+
get innerManager(): T;
|
|
134
|
+
/** Warns once about sources that cannot be routed by domain, because their URLs are not known yet. */
|
|
135
|
+
private warnIfNotRoutable;
|
|
136
|
+
private warnOnce;
|
|
137
|
+
private extractDomain;
|
|
138
|
+
private getDomainState;
|
|
139
|
+
private selectManager;
|
|
140
|
+
/** Only valid once {@link ThrottlingRequestManager.ensureSubManagers} has resolved. */
|
|
141
|
+
private managerForUrl;
|
|
142
|
+
private ensureSubManagers;
|
|
143
|
+
private getSubManagers;
|
|
144
|
+
/** Configured domains that are not currently backing off, longest-overdue first. */
|
|
145
|
+
private fetchableDomains;
|
|
146
|
+
/**
|
|
147
|
+
* Records a 429 response and puts the URL's domain into backoff.
|
|
148
|
+
*
|
|
149
|
+
* @returns `false` if the domain is not configured for throttling, in which case this is a no-op.
|
|
150
|
+
*/
|
|
151
|
+
recordDomainDelay(url: string, retryAfterMs?: number | null): boolean;
|
|
152
|
+
/**
|
|
153
|
+
* Applies a robots.txt `Crawl-delay` to the URL's domain, as a minimum interval between dispatches.
|
|
154
|
+
*
|
|
155
|
+
* The first value wins, so a robots.txt re-fetch cannot change the cadence mid-crawl.
|
|
156
|
+
*
|
|
157
|
+
* @returns `false` if the domain is not configured for throttling, in which case this is a no-op.
|
|
158
|
+
*/
|
|
159
|
+
setCrawlDelay(url: string, delaySeconds: number): boolean;
|
|
160
|
+
/**
|
|
161
|
+
* Throws {@link PersistentRateLimitError} if any domain has been rate-limiting us past
|
|
162
|
+
* {@link ThrottlingRequestManagerOptions.maxDomainStallSecs|`maxDomainStallSecs`} without letting a single
|
|
163
|
+
* request through.
|
|
164
|
+
*
|
|
165
|
+
* A domain qualifies only while it still has queued requests and is actively rate-limiting - a domain that
|
|
166
|
+
* has simply run out of work is finished, not stalled, and one being waited out under a long robots.txt
|
|
167
|
+
* `Crawl-delay` is being obeyed, not stonewalled.
|
|
168
|
+
*/
|
|
169
|
+
assertNoStalledDomains(): Promise<void>;
|
|
170
|
+
/** Records that a domain let a request through, which ends any rate-limit run stall detection was timing. */
|
|
171
|
+
private recordProgress;
|
|
172
|
+
addRequest(requestLike: Source, options?: RequestQueueOperationOptions): Promise<RequestQueueOperationInfo>;
|
|
173
|
+
/**
|
|
174
|
+
* Adds requests in batches, routing each one to the manager that owns its domain.
|
|
175
|
+
*
|
|
176
|
+
* Batching, validation, deduplication and `Retry-After`-free bookkeeping are all delegated to the target
|
|
177
|
+
* managers - this only decides where each request goes, one batch at a time, so a lazy or unbounded input
|
|
178
|
+
* iterable is never fully materialized.
|
|
179
|
+
*/
|
|
180
|
+
addRequestsBatched(requests: RequestsLike, options?: AddRequestsBatchedOptions): Promise<AddRequestsBatchedResult>;
|
|
181
|
+
reclaimRequest(request: Request, options?: RequestQueueOperationOptions): Promise<RequestQueueOperationInfo | null>;
|
|
182
|
+
markRequestAsHandled(request: Request): Promise<RequestQueueOperationInfo | void | null>;
|
|
183
|
+
getTotalCount(): Promise<number>;
|
|
184
|
+
getPendingCount(): Promise<number>;
|
|
185
|
+
getHandledCount(): Promise<number>;
|
|
186
|
+
/**
|
|
187
|
+
* Whether the next {@link ThrottlingRequestManager.fetchNextRequest} would return `null`.
|
|
188
|
+
*
|
|
189
|
+
* Requests waiting on a throttled domain count as unavailable, so a crawler whose task loop is gated on
|
|
190
|
+
* this idles for the backoff instead of spinning on a fetch that cannot succeed yet.
|
|
191
|
+
*/
|
|
192
|
+
isEmpty(): Promise<boolean>;
|
|
193
|
+
/** Unlike {@link ThrottlingRequestManager.isEmpty}, throttled requests still count as outstanding work. */
|
|
194
|
+
isFinished(): Promise<boolean>;
|
|
195
|
+
/**
|
|
196
|
+
* Empties every manager and clears the accumulated backoff. A robots.txt `Crawl-delay` is a property of the
|
|
197
|
+
* site rather than of the run, so it survives.
|
|
198
|
+
*/
|
|
199
|
+
purge(): Promise<void>;
|
|
200
|
+
setExpectedRequestProcessingTimeSecs(secs: number): Promise<void>;
|
|
201
|
+
private forEachManager;
|
|
202
|
+
private sumOverManagers;
|
|
203
|
+
private everyManager;
|
|
204
|
+
/**
|
|
205
|
+
* Returns the next request from a domain that is not backing off, or from the inner manager.
|
|
206
|
+
*
|
|
207
|
+
* Returns `null` while every remaining request belongs to a throttled domain - it never waits the backoff
|
|
208
|
+
* out, because a consumer parked in here holds a concurrency slot, which the autoscaler reads as spare
|
|
209
|
+
* capacity and answers by scaling up. Callers poll instead, and {@link ThrottlingRequestManager.isEmpty}
|
|
210
|
+
* reports `true` meanwhile so the crawler's task loop idles rather than spins.
|
|
211
|
+
*/
|
|
212
|
+
fetchNextRequest<R extends Dictionary = Dictionary>(): Promise<Request<R> | null>;
|
|
213
|
+
[Symbol.asyncIterator](): AsyncGenerator<Request<Dictionary>, void, unknown>;
|
|
214
|
+
persistState(): Promise<void>;
|
|
215
|
+
drop(): Promise<void>;
|
|
216
|
+
}
|