@crawlee/core 4.0.0-beta.211 → 4.0.0-beta.212
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/configuration.d.ts +0 -19
- package/configuration.js +8 -8
- package/events/local_event_manager.d.ts +0 -7
- package/events/local_event_manager.js +8 -8
- package/memory-storage/resource-clients/dataset.d.ts +0 -5
- package/memory-storage/resource-clients/dataset.js +15 -16
- package/memory-storage/resource-clients/key-value-store.d.ts +0 -2
- package/memory-storage/resource-clients/key-value-store.js +10 -11
- package/memory-storage/resource-clients/request-queue.d.ts +0 -22
- package/memory-storage/resource-clients/request-queue.js +33 -34
- package/package.json +6 -6
- package/proxy_configuration.d.ts +0 -7
- package/proxy_configuration.js +8 -8
- package/storages/dataset.d.ts +0 -10
- package/storages/dataset.js +13 -13
- package/storages/key_value_store.d.ts +0 -18
- package/storages/key_value_store.js +16 -16
- package/storages/request_dedup_cache.d.ts +0 -2
- package/storages/request_dedup_cache.js +8 -8
- package/storages/request_list.d.ts +0 -57
- package/storages/request_list.js +38 -38
- package/storages/request_manager_tandem.d.ts +0 -12
- package/storages/request_manager_tandem.js +15 -15
- package/storages/request_queue.d.ts +0 -32
- package/storages/request_queue.js +29 -29
- package/storages/storage_instance_manager.js +4 -4
- package/storages/transaction.d.ts +0 -1
- package/storages/transaction.js +2 -2
package/configuration.d.ts
CHANGED
|
@@ -129,23 +129,4 @@ export declare class Configuration {
|
|
|
129
129
|
* Delegates to the global ServiceLocator, making it the single source of truth for service management.
|
|
130
130
|
*/
|
|
131
131
|
static getGlobalConfiguration(): Configuration;
|
|
132
|
-
/**
|
|
133
|
-
* Resolves all field values once using the priority chain:
|
|
134
|
-
* constructor options > env vars > crawlee.json > schema defaults.
|
|
135
|
-
*/
|
|
136
|
-
private static resolveAll;
|
|
137
|
-
/**
|
|
138
|
-
* Registers getters (and throwing setters) on the instance for each field.
|
|
139
|
-
*/
|
|
140
|
-
private registerAccessors;
|
|
141
|
-
/**
|
|
142
|
-
* Reads the first defined env var value for a field definition.
|
|
143
|
-
* Empty strings are treated as unset, falling through to crawlee.json or schema defaults.
|
|
144
|
-
* (Crawlee v3 coerced `''` to `false`/`0`/`''` per type — v4 drops that for consistency.)
|
|
145
|
-
*/
|
|
146
|
-
private static readEnvVar;
|
|
147
|
-
/**
|
|
148
|
-
* Loads config options from crawlee.json in the current working directory.
|
|
149
|
-
*/
|
|
150
|
-
private static loadFileOptions;
|
|
151
132
|
}
|
package/configuration.js
CHANGED
|
@@ -143,9 +143,9 @@ export class Configuration {
|
|
|
143
143
|
*/
|
|
144
144
|
constructor(options = {}) {
|
|
145
145
|
const fields = this.constructor.fields;
|
|
146
|
-
const fileOptions = Configuration
|
|
147
|
-
this.#resolvedValues = Configuration
|
|
148
|
-
this
|
|
146
|
+
const fileOptions = Configuration.#loadFileOptions();
|
|
147
|
+
this.#resolvedValues = Configuration.#resolveAll(fields, options, fileOptions);
|
|
148
|
+
this.#registerAccessors();
|
|
149
149
|
// Set the log level
|
|
150
150
|
const logLevel = this.logLevel;
|
|
151
151
|
if (logLevel != null) {
|
|
@@ -164,7 +164,7 @@ export class Configuration {
|
|
|
164
164
|
* Resolves all field values once using the priority chain:
|
|
165
165
|
* constructor options > env vars > crawlee.json > schema defaults.
|
|
166
166
|
*/
|
|
167
|
-
static resolveAll(fields, userOptions, fileOptions) {
|
|
167
|
+
static #resolveAll(fields, userOptions, fileOptions) {
|
|
168
168
|
const values = {};
|
|
169
169
|
for (const [key, fieldDef] of Object.entries(fields)) {
|
|
170
170
|
// 1. Constructor options (highest priority)
|
|
@@ -173,7 +173,7 @@ export class Configuration {
|
|
|
173
173
|
continue;
|
|
174
174
|
}
|
|
175
175
|
// 2. Environment variables
|
|
176
|
-
const envValue = Configuration
|
|
176
|
+
const envValue = Configuration.#readEnvVar(fieldDef);
|
|
177
177
|
if (envValue != null) {
|
|
178
178
|
values[key] = fieldDef.schema.parse(envValue);
|
|
179
179
|
continue;
|
|
@@ -192,7 +192,7 @@ export class Configuration {
|
|
|
192
192
|
/**
|
|
193
193
|
* Registers getters (and throwing setters) on the instance for each field.
|
|
194
194
|
*/
|
|
195
|
-
registerAccessors() {
|
|
195
|
+
#registerAccessors() {
|
|
196
196
|
const fields = this.constructor.fields;
|
|
197
197
|
const descriptors = {};
|
|
198
198
|
for (const key of Object.keys(fields)) {
|
|
@@ -212,7 +212,7 @@ export class Configuration {
|
|
|
212
212
|
* Empty strings are treated as unset, falling through to crawlee.json or schema defaults.
|
|
213
213
|
* (Crawlee v3 coerced `''` to `false`/`0`/`''` per type — v4 drops that for consistency.)
|
|
214
214
|
*/
|
|
215
|
-
static readEnvVar(fieldDef) {
|
|
215
|
+
static #readEnvVar(fieldDef) {
|
|
216
216
|
if (!fieldDef.envVar)
|
|
217
217
|
return undefined;
|
|
218
218
|
const envVars = Array.isArray(fieldDef.envVar) ? fieldDef.envVar : [fieldDef.envVar];
|
|
@@ -226,7 +226,7 @@ export class Configuration {
|
|
|
226
226
|
/**
|
|
227
227
|
* Loads config options from crawlee.json in the current working directory.
|
|
228
228
|
*/
|
|
229
|
-
static loadFileOptions() {
|
|
229
|
+
static #loadFileOptions() {
|
|
230
230
|
try {
|
|
231
231
|
const file = readFileSync(join(process.cwd(), 'crawlee.json'));
|
|
232
232
|
return JSON.parse(file.toString());
|
|
@@ -29,11 +29,4 @@ export declare class LocalEventManager extends EventManager {
|
|
|
29
29
|
* @internal
|
|
30
30
|
*/
|
|
31
31
|
isContainerizedWrapper(): Promise<boolean>;
|
|
32
|
-
/**
|
|
33
|
-
* Creates a SystemInfo object based on local metrics.
|
|
34
|
-
*/
|
|
35
|
-
private createSystemInfo;
|
|
36
|
-
private createCpuInfo;
|
|
37
|
-
private createMemoryInfo;
|
|
38
|
-
private getMemoryInfo;
|
|
39
32
|
}
|
|
@@ -44,7 +44,7 @@ export class LocalEventManager extends EventManager {
|
|
|
44
44
|
* @internal
|
|
45
45
|
*/
|
|
46
46
|
async emitSystemInfoEvent(intervalCallback) {
|
|
47
|
-
const info = await this
|
|
47
|
+
const info = await this.#createSystemInfo({
|
|
48
48
|
maxUsedCpuRatio: serviceLocator.getConfiguration().maxUsedCpuRatio,
|
|
49
49
|
});
|
|
50
50
|
this.events.emit(EventType.SYSTEM_INFO, info);
|
|
@@ -60,14 +60,14 @@ export class LocalEventManager extends EventManager {
|
|
|
60
60
|
/**
|
|
61
61
|
* Creates a SystemInfo object based on local metrics.
|
|
62
62
|
*/
|
|
63
|
-
async createSystemInfo(options) {
|
|
63
|
+
async #createSystemInfo(options) {
|
|
64
64
|
return {
|
|
65
65
|
createdAt: new Date(),
|
|
66
|
-
...(await this
|
|
67
|
-
...(await this
|
|
66
|
+
...(await this.#createCpuInfo(options)),
|
|
67
|
+
...(await this.#createMemoryInfo()),
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
|
-
async createCpuInfo(options) {
|
|
70
|
+
async #createCpuInfo(options) {
|
|
71
71
|
const { getCurrentCpuTicksV2 } = await import('../system-info/cpu-info.js');
|
|
72
72
|
const usedCpuRatio = await getCurrentCpuTicksV2({
|
|
73
73
|
containerized: await this.isContainerizedWrapper(),
|
|
@@ -78,9 +78,9 @@ export class LocalEventManager extends EventManager {
|
|
|
78
78
|
isCpuOverloaded: usedCpuRatio > options.maxUsedCpuRatio,
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
|
-
async createMemoryInfo() {
|
|
81
|
+
async #createMemoryInfo() {
|
|
82
82
|
try {
|
|
83
|
-
const memInfo = await this
|
|
83
|
+
const memInfo = await this.#getMemoryInfo();
|
|
84
84
|
return {
|
|
85
85
|
memTotalBytes: memInfo.totalBytes,
|
|
86
86
|
memCurrentBytes: memInfo.mainProcessBytes + memInfo.childProcessesBytes,
|
|
@@ -91,7 +91,7 @@ export class LocalEventManager extends EventManager {
|
|
|
91
91
|
return {};
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
-
async getMemoryInfo() {
|
|
94
|
+
async #getMemoryInfo() {
|
|
95
95
|
const { getMemoryInfo } = await import('../system-info/memory-info.js');
|
|
96
96
|
return getMemoryInfo({
|
|
97
97
|
containerized: await this.isContainerizedWrapper(),
|
|
@@ -25,16 +25,11 @@ export declare class DatasetBackend<Data extends Dictionary = Dictionary> extend
|
|
|
25
25
|
accessedAt: Date;
|
|
26
26
|
modifiedAt: Date;
|
|
27
27
|
itemCount: number;
|
|
28
|
-
private readonly storageBackend;
|
|
29
28
|
constructor(options: DatasetBackendOptions);
|
|
30
29
|
getMetadata(): Promise<storage.DatasetInfo>;
|
|
31
30
|
drop(): Promise<void>;
|
|
32
31
|
purge(): Promise<void>;
|
|
33
32
|
getData(options?: storage.DatasetBackendListOptions): Promise<storage.PaginatedList<Data>>;
|
|
34
|
-
private getDataPage;
|
|
35
33
|
pushData(items: Data[]): Promise<void>;
|
|
36
34
|
toDatasetInfo(): storage.DatasetInfo;
|
|
37
|
-
private generateLocalEntryName;
|
|
38
|
-
private getStartAndEndIndexes;
|
|
39
|
-
private updateTimestamps;
|
|
40
35
|
}
|
|
@@ -23,20 +23,19 @@ export class DatasetBackend extends BaseClient {
|
|
|
23
23
|
modifiedAt = new Date();
|
|
24
24
|
itemCount = 0;
|
|
25
25
|
#datasetEntries = new Map();
|
|
26
|
-
|
|
27
|
-
storageBackend;
|
|
26
|
+
#storageBackend;
|
|
28
27
|
constructor(options) {
|
|
29
28
|
super(options.id ?? randomUUID());
|
|
30
29
|
this.name = options.name;
|
|
31
30
|
this.cacheKey = options.cacheKey ?? this.name ?? this.id;
|
|
32
|
-
this
|
|
31
|
+
this.#storageBackend = options.storageBackend;
|
|
33
32
|
}
|
|
34
33
|
async getMetadata() {
|
|
35
|
-
this
|
|
34
|
+
this.#updateTimestamps(false);
|
|
36
35
|
return this.toDatasetInfo();
|
|
37
36
|
}
|
|
38
37
|
async drop() {
|
|
39
|
-
if (this
|
|
38
|
+
if (this.#storageBackend.evictBackend('Dataset', this.id)) {
|
|
40
39
|
this.itemCount = 0;
|
|
41
40
|
this.#datasetEntries.clear();
|
|
42
41
|
}
|
|
@@ -44,25 +43,25 @@ export class DatasetBackend extends BaseClient {
|
|
|
44
43
|
async purge() {
|
|
45
44
|
this.itemCount = 0;
|
|
46
45
|
this.#datasetEntries.clear();
|
|
47
|
-
this
|
|
46
|
+
this.#updateTimestamps(true);
|
|
48
47
|
}
|
|
49
48
|
getData(options = {}) {
|
|
50
49
|
const { desc, limit, offset } = parseArgument(options, schemas.datasetListItemsOptions);
|
|
51
|
-
return this
|
|
50
|
+
return this.#getDataPage({
|
|
52
51
|
desc,
|
|
53
52
|
offset: offset ?? 0,
|
|
54
53
|
limit: Math.min(limit ?? LIST_ITEMS_LIMIT, LIST_ITEMS_LIMIT),
|
|
55
54
|
});
|
|
56
55
|
}
|
|
57
|
-
async getDataPage(options = {}) {
|
|
56
|
+
async #getDataPage(options = {}) {
|
|
58
57
|
const { limit = LIST_ITEMS_LIMIT, offset = 0, desc } = options;
|
|
59
|
-
const [start, end] = this
|
|
58
|
+
const [start, end] = this.#getStartAndEndIndexes(desc ? Math.max(this.itemCount - offset - limit, 0) : offset, limit);
|
|
60
59
|
const items = [];
|
|
61
60
|
for (let idx = start; idx < end; idx++) {
|
|
62
|
-
const entryNumber = this
|
|
61
|
+
const entryNumber = this.#generateLocalEntryName(idx);
|
|
63
62
|
items.push(this.#datasetEntries.get(entryNumber));
|
|
64
63
|
}
|
|
65
|
-
this
|
|
64
|
+
this.#updateTimestamps(false);
|
|
66
65
|
return {
|
|
67
66
|
count: items.length,
|
|
68
67
|
desc: desc ?? false,
|
|
@@ -74,10 +73,10 @@ export class DatasetBackend extends BaseClient {
|
|
|
74
73
|
}
|
|
75
74
|
async pushData(items) {
|
|
76
75
|
for (const entry of items) {
|
|
77
|
-
const idx = this
|
|
76
|
+
const idx = this.#generateLocalEntryName(++this.itemCount);
|
|
78
77
|
this.#datasetEntries.set(idx, JSON.parse(JSON.stringify(entry)));
|
|
79
78
|
}
|
|
80
|
-
this
|
|
79
|
+
this.#updateTimestamps(true);
|
|
81
80
|
}
|
|
82
81
|
toDatasetInfo() {
|
|
83
82
|
return {
|
|
@@ -89,15 +88,15 @@ export class DatasetBackend extends BaseClient {
|
|
|
89
88
|
name: this.name,
|
|
90
89
|
};
|
|
91
90
|
}
|
|
92
|
-
generateLocalEntryName(idx) {
|
|
91
|
+
#generateLocalEntryName(idx) {
|
|
93
92
|
return idx.toString().padStart(LOCAL_ENTRY_NAME_DIGITS, '0');
|
|
94
93
|
}
|
|
95
|
-
getStartAndEndIndexes(offset, limit = this.itemCount) {
|
|
94
|
+
#getStartAndEndIndexes(offset, limit = this.itemCount) {
|
|
96
95
|
const start = offset + 1;
|
|
97
96
|
const end = Math.min(offset + limit, this.itemCount) + 1;
|
|
98
97
|
return [start, end];
|
|
99
98
|
}
|
|
100
|
-
updateTimestamps(hasBeenModified) {
|
|
99
|
+
#updateTimestamps(hasBeenModified) {
|
|
101
100
|
this.accessedAt = new Date();
|
|
102
101
|
if (hasBeenModified) {
|
|
103
102
|
this.modifiedAt = new Date();
|
|
@@ -29,7 +29,6 @@ export declare class KeyValueStoreBackend extends BaseClient implements storage.
|
|
|
29
29
|
createdAt: Date;
|
|
30
30
|
accessedAt: Date;
|
|
31
31
|
modifiedAt: Date;
|
|
32
|
-
private readonly storageBackend;
|
|
33
32
|
constructor(options: KeyValueStoreBackendOptions);
|
|
34
33
|
getMetadata(): Promise<storage.KeyValueStoreInfo>;
|
|
35
34
|
drop(): Promise<void>;
|
|
@@ -52,5 +51,4 @@ export declare class KeyValueStoreBackend extends BaseClient implements storage.
|
|
|
52
51
|
setValue(record: storage.KeyValueStoreInputRecord): Promise<void>;
|
|
53
52
|
deleteValue(key: string): Promise<void>;
|
|
54
53
|
toKeyValueStoreInfo(): storage.KeyValueStoreInfo;
|
|
55
|
-
private updateTimestamps;
|
|
56
54
|
}
|
|
@@ -31,26 +31,25 @@ export class KeyValueStoreBackend extends BaseClient {
|
|
|
31
31
|
accessedAt = new Date();
|
|
32
32
|
modifiedAt = new Date();
|
|
33
33
|
#keyValueEntries = new Map();
|
|
34
|
-
|
|
35
|
-
storageBackend;
|
|
34
|
+
#storageBackend;
|
|
36
35
|
constructor(options) {
|
|
37
36
|
super(options.id ?? randomUUID());
|
|
38
37
|
this.name = options.name;
|
|
39
38
|
this.cacheKey = options.cacheKey ?? this.name ?? this.id;
|
|
40
|
-
this
|
|
39
|
+
this.#storageBackend = options.storageBackend;
|
|
41
40
|
}
|
|
42
41
|
async getMetadata() {
|
|
43
|
-
this
|
|
42
|
+
this.#updateTimestamps(false);
|
|
44
43
|
return this.toKeyValueStoreInfo();
|
|
45
44
|
}
|
|
46
45
|
async drop() {
|
|
47
|
-
if (this
|
|
46
|
+
if (this.#storageBackend.evictBackend('KeyValueStore', this.id)) {
|
|
48
47
|
this.#keyValueEntries.clear();
|
|
49
48
|
}
|
|
50
49
|
}
|
|
51
50
|
async purge() {
|
|
52
51
|
this.#keyValueEntries.clear();
|
|
53
|
-
this
|
|
52
|
+
this.#updateTimestamps(true);
|
|
54
53
|
}
|
|
55
54
|
async listKeys(options = {}) {
|
|
56
55
|
const { prefix, exclusiveStartKey, limit } = parseArgument(options, schemas.keyValueStoreListKeysOptions);
|
|
@@ -77,7 +76,7 @@ export class KeyValueStoreBackend extends BaseClient {
|
|
|
77
76
|
const isTruncated = limit !== undefined && filteredItems.length > limit;
|
|
78
77
|
const pageItems = isTruncated ? filteredItems.slice(0, limit) : filteredItems;
|
|
79
78
|
const nextExclusiveStartKey = isTruncated ? pageItems[pageItems.length - 1].key : undefined;
|
|
80
|
-
this
|
|
79
|
+
this.#updateTimestamps(false);
|
|
81
80
|
return {
|
|
82
81
|
items: pageItems,
|
|
83
82
|
count: pageItems.length,
|
|
@@ -121,7 +120,7 @@ export class KeyValueStoreBackend extends BaseClient {
|
|
|
121
120
|
// frontend treats it as "no content type" rather than a bogus value.
|
|
122
121
|
contentType: entry.contentType ?? (mime.contentType(entry.extension) || undefined),
|
|
123
122
|
};
|
|
124
|
-
this
|
|
123
|
+
this.#updateTimestamps(false);
|
|
125
124
|
return record;
|
|
126
125
|
}
|
|
127
126
|
async setValue(record) {
|
|
@@ -153,13 +152,13 @@ export class KeyValueStoreBackend extends BaseClient {
|
|
|
153
152
|
contentType,
|
|
154
153
|
};
|
|
155
154
|
this.#keyValueEntries.set(key, normalizedRecord);
|
|
156
|
-
this
|
|
155
|
+
this.#updateTimestamps(true);
|
|
157
156
|
}
|
|
158
157
|
async deleteValue(key) {
|
|
159
158
|
parseArgument(key, keySchema);
|
|
160
159
|
if (this.#keyValueEntries.has(key)) {
|
|
161
160
|
this.#keyValueEntries.delete(key);
|
|
162
|
-
this
|
|
161
|
+
this.#updateTimestamps(true);
|
|
163
162
|
}
|
|
164
163
|
}
|
|
165
164
|
toKeyValueStoreInfo() {
|
|
@@ -171,7 +170,7 @@ export class KeyValueStoreBackend extends BaseClient {
|
|
|
171
170
|
modifiedAt: this.modifiedAt,
|
|
172
171
|
};
|
|
173
172
|
}
|
|
174
|
-
updateTimestamps(hasBeenModified) {
|
|
173
|
+
#updateTimestamps(hasBeenModified) {
|
|
175
174
|
this.accessedAt = new Date();
|
|
176
175
|
if (hasBeenModified) {
|
|
177
176
|
this.modifiedAt = new Date();
|
|
@@ -34,28 +34,10 @@ export declare class RequestQueueBackend extends BaseClient implements storage.R
|
|
|
34
34
|
modifiedAt: Date;
|
|
35
35
|
handledRequestCount: number;
|
|
36
36
|
pendingRequestCount: number;
|
|
37
|
-
private readonly storageBackend;
|
|
38
37
|
constructor(options: RequestQueueBackendOptions);
|
|
39
38
|
getMetadata(): Promise<storage.RequestQueueInfo>;
|
|
40
39
|
drop(): Promise<void>;
|
|
41
40
|
purge(): Promise<void>;
|
|
42
|
-
private requestKeyIterator;
|
|
43
|
-
/**
|
|
44
|
-
* Scans the queue and returns the pending head — requests that are neither handled nor currently
|
|
45
|
-
* in progress — ordered by `orderNo`, deduplicated.
|
|
46
|
-
*
|
|
47
|
-
* When `detectInProgressRequests` is set, the result also carries an `hasInProgressRequests` flag
|
|
48
|
-
* telling whether any unhandled-but-in-progress request was skipped along the way. It lets
|
|
49
|
-
* {@link isFinished} distinguish "no work left at all" from "work remains, but it is currently being
|
|
50
|
-
* processed". Without it, a consumer with concurrency could consider the queue finished and shut the
|
|
51
|
-
* crawler down while it is still handling the last requests.
|
|
52
|
-
*
|
|
53
|
-
* Computing the flag is expensive: because an in-progress request may sit anywhere in the queue, it
|
|
54
|
-
* forces a scan of every pending entry even when only `limit` items are wanted. Callers that only
|
|
55
|
-
* need the head (e.g. {@link fetchNextRequest}, {@link isEmpty}) leave it off so the scan can stop as
|
|
56
|
-
* soon as the page is filled, keeping those calls O(head) instead of O(pending).
|
|
57
|
-
*/
|
|
58
|
-
private listPendingHead;
|
|
59
41
|
fetchNextRequest(): Promise<storage.UpdateRequestSchema | undefined>;
|
|
60
42
|
addBatchOfRequests(requests: storage.RequestSchema[], options?: storage.RequestQueueOperationOptions): Promise<storage.BatchAddRequestsResult>;
|
|
61
43
|
getRequest(uniqueKey: string): Promise<storage.UpdateRequestSchema | undefined>;
|
|
@@ -70,8 +52,4 @@ export declare class RequestQueueBackend extends BaseClient implements storage.R
|
|
|
70
52
|
*/
|
|
71
53
|
listItems(): Promise<storage.UpdateRequestSchema[]>;
|
|
72
54
|
toRequestQueueInfo(): storage.RequestQueueInfo;
|
|
73
|
-
private updateTimestamps;
|
|
74
|
-
private jsonToRequest;
|
|
75
|
-
private createInternalRequest;
|
|
76
|
-
private calculateOrderNo;
|
|
77
55
|
}
|
|
@@ -43,16 +43,15 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
43
43
|
* ever walk the unhandled tail instead of every request the queue has ever seen.
|
|
44
44
|
*/
|
|
45
45
|
#pendingRequestIds = new Set();
|
|
46
|
-
|
|
47
|
-
storageBackend;
|
|
46
|
+
#storageBackend;
|
|
48
47
|
constructor(options) {
|
|
49
48
|
super(options.id ?? randomUUID());
|
|
50
49
|
this.name = options.name;
|
|
51
50
|
this.cacheKey = options.cacheKey ?? this.name ?? this.id;
|
|
52
|
-
this
|
|
51
|
+
this.#storageBackend = options.storageBackend;
|
|
53
52
|
}
|
|
54
53
|
async getMetadata() {
|
|
55
|
-
this
|
|
54
|
+
this.#updateTimestamps(false);
|
|
56
55
|
return this.toRequestQueueInfo();
|
|
57
56
|
}
|
|
58
57
|
async drop() {
|
|
@@ -61,7 +60,7 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
61
60
|
// removed, which `listPendingHead` would then dereference as `undefined`.
|
|
62
61
|
await this.#queueStateMutex.wait();
|
|
63
62
|
try {
|
|
64
|
-
if (this
|
|
63
|
+
if (this.#storageBackend.evictBackend('RequestQueue', this.id)) {
|
|
65
64
|
this.pendingRequestCount = 0;
|
|
66
65
|
// Clear all in-memory state, consistent with `purge`. Clearing `requests` alone would
|
|
67
66
|
// leave dangling ids in `forefrontRequestIds`/`inProgressRequestIds`, which a later head
|
|
@@ -88,13 +87,13 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
88
87
|
this.#inProgressRequestIds.clear();
|
|
89
88
|
this.handledRequestCount = 0;
|
|
90
89
|
this.pendingRequestCount = 0;
|
|
91
|
-
this
|
|
90
|
+
this.#updateTimestamps(true);
|
|
92
91
|
}
|
|
93
92
|
finally {
|
|
94
93
|
this.#queueStateMutex.shift();
|
|
95
94
|
}
|
|
96
95
|
}
|
|
97
|
-
|
|
96
|
+
*#requestKeyIterator() {
|
|
98
97
|
for (let i = this.#forefrontRequestIds.length - 1; i >= 0; i--) {
|
|
99
98
|
yield this.#forefrontRequestIds[i];
|
|
100
99
|
}
|
|
@@ -115,14 +114,14 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
115
114
|
* need the head (e.g. {@link fetchNextRequest}, {@link isEmpty}) leave it off so the scan can stop as
|
|
116
115
|
* soon as the page is filled, keeping those calls O(head) instead of O(pending).
|
|
117
116
|
*/
|
|
118
|
-
async listPendingHead(limit, detectInProgressRequests = false) {
|
|
117
|
+
async #listPendingHead(limit, detectInProgressRequests = false) {
|
|
119
118
|
const items = [];
|
|
120
119
|
let hasInProgressRequests = false;
|
|
121
120
|
// Tracks processed request IDs to avoid duplicates (request in both `forefrontRequestIds` and `requests`).
|
|
122
121
|
const seenRequestIds = new Set();
|
|
123
122
|
// Tracks handled request IDs from `forefrontRequestIds` to be removed.
|
|
124
123
|
const handledForefrontIds = new Set();
|
|
125
|
-
for (const requestId of this
|
|
124
|
+
for (const requestId of this.#requestKeyIterator()) {
|
|
126
125
|
// Once the requested page is filled we can stop — unless the caller asked us to detect
|
|
127
126
|
// in-progress requests and we have not yet seen one, in which case we must keep scanning.
|
|
128
127
|
if (items.length >= limit && (!detectInProgressRequests || hasInProgressRequests)) {
|
|
@@ -156,17 +155,17 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
156
155
|
};
|
|
157
156
|
}
|
|
158
157
|
async fetchNextRequest() {
|
|
159
|
-
this
|
|
158
|
+
this.#updateTimestamps(false);
|
|
160
159
|
await this.#queueStateMutex.wait();
|
|
161
160
|
try {
|
|
162
|
-
const { items: [head], } = await this
|
|
161
|
+
const { items: [head], } = await this.#listPendingHead(1);
|
|
163
162
|
if (!head) {
|
|
164
163
|
return undefined;
|
|
165
164
|
}
|
|
166
165
|
// Mark the request as in progress so it is not handed out again until it is handled or
|
|
167
166
|
// reclaimed. The request keeps its `orderNo` (and thus its forefront / normal ordering).
|
|
168
167
|
this.#inProgressRequestIds.add(head.id);
|
|
169
|
-
return this
|
|
168
|
+
return this.#jsonToRequest(head.json) ?? undefined;
|
|
170
169
|
}
|
|
171
170
|
finally {
|
|
172
171
|
this.#queueStateMutex.shift();
|
|
@@ -185,7 +184,7 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
185
184
|
unprocessedRequests: [],
|
|
186
185
|
};
|
|
187
186
|
for (const model of requests) {
|
|
188
|
-
const requestModel = this
|
|
187
|
+
const requestModel = this.#createInternalRequest(model, options.forefront);
|
|
189
188
|
const existingRequestWithId = this.#requests.get(requestModel.id);
|
|
190
189
|
if (existingRequestWithId) {
|
|
191
190
|
result.processedRequests.push({
|
|
@@ -216,7 +215,7 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
216
215
|
wasAlreadyPresent: false,
|
|
217
216
|
});
|
|
218
217
|
}
|
|
219
|
-
this
|
|
218
|
+
this.#updateTimestamps(true);
|
|
220
219
|
return result;
|
|
221
220
|
}
|
|
222
221
|
finally {
|
|
@@ -225,14 +224,14 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
225
224
|
}
|
|
226
225
|
async getRequest(uniqueKey) {
|
|
227
226
|
parseArgument(uniqueKey, uniqueKeySchema);
|
|
228
|
-
this
|
|
227
|
+
this.#updateTimestamps(false);
|
|
229
228
|
const id = uniqueKeyToRequestId(uniqueKey);
|
|
230
229
|
const json = this.#requests.get(id)?.json;
|
|
231
|
-
return this
|
|
230
|
+
return this.#jsonToRequest(json);
|
|
232
231
|
}
|
|
233
232
|
async markRequestAsHandled(request) {
|
|
234
233
|
parseArgument(request, schemas.storageRequest);
|
|
235
|
-
this
|
|
234
|
+
this.#updateTimestamps(false);
|
|
236
235
|
// Serialize against other mutators (and the head scans in `isEmpty`/`isFinished`) so the shared
|
|
237
236
|
// `requests` map, `inProgressRequestIds` set and request counts stay consistent across the
|
|
238
237
|
// `await` points below.
|
|
@@ -249,7 +248,7 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
249
248
|
// A handled request has `orderNo === null`. Marking it again is an idempotent no-op.
|
|
250
249
|
const wasAlreadyHandled = existingRequest.orderNo === null;
|
|
251
250
|
const handledAt = request.handledAt ?? new Date().toISOString();
|
|
252
|
-
const requestModel = this
|
|
251
|
+
const requestModel = this.#createInternalRequest({ ...request, handledAt }, false);
|
|
253
252
|
this.#requests.set(id, requestModel);
|
|
254
253
|
this.#pendingRequestIds.delete(id);
|
|
255
254
|
// The request is no longer in progress for this client.
|
|
@@ -258,7 +257,7 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
258
257
|
this.pendingRequestCount -= 1;
|
|
259
258
|
this.handledRequestCount += 1;
|
|
260
259
|
}
|
|
261
|
-
this
|
|
260
|
+
this.#updateTimestamps(true);
|
|
262
261
|
return {
|
|
263
262
|
requestId: id,
|
|
264
263
|
wasAlreadyHandled,
|
|
@@ -272,7 +271,7 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
272
271
|
async reclaimRequest(request, options = {}) {
|
|
273
272
|
parseArgument(request, schemas.storageRequest);
|
|
274
273
|
parseArgument(options, schemas.requestQueueOperationOptions);
|
|
275
|
-
this
|
|
274
|
+
this.#updateTimestamps(false);
|
|
276
275
|
// Serialize against other mutators (and the head scans in `isEmpty`/`isFinished`) so the shared
|
|
277
276
|
// `requests` map, `forefrontRequestIds` array and `inProgressRequestIds` set stay consistent
|
|
278
277
|
// across the `await` points below.
|
|
@@ -289,14 +288,14 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
289
288
|
}
|
|
290
289
|
// Reclaiming resets the `orderNo` to a fresh timestamp, restoring the request to the queue
|
|
291
290
|
// (at the front if `forefront`).
|
|
292
|
-
const requestModel = this
|
|
291
|
+
const requestModel = this.#createInternalRequest(request, options.forefront);
|
|
293
292
|
this.#requests.set(id, requestModel);
|
|
294
293
|
// The request is no longer in progress for this client.
|
|
295
294
|
this.#inProgressRequestIds.delete(id);
|
|
296
295
|
if (options.forefront) {
|
|
297
296
|
this.#forefrontRequestIds.push(id);
|
|
298
297
|
}
|
|
299
|
-
this
|
|
298
|
+
this.#updateTimestamps(true);
|
|
300
299
|
return {
|
|
301
300
|
requestId: id,
|
|
302
301
|
wasAlreadyHandled: false,
|
|
@@ -308,7 +307,7 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
308
307
|
}
|
|
309
308
|
}
|
|
310
309
|
async isEmpty() {
|
|
311
|
-
this
|
|
310
|
+
this.#updateTimestamps(false);
|
|
312
311
|
// "Empty" means there is nothing left to fetch right now — i.e. the next `fetchNextRequest`
|
|
313
312
|
// would return `null`. Requests that are currently in progress are intentionally NOT counted
|
|
314
313
|
// here: they are not fetchable, so the queue is empty from a consumer's point of view. Whether
|
|
@@ -319,7 +318,7 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
319
318
|
// racing a concurrent mutator (e.g. `addBatchOfRequests`) at its `await` points.
|
|
320
319
|
await this.#queueStateMutex.wait();
|
|
321
320
|
try {
|
|
322
|
-
const { items } = await this
|
|
321
|
+
const { items } = await this.#listPendingHead(1);
|
|
323
322
|
return items.length === 0;
|
|
324
323
|
}
|
|
325
324
|
finally {
|
|
@@ -327,7 +326,7 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
327
326
|
}
|
|
328
327
|
}
|
|
329
328
|
async isFinished() {
|
|
330
|
-
this
|
|
329
|
+
this.#updateTimestamps(false);
|
|
331
330
|
// The queue is finished only when there is nothing left to fetch AND nothing currently in
|
|
332
331
|
// progress. Counting in-progress requests is what allows a crawler with concurrency to keep
|
|
333
332
|
// waiting while it still holds the last requests, instead of finishing prematurely.
|
|
@@ -339,7 +338,7 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
339
338
|
// racing a concurrent mutator (e.g. `addBatchOfRequests`) at its `await` points.
|
|
340
339
|
await this.#queueStateMutex.wait();
|
|
341
340
|
try {
|
|
342
|
-
const { items, hasInProgressRequests } = await this
|
|
341
|
+
const { items, hasInProgressRequests } = await this.#listPendingHead(1, true);
|
|
343
342
|
return items.length === 0 && !hasInProgressRequests;
|
|
344
343
|
}
|
|
345
344
|
finally {
|
|
@@ -352,13 +351,13 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
352
351
|
* nothing is marked in progress.
|
|
353
352
|
*/
|
|
354
353
|
async listItems() {
|
|
355
|
-
this
|
|
354
|
+
this.#updateTimestamps(false);
|
|
356
355
|
// `listPendingHead` prunes `forefrontRequestIds` as it scans, so we must hold the queue-state
|
|
357
356
|
// mutex to avoid racing a concurrent mutator at its `await` points.
|
|
358
357
|
await this.#queueStateMutex.wait();
|
|
359
358
|
try {
|
|
360
|
-
const { items } = await this
|
|
361
|
-
return items.map((request) => this
|
|
359
|
+
const { items } = await this.#listPendingHead(Number.POSITIVE_INFINITY);
|
|
360
|
+
return items.map((request) => this.#jsonToRequest(request.json));
|
|
362
361
|
}
|
|
363
362
|
finally {
|
|
364
363
|
this.#queueStateMutex.shift();
|
|
@@ -376,20 +375,20 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
376
375
|
totalRequestCount: this.#requests.size,
|
|
377
376
|
};
|
|
378
377
|
}
|
|
379
|
-
updateTimestamps(hasBeenModified) {
|
|
378
|
+
#updateTimestamps(hasBeenModified) {
|
|
380
379
|
this.accessedAt = new Date();
|
|
381
380
|
if (hasBeenModified) {
|
|
382
381
|
this.modifiedAt = new Date();
|
|
383
382
|
}
|
|
384
383
|
}
|
|
385
|
-
jsonToRequest(requestJson) {
|
|
384
|
+
#jsonToRequest(requestJson) {
|
|
386
385
|
if (!requestJson)
|
|
387
386
|
return undefined;
|
|
388
387
|
const request = JSON.parse(requestJson);
|
|
389
388
|
return purgeNullsFromObject(request);
|
|
390
389
|
}
|
|
391
|
-
createInternalRequest(request, forefront) {
|
|
392
|
-
const orderNo = this
|
|
390
|
+
#createInternalRequest(request, forefront) {
|
|
391
|
+
const orderNo = this.#calculateOrderNo(request, forefront);
|
|
393
392
|
const id = uniqueKeyToRequestId(request.uniqueKey);
|
|
394
393
|
if (request.id && request.id !== id) {
|
|
395
394
|
throw new Error('Request ID does not match its uniqueKey.');
|
|
@@ -405,7 +404,7 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
405
404
|
url: request.url,
|
|
406
405
|
};
|
|
407
406
|
}
|
|
408
|
-
calculateOrderNo(request, forefront) {
|
|
407
|
+
#calculateOrderNo(request, forefront) {
|
|
409
408
|
if (request.handledAt)
|
|
410
409
|
return null;
|
|
411
410
|
const timestamp = Date.now();
|
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.212",
|
|
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,10 +53,10 @@
|
|
|
53
53
|
"@apify/log": "^3.0.1",
|
|
54
54
|
"@apify/timeout": "^1.0.1",
|
|
55
55
|
"@apify/utilities": "^3.0.1",
|
|
56
|
-
"@crawlee/fs-storage": "4.0.0-beta.
|
|
57
|
-
"@crawlee/http-client": "4.0.0-beta.
|
|
58
|
-
"@crawlee/types": "4.0.0-beta.
|
|
59
|
-
"@crawlee/utils": "4.0.0-beta.
|
|
56
|
+
"@crawlee/fs-storage": "4.0.0-beta.212",
|
|
57
|
+
"@crawlee/http-client": "4.0.0-beta.212",
|
|
58
|
+
"@crawlee/types": "4.0.0-beta.212",
|
|
59
|
+
"@crawlee/utils": "4.0.0-beta.212",
|
|
60
60
|
"@sapphire/async-queue": "^1.5.5",
|
|
61
61
|
"@standard-schema/spec": "^1.0.0",
|
|
62
62
|
"@vladfrangu/async_event_emitter": "^2.4.6",
|
|
@@ -76,5 +76,5 @@
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
},
|
|
79
|
-
"gitHead": "
|
|
79
|
+
"gitHead": "171a63a5d7f2a6d74cbce58479204ac20d30b87e"
|
|
80
80
|
}
|
package/proxy_configuration.d.ts
CHANGED
|
@@ -110,11 +110,4 @@ export declare class ProxyConfiguration implements IProxyConfiguration {
|
|
|
110
110
|
* For example, `http://bob:password123@proxy.example.com:8000`
|
|
111
111
|
*/
|
|
112
112
|
newUrl(): Promise<string | undefined>;
|
|
113
|
-
private handleProxyUrlsList;
|
|
114
|
-
/**
|
|
115
|
-
* Calls the custom newUrlFunction and checks format of its return value
|
|
116
|
-
*/
|
|
117
|
-
private callNewUrlFunction;
|
|
118
|
-
private throwCannotCombineCustomMethods;
|
|
119
|
-
private throwNoOptionsProvided;
|
|
120
113
|
}
|