@crawlee/core 4.0.0-beta.99 → 4.0.0-rc.0

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 (109) hide show
  1. package/autoscaling/autoscaled_pool.d.ts +3 -21
  2. package/autoscaling/autoscaled_pool.js +105 -97
  3. package/autoscaling/concurrency_system.d.ts +7 -22
  4. package/autoscaling/concurrency_system.js +112 -100
  5. package/autoscaling/cpu_load_signal.d.ts +1 -2
  6. package/autoscaling/cpu_load_signal.js +11 -10
  7. package/autoscaling/event_loop_load_signal.d.ts +1 -4
  8. package/autoscaling/event_loop_load_signal.js +18 -18
  9. package/autoscaling/index.d.ts +1 -1
  10. package/autoscaling/index.js +1 -1
  11. package/autoscaling/load_signal.d.ts +8 -7
  12. package/autoscaling/load_signal.js +14 -12
  13. package/autoscaling/memory_load_signal.d.ts +3 -12
  14. package/autoscaling/memory_load_signal.js +41 -41
  15. package/autoscaling/snapshotter.d.ts +7 -10
  16. package/autoscaling/snapshotter.js +18 -18
  17. package/autoscaling/{client_load_signal.d.ts → storage_backend_load_signal.d.ts} +14 -17
  18. package/autoscaling/{client_load_signal.js → storage_backend_load_signal.js} +27 -27
  19. package/autoscaling/system_status.d.ts +9 -11
  20. package/autoscaling/system_status.js +13 -13
  21. package/configuration.d.ts +16 -16
  22. package/configuration.js +6 -6
  23. package/crawlers/context_pipeline.js +6 -6
  24. package/crawlers/crawler_commons.d.ts +14 -112
  25. package/crawlers/crawler_commons.js +1 -107
  26. package/crawlers/error_snapshotter.d.ts +2 -4
  27. package/crawlers/error_snapshotter.js +5 -5
  28. package/crawlers/index.d.ts +1 -1
  29. package/crawlers/index.js +0 -1
  30. package/crawlers/statistics.d.ts +164 -45
  31. package/crawlers/statistics.js +339 -153
  32. package/debug.js +4 -4
  33. package/enqueue_links/enqueue_links.d.ts +51 -159
  34. package/enqueue_links/enqueue_links.js +38 -231
  35. package/enqueue_links/shared.d.ts +34 -24
  36. package/enqueue_links/shared.js +54 -36
  37. package/errors.d.ts +43 -12
  38. package/errors.js +48 -9
  39. package/events/event_manager.d.ts +2 -2
  40. package/events/event_manager.js +7 -6
  41. package/events/local_event_manager.d.ts +1 -1
  42. package/events/local_event_manager.js +5 -5
  43. package/http.d.ts +9 -0
  44. package/http.js +28 -0
  45. package/index.d.ts +1 -1
  46. package/index.js +1 -1
  47. package/log.js +5 -1
  48. package/memory-storage/memory-storage.d.ts +3 -11
  49. package/memory-storage/memory-storage.js +42 -48
  50. package/memory-storage/resource-clients/dataset.d.ts +1 -1
  51. package/memory-storage/resource-clients/dataset.js +8 -13
  52. package/memory-storage/resource-clients/key-value-store.d.ts +1 -1
  53. package/memory-storage/resource-clients/key-value-store.js +36 -38
  54. package/memory-storage/resource-clients/request-queue.d.ts +4 -23
  55. package/memory-storage/resource-clients/request-queue.js +68 -80
  56. package/owned_or_injected.d.ts +1 -3
  57. package/owned_or_injected.js +17 -17
  58. package/package.json +8 -9
  59. package/proxy_configuration.d.ts +1 -3
  60. package/proxy_configuration.js +18 -14
  61. package/recoverable_state.d.ts +80 -45
  62. package/recoverable_state.js +163 -75
  63. package/request.d.ts +3 -4
  64. package/request.js +57 -44
  65. package/router.d.ts +6 -9
  66. package/router.js +23 -23
  67. package/serialization.js +14 -13
  68. package/service_locator.d.ts +19 -10
  69. package/service_locator.js +57 -48
  70. package/session_pool/session.d.ts +3 -14
  71. package/session_pool/session.js +76 -74
  72. package/session_pool/session_pool.d.ts +6 -12
  73. package/session_pool/session_pool.js +85 -77
  74. package/storages/batched_adds.d.ts +37 -0
  75. package/storages/batched_adds.js +73 -0
  76. package/storages/dataset.d.ts +12 -1
  77. package/storages/dataset.js +133 -32
  78. package/storages/index.d.ts +2 -1
  79. package/storages/index.js +2 -1
  80. package/storages/key_value_store.d.ts +19 -4
  81. package/storages/key_value_store.js +204 -75
  82. package/storages/request_dedup_cache.d.ts +1 -2
  83. package/storages/request_dedup_cache.js +9 -9
  84. package/storages/request_list.d.ts +4 -23
  85. package/storages/request_list.js +103 -96
  86. package/storages/request_loader.d.ts +5 -0
  87. package/storages/request_manager_tandem.d.ts +1 -10
  88. package/storages/request_manager_tandem.js +27 -27
  89. package/storages/request_queue.d.ts +21 -18
  90. package/storages/request_queue.js +318 -177
  91. package/storages/sitemap_request_loader.d.ts +22 -70
  92. package/storages/sitemap_request_loader.js +122 -116
  93. package/storages/storage_instance_manager.d.ts +1 -2
  94. package/storages/storage_instance_manager.js +17 -17
  95. package/storages/storage_stats.d.ts +1 -1
  96. package/storages/storage_stats.js +4 -4
  97. package/storages/throttling_request_manager.d.ts +239 -0
  98. package/storages/throttling_request_manager.js +646 -0
  99. package/storages/transaction.d.ts +252 -0
  100. package/storages/transaction.js +251 -0
  101. package/storages/utils.d.ts +6 -3
  102. package/storages/utils.js +11 -2
  103. package/system-info/runtime.js +7 -7
  104. package/url.d.ts +9 -0
  105. package/url.js +11 -0
  106. package/validators.d.ts +22 -25
  107. package/validators.js +13 -25
  108. package/storages/access_checking.d.ts +0 -12
  109. package/storages/access_checking.js +0 -17
