@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.
Files changed (44) hide show
  1. package/LICENSE +661 -0
  2. package/dist/asset-block-measure.d.ts +14 -0
  3. package/dist/asset-block-measure.js +82 -0
  4. package/dist/asset-block-measure.js.map +1 -0
  5. package/dist/browser-pool.d.ts +65 -0
  6. package/dist/browser-pool.js +161 -0
  7. package/dist/browser-pool.js.map +1 -0
  8. package/dist/cost.d.ts +51 -0
  9. package/dist/cost.js +54 -0
  10. package/dist/cost.js.map +1 -0
  11. package/dist/fetcher-brightdata.d.ts +52 -0
  12. package/dist/fetcher-brightdata.js +135 -0
  13. package/dist/fetcher-brightdata.js.map +1 -0
  14. package/dist/fetcher-escalating.d.ts +100 -0
  15. package/dist/fetcher-escalating.js +489 -0
  16. package/dist/fetcher-escalating.js.map +1 -0
  17. package/dist/fetcher-http.d.ts +81 -0
  18. package/dist/fetcher-http.js +395 -0
  19. package/dist/fetcher-http.js.map +1 -0
  20. package/dist/fetcher-patchright.d.ts +212 -0
  21. package/dist/fetcher-patchright.js +505 -0
  22. package/dist/fetcher-patchright.js.map +1 -0
  23. package/dist/fetcher-robots-gate.d.ts +60 -0
  24. package/dist/fetcher-robots-gate.js +87 -0
  25. package/dist/fetcher-robots-gate.js.map +1 -0
  26. package/dist/index.d.ts +93 -0
  27. package/dist/index.js +219 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/internal-options.d.ts +59 -0
  30. package/dist/internal-options.js +16 -0
  31. package/dist/internal-options.js.map +1 -0
  32. package/dist/perf-harness.d.ts +105 -0
  33. package/dist/perf-harness.js +176 -0
  34. package/dist/perf-harness.js.map +1 -0
  35. package/dist/storage-memory.d.ts +18 -0
  36. package/dist/storage-memory.js +45 -0
  37. package/dist/storage-memory.js.map +1 -0
  38. package/dist/storage-r2.d.ts +44 -0
  39. package/dist/storage-r2.js +209 -0
  40. package/dist/storage-r2.js.map +1 -0
  41. package/dist/types.d.ts +231 -0
  42. package/dist/types.js +22 -0
  43. package/dist/types.js.map +1 -0
  44. 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(/&nbsp;/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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"}