@crawlee/core 4.0.0-rc.0 → 4.0.0-rc.1

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.
Files changed (122) hide show
  1. package/README.md +1 -1
  2. package/configuration.d.ts +15 -46
  3. package/configuration.js +8 -20
  4. package/errors.d.ts +12 -53
  5. package/errors.js +13 -66
  6. package/events/index.d.ts +1 -0
  7. package/events/local_event_manager.d.ts +0 -7
  8. package/events/local_event_manager.js +8 -8
  9. package/events/system_info.d.ts +38 -0
  10. package/index.d.ts +2 -8
  11. package/index.js +4 -8
  12. package/internal.d.ts +8 -0
  13. package/internal.js +9 -0
  14. package/log.d.ts +10 -11
  15. package/log.js +53 -25
  16. package/memory-storage/memory-storage.d.ts +12 -7
  17. package/memory-storage/memory-storage.js +45 -17
  18. package/memory-storage/resource-clients/dataset.d.ts +0 -5
  19. package/memory-storage/resource-clients/dataset.js +17 -20
  20. package/memory-storage/resource-clients/key-value-store.d.ts +0 -9
  21. package/memory-storage/resource-clients/key-value-store.js +11 -33
  22. package/memory-storage/resource-clients/request-queue.d.ts +0 -22
  23. package/memory-storage/resource-clients/request-queue.js +57 -53
  24. package/package.json +16 -18
  25. package/proxy_configuration.d.ts +20 -23
  26. package/proxy_configuration.js +18 -12
  27. package/recoverable_state.d.ts +28 -6
  28. package/recoverable_state.js +51 -14
  29. package/request.d.ts +18 -104
  30. package/request.js +41 -220
  31. package/serialization.js +3 -3
  32. package/service_locator.d.ts +3 -0
  33. package/service_locator.js +2 -0
  34. package/storages/dataset.d.ts +9 -15
  35. package/storages/dataset.js +22 -14
  36. package/storages/index.d.ts +3 -4
  37. package/storages/index.js +1 -4
  38. package/storages/key_value_store.d.ts +12 -46
  39. package/storages/key_value_store.js +31 -47
  40. package/storages/key_value_store_codec.js +6 -11
  41. package/storages/request_dedup_cache.d.ts +0 -2
  42. package/storages/request_dedup_cache.js +8 -8
  43. package/storages/request_list.d.ts +6 -82
  44. package/storages/request_list.js +175 -179
  45. package/storages/request_loader.d.ts +48 -22
  46. package/storages/request_loader.js +36 -1
  47. package/storages/request_manager.d.ts +86 -0
  48. package/storages/request_manager_tandem.d.ts +13 -28
  49. package/storages/request_manager_tandem.js +46 -43
  50. package/storages/request_queue.d.ts +19 -49
  51. package/storages/request_queue.js +92 -88
  52. package/storages/storage_instance_manager.d.ts +1 -2
  53. package/storages/storage_instance_manager.js +4 -4
  54. package/storages/transaction.d.ts +27 -9
  55. package/storages/transaction.js +56 -11
  56. package/storages/utils.d.ts +2 -2
  57. package/validators.d.ts +3 -2
  58. package/validators.js +3 -2
  59. package/autoscaling/autoscaled_pool.d.ts +0 -195
  60. package/autoscaling/autoscaled_pool.js +0 -386
  61. package/autoscaling/concurrency_system.d.ts +0 -268
  62. package/autoscaling/concurrency_system.js +0 -362
  63. package/autoscaling/cpu_load_signal.d.ts +0 -43
  64. package/autoscaling/cpu_load_signal.js +0 -47
  65. package/autoscaling/event_loop_load_signal.d.ts +0 -51
  66. package/autoscaling/event_loop_load_signal.js +0 -60
  67. package/autoscaling/index.d.ts +0 -9
  68. package/autoscaling/index.js +0 -9
  69. package/autoscaling/load_signal.d.ts +0 -100
  70. package/autoscaling/load_signal.js +0 -105
  71. package/autoscaling/memory_load_signal.d.ts +0 -47
  72. package/autoscaling/memory_load_signal.js +0 -106
  73. package/autoscaling/snapshotter.d.ts +0 -84
  74. package/autoscaling/snapshotter.js +0 -67
  75. package/autoscaling/storage_backend_load_signal.d.ts +0 -56
  76. package/autoscaling/storage_backend_load_signal.js +0 -73
  77. package/autoscaling/system_status.d.ts +0 -159
  78. package/autoscaling/system_status.js +0 -139
  79. package/autoscaling/weighted_avg.d.ts +0 -5
  80. package/autoscaling/weighted_avg.js +0 -14
  81. package/cookie_utils.d.ts +0 -44
  82. package/cookie_utils.js +0 -122
  83. package/crawlers/context_pipeline.d.ts +0 -70
  84. package/crawlers/context_pipeline.js +0 -122
  85. package/crawlers/crawler_commons.d.ts +0 -159
  86. package/crawlers/error_snapshotter.d.ts +0 -57
  87. package/crawlers/error_snapshotter.js +0 -117
  88. package/crawlers/error_tracker.d.ts +0 -54
  89. package/crawlers/error_tracker.js +0 -308
  90. package/crawlers/index.d.ts +0 -5
  91. package/crawlers/index.js +0 -4
  92. package/crawlers/internals/types.d.ts +0 -7
  93. package/crawlers/internals/types.js +0 -1
  94. package/crawlers/statistics.d.ts +0 -328
  95. package/crawlers/statistics.js +0 -536
  96. package/enqueue_links/enqueue_links.d.ts +0 -156
  97. package/enqueue_links/enqueue_links.js +0 -78
  98. package/enqueue_links/index.d.ts +0 -2
  99. package/enqueue_links/index.js +0 -2
  100. package/enqueue_links/shared.d.ts +0 -93
  101. package/enqueue_links/shared.js +0 -239
  102. package/http.d.ts +0 -9
  103. package/http.js +0 -28
  104. package/router.d.ts +0 -306
  105. package/router.js +0 -309
  106. package/session_pool/consts.d.ts +0 -3
  107. package/session_pool/consts.js +0 -3
  108. package/session_pool/errors.d.ts +0 -7
  109. package/session_pool/errors.js +0 -11
  110. package/session_pool/fingerprint.d.ts +0 -9
  111. package/session_pool/fingerprint.js +0 -30
  112. package/session_pool/index.d.ts +0 -4
  113. package/session_pool/index.js +0 -4
  114. package/session_pool/session.d.ts +0 -150
  115. package/session_pool/session.js +0 -220
  116. package/session_pool/session_pool.d.ts +0 -240
  117. package/session_pool/session_pool.js +0 -394
  118. package/storages/sitemap_request_loader.d.ts +0 -201
  119. package/storages/sitemap_request_loader.js +0 -438
  120. package/storages/throttling_request_manager.d.ts +0 -239
  121. package/storages/throttling_request_manager.js +0 -646
  122. /package/{crawlers/crawler_commons.js → events/system_info.js} +0 -0
