@crawlee/core 4.0.0-beta.126 → 4.0.0-beta.128

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.
@@ -1,4 +1,5 @@
1
1
  import type { Dictionary } from '@crawlee/types';
2
+ import { EnqueueStrategy } from '@crawlee/utils';
2
3
  import type { RequestQueueOperationOptions } from '../storages/request_queue.js';
3
4
  import type { RequestTransform, SkippedRequestCallback, UrlPatternInput, UrlPatternObject } from './shared.js';
4
5
  /**
@@ -127,57 +128,7 @@ export interface EnqueueUrlsOptions extends RequestQueueOperationOptions {
127
128
  }
128
129
  /** The combined options accepted by a crawler context's `enqueueLinks()` helper: `extractLinks()` + `enqueueUrls()`. */
129
130
  export type EnqueueLinksOptions = ExtractLinksOptions & EnqueueUrlsOptions;
130
- /**
131
- * The different enqueueing strategies available.
132
- *
133
- * 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:
134
- *
135
- * ```md
136
- * Protocol Domain
137
- * ┌────┐ ┌─────────┐
138
- * https://example.crawlee.dev/...
139
- * │ └─────────────────┤
140
- * │ Hostname │
141
- * │ │
142
- * └─────────────────────────┘
143
- * Origin
144
- *```
145
- *
146
- * - The `Protocol` is usually `http` or `https`
147
- * - The `Domain` represents the path without any possible subdomains to a website. For example, `crawlee.dev` is the domain of `https://example.crawlee.dev/`
148
- * - The `Hostname` is the full path to a website, including any subdomains. For example, `example.crawlee.dev` is the hostname of `https://example.crawlee.dev/`
149
- * - The `Origin` is the combination of the `Protocol` and `Hostname`. For example, `https://example.crawlee.dev` is the origin of `https://example.crawlee.dev/`
150
- */
151
- export declare enum EnqueueStrategy {
152
- /**
153
- * Matches any URLs found
154
- */
155
- All = "all",
156
- /**
157
- * Matches any URLs that have the same hostname.
158
- * For example, `https://wow.example.com/hello` will be matched for a base url of `https://wow.example.com/`, but
159
- * `https://example.com/hello` will not be matched.
160
- *
161
- * > This strategy will match both `http` and `https` protocols regardless of the base URL protocol.
162
- */
163
- SameHostname = "same-hostname",
164
- /**
165
- * Matches any URLs that have the same domain as the base URL.
166
- * For example, `https://wow.an.example.com` and `https://example.com` will both be matched for a base url of
167
- * `https://example.com`.
168
- *
169
- * > This strategy will match both `http` and `https` protocols regardless of the base URL protocol.
170
- */
171
- SameDomain = "same-domain",
172
- /**
173
- * Matches any URLs that have the same hostname and protocol.
174
- * For example, `https://wow.example.com/hello` will be matched for a base url of `https://wow.example.com/`, but
175
- * `http://wow.example.com/hello` will not be matched.
176
- *
177
- * > This strategy will ensure the protocol of the base URL is the same as the protocol of the URL to be enqueued.
178
- */
179
- SameOrigin = "same-origin"
180
- }
131
+ export { EnqueueStrategy };
181
132
  /** The `strategy` option accepted by {@link ExtractLinksOptions} and {@link EnqueueUrlsOptions}. */
182
133
  export type EnqueueStrategyOption = EnqueueStrategy | 'all' | 'same-domain' | 'same-hostname' | 'same-origin';
