@opencraw/core 0.1.2 → 0.1.3

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 (28) hide show
  1. package/README.md +1 -1
  2. package/dist/index.esm.js +762 -71
  3. package/dist/src/access/access-profile.contract.d.ts +4 -0
  4. package/dist/src/access/index.d.ts +1 -1
  5. package/dist/src/api-steps/send-request.use-case.d.ts +3 -2
  6. package/dist/src/browser-session/browser-profile.store.d.ts +52 -0
  7. package/dist/src/browser-session/browser.client.d.ts +8 -0
  8. package/dist/src/browser-session/index.d.ts +1 -0
  9. package/dist/src/crawl-events/crawl-event.contract.d.ts +8 -0
  10. package/dist/src/crawl-execution/bootstrap-session.use-case.d.ts +23 -2
  11. package/dist/src/crawl-execution/crawl-options.config.d.ts +27 -0
  12. package/dist/src/crawl-execution/rotating-runner.use-case.d.ts +9 -0
  13. package/dist/src/crawl-execution/run-crawl.use-case.d.ts +6 -2
  14. package/dist/src/crawl-execution/run-input-recipe.use-case.d.ts +11 -3
  15. package/dist/src/index.d.ts +5 -4
  16. package/dist/src/recipe-schema/index.d.ts +2 -2
  17. package/dist/src/recipe-schema/input-recipe.contract.d.ts +24 -1
  18. package/dist/src/record-sink/dedupe.policy.d.ts +19 -7
  19. package/dist/src/record-sink/index.d.ts +1 -0
  20. package/dist/src/step-flow/for-each.use-case.d.ts +4 -2
  21. package/dist/src/step-flow/host-throttle.policy.d.ts +49 -0
  22. package/dist/src/step-flow/index.d.ts +5 -0
  23. package/dist/src/step-flow/run-gate.policy.d.ts +12 -1
  24. package/dist/src/step-flow/step-runner.contract.d.ts +13 -0
  25. package/dist/src/step-flow/transport-retry.policy.d.ts +76 -0
  26. package/dist/src/web-steps/navigate.use-case.d.ts +3 -2
  27. package/dist/src/web-steps/run-web-step.use-case.d.ts +8 -1
  28. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import type { ThrottleConfig } from '../step-flow/index.js';
2
3
  /**
3
4
  * Where a crawl's traffic goes. Profiles live in the runner's access config,
4
5
  * never in a recipe: they hold the user's accounts. Every string may use
@@ -84,8 +85,11 @@ export interface AccessConfig {
84
85
  $schema?: string;
85
86
  profiles: Record<string, AccessProfile>;
86
87
  default?: string;
88
+ /** How gently each site is crawled, across every recipe: `{ delayMs?, concurrency?, domains? }`. */
89
+ throttle?: ThrottleConfig;
87
90
  }
88
91
  export declare const accessProfileSchema: z.ZodType<AccessProfile>;
92
+ export declare const throttleConfigSchema: z.ZodType<ThrottleConfig>;
89
93
  export declare const accessConfigSchema: z.ZodType<AccessConfig>;
90
94
  export {};
91
95
  //# sourceMappingURL=access-profile.contract.d.ts.map
@@ -3,7 +3,7 @@ export { loadAccessConfig } from './access-config.repository.js';
3
3
  export { AccessConfigError } from './access-config.error.js';
4
4
  export { ACCESS_PRESETS } from './access-preset.store.js';
5
5
  export type { AccessPreset } from './access-preset.store.js';
6
- export { accessConfigSchema, accessProfileSchema, BLOCKABLE_RESOURCES } from './access-profile.contract.js';
6
+ export { accessConfigSchema, accessProfileSchema, throttleConfigSchema, BLOCKABLE_RESOURCES } from './access-profile.contract.js';
7
7
  export type { AccessConfig, AccessProfile, ProxyProfile, PoolProfile, CdpProfile, PluginProfile, DirectProfile, ProxySettings, BlockableResource } from './access-profile.contract.js';
8
8
  export type { AccessPlugin, AccessLease, LeaseRequest, PluginLeaseRequest } from './access-plugin.contract.js';
9
9
  export { newSessionId } from './session-id.algorithm.js';
@@ -5,8 +5,9 @@ import type { InputRecipe, RequestStep } from '../recipe-schema/index.js';
5
5
  import type { RunGate } from '../step-flow/index.js';
