@bluefields/fetcher 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/dist/asset-block-measure.d.ts +14 -0
- package/dist/asset-block-measure.js +82 -0
- package/dist/asset-block-measure.js.map +1 -0
- package/dist/browser-pool.d.ts +65 -0
- package/dist/browser-pool.js +161 -0
- package/dist/browser-pool.js.map +1 -0
- package/dist/cost.d.ts +51 -0
- package/dist/cost.js +54 -0
- package/dist/cost.js.map +1 -0
- package/dist/fetcher-brightdata.d.ts +52 -0
- package/dist/fetcher-brightdata.js +135 -0
- package/dist/fetcher-brightdata.js.map +1 -0
- package/dist/fetcher-escalating.d.ts +100 -0
- package/dist/fetcher-escalating.js +489 -0
- package/dist/fetcher-escalating.js.map +1 -0
- package/dist/fetcher-http.d.ts +81 -0
- package/dist/fetcher-http.js +395 -0
- package/dist/fetcher-http.js.map +1 -0
- package/dist/fetcher-patchright.d.ts +212 -0
- package/dist/fetcher-patchright.js +505 -0
- package/dist/fetcher-patchright.js.map +1 -0
- package/dist/fetcher-robots-gate.d.ts +60 -0
- package/dist/fetcher-robots-gate.js +87 -0
- package/dist/fetcher-robots-gate.js.map +1 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.js +219 -0
- package/dist/index.js.map +1 -0
- package/dist/internal-options.d.ts +59 -0
- package/dist/internal-options.js +16 -0
- package/dist/internal-options.js.map +1 -0
- package/dist/perf-harness.d.ts +105 -0
- package/dist/perf-harness.js +176 -0
- package/dist/perf-harness.js.map +1 -0
- package/dist/storage-memory.d.ts +18 -0
- package/dist/storage-memory.js +45 -0
- package/dist/storage-memory.js.map +1 -0
- package/dist/storage-r2.d.ts +44 -0
- package/dist/storage-r2.js +209 -0
- package/dist/storage-r2.js.map +1 -0
- package/dist/types.d.ts +231 -0
- package/dist/types.js +22 -0
- package/dist/types.js.map +1 -0
- package/package.json +54 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escalating fetcher — smart fetcher-tier escalation.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the three fetcher tiers and tries them cheapest-first, escalating only
|
|
5
|
+
* when a result looks like a JS shell or an anti-bot block. Easy pages stay on
|
|
6
|
+
* the cheap/fast HTTP tier (1 credit); only hard pages pay for the browser
|
|
7
|
+
* (3 credits) or unlocker (8 credits) tiers. This lifts coverage on JS-render /
|
|
8
|
+
* anti-bot pages while keeping blended cost low — and is actually CHEAPER than
|
|
9
|
+
* the previous default (which went straight to the browser tier whenever
|
|
10
|
+
* IPROYAL was configured).
|
|
11
|
+
*
|
|
12
|
+
* Four-layer rule: this is a Fetcher-layer concern — it only
|
|
13
|
+
* selects which fetcher to use and returns the winning tier's raw FetchResult
|
|
14
|
+
* unchanged (its real `fetcherId`), so downstream credit billing charges for
|
|
15
|
+
* the tier actually used. It does not extract, diff, or notify.
|
|
16
|
+
*
|
|
17
|
+
* Tiers are env-gated: the browser (Patchright/IPROYAL) and unlocker (Bright
|
|
18
|
+
* Data) tiers are built only when their credentials are present, so escalation
|
|
19
|
+
* is implicitly enabled by configuring those creds. In an env with neither,
|
|
20
|
+
* this behaves exactly like the plain HTTP fetcher.
|
|
21
|
+
*
|
|
22
|
+
* Browser-only requests (page actions or a screenshot) bypass escalation and go
|
|
23
|
+
* straight to the browser tier — HTTP rejects actions and can't screenshot, and
|
|
24
|
+
* the unlocker can't run actions.
|
|
25
|
+
*
|
|
26
|
+
* NOTE: this classifier is deliberately mirrored by the benchmark suite's
|
|
27
|
+
* coverage block-detector (same anti-bot fingerprints + thin-body threshold +
|
|
28
|
+
* empty-SPA-root detection), since the benchmark suite cannot be a dependency
|
|
29
|
+
* of the fetcher. Keep them in sync. A future refactor could hoist the shared
|
|
30
|
+
* heuristic into one module both import.
|
|
31
|
+
*/
|
|
32
|
+
import type { FetchResult, FetcherAdapter, WarmableFetcherAdapter } from './types.js';
|
|
33
|
+
/** Live fetch outcome — the live subset of block-detector's FetchOutcome. */
|
|
34
|
+
export type FetchOutcome = 'success' | 'empty_shell' | 'challenge' | 'blocked' | 'login_wall' | 'error';
|
|
35
|
+
/**
|
|
36
|
+
* Crude visible-text length: strip script/style/comment bodies and tags,
|
|
37
|
+
* collapse whitespace. An order-of-magnitude "is there a page here?" signal,
|
|
38
|
+
* not a parse. No deps.
|
|
39
|
+
*/
|
|
40
|
+
export declare function visibleTextLength(html: string): number;
|
|
41
|
+
/**
|
|
42
|
+
* True when the HTML carries a known SPA mount point (SPA_ROOT_IDS) that is
|
|
43
|
+
* (near-)empty — a framework shell that rendered no content server-side. This
|
|
44
|
+
* catches "padded" shells that clear MIN_CONTENT_CHARS on boilerplate alone, so
|
|
45
|
+
* the escalating fetcher hands them to a browser tier that can hydrate the app.
|
|
46
|
+
* Cheap string/regex scan, no deps. A false positive only costs one escalation
|
|
47
|
+
* (the higher tier returns equal-or-better content); a false negative leaves
|
|
48
|
+
* coverage on the floor — so this errs toward catching shells.
|
|
49
|
+
*/
|
|
50
|
+
export declare function hasEmptySpaRoot(html: string): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Classify a fetch result the same way the coverage benchmark does. 404s and
|
|
53
|
+
* 5xx are `error` (a real response — escalation won't help); 403/429/503 are a
|
|
54
|
+
* challenge (anti-bot markers) or a hard block; a thin 2xx body is a JS shell;
|
|
55
|
+
* a padded body whose SPA mount point is empty is also a shell.
|
|
56
|
+
*/
|
|
57
|
+
export declare function classifyFetchResult(r: FetchResult): FetchOutcome;
|
|
58
|
+
/** Outcomes a higher tier might fix. 404s, login walls, and success do NOT escalate. */
|
|
59
|
+
export declare function needsEscalation(o: FetchOutcome): boolean;
|
|
60
|
+
export interface EscalatingFetcherOptions {
|
|
61
|
+
/** Tier-0 HTTP adapter. Default: createHttpFetcherAdapter(). */
|
|
62
|
+
tier0?: FetcherAdapter;
|
|
63
|
+
/** Tier-1 browser adapter (Patchright). Default: tryCreatePatchrightAdapter(env) — may be null. */
|
|
64
|
+
tier1?: FetcherAdapter | null;
|
|
65
|
+
/** Tier-2 unlocker adapter (Bright Data). Default: tryCreateBrightDataAdapter(env) — may be null. */
|
|
66
|
+
tier2?: FetcherAdapter | null;
|
|
67
|
+
/** Env used to build the default tiers. Default: process.env. */
|
|
68
|
+
env?: NodeJS.ProcessEnv;
|
|
69
|
+
/** Tier-0 fast-fail budget (ms). Default: TIER0_FAST_FAIL_MS. */
|
|
70
|
+
tier0TimeoutMs?: number;
|
|
71
|
+
/**
|
|
72
|
+
* P2 hedged escalation (opt-in). When > 0, after a slow tier-0 has run this
|
|
73
|
+
* long without a good-enough result, tier-1 (browser) is launched IN PARALLEL
|
|
74
|
+
* rather than waiting the full tier-0 fast-fail — cutting the tail on
|
|
75
|
+
* slow-shell pages. Default: `HEDGE_TIER1_DELAY_MS` (0 = OFF → pure sequential
|
|
76
|
+
* escalation, the shipped behavior). Also settable via the
|
|
77
|
+
* `HEDGE_TIER1_DELAY_MS` env. Coverage-safe by construction (best result kept).
|
|
78
|
+
*/
|
|
79
|
+
hedgeDelayMs?: number;
|
|
80
|
+
/** Test seam: clock for the shared deadline. Default: Date.now. */
|
|
81
|
+
nowMsFn?: () => number;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Race a tier fetch against a hard wall-clock budget. Some fetch paths (notably
|
|
85
|
+
* `ssrfSafeFetch`'s undici wrapper) don't honor the AbortSignal during the
|
|
86
|
+
* connect/headers phase, so an adapter's own timeout can be silently ignored —
|
|
87
|
+
* the perf benchmark caught a tier-0 fetch running 90s past its 8s budget while
|
|
88
|
+
* the target slow-rolled its response headers. This guarantees the escalating
|
|
89
|
+
* fetcher honors the deadline regardless: on expiry it fires `onTimeout` (used to
|
|
90
|
+
* abort the abandoned tier so it releases resources — e.g. closes a browser) and
|
|
91
|
+
* rejects so the loop escalates (or breaks). `setTimer` is a test seam.
|
|
92
|
+
*/
|
|
93
|
+
export declare function raceTierBudget<T>(p: Promise<T>, budgetMs: number, onTimeout?: () => void, setTimer?: typeof setTimeout): Promise<T>;
|
|
94
|
+
/**
|
|
95
|
+
* Build an escalating fetcher. Tries tier-0 (HTTP) first and escalates to the
|
|
96
|
+
* browser then unlocker tiers only when the result is a shell/challenge/block.
|
|
97
|
+
* Returns the winning tier's FetchResult unchanged (real `fetcherId`, so the
|
|
98
|
+
* route bills for the tier actually used).
|
|
99
|
+
*/
|
|
100
|
+
export declare function createEscalatingFetcherAdapter(opts?: EscalatingFetcherOptions): WarmableFetcherAdapter;
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
/**
|
|
3
|
+
* Escalating fetcher — smart fetcher-tier escalation.
|
|
4
|
+
*
|
|
5
|
+
* Wraps the three fetcher tiers and tries them cheapest-first, escalating only
|
|
6
|
+
* when a result looks like a JS shell or an anti-bot block. Easy pages stay on
|
|
7
|
+
* the cheap/fast HTTP tier (1 credit); only hard pages pay for the browser
|
|
8
|
+
* (3 credits) or unlocker (8 credits) tiers. This lifts coverage on JS-render /
|
|
9
|
+
* anti-bot pages while keeping blended cost low — and is actually CHEAPER than
|
|
10
|
+
* the previous default (which went straight to the browser tier whenever
|
|
11
|
+
* IPROYAL was configured).
|
|
12
|
+
*
|
|
13
|
+
* Four-layer rule: this is a Fetcher-layer concern — it only
|
|
14
|
+
* selects which fetcher to use and returns the winning tier's raw FetchResult
|
|
15
|
+
* unchanged (its real `fetcherId`), so downstream credit billing charges for
|
|
16
|
+
* the tier actually used. It does not extract, diff, or notify.
|
|
17
|
+
*
|
|
18
|
+
* Tiers are env-gated: the browser (Patchright/IPROYAL) and unlocker (Bright
|
|
19
|
+
* Data) tiers are built only when their credentials are present, so escalation
|
|
20
|
+
* is implicitly enabled by configuring those creds. In an env with neither,
|
|
21
|
+
* this behaves exactly like the plain HTTP fetcher.
|
|
22
|
+
*
|
|
23
|
+
* Browser-only requests (page actions or a screenshot) bypass escalation and go
|
|
24
|
+
* straight to the browser tier — HTTP rejects actions and can't screenshot, and
|
|
25
|
+
* the unlocker can't run actions.
|
|
26
|
+
*
|
|
27
|
+
* NOTE: this classifier is deliberately mirrored by the benchmark suite's
|
|
28
|
+
* coverage block-detector (same anti-bot fingerprints + thin-body threshold +
|
|
29
|
+
* empty-SPA-root detection), since the benchmark suite cannot be a dependency
|
|
30
|
+
* of the fetcher. Keep them in sync. A future refactor could hoist the shared
|
|
31
|
+
* heuristic into one module both import.
|
|
32
|
+
*/
|
|
33
|
+
import { createLayerLogger, envInt } from '@bluefields/primitives';
|
|
34
|
+
import { tryCreateBrightDataAdapter } from './fetcher-brightdata.js';
|
|
35
|
+
import { createHttpFetcherAdapter } from './fetcher-http.js';
|
|
36
|
+
import { tryCreatePatchrightAdapter } from './fetcher-patchright.js';
|
|
37
|
+
const log = createLayerLogger('fetcher');
|
|
38
|
+
/** Below this many chars of *visible* text, a 2xx body is a JS shell, not content. */
|
|
39
|
+
const MIN_CONTENT_CHARS = 500;
|
|
40
|
+
/**
|
|
41
|
+
* Canonical SPA mount-point ids. When one of these containers is present but
|
|
42
|
+
* (near-)empty, the 2xx body is a framework shell whose real content renders
|
|
43
|
+
* client-side — even when the page is padded with enough boilerplate (nav,
|
|
44
|
+
* footer, <noscript>, cookie banner) to clear MIN_CONTENT_CHARS. The list is
|
|
45
|
+
* deliberately tight (well-known mount points only) so static pages that merely
|
|
46
|
+
* happen to use one of these ids with real content are not flagged.
|
|
47
|
+
*/
|
|
48
|
+
const SPA_ROOT_IDS = [
|
|
49
|
+
'root',
|
|
50
|
+
'__next',
|
|
51
|
+
'__nuxt',
|
|
52
|
+
'app',
|
|
53
|
+
'application',
|
|
54
|
+
'gatsby-focus-wrapper',
|
|
55
|
+
];
|
|
56
|
+
/** A known SPA root with fewer than this many chars of *visible* inner text is "empty". */
|
|
57
|
+
const ROOT_EMPTY_CHARS = 64;
|
|
58
|
+
/**
|
|
59
|
+
* Cap the inner-HTML scan for a root container. If the matching close tag isn't
|
|
60
|
+
* found within this many chars, the container is large (so it has content and is
|
|
61
|
+
* not a shell) — and the cap keeps a pathological page from stalling the
|
|
62
|
+
* classifier.
|
|
63
|
+
*/
|
|
64
|
+
const ROOT_SCAN_WINDOW = 20_000;
|
|
65
|
+
/** Anti-bot interstitial fingerprints (lowercased substring match). */
|
|
66
|
+
const CHALLENGE_MARKERS = [
|
|
67
|
+
'just a moment',
|
|
68
|
+
'checking your browser',
|
|
69
|
+
'cf-browser-verification',
|
|
70
|
+
'cf_chl_opt',
|
|
71
|
+
'attention required! | cloudflare',
|
|
72
|
+
'enable javascript and cookies to continue',
|
|
73
|
+
'datadome',
|
|
74
|
+
'px-captcha',
|
|
75
|
+
'captcha-delivery.com',
|
|
76
|
+
'_imperva_',
|
|
77
|
+
'are you a robot',
|
|
78
|
+
'verify you are human',
|
|
79
|
+
];
|
|
80
|
+
/** Login / auth-wall fingerprints (decisive only when the page is also thin). */
|
|
81
|
+
const LOGIN_MARKERS = [
|
|
82
|
+
'sign in to continue',
|
|
83
|
+
'please log in',
|
|
84
|
+
'log in to continue',
|
|
85
|
+
'you must be logged in',
|
|
86
|
+
'join linkedin',
|
|
87
|
+
'log in to linkedin',
|
|
88
|
+
'sign in to linkedin',
|
|
89
|
+
'log into facebook',
|
|
90
|
+
'log in to instagram',
|
|
91
|
+
];
|
|
92
|
+
/** Outcome rank — higher = closer to a real page; used to keep the best result. */
|
|
93
|
+
const OUTCOME_RANK = {
|
|
94
|
+
success: 7,
|
|
95
|
+
empty_shell: 5,
|
|
96
|
+
login_wall: 4,
|
|
97
|
+
challenge: 3,
|
|
98
|
+
blocked: 2,
|
|
99
|
+
error: 0,
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Crude visible-text length: strip script/style/comment bodies and tags,
|
|
103
|
+
* collapse whitespace. An order-of-magnitude "is there a page here?" signal,
|
|
104
|
+
* not a parse. No deps.
|
|
105
|
+
*/
|
|
106
|
+
export function visibleTextLength(html) {
|
|
107
|
+
return html
|
|
108
|
+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, ' ')
|
|
109
|
+
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, ' ')
|
|
110
|
+
.replace(/<!--[\s\S]*?-->/g, ' ')
|
|
111
|
+
.replace(/<[^>]+>/g, ' ')
|
|
112
|
+
.replace(/ /gi, ' ')
|
|
113
|
+
.replace(/\s+/g, ' ')
|
|
114
|
+
.trim().length;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Inner HTML of an element of `tag` whose content starts at `innerStart`, found
|
|
118
|
+
* by depth-matching open/close tags of the same name. Returns null when the
|
|
119
|
+
* matching close isn't reached within ROOT_SCAN_WINDOW chars — i.e. the
|
|
120
|
+
* container is large, so it has content (not a shell). Script/style/comment
|
|
121
|
+
* bodies are stripped first so `<div>`-like strings inside them don't skew the
|
|
122
|
+
* depth count. No DOM parser (stdlib only); self-closing roots (not
|
|
123
|
+
* valid HTML for div/main/section) are not handled — they read as "has content".
|
|
124
|
+
*/
|
|
125
|
+
function extractRootInnerHtml(html, tag, innerStart) {
|
|
126
|
+
const slice = html
|
|
127
|
+
.slice(innerStart, innerStart + ROOT_SCAN_WINDOW)
|
|
128
|
+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, ' ')
|
|
129
|
+
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, ' ')
|
|
130
|
+
.replace(/<!--[\s\S]*?-->/g, ' ');
|
|
131
|
+
// tag comes from a [a-z][a-z0-9]* capture, so it carries no regex metachars.
|
|
132
|
+
const tagRe = new RegExp(`<(/?)${tag}\\b`, 'gi');
|
|
133
|
+
let depth = 1;
|
|
134
|
+
let m = tagRe.exec(slice);
|
|
135
|
+
while (m !== null) {
|
|
136
|
+
if (m[1] === '/') {
|
|
137
|
+
depth -= 1;
|
|
138
|
+
if (depth === 0)
|
|
139
|
+
return slice.slice(0, m.index);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
depth += 1;
|
|
143
|
+
}
|
|
144
|
+
m = tagRe.exec(slice);
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* True when the HTML carries a known SPA mount point (SPA_ROOT_IDS) that is
|
|
150
|
+
* (near-)empty — a framework shell that rendered no content server-side. This
|
|
151
|
+
* catches "padded" shells that clear MIN_CONTENT_CHARS on boilerplate alone, so
|
|
152
|
+
* the escalating fetcher hands them to a browser tier that can hydrate the app.
|
|
153
|
+
* Cheap string/regex scan, no deps. A false positive only costs one escalation
|
|
154
|
+
* (the higher tier returns equal-or-better content); a false negative leaves
|
|
155
|
+
* coverage on the floor — so this errs toward catching shells.
|
|
156
|
+
*/
|
|
157
|
+
export function hasEmptySpaRoot(html) {
|
|
158
|
+
const idAlternation = SPA_ROOT_IDS.join('|');
|
|
159
|
+
const openRe = new RegExp(`<([a-z][a-z0-9]*)\\b[^>]*\\bid\\s*=\\s*["'](?:${idAlternation})["'][^>]*>`, 'gi');
|
|
160
|
+
let m = openRe.exec(html);
|
|
161
|
+
while (m !== null) {
|
|
162
|
+
const tag = m[1]?.toLowerCase();
|
|
163
|
+
if (tag) {
|
|
164
|
+
const inner = extractRootInnerHtml(html, tag, m.index + m[0].length);
|
|
165
|
+
// inner === null → container too large to be a shell (has content).
|
|
166
|
+
if (inner !== null && visibleTextLength(inner) < ROOT_EMPTY_CHARS)
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
m = openRe.exec(html);
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Classify a fetch result the same way the coverage benchmark does. 404s and
|
|
175
|
+
* 5xx are `error` (a real response — escalation won't help); 403/429/503 are a
|
|
176
|
+
* challenge (anti-bot markers) or a hard block; a thin 2xx body is a JS shell;
|
|
177
|
+
* a padded body whose SPA mount point is empty is also a shell.
|
|
178
|
+
*/
|
|
179
|
+
export function classifyFetchResult(r) {
|
|
180
|
+
const status = r.responseStatus;
|
|
181
|
+
const bodyLower = r.html.toLowerCase();
|
|
182
|
+
const isChallenge = CHALLENGE_MARKERS.some((m) => bodyLower.includes(m));
|
|
183
|
+
if (status === 401)
|
|
184
|
+
return 'login_wall';
|
|
185
|
+
if (status === 403 || status === 429 || status === 503) {
|
|
186
|
+
return isChallenge ? 'challenge' : 'blocked';
|
|
187
|
+
}
|
|
188
|
+
if (status >= 400)
|
|
189
|
+
return 'error'; // 404 / 5xx — a real response, not escalation-worthy
|
|
190
|
+
const textLen = visibleTextLength(r.html);
|
|
191
|
+
if (isChallenge && textLen < MIN_CONTENT_CHARS)
|
|
192
|
+
return 'challenge';
|
|
193
|
+
const looksLikeLogin = LOGIN_MARKERS.some((m) => bodyLower.includes(m));
|
|
194
|
+
if (looksLikeLogin && textLen < MIN_CONTENT_CHARS)
|
|
195
|
+
return 'login_wall';
|
|
196
|
+
if (textLen < MIN_CONTENT_CHARS)
|
|
197
|
+
return 'empty_shell';
|
|
198
|
+
// Padded shell: boilerplate clears the thin-body threshold, but the SPA mount
|
|
199
|
+
// point is empty — real content never rendered server-side. Escalate so a
|
|
200
|
+
// browser tier can hydrate it.
|
|
201
|
+
if (hasEmptySpaRoot(r.html))
|
|
202
|
+
return 'empty_shell';
|
|
203
|
+
return 'success';
|
|
204
|
+
}
|
|
205
|
+
/** Outcomes a higher tier might fix. 404s, login walls, and success do NOT escalate. */
|
|
206
|
+
export function needsEscalation(o) {
|
|
207
|
+
return o === 'empty_shell' || o === 'challenge' || o === 'blocked';
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Tier-0 (HTTP) fast-fail budget. A slow/dead static fetch escalates to the
|
|
211
|
+
* browser tier after this long instead of burning the whole request budget
|
|
212
|
+
* before tier-1 even starts — the #1 tail-latency cause (FETCH_PERF_PROPOSAL P0).
|
|
213
|
+
* Healthy HTTP returns well under this, so easy pages are unaffected.
|
|
214
|
+
*
|
|
215
|
+
* Perf round (L2): lowered 8s → 5s and made it env-overridable via
|
|
216
|
+
* `BLUEFIELDS_TIER0_FAST_FAIL_MS` (or per-call `opts.tier0TimeoutMs`). Coverage-safe:
|
|
217
|
+
* abandoning a slow tier-0 only ESCALATES it to the browser (which returns
|
|
218
|
+
* equal-or-better content), never drops coverage — so the only cost is one extra
|
|
219
|
+
* escalation for a page that genuinely takes 5–8s on tier-0. With L1 now escalating
|
|
220
|
+
* detectable blocks at time-to-headers, the 5–8s band is dominated by slow-doomed
|
|
221
|
+
* fetches, not valid slow-static pages. The fixture harness shows p95 11000→9300ms
|
|
222
|
+
* with coverage held at the floor; confirm on real-traffic tier-0 latencies before
|
|
223
|
+
* relying on 5s in prod (set the env back to 8000 to revert instantly).
|
|
224
|
+
*/
|
|
225
|
+
const TIER0_FAST_FAIL_MS = 5_000;
|
|
226
|
+
/** Overall request budget assumed when the caller passes no `timeoutMs`. */
|
|
227
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
228
|
+
/**
|
|
229
|
+
* Race a tier fetch against a hard wall-clock budget. Some fetch paths (notably
|
|
230
|
+
* `ssrfSafeFetch`'s undici wrapper) don't honor the AbortSignal during the
|
|
231
|
+
* connect/headers phase, so an adapter's own timeout can be silently ignored —
|
|
232
|
+
* the perf benchmark caught a tier-0 fetch running 90s past its 8s budget while
|
|
233
|
+
* the target slow-rolled its response headers. This guarantees the escalating
|
|
234
|
+
* fetcher honors the deadline regardless: on expiry it fires `onTimeout` (used to
|
|
235
|
+
* abort the abandoned tier so it releases resources — e.g. closes a browser) and
|
|
236
|
+
* rejects so the loop escalates (or breaks). `setTimer` is a test seam.
|
|
237
|
+
*/
|
|
238
|
+
export function raceTierBudget(p, budgetMs, onTimeout, setTimer = setTimeout) {
|
|
239
|
+
return new Promise((resolve, reject) => {
|
|
240
|
+
const timer = setTimer(() => {
|
|
241
|
+
onTimeout?.();
|
|
242
|
+
reject(new Error(`tier fetch exceeded ${budgetMs}ms budget`));
|
|
243
|
+
}, budgetMs);
|
|
244
|
+
p.then((v) => {
|
|
245
|
+
clearTimeout(timer);
|
|
246
|
+
resolve(v);
|
|
247
|
+
}, (e) => {
|
|
248
|
+
clearTimeout(timer);
|
|
249
|
+
reject(e);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Default hedge delay (ms) for P2 hedged escalation. 0 = OFF — pure sequential
|
|
255
|
+
* escalation, the shipped behavior. Overridden per-call by `hedgeDelayMs` or
|
|
256
|
+
* process-wide by the `HEDGE_TIER1_DELAY_MS` env.
|
|
257
|
+
*/
|
|
258
|
+
const HEDGE_TIER1_DELAY_MS = 0;
|
|
259
|
+
/** Promise-based delay — the hedge timer. */
|
|
260
|
+
function delayMs(ms) {
|
|
261
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
262
|
+
}
|
|
263
|
+
/** A stage produced a good-enough result (a real page, not a shell/challenge/block). */
|
|
264
|
+
function isGoodEnough(o) {
|
|
265
|
+
return o !== null && 'outcome' in o && !needsEscalation(o.outcome);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Resolve as soon as one stage returns a good-enough result (not a
|
|
269
|
+
* shell/challenge/block), aborting the losers; otherwise wait for all stages and
|
|
270
|
+
* return the best by `OUTCOME_RANK`. This mirrors the sequential loop's "stop
|
|
271
|
+
* when the best is good enough" rule, so a hedged race never yields lower
|
|
272
|
+
* coverage than the sequential ladder. Stages are pre-wrapped to never reject
|
|
273
|
+
* (`{error}`), but a rejection is tolerated defensively.
|
|
274
|
+
*/
|
|
275
|
+
function firstSuccessOrBest(stages) {
|
|
276
|
+
return new Promise((resolve) => {
|
|
277
|
+
if (stages.length === 0) {
|
|
278
|
+
resolve({ best: null, lastError: undefined });
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
let remaining = stages.length;
|
|
282
|
+
let best = null;
|
|
283
|
+
let lastError;
|
|
284
|
+
let done = false;
|
|
285
|
+
const settle = () => {
|
|
286
|
+
if (!done) {
|
|
287
|
+
done = true;
|
|
288
|
+
resolve({ best, lastError });
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
for (const s of stages) {
|
|
292
|
+
s.p.then((res) => {
|
|
293
|
+
if (done)
|
|
294
|
+
return;
|
|
295
|
+
remaining -= 1;
|
|
296
|
+
if ('error' in res) {
|
|
297
|
+
lastError = res.error;
|
|
298
|
+
}
|
|
299
|
+
else if (!('skipped' in res)) {
|
|
300
|
+
if (!best || OUTCOME_RANK[res.outcome] > OUTCOME_RANK[best.outcome])
|
|
301
|
+
best = res;
|
|
302
|
+
if (!needsEscalation(res.outcome)) {
|
|
303
|
+
// Good enough — take it and abandon the losers.
|
|
304
|
+
for (const other of stages)
|
|
305
|
+
if (other !== s)
|
|
306
|
+
other.abort.abort();
|
|
307
|
+
settle();
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (remaining === 0)
|
|
312
|
+
settle();
|
|
313
|
+
}, (err) => {
|
|
314
|
+
if (done)
|
|
315
|
+
return;
|
|
316
|
+
remaining -= 1;
|
|
317
|
+
lastError = err;
|
|
318
|
+
if (remaining === 0)
|
|
319
|
+
settle();
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Build an escalating fetcher. Tries tier-0 (HTTP) first and escalates to the
|
|
326
|
+
* browser then unlocker tiers only when the result is a shell/challenge/block.
|
|
327
|
+
* Returns the winning tier's FetchResult unchanged (real `fetcherId`, so the
|
|
328
|
+
* route bills for the tier actually used).
|
|
329
|
+
*/
|
|
330
|
+
export function createEscalatingFetcherAdapter(opts = {}) {
|
|
331
|
+
const env = opts.env ?? process.env;
|
|
332
|
+
const tier0 = opts.tier0 ?? createHttpFetcherAdapter();
|
|
333
|
+
const tier1 = opts.tier1 !== undefined ? opts.tier1 : tryCreatePatchrightAdapter(env);
|
|
334
|
+
const tier2 = opts.tier2 !== undefined ? opts.tier2 : tryCreateBrightDataAdapter(env);
|
|
335
|
+
const tier0FastFailMs = opts.tier0TimeoutMs ?? envInt('BLUEFIELDS_TIER0_FAST_FAIL_MS', TIER0_FAST_FAIL_MS);
|
|
336
|
+
const now = opts.nowMsFn ?? Date.now;
|
|
337
|
+
// P2 hedged escalation delay: option wins, else env, else OFF. NaN/≤0 → OFF.
|
|
338
|
+
const hedgeDelayMs = (() => {
|
|
339
|
+
const v = opts.hedgeDelayMs ?? Number(env.HEDGE_TIER1_DELAY_MS ?? HEDGE_TIER1_DELAY_MS);
|
|
340
|
+
return Number.isFinite(v) && v > 0 ? v : 0;
|
|
341
|
+
})();
|
|
342
|
+
const ladder = [{ adapter: tier0, tier: 0 }];
|
|
343
|
+
if (tier1)
|
|
344
|
+
ladder.push({ adapter: tier1, tier: 1 });
|
|
345
|
+
if (tier2)
|
|
346
|
+
ladder.push({ adapter: tier2, tier: 2 });
|
|
347
|
+
return {
|
|
348
|
+
/**
|
|
349
|
+
* L4: pre-warm the browser tier's pool at boot so the first escalating fetch that
|
|
350
|
+
* hits tier-1 doesn't pay the cold Chromium launch. Best-effort; no-op when tier-1
|
|
351
|
+
* has no pool (PATCHRIGHT_POOL unset) or no browser tier is configured. Call once at
|
|
352
|
+
* app boot, fire-and-forget (`void fetcher.warmUp?.()` after construction).
|
|
353
|
+
*/
|
|
354
|
+
async warmUp() {
|
|
355
|
+
await tier1?.warmUp?.();
|
|
356
|
+
},
|
|
357
|
+
async fetch(url, options) {
|
|
358
|
+
// Page actions + screenshots require the browser tier; HTTP rejects
|
|
359
|
+
// actions and can't screenshot, and the unlocker can't run actions.
|
|
360
|
+
// Falling back to tier-0 preserves today's behavior when no browser tier
|
|
361
|
+
// is configured (HTTP throws on actions / returns a null screenshot).
|
|
362
|
+
const needsBrowser = (options?.actions?.length ?? 0) > 0 || options?.captureScreenshot === true;
|
|
363
|
+
if (needsBrowser) {
|
|
364
|
+
return (tier1 ?? tier0).fetch(url, options);
|
|
365
|
+
}
|
|
366
|
+
let best = null;
|
|
367
|
+
let lastError = null;
|
|
368
|
+
// Shared, decrementing deadline across the ladder so worst-case wall-clock
|
|
369
|
+
// ≈ the request budget, NOT the sum of per-tier timeouts (FETCH_PERF_PROPOSAL
|
|
370
|
+
// P0). Tier-0 additionally fast-fails so a dead static fetch escalates
|
|
371
|
+
// quickly; later tiers get whatever budget remains.
|
|
372
|
+
const deadlineMs = now() + (options?.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS);
|
|
373
|
+
// Run one tier with its budget under a hard wall-clock bound (independent of
|
|
374
|
+
// the abort signal, which some fetch paths ignore) so one slow tier can't
|
|
375
|
+
// eat the whole budget. On expiry / abandonment we abort the tier so it
|
|
376
|
+
// frees resources (the browser tier closes its context; HTTP/unlocker
|
|
377
|
+
// ignore it). The __fullRender hint renders SPA shells in browser tiers
|
|
378
|
+
// when the customer gave no waitFor. These internal fields are NOT part of
|
|
379
|
+
// the public FetchOptions contract.
|
|
380
|
+
const runStage = async (adapter, tier, budgetMs, abort) => {
|
|
381
|
+
const tierOptions = {
|
|
382
|
+
...(options ?? {}),
|
|
383
|
+
timeoutMs: budgetMs,
|
|
384
|
+
__abortSignal: abort.signal,
|
|
385
|
+
// L1: only tier-0 opts into early-block escalation (skip draining a doomed
|
|
386
|
+
// 403/429/503 body). Tiers 1/2 ignore the flag; a standalone HTTP caller
|
|
387
|
+
// never sets it. Coverage-safe — see internal-options.ts / classifyFetchResult.
|
|
388
|
+
...(tier === 0 ? { __earlyClassify: true } : {}),
|
|
389
|
+
...(tier > 0 && !options?.waitFor ? { __fullRender: true } : {}),
|
|
390
|
+
};
|
|
391
|
+
const result = await raceTierBudget(adapter.fetch(url, tierOptions), budgetMs, () => abort.abort());
|
|
392
|
+
return { result, outcome: classifyFetchResult(result), tier };
|
|
393
|
+
};
|
|
394
|
+
if (hedgeDelayMs > 0 && tier1) {
|
|
395
|
+
// ── P2 hedged escalation ──────────────────────────────────────────────
|
|
396
|
+
// Launch tier-1 in parallel with a slow tier-0 instead of waiting the
|
|
397
|
+
// full tier-0 fast-fail. Eligible only when both tiers exist (browser-only
|
|
398
|
+
// requests already returned above). Coverage-safe: firstSuccessOrBest
|
|
399
|
+
// keeps the best result and mirrors the sequential "stop when good enough"
|
|
400
|
+
// rule, so coverage ≥ the sequential ladder.
|
|
401
|
+
const t0Budget = Math.min(deadlineMs - now(), tier0FastFailMs);
|
|
402
|
+
const t0Abort = new AbortController();
|
|
403
|
+
let t0Settled = null;
|
|
404
|
+
const p0 = runStage(tier0, 0, t0Budget, t0Abort).then((s) => {
|
|
405
|
+
t0Settled = s;
|
|
406
|
+
return s;
|
|
407
|
+
}, (e) => {
|
|
408
|
+
const r = { error: e };
|
|
409
|
+
t0Settled = r;
|
|
410
|
+
return r;
|
|
411
|
+
});
|
|
412
|
+
const t1Abort = new AbortController();
|
|
413
|
+
const p1 = (async () => {
|
|
414
|
+
// Start tier-1 on the earlier of: tier-0 settling, or the hedge delay.
|
|
415
|
+
await Promise.race([p0, delayMs(hedgeDelayMs)]);
|
|
416
|
+
// If tier-0 already produced a good-enough result, don't spend tier-1.
|
|
417
|
+
if (isGoodEnough(t0Settled)) {
|
|
418
|
+
return { skipped: true };
|
|
419
|
+
}
|
|
420
|
+
const rem = deadlineMs - now();
|
|
421
|
+
if (rem <= 0)
|
|
422
|
+
return { skipped: true };
|
|
423
|
+
return runStage(tier1, 1, rem, t1Abort).catch((e) => ({ error: e }));
|
|
424
|
+
})();
|
|
425
|
+
const picked = await firstSuccessOrBest([
|
|
426
|
+
{ p: p0, abort: t0Abort },
|
|
427
|
+
{ p: p1, abort: t1Abort },
|
|
428
|
+
]);
|
|
429
|
+
best = picked.best;
|
|
430
|
+
if (picked.lastError !== undefined)
|
|
431
|
+
lastError = picked.lastError;
|
|
432
|
+
// Tier-2 sequentially if tier-0/tier-1 didn't satisfy and budget remains.
|
|
433
|
+
if ((!best || needsEscalation(best.outcome)) && tier2) {
|
|
434
|
+
const rem = deadlineMs - now();
|
|
435
|
+
if (rem > 0) {
|
|
436
|
+
try {
|
|
437
|
+
const staged = await runStage(tier2, 2, rem, new AbortController());
|
|
438
|
+
if (!best || OUTCOME_RANK[staged.outcome] > OUTCOME_RANK[best.outcome])
|
|
439
|
+
best = staged;
|
|
440
|
+
}
|
|
441
|
+
catch (err) {
|
|
442
|
+
lastError = err;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
// ── Sequential ladder (shipped default) ───────────────────────────────
|
|
449
|
+
for (const { adapter, tier } of ladder) {
|
|
450
|
+
// Stop once the best result so far is good enough (not a shell/block).
|
|
451
|
+
if (best && !needsEscalation(best.outcome))
|
|
452
|
+
break;
|
|
453
|
+
const remainingMs = deadlineMs - now();
|
|
454
|
+
if (remainingMs <= 0)
|
|
455
|
+
break; // shared budget exhausted — honor the deadline
|
|
456
|
+
// Tier-0 fast-fails ONLY when there's a higher tier to escalate to. If tier-0 is
|
|
457
|
+
// the only tier (HTTP-only deployment — no browser/unlocker configured), capping it
|
|
458
|
+
// would needlessly abandon a slow-but-valid static page with nowhere to go and then
|
|
459
|
+
// throw. Give it the full remaining deadline instead. (Adversarial-review fix: this
|
|
460
|
+
// closes the HTTP-only 5–8s cliff that lowering the fast-fail to 5s would widen.)
|
|
461
|
+
const hasHigherTier = ladder.length > 1;
|
|
462
|
+
const tierTimeoutMs = tier === 0 && hasHigherTier ? Math.min(remainingMs, tier0FastFailMs) : remainingMs;
|
|
463
|
+
try {
|
|
464
|
+
const staged = await runStage(adapter, tier, tierTimeoutMs, new AbortController());
|
|
465
|
+
if (!best || OUTCOME_RANK[staged.outcome] > OUTCOME_RANK[best.outcome])
|
|
466
|
+
best = staged;
|
|
467
|
+
}
|
|
468
|
+
catch (err) {
|
|
469
|
+
// Transport throw (DNS/TLS/socket/timeout): try the next tier (a
|
|
470
|
+
// different network path may succeed). Keep it in case all fail.
|
|
471
|
+
lastError = err;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (best) {
|
|
476
|
+
if (best.tier > 0) {
|
|
477
|
+
log.debug({
|
|
478
|
+
winning_tier: best.tier,
|
|
479
|
+
outcome: best.outcome,
|
|
480
|
+
fetcher_id: best.result.fetcherId,
|
|
481
|
+
}, 'fetch escalated to a higher tier');
|
|
482
|
+
}
|
|
483
|
+
return best.result;
|
|
484
|
+
}
|
|
485
|
+
throw lastError ?? new Error('escalating fetcher: no tier produced a result');
|
|
486
|
+
},
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
//# sourceMappingURL=fetcher-escalating.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetcher-escalating.js","sourceRoot":"","sources":["../src/fetcher-escalating.ts"],"names":[],"mappings":"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"}
|