183
134
  /**
@@ -1,56 +1,6 @@
1
+ import { EnqueueStrategy } from '@crawlee/utils';
1
2
  import { getDomain } from 'tldts';
2
- /**
3
- * The different enqueueing strategies available.
4
- *
5
- * 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:
6
- *
7
- * ```md
8
- * Protocol Domain
9
- * ┌────┐ ┌─────────┐
10
- * https://example.crawlee.dev/...
11
- * │ └─────────────────┤
12
- * │ Hostname │
13
- * │ │
14
- * └─────────────────────────┘
15
- * Origin
16
- *```
17
- *
18
- * - The `Protocol` is usually `http` or `https`
19
- * - The `Domain` represents the path without any possible subdomains to a website. For example, `crawlee.dev` is the domain of `https://example.crawlee.dev/`
20
- * - The `Hostname` is the full path to a website, including any subdomains. For example, `example.crawlee.dev` is the hostname of `https://example.crawlee.dev/`
21
- * - The `Origin` is the combination of the `Protocol` and `Hostname`. For example, `https://example.crawlee.dev` is the origin of `https://example.crawlee.dev/`
22
- */
23
- export var EnqueueStrategy;
24
- (function (EnqueueStrategy) {
25
- /**
26
- * Matches any URLs found
27
- */
28
- EnqueueStrategy["All"] = "all";
29
- /**
30
- * Matches any URLs that have the same hostname.
31
- * For example, `https://wow.example.com/hello` will be matched for a base url of `https://wow.example.com/`, but
32
- * `https://example.com/hello` will not be matched.
33
- *
34
- * > This strategy will match both `http` and `https` protocols regardless of the base URL protocol.
35
- */
36
- EnqueueStrategy["SameHostname"] = "same-hostname";
37
- /**
38
- * Matches any URLs that have the same domain as the base URL.
39
- * For example, `https://wow.an.example.com` and `https://example.com` will both be matched for a base url of
40
- * `https://example.com`.
41
- *
42
- * > This strategy will match both `http` and `https` protocols regardless of the base URL protocol.
43
- */
44
- EnqueueStrategy["SameDomain"] = "same-domain";
45
- /**
46
- * Matches any URLs that have the same hostname and protocol.
47
- * For example, `https://wow.example.com/hello` will be matched for a base url of `https://wow.example.com/`, but
48
- * `http://wow.example.com/hello` will not be matched.
49
- *
50
- * > This strategy will ensure the protocol of the base URL is the same as the protocol of the URL to be enqueued.
51
- */
52
- EnqueueStrategy["SameOrigin"] = "same-origin";
53
- })(EnqueueStrategy || (EnqueueStrategy = {}));
3
+ export { EnqueueStrategy };
54
4
  /**
55
5
  * @internal
56
6
  * This method helps resolve the baseUrl that will be used for filtering in {@link enqueueLinks}.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlee/core",
3
- "version": "4.0.0-beta.126",
3
+ "version": "4.0.0-beta.128",
4
4
  "description": "The scalable web crawling and scraping library for JavaScript/Node.js. Enables development of data extraction and web automation jobs (not only) with headless Chrome and Puppeteer.",
5
5
  "engines": {
6
6
  "node": ">=22.0.0"
@@ -52,10 +52,10 @@
52
52
  "@apify/log": "^2.5.18",
53
53
  "@apify/timeout": "^0.4.4",
54
54
  "@apify/utilities": "^2.15.5",
55
- "@crawlee/fs-storage": "4.0.0-beta.126",
56
- "@crawlee/http-client": "4.0.0-beta.126",
57
- "@crawlee/types": "4.0.0-beta.126",
58
- "@crawlee/utils": "4.0.0-beta.126",
55
+ "@crawlee/fs-storage": "4.0.0-beta.128",
56
+ "@crawlee/http-client": "4.0.0-beta.128",
57
+ "@crawlee/types": "4.0.0-beta.128",
58
+ "@crawlee/utils": "4.0.0-beta.128",
59
59
  "@sapphire/async-queue": "^1.5.5",
60
60
  "@standard-schema/spec": "^1.0.0",
61
61
  "@vladfrangu/async_event_emitter": "^2.4.6",
@@ -78,5 +78,5 @@
78
78
  }
79
79
  }
80
80
  },
81
- "gitHead": "5f0a8c1a43d4587f8640881fc55836a23e1c4f30"
81
+ "gitHead": "2c017f04d564e8fa13855bf19f31f43cb65f4f44"
82
82
  }
@@ -181,8 +181,11 @@ export declare class SessionPool implements ISessionPool {
181
181
  /**
182
182
  * Removes listener from `persistState` event.
183
183
  * This function should be called after you are done with using the `SessionPool` instance.
184
+ * @param options - Set `persistState` to false when the final state was already persisted by the event manager.
184
185
  */
185
- teardown(): Promise<void>;
186
+ teardown({ persistState }?: {
187
+ persistState?: boolean;
188
+ }): Promise<void>;
186
189
  /**
187
190
  * Removes retired `Session` instances from `SessionPool`.
188
191
  */