@@ -1,23 +1,27 @@
1
- import type { Awaitable } from '@crawlee/types';
1
+ import type { Awaitable, Dictionary } from '@crawlee/types';
2
+ import { z } from 'zod';
2
3
  import type { RequestOptions } from '../request.js';
3
- import type { EnqueueLinksOptions } from './enqueue_links.js';
4
- export { tryAbsoluteURL } from '@crawlee/utils';
5
- export type UrlPatternObject = {
4
+ import type { EnqueueStrategyOption } from './enqueue_links.js';
5
+ export { tryAbsoluteURL } from '@crawlee/utils/internal';
6
+ export interface UrlPatternObject {
6
7
  glob?: string;
7
8
  regexp?: RegExp;
8
- } & Pick<RequestOptions, 'method' | 'payload' | 'label' | 'userData' | 'headers'>;
9
- export type PseudoUrlObject = {
10
- purl: string;
11
- } & Pick<RequestOptions, 'method' | 'payload' | 'label' | 'userData' | 'headers'>;
12
- export type PseudoUrlInput = string | PseudoUrlObject;
13
- export type GlobObject = {
9
+ }
10
+ export interface GlobObject {
14
11
  glob: string;
15
- } & Pick<RequestOptions, 'method' | 'payload' | 'label' | 'userData' | 'headers'>;
12
+ }
16
13
  export type GlobInput = string | GlobObject;
17
- export type RegExpObject = {
14
+ export interface RegExpObject {
18
15
  regexp: RegExp;
19
- } & Pick<RequestOptions, 'method' | 'payload' | 'label' | 'userData' | 'headers'>;
16
+ }
20
17
  export type RegExpInput = RegExp | RegExpObject;
18
+ /** Unified URL pattern input — accepts glob strings, glob objects, RegExp instances, or regexp objects. */
19
+ export type UrlPatternInput = GlobInput | RegExpInput;
20
+ /**
21
+ * Accepts one {@link UrlPatternInput} — a glob string, a RegExp instance, or a `{ glob }` / `{ regexp }` object.
22
+ * @internal
23
+ */
24
+ export declare const urlPatternSchema: z.ZodType<UrlPatternInput>;
21
25
  export type SkippedRequestReason = 'robotsTxt' | 'limit' | 'enqueueLimit' | 'filters' | 'transform' | 'redirect' | 'depth';
