@nitpicker/crawler 0.11.0 → 0.12.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/lib/archive/archive.d.ts +117 -2
- package/lib/archive/archive.js +147 -2
- package/lib/archive/cache/compute-archive-cache-key.d.ts +39 -0
- package/lib/archive/cache/compute-archive-cache-key.js +95 -0
- package/lib/archive/cache/extract-archive-to-cache.d.ts +43 -0
- package/lib/archive/cache/extract-archive-to-cache.js +309 -0
- package/lib/archive/cache/get-archive-cache-root.d.ts +20 -0
- package/lib/archive/cache/get-archive-cache-root.js +53 -0
- package/lib/archive/cache/is-archive-cache-disabled.d.ts +24 -0
- package/lib/archive/cache/is-archive-cache-disabled.js +34 -0
- package/lib/archive/cache/resolve-archive-cache-dir.d.ts +26 -0
- package/lib/archive/cache/resolve-archive-cache-dir.js +32 -0
- package/lib/archive/database.d.ts +216 -15
- package/lib/archive/database.js +1459 -938
- package/lib/archive/derive-lineage-from-parent.d.ts +37 -0
- package/lib/archive/derive-lineage-from-parent.js +42 -0
- package/lib/archive/get-failed-page-messages.d.ts +43 -0
- package/lib/archive/get-failed-page-messages.js +131 -0
- package/lib/archive/init-schema.js +153 -1
- package/lib/archive/is-inventory-source.d.ts +21 -0
- package/lib/archive/is-inventory-source.js +22 -0
- package/lib/archive/migrate-inventory-runs.d.ts +29 -0
- package/lib/archive/migrate-inventory-runs.js +52 -0
- package/lib/archive/types.d.ts +33 -0
- package/lib/classify-error-kind.d.ts +19 -0
- package/lib/classify-error-kind.js +122 -0
- package/lib/crawler/build-js-redirect-edge.d.ts +68 -0
- package/lib/crawler/build-js-redirect-edge.js +57 -0
- package/lib/crawler/build-redirect-event.d.ts +24 -0
- package/lib/crawler/build-redirect-event.js +28 -0
- package/lib/crawler/clear-dns-burned-host-cache.d.ts +6 -0
- package/lib/crawler/clear-dns-burned-host-cache.js +11 -0
- package/lib/crawler/crawler.d.ts +3 -1
- package/lib/crawler/crawler.js +655 -107
- package/lib/crawler/derive-js-redirect-target.d.ts +68 -0
- package/lib/crawler/derive-js-redirect-target.js +129 -0
- package/lib/crawler/derive-resource-source.d.ts +25 -15
- package/lib/crawler/derive-resource-source.js +28 -17
- package/lib/crawler/dns-burned-host-cache.d.ts +26 -0
- package/lib/crawler/dns-burned-host-cache.js +25 -0
- package/lib/crawler/dns-burned-host-short-circuit-counter.d.ts +13 -0
- package/lib/crawler/dns-burned-host-short-circuit-counter.js +11 -0
- package/lib/crawler/fetch-destination.d.ts +12 -4
- package/lib/crawler/fetch-destination.js +94 -16
- package/lib/crawler/is-js-redirect-error-shape.d.ts +40 -0
- package/lib/crawler/is-js-redirect-error-shape.js +53 -0
- package/lib/crawler/is-puppeteer-fallback-candidate.d.ts +16 -0
- package/lib/crawler/is-puppeteer-fallback-candidate.js +63 -0
- package/lib/crawler/link-list.d.ts +21 -1
- package/lib/crawler/link-list.js +23 -3
- package/lib/crawler/plan-sub-resource-emits.d.ts +63 -0
- package/lib/crawler/plan-sub-resource-emits.js +44 -0
- package/lib/crawler/preload-short-circuit-error.d.ts +22 -0
- package/lib/crawler/preload-short-circuit-error.js +25 -0
- package/lib/crawler/should-burn-host.d.ts +78 -0
- package/lib/crawler/should-burn-host.js +61 -0
- package/lib/crawler/should-get-fallback-on-head-failure.d.ts +38 -0
- package/lib/crawler/should-get-fallback-on-head-failure.js +46 -0
- package/lib/crawler/types.d.ts +107 -0
- package/lib/crawler-orchestrator.d.ts +13 -3
- package/lib/crawler-orchestrator.js +292 -69
- package/lib/crawler.d.ts +3 -2
- package/lib/crawler.js +3 -1
- package/lib/permanent-error-kinds.d.ts +43 -0
- package/lib/permanent-error-kinds.js +48 -0
- package/lib/types.d.ts +84 -0
- package/lib/utils/compute-file-sha256.d.ts +23 -0
- package/lib/utils/compute-file-sha256.js +55 -0
- package/lib/utils/error/emit-error-with-retry.d.ts +40 -0
- package/lib/utils/error/emit-error-with-retry.js +44 -0
- package/lib/utils/error/emit-error.d.ts +39 -0
- package/lib/utils/error/emit-error.js +41 -0
- package/package.json +11 -11
- package/lib/utils/error/error-emitter.d.ts +0 -18
- package/lib/utils/error/error-emitter.js +0 -29
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ordered message matchers. The first pattern that matches wins, so more
|
|
3
|
+
* specific transport causes (DNS, TLS, connection-*) are tested before the
|
|
4
|
+
* broader `protocol` / `timeout` buckets — e.g. `ETIMEDOUT` must classify as
|
|
5
|
+
* `connection-timeout`, not the page-level `timeout`, and a puppeteer
|
|
6
|
+
* `Protocol error` must not be swallowed by the `timeout` matcher.
|
|
7
|
+
*/
|
|
8
|
+
const MATCHERS = [
|
|
9
|
+
// `dns-transient` must be evaluated before `dns`: an `EAI_AGAIN` line also
|
|
10
|
+
// carries the `getaddrinfo` token, so the more specific transient pattern
|
|
11
|
+
// has to win. Splitting it out from `dns` keeps the DNS-burned host cache
|
|
12
|
+
// (which marks on `kind === 'dns'`) from punishing a host whose only sin
|
|
13
|
+
// was a local resolver hiccup.
|
|
14
|
+
{ kind: 'dns-transient', pattern: /EAI_AGAIN|\bEREFUSED\b/i },
|
|
15
|
+
{
|
|
16
|
+
kind: 'dns',
|
|
17
|
+
pattern: /ENOTFOUND|getaddrinfo|ERR_NAME_NOT_RESOLVED|ERR_NAME_RESOLUTION_FAILED/i,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
kind: 'tls',
|
|
21
|
+
// `Hostname/IP does not match certificate's altnames` is Node's
|
|
22
|
+
// node:tls hostname mismatch error and is emphatically a TLS issue;
|
|
23
|
+
// adding it here (alongside the OpenSSL / Chromium tokens) keeps
|
|
24
|
+
// hosts that serve the wrong-name cert (common with misconfigured
|
|
25
|
+
// edge / load-balancer setups) out of `unknown`. `altnames` is
|
|
26
|
+
// anchored to the preceding `certificate` token so a request whose
|
|
27
|
+
// error message merely mentions a path containing the substring
|
|
28
|
+
// `altnames` (e.g. `https://api.example.com/altnames/lookup` in a
|
|
29
|
+
// 5xx body) does NOT get misclassified into `tls` (which is a
|
|
30
|
+
// `PERMANENT_ERROR_KINDS` member — a false-positive would
|
|
31
|
+
// permanently exclude that page from `--retry-failed`).
|
|
32
|
+
pattern: /ERR_CERT|ERR_SSL|\bCERT_|SSL routines|ERR_BAD_SSL|UNABLE_TO_VERIFY|unable to verify|self.signed certificate|certificate has expired\s*$|\bERR_TLS|Hostname\/IP does not match certificate|certificate'?s? altnames/i,
|
|
33
|
+
},
|
|
34
|
+
{ kind: 'connection-refused', pattern: /ECONNREFUSED|ERR_CONNECTION_REFUSED/i },
|
|
35
|
+
{
|
|
36
|
+
kind: 'connection-reset',
|
|
37
|
+
pattern: /ECONNRESET|socket hang up|ERR_CONNECTION_RESET|ERR_CONNECTION_CLOSED|ERR_EMPTY_RESPONSE/i,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
kind: 'connection-timeout',
|
|
41
|
+
pattern: /ETIMEDOUT|ERR_CONNECTION_TIMED_OUT|ERR_TIMED_OUT/i,
|
|
42
|
+
},
|
|
43
|
+
// `local-network` is evaluated AFTER the connection-* matchers so a
|
|
44
|
+
// concrete cause (refused / reset / timeout) wins when both apply. Only
|
|
45
|
+
// "local network is unreachable / changed" symptoms — and the OS-level
|
|
46
|
+
// errors that surface them — land here. Short tokens (`EPIPE`, `EREFUSED`)
|
|
47
|
+
// are word-bounded so unrelated identifiers don't false-positive.
|
|
48
|
+
{
|
|
49
|
+
kind: 'local-network',
|
|
50
|
+
pattern: /ERR_INTERNET_DISCONNECTED|ERR_NETWORK_CHANGED|ERR_NETWORK_IO_SUSPENDED|ERR_ADDRESS_UNREACHABLE|ERR_NETWORK_UNREACHABLE|ENETUNREACH|EHOSTUNREACH|EADDRNOTAVAIL|ENOTCONN|\bEPIPE\b/i,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
kind: 'parse-error',
|
|
54
|
+
pattern: /Parse Error|Expected HTTP\/|Unexpected end of stream/i,
|
|
55
|
+
},
|
|
56
|
+
// `client-blocked` covers Chromium's ERR_BLOCKED_* family — the browser
|
|
57
|
+
// actively decided to reject the request (ad/tracker heuristics, CSP,
|
|
58
|
+
// CORB / ORB, administrator block list, fingerprinting protection,
|
|
59
|
+
// cleartext policy, …). Per the upstream `net/base/net_error_list.h`,
|
|
60
|
+
// `ERR_BLOCKED_BY_CLIENT` is documented as "The client chose to block
|
|
61
|
+
// the request." — i.e. the server was never the deciding party. Listed
|
|
62
|
+
// before `protocol` so puppeteer's generic "Protocol error" wrapper
|
|
63
|
+
// (which sometimes embeds the underlying net error code) is correctly
|
|
64
|
+
// attributed to the blocked layer rather than the protocol layer.
|
|
65
|
+
{
|
|
66
|
+
kind: 'client-blocked',
|
|
67
|
+
pattern: /ERR_BLOCKED_BY_CLIENT|ERR_BLOCKED_BY_ADMINISTRATOR|ERR_BLOCKED_IN_INCOGNITO_BY_ADMINISTRATOR|ERR_BLOCKED_BY_RESPONSE|ERR_BLOCKED_BY_CSP|ERR_BLOCKED_BY_ORB|ERR_BLOCKED_BY_FINGERPRINTING_PROTECTION|ERR_CLEARTEXT_NOT_PERMITTED|ERR_NETWORK_ACCESS_REVOKED/i,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
kind: 'protocol',
|
|
71
|
+
// `detached frame` is anchored to puppeteer's exact prefix
|
|
72
|
+
// `Attempted to use detached Frame` (its current Frame.ts
|
|
73
|
+
// emitter; the `i` flag below catches the lowercase variant
|
|
74
|
+
// automatically), not the bare two-token substring. The bare
|
|
75
|
+
// form would match unrelated diagnostics like a console message
|
|
76
|
+
// "detached frame ref leaked" echoed through a logger. The older
|
|
77
|
+
// Page-domain `frame (?:was |got )?detached` form is kept as a
|
|
78
|
+
// separate alternative because Chromium still surfaces that
|
|
79
|
+
// phrasing in some legacy code paths. Without one of these, the
|
|
80
|
+
// "Attempted to use detached Frame ..." messages observed on a
|
|
81
|
+
// real archive would slip into `unknown`.
|
|
82
|
+
pattern: /Protocol error|Target closed|Session closed|Execution context was destroyed|frame (?:was |got )?detached|Attempted to use detached frame|Navigating frame was detached|Cannot find context|Node with given id|Page\.\w+ returned/i,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
kind: 'timeout',
|
|
86
|
+
// `Timeout: https?:` matches the NetTimeoutError "Timeout: <url>"
|
|
87
|
+
// form. Looking for the URL-shaped tail (rather than anchoring at
|
|
88
|
+
// line start) is what lets us catch the beholder-wrapped variant
|
|
89
|
+
// `[Retried N times] Timeout: https://...` that gets stored in
|
|
90
|
+
// `crawl_errors` / `error.log` after retry exhaustion — the bare
|
|
91
|
+
// `^Timeout:` form would only fire on the immediate failure and
|
|
92
|
+
// miss every retry-exhausted record (the ones that actually land
|
|
93
|
+
// in the archive). Required for slow-server timeouts that
|
|
94
|
+
// previously fell into `unknown`.
|
|
95
|
+
pattern: /Race \d|Navigation timeout|timeout of \d+\s*ms exceeded|TimeoutError|Timed out|Timeout: https?:/i,
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
/**
|
|
99
|
+
* Classify a raw crawler/scraper error message into a coarse {@link ErrorKind}.
|
|
100
|
+
*
|
|
101
|
+
* Pure and deterministic: the same message always yields the same kind, which
|
|
102
|
+
* is why the kind is derived on read rather than persisted — it can be applied
|
|
103
|
+
* uniformly to freshly captured `crawl_errors`, legacy `error.log` lines, and
|
|
104
|
+
* `page_errors` alike.
|
|
105
|
+
* @param message - The raw error message (a single line is sufficient; the
|
|
106
|
+
* cause token such as `ENOTFOUND` or `Navigation timeout` lives there).
|
|
107
|
+
* @returns The matched kind, or `unknown` when no matcher applies.
|
|
108
|
+
* @example
|
|
109
|
+
* ```ts
|
|
110
|
+
* classifyErrorKind('getaddrinfo ENOTFOUND www.example.com'); // 'dns'
|
|
111
|
+
* classifyErrorKind('gave up after 3 retries — Race 180,000ms'); // 'timeout'
|
|
112
|
+
* classifyErrorKind('Protocol error (Page.reload): Target closed'); // 'protocol'
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
export function classifyErrorKind(message) {
|
|
116
|
+
for (const { kind, pattern } of MATCHERS) {
|
|
117
|
+
if (pattern.test(message)) {
|
|
118
|
+
return kind;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return 'unknown';
|
|
122
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { RedirectEdgeResult } from './types.js';
|
|
2
|
+
import type { PageData } from '@d-zero/beholder';
|
|
3
|
+
import type { ExURL } from '@d-zero/shared/parse-url';
|
|
4
|
+
/**
|
|
5
|
+
* Parameters for {@link buildJsRedirectEdge}.
|
|
6
|
+
*/
|
|
7
|
+
export interface BuildJsRedirectEdgeParams {
|
|
8
|
+
/** Source URL puppeteer was asked to navigate to. */
|
|
9
|
+
url: ExURL;
|
|
10
|
+
/**
|
|
11
|
+
* Whether `url` is external to the crawl scope. Propagated onto the
|
|
12
|
+
* synthesised `PageData` so downstream `linkList.done` classifies the
|
|
13
|
+
* source the same way the original scrape would have.
|
|
14
|
+
*/
|
|
15
|
+
isExternal: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Raw error message from `BrowserScrapeResult.error`. The rescue only
|
|
18
|
+
* fires when this matches the narrow `Page.goto returned null` sentinel
|
|
19
|
+
* (see {@link isJsRedirectErrorShape}) — anything else falls through
|
|
20
|
+
* so genuine browser failures surface unchanged.
|
|
21
|
+
*/
|
|
22
|
+
errorMessage: string | null | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Value of `BrowserScrapeResult.postNavigationUrl` — the URL puppeteer's
|
|
25
|
+
* `page.url()` reported after the throw. Passed to
|
|
26
|
+
* {@link deriveJsRedirectTarget} for canonicalisation, credential strip,
|
|
27
|
+
* and scheme filtering.
|
|
28
|
+
*/
|
|
29
|
+
postNavigationUrl: string | null | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* When present, the rescue builds the redirect-edge `PageData` by
|
|
32
|
+
* spreading this HEAD-pre-flight result and overriding `redirectPaths`.
|
|
33
|
+
* Use this on the HEAD-success-then-puppeteer-fail path so the source row
|
|
34
|
+
* carries the real HTTP-level status / content-type from HEAD. When
|
|
35
|
+
* omitted, the rescue builds a synthetic placeholder via
|
|
36
|
+
* {@link linkToPageData} with `status = -1` / `statusText = errorMessage`
|
|
37
|
+
* — used on the HEAD-fail-then-puppeteer-fallback path where there is
|
|
38
|
+
* no HEAD response to draw from.
|
|
39
|
+
*/
|
|
40
|
+
headCheckResult?: PageData;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build a JS-redirect `RedirectEdgeResult` when the browser-scrape error
|
|
44
|
+
* looks like puppeteer's `page.goto() === null` shape AND `page.url()`
|
|
45
|
+
* resolved to a meaningfully-different URL.
|
|
46
|
+
*
|
|
47
|
+
* **Why this helper exists** — both rescue call sites in
|
|
48
|
+
* `Crawler.#scrapePage` (the HEAD-success-then-puppeteer-fail tail and the
|
|
49
|
+
* HEAD-fail-then-puppeteer-fallback branch inside `#sendHeadRequest`) run
|
|
50
|
+
* the same three-step recipe: classify the error shape → derive a JS target
|
|
51
|
+
* from `page.url()` → synthesise a redirect-edge result. Inlining the recipe
|
|
52
|
+
* twice means a future change to `deriveJsRedirectTarget`'s contract or the
|
|
53
|
+
* sentinel string has to be applied to both copies in sync; collapsing into
|
|
54
|
+
* a single helper keeps the rescue's invariants in one place. The
|
|
55
|
+
* intentional difference between the two call sites (whether to fold a
|
|
56
|
+
* HEAD result into the synthesised PageData or to start from a
|
|
57
|
+
* `linkToPageData` placeholder) is reduced to a single optional parameter
|
|
58
|
+
* (`headCheckResult`), preserving both shapes without behavioural drift.
|
|
59
|
+
*
|
|
60
|
+
* The helper logs at `crawlerLog` whenever it fires so operators tailing
|
|
61
|
+
* `DEBUG=Nitpicker:Crawler` see exactly which URL got rescued and to which
|
|
62
|
+
* destination — without this trail the rescue is invisible to anyone
|
|
63
|
+
* debugging "why did this 200-OK source row land in the archive as a 301?".
|
|
64
|
+
* @param params - Inputs gathered at the call site.
|
|
65
|
+
* @returns A `RedirectEdgeResult` when the rescue applies, otherwise
|
|
66
|
+
* `null` (the caller should fall through to the existing error path).
|
|
67
|
+
*/
|
|
68
|
+
export declare function buildJsRedirectEdge(params: BuildJsRedirectEdgeParams): RedirectEdgeResult | null;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { crawlerLog } from '../debug.js';
|
|
2
|
+
import { deriveJsRedirectTarget } from './derive-js-redirect-target.js';
|
|
3
|
+
import { isJsRedirectErrorShape } from './is-js-redirect-error-shape.js';
|
|
4
|
+
import { linkToPageData } from './link-to-page-data.js';
|
|
5
|
+
/**
|
|
6
|
+
* Build a JS-redirect `RedirectEdgeResult` when the browser-scrape error
|
|
7
|
+
* looks like puppeteer's `page.goto() === null` shape AND `page.url()`
|
|
8
|
+
* resolved to a meaningfully-different URL.
|
|
9
|
+
*
|
|
10
|
+
* **Why this helper exists** — both rescue call sites in
|
|
11
|
+
* `Crawler.#scrapePage` (the HEAD-success-then-puppeteer-fail tail and the
|
|
12
|
+
* HEAD-fail-then-puppeteer-fallback branch inside `#sendHeadRequest`) run
|
|
13
|
+
* the same three-step recipe: classify the error shape → derive a JS target
|
|
14
|
+
* from `page.url()` → synthesise a redirect-edge result. Inlining the recipe
|
|
15
|
+
* twice means a future change to `deriveJsRedirectTarget`'s contract or the
|
|
16
|
+
* sentinel string has to be applied to both copies in sync; collapsing into
|
|
17
|
+
* a single helper keeps the rescue's invariants in one place. The
|
|
18
|
+
* intentional difference between the two call sites (whether to fold a
|
|
19
|
+
* HEAD result into the synthesised PageData or to start from a
|
|
20
|
+
* `linkToPageData` placeholder) is reduced to a single optional parameter
|
|
21
|
+
* (`headCheckResult`), preserving both shapes without behavioural drift.
|
|
22
|
+
*
|
|
23
|
+
* The helper logs at `crawlerLog` whenever it fires so operators tailing
|
|
24
|
+
* `DEBUG=Nitpicker:Crawler` see exactly which URL got rescued and to which
|
|
25
|
+
* destination — without this trail the rescue is invisible to anyone
|
|
26
|
+
* debugging "why did this 200-OK source row land in the archive as a 301?".
|
|
27
|
+
* @param params - Inputs gathered at the call site.
|
|
28
|
+
* @returns A `RedirectEdgeResult` when the rescue applies, otherwise
|
|
29
|
+
* `null` (the caller should fall through to the existing error path).
|
|
30
|
+
*/
|
|
31
|
+
export function buildJsRedirectEdge(params) {
|
|
32
|
+
if (!isJsRedirectErrorShape(params.errorMessage)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const jsRedirectTarget = deriveJsRedirectTarget(params.url.withoutHashAndAuth, params.postNavigationUrl);
|
|
36
|
+
if (jsRedirectTarget === null) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const pageData = params.headCheckResult
|
|
40
|
+
? { ...params.headCheckResult, redirectPaths: [jsRedirectTarget] }
|
|
41
|
+
: linkToPageData({
|
|
42
|
+
url: params.url,
|
|
43
|
+
isExternal: params.isExternal,
|
|
44
|
+
isLowerLayer: false,
|
|
45
|
+
dest: {
|
|
46
|
+
redirectPaths: [jsRedirectTarget],
|
|
47
|
+
status: -1,
|
|
48
|
+
statusText: typeof params.errorMessage === 'string' ? params.errorMessage : '',
|
|
49
|
+
contentType: null,
|
|
50
|
+
contentLength: null,
|
|
51
|
+
responseHeaders: null,
|
|
52
|
+
title: '',
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
crawlerLog('JS-redirect rescue fired for %s → %s (HEAD %s)', params.url.href, jsRedirectTarget, params.headCheckResult ? 'available' : 'absent');
|
|
56
|
+
return { type: 'redirect-edge', source: 'js-redirect', pageData };
|
|
57
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CrawlerEventTypes, InventoryMode } from './types.js';
|
|
2
|
+
import type { PageData } from '../utils/types/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Build the payload for the {@link CrawlerEventTypes.redirect} event so the
|
|
5
|
+
* `{ result, source }` shape is constructed in one place. Pure function —
|
|
6
|
+
* no I/O, no side effects, just the wiring that connects
|
|
7
|
+
* {@link derivePageSource} to the emit call.
|
|
8
|
+
*
|
|
9
|
+
* Exists so a unit test can pin the wiring directly. Before extraction
|
|
10
|
+
* the assembly lived inline in `Crawler.#scrape`, which meant the only
|
|
11
|
+
* way to catch "someone deletes the `source` field" or "someone passes
|
|
12
|
+
* `inventoryMode` where it should pass `derivePageSource(...)`" was the
|
|
13
|
+
* inventory E2E. Now a single-purpose spec exercises the construction.
|
|
14
|
+
*
|
|
15
|
+
* The originating URL passed in `pageUrlWithoutHashAndAuth` must be the
|
|
16
|
+
* URL the redirect chain STARTS from (the page being scraped), not the
|
|
17
|
+
* destination — see {@link Database.recordRedirect} JSDoc for why
|
|
18
|
+
* lineage propagates from origin, not from the destination.
|
|
19
|
+
* @param pageData - HEAD-resolved page data carrying the redirect chain.
|
|
20
|
+
* @param inventoryMode - Inventory-mode config from `CrawlerOptions.inventoryMode`, or `null` outside `--inventory`.
|
|
21
|
+
* @param pageUrlWithoutHashAndAuth - The originating page URL keyed by `withoutHashAndAuth`.
|
|
22
|
+
* @returns The exact event payload to feed `emit('redirect', ...)`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildRedirectEvent(pageData: PageData, inventoryMode: InventoryMode | null, pageUrlWithoutHashAndAuth: string): CrawlerEventTypes['redirect'];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { derivePageSource } from './derive-page-source.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build the payload for the {@link CrawlerEventTypes.redirect} event so the
|
|
4
|
+
* `{ result, source }` shape is constructed in one place. Pure function —
|
|
5
|
+
* no I/O, no side effects, just the wiring that connects
|
|
6
|
+
* {@link derivePageSource} to the emit call.
|
|
7
|
+
*
|
|
8
|
+
* Exists so a unit test can pin the wiring directly. Before extraction
|
|
9
|
+
* the assembly lived inline in `Crawler.#scrape`, which meant the only
|
|
10
|
+
* way to catch "someone deletes the `source` field" or "someone passes
|
|
11
|
+
* `inventoryMode` where it should pass `derivePageSource(...)`" was the
|
|
12
|
+
* inventory E2E. Now a single-purpose spec exercises the construction.
|
|
13
|
+
*
|
|
14
|
+
* The originating URL passed in `pageUrlWithoutHashAndAuth` must be the
|
|
15
|
+
* URL the redirect chain STARTS from (the page being scraped), not the
|
|
16
|
+
* destination — see {@link Database.recordRedirect} JSDoc for why
|
|
17
|
+
* lineage propagates from origin, not from the destination.
|
|
18
|
+
* @param pageData - HEAD-resolved page data carrying the redirect chain.
|
|
19
|
+
* @param inventoryMode - Inventory-mode config from `CrawlerOptions.inventoryMode`, or `null` outside `--inventory`.
|
|
20
|
+
* @param pageUrlWithoutHashAndAuth - The originating page URL keyed by `withoutHashAndAuth`.
|
|
21
|
+
* @returns The exact event payload to feed `emit('redirect', ...)`.
|
|
22
|
+
*/
|
|
23
|
+
export function buildRedirectEvent(pageData, inventoryMode, pageUrlWithoutHashAndAuth) {
|
|
24
|
+
return {
|
|
25
|
+
result: pageData,
|
|
26
|
+
source: derivePageSource(inventoryMode, pageUrlWithoutHashAndAuth),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clears the DNS-burned host cache and resets the short-circuit counter.
|
|
3
|
+
* Called between crawl sessions, alongside {@link clearDestinationCache}, to
|
|
4
|
+
* avoid leaking state from a previous session into a fresh one.
|
|
5
|
+
*/
|
|
6
|
+
export declare function clearDnsBurnedHostCache(): void;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { dnsBurnedHostCache } from './dns-burned-host-cache.js';
|
|
2
|
+
import { dnsBurnedHostShortCircuitCounter } from './dns-burned-host-short-circuit-counter.js';
|
|
3
|
+
/**
|
|
4
|
+
* Clears the DNS-burned host cache and resets the short-circuit counter.
|
|
5
|
+
* Called between crawl sessions, alongside {@link clearDestinationCache}, to
|
|
6
|
+
* avoid leaking state from a previous session into a fresh one.
|
|
7
|
+
*/
|
|
8
|
+
export function clearDnsBurnedHostCache() {
|
|
9
|
+
dnsBurnedHostCache.clear();
|
|
10
|
+
dnsBurnedHostShortCircuitCounter.count = 0;
|
|
11
|
+
}
|
package/lib/crawler/crawler.d.ts
CHANGED
|
@@ -71,7 +71,8 @@ export default class Crawler extends EventEmitter<CrawlerEventTypes> {
|
|
|
71
71
|
* before reaching the dealer so a URL that exists in both sources — which
|
|
72
72
|
* is common in append-mode when a new root coincides with a repromoted
|
|
73
73
|
* previously-external page — does not race on two parallel slots.
|
|
74
|
-
* @param urls - The list of root URLs to begin crawling from.
|
|
74
|
+
* @param urls - The list of root URLs to begin crawling from. May be empty
|
|
75
|
+
* when resumed pending URLs already exist (for example `--retry-failed`).
|
|
75
76
|
* @param opts - Optional overrides; currently only `recursive` is honoured.
|
|
76
77
|
* @param opts.recursive - When `false`, disables recursive discovery and forces list-mode.
|
|
77
78
|
* Defaults to the constructor option's `recursive` value.
|
|
@@ -80,6 +81,7 @@ export default class Crawler extends EventEmitter<CrawlerEventTypes> {
|
|
|
80
81
|
start(urls: ExURL[], opts?: {
|
|
81
82
|
recursive?: boolean;
|
|
82
83
|
}): void;
|
|
84
|
+
private _launchBrowserAndScrape;
|
|
83
85
|
/**
|
|
84
86
|
* The default maximum number of concurrent scraping processes.
|
|
85
87
|
*
|