6
6
  /**
7
7
  * Sends a `request` step: renders its templates, waits for the gate's throttle,
8
- * sends, checks the response against the recipe's block rule, then binds it as
9
- * the scope's current document (and under the step id).
8
+ * sends (again, after a pause, while it fails in passing: `limits.retry`),
9
+ * checks the response against the recipe's block rule, then binds it as the
10
+ * scope's current document (and under the step id).
10
11
  *
11
12
  * @param step - The request step.
12
13
  * @param scope - The scope to render in and bind into.
@@ -0,0 +1,52 @@
1
+ import type { BrowserSessionConfig } from './browser-session.config.js';
2
+ import { BrowserSession } from './browser.client.js';
3
+ import type { SessionOptions } from './browser.client.js';
4
+ /** A browser profile name: it becomes a directory, so no separators or dots. */
5
+ export declare const BROWSER_PROFILE_NAME: RegExp;
6
+ /**
7
+ * Browser profiles that persist between runs: each is a directory of a real
8
+ * browser's user data (cookies, local storage, IndexedDB, cache, service
9
+ * workers), so a login, a consent choice or a site's trust in a returning
10
+ * visitor carries over to the next run. The browser equivalent of a user who
11
+ * never clears their history.
12
+ *
13
+ * A profile directory can be open in one browser at a time. Within this
14
+ * crawler, a second use waits for the first to close; the same owner (one
15
+ * recipe run reopening after a rotation) takes it over instead. Another
16
+ * crawler holding it, in this process or another, is reported, not waited
17
+ * for: a lock file in the profile names the process, and one left by a
18
+ * process that died is taken over. (Chromium's own profile lock is not
19
+ * enough: headless builds do not take it.)
20
+ */
21
+ export declare class BrowserProfiles {
22
+ readonly directory: string;
23
+ private readonly config;
24
+ private readonly held;
25
+ private readonly waiting;
26
+ /**
27
+ * @param directory - Where the profiles live, one subdirectory each.
28
+ * @param config - The crawler's browser settings (type, binary, headless, timeouts).
29
+ */
30
+ constructor(directory: string, config?: BrowserSessionConfig);
31
+ private take;
32
+ private free;
33
+ private launch;
34
+ /**
35
+ * The profile's directory.
36
+ *
37
+ * @param name - A profile name.
38
+ * @returns The absolute path.
39
+ */
40
+ pathOf(name: string): string;
41
+ /**
42
+ * Opens a profile in its own browser, waiting while another run of this
43
+ * crawler uses it.
44
+ *
45
+ * @param name - The profile.
46
+ * @param options - Proxy, headers, viewport, cookies to add. `storageState` is ignored: the profile has its own.
47
+ * @param owner - Who opens it; the same owner reopening closes its previous session first.
48
+ * @returns The session; closing it frees the profile.
49
+ */
50
+ open(name: string, options: SessionOptions, owner: object): Promise<BrowserSession>;
51
+ }
52
+ //# sourceMappingURL=browser-profile.store.d.ts.map
@@ -39,6 +39,14 @@ export declare class BrowserSession {
39
39
  storageState(): Promise<StorageState>;
40
40
  close(): Promise<void>;
41
41
  }
42
+ /**
43
+ * What a context gets after it opened: the cookies to add and the resource
44
+ * types to skip.
45
+ *
46
+ * @param context - The context.
47
+ * @param options - The session options.
48
+ */
49
+ export declare function applySessionExtras(context: BrowserContext, options: SessionOptions): Promise<void>;
42
50
  /** A launched browser; sessions are opened from it and closed independently. */
