@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.
Files changed (75) hide show
  1. package/lib/archive/archive.d.ts +117 -2
  2. package/lib/archive/archive.js +147 -2
  3. package/lib/archive/cache/compute-archive-cache-key.d.ts +39 -0
  4. package/lib/archive/cache/compute-archive-cache-key.js +95 -0
  5. package/lib/archive/cache/extract-archive-to-cache.d.ts +43 -0
  6. package/lib/archive/cache/extract-archive-to-cache.js +309 -0
  7. package/lib/archive/cache/get-archive-cache-root.d.ts +20 -0
  8. package/lib/archive/cache/get-archive-cache-root.js +53 -0
  9. package/lib/archive/cache/is-archive-cache-disabled.d.ts +24 -0
  10. package/lib/archive/cache/is-archive-cache-disabled.js +34 -0
  11. package/lib/archive/cache/resolve-archive-cache-dir.d.ts +26 -0
  12. package/lib/archive/cache/resolve-archive-cache-dir.js +32 -0
  13. package/lib/archive/database.d.ts +216 -15
  14. package/lib/archive/database.js +1459 -938
  15. package/lib/archive/derive-lineage-from-parent.d.ts +37 -0
  16. package/lib/archive/derive-lineage-from-parent.js +42 -0
  17. package/lib/archive/get-failed-page-messages.d.ts +43 -0
  18. package/lib/archive/get-failed-page-messages.js +131 -0
  19. package/lib/archive/init-schema.js +153 -1
  20. package/lib/archive/is-inventory-source.d.ts +21 -0
  21. package/lib/archive/is-inventory-source.js +22 -0
  22. package/lib/archive/migrate-inventory-runs.d.ts +29 -0
  23. package/lib/archive/migrate-inventory-runs.js +52 -0
  24. package/lib/archive/types.d.ts +33 -0
  25. package/lib/classify-error-kind.d.ts +19 -0
  26. package/lib/classify-error-kind.js +122 -0
  27. package/lib/crawler/build-js-redirect-edge.d.ts +68 -0
  28. package/lib/crawler/build-js-redirect-edge.js +57 -0
  29. package/lib/crawler/build-redirect-event.d.ts +24 -0
  30. package/lib/crawler/build-redirect-event.js +28 -0
  31. package/lib/crawler/clear-dns-burned-host-cache.d.ts +6 -0
  32. package/lib/crawler/clear-dns-burned-host-cache.js +11 -0
  33. package/lib/crawler/crawler.d.ts +3 -1
  34. package/lib/crawler/crawler.js +655 -107
  35. package/lib/crawler/derive-js-redirect-target.d.ts +68 -0
  36. package/lib/crawler/derive-js-redirect-target.js +129 -0
  37. package/lib/crawler/derive-resource-source.d.ts +25 -15
  38. package/lib/crawler/derive-resource-source.js +28 -17
  39. package/lib/crawler/dns-burned-host-cache.d.ts +26 -0
  40. package/lib/crawler/dns-burned-host-cache.js +25 -0
  41. package/lib/crawler/dns-burned-host-short-circuit-counter.d.ts +13 -0
  42. package/lib/crawler/dns-burned-host-short-circuit-counter.js +11 -0
  43. package/lib/crawler/fetch-destination.d.ts +12 -4
  44. package/lib/crawler/fetch-destination.js +94 -16
  45. package/lib/crawler/is-js-redirect-error-shape.d.ts +40 -0
  46. package/lib/crawler/is-js-redirect-error-shape.js +53 -0
  47. package/lib/crawler/is-puppeteer-fallback-candidate.d.ts +16 -0
  48. package/lib/crawler/is-puppeteer-fallback-candidate.js +63 -0
  49. package/lib/crawler/link-list.d.ts +21 -1
  50. package/lib/crawler/link-list.js +23 -3
  51. package/lib/crawler/plan-sub-resource-emits.d.ts +63 -0
  52. package/lib/crawler/plan-sub-resource-emits.js +44 -0
  53. package/lib/crawler/preload-short-circuit-error.d.ts +22 -0
  54. package/lib/crawler/preload-short-circuit-error.js +25 -0
  55. package/lib/crawler/should-burn-host.d.ts +78 -0
  56. package/lib/crawler/should-burn-host.js +61 -0
  57. package/lib/crawler/should-get-fallback-on-head-failure.d.ts +38 -0
  58. package/lib/crawler/should-get-fallback-on-head-failure.js +46 -0
  59. package/lib/crawler/types.d.ts +107 -0
  60. package/lib/crawler-orchestrator.d.ts +13 -3
  61. package/lib/crawler-orchestrator.js +292 -69
  62. package/lib/crawler.d.ts +3 -2
  63. package/lib/crawler.js +3 -1
  64. package/lib/permanent-error-kinds.d.ts +43 -0
  65. package/lib/permanent-error-kinds.js +48 -0
  66. package/lib/types.d.ts +84 -0
  67. package/lib/utils/compute-file-sha256.d.ts +23 -0
  68. package/lib/utils/compute-file-sha256.js +55 -0
  69. package/lib/utils/error/emit-error-with-retry.d.ts +40 -0
  70. package/lib/utils/error/emit-error-with-retry.js +44 -0
  71. package/lib/utils/error/emit-error.d.ts +39 -0
  72. package/lib/utils/error/emit-error.js +41 -0
  73. package/package.json +11 -11
  74. package/lib/utils/error/error-emitter.d.ts +0 -18
  75. 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
+ }
@@ -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. Must be non-empty.
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
  *