22
26
  export type SkippedRequestCallback = (args: {
23
27
  url: string;
@@ -26,13 +30,7 @@ export type SkippedRequestCallback = (args: {
26
30
  /**
27
31
  * @ignore
28
32
  */
29
- export declare function updateEnqueueLinksPatternCache(item: GlobInput | RegExpInput | PseudoUrlInput, pattern: RegExpObject | GlobObject): void;
30
- /**
31
- * Helper factory used in the `enqueueLinks()` and enqueueLinksByClickingElements() function
32
- * to construct RegExps from PseudoUrl strings.
33
- * @ignore
34
- */
35
- export declare function constructRegExpObjectsFromPseudoUrls(pseudoUrls: readonly PseudoUrlInput[]): RegExpObject[];
33
+ export declare function updateEnqueueLinksPatternCache(item: GlobInput | RegExpInput, pattern: RegExpObject | GlobObject): void;
36
34
  /**
37
35
  * Helper factory used in the `enqueueLinks()` and enqueueLinksByClickingElements() function
38
36
  * to construct Glob objects from Glob pattern strings.
@@ -50,17 +48,29 @@ export declare function validateGlobPattern(glob: string): string;
50
48
  */
51
49
  export declare function constructRegExpObjectsFromRegExps(regexps: readonly RegExpInput[]): RegExpObject[];
52
50
  /**
53
- * Filters request options by URL patterns and merges pattern-level options (label, userData, method, payload, headers)
54
- * from the first matching pattern into each RequestOptions entry.
51
+ * Helper factory used in the `enqueueLinks()` function to construct UrlPatternObjects
52
+ * from a mixed array of glob strings, glob objects, RegExp instances, and regexp objects.
53
+ * @ignore
54
+ */
55
+ export declare function constructUrlPatternObjects(patterns: readonly UrlPatternInput[]): UrlPatternObject[];
56
+ /**
57
+ * Filters request options by URL patterns.
55
58
  *
56
59
  * When `includePatterns` is empty/undefined, all options pass through (only exclude filtering applies).
57
60
  * @ignore
58
61
  */
59
- export declare function filterRequestOptionsByPatterns(requestOptions: RequestOptions[], includePatterns: UrlPatternObject[] | undefined, excludePatterns?: UrlPatternObject[], strategy?: EnqueueLinksOptions['strategy'], onSkippedUrl?: (url: string) => void): RequestOptions[];
62
+ export declare function filterRequestOptionsByPatterns(requestOptions: RequestOptions[], includePatterns: UrlPatternObject[] | undefined, excludePatterns?: UrlPatternObject[], strategy?: EnqueueStrategyOption, onSkippedUrl?: (url: string) => void): RequestOptions[];
60
63
  /**
61
64
  * @ignore
62
65
  */
63
- export declare function createRequestOptions(sources: readonly (string | Record<string, unknown>)[], options?: Pick<EnqueueLinksOptions, 'label' | 'userData' | 'baseUrl' | 'skipNavigation' | 'sessionId' | 'strategy'>): RequestOptions[];
66
+ export declare function createRequestOptions(sources: readonly (string | Record<string, unknown>)[], options?: {
67
+ label?: string;
68
+ userData?: Dictionary;
69
+ baseUrl?: string;
70
+ skipNavigation?: boolean;
71
+ sessionId?: string;
72
+ strategy?: EnqueueStrategyOption;
73
+ }): RequestOptions[];
64
74
  /**
65
75
  * Takes a {@link RequestOptions} object and changes its attributes in a desired way. This user-function is used
66
76
  * by {@link enqueueLinks} to modify request options before they are converted to {@link Request} instances.
@@ -1,15 +1,25 @@
1
1
  import { URL } from 'node:url';
2
2
  import { Minimatch } from 'minimatch';
3
- import { purlToRegExp } from '@apify/pseudo_url';
4
- export { tryAbsoluteURL } from '@crawlee/utils';
3
+ import { z } from 'zod';
4
+ import { schemas } from '../validators.js';
5
+ export { tryAbsoluteURL } from '@crawlee/utils/internal';
5
6
  const MAX_ENQUEUE_LINKS_CACHE_SIZE = 1000;
6
7
  /**
7
- * To enable direct use of the Actor UI `globs`/`regexps`/`pseudoUrls` output while keeping high performance,
8
- * all the regexps from the output are only constructed once and kept in a cache
9
- * by the `enqueueLinks()` function.
8
+ * To keep high performance when the same patterns are passed on every `enqueueLinks()` call,
9
+ * each glob/regexp is only compiled once and then kept in a cache.
10
10
  * @ignore
11
11
  */
12
12
  const enqueueLinksPatternCache = new Map();
13
+ /**
14
+ * Accepts one {@link UrlPatternInput} — a glob string, a RegExp instance, or a `{ glob }` / `{ regexp }` object.
15
+ * @internal
16
+ */
17
+ export const urlPatternSchema = z.union([
18
+ z.string(),
19
+ z.instanceof(RegExp),
20
+ schemas.objectWithKeys(['glob']),
21
+ schemas.objectWithKeys(['regexp']),
22
+ ]);
13
23
  /**
14
24
  * @ignore
15
25
  */
@@ -20,28 +30,6 @@ export function updateEnqueueLinksPatternCache(item, pattern) {
20
30
  enqueueLinksPatternCache.delete(key);
21
31
  }
22
32
  }
23
- /**
24
- * Helper factory used in the `enqueueLinks()` and enqueueLinksByClickingElements() function
25
- * to construct RegExps from PseudoUrl strings.
26
- * @ignore
27
- */
28
- export function constructRegExpObjectsFromPseudoUrls(pseudoUrls) {
29
- return pseudoUrls.map((item) => {
30
- // Get pseudoUrl object from cache.
31
- let regexpObject = enqueueLinksPatternCache.get(item);
32
- if (regexpObject)
33
- return regexpObject;
34
- if (typeof item === 'string') {
35
- regexpObject = { regexp: purlToRegExp(item) };
36
- }
37
- else {
38
- const { purl, ...requestOptions } = item;
39
- regexpObject = { regexp: purlToRegExp(purl), ...requestOptions };
40
- }
41
- updateEnqueueLinksPatternCache(item, regexpObject);
42
- return regexpObject;
43
- });
44
- }
45
33
  /**
46
34
  * Helper factory used in the `enqueueLinks()` and enqueueLinksByClickingElements() function
47
35
  * to construct Glob objects from Glob pattern strings.
@@ -71,8 +59,7 @@ export function constructGlobObjectsFromGlobs(globs) {
71
59
  globObject = { glob: validateGlobPattern(item) };
72
60
  }
73
61
  else {
74
- const { glob, ...requestOptions } = item;
75
- globObject = { glob: validateGlobPattern(glob), ...requestOptions };
62
+ globObject = { glob: validateGlobPattern(item.glob) };
76
63
  }
77
64
  updateEnqueueLinksPatternCache(item, globObject);
78
65
  return globObject;
@@ -102,15 +89,31 @@ export function constructRegExpObjectsFromRegExps(regexps) {
102
89
  regexpObject = { regexp: item };
103
90
  }
104
91
  else {
105
- regexpObject = item;
92
+ regexpObject = { regexp: item.regexp };
106
93
  }
107
94
  updateEnqueueLinksPatternCache(item, regexpObject);
108
95
  return regexpObject;
109
96
  });
110
97
  }
111
98
  /**
112
- * Filters request options by URL patterns and merges pattern-level options (label, userData, method, payload, headers)
113
- * from the first matching pattern into each RequestOptions entry.
99
+ * Helper factory used in the `enqueueLinks()` function to construct UrlPatternObjects
100
+ * from a mixed array of glob strings, glob objects, RegExp instances, and regexp objects.
101
+ * @ignore
102
+ */
103
+ export function constructUrlPatternObjects(patterns) {
104
+ const result = [];
105
+ for (const item of patterns) {
106
+ if (typeof item === 'string' || 'glob' in item) {
107
+ result.push(...constructGlobObjectsFromGlobs([item]));
108
+ }
109
+ else if (item instanceof RegExp || 'regexp' in item) {
110
+ result.push(...constructRegExpObjectsFromRegExps([item]));
111
+ }
112
+ }
113
+ return result;
114
+ }
115
+ /**
116
+ * Filters request options by URL patterns.
114
117
  *
115
118
  * When `includePatterns` is empty/undefined, all options pass through (only exclude filtering applies).
116
119
  * @ignore
@@ -130,9 +133,9 @@ export function filterRequestOptionsByPatterns(requestOptions, includePatterns,
130
133
  if (!includeMatchers) {
131
134
  return { ...opts, enqueueStrategy: strategy };
132
135
  }
133
- for (const { match, glob, regexp, ...patternOptions } of includeMatchers) {
136
+ for (const { match } of includeMatchers) {
134
137
  if (match(opts.url)) {
135
- return { ...opts, ...patternOptions, enqueueStrategy: strategy };
138
+ return { ...opts, enqueueStrategy: strategy };
136
139
  }
137
140
  }
138
141
  // didn't match any positive pattern
@@ -141,6 +144,16 @@ export function filterRequestOptionsByPatterns(requestOptions, includePatterns,
141
144
  })
142
145
  .filter((opts) => opts !== null);
143
146
  }
147
+ function isAbsoluteUrl(url) {
148
+ try {
149
+ // eslint-disable-next-line no-new
150
+ new URL(url);
151
+ return true;
152
+ }
153
+ catch {
154
+ return false;
155
+ }
156
+ }
144
157
  /**
145
158
  * @ignore
146
159
  */
@@ -158,7 +171,12 @@ export function createRequestOptions(sources, options = {}) {
158
171
  }
159
172
  })
160
173
  .map((requestOptions) => {
161
- requestOptions.url = new URL(requestOptions.url, options.baseUrl).href;
174
+ // Leave already-absolute URLs untouched - re-deriving them via `new URL()` would normalize them
175
+ // (e.g. adding a trailing slash to a bare domain), which is surprising for URLs that didn't need
176
+ // resolving against `baseUrl` in the first place.
177
+ if (!isAbsoluteUrl(requestOptions.url)) {
178
+ requestOptions.url = new URL(requestOptions.url, options.baseUrl).href;
179
+ }
162
180
  requestOptions.userData ??= options.userData ?? {};
163
181
  if (typeof options.label === 'string') {
164
182
  requestOptions.userData = {
@@ -191,7 +209,7 @@ function createPatternObjectMatcher(urlPatternObject) {
191
209
  else {
192
210
  match = () => false;
193
211
  }
194
- return { ...urlPatternObject, match };
212
+ return { match };
195
213
  }
196
214
  /**
197
215
  * Applies a {@link RequestTransform} function to a list of request options.
package/errors.d.ts CHANGED
@@ -14,6 +14,16 @@ export declare class CriticalError extends NonRetryableError {
14
14
  */
15
15
  export declare class MissingRouteError extends CriticalError {
16
16
  }
17
+ /**
18
+ * A schema validation issue, structurally compatible with `StandardSchemaV1.Issue`. Declared here so that
19
+ * error types do not have to depend on `@standard-schema/spec`.
20
+ */
21
+ export interface SchemaIssue {
22
+ readonly message: string;
23
+ readonly path?: readonly (PropertyKey | {
24
+ key: PropertyKey;
25
+ })[];
26
+ }
17
27
  /**
18
28
  * Thrown when a request's `userData` does not match the {@link RouteSchemas|Standard Schema} registered for its label.
19
29
  *
@@ -21,18 +31,19 @@ export declare class MissingRouteError extends CriticalError {
21
31
  */
22
32
  export declare class RequestValidationError extends NonRetryableError {
23
33
  readonly label: string | symbol;
24
- readonly issues: readonly {
25
- readonly message: string;
26
- readonly path?: readonly (PropertyKey | {
27
- key: PropertyKey;
28
- })[];
29
- }[];
30
- constructor(label: string | symbol, issues: readonly {
31
- readonly message: string;
32
- readonly path?: readonly (PropertyKey | {
33
- key: PropertyKey;
34
- })[];
35
- }[]);
34
+ readonly issues: readonly SchemaIssue[];
35
+ constructor(label: string | symbol, issues: readonly SchemaIssue[]);
36
+ }
37
+ /**
38
+ * Thrown by {@link RecoverableState} when a persisted state record does not match its `stateSchema`.
39
+ *
40
+ * Whether a corrupt record should abort the run or be discarded in favour of the defaults depends on what the
41
+ * state is for, so {@link RecoverableState.initialize} always throws and leaves the choice to the caller.
42
+ */
43
+ export declare class StateValidationError extends Error {
44
+ readonly persistStateKey: string;
45
+ readonly issues: readonly SchemaIssue[];
46
+ constructor(persistStateKey: string, issues: readonly SchemaIssue[]);
36
47
  }
37
48
  /**
38
49
  * Errors of `RetryRequestError` type will always be retried by the crawler.
@@ -42,6 +53,26 @@ export declare class RequestValidationError extends NonRetryableError {
42
53
  export declare class RetryRequestError extends Error {
43
54
  constructor(message?: string);
44
55
  }
56
+ /**
57
+ * Thrown when a domain has rate-limited us and the request should simply be attempted again later.
58
+ *
59
+ * The request is reclaimed without recording a failure: it costs neither a retry nor session reputation, because
60
+ * nothing about the request or the session was at fault. A {@link ThrottlingRequestManager} holds it back until
61
+ * the domain's backoff expires, so retries are paced rather than immediate.
62
+ */
63
+ export declare class RequestThrottledError extends RetryRequestError {
64
+ constructor(message?: string);
65
+ }
66
+ /**
67
+ * Thrown when a domain has rate-limited us for so long that no request has got through, and the crawl is
68
+ * abandoned rather than kept waiting.
69
+ *
70
+ * Waiting longer will not help: at this point the concurrency is too high for the domain, or it has blocked us.
71
+ * The affected requests are deliberately left in their queue, so re-running the crawl without purging storages
72
+ * resumes them once the domain recovers.
73
+ */
74
+ export declare class PersistentRateLimitError extends CriticalError {
75
+ }
45
76
  /**
46
77
  * Errors of `SessionError` type retire the session associated with the request and trigger a regular retry.
47
78
  *
package/errors.js CHANGED
@@ -15,6 +15,16 @@ export class CriticalError extends NonRetryableError {
15
15
  */
16
16
  export class MissingRouteError extends CriticalError {
17
17
  }
18
+ function formatIssues(issues) {
19
+ return issues
20
+ .map((issue) => {
21
+ const path = (issue.path ?? [])
22
+ .map((segment) => (typeof segment === 'object' ? segment.key : segment))
23
+ .join('.');
24
+ return `- ${path ? `${path}: ` : ''}${issue.message}`;
25
+ })
26
+ .join('\n');
27
+ }
18
28
  /**
19
29
  * Thrown when a request's `userData` does not match the {@link RouteSchemas|Standard Schema} registered for its label.
20
30
  *
@@ -24,19 +34,26 @@ export class RequestValidationError extends NonRetryableError {
24
34
  label;
25
35
  issues;
26
36
  constructor(label, issues) {
27
- const details = issues
28
- .map((issue) => {
29
- const path = (issue.path ?? [])
30
- .map((segment) => (typeof segment === 'object' ? segment.key : segment))
31
- .join('.');
32
- return `- ${path ? `${path}: ` : ''}${issue.message}`;
33
- })
34
- .join('\n');
35
- super(`Request userData for label '${String(label)}' failed schema validation:\n${details}`);
37
+ super(`Request userData for label '${String(label)}' failed schema validation:\n${formatIssues(issues)}`);
36
38
  this.label = label;
37
39
  this.issues = issues;
38
40
  }
39
41
  }
42
+ /**
43
+ * Thrown by {@link RecoverableState} when a persisted state record does not match its `stateSchema`.
44
+ *
45
+ * Whether a corrupt record should abort the run or be discarded in favour of the defaults depends on what the
46
+ * state is for, so {@link RecoverableState.initialize} always throws and leaves the choice to the caller.
47
+ */
48
+ export class StateValidationError extends Error {
49
+ persistStateKey;
50
+ issues;
51
+ constructor(persistStateKey, issues) {
52
+ super(`State persisted under key '${persistStateKey}' failed schema validation:\n${formatIssues(issues)}`);
53
+ this.persistStateKey = persistStateKey;
54
+ this.issues = issues;
55
+ }
56
+ }
40
57
  /**
41
58
  * Errors of `RetryRequestError` type will always be retried by the crawler.
42
59
  *
@@ -47,6 +64,28 @@ export class RetryRequestError extends Error {
47
64
  super(message ?? "Request is being retried at the user's request");
48
65
  }
49
66
  }
67
+ /**
68
+ * Thrown when a domain has rate-limited us and the request should simply be attempted again later.
69
+ *
70
+ * The request is reclaimed without recording a failure: it costs neither a retry nor session reputation, because
71
+ * nothing about the request or the session was at fault. A {@link ThrottlingRequestManager} holds it back until
72
+ * the domain's backoff expires, so retries are paced rather than immediate.
73
+ */
74
+ export class RequestThrottledError extends RetryRequestError {
75
+ constructor(message) {
76
+ super(message ?? 'Request is being retried later because its domain is rate-limiting us');
77
+ }
78
+ }
79
+ /**
80
+ * Thrown when a domain has rate-limited us for so long that no request has got through, and the crawl is
81
+ * abandoned rather than kept waiting.
82
+ *
83
+ * Waiting longer will not help: at this point the concurrency is too high for the domain, or it has blocked us.
84
+ * The affected requests are deliberately left in their queue, so re-running the crawl without purging storages
85
+ * resumes them once the domain recovers.
86
+ */
87
+ export class PersistentRateLimitError extends CriticalError {
88
+ }
50
89
  /**
51
90
  * Errors of `SessionError` type retire the session associated with the request and trigger a regular retry.
52
91
  *
@@ -4,7 +4,7 @@ export interface EventManagerOptions {
4
4
  /** Interval between emitted `persistState` events in milliseconds. */
5
5
  persistStateIntervalMillis: number;
6
6
  }
7
- export declare const enum EventType {
7
+ export declare enum EventType {
8
8
  PERSIST_STATE = "persistState",
9
9
  SYSTEM_INFO = "systemInfo",
10
10
  MIGRATING = "migrating",
@@ -41,12 +41,12 @@ interface Intervals {
41
41
  systemInfo?: BetterIntervalID;
42
42
  }
43
43
  export declare abstract class EventManager {
44
+ #private;
44
45
  protected events: AsyncEventEmitter<{}>;
45
46
  protected initialized: boolean;
46
47
  protected intervals: Intervals;
47
48
  // @ts-ignore optional peer dependency or compatibility with es2022
48
49
  protected log: import("@crawlee/types").CrawleeLogger;
49
- private persistStateIntervalMillis;
50
50
  constructor(options: EventManagerOptions);
51
51
  /**
52
52
  * Initializes the event manager by starting the `persistState` event interval.
@@ -15,10 +15,11 @@ export class EventManager {
15
15
  initialized = false;
16
16
  intervals = {};
17
17
  log = serviceLocator.getLogger().child({ prefix: 'Events' });
18
- persistStateIntervalMillis;
18
+ #persistStateIntervalMillis;
19
19
  constructor(options) {
20
- this.persistStateIntervalMillis = options.persistStateIntervalMillis;
21
- this.events.setMaxListeners(50);
20
+ this.#persistStateIntervalMillis = options.persistStateIntervalMillis;
21
+ // One MIGRATING listener per RequestQueue, and ThrottlingRequestManager opens one per domain.
22
+ this.events.setMaxListeners(150);
22
23
  }
23
24
  /**
24
25
  * Initializes the event manager by starting the `persistState` event interval.
@@ -29,9 +30,9 @@ export class EventManager {
29
30
  return;
30
31
  }
31
32
  this.intervals.persistState = betterSetInterval((intervalCallback) => {
32
- this.emit("persistState" /* EventType.PERSIST_STATE */, { isMigrating: false });
33
+ this.emit(EventType.PERSIST_STATE, { isMigrating: false });
33
34
  intervalCallback();
34
- }, this.persistStateIntervalMillis);
35
+ }, this.#persistStateIntervalMillis);
35
36
  this.initialized = true;
36
37
  }
37
38
  /**
@@ -45,7 +46,7 @@ export class EventManager {
45
46
  betterClearInterval(this.intervals.persistState);
46
47
  this.initialized = false;
47
48
  // Emit final PERSIST_STATE event
48
- this.emit("persistState" /* EventType.PERSIST_STATE */, { isMigrating: false });
49
+ this.emit(EventType.PERSIST_STATE, { isMigrating: false });
49
50
  // Wait for PERSIST_STATE to process
50
51
  await this.waitForAllListenersToComplete();
51
52
  }