43
51
  export declare class BrowserClient {
44
52
  private readonly browser;
@@ -1,4 +1,5 @@
1
1
  export { BrowserClient, BrowserSession } from './browser.client.js';
2
+ export { BrowserProfiles, BROWSER_PROFILE_NAME } from './browser-profile.store.js';
2
3
  export type { StorageState, SessionOptions } from './browser.client.js';
3
4
  export { DEFAULT_BROWSER_CONFIG } from './browser-session.config.js';
4
5
  export type { BrowserSessionConfig } from './browser-session.config.js';
@@ -49,6 +49,14 @@ export type CrawlEvent = (Base & {
49
49
  attempt: number;
50
50
  reason: string;
51
51
  }) |
52
+ /** A request failed in passing (a dropped connection, a 503, a 429) and is sent again after `delayMs`. */
53
+ (Base & {
54
+ type: 'request:retry';
55
+ url: string;
56
+ attempt: number;
57
+ reason: string;
58
+ delayMs: number;
59
+ }) |
52
60
  /** A captcha challenge is on the page. */
53
61
  (Base & {
54
62
  type: 'captcha:detected';
@@ -1,15 +1,20 @@
1
1
  import type { AccessLease } from '../access/index.js';
2
- import type { BrowserClient, BrowserSession, SessionOptions, StorageState } from '../browser-session/index.js';
2
+ import type { BrowserClient, BrowserProfiles, BrowserSession, SessionOptions, StorageState } from '../browser-session/index.js';
3
3
  import type { CaptchaGuard } from '../captcha/index.js';
4
4
  import type { EventBus } from '../crawl-events/index.js';
5
5
  import type { HookRegistry } from '../hooks/index.js';
6
6
  import type { InputRecipe } from '../recipe-schema/index.js';
7
+ import type { HostThrottle } from '../step-flow/index.js';
7
8
  export interface BootstrapDependencies {
8
9
  /** Launches (or returns) the shared browser; only called when a browser is needed. */
9
10
  browser: () => Promise<BrowserClient>;
10
11
  hooks: HookRegistry;
11
12
  events: EventBus;
12
13
  storageStateDir?: string;
14
+ /** The crawler's per-site throttle: the bootstrap's pages count too. */
15
+ hosts?: HostThrottle;
16
+ /** The runner's persistent browser profiles, for `session.browserProfile`. */
17
+ profiles?: BrowserProfiles;
13
18
  }
14
19
  /**
15
20
  * The session options an access lease contributes: its proxy, TLS leniency,
@@ -28,13 +33,29 @@ export declare function accessOptions(lease: AccessLease | undefined, headers: R
28
33
  * The bootstrap runs through the same access lease as the crawl that follows,
29
34
  * so a login and the requests that use its cookies come from one IP.
30
35
  *
36
+ * With `session.browserProfile`, the bootstrap runs in that profile, and
37
+ * without a bootstrap the profile's own cookies and storage are the state: an
38
+ * api recipe picks up a login a browser left in the profile.
39
+ *
31
40
  * @param recipe - The input recipe.
32
41
  * @param deps - Browser, hooks, events.
33
42
  * @param lease - The recipe run's access; direct when omitted.
34
43
  * @param captcha - Solves the bootstrap's captchas (a login form's).
44
+ * @param owner - The recipe run, which a browser profile is held by.
35
45
  * @returns The state, or `undefined` when the recipe declares none.
36
46
  */
37
- export declare function resolveStorageState(recipe: InputRecipe, deps: BootstrapDependencies, lease?: AccessLease, captcha?: CaptchaGuard): Promise<StorageState | undefined>;
47
+ export declare function resolveStorageState(recipe: InputRecipe, deps: BootstrapDependencies, lease?: AccessLease, captcha?: CaptchaGuard, owner?: object): Promise<StorageState | undefined>;
48
+ /**
49
+ * Opens the recipe's `session.browserProfile` with its session options and
50
+ * the lease's proxy.
51
+ *
52
+ * @param recipe - A recipe with `session.browserProfile`.
53
+ * @param deps - For `profiles`.
54
+ * @param lease - The access lease.
55
+ * @param owner - The recipe run.
56
+ * @returns The session in the profile.
57
+ */
58
+ export declare function openBrowserProfile(recipe: InputRecipe, deps: Pick<BootstrapDependencies, 'profiles'>, lease: AccessLease | undefined, owner: object): Promise<BrowserSession>;
38
59
  /**
39
60
  * The storage state saved by an earlier bootstrap (`session.storageStatePath`), if the recipe names one.
40
61
  *
@@ -3,6 +3,8 @@ import type { BrowserSessionConfig } from '../browser-session/index.js';
3
3
  import type { CaptchaSolver } from '../captcha/index.js';
4
4
  import type { CrawlListener } from '../crawl-events/index.js';
5
5
  import type { HookMap } from '../hooks/index.js';
6
+ import type { ThrottleConfig } from '../step-flow/index.js';
7
+ import type { RetryRule } from '../recipe-schema/index.js';
6
8
  import type { DedupeScope, RecordSink } from '../record-sink/index.js';
7
9
  /** How a crawler is created. Everything is optional. */
8
10
  export interface CrawlOptions {
@@ -15,10 +17,21 @@ export interface CrawlOptions {
15
17
  onEvent?: CrawlListener;
16
18
  /** Default `run`: a key seen once is dropped for the rest of the run. */
17
19
  dedupe?: DedupeScope;
20
+ /**
21
+ * How many input recipes of a set run at once; default 1, one after
22
+ * another. Each has its own browser context or HTTP session; the browser,
23
+ * the sink and the per-site `throttle` are shared.
24
+ */
25
+ parallel?: number;
18
26
  /** Whether a failed input recipe stops the run; default `continue`. */
19
27
  onRecipeError?: 'continue' | 'stop';
20
28
  /** Base directory for relative `storageStatePath` and `saveTo` values. */
21
29
  storageStateDir?: string;
30
+ /**
31
+ * Where `session.browserProfile` profiles live, one directory each. Default:
32
+ * `.opencraw/profiles` under `storageStateDir` (or the working directory).
33
+ */
34
+ profilesDir?: string;
22
35
  /**
23
36
  * Skip records whose key the sink already has (`sink.has`), reporting them as
24
37
  * `skipped`. Needs a sink that can answer, such as `jsonLinesSink(path, { append: true })`.
@@ -32,6 +45,20 @@ export interface CrawlOptions {
32
45
  * with `session.access.profile`. Without it every recipe goes direct.
33
46
  */
34
47
  access?: AccessConfig;
48
+ /**
49
+ * How gently each site is crawled, across every recipe this crawler runs:
50
+ * `delayMs` between request starts and `concurrency` requests in flight,
51
+ * per site, with `domains` for site-specific rules. Defaults to
52
+ * `access.throttle`; without either, only each recipe's `limits` apply.
53
+ */
54
+ throttle?: ThrottleConfig;
55
+ /**
56
+ * How a request that fails in passing (a dropped connection, a timeout, a
57
+ * 503, a 429) is sent again, for recipes whose `limits.retry` says
58
+ * nothing: `{ attempts?, backoffMs?, maxDelayMs?, statuses? }`. Default:
59
+ * three tries, one then two seconds apart, `Retry-After` honoured.
60
+ */
61
+ retry?: RetryRule;
35
62
  /** Plugins `{ kind: 'plugin', name }` profiles refer to. */
36
63
  accessPlugins?: AccessPlugin[];
37
64
  /** Solvers recipes name in `session.captcha.solver` and `captcha` steps. */
@@ -50,6 +50,15 @@ export declare class RotatingRunner implements StepRunner {
50
50
  private note;
51
51
  runLeaf(step: Step, scope: ExtractionScope): Promise<void>;
52
52
  nextPage(next: PaginateNext, scope: ExtractionScope): Promise<NextPageResult>;
53
+ /**
54
+ * A runner for one parallel iteration, forked from whichever runner is
55
+ * current when it runs a step: after a rotation it forks again from the new
56
+ * one, since the old context is gone (or going). Blocks are noted and
57
+ * rotated like the main runner's.
58
+ *
59
+ * @returns The iteration's runner.
60
+ */
61
+ fork(): Promise<StepRunner>;
53
62
  elements(selector: string, scope: ExtractionScope): Promise<LiveElement[]>;
54
63
  rotate(error: BlockedError): Promise<boolean>;
55
64
  dispose(): Promise<void>;
@@ -2,12 +2,16 @@ import type { RecipeSet } from '../recipe-loading/index.js';
2
2
  import type { CrawlReport } from './crawl-report.model.js';
3
3
  import type { RecipeRunDependencies } from './run-input-recipe.use-case.js';
4
4
  /**
5
- * Runs every input recipe of a set, one after another, into one sink.
5
+ * Runs every input recipe of a set into one sink, `parallel` at a time
6
+ * (default one after another). Reports come back in the set's order whatever
7
+ * order the recipes finish in. Under `onRecipeError: 'stop'`, a failed recipe
8
+ * stops the ones not started yet; those already running finish.
6
9
  *
7
10
  * @param set - The bound recipes.
8
11
  * @param deps - Shared browser, hooks, events, sink and de-duplication.
9
12
  * @param onRecipeError - Whether a failed recipe stops the run.
13
+ * @param parallel - How many input recipes run at once.
10
14
  * @returns The report.
11
15
  */
12
- export declare function runCrawl(set: RecipeSet, deps: RecipeRunDependencies, onRecipeError: 'continue' | 'stop'): Promise<CrawlReport>;
16
+ export declare function runCrawl(set: RecipeSet, deps: RecipeRunDependencies, onRecipeError: 'continue' | 'stop', parallel?: number): Promise<CrawlReport>;
13
17
  //# sourceMappingURL=run-crawl.use-case.d.ts.map
@@ -1,10 +1,12 @@
1
1
  import type { AccessBroker } from '../access/index.js';
2
2
  import { BrowserClient } from '../browser-session/index.js';
3
+ import type { BrowserProfiles } from '../browser-session/index.js';
3
4
  import { CaptchaSolverRegistry } from '../captcha/index.js';
4
5
  import type { EventBus } from '../crawl-events/index.js';
5
6
  import type { HookRegistry } from '../hooks/index.js';
6
- import type { InputRecipe, OutputRecipe } from '../recipe-schema/index.js';
7
+ import type { InputRecipe, OutputRecipe, RetryRule } from '../recipe-schema/index.js';
7
8
  import type { DedupePolicy, RecordSink } from '../record-sink/index.js';
9
+ import type { HostThrottle } from '../step-flow/index.js';
8
10
  import type { RecipeReport } from './crawl-report.model.js';
9
11
  export interface RecipeRunDependencies {
10
12
  browser: () => Promise<BrowserClient>;
@@ -23,6 +25,12 @@ export interface RecipeRunDependencies {
23
25
  access: AccessBroker;
24
26
  /** The solvers recipes name; none when omitted. */
25
27
  captchaSolvers?: CaptchaSolverRegistry;
28
+ /** The crawler's per-site throttle, shared by every recipe. */
29
+ hosts?: HostThrottle;
30
+ /** The runner's persistent browser profiles, for `session.browserProfile`. */
31
+ profiles?: BrowserProfiles;
32
+ /** The crawler's retry rule, under each recipe's `limits.retry`. */
33
+ retry?: RetryRule;
26
34
  }
27
35
  /**
28
36
  * Runs one input recipe end to end: session, runner, the step walk, and for
@@ -34,10 +42,10 @@ export interface RecipeRunDependencies {
34
42
  * the sink sees one record at a time and `maxRecords` is exact: once reached,
35
43
  * every later emit returns `stop` before mapping.
36
44
  *
37
- * @param input - The input recipe.
45
+ * @param recipe - The input recipe.
38
46
  * @param output - The output recipe it feeds.
39
47
  * @param deps - Shared browser, hooks, events, sink and de-duplication.
40
48
  * @returns What happened.
41
49
  */
42
- export declare function runInputRecipe(input: InputRecipe, output: OutputRecipe, deps: RecipeRunDependencies): Promise<RecipeReport>;
50
+ export declare function runInputRecipe(recipe: InputRecipe, output: OutputRecipe, deps: RecipeRunDependencies): Promise<RecipeReport>;
43
51
  //# sourceMappingURL=run-input-recipe.use-case.d.ts.map
@@ -1,11 +1,11 @@
1
1
  export { createCrawler } from './crawl-execution/index.js';
2
- export { AccessBroker, AccessConfigError, ACCESS_PRESETS, loadAccessConfig, accessConfigSchema, accessConfigJsonSchema } from './access/index.js';
2
+ export { AccessBroker, AccessConfigError, ACCESS_PRESETS, loadAccessConfig, accessConfigSchema, accessConfigJsonSchema, throttleConfigSchema } from './access/index.js';
3
3
  export type { AccessConfig, AccessProfile, AccessPlugin, AccessLease, LeaseRequest, PluginLeaseRequest, ProxySettings, AccessPreset } from './access/index.js';
4
4
  export type { Crawler, CrawlOptions, CrawlReport, RecipeReport } from './crawl-execution/index.js';
5
5
  export { loadRecipeSet, loadRecipes, readRecipeSource, bindRecipeSet, RecipeSet, RecipeBindingError, validateBinding } from './recipe-loading/index.js';
6
6
  export type { RecipeSetSource, RecipeSource, RecipeBytes, RecipeDocument, BindingIssue } from './recipe-loading/index.js';
7
- export { parseInputRecipe, parseOutputRecipe, RecipeValidationError, inputRecipeJsonSchema, outputRecipeJsonSchema, inputRecipeSchema, outputRecipeSchema } from './recipe-schema/index.js';
8
- export type { InputRecipe, OutputRecipe, FieldSpec, Step, StepType, MappingRule, TransformRule, ErrorPolicy, PaginateNext, SessionSpec, SessionAccess, CaptchaSettings, CaptchaStep, RecipeIssue } from './recipe-schema/index.js';
7
+ export { parseInputRecipe, parseOutputRecipe, RecipeValidationError, inputRecipeJsonSchema, outputRecipeJsonSchema, inputRecipeSchema, outputRecipeSchema, retryRuleSchema } from './recipe-schema/index.js';
8
+ export type { InputRecipe, OutputRecipe, FieldSpec, Step, StepType, MappingRule, TransformRule, ErrorPolicy, PaginateNext, SessionSpec, SessionAccess, CaptchaSettings, CaptchaStep, RetryRule, RecipeIssue } from './recipe-schema/index.js';
9
9
  export type { Hook, HookMap, HookContext } from './hooks/index.js';
10
10
  export { UnknownHookError } from './hooks/index.js';
11
11
  export type { OutputRecord } from './output-mapping/index.js';
@@ -27,7 +27,8 @@ export { readYaml } from './yaml-document/index.js';
27
27
  export { findDeckTables, deckText, isDeckDocument } from './deck-document/index.js';
28
28
  export type { DeckDocument, DeckSlide, DeckShape, DeckChart, DeckTable, DeckTableQuery } from './deck-document/index.js';
29
29
  export type { WorkbookDocument, WorkbookCell, Sheet, CsvFormat, GridTable, GridTableQuery } from './workbook-document/index.js';
30
- export { StepFailure } from './step-flow/index.js';
30
+ export { StepFailure, HostThrottle, DEFAULT_RETRY_RULE } from './step-flow/index.js';
31
+ export type { ThrottleConfig, HostRule } from './step-flow/index.js';
31
32
  export { TransformError } from './transformation/index.js';
32
33
  export { CaptchaError, DEFAULT_CAPTCHA_SELECTOR, detectChallenge } from './captcha/index.js';
33
34
  export type { CaptchaSolver, CaptchaChallenge, CaptchaContext, CaptchaOutcome, CaptchaKind, CaptchaLog } from './captcha/index.js';
@@ -5,8 +5,8 @@ export { stepSchema, errorPolicySchema, paginateNextSchema } from './step.contra
5
5
  export type { Step, StepType, StepBaseFields, TargetFields, ErrorPolicy, PaginateNext, TakeKind, GotoStep, ClickStep, FillStep, PressStep, SelectStep, ScrollStep, WaitStep, EvaluateStep, ScreenshotStep, RequestStep, ExtractStep, SetStep, CollectStep, ForEachStep, IfStep, PaginateStep, EmitStep, HookStep, CaptchaStep, CaptchaCheck, } from './step.contract.js';
6
6
  export { transformRuleSchema, mappingRuleSchema } from './transform-rule.contract.js';
7
7
  export type { TransformRule, TransformOp, MappingRule, FromRule, EachRule } from './transform-rule.contract.js';
8
- export { inputRecipeSchema, sessionSpecSchema, startPointSchema } from './input-recipe.contract.js';
9
- export type { InputRecipe, SessionSpec, SessionBootstrap, SessionAccess, BlockRule, BlockRotation, CaptchaSettings, StartPoint, CrawlLimits, RecipeCookie } from './input-recipe.contract.js';
8
+ export { inputRecipeSchema, sessionSpecSchema, startPointSchema, retryRuleSchema } from './input-recipe.contract.js';
9
+ export type { InputRecipe, SessionSpec, SessionBootstrap, SessionAccess, BlockRule, BlockRotation, CaptchaSettings, RetryRule, StartPoint, CrawlLimits, RecipeCookie } from './input-recipe.contract.js';
10
10
  export { parseInputRecipe, parseOutputRecipe, recipeKindOf } from './recipe.validator.js';
11
11
  export { RecipeValidationError } from './recipe-validation.error.js';
12
12
  export type { RecipeIssue } from './recipe-validation.error.js';
@@ -99,14 +99,36 @@ export interface SessionSpec {
99
99
  blockedWhen?: BlockRule;
100
100
  onBlock?: BlockRotation;
101
101
  captcha?: CaptchaSettings;
102
+ /**
103
+ * A browser profile of the runner that persists between runs (cookies,
104
+ * storage, cache): web recipes and bootstraps run in it. A name; the runner
105
+ * decides where profiles live.
106
+ */
107
+ browserProfile?: string;
108
+ }
109
+ /**
110
+ * How a request that fails in passing (a dropped connection, a timeout, a
111
+ * 503, a 429) is sent again. On by default: three tries in all.
112
+ */
113
+ export interface RetryRule {
114
+ /** Tries per request, the first included; `1` turns retrying off. Default 3. */
115
+ attempts?: number;
116
+ /** The first pause; it doubles on every retry. Default 1000. */
117
+ backoffMs?: number;
118
+ /** The longest pause, `Retry-After` included; a server asking for longer is not retried. Default 30000. */
119
+ maxDelayMs?: number;
120
+ /** The statuses retried. Default `[408, 425, 429, 500, 502, 503, 504]`. */
121
+ statuses?: number[];
102
122
  }
103
123
  export interface CrawlLimits {
104
124
  maxRecords?: number;
105
125
  /** Minimum interval between two request starts across the recipe, whatever runs in parallel. */
106
126
  delayMs?: number;
107
127
  timeoutMs?: number;
108
- /** How many `forEach` iterations may run at once (api mode; a web recipe drives one page). Default 1. */
128
+ /** How many `forEach` iterations over a list may run at once: requests in api mode, tabs in web mode. Default 1. */
109
129
  concurrency?: number;
130
+ /** How requests that fail in passing are sent again; the crawler's `retry`, else three tries, when omitted. */
131
+ retry?: RetryRule;
110
132
  }
111
133
  /** Where to start, how to navigate, what to extract, and how it maps to one output recipe. */
112
134
  export interface InputRecipe {
@@ -129,5 +151,6 @@ export interface InputRecipe {
129
151
  }
130
152
  export declare const startPointSchema: z.ZodType<StartPoint>;
131
153
  export declare const sessionSpecSchema: z.ZodType<SessionSpec>;
154
+ export declare const retryRuleSchema: z.ZodType<RetryRule>;
132
155
  export declare const inputRecipeSchema: z.ZodType<InputRecipe>;
133
156
  //# sourceMappingURL=input-recipe.contract.d.ts.map
@@ -1,17 +1,29 @@
1
1
  import type { OutputRecord } from '../output-mapping/index.js';
2
2
  /** How far de-duplication reaches: the whole run, one input recipe, or not at all. */
3
3
  export type DedupeScope = 'run' | 'recipe' | 'off';
4
- /** Drops records whose key was already seen. First record wins; keyless records always pass. */
4
+ /** One input recipe's view of de-duplication: whether a record repeats a key already seen. */
5
+ export interface RecipeDedupe {
6
+ /**
7
+ * @param record - A validated record.
8
+ * @returns `true` when the record repeats an earlier key and must be dropped.
9
+ */
10
+ isDuplicate: (record: OutputRecord) => boolean;
11
+ }
12
+ /**
13
+ * Drops records whose key was already seen. First record wins; keyless
14
+ * records always pass. Recipes running in parallel each get their own view:
15
+ * under `recipe` scope they never see each other's keys, under `run` scope
16
+ * they share them (and whichever emits a key first keeps it).
17
+ */
5
18
  export declare class DedupePolicy {
6
19
  readonly scope: DedupeScope;
7
- private seen;
20
+ private readonly shared;
8
21
  constructor(scope?: DedupeScope);
9
- /** Called when an input recipe starts; forgets keys under `recipe` scope. */
10
- startRecipe(): void;
11
22
  /**
12
- * @param record - A validated record.
13
- * @returns `true` when the record repeats an earlier key and must be dropped.
23
+ * The de-duplication one input recipe run uses.
24
+ *
25
+ * @returns Its view: keys shared with the run, its own, or none checked.
14
26
  */
15
- isDuplicate(record: OutputRecord): boolean;
27
+ forRecipe(): RecipeDedupe;
16
28
  }
17
29
  //# sourceMappingURL=dedupe.policy.d.ts.map
@@ -4,5 +4,6 @@ export type { MemorySink } from './memory-sink.repository.js';
4
4
  export { jsonLinesSink } from './json-lines-sink.repository.js';
5
5
  export type { JsonLinesSinkOptions } from './json-lines-sink.repository.js';
6
6
  export { DedupePolicy } from './dedupe.policy.js';
7
+ export type { RecipeDedupe } from './dedupe.policy.js';
7
8
  export type { DedupeScope } from './dedupe.policy.js';
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -6,8 +6,10 @@ import type { EmitOutcome, StepWalk } from './run-steps.use-case.js';
6
6
  * matching `selector`, each in a fresh child scope with the item bound under
7
7
  * `as`; emits a record per iteration when asked.
8
8
  *
9
- * With a concurrent gate, iterations run as permits allow and records come
10
- * out in completion order; without one, in list order.
9
+ * With a concurrent gate, iterations of a list run as permits allow and
10
+ * records come out in completion order; without one, in list order. In web
11
+ * mode each parallel iteration runs in a tab of its own (`runner.fork`); a
12
+ * loop over live elements stays sequential, since its elements live on one page.
11
13
  *
12
14
  * @param step - The forEach step.
13
15
  * @param scope - The scope the list lives in.
@@ -0,0 +1,49 @@
1
+ /** How gently one site is crawled. */
2
+ export interface HostRule {
3
+ /** Minimum time between two request starts to the site, whatever recipe sends them. */
4
+ delayMs?: number;
5
+ /** Requests to the site in flight at once. */
6
+ concurrency?: number;
7
+ }
8
+ /**
9
+ * The crawler's politeness towards each site, across every recipe and run it
10
+ * executes: a default rule for any host, and rules by domain (`example.com`
11
+ * also covers `www.example.com`; the longest match wins).
12
+ */
13
+ export interface ThrottleConfig extends HostRule {
14
+ domains?: Record<string, HostRule>;
15
+ }
16
+ /**
17
+ * Spaces and bounds requests per site, shared by every recipe of a crawler,
18
+ * so two recipes (or two parallel iterations) that hit one site add up to one
19
+ * polite client rather than two. A recipe's own `limits.delayMs` still applies
20
+ * on top, per recipe.
21
+ *
22
+ * Like a single-lane bridge with a traffic light: whoever arrives waits for
23
+ * the car ahead to be far enough, and for a free lane.
24
+ */
25
+ export declare class HostThrottle {
26
+ private readonly config;
27
+ private readonly buckets;
28
+ private readonly domains;
29
+ constructor(config?: ThrottleConfig);
30
+ private bucketFor;
31
+ /** Whether any rule can hold a request back. */
32
+ get active(): boolean;
33
+ /**
34
+ * Waits until a request to `url` may start, then holds one of its site's
35
+ * lanes until the returned release is called.
36
+ *
37
+ * @param url - Where the request goes; anything but `http(s):` passes at once.
38
+ * @returns The release: call it once, when the response arrived or the request failed.
39
+ */
40
+ slot(url: string): Promise<() => void>;
41
+ /**
42
+ * Holds every request to the site of `url` back until `untilMs` (a `Retry-After`).
43
+ *
44
+ * @param url - A URL of the site.
45
+ * @param untilMs - An epoch time.
46
+ */
47
+ pause(url: string, untilMs: number): void;
48
+ }
49
+ //# sourceMappingURL=host-throttle.policy.d.ts.map
@@ -1,9 +1,14 @@
1
1
  export { runSteps } from './run-steps.use-case.js';
2
2
  export type { StepWalkOptions, EmitOutcome } from './run-steps.use-case.js';
3
3
  export type { StepRunner, NextPageResult } from './step-runner.contract.js';
4
+ export { disposeQuietly } from './step-runner.contract.js';
4
5
  export { StepFailure, NoMatchError } from './step-failure.error.js';
5
6
  export { resolveErrorPolicy, backoffFor, sleep } from './retry.policy.js';
6
7
  export { RunGate } from './run-gate.policy.js';
8
+ export { HostThrottle } from './host-throttle.policy.js';
9
+ export { withTransportRetry, resolveRetryRule, transientError, retryDelay, DEFAULT_RETRY_RULE, RETRY_STATUSES } from './transport-retry.policy.js';
10
+ export type { ResolvedRetryRule, Transient, TransportAttempt, RetryContext } from './transport-retry.policy.js';
11
+ export type { HostRule, ThrottleConfig } from './host-throttle.policy.js';
7
12
  export { BlockedError } from './blocked.error.js';
8
13
  export { detectBlock, DEFAULT_BLOCK_RULE } from './block-rule.policy.js';
9
14
  export type { ObservedResponse } from './block-rule.policy.js';
@@ -1,3 +1,4 @@
1
+ import type { HostThrottle } from './host-throttle.policy.js';
1
2
  /**
2
3
  * What bounds a recipe run: how many `forEach` iterations may be in flight and
3
4
  * how close together requests may start. One gate per recipe run, shared by
@@ -10,6 +11,7 @@
10
11
  export declare class RunGate {
11
12
  readonly permits: number;
12
13
  readonly minIntervalMs: number;
14
+ readonly hosts?: HostThrottle | undefined;
13
15
  private readonly shared?;
14
16
  private inFlight;
15
17
  private readonly waiting;
@@ -17,9 +19,10 @@ export declare class RunGate {
17
19
  /**
18
20
  * @param permits - Iterations allowed in flight; 1 is sequential.
19
21
  * @param minIntervalMs - Minimum time between two request starts across the run.
22
+ * @param hosts - The crawler's per-site throttle, shared with every other recipe.
20
23
  * @param shared - The throttle state to share (internal: `nested` gates keep their parent's).
21
24
  */
22
- constructor(permits: number, minIntervalMs: number, shared?: RunGate | undefined);
25
+ constructor(permits: number, minIntervalMs: number, hosts?: HostThrottle | undefined, shared?: RunGate | undefined);
23
26
  /** Whether this gate lets more than one iteration run at once. */
24
27
  get concurrent(): boolean;
25
28
  /**
@@ -33,6 +36,14 @@ export declare class RunGate {
33
36
  * whichever loop started it. Returns at once when the interval has passed.
34
37
  */
35
38
  throttle(): Promise<void>;
39
+ /**
40
+ * Waits until a request to `url` may start: the recipe's interval, then its
41
+ * site's turn in the crawler's per-site throttle.
42
+ *
43
+ * @param url - Where the request goes.
44
+ * @returns The release of the site's lane: call it once the response arrived or the request failed.
45
+ */
46
+ request(url: string): Promise<() => void>;
36
47
  /** The gate for a body running inside an iteration that holds a permit: sequential, same throttle. */
37
48
  nested(): RunGate;
38
49
  }
@@ -1,6 +1,13 @@
1
1
  import type { ExtractionScope, LiveElement } from '../extraction-scope/index.js';
2
2
  import type { PaginateNext, Step } from '../recipe-schema/index.js';
3
3
  import type { BlockedError } from './blocked.error.js';
4
+ /**
5
+ * Disposes a runner, ignoring a failure: a tab whose browser already went
6
+ * away (a rotation, a crash) has nothing left to close.
7
+ *
8
+ * @param runner - The runner.
9
+ */
10
+ export declare function disposeQuietly(runner: StepRunner): Promise<void>;
4
11
  /** What `paginate` learns from the runner after a page body ran. */
5
12
  export type NextPageResult =
6
13
  /** The next page is at this URL (the runner already navigated in web mode). */
@@ -34,6 +41,12 @@ export interface StepRunner {
34
41
  * `false` means it cannot, and the block fails the step like any error.
35
42
  */
36
43
  rotate?: (error: BlockedError) => Promise<boolean>;
44
+ /**
45
+ * A runner of its own for one parallel `forEach` iteration: a new tab in the
46
+ * same browser context (same cookies, its own page), disposed when the
47
+ * iteration ends. Web mode; an api runner is shared as it is.
48
+ */
49
+ fork?: () => Promise<StepRunner>;
37
50
  dispose: () => Promise<void>;
38
51
  }
39
52
  //# sourceMappingURL=step-runner.contract.d.ts.map