@@ -1,536 +0,0 @@
1
- import { z } from 'zod';
2
- import { convertStateSync, RecoverableState } from '../recoverable_state.js';
3
- import { serviceLocator } from '../service_locator.js';
4
- import { KeyValueStore } from '../storages/key_value_store.js';
5
- import { parseArgument, schemas, validators } from '../validators.js';
6
- import { ErrorTracker } from './error_tracker.js';
7
- /**
8
- * @ignore
9
- */
10
- class Job {
11
- #lastRunAt = null;
12
- #durationMillis;
13
- run() {
14
- this.#lastRunAt = Date.now();
15
- }
16
- finish() {
17
- this.#durationMillis = Date.now() - this.#lastRunAt;
18
- return this.#durationMillis;
19
- }
20
- }
21
- const statisticsOptionsSchema = z.strictObject({
22
- logIntervalSecs: schemas.anyNumber.default(60),
23
- logMessage: z.string().default('Statistics'),
24
- log: validators.logger.optional(),
25
- keyValueStore: z.instanceof(KeyValueStore).optional(),
26
- // `schemas.anyObject` passes values through by reference (object schemas return a pruned plain copy).
27
- persistenceOptions: schemas.anyObject.default(() => ({ enable: true })),
28
- saveErrorSnapshots: z.boolean().default(false),
29
- id: z.union([schemas.anyNumber, z.string()]).optional(),
30
- stateExtension: schemas.anyObject.default(() => ({})),
31
- });
32
- const errorTrackerConfig = {
33
- showErrorCode: true,
34
- showErrorName: true,
35
- showStackTrace: true,
36
- showFullStack: false,
37
- showErrorMessage: true,
38
- showFullMessage: false,
39
- };
40
- /**
41
- * The persisted record, in the order it is written - the schema rebuilds the object on the way out, so the field
42
- * order here *is* the record's field order (guarded by a test).
43
- *
44
- * JSON has no infinity, so the three fields that are `Infinity` until the first request settles are written as
45
- * `null`. Both {@link Statistics.serializeState} and {@link Statistics.deserializeState} run through this,
46
- * which is what keeps them describing the same record.
47
- *
48
- * Nothing is optional on purpose: the record has always carried every field, so one missing a field is not one of
49
- * ours and is discarded whole rather than partially trusted - a counter restored as a string would poison every
50
- * later increment.
51
- *
52
- * Custom fields are not this schema's business either way: they are added to the record after the encode and
53
- * validated by their own conversion on the way back, so the keys it does not know about are simply dropped here.
54
- */
55
- const persistedStatisticState = z.object({
56
- requestsFinished: z.number(),
57
- requestsFailed: z.number(),
58
- requestsRetries: z.number(),
59
- requestsFailedPerMinute: z.number().nullable(),
60
- requestsFinishedPerMinute: z.number().nullable(),
61
- requestMinDurationMillis: z.number().nullable(),
62
- requestMaxDurationMillis: z.number(),
63
- requestTotalFailedDurationMillis: z.number(),
64
- requestTotalFinishedDurationMillis: z.number(),
65
- crawlerStartedAt: z.string().nullable(),
66
- crawlerFinishedAt: z.string().nullable(),
67
- statsPersistedAt: z.string(),
68
- crawlerRuntimeMillis: z.number(),
69
- crawlerLastStartTimestamp: z.number(),
70
- // A retry count that never occurred leaves a hole in the live histogram, written out as a `null`. We
71
- // once saw a record whose histogram was not an array at all and crashed the crawler on load.
72
- requestRetryHistogram: z.array(z.number().nullable()),
73
- statsId: z.string(),
74
- requestAvgFailedDurationMillis: z.number().nullable(),
75
- requestAvgFinishedDurationMillis: z.number().nullable(),
76
- requestTotalDurationMillis: z.number(),
77
- requestsTotal: z.number(),
78
- requestsWithStatusCode: z.record(z.string(), z.number()),
79
- errors: z.record(z.string(), z.unknown()),
80
- retryErrors: z.record(z.string(), z.unknown()),
81
- });
82
- /** `Infinity` is what the statistics use for "nothing to average yet"; JSON has only `null` for it. */
83
- function finiteOrNull(value) {
84
- return Number.isFinite(value) ? value : null;
85
- }
86
- /**
87
- * The conversion between the live state and the record above, in both directions.
88
- *
89
- * Built per instance rather than kept as a constant because a record carries three things the state does not: the
90
- * instance `id`, the derived aggregates of the overridable {@link Statistics.calculate}, and - on the way back -
91
- * the fields that are rebuilt from the instance's default state rather than restored, the error trackers among them.
92
- *
93
- * The model side is deliberately opaque: zod rebuilds what it validates, and `state.errors` has to stay the very
94
- * object the error trackers write into, not a copy of it.
95
- */
96
- function buildStatisticStateCodec(statistics) {
97
- return z.codec(persistedStatisticState, z.custom(), {
98
- decode: (record) => ({
99
- ...statistics.defaultState(),
100
- requestsFinished: record.requestsFinished,
101
- requestsFailed: record.requestsFailed,
102
- requestsRetries: record.requestsRetries,
103
- requestTotalFailedDurationMillis: record.requestTotalFailedDurationMillis,
104
- requestTotalFinishedDurationMillis: record.requestTotalFinishedDurationMillis,
105
- // Restoring the `null` as-is would make every later `duration < min` comparison fail, leaving the
106
- // minimum `null` for the rest of the run.
107
- requestMinDurationMillis: record.requestMinDurationMillis ?? Infinity,
108
- requestMaxDurationMillis: record.requestMaxDurationMillis,
109
- crawlerRuntimeMillis: record.crawlerRuntimeMillis,
110
- // A `null` stands for the zero requests that reached that retry count - restore it as such.
111
- requestRetryHistogram: record.requestRetryHistogram.map((count) => count ?? 0),
112
- // The record keeps ISO strings, the live state keeps `Date`s.
113
- crawlerStartedAt: record.crawlerStartedAt === null ? null : new Date(record.crawlerStartedAt),
114
- crawlerFinishedAt: record.crawlerFinishedAt === null ? null : new Date(record.crawlerFinishedAt),
115
- statsPersistedAt: new Date(record.statsPersistedAt),
116
- // Rebased so that the runtime reported by `calculate()` spans the migration instead of restarting.
117
- instanceStart: Date.now() - (new Date(record.statsPersistedAt).getTime() - record.crawlerLastStartTimestamp),
118
- }),
119
- encode: (state) => {
120
- const { requestsWithStatusCode, errors, retryErrors, requestRetryHistogram, instanceStart, ...counters } = state;
121
- // Every rate and average `calculate()` derives is `Infinity` until the run is long enough, or until
122
- // something has finished or failed, to divide by.
123
- const { requestAvgFailedDurationMillis, requestAvgFinishedDurationMillis, requestsFailedPerMinute, requestsFinishedPerMinute, ...aggregates } = statistics.calculate();
124
- return {
125
- ...counters,
126
- requestMinDurationMillis: finiteOrNull(state.requestMinDurationMillis),
127
- crawlerStartedAt: state.crawlerStartedAt ? new Date(state.crawlerStartedAt).toISOString() : null,
128
- crawlerFinishedAt: state.crawlerFinishedAt ? new Date(state.crawlerFinishedAt).toISOString() : null,
129
- statsPersistedAt: new Date().toISOString(),
130
- crawlerLastStartTimestamp: instanceStart,
131
- // `Array.from`, not `map` - a hole left by a retry count no request reached is skipped by `map`
132
- // and would stay a hole, which is not something the record can carry.
133
- requestRetryHistogram: Array.from(requestRetryHistogram, (count) => count ?? null),
134
- statsId: statistics.statsId,
135
- ...aggregates,
136
- requestAvgFailedDurationMillis: finiteOrNull(requestAvgFailedDurationMillis),
137
- requestAvgFinishedDurationMillis: finiteOrNull(requestAvgFinishedDurationMillis),
138
- requestsFailedPerMinute: finiteOrNull(requestsFailedPerMinute),
139
- requestsFinishedPerMinute: finiteOrNull(requestsFinishedPerMinute),
140
- requestsWithStatusCode,
141
- errors,
142
- retryErrors,
143
- };
144
- },
145
- });
146
- }
147
- /**
148
- * The statistics class provides an interface to collecting and logging run
149
- * statistics for requests.
150
- *
151
- * All statistic information is saved on key value store
152
- * under the key `CRAWLEE_CRAWLER_STATISTICS_*`, persists between
153
- * migrations and abort/resurrect
154
- *
155
- * Custom fields are tracked by passing {@link StatisticsOptions.stateExtension|`stateExtension`} - the extra fields are then part
156
- * of {@link Statistics.state|`state`}, persisted and restored along with the built-in ones.
157
- *
158
- * @category Crawlers
159
- */
160
- export class Statistics {
161
- // kept as TS-private: statistics tests read the static counter directly
162
- static id = 0;
163
- /**
164
- * An error tracker for final retry errors.
165
- */
166
- errorTracker;
167
- /**
168
- * An error tracker for retry errors prior to the final retry.
169
- */
170
- errorTrackerRetry;
171
- /**
172
- * Statistic instance id.
173
- */
174
- id;
175
- #persistStateKey;
176
- #stateCodec;
177
- #recoverableState;
178
- #stateExtension;
179
- #defaultStateExtension;
180
- #stateExtensionKeys;
181
- #logIntervalMillis;
182
- #logMessage;
183
- #requestsInProgress = new Map();
184
- log;
185
- #logInterval;
186
- /**
187
- * Current statistic state used for doing calculations on {@link Statistics.calculate} calls
188
- */
189
- get state() {
190
- return this.#recoverableState.currentValue;
191
- }
192
- /**
193
- * Contains the current retries histogram. Index 0 means 0 retries, index 2, 2 retries, and so on
194
- */
195
- get requestRetryHistogram() {
196
- return this.state.requestRetryHistogram;
197
- }
198
- /**
199
- * Construct a statistics instance to pass to a crawler via its `statistics` option, e.g. to preconfigure
200
- * persistence or error snapshots, share it across sequential runs, or track extra fields via `state`.
201
- */
202
- constructor(options = {}) {
203
- const { logIntervalSecs, logMessage, log, keyValueStore, persistenceOptions, saveErrorSnapshots, id, stateExtension, } = parseArgument(options, statisticsOptionsSchema);
204
- this.id = id ?? String(Statistics.id++);
205
- this.#persistStateKey = `CRAWLEE_CRAWLER_STATISTICS_${this.id}`;
206
- this.log = (log ?? serviceLocator.getLogger()).child({ prefix: 'Statistics' });
207
- this.errorTracker = new ErrorTracker({ ...errorTrackerConfig, saveErrorSnapshots });
208
- this.errorTrackerRetry = new ErrorTracker({ ...errorTrackerConfig, saveErrorSnapshots });
209
- this.#logIntervalMillis = logIntervalSecs * 1000;
210
- this.#logMessage = logMessage;
211
- this.#stateExtension = stateExtension;
212
- this.#defaultStateExtension = this.#resolveDefaultStateExtension(this.#stateExtension);
213
- this.#stateExtensionKeys = Object.keys(this.#defaultStateExtension());
214
- for (const key of this.#stateExtensionKeys) {
215
- if (key in this.#builtInDefaultState()) {
216
- throw new Error(`The custom statistics field \`${String(key)}\` collides with a built-in one - it would shadow ` +
217
- 'the value the crawler tracks. Rename it in `stateExtension`.');
218
- }
219
- }
220
- // `calculate()` is late-bound on purpose - it is an override point, and a subclass's must be the one that runs.
221
- this.#stateCodec = buildStatisticStateCodec({
222
- statsId: this.id,
223
- defaultState: () => this.#defaultState(),
224
- calculate: () => this.calculate(),
225
- });
226
- this.#recoverableState = new RecoverableState({
227
- persistStateKey: this.#persistStateKey,
228
- persistenceEnabled: persistenceOptions.enable,
229
- keyValueStore,
230
- logger: this.log,
231
- defaultState: () => this.#defaultState(),
232
- serialize: (state) => this.#serializeState(state),
233
- deserialize: (persistedState) => this.#deserializeState(persistedState),
234
- });
235
- // initialize by "resetting"
236
- this.reset();
237
- }
238
- /**
239
- * Set the current statistic instance to pristine values.
240
- *
241
- * The persisted record is left alone - use {@link Statistics.resetStore} to clear that as well.
242
- */
243
- reset() {
244
- this.errorTracker.reset();
245
- this.errorTrackerRetry.reset();
246
- this.#recoverableState.reset();
247
- this.#requestsInProgress.clear();
248
- }
249
- /** The pristine state a new instance starts with and {@link Statistics.reset} restores. */
250
- #defaultState() {
251
- return {
252
- ...this.#builtInDefaultState(),
253
- ...this.#defaultStateExtension(),
254
- };
255
- }
256
- /**
257
- * The factory behind the custom half of the default state - it has to hand out a fresh object every time, or a
258
- * `reset()` would write through to the defaults of the next one.
259
- *
260
- * With no `defaultState` given, the defaults are what `deserialize` makes of an empty record. That keeps a
261
- * single declaration of the custom fields - a schema with a `.default()` per field is enough - and the defaults
262
- * cannot then disagree with the conversion that has to accept them back.
263
- */
264
- #resolveDefaultStateExtension(options) {
265
- const { defaultState, deserialize } = options;
266
- if (typeof defaultState === 'function') {
267
- return defaultState;
268
- }
269
- if (defaultState !== undefined) {
270
- return () => structuredClone(defaultState);
271
- }
272
- if (deserialize === undefined) {
273
- return () => ({});
274
- }
275
- return () => {
276
- try {
277
- return convertStateSync(deserialize, {}, this.#persistStateKey);
278
- }
279
- catch (error) {
280
- throw new Error('Could not derive the default values of the custom statistics fields from `stateExtension.deserialize` - ' +
281
- 'give every field a default, or declare `stateExtension.defaultState` explicitly.', { cause: error });
282
- }
283
- };
284
- }
285
- /** The built-in half of {@link Statistics.defaultState}, before any custom fields are merged over it. */
286
- #builtInDefaultState() {
287
- return {
288
- requestsFinished: 0,
289
- requestsFailed: 0,
290
- requestsRetries: 0,
291
- requestsFailedPerMinute: 0,
292
- requestsFinishedPerMinute: 0,
293
- requestMinDurationMillis: Infinity,
294
- requestMaxDurationMillis: 0,
295
- requestTotalFailedDurationMillis: 0,
296
- requestTotalFinishedDurationMillis: 0,
297
- crawlerStartedAt: null,
298
- crawlerFinishedAt: null,
299
- statsPersistedAt: null,
300
- crawlerRuntimeMillis: 0,
301
- requestsWithStatusCode: {},
302
- // Aliases, not copies - the trackers keep writing into these objects.
303
- errors: this.errorTracker.result,
304
- retryErrors: this.errorTrackerRetry.result,
305
- requestRetryHistogram: [],
306
- instanceStart: Date.now(),
307
- };
308
- }
309
- /**
310
- * Clear the persisted statistics record, leaving the in-memory state alone.
311
- *
312
- * Throws while capturing - the next PERSIST_STATE event would write the record straight back.
313
- */
314
- async resetStore() {
315
- await this.#recoverableState.resetStore();
316
- }
317
- /**
318
- * Increments the status code counter.
319
- */
320
- registerStatusCode(code) {
321
- const s = String(code);
322
- if (this.state.requestsWithStatusCode[s] === undefined) {
323
- this.state.requestsWithStatusCode[s] = 0;
324
- }
325
- this.state.requestsWithStatusCode[s]++;
326
- }
327
- /**
328
- * Starts a job
329
- * @ignore
330
- */
331
- startJob(id) {
332
- let job = this.#requestsInProgress.get(id);
333
- if (!job)
334
- job = new Job();
335
- job.run();
336
- this.#requestsInProgress.set(id, job);
337
- }
338
- /**
339
- * Mark job as finished and sets the state
340
- * @ignore
341
- */
342
- finishJob(id, retryCount) {
343
- const job = this.#requestsInProgress.get(id);
344
- if (!job)
345
- return;
346
- const jobDurationMillis = job.finish();
347
- this.state.requestsFinished++;
348
- this.state.requestTotalFinishedDurationMillis += jobDurationMillis;
349
- this.saveRetryCountForJob(retryCount);
350
- if (jobDurationMillis < this.state.requestMinDurationMillis)
351
- this.state.requestMinDurationMillis = jobDurationMillis;
352
- if (jobDurationMillis > this.state.requestMaxDurationMillis)
353
- this.state.requestMaxDurationMillis = jobDurationMillis;
354
- this.#requestsInProgress.delete(id);
355
- }
356
- /**
357
- * Mark job as failed and sets the state
358
- * @ignore
359
- */
360
- failJob(id, retryCount) {
361
- const job = this.#requestsInProgress.get(id);
362
- if (!job)
363
- return;
364
- this.state.requestTotalFailedDurationMillis += job.finish();
365
- this.state.requestsFailed++;
366
- this.saveRetryCountForJob(retryCount);
367
- this.#requestsInProgress.delete(id);
368
- }
369
- /**
370
- * Discards a started job without affecting the finished/failed counters, e.g. when a request
371
- * turns out to be skipped (robots.txt, enqueue strategy) after `startJob` was already called for it.
372
- * @ignore
373
- */
374
- discardJob(id) {
375
- this.#requestsInProgress.delete(id);
376
- }
377
- /**
378
- * Calculate the current statistics
379
- */
380
- calculate() {
381
- const { requestsFailed, requestsFinished, requestTotalFailedDurationMillis, requestTotalFinishedDurationMillis, } = this.state;
382
- const totalMillis = Date.now() - this.state.instanceStart;
383
- const totalMinutes = totalMillis / 1000 / 60;
384
- return {
385
- requestAvgFailedDurationMillis: Math.round(requestTotalFailedDurationMillis / requestsFailed) || Infinity,
386
- requestAvgFinishedDurationMillis: Math.round(requestTotalFinishedDurationMillis / requestsFinished) || Infinity,
387
- requestsFinishedPerMinute: Math.round(requestsFinished / totalMinutes) || 0,
388
- requestsFailedPerMinute: Math.floor(requestsFailed / totalMinutes) || 0,
389
- requestTotalDurationMillis: requestTotalFinishedDurationMillis + requestTotalFailedDurationMillis,
390
- requestsTotal: requestsFailed + requestsFinished,
391
- crawlerRuntimeMillis: totalMillis,
392
- };
393
- }
394
- /**
395
- * Initializes the key value store for persisting the statistics,
396
- * displaying the current state in predefined intervals
397
- */
398
- async startCapturing() {
399
- // A single instance drives one logging interval and one PERSIST_STATE listener, so a second concurrent
400
- // capture (e.g. sharing one instance across crawlers running at once) would orphan the first. Fail loudly.
401
- if (this.#logInterval) {
402
- throw new Error('Statistics.startCapturing() was already called - this instance is already capturing.');
403
- }
404
- await this.#recoverableState.initialize();
405
- // After the load, so that a restored record keeps the timestamp of the run it belongs to.
406
- if (this.state.crawlerStartedAt === null) {
407
- this.state.crawlerStartedAt = new Date();
408
- }
409
- this.#logInterval = setInterval(() => {
410
- this.log.info(this.#logMessage, {
411
- ...this.calculate(),
412
- retryHistogram: this.requestRetryHistogram,
413
- });
414
- }, this.#logIntervalMillis);
415
- }
416
- /**
417
- * Stops logging and remove event listeners, then persist
418
- */
419
- async stopCapturing() {
420
- this.#stopLogging();
421
- this.state.crawlerFinishedAt = new Date();
422
- await this.#recoverableState.teardown();
423
- }
424
- saveRetryCountForJob(retryCount) {
425
- if (retryCount > 0)
426
- this.state.requestsRetries++;
427
- this.requestRetryHistogram[retryCount] ??= 0;
428
- this.requestRetryHistogram[retryCount]++;
429
- }
430
- /**
431
- * Persist internal state to the key value store.
432
- *
433
- * Statistics are bookkeeping - a store that refuses the write is worth a warning, not a failed crawl. The
434
- * crawler calls this from its migration handler, where a rejection would go unhandled.
435
- */
436
- async persistState() {
437
- await this.#recoverableState
438
- .persistState()
439
- .catch((error) => this.log.warning(`Failed to persist the statistics to ${this.#persistStateKey}`, { error }));
440
- }
441
- /** Rebuilds the state from a persisted record. */
442
- #deserializeState(persistedState) {
443
- // The cast covers the custom fields, whose type is open here - and the record is an unvalidated blob off the
444
- // key-value store either way, which is what the decode is for. Their keys are not in the schema, so the
445
- // decode drops them; `#restoreStateExtension` is what brings them back.
446
- const restored = z.safeDecode(this.#stateCodec, persistedState);
447
- if (!restored.success) {
448
- // Statistics are bookkeeping - a record that cannot be made sense of is worth a warning and a fresh
449
- // start, not a failed crawl.
450
- this.log.warning('Received invalid state from Key-value store, starting the statistics from scratch.', {
451
- persistStateKey: this.#persistStateKey,
452
- issues: restored.error.issues,
453
- });
454
- return this.#defaultState();
455
- }
456
- this.log.debug('Recreating state from KeyValueStore', { persistStateKey: this.#persistStateKey });
457
- return { ...this.#defaultState(), ...restored.data, ...this.#restoreStateExtension(persistedState) };
458
- }
459
- /**
460
- * The custom {@link StatisticsOptions.stateExtension|`stateExtension`} fields as they were persisted - the codec only rebuilds
461
- * the built-in ones.
462
- *
463
- * Given a `deserialize`, the whole record goes through it, so a field the record does not carry - one declared
464
- * after the record was written - comes back as whatever default the conversion gives it. Without one, the
465
- * declared keys are copied over as they were persisted and a missing one keeps the default it already has.
466
- */
467
- #restoreStateExtension(persistedState) {
468
- const { deserialize } = this.#stateExtension;
469
- if (deserialize === undefined) {
470
- const restored = {};
471
- for (const key of this.#stateExtensionKeys) {
472
- // The record is an unvalidated blob and there is no conversion to check it with, so this is the
473
- // caller's word for it - the state ends up holding whatever was written. The two halves only line
474
- // up at all because the fields are persisted as they are without a `serialize`.
475
- const persistedValue = persistedState[key];
476
- if (persistedValue !== undefined) {
477
- restored[key] = persistedValue;
478
- }
479
- }
480
- return restored;
481
- }
482
- try {
483
- return convertStateSync(deserialize, persistedState, this.#persistStateKey);
484
- }
485
- catch (error) {
486
- // Same policy as a built-in field that cannot be made sense of, but scoped to the custom ones - a
487
- // corrupt counter of your own is no reason to throw away the crawler's.
488
- this.log.warning('Received invalid custom statistics fields from Key-value store, starting those from scratch.', { persistStateKey: this.#persistStateKey, error });
489
- return {};
490
- }
491
- }
492
- #stopLogging() {
493
- if (this.#logInterval) {
494
- clearInterval(this.#logInterval);
495
- this.#logInterval = null;
496
- }
497
- }
498
- /**
499
- * Builds the record written to the key value store, merging in the derived aggregates so that a consumer
500
- * reading the record does not have to reconstruct them.
501
- */
502
- #serializeState(state) {
503
- const { builtIn, extension } = this.#splitState(state);
504
- return {
505
- ...z.encode(this.#stateCodec, builtIn),
506
- ...this.#serializeStateExtension(extension),
507
- };
508
- }
509
- /** The custom {@link StatisticsOptions.stateExtension|`stateExtension`} fields as they go into the record. */
510
- #serializeStateExtension(extension) {
511
- const { serialize } = this.#stateExtension;
512
- if (serialize === undefined) {
513
- return extension;
514
- }
515
- // Unlike the way back, a failure here throws: the value is the caller's own, and a record written from a
516
- // state that does not match its own declaration is not worth having.
517
- return convertStateSync(serialize, extension, this.#persistStateKey);
518
- }
519
- /** Separates the declared custom fields from the built-in ones, so that each half goes through its own conversion. */
520
- #splitState(state) {
521
- const builtIn = { ...state };
522
- const extension = {};
523
- for (const key of this.#stateExtensionKeys) {
524
- extension[key] = state[key];
525
- delete builtIn[key];
526
- }
527
- return { builtIn, extension };
528
- }
529
- /**
530
- * Make this class serializable when called with `JSON.stringify(statsInstance)` directly
531
- * or through `keyValueStore.setValue('KEY', statsInstance)`
532
- */
533
- toJSON() {
534
- return this.#serializeState(this.state);
535
- }
536
- }
@@ -1,156 +0,0 @@
1
- import type { Dictionary } from '@crawlee/types';
2
- import { EnqueueStrategy } from '@crawlee/utils';
3
- import type { RequestQueueOperationOptions } from '../storages/request_queue.js';
4
- import type { RequestTransform, SkippedRequestCallback, UrlPatternInput, UrlPatternObject } from './shared.js';
5
- /**
6
- * Options shared by the `extractLinks()` context helper across crawler types.
7
- */
8
- export interface ExtractLinksOptions {
9
- /** A CSS selector matching links to be extracted. */
10
- selector?: string;
11
- /**
12
- * A base URL that will be used to resolve relative URLs when using Cheerio. Ignored when using Puppeteer,
13
- * since the relative URL resolution is done inside the browser automatically.
14
- */
15
- baseUrl?: string;
16
- }
17
- /**
18
- * Options accepted by the `enqueueUrls()` context helper exposed by `BasicCrawler`.
19
- */
20
- export interface EnqueueUrlsOptions extends RequestQueueOperationOptions {
21
- /** Limit the amount of actually enqueued URLs to this number. Useful for testing across the entire crawling scope. */
22
- limit?: number;
23
- /** Sets {@link Request.userData} for newly enqueued requests. */
24
- userData?: Dictionary;
25
- /**
26
- * Sets {@link Request.label} for newly enqueued requests.
27
- *
28
- * Can be overwritten by `transformRequestFunction`.
29
- */
30
- label?: string;
31
- /** Sets {@link Request.sessionId} for newly enqueued requests. */
32
- sessionId?: string;
33
- /**
34
- * If set to `true`, tells the crawler to skip navigation and process the request directly.
35
- * @default false
36
- */
37
- skipNavigation?: boolean;
38
- /**
39
- * A base URL that will be used to resolve relative URLs.
40
- */
41
- baseUrl?: string;
42
- /**
43
- * An array of URL patterns that URLs must match to be enqueued.
44
- *
45
- * Accepts glob pattern strings, `{ glob: string }` objects, `RegExp` instances, or `{ regexp: RegExp }` objects.
46
- *
47
- * Glob matching is always case-insensitive.
48
- * If you need case-sensitive matching, use a `RegExp`.
49
- *
50
- * The patterns are combined with the {@link EnqueueUrlsOptions.strategy|`strategy`} using AND logic - a URL
51
- * must match at least one `include` pattern **and** satisfy the strategy to be enqueued. To match URLs across
52
- * hostnames, pass an explicit {@link EnqueueStrategy.All} strategy.
53
- *
54
- * If `undefined`, the links are enqueued based on the {@link EnqueueUrlsOptions.strategy|`strategy`} alone.
55
- * Passing an empty array is not allowed.
56
- */
57
- include?: readonly UrlPatternInput[];
58
- /**
59
- * An array of URL patterns. Matching URLs will **not** be enqueued.
60
- *
61
- * Accepts glob pattern strings, `{ glob: string }` objects, `RegExp` instances, or `{ regexp: RegExp }` objects.
62
- *
63
- * Glob matching is always case-insensitive.
64
- * If you need case-sensitive matching, use a `RegExp`.
65
- */
66
- exclude?: readonly UrlPatternInput[];
67
- /**
68
- * After request options are filtered by `include`/`exclude` patterns, this function can be used
69
- * to remove them or modify their contents such as `userData`, `payload` or, most importantly `uniqueKey`. This is useful
70
- * when you need to enqueue multiple `Requests` to the queue that share the same URL, but differ in methods or payloads,
71
- * or to dynamically update or create `userData`.
72
- *
73
- * For example: by adding `keepUrlFragment: true` to the request options, URL fragments will not be removed
74
- * when `uniqueKey` is computed.
75
- *
76
- * **Example:**
77
- * ```javascript
78
- * {
79
- * transformRequestFunction: (request) => {
80
- * request.userData.foo = 'bar';
81
- * request.keepUrlFragment = true;
82
- * return request;
83
- * }
84
- * }
85
- * ```
86
- *
87
- * Note that `transformRequestFunction` has the highest priority and can overwrite
88
- * the global `label` option.
89
- *
90
- * The function receives a {@link RequestOptions} object and can return either:
91
- * - The modified {@link RequestOptions} object
92
- * - `'unchanged'` to keep the original options as-is
93
- * - A falsy value or `'skip'` to exclude the request from the queue
94
- */
95
- transformRequestFunction?: RequestTransform;
96
- /**
97
- * The strategy to use when enqueueing the urls.
98
- *
99
- * Depending on the strategy you select, we will only check certain parts of the URLs found. Here is a diagram of each URL part and their name:
100
- *
101
- * ```md
102
- * Protocol Domain
103
- * ┌────┐ ┌─────────┐
104
- * https://example.crawlee.dev/...
105
- * │ └─────────────────┤
106
- * │ Hostname │
107
- * │ │
108
- * └─────────────────────────┘
109
- * Origin
110
- *```
111
- *
112
- * @default EnqueueStrategy.SameHostname
113
- */
114
- strategy?: EnqueueStrategyOption;
115
- /**
116
- * By default, only the first batch (1000) of found requests will be added to the queue before resolving the call.
117
- * You can use this option to wait for adding all of them.
118
- */
119
- waitForAllRequestsToBeAdded?: boolean;
120
- /**
121
- * When a request is skipped for some reason, you can use this callback to act on it.
122
- * This is currently fired for requests skipped
123
- * 1. based on robots.txt file,
124
- * 2. because they don't match enqueueLinks filters,
125
- * 3. or because the maxRequestsPerCrawl limit has been reached
126
- */
127
- onSkippedRequest?: SkippedRequestCallback;
128
- }
129
- /** The combined options accepted by a crawler context's `enqueueLinks()` helper: `extractLinks()` + `enqueueUrls()`. */
130
- export type EnqueueLinksOptions = ExtractLinksOptions & EnqueueUrlsOptions;
131
- export { EnqueueStrategy };
132
- /** The `strategy` option accepted by {@link ExtractLinksOptions} and {@link EnqueueUrlsOptions}. */
133
- export type EnqueueStrategyOption = EnqueueStrategy | 'all' | 'same-domain' | 'same-hostname' | 'same-origin';
134
- /**
135
- * @internal
136
- * This method helps resolve the baseUrl that will be used for filtering in {@link enqueueLinks}.
137
- * - If a user provides a base url, we always return it
138
- * - If a user specifies {@link EnqueueStrategy.All} strategy, they do not care if the newly found urls are on the original
139
- * request domain, or a redirected one
140
- * - In all other cases, we return the domain of the original request as that's the one we need to use for filtering
141
- */
142
- export declare function resolveBaseUrlForEnqueueLinksFiltering({ enqueueStrategy, finalRequestUrl, originalRequestUrl, userProvidedBaseUrl, }: ResolveBaseUrl): string | undefined;
143
- /**
144
- * @internal
145
- */
146
- export interface ResolveBaseUrl {
147
- userProvidedBaseUrl?: string;
148
- enqueueStrategy?: EnqueueStrategyOption;
149
- originalRequestUrl: string;
150
- finalRequestUrl?: string;
151
- }
152
- /**
153
- * @internal
154
- * Builds the glob patterns a URL must match to satisfy the given enqueue `strategy`, anchored at `baseUrl`.
155
- */
156
- export declare function buildEnqueueStrategyPatterns(baseUrl: string, strategy: EnqueueStrategyOption): UrlPatternObject[];