@@ -264,15 +264,18 @@ export class SessionPool {
264
264
  /**
265
265
  * Removes listener from `persistState` event.
266
266
  * This function should be called after you are done with using the `SessionPool` instance.
267
+ * @param options - Set `persistState` to false when the final state was already persisted by the event manager.
267
268
  */
268
- async teardown() {
269
+ async teardown({ persistState = true } = {}) {
269
270
  if (!this.#initPromise)
270
271
  return;
271
272
  await this.ensureInitialized();
272
273
  if (this.#listener) {
273
274
  this.#events.off(EventType.PERSIST_STATE, this.#listener);
274
275
  }
275
- await this.persistState();
276
+ if (persistState) {
277
+ await this.persistState();
278
+ }
276
279
  }
277
280
  /**
278
281
  * Removes retired `Session` instances from `SessionPool`.
@@ -1,5 +1,5 @@
1
1
  import type { BaseHttpClient } from '@crawlee/http-client';
2
- import { type ParseSitemapOptions } from '@crawlee/utils';
2
+ import { EnqueueStrategy, type ParseSitemapOptions } from '@crawlee/utils';
3
3
  import type { UrlPatternInput } from '../enqueue_links/shared.js';
4
4
  import { Request } from '../request.js';
5
5
  import type { IRequestLoader } from './request_loader.js';
@@ -65,6 +65,13 @@ export interface SitemapRequestLoaderOptions extends UrlConstraints {
65
65
  * @default 200
66
66
  */
67
67
  maxBufferSize?: number;
68
+ /**
69
+ * Keep only sitemap-derived URLs matching this strategy relative to the parent sitemap URL; non-`http(s)`
70
+ * schemes are always dropped. The filtering stays enforced after navigation (e.g. across redirects).
71
+ * Pass `'all'` to disable host filtering.
72
+ * @default EnqueueStrategy.SameHostname
73
+ */
74
+ enqueueStrategy?: EnqueueStrategy | `${EnqueueStrategy}`;
68
75
  /**
69
76
  * Advanced options for the underlying `parseSitemap` call.
70
77
  */
@@ -1,5 +1,5 @@
1
1
  import { Transform } from 'node:stream';
2
- import { parseSitemap } from '@crawlee/utils';
2
+ import { EnqueueStrategy, parseSitemap } from '@crawlee/utils';
3
3
  import { minimatch } from 'minimatch';
4
4
  import { z } from 'zod';
5
5
  import { constructUrlPatternObjects, urlPatternSchema } from '../enqueue_links/shared.js';
@@ -16,6 +16,7 @@ const sitemapRequestLoaderOptionsSchema = z.strictObject({
16
16
  signal: z.unknown().optional(),
17
17
  timeoutMillis: schemas.anyNumber.optional(),
18
18
  maxBufferSize: schemas.anyNumber.default(200),
19
+ enqueueStrategy: z.enum(EnqueueStrategy).default(EnqueueStrategy.SameHostname),
19
20
  parseSitemapOptions: z.looseObject({}).optional(),
20
21
  include: schemas.arrayOf(urlPatternSchema, 'URL patterns').optional(),
21
22
  exclude: schemas.arrayOf(urlPatternSchema, 'URL patterns').optional(),
@@ -83,6 +84,10 @@ export class SitemapRequestLoader {
83
84
  * Proxy URL to be used for sitemap loading.
84
85
  */
85
86
  #proxyUrl;
87
+ /**
88
+ * Enqueue strategy applied to sitemap-derived URLs and stamped onto the emitted `Request` objects.
89
+ */
90
+ #enqueueStrategy;
86
91
  /**
87
92
  * Logger instance.
88
93
  */
@@ -94,7 +99,7 @@ export class SitemapRequestLoader {
94
99
  #persistenceOptions;
95
100
  /** @internal */
96
101
  constructor(options) {
97
- const { include, exclude, persistStateKey, persistenceOptions, proxyUrl, maxBufferSize, sitemapUrls } = parseArgument(options, sitemapRequestLoaderOptionsSchema, 'SitemapRequestLoaderOptions');
102
+ const { include, exclude, persistStateKey, persistenceOptions, proxyUrl, maxBufferSize, sitemapUrls, enqueueStrategy, } = parseArgument(options, sitemapRequestLoaderOptionsSchema, 'SitemapRequestLoaderOptions');
98
103
  this.#log = serviceLocator.getLogger().child({ prefix: 'SitemapRequestLoader' });
99
104
  if (exclude?.length) {
100
105
  this.#urlExcludePatternObjects.push(...constructUrlPatternObjects(exclude));
@@ -105,6 +110,7 @@ export class SitemapRequestLoader {
105
110
  this.#persistStateKey = persistStateKey;
106
111
  this.#persistenceOptions = { enable: true, ...persistenceOptions };
107
112
  this.#proxyUrl = proxyUrl;
113
+ this.#enqueueStrategy = enqueueStrategy;
108
114
  this.#urlQueueStream = this.createNewStream(maxBufferSize);
109
115
  this.#sitemapParsingProgress.pendingSitemapUrls = new Set(sitemapUrls);
110
116
  this.#events = serviceLocator.getEventManager();
@@ -213,6 +219,7 @@ export class SitemapRequestLoader {
213
219
  ...parseSitemapOptions,
214
220
  maxDepth: 0,
215
221
  emitNestedSitemaps: true,
222
+ enqueueStrategy: this.#enqueueStrategy,
216
223
  })) {
217
224
  if (!item.originSitemapUrl) {
218
225
  // This is a nested sitemap
@@ -326,14 +333,21 @@ export class SitemapRequestLoader {
326
333
  }
327
334
  // Create a new stream, as we have read all the URLs from the current one.
328
335
  // Pushing the urls back to the original stream might not be possible if it has been ended.
329
- const newStream = this.createNewStream(this.#urlQueueStream.readableHighWaterMark);
336
+ const previousStream = this.#urlQueueStream;
337
+ const newStream = this.createNewStream(previousStream.readableHighWaterMark);
330
338
  for (const url of urlQueue) {
331
339
  newStream.push(url);
332
340
  }
333
- if (this.#urlQueueStream.writableEnded) {
341
+ if (previousStream.writableEnded) {
334
342
  newStream.end();
335
343
  }
336
344
  this.#urlQueueStream = newStream;
345
+ // A `pushNextUrl()` call may be blocked on backpressure, waiting for a `readdata` event on the
346
+ // previous stream. That event is only ever emitted by `readNextUrl()` on the current stream, so
347
+ // after the swap the waiter would never be notified and the background sitemap loading would hang.
348
+ // Re-emit `readdata` on the previous stream to release any such pending waiter (its URL has already
349
+ // been transferred to the new stream above).
350
+ previousStream.emit('readdata');
337
351
  await this.#store.setValue(this.#persistStateKey, {
338
352
  sitemapParsingProgress: {
339
353
  pendingSitemapUrls: Array.from(this.#sitemapParsingProgress.pendingSitemapUrls),
@@ -379,7 +393,7 @@ export class SitemapRequestLoader {
379
393
  }
380
394
  // A restored in-progress request already has its Request data; don't overwrite it.
381
395
  if (!this.#requestData.has(nextUrl)) {
382
- this.#requestData.set(nextUrl, new Request({ url: nextUrl }));
396
+ this.#requestData.set(nextUrl, new Request({ url: nextUrl, enqueueStrategy: this.#enqueueStrategy }));
383
397
  }
384
398
  this.inProgress.add(nextUrl);
385
399
  return this.#requestData.get(nextUrl);
package/storages/utils.js CHANGED
@@ -12,11 +12,20 @@ export async function purgeDefaultStorages(configurationOrOptions, storageBacken
12
12
  const { configuration = serviceLocator.getConfiguration(), onlyPurgeOnce = false } = options;
13
13
  ({ storageBackend = serviceLocator.getStorageBackend() } = options);
14
14
  const casted = storageBackend;
15
+ const runPurge = async () => {
16
+ try {
17
+ await casted.purge?.();
18
+ }
19
+ catch (e) {
20
+ casted.__purged = undefined;
21
+ throw e;
22
+ }
23
+ };
15
24
  // if `onlyPurgeOnce` is true, will purge anytime this function is called, otherwise - only on start
16
25
  if (!onlyPurgeOnce || (configuration.purgeOnStart && !casted.__purged)) {
17
- casted.__purged = true;
18
- await casted.purge?.();
26
+ casted.__purged = runPurge();
19
27
  }
28
+ await casted.__purged;
20
29
  }
21
30
  /**
22
31
  * Easily create and manage state values. All state values are automatically persisted.