@@ -5,7 +5,7 @@ export interface LocalEventManagerOptions extends EventManagerOptions {
5
5
  systemInfoIntervalMillis: number;
6
6
  }
7
7
  export declare class LocalEventManager extends EventManager {
8
- private systemInfoIntervalMillis;
8
+ #private;
9
9
  constructor(options: LocalEventManagerOptions);
10
10
  /**
11
11
  * Creates a new `LocalEventManager` based on the provided `Configuration`.
@@ -1,11 +1,11 @@
1
1
  import { betterClearInterval, betterSetInterval } from '@apify/utilities';
2
2
  import { serviceLocator } from '../service_locator.js';
3
- import { EventManager } from './event_manager.js';
3
+ import { EventManager, EventType } from './event_manager.js';
4
4
  export class LocalEventManager extends EventManager {
5
- systemInfoIntervalMillis;
5
+ #systemInfoIntervalMillis;
6
6
  constructor(options) {
7
7
  super(options);
8
- this.systemInfoIntervalMillis = options.systemInfoIntervalMillis;
8
+ this.#systemInfoIntervalMillis = options.systemInfoIntervalMillis;
9
9
  }
10
10
  /**
11
11
  * Creates a new `LocalEventManager` based on the provided `Configuration`.
@@ -28,7 +28,7 @@ export class LocalEventManager extends EventManager {
28
28
  }
29
29
  await super.init();
30
30
  this.emitSystemInfoEvent = this.emitSystemInfoEvent.bind(this);
31
- this.intervals.systemInfo = betterSetInterval(this.emitSystemInfoEvent.bind(this), this.systemInfoIntervalMillis);
31
+ this.intervals.systemInfo = betterSetInterval(this.emitSystemInfoEvent.bind(this), this.#systemInfoIntervalMillis);
32
32
  }
33
33
  /**
34
34
  * @inheritDoc
@@ -47,7 +47,7 @@ export class LocalEventManager extends EventManager {
47
47
  const info = await this.createSystemInfo({
48
48
  maxUsedCpuRatio: serviceLocator.getConfiguration().maxUsedCpuRatio,
49
49
  });
50
- this.events.emit("systemInfo" /* EventType.SYSTEM_INFO */, info);
50
+ this.events.emit(EventType.SYSTEM_INFO, info);
51
51
  intervalCallback();
