Files
apify--crawlee/docs/guides/request_storage_queue_crawler.ts
Jan Buchar 70aa8c9df1 refactor!: Introduce the ContextPipeline abstraction (#3119)
- closes #2479
- closes #3106
- closes #3107
- closes #3078

In my opinion, it makes a lot of sense to do the remaining changes in a
separate PR.

- [x] Introduce a `ContextPipeline` abstraction
- [x] Update crawlers to use it
- [x] Make sure that existing tests pass
- [ ] Refine the `ContextPipeline.compose` signature and the semantics
of `BasicCrawlerOptions.contextPipelineEnhancer` to maximize DX
- [x] Write tests for the `contextPipelineEnhancer`
- [x] Resolve added TODO comments (fix immediately or make issues)
- [ ] Update documentation

The `context-pipeline` branch introduces a fundamental architectural
change to how Crawlee crawlers build and enhance the crawling context
passed to request handlers. The core motivation is to fix the
composition and extensibility nightmare in the current crawler
hierarchy.

1. **Rigid inheritance hierarchy**: Crawlers were stuck in a brittle
inheritance chain where each layer manipulated the context object while
assuming that it already satisfied its final type. Multiple overrides of
`BasicCrawler` lifecycle methods made the execution flow even harder to
follow.

2. **Context enhancement via monkey-patching**: Manual property
assignment (`crawlingContext.page = page`, `crawlingContext.$ = $`)
scattered everywhere. It was a mess to follow and impossible to reason
about.

3. **Cleanup coordination**: Resource cleanup was handled by separate
`_cleanupContext` methods that were not co-located with the
initialization.

4. **Extension mechanism was broken**: The `CrawlerExtension.use()` API
tried to let you extend crawlers (the ones based on `HttpCrawler`) by
overwriting properties - completely type-unsafe and fragile as hell.

Introduces `ContextPipeline` - a **middleware-based composition
pattern** where:

- Each crawler layer defines how it enhances the context through
explicit `action` functions
- Cleanup logic is co-located with initialization via optional `cleanup`
functions
- Type safety is maintained through TypeScript generics that track
context transformations
- The pipeline executes middleware sequentially with proper error
handling and guaranteed cleanup

Declarative middleware composition with co-located cleanup:

```typescript
contextPipeline.compose({
  action: async (context) => ({ page, $ }),
  cleanup: async (context) => { await page.close(); }
})
```

The `ContextPipeline<TBase, TFinal>` tracks type transformations through
the chain:

```typescript
ContextPipeline<CrawlingContext, CrawlingContext>
  .compose<{ page: Page }>(...) // ContextPipeline<CrawlingContext, CrawlingContext & { page: Page }>
  .compose<{ $: CheerioAPI }>(...) // ContextPipeline<CrawlingContext, CrawlingContext & { page: Page, $: CheerioAPI }>
```

The `CrawlerExtension.use()` is gone. New approach via
`contextPipelineEnhancer`:

```typescript
new BasicCrawler({
  contextPipelineEnhancer: (pipeline) =>
    pipeline.compose({
      action: async (context) => ({ myCustomProp: ... })
    })
})
```

The current way to express a context pipeline middleware has some
shortcomings (`ContextPipeline.compose`,
`BasicCrawlerOptions.contextPipelineEnhancer`). I suggest resolving this
in another PR.

For most legitimate use cases, this should be non-breaking. Those who
extend the Crawler classes in non-trivial ways may need to adjust their
code though - the non-public interface of `BasicCrawler` and
`HttpCrawler` changed quite a bit.

The pipeline uses `Object.defineProperties` for each middleware. Is this
a serious performance consideration?

---------

Co-authored-by: Martin Adámek <banan23@gmail.com>
2026-08-18 17:48:34 +02:00

26 lines
816 B
TypeScript

import { CheerioCrawler } from 'crawlee';
// The crawler will automatically process requests from the queue.
// It's used the same way for Puppeteer/Playwright crawlers.
const crawler = new CheerioCrawler({
// Note that we're not specifying the requestQueue here
async requestHandler({ enqueueLinks }) {
// Add new request to the queue
await crawler.addRequests([{ url: 'https://example.com/new-page' }]);
// Add links found on page to the queue
await enqueueLinks();
},
});
// Add the initial requests.
// Note that we are not opening the request queue explicitly before
await crawler.addRequests([
{ url: 'https://example.com/1' },
{ url: 'https://example.com/2' },
{ url: 'https://example.com/3' },
// ...
]);
// Run the crawler
await crawler.run();