@crawlee/core 4.0.0-beta.105 → 4.0.0-beta.107
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/autoscaling/autoscaled_pool.d.ts +3 -21
- package/autoscaling/autoscaled_pool.js +85 -85
- package/autoscaling/client_load_signal.d.ts +1 -5
- package/autoscaling/client_load_signal.js +20 -20
- package/autoscaling/concurrency_system.d.ts +5 -20
- package/autoscaling/concurrency_system.js +81 -80
- package/autoscaling/cpu_load_signal.d.ts +1 -2
- package/autoscaling/cpu_load_signal.js +10 -10
- package/autoscaling/event_loop_load_signal.d.ts +1 -4
- package/autoscaling/event_loop_load_signal.js +18 -18
- package/autoscaling/load_signal.d.ts +1 -1
- package/autoscaling/load_signal.js +12 -11
- package/autoscaling/memory_load_signal.d.ts +3 -12
- package/autoscaling/memory_load_signal.js +40 -41
- package/autoscaling/snapshotter.d.ts +1 -4
- package/autoscaling/snapshotter.js +12 -12
- package/autoscaling/system_status.d.ts +1 -3
- package/autoscaling/system_status.js +11 -11
- package/configuration.d.ts +1 -1
- package/configuration.js +3 -3
- package/crawlers/context_pipeline.js +6 -6
- package/crawlers/statistics.d.ts +1 -8
- package/crawlers/statistics.js +45 -44
- package/events/event_manager.d.ts +1 -1
- package/events/event_manager.js +3 -3
- package/events/local_event_manager.d.ts +1 -1
- package/events/local_event_manager.js +3 -3
- package/log.js +5 -1
- package/memory-storage/memory-storage.d.ts +1 -5
- package/memory-storage/memory-storage.js +2 -2
- package/memory-storage/resource-clients/dataset.d.ts +1 -1
- package/memory-storage/resource-clients/dataset.js +6 -5
- package/memory-storage/resource-clients/key-value-store.d.ts +1 -1
- package/memory-storage/resource-clients/key-value-store.js +13 -12
- package/memory-storage/resource-clients/request-queue.d.ts +4 -23
- package/memory-storage/resource-clients/request-queue.js +59 -58
- package/owned_or_injected.d.ts +1 -3
- package/owned_or_injected.js +17 -17
- package/package.json +5 -5
- package/proxy_configuration.d.ts +1 -3
- package/proxy_configuration.js +8 -8
- package/recoverable_state.d.ts +1 -10
- package/recoverable_state.js +41 -41
- package/request.d.ts +1 -2
- package/request.js +10 -13
- package/router.d.ts +1 -4
- package/router.js +23 -23
- package/serialization.js +8 -9
- package/service_locator.d.ts +1 -10
- package/service_locator.js +48 -48
- package/session_pool/session.d.ts +3 -14
- package/session_pool/session.js +54 -54
- package/session_pool/session_pool.d.ts +2 -11
- package/session_pool/session_pool.js +59 -58
- package/storages/dataset.d.ts +1 -1
- package/storages/dataset.js +5 -5
- package/storages/key_value_store.d.ts +1 -4
- package/storages/key_value_store.js +21 -20
- package/storages/request_dedup_cache.d.ts +1 -2
- package/storages/request_dedup_cache.js +9 -9
- package/storages/request_list.d.ts +2 -22
- package/storages/request_list.js +74 -73
- package/storages/request_manager_tandem.d.ts +1 -10
- package/storages/request_manager_tandem.js +27 -27
- package/storages/request_queue.d.ts +2 -18
- package/storages/request_queue.js +37 -35
- package/storages/sitemap_request_loader.d.ts +1 -44
- package/storages/sitemap_request_loader.js +87 -87
- package/storages/storage_instance_manager.d.ts +1 -2
- package/storages/storage_instance_manager.js +17 -17
- package/storages/storage_stats.d.ts +1 -1
- package/storages/storage_stats.js +4 -4
- package/storages/transaction.d.ts +1 -3
- package/storages/transaction.js +17 -17
- package/system-info/runtime.js +7 -7
|
@@ -60,24 +60,25 @@ const SESSION_REUSE_STRATEGIES = ['random', 'round-robin', 'use-until-failure'];
|
|
|
60
60
|
* @category Scaling
|
|
61
61
|
*/
|
|
62
62
|
export class SessionPool {
|
|
63
|
-
static nextId = 0;
|
|
63
|
+
static #nextId = 0;
|
|
64
64
|
id;
|
|
65
|
-
log;
|
|
65
|
+
#log;
|
|
66
|
+
#sessions = [];
|
|
67
|
+
// kept as TS-private: session_pool tests read/override the members below directly
|
|
66
68
|
maxPoolSize;
|
|
67
69
|
createSessionFunction;
|
|
68
70
|
keyValueStore;
|
|
69
|
-
sessions = [];
|
|
70
71
|
sessionMap = new Map();
|
|
71
72
|
sessionOptions;
|
|
72
73
|
persistStateKeyValueStoreId;
|
|
73
74
|
persistStateKey;
|
|
74
|
-
listener;
|
|
75
|
-
events;
|
|
76
|
-
persistenceOptions;
|
|
77
|
-
sessionReuseStrategy;
|
|
78
|
-
initPromise;
|
|
79
|
-
queue = new AsyncQueue();
|
|
80
|
-
roundRobinIndex = 0;
|
|
75
|
+
#listener;
|
|
76
|
+
#events;
|
|
77
|
+
#persistenceOptions;
|
|
78
|
+
#sessionReuseStrategy;
|
|
79
|
+
#initPromise;
|
|
80
|
+
#queue = new AsyncQueue();
|
|
81
|
+
#roundRobinIndex = 0;
|
|
81
82
|
constructor(options = {}) {
|
|
82
83
|
ow(options, ow.object.exactShape({
|
|
83
84
|
id: ow.optional.any(ow.number, ow.string),
|
|
@@ -93,20 +94,20 @@ export class SessionPool {
|
|
|
93
94
|
const { id, maxPoolSize = MAX_POOL_SIZE, persistStateKeyValueStoreId, persistStateKey, createSessionFunction, sessionOptions = {}, log = serviceLocator.getLogger(), persistenceOptions = {
|
|
94
95
|
enable: true,
|
|
95
96
|
}, sessionReuseStrategy = 'random', } = options;
|
|
96
|
-
this.id = id != null ? String(id) : String(SessionPool
|
|
97
|
-
this
|
|
98
|
-
this
|
|
99
|
-
this
|
|
100
|
-
this
|
|
97
|
+
this.id = id != null ? String(id) : String(SessionPool.#nextId++);
|
|
98
|
+
this.#sessionReuseStrategy = sessionReuseStrategy;
|
|
99
|
+
this.#events = serviceLocator.getEventManager();
|
|
100
|
+
this.#log = log.child({ prefix: 'SessionPool' });
|
|
101
|
+
this.#persistenceOptions = persistenceOptions;
|
|
101
102
|
// Pool Configuration
|
|
102
103
|
this.maxPoolSize = maxPoolSize;
|
|
103
104
|
this.createSessionFunction = createSessionFunction || this.defaultCreateSessionFunction;
|
|
104
105
|
// Session configuration. The pool-scoped logger is merged into per-call sessionOptions inside
|
|
105
|
-
// `
|
|
106
|
+
// `invokeCreateSessionFunction`, so every Session inherits it without custom createSessionFunctions
|
|
106
107
|
// having to know about it.
|
|
107
108
|
this.sessionOptions = {
|
|
108
109
|
...sessionOptions,
|
|
109
|
-
log: this
|
|
110
|
+
log: this.#log,
|
|
110
111
|
};
|
|
111
112
|
// Session keyValueStore
|
|
112
113
|
this.persistStateKeyValueStoreId = persistStateKeyValueStoreId;
|
|
@@ -117,39 +118,39 @@ export class SessionPool {
|
|
|
117
118
|
*/
|
|
118
119
|
async usableSessionsCount() {
|
|
119
120
|
await this.ensureInitialized();
|
|
120
|
-
return this
|
|
121
|
+
return this.#sessions.filter((session) => session.isUsable()).length;
|
|
121
122
|
}
|
|
122
123
|
/**
|
|
123
124
|
* Gets count of retired sessions in the pool.
|
|
124
125
|
*/
|
|
125
126
|
async retiredSessionsCount() {
|
|
126
127
|
await this.ensureInitialized();
|
|
127
|
-
return this
|
|
128
|
+
return this.#sessions.filter((session) => !session.isUsable()).length;
|
|
128
129
|
}
|
|
129
130
|
/**
|
|
130
131
|
* Starts periodic state persistence and potentially loads SessionPool state from {@link KeyValueStore}.
|
|
131
132
|
* Called automatically on first use of any public method.
|
|
132
133
|
*/
|
|
133
134
|
async ensureInitialized() {
|
|
134
|
-
if (!this
|
|
135
|
-
this
|
|
135
|
+
if (!this.#initPromise) {
|
|
136
|
+
this.#initPromise = this.setupPool();
|
|
136
137
|
}
|
|
137
|
-
return this
|
|
138
|
+
return this.#initPromise;
|
|
138
139
|
}
|
|
139
140
|
async setupPool() {
|
|
140
|
-
if (!this
|
|
141
|
+
if (!this.#persistenceOptions.enable) {
|
|
141
142
|
return;
|
|
142
143
|
}
|
|
143
144
|
this.keyValueStore = await KeyValueStore.open(this.persistStateKeyValueStoreId ? { id: this.persistStateKeyValueStoreId } : null, {
|
|
144
145
|
configuration: serviceLocator.getConfiguration(),
|
|
145
146
|
});
|
|
146
147
|
if (!this.persistStateKeyValueStoreId) {
|
|
147
|
-
this
|
|
148
|
+
this.#log.debug(`No 'persistStateKeyValueStoreId' options specified, this session pool's data has been saved in the KeyValueStore with the id: ${this.keyValueStore.id}`);
|
|
148
149
|
}
|
|
149
150
|
// in case of migration happened and SessionPool state should be restored from the keyValueStore.
|
|
150
151
|
await this.maybeLoadSessionPool();
|
|
151
|
-
this
|
|
152
|
-
this
|
|
152
|
+
this.#listener = this.persistState.bind(this);
|
|
153
|
+
this.#events.on("persistState" /* EventType.PERSIST_STATE */, this.#listener);
|
|
153
154
|
}
|
|
154
155
|
/**
|
|
155
156
|
* Adds a new session to the session pool. The pool automatically creates sessions up to the maximum size of the pool,
|
|
@@ -169,8 +170,8 @@ export class SessionPool {
|
|
|
169
170
|
if (!this.hasSpaceForSession()) {
|
|
170
171
|
this.removeRetiredSessions();
|
|
171
172
|
}
|
|
172
|
-
const newSession = options instanceof Session ? options : await this.
|
|
173
|
-
this
|
|
173
|
+
const newSession = options instanceof Session ? options : await this.invokeCreateSessionFunction(options);
|
|
174
|
+
this.#log.debug(`Adding new Session - ${newSession.id}`);
|
|
174
175
|
this.registerSession(newSession);
|
|
175
176
|
}
|
|
176
177
|
/**
|
|
@@ -181,7 +182,7 @@ export class SessionPool {
|
|
|
181
182
|
*/
|
|
182
183
|
async newSession(sessionOptions) {
|
|
183
184
|
await this.ensureInitialized();
|
|
184
|
-
const newSession = await this.
|
|
185
|
+
const newSession = await this.invokeCreateSessionFunction(sessionOptions);
|
|
185
186
|
this.registerSession(newSession);
|
|
186
187
|
return newSession;
|
|
187
188
|
}
|
|
@@ -194,7 +195,7 @@ export class SessionPool {
|
|
|
194
195
|
*/
|
|
195
196
|
async getSession(sessionId) {
|
|
196
197
|
await this.ensureInitialized();
|
|
197
|
-
await this
|
|
198
|
+
await this.#queue.wait();
|
|
198
199
|
try {
|
|
199
200
|
if (sessionId) {
|
|
200
201
|
const session = this.sessionMap.get(sessionId);
|
|
@@ -212,14 +213,14 @@ export class SessionPool {
|
|
|
212
213
|
return await this.createSession();
|
|
213
214
|
}
|
|
214
215
|
finally {
|
|
215
|
-
this
|
|
216
|
+
this.#queue.shift();
|
|
216
217
|
}
|
|
217
218
|
}
|
|
218
219
|
/**
|
|
219
220
|
* @param options - Override the persistence options provided in the constructor
|
|
220
221
|
*/
|
|
221
222
|
async resetStore(options) {
|
|
222
|
-
if (!this
|
|
223
|
+
if (!this.#persistenceOptions.enable && !options?.enable) {
|
|
223
224
|
return;
|
|
224
225
|
}
|
|
225
226
|
await this.ensureInitialized();
|
|
@@ -234,7 +235,7 @@ export class SessionPool {
|
|
|
234
235
|
return {
|
|
235
236
|
usableSessionsCount: await this.usableSessionsCount(),
|
|
236
237
|
retiredSessionsCount: await this.retiredSessionsCount(),
|
|
237
|
-
sessions: this
|
|
238
|
+
sessions: this.#sessions.map((session) => session.getState()),
|
|
238
239
|
};
|
|
239
240
|
}
|
|
240
241
|
/**
|
|
@@ -243,28 +244,28 @@ export class SessionPool {
|
|
|
243
244
|
* @param options - Override the persistence options provided in the constructor
|
|
244
245
|
*/
|
|
245
246
|
async persistState(options) {
|
|
246
|
-
if (!this
|
|
247
|
+
if (!this.#persistenceOptions.enable && !options?.enable) {
|
|
247
248
|
return;
|
|
248
249
|
}
|
|
249
250
|
await this.ensureInitialized();
|
|
250
|
-
this
|
|
251
|
+
this.#log.debug('Persisting state', {
|
|
251
252
|
persistStateKeyValueStoreId: this.persistStateKeyValueStoreId,
|
|
252
253
|
persistStateKey: this.persistStateKey,
|
|
253
254
|
});
|
|
254
255
|
await this.keyValueStore
|
|
255
256
|
?.setValue(this.persistStateKey, await this.getState())
|
|
256
|
-
.catch((error) => this
|
|
257
|
+
.catch((error) => this.#log.warning(`Failed to persist the session pool stats to ${this.persistStateKey}`, { error }));
|
|
257
258
|
}
|
|
258
259
|
/**
|
|
259
260
|
* Removes listener from `persistState` event.
|
|
260
261
|
* This function should be called after you are done with using the `SessionPool` instance.
|
|
261
262
|
*/
|
|
262
263
|
async teardown() {
|
|
263
|
-
if (!this
|
|
264
|
+
if (!this.#initPromise)
|
|
264
265
|
return;
|
|
265
266
|
await this.ensureInitialized();
|
|
266
|
-
if (this
|
|
267
|
-
this
|
|
267
|
+
if (this.#listener) {
|
|
268
|
+
this.#events.off("persistState" /* EventType.PERSIST_STATE */, this.#listener);
|
|
268
269
|
}
|
|
269
270
|
await this.persistState();
|
|
270
271
|
}
|
|
@@ -272,11 +273,11 @@ export class SessionPool {
|
|
|
272
273
|
* Removes retired `Session` instances from `SessionPool`.
|
|
273
274
|
*/
|
|
274
275
|
removeRetiredSessions() {
|
|
275
|
-
this
|
|
276
|
+
this.#sessions = this.#sessions.filter((storedSession) => {
|
|
276
277
|
if (storedSession.isUsable())
|
|
277
278
|
return true;
|
|
278
279
|
this.sessionMap.delete(storedSession.id);
|
|
279
|
-
this
|
|
280
|
+
this.#log.debug(`Removed Session - ${storedSession.id}`);
|
|
280
281
|
return false;
|
|
281
282
|
});
|
|
282
283
|
}
|
|
@@ -285,14 +286,14 @@ export class SessionPool {
|
|
|
285
286
|
* @param newSession `Session` instance to be added.
|
|
286
287
|
*/
|
|
287
288
|
registerSession(newSession) {
|
|
288
|
-
this
|
|
289
|
+
this.#sessions.push(newSession);
|
|
289
290
|
this.sessionMap.set(newSession.id, newSession);
|
|
290
291
|
}
|
|
291
292
|
/**
|
|
292
293
|
* Gets random index.
|
|
293
294
|
*/
|
|
294
295
|
getRandomIndex() {
|
|
295
|
-
return Math.floor(Math.random() * this
|
|
296
|
+
return Math.floor(Math.random() * this.#sessions.length);
|
|
296
297
|
}
|
|
297
298
|
/**
|
|
298
299
|
* Creates new session without any extra behavior.
|
|
@@ -315,7 +316,7 @@ export class SessionPool {
|
|
|
315
316
|
* through `maybeLoadSessionPool` naturally wins because it arrives in
|
|
316
317
|
* `perCallOptions`.
|
|
317
318
|
*/
|
|
318
|
-
async
|
|
319
|
+
async invokeCreateSessionFunction(perCallOptions) {
|
|
319
320
|
const sessionOptions = {
|
|
320
321
|
fingerprint: createDefaultSessionFingerprint(),
|
|
321
322
|
...this.sessionOptions,
|
|
@@ -328,35 +329,35 @@ export class SessionPool {
|
|
|
328
329
|
* @returns Newly created `Session` instance.
|
|
329
330
|
*/
|
|
330
331
|
async createSession() {
|
|
331
|
-
const newSession = await this.
|
|
332
|
+
const newSession = await this.invokeCreateSessionFunction();
|
|
332
333
|
this.registerSession(newSession);
|
|
333
|
-
this
|
|
334
|
+
this.#log.debug(`Created new Session - ${newSession.id}`);
|
|
334
335
|
return newSession;
|
|
335
336
|
}
|
|
336
337
|
/**
|
|
337
338
|
* Decides whether there is enough space for creating new session.
|
|
338
339
|
*/
|
|
339
340
|
hasSpaceForSession() {
|
|
340
|
-
return this
|
|
341
|
+
return this.#sessions.length < this.maxPoolSize;
|
|
341
342
|
}
|
|
342
343
|
/**
|
|
343
344
|
* Picks a session from the `SessionPool` according to the configured `sessionReuseStrategy`.
|
|
344
345
|
* Returns `undefined` when no session should be reused and a new one should be created instead.
|
|
345
346
|
*/
|
|
346
347
|
pickSession() {
|
|
347
|
-
if (this
|
|
348
|
+
if (this.#sessionReuseStrategy !== 'use-until-failure' && this.hasSpaceForSession())
|
|
348
349
|
return undefined;
|
|
349
|
-
if (this
|
|
350
|
-
return this
|
|
350
|
+
if (this.#sessionReuseStrategy === 'use-until-failure') {
|
|
351
|
+
return this.#sessions.find((session) => session.isUsable());
|
|
351
352
|
}
|
|
352
353
|
let picked;
|
|
353
|
-
if (this
|
|
354
|
-
const index = this
|
|
355
|
-
this
|
|
356
|
-
picked = this
|
|
354
|
+
if (this.#sessionReuseStrategy === 'round-robin') {
|
|
355
|
+
const index = this.#roundRobinIndex % this.#sessions.length;
|
|
356
|
+
this.#roundRobinIndex = index + 1;
|
|
357
|
+
picked = this.#sessions[index];
|
|
357
358
|
}
|
|
358
359
|
else {
|
|
359
|
-
picked = this
|
|
360
|
+
picked = this.#sessions[this.getRandomIndex()];
|
|
360
361
|
}
|
|
361
362
|
return picked.isUsable() ? picked : undefined;
|
|
362
363
|
}
|
|
@@ -369,18 +370,18 @@ export class SessionPool {
|
|
|
369
370
|
if (!loadedSessionPool)
|
|
370
371
|
return;
|
|
371
372
|
// Invalidate old sessions and load active sessions only
|
|
372
|
-
this
|
|
373
|
+
this.#log.debug('Recreating state from KeyValueStore', {
|
|
373
374
|
persistStateKeyValueStoreId: this.persistStateKeyValueStoreId,
|
|
374
375
|
persistStateKey: this.persistStateKey,
|
|
375
376
|
});
|
|
376
377
|
for (const sessionObject of loadedSessionPool.sessions) {
|
|
377
378
|
sessionObject.createdAt = new Date(sessionObject.createdAt);
|
|
378
379
|
sessionObject.expiresAt = new Date(sessionObject.expiresAt);
|
|
379
|
-
const recreatedSession = await this.
|
|
380
|
+
const recreatedSession = await this.invokeCreateSessionFunction(sessionObject);
|
|
380
381
|
if (recreatedSession.isUsable()) {
|
|
381
382
|
this.registerSession(recreatedSession);
|
|
382
383
|
}
|
|
383
384
|
}
|
|
384
|
-
this
|
|
385
|
+
this.#log.debug(`${this.#sessions.length} active sessions loaded from KeyValueStore`);
|
|
385
386
|
}
|
|
386
387
|
}
|
package/storages/dataset.d.ts
CHANGED
|
@@ -140,12 +140,12 @@ export interface DatasetExportToOptions extends DatasetExportOptions {
|
|
|
140
140
|
* @category Result Stores
|
|
141
141
|
*/
|
|
142
142
|
export declare class Dataset<Data extends Dictionary = Dictionary> {
|
|
143
|
+
#private;
|
|
143
144
|
readonly configuration: Configuration;
|
|
144
145
|
id: string;
|
|
145
146
|
name?: string;
|
|
146
147
|
backend: DatasetBackend<Data>;
|
|
147
148
|
log: CrawleeLogger;
|
|
148
|
-
private readonly statsTracker;
|
|
149
149
|
/**
|
|
150
150
|
* @internal
|
|
151
151
|
*/
|
package/storages/dataset.js
CHANGED
|
@@ -89,7 +89,7 @@ export class Dataset {
|
|
|
89
89
|
name;
|
|
90
90
|
backend;
|
|
91
91
|
log;
|
|
92
|
-
statsTracker = new StorageStatsTracker({
|
|
92
|
+
#statsTracker = new StorageStatsTracker({
|
|
93
93
|
readCount: 0,
|
|
94
94
|
writeCount: 0,
|
|
95
95
|
});
|
|
@@ -108,7 +108,7 @@ export class Dataset {
|
|
|
108
108
|
* the underlying storage backend). Counted per backend call.
|
|
109
109
|
*/
|
|
110
110
|
get stats() {
|
|
111
|
-
return this
|
|
111
|
+
return this.#statsTracker.current;
|
|
112
112
|
}
|
|
113
113
|
/**
|
|
114
114
|
* Stores an object or an array of objects to the dataset.
|
|
@@ -140,7 +140,7 @@ export class Dataset {
|
|
|
140
140
|
});
|
|
141
141
|
return;
|
|
142
142
|
}
|
|
143
|
-
this
|
|
143
|
+
this.#statsTracker.add('writeCount');
|
|
144
144
|
await this.backend.pushData(items);
|
|
145
145
|
}
|
|
146
146
|
/**
|
|
@@ -166,7 +166,7 @@ export class Dataset {
|
|
|
166
166
|
async readPage(options) {
|
|
167
167
|
const buffered = this.bufferedJournalEntries()?.flatMap((entry) => entry.items);
|
|
168
168
|
// Every branch below hits the backend exactly once.
|
|
169
|
-
this
|
|
169
|
+
this.#statsTracker.add('readCount');
|
|
170
170
|
if (!buffered?.length) {
|
|
171
171
|
return this.backend.getData(options);
|
|
172
172
|
}
|
|
@@ -235,7 +235,7 @@ export class Dataset {
|
|
|
235
235
|
// One backend call with all journaled items, in order - as close to atomic as the backend allows.
|
|
236
236
|
// Straight to the backend: the items were validated and snapshotted at write time.
|
|
237
237
|
if (items.length > 0) {
|
|
238
|
-
this
|
|
238
|
+
this.#statsTracker.add('writeCount');
|
|
239
239
|
await this.backend.pushData(items);
|
|
240
240
|
}
|
|
241
241
|
}
|
|
@@ -61,14 +61,11 @@ import type { StorageIdentifier } from './storage_instance_manager.js';
|
|
|
61
61
|
* @category Result Stores
|
|
62
62
|
*/
|
|
63
63
|
export declare class KeyValueStore {
|
|
64
|
+
#private;
|
|
64
65
|
readonly configuration: Configuration;
|
|
65
66
|
readonly id: string;
|
|
66
67
|
readonly name?: string;
|
|
67
68
|
private readonly backend;
|
|
68
|
-
private persistStateEventStarted;
|
|
69
|
-
/** Cache for persistent (auto-saved) values. When we try to set such value, the cache will be updated automatically. */
|
|
70
|
-
private readonly cache;
|
|
71
|
-
private readonly statsTracker;
|
|
72
69
|
/**
|
|
73
70
|
* @internal
|
|
74
71
|
*/
|
|
@@ -71,11 +71,12 @@ export class KeyValueStore {
|
|
|
71
71
|
configuration;
|
|
72
72
|
id;
|
|
73
73
|
name;
|
|
74
|
+
// kept as TS-private: key_value_store tests spy on the backend directly
|
|
74
75
|
backend;
|
|
75
|
-
persistStateEventStarted = false;
|
|
76
|
+
#persistStateEventStarted = false;
|
|
76
77
|
/** Cache for persistent (auto-saved) values. When we try to set such value, the cache will be updated automatically. */
|
|
77
|
-
cache = new Map();
|
|
78
|
-
statsTracker = new StorageStatsTracker({
|
|
78
|
+
#cache = new Map();
|
|
79
|
+
#statsTracker = new StorageStatsTracker({
|
|
79
80
|
readCount: 0,
|
|
80
81
|
writeCount: 0,
|
|
81
82
|
deleteCount: 0,
|
|
@@ -95,7 +96,7 @@ export class KeyValueStore {
|
|
|
95
96
|
* list operations issued to the underlying storage backend). Counted per backend call.
|
|
96
97
|
*/
|
|
97
98
|
get stats() {
|
|
98
|
-
return this
|
|
99
|
+
return this.#statsTracker.current;
|
|
99
100
|
}
|
|
100
101
|
/**
|
|
101
102
|
* Gets a value from the key-value store.
|
|
@@ -178,7 +179,7 @@ export class KeyValueStore {
|
|
|
178
179
|
contentType: serialized.contentType ?? null,
|
|
179
180
|
};
|
|
180
181
|
}
|
|
181
|
-
this
|
|
182
|
+
this.#statsTracker.add('readCount');
|
|
182
183
|
const record = await this.backend.getValue(key);
|
|
183
184
|
if (!record)
|
|
184
185
|
return null;
|
|
@@ -235,8 +236,8 @@ export class KeyValueStore {
|
|
|
235
236
|
}
|
|
236
237
|
async getAutoSavedValue(key, defaultValue = {}) {
|
|
237
238
|
tryCancel();
|
|
238
|
-
if (this
|
|
239
|
-
return this
|
|
239
|
+
if (this.#cache.has(key)) {
|
|
240
|
+
return this.#cache.get(key);
|
|
240
241
|
}
|
|
241
242
|
// Auto-saved state is deliberately *not* transactional. The direct read bypasses any active
|
|
242
243
|
// transaction - a buffered value seeded into this shared cache would survive a rollback forever.
|
|
@@ -245,25 +246,25 @@ export class KeyValueStore {
|
|
|
245
246
|
// the value will in cache at this point, and returning the new fetched value would introduce two different instances of
|
|
246
247
|
// the auto-saved object, and only the latter one would be persisted.
|
|
247
248
|
// Therefore we re-check the cache here, and if such race condition happened, we drop the fetched value and return the cached one.
|
|
248
|
-
if (this
|
|
249
|
-
return this
|
|
249
|
+
if (this.#cache.has(key)) {
|
|
250
|
+
return this.#cache.get(key);
|
|
250
251
|
}
|
|
251
|
-
this
|
|
252
|
+
this.#cache.set(key, value);
|
|
252
253
|
this.ensurePersistStateEvent();
|
|
253
254
|
return value;
|
|
254
255
|
}
|
|
255
256
|
ensurePersistStateEvent() {
|
|
256
|
-
if (this
|
|
257
|
+
if (this.#persistStateEventStarted) {
|
|
257
258
|
return;
|
|
258
259
|
}
|
|
259
260
|
serviceLocator.getEventManager().on('persistState', async () => {
|
|
260
261
|
const promises = [];
|
|
261
|
-
for (const [key, value] of this
|
|
262
|
+
for (const [key, value] of this.#cache) {
|
|
262
263
|
promises.push(this.setValue(key, value).catch((error) => serviceLocator.getLogger().warning(`Failed to persist the state value to ${key}`, { error })));
|
|
263
264
|
}
|
|
264
265
|
await Promise.all(promises);
|
|
265
266
|
});
|
|
266
|
-
this
|
|
267
|
+
this.#persistStateEventStarted = true;
|
|
267
268
|
}
|
|
268
269
|
async *fetchKeyValuePages(options, mapRecord) {
|
|
269
270
|
// Reduce the journal once for the whole iteration, not once per key inside `readRecord`.
|
|
@@ -303,7 +304,7 @@ export class KeyValueStore {
|
|
|
303
304
|
}
|
|
304
305
|
let exclusiveStartKey;
|
|
305
306
|
while (true) {
|
|
306
|
-
this
|
|
307
|
+
this.#statsTracker.add('listCount');
|
|
307
308
|
const { items, isTruncated, nextExclusiveStartKey } = await this.backend.listKeys({
|
|
308
309
|
...options,
|
|
309
310
|
exclusiveStartKey,
|
|
@@ -401,28 +402,28 @@ export class KeyValueStore {
|
|
|
401
402
|
return;
|
|
402
403
|
}
|
|
403
404
|
// If we try to set the value of a cached state to a different reference, we need to update the cache accordingly.
|
|
404
|
-
const cachedValue = this
|
|
405
|
+
const cachedValue = this.#cache.get(key);
|
|
405
406
|
if (cachedValue && cachedValue !== value) {
|
|
406
407
|
if (value === null) {
|
|
407
408
|
// Cached state can be only object, so a propagation of `null` means removing all its properties.
|
|
408
|
-
Object.keys(cachedValue).forEach((k) => this
|
|
409
|
+
Object.keys(cachedValue).forEach((k) => this.#cache.delete(k));
|
|
409
410
|
}
|
|
410
411
|
else if (typeof value === 'object') {
|
|
411
412
|
// We need to remove the keys that are no longer present in the new value.
|
|
412
413
|
Object.keys(cachedValue)
|
|
413
414
|
.filter((k) => !(k in value))
|
|
414
|
-
.forEach((k) => this
|
|
415
|
+
.forEach((k) => this.#cache.delete(k));
|
|
415
416
|
// And update the existing ones + add new ones.
|
|
416
417
|
Object.assign(cachedValue, value);
|
|
417
418
|
}
|
|
418
419
|
}
|
|
419
420
|
// In this case delete the record.
|
|
420
421
|
if (value === null) {
|
|
421
|
-
this
|
|
422
|
+
this.#statsTracker.add('deleteCount');
|
|
422
423
|
return this.backend.deleteValue(key);
|
|
423
424
|
}
|
|
424
425
|
const serialized = serializeValue(value, optionsCopy.contentType);
|
|
425
|
-
this
|
|
426
|
+
this.#statsTracker.add('writeCount');
|
|
426
427
|
return this.backend.setValue({
|
|
427
428
|
key,
|
|
428
429
|
value: serialized.value,
|
|
@@ -454,7 +455,7 @@ export class KeyValueStore {
|
|
|
454
455
|
/** @internal */
|
|
455
456
|
clearCache() {
|
|
456
457
|
rejectOperationInTransaction('KeyValueStore.clearCache()');
|
|
457
|
-
this
|
|
458
|
+
this.#cache.clear();
|
|
458
459
|
}
|
|
459
460
|
/**
|
|
460
461
|
* Iterates over key-value store keys, yielding each in turn to an `iteratee` function.
|
|
@@ -12,9 +12,8 @@
|
|
|
12
12
|
* @internal
|
|
13
13
|
*/
|
|
14
14
|
export declare class RequestDeduplicationCache {
|
|
15
|
+
#private;
|
|
15
16
|
private readonly size;
|
|
16
|
-
private keys;
|
|
17
|
-
private ids;
|
|
18
17
|
constructor(size?: number);
|
|
19
18
|
get(cacheKey: string): string | null;
|
|
20
19
|
add(cacheKey: string, requestId: string): void;
|
|
@@ -13,26 +13,26 @@
|
|
|
13
13
|
*/
|
|
14
14
|
export class RequestDeduplicationCache {
|
|
15
15
|
size;
|
|
16
|
-
keys;
|
|
17
|
-
ids;
|
|
16
|
+
#keys;
|
|
17
|
+
#ids;
|
|
18
18
|
// The slot count is the same for every queue, so it's a fixed default rather than a per-consumer option.
|
|
19
19
|
constructor(size = 1_000_000) {
|
|
20
20
|
this.size = size;
|
|
21
|
-
this
|
|
22
|
-
this
|
|
21
|
+
this.#keys = new Array(size);
|
|
22
|
+
this.#ids = new Array(size);
|
|
23
23
|
}
|
|
24
24
|
get(cacheKey) {
|
|
25
25
|
const index = this.indexOf(cacheKey);
|
|
26
|
-
return this
|
|
26
|
+
return this.#keys[index] === cacheKey ? this.#ids[index] : null;
|
|
27
27
|
}
|
|
28
28
|
add(cacheKey, requestId) {
|
|
29
29
|
const index = this.indexOf(cacheKey);
|
|
30
|
-
this
|
|
31
|
-
this
|
|
30
|
+
this.#keys[index] = cacheKey;
|
|
31
|
+
this.#ids[index] = requestId;
|
|
32
32
|
}
|
|
33
33
|
clear() {
|
|
34
|
-
this
|
|
35
|
-
this
|
|
34
|
+
this.#keys = new Array(this.size);
|
|
35
|
+
this.#ids = new Array(this.size);
|
|
36
36
|
}
|
|
37
37
|
// A cheap FNV-1a hash of the cache key — avoids pulling in a dedicated hashing dependency.
|
|
38
38
|
indexOf(cacheKey) {
|
|
@@ -216,28 +216,18 @@ export interface RequestListOptions {
|
|
|
216
216
|
* @category Sources
|
|
217
217
|
*/
|
|
218
218
|
export declare class RequestList implements IRequestLoader {
|
|
219
|
-
private
|
|
219
|
+
#private;
|
|
220
220
|
/**
|
|
221
221
|
* Array of all requests from all sources, in the order as they appeared in sources.
|
|
222
222
|
* All requests in the array have distinct uniqueKey!
|
|
223
223
|
* @internal
|
|
224
224
|
*/
|
|
225
225
|
readonly requests: (Request | RequestOptions)[];
|
|
226
|
-
/** Index to the next item in requests array to fetch. All previous requests are either handled or in progress. */
|
|
227
|
-
private nextIndex;
|
|
228
|
-
/** Dictionary, key is Request.uniqueKey, value is corresponding index in the requests array. */
|
|
229
|
-
private uniqueKeyToIndex;
|
|
230
226
|
/**
|
|
231
227
|
* Set of `uniqueKey`s of requests that were returned by fetchNextRequest().
|
|
232
228
|
* @internal
|
|
233
229
|
*/
|
|
234
230
|
inProgress: Set<string>;
|
|
235
|
-
/**
|
|
236
|
-
* `uniqueKey`s of requests that were in progress when the state was last persisted and thus need to be
|
|
237
|
-
* re-crawled after a restart. They are served before advancing through the rest of the sources.
|
|
238
|
-
* @internal
|
|
239
|
-
*/
|
|
240
|
-
private requestsToRetry;
|
|
241
231
|
/**
|
|
242
232
|
* Starts as true because until we handle the first request, the list is effectively persisted by doing nothing.
|
|
243
233
|
* @internal
|
|
@@ -248,17 +238,7 @@ export declare class RequestList implements IRequestLoader {
|
|
|
248
238
|
* @internal
|
|
249
239
|
*/
|
|
250
240
|
areRequestsPersisted: boolean;
|
|
251
|
-
private isLoading;
|
|
252
|
-
private isInitialized;
|
|
253
|
-
private persistStateKey?;
|
|
254
|
-
private persistRequestsKey?;
|
|
255
|
-
private initialState?;
|
|
256
|
-
private store?;
|
|
257
|
-
private keepDuplicateUrls;
|
|
258
241
|
private sources;
|
|
259
|
-
private sourcesFunction?;
|
|
260
|
-
private proxyConfiguration?;
|
|
261
|
-
private httpClient?;
|
|
262
242
|
/**
|
|
263
243
|
* To create new instance of `RequestList` we need to use `RequestList.open()` factory method.
|
|
264
244
|
* @param options All `RequestList` configuration options
|
|
@@ -437,7 +417,7 @@ export declare class RequestList implements IRequestLoader {
|
|
|
437
417
|
/**
|
|
438
418
|
* @internal wraps public utility for mocking purposes
|
|
439
419
|
*/
|
|
440
|
-
private
|
|
420
|
+
private downloadListOfUrls;
|
|
441
421
|
}
|
|
442
422
|
/**
|
|
443
423
|
* Represents state of a {@link RequestList}. It can be used to resume a {@link RequestList} which has been previously processed.
|