52
52
  }
53
53
  /**
package/http.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Parses a `Retry-After` response header into a delay in milliseconds.
3
+ *
4
+ * The header holds either a non-negative number of seconds or an HTTP-date.
5
+ * See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Retry-After).
6
+ *
7
+ * @returns The delay in milliseconds, or `null` if the header is absent, unparseable, or already elapsed.
8
+ */
9
+ export declare function parseRetryAfterHeader(value?: string | null): number | null;
package/http.js ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Parses a `Retry-After` response header into a delay in milliseconds.
3
+ *
4
+ * The header holds either a non-negative number of seconds or an HTTP-date.
5
+ * See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Retry-After).
6
+ *
7
+ * @returns The delay in milliseconds, or `null` if the header is absent, unparseable, or already elapsed.
8
+ */
9
+ export function parseRetryAfterHeader(value) {
10
+ if (!value) {
11
+ return null;
12
+ }
13
+ const trimmed = value.trim();
14
+ // Per the spec this is a `delay-seconds`: digits only, so a negative or fractional value is not one.
15
+ if (/^\d+$/.test(trimmed)) {
16
+ // `Retry-After: 0` names no future deadline, same as an HTTP-date that has already passed. Reporting it
17
+ // as a zero delay would leave the domain unthrottled while still counting as a rate-limit event, so the
18
+ // caller would defer the request for free and re-send it immediately.
19
+ const delayMs = Number(trimmed) * 1000;
20
+ return delayMs > 0 ? delayMs : null;
21
+ }
22
+ const date = Date.parse(trimmed);
23
+ if (!Number.isNaN(date)) {
24
+ const delayMs = date - Date.now();
25
+ return delayMs > 0 ? delayMs : null;
26
+ }
27
+ return null;
28
+ }
package/index.d.ts CHANGED
@@ -17,6 +17,6 @@ export * from './storages/index.js';
17
17
  export * from './memory-storage/index.js';
