@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.
- package/README.md +1 -1
- package/dist/index.esm.js +762 -71
- package/dist/src/access/access-profile.contract.d.ts +4 -0
- package/dist/src/access/index.d.ts +1 -1
- package/dist/src/api-steps/send-request.use-case.d.ts +3 -2
- package/dist/src/browser-session/browser-profile.store.d.ts +52 -0
- package/dist/src/browser-session/browser.client.d.ts +8 -0
- package/dist/src/browser-session/index.d.ts +1 -0
- package/dist/src/crawl-events/crawl-event.contract.d.ts +8 -0
- package/dist/src/crawl-execution/bootstrap-session.use-case.d.ts +23 -2
- package/dist/src/crawl-execution/crawl-options.config.d.ts +27 -0
- package/dist/src/crawl-execution/rotating-runner.use-case.d.ts +9 -0
- package/dist/src/crawl-execution/run-crawl.use-case.d.ts +6 -2
- package/dist/src/crawl-execution/run-input-recipe.use-case.d.ts +11 -3
- package/dist/src/index.d.ts +5 -4
- package/dist/src/recipe-schema/index.d.ts +2 -2
- package/dist/src/recipe-schema/input-recipe.contract.d.ts +24 -1
- package/dist/src/record-sink/dedupe.policy.d.ts +19 -7
- package/dist/src/record-sink/index.d.ts +1 -0
- package/dist/src/step-flow/for-each.use-case.d.ts +4 -2
- package/dist/src/step-flow/host-throttle.policy.d.ts +49 -0
- package/dist/src/step-flow/index.d.ts +5 -0
- package/dist/src/step-flow/run-gate.policy.d.ts +12 -1
- package/dist/src/step-flow/step-runner.contract.d.ts +13 -0
- package/dist/src/step-flow/transport-retry.policy.d.ts +76 -0
- package/dist/src/web-steps/navigate.use-case.d.ts +3 -2
- package/dist/src/web-steps/run-web-step.use-case.d.ts +8 -1
- 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,
|
|
9
|
-
* the
|
|
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
|
|
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
|
|
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(
|
|
50
|
+
export declare function runInputRecipe(recipe: InputRecipe, output: OutputRecipe, deps: RecipeRunDependencies): Promise<RecipeReport>;
|
|
43
51
|
//# sourceMappingURL=run-input-recipe.use-case.d.ts.map
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
*
|
|
13
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|