@crawlee/core 4.0.0-beta.95 → 4.0.0-beta.97

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.
@@ -24,6 +24,11 @@ export declare const crawleeConfigFields: {
24
24
  memoryMbytes: ConfigField<z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodNumber>>>;
25
25
  /** @default 60_000 */
26
26
  persistStateIntervalMillis: ConfigField<z.ZodDefault<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodNumber>>>;
27
+ /**
28
+ * Internal safety-net timeout for a single request, in milliseconds. When unset the crawler derives it from
29
+ * the request handler timeout (twice it, and never below 5 minutes).
30
+ */
31
+ internalTimeoutMillis: ConfigField<z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodNumber>>>;
27
32
  /** @default 1_000 */
28
33
  systemInfoIntervalMillis: ConfigField<z.ZodDefault<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodNumber>>>;
29
34
  /** @default 'INPUT' */
@@ -100,6 +105,7 @@ export interface Configuration extends ResolvedConfigValues {
100
105
  * `defaultKeyValueStoreId` | `CRAWLEE_DEFAULT_KEY_VALUE_STORE_ID` | `'default'`
101
106
  * `defaultRequestQueueId` | `CRAWLEE_DEFAULT_REQUEST_QUEUE_ID` | `'default'`
102
107
  * `persistStateIntervalMillis` | `CRAWLEE_PERSIST_STATE_INTERVAL_MILLIS` | `60_000`
108
+ * `internalTimeoutMillis` | `CRAWLEE_INTERNAL_TIMEOUT` | -
103
109
  * `purgeOnStart` | `CRAWLEE_PURGE_ON_START` | `true`
104
110
  * `persistStorage` | `CRAWLEE_PERSIST_STORAGE` | `true`
105
111
  * `storageDir` | `CRAWLEE_STORAGE_DIR` | `'./storage'`
package/configuration.js CHANGED
@@ -54,6 +54,11 @@ export const crawleeConfigFields = {
54
54
  memoryMbytes: field(coerceNumber.optional(), 'CRAWLEE_MEMORY_MBYTES'),
55
55
  /** @default 60_000 */
56
56
  persistStateIntervalMillis: field(coerceNumber.default(60_000), 'CRAWLEE_PERSIST_STATE_INTERVAL_MILLIS'),
57
+ /**
58
+ * Internal safety-net timeout for a single request, in milliseconds. When unset the crawler derives it from
59
+ * the request handler timeout (twice it, and never below 5 minutes).
60
+ */
61
+ internalTimeoutMillis: field(coerceNumber.optional(), 'CRAWLEE_INTERNAL_TIMEOUT'),
57
62
  /** @default 1_000 */
58
63
  systemInfoIntervalMillis: field(coerceNumber.default(1_000)),
59
64
  /** @default 'INPUT' */
@@ -118,6 +123,7 @@ export const crawleeConfigFields = {
118
123
  * `defaultKeyValueStoreId` | `CRAWLEE_DEFAULT_KEY_VALUE_STORE_ID` | `'default'`
119
124
  * `defaultRequestQueueId` | `CRAWLEE_DEFAULT_REQUEST_QUEUE_ID` | `'default'`
120
125
  * `persistStateIntervalMillis` | `CRAWLEE_PERSIST_STATE_INTERVAL_MILLIS` | `60_000`
126
+ * `internalTimeoutMillis` | `CRAWLEE_INTERNAL_TIMEOUT` | -
121
127
  * `purgeOnStart` | `CRAWLEE_PURGE_ON_START` | `true`
122
128
  * `persistStorage` | `CRAWLEE_PERSIST_STORAGE` | `true`
123
129
  * `storageDir` | `CRAWLEE_STORAGE_DIR` | `'./storage'`
@@ -184,6 +184,25 @@ export interface CrawlingContext<UserData extends Dictionary = Dictionary> exten
184
184
  * Register a function to be called at the very end of the request handling process. This is useful for resources that should be accessible to error handlers, for instance.
185
185
  */
186
186
  registerDeferredCleanup(cleanup: () => Promise<unknown>): void;
187
+ /**
188
+ * Gives the current request `secs` more seconds to finish, for when how long it needs is only apparent
189
+ * once it is already running - a listing page that turns out to have far more to scroll through than
190
+ * usual, say. Prefer `requestHandlerTimeoutSecs`, or a per-route override via
191
+ * {@link Router.addHandler|`router.addHandler`}, whenever the time needed is known up front.
192
+ *
193
+ * ```ts
194
+ * router.addHandler('LIST', async ({ extendTimeout, page }) => {
195
+ * const pageCount = await countPages(page);
196
+ * extendTimeout(pageCount * 10);
197
+ * await scrapeAllPages(page);
198
+ * });
199
+ * ```
200
+ *
201
+ * Extends the request handler's own timeout and the crawler's internal one together, so the extension
202
+ * is not immediately undone by the latter. Calling it from a handler that has already timed out does
203
+ * nothing.
204
+ */
205
+ extendTimeout(secs: number): void;
187
206
  }
188
207
  /**
189
208
  * A partial implementation of {@link RestrictedCrawlingContext} that stores parameters of calls to context methods for later inspection.
@@ -1,6 +1,5 @@
1
1
  export * from './context_pipeline.js';
2
2
  export * from './crawler_commons.js';
3
- export * from './crawler_utils.js';
4
3
  export * from './statistics.js';
5
4
  export * from './error_tracker.js';
6
5
  export * from './error_snapshotter.js';
package/crawlers/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  export * from './context_pipeline.js';
2
2
  export * from './crawler_commons.js';
3
- export * from './crawler_utils.js';
4
3
  export * from './statistics.js';
5
4
  export * from './error_tracker.js';
6
5
  export * from './error_snapshotter.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlee/core",
3
- "version": "4.0.0-beta.95",
3
+ "version": "4.0.0-beta.97",
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"
@@ -51,11 +51,11 @@
51
51
  "@apify/datastructures": "^2.0.3",
52
52
  "@apify/log": "^2.5.18",
53
53
  "@apify/pseudo_url": "^2.0.59",
54
- "@apify/timeout": "^0.3.2",
54
+ "@apify/timeout": "^0.4.4",
55
55
  "@apify/utilities": "^2.15.5",
56
- "@crawlee/fs-storage": "4.0.0-beta.95",
57
- "@crawlee/types": "4.0.0-beta.95",
58
- "@crawlee/utils": "4.0.0-beta.95",
56
+ "@crawlee/fs-storage": "4.0.0-beta.97",
57
+ "@crawlee/types": "4.0.0-beta.97",
58
+ "@crawlee/utils": "4.0.0-beta.97",
59
59
  "@sapphire/async-queue": "^1.5.5",
60
60
  "@sapphire/shapeshift": "^4.0.0",
61
61
  "@vladfrangu/async_event_emitter": "^2.4.6",
@@ -79,5 +79,5 @@
79
79
  }
80
80
  }
81
81
  },
82
- "gitHead": "ba8602d011706fb5c9930232402156eb5b657eb3"
82
+ "gitHead": "43008dd43f4832d083353ba1b731c0b4607c9cfa"
83
83
  }
package/router.d.ts CHANGED
@@ -70,6 +70,20 @@ export interface RouterHandler<Context extends Omit<RestrictedCrawlingContext, '
70
70
  (ctx: Context): Awaitable<void>;
71
71
  }
72
72
  export type GetUserDataFromRequest<T> = T extends Request<infer Y> ? Y : never;
73
+ /**
74
+ * Per-route overrides, passed as the last argument of {@link Router.addHandler|`addHandler`} and
75
+ * {@link Router.addDefaultHandler|`addDefaultHandler`}.
76
+ */
77
+ export interface RouteOptions {
78
+ /**
79
+ * Overrides the crawler's `requestHandlerTimeoutSecs` for this route only. Useful when one kind of page
80
+ * needs markedly more time than the rest - a listing page behind an infinite scroll, say - and you do not
81
+ * want to raise the timeout for every other page to accommodate it.
82
+ *
83
+ * Applies only to this route's handler. The navigation and the navigation hooks keep their own timeouts.
84
+ */
85
+ requestHandlerTimeoutSecs?: number;
86
+ }
73
87
  export type RouterRoutes<Context, Routes extends Record<keyof Routes, Dictionary>> = {
74
88
  [Label in keyof Routes]: (ctx: Omit<Context, 'request'> & {
75
89
  request: Request<Routes[Label]>;
@@ -180,10 +194,31 @@ export type RouterRoutes<Context, Routes extends Record<keyof Routes, Dictionary
180
194
  * request.userData.price; // number, inferred from the schema and validated at runtime
181
195
  * });
182
196
  * ```
197
+ *
198
+ * A single route can take longer than the rest without raising the crawler-wide
199
+ * `requestHandlerTimeoutSecs` for everything - pass a per-route timeout as the last argument:
200
+ *
201
+ * ```ts
202
+ * // LIST pages scroll through a lot of content, DETAIL pages are quick
203
+ * router.addHandler('LIST', async (ctx) => { ... }, { requestHandlerTimeoutSecs: 120 });
204
+ * router.addHandler('DETAIL', async (ctx) => { ... }); // keeps the crawler's default
205
+ * ```
206
+ *
207
+ * When the time a route needs is only apparent once it is already running, call
208
+ * {@link CrawlingContext.extendTimeout|`context.extendTimeout`} from inside the handler:
209
+ *
210
+ * ```ts
211
+ * router.addHandler('LIST', async ({ page, extendTimeout }) => {
212
+ * const pageCount = await countPages(page);
213
+ * extendTimeout(pageCount * 10); // ask for 10 more seconds per page
214
+ * await scrapeAllPages(page);
215
+ * });
216
+ * ```
183
217
  */
184
218
  export declare class Router<Context extends Omit<RestrictedCrawlingContext, 'enqueueLinks'>, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>> {
185
219
  private readonly routes;
186
220
  private readonly schemas;
221
+ private readonly timeouts;
187
222
  private readonly middlewares;
188
223
  /**
189
224
  * use Router.create() instead!
@@ -192,22 +227,26 @@ export declare class Router<Context extends Omit<RestrictedCrawlingContext, 'enq
192
227
  private constructor();
193
228
  /**
194
229
  * Registers new route handler for given label. When the router declares a route map, the
195
- * `label` is restricted to the declared labels and `request.userData` is typed accordingly.
230
+ * `label` is restricted to the declared labels and `request.userData` is typed accordingly. Pass
231
+ * {@link RouteOptions|`options`} to give this route its own `requestHandlerTimeoutSecs`,
232
+ * overriding the crawler's default for requests with this label.
196
233
  */
197
- addHandler<Label extends keyof Routes & string>(label: Label, handler: (ctx: RouterHandlerContext<Context, Routes[Label], Routes>) => Awaitable<void>): void;
234
+ addHandler<Label extends keyof Routes & string>(label: Label, handler: (ctx: RouterHandlerContext<Context, Routes[Label], Routes>) => Awaitable<void>, options?: RouteOptions): void;
198
235
  /**
199
236
  * Registers new route handler for given label, explicitly typing `request.userData` via the
200
237
  * `UserData` type argument. Useful when the router has no declared route map (the open default)
201
238
  * and you want to type a single handler, or to register a handler under a `symbol` label.
202
239
  */
203
- addHandler<UserData extends Dictionary = GetUserDataFromRequest<Context['request']>>(label: RouterLabel<Routes>, handler: (ctx: RouterHandlerContext<Context, UserData, Routes>) => Awaitable<void>): void;
240
+ addHandler<UserData extends Dictionary = GetUserDataFromRequest<Context['request']>>(label: RouterLabel<Routes>, handler: (ctx: RouterHandlerContext<Context, UserData, Routes>) => Awaitable<void>, options?: RouteOptions): void;
204
241
  /**
205
242
  * Registers default route handler. As a fallback it can receive any request (including labels not
206
243
  * declared in the route map). When the router was created with a {@link defaultRoute} schema,
207
244
  * `request.userData` is typed from it; otherwise it defaults to the context's (loosely typed) `userData`.
208
- * Pass an explicit `UserData` type argument to narrow it.
245
+ * Pass an explicit `UserData` type argument to narrow it. Pass {@link RouteOptions|`options`} to give the
246
+ * default route its own `requestHandlerTimeoutSecs`, overriding the crawler's default for requests that fall
247
+ * through to it.
209
248
  */
210
- addDefaultHandler<UserData extends Dictionary = DefaultRouteUserData<Routes, GetUserDataFromRequest<Context['request']>>>(handler: (ctx: RouterHandlerContext<Context, UserData, Routes>) => Awaitable<void>): void;
249
+ addDefaultHandler<UserData extends Dictionary = DefaultRouteUserData<Routes, GetUserDataFromRequest<Context['request']>>>(handler: (ctx: RouterHandlerContext<Context, UserData, Routes>) => Awaitable<void>, options?: RouteOptions): void;
211
250
  /**
212
251
  * Returns the {@link RouteSchemas|Standard Schema} registered for a label, if any. Used by the crawler
213
252
  * to validate `request.userData` when requests are added.
@@ -219,6 +258,18 @@ export declare class Router<Context extends Omit<RestrictedCrawlingContext, 'enq
219
258
  * Multiple middlewares can be registered, they will be fired in the same order.
220
259
  */
221
260
  use(middleware: (ctx: Context) => Awaitable<void>): void;
261
+ /**
262
+ * Returns the `requestHandlerTimeoutSecs` registered for a label, or `undefined` when the route did not
263
+ * override it and the crawler's own timeout should apply. Falls back to the default route the same way
264
+ * {@link Router.getHandler|`getHandler`} does, so a label with no route of its own inherits whatever
265
+ * the default route asked for. Used by the crawler; not meant to be called directly.
266
+ */
267
+ getTimeoutSecs(label?: string | symbol): number | undefined;
268
+ /**
269
+ * The longest `requestHandlerTimeoutSecs` any route asked for, or `undefined` when no route overrides it.
270
+ * The crawler needs an upper bound up front, before it knows which routes a run will actually hit.
271
+ */
272
+ getMaxTimeoutSecs(): number | undefined;
222
273
  /**
223
274
  * Returns route handler for given label. If no label is provided, the default request handler will be returned.
224
275
  */
package/router.js CHANGED
@@ -139,29 +139,58 @@ export async function validateUserData(label, schema, userData) {
139
139
  * request.userData.price; // number, inferred from the schema and validated at runtime
140
140
  * });
141
141
  * ```
142
+ *
143
+ * A single route can take longer than the rest without raising the crawler-wide
144
+ * `requestHandlerTimeoutSecs` for everything - pass a per-route timeout as the last argument:
145
+ *
146
+ * ```ts
147
+ * // LIST pages scroll through a lot of content, DETAIL pages are quick
148
+ * router.addHandler('LIST', async (ctx) => { ... }, { requestHandlerTimeoutSecs: 120 });
149
+ * router.addHandler('DETAIL', async (ctx) => { ... }); // keeps the crawler's default
150
+ * ```
151
+ *
152
+ * When the time a route needs is only apparent once it is already running, call
153
+ * {@link CrawlingContext.extendTimeout|`context.extendTimeout`} from inside the handler:
154
+ *
155
+ * ```ts
156
+ * router.addHandler('LIST', async ({ page, extendTimeout }) => {
157
+ * const pageCount = await countPages(page);
158
+ * extendTimeout(pageCount * 10); // ask for 10 more seconds per page
159
+ * await scrapeAllPages(page);
160
+ * });
161
+ * ```
142
162
  */
143
163
  export class Router {
144
164
  routes = new Map();
145
165
  schemas = new Map();
166
+ timeouts = new Map();
146
167
  middlewares = [];
147
168
  /**
148
169
  * use Router.create() instead!
149
170
  * @ignore
150
171
  */
151
172
  constructor() { }
152
- addHandler(label, handler) {
173
+ addHandler(label, handler, options = {}) {
153
174
  this.validate(label);
154
175
  this.routes.set(label, handler);
176
+ if (options.requestHandlerTimeoutSecs !== undefined) {
177
+ this.timeouts.set(label, options.requestHandlerTimeoutSecs);
178
+ }
155
179
  }
156
180
  /**
157
181
  * Registers default route handler. As a fallback it can receive any request (including labels not
158
182
  * declared in the route map). When the router was created with a {@link defaultRoute} schema,
159
183
  * `request.userData` is typed from it; otherwise it defaults to the context's (loosely typed) `userData`.
160
- * Pass an explicit `UserData` type argument to narrow it.
184
+ * Pass an explicit `UserData` type argument to narrow it. Pass {@link RouteOptions|`options`} to give the
185
+ * default route its own `requestHandlerTimeoutSecs`, overriding the crawler's default for requests that fall
186
+ * through to it.
161
187
  */
162
- addDefaultHandler(handler) {
188
+ addDefaultHandler(handler, options = {}) {
163
189
  this.validate(defaultRoute);
164
190
  this.routes.set(defaultRoute, handler);
191
+ if (options.requestHandlerTimeoutSecs !== undefined) {
192
+ this.timeouts.set(defaultRoute, options.requestHandlerTimeoutSecs);
193
+ }
165
194
  }
166
195
  /**
167
196
  * Returns the {@link RouteSchemas|Standard Schema} registered for a label, if any. Used by the crawler
@@ -190,6 +219,25 @@ export class Router {
190
219
  use(middleware) {
191
220
  this.middlewares.push(middleware);
192
221
  }
222
+ /**
223
+ * Returns the `requestHandlerTimeoutSecs` registered for a label, or `undefined` when the route did not
224
+ * override it and the crawler's own timeout should apply. Falls back to the default route the same way
225
+ * {@link Router.getHandler|`getHandler`} does, so a label with no route of its own inherits whatever
226
+ * the default route asked for. Used by the crawler; not meant to be called directly.
227
+ */
228
+ getTimeoutSecs(label) {
229
+ if (label && this.routes.has(label)) {
230
+ return this.timeouts.get(label);
231
+ }
232
+ return this.timeouts.get(defaultRoute);
233
+ }
234
+ /**
235
+ * The longest `requestHandlerTimeoutSecs` any route asked for, or `undefined` when no route overrides it.
236
+ * The crawler needs an upper bound up front, before it knows which routes a run will actually hit.
237
+ */
238
+ getMaxTimeoutSecs() {
239
+ return this.timeouts.size > 0 ? Math.max(...this.timeouts.values()) : undefined;
240
+ }
193
241
  /**
194
242
  * Returns route handler for given label. If no label is provided, the default request handler will be returned.
195
243
  */
@@ -233,6 +281,8 @@ export class Router {
233
281
  obj.addDefaultHandler = router.addDefaultHandler.bind(router);
234
282
  obj.getSchema = router.getSchema.bind(router);
235
283
  obj.getHandler = router.getHandler.bind(router);
284
+ obj.getTimeoutSecs = router.getTimeoutSecs.bind(router);
285
+ obj.getMaxTimeoutSecs = router.getMaxTimeoutSecs.bind(router);
236
286
  obj.use = router.use.bind(router);
237
287
  // `Reflect.ownKeys` (unlike `Object.entries`) also yields the `defaultRoute` symbol key.
238
288
  for (const label of Reflect.ownKeys(routesOrSchemas ?? {})) {
@@ -1,9 +0,0 @@
1
- import type { ISession } from '@crawlee/types';
2
- /**
3
- * Handles timeout request
4
- * @internal
5
- */
6
- export declare function handleRequestTimeout({ session, errorMessage }: {
7
- session?: ISession;
8
- errorMessage: string;
9
- }): void;
@@ -1,11 +0,0 @@
1
- import { TimeoutError } from '@apify/timeout';
2
- /**
3
- * Handles timeout request
4
- * @internal
5
- */
6
- export function handleRequestTimeout({ session, errorMessage }) {
7
- session?.markBad();
8
- const timeoutMillis = /(\d+)\s?ms/.exec(errorMessage)?.[1]; // first capturing group
9
- const timeoutSecs = Number(timeoutMillis) / 1000;
10
- throw new TimeoutError(`Navigation timed out after ${timeoutSecs} seconds.`);
11
- }