18
18
  export * from './validators.js';
19
19
  export * from './cookie_utils.js';
20
+ export * from './http.js';
20
21
  export * from './recoverable_state.js';
21
- export { PseudoUrl } from '@apify/pseudo_url';
22
22
  export type { StorageBackend } from '@crawlee/types';
package/index.js CHANGED
@@ -17,5 +17,5 @@ export * from './storages/index.js';
17
17
  export * from './memory-storage/index.js';
18
18
  export * from './validators.js';
19
19
  export * from './cookie_utils.js';
20
+ export * from './http.js';
20
21
  export * from './recoverable_state.js';
21
- export { PseudoUrl } from '@apify/pseudo_url';
package/log.js CHANGED
@@ -30,6 +30,8 @@ import log, { Log, Logger, LoggerJson, LoggerText, LogLevel } from '@apify/log';
30
30
  * ```
31
31
  */
32
32
  export class BaseCrawleeLogger {
33
+ // kept as TS-private: the adaptive crawler's log proxy calls non-intercepted methods with `this === proxy`,
34
+ // where `#`-field access would throw at runtime
33
35
  options;
34
36
  warningsLogged = new Set();
35
37
  constructor(options = {}) {
@@ -87,7 +89,9 @@ export class BaseCrawleeLogger {
87
89
  */
88
90
  export class ApifyLogAdapter extends BaseCrawleeLogger {
89
91
  apifyLog;
90
- constructor(apifyLog, options) {
92
+ constructor(
93
+ // kept as a TS-private parameter property: reached through the adaptive crawler's log proxy, see above
94
+ apifyLog, options) {
91
95
  super(options ?? {});
92
96
  this.apifyLog = apifyLog;
93
97
  }
@@ -10,12 +10,8 @@ export interface MemoryStorageOptions {
10
10
  logger?: CrawleeLogger;
11
11
  }
12
12
  export declare class MemoryStorageBackend implements storage.StorageBackend {
13
+ #private;
13
14
  readonly logger?: CrawleeLogger;
14
- /**
15
- * Unique per-instance cache partition key. Mirrors the way `FileSystemStorageBackend` partitions its
16
- * cache by storage directory: two distinct `MemoryStorageBackend` instances must not share cached backends.
17
- */
18
- private readonly instanceCacheKey;
19
15
  readonly keyValueStoreBackendCache: KeyValueStoreBackend[];
20
16
  readonly datasetBackendCache: DatasetBackend[];
21
17
  readonly requestQueueBackendCache: RequestQueueBackend[];
@@ -25,17 +21,13 @@ export declare class MemoryStorageBackend implements storage.StorageBackend {
25
21
  * cache partitions in the storage backend cache.
26
22
  */
27
23
  getStorageBackendCacheKey(): string;
28
- private static resolveStorageKey;
29
24
  createDatasetBackend(options?: storage.StorageIdentifier): Promise<storage.DatasetBackend>;
30
25
  createKeyValueStoreBackend(options?: storage.StorageIdentifier): Promise<storage.KeyValueStoreBackend>;
31
26
  createRequestQueueBackend(options?: storage.StorageIdentifier): Promise<RequestQueueBackend>;
32
27
  storageExists(id: string, type: 'Dataset' | 'KeyValueStore' | 'RequestQueue'): Promise<boolean>;
33
28
  /**
34
- * Cleans up the default storages before the run starts. For the in-memory storage this simply
35
- * resets the in-memory state of the cached default dataset, key-value store and request queue.
36
- *
37
- * As with `FileSystemStorageBackend`, the run's input (the `INPUT` key in the default key-value
38
- * store) is preserved — only the rest of the default storages is cleared.
29
+ * Cleans up the run-scoped storages before the run starts. For the in-memory storage this simply
30
+ * resets the in-memory state of the cached backends.
39
31
  */
40
32
  purge(): Promise<void>;
41
33
  /**