@nitpicker/crawler 0.9.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 (148) hide show
  1. package/lib/archive/archive-accessor.d.ts +87 -14
  2. package/lib/archive/archive-accessor.js +162 -36
  3. package/lib/archive/archive.d.ts +262 -24
  4. package/lib/archive/archive.js +397 -86
  5. package/lib/archive/cache/compute-archive-cache-key.d.ts +39 -0
  6. package/lib/archive/cache/compute-archive-cache-key.js +95 -0
  7. package/lib/archive/cache/extract-archive-to-cache.d.ts +43 -0
  8. package/lib/archive/cache/extract-archive-to-cache.js +309 -0
  9. package/lib/archive/cache/get-archive-cache-root.d.ts +20 -0
  10. package/lib/archive/cache/get-archive-cache-root.js +53 -0
  11. package/lib/archive/cache/is-archive-cache-disabled.d.ts +24 -0
  12. package/lib/archive/cache/is-archive-cache-disabled.js +34 -0
  13. package/lib/archive/cache/resolve-archive-cache-dir.d.ts +26 -0
  14. package/lib/archive/cache/resolve-archive-cache-dir.js +32 -0
  15. package/lib/archive/database.d.ts +411 -26
  16. package/lib/archive/database.js +1881 -540
  17. package/lib/archive/derive-lineage-from-parent.d.ts +37 -0
  18. package/lib/archive/derive-lineage-from-parent.js +42 -0
  19. package/lib/archive/filesystem/peek-tar-top-dir.d.ts +28 -0
  20. package/lib/archive/filesystem/peek-tar-top-dir.js +65 -0
  21. package/lib/archive/get-failed-page-messages.d.ts +43 -0
  22. package/lib/archive/get-failed-page-messages.js +131 -0
  23. package/lib/archive/init-schema.d.ts +53 -2
  24. package/lib/archive/init-schema.js +399 -15
  25. package/lib/archive/is-inventory-source.d.ts +21 -0
  26. package/lib/archive/is-inventory-source.js +22 -0
  27. package/lib/archive/meta/assert-compatible-version.d.ts +39 -0
  28. package/lib/archive/meta/assert-compatible-version.js +72 -0
  29. package/lib/archive/meta/classify-jsonld-type.d.ts +23 -0
  30. package/lib/archive/meta/classify-jsonld-type.js +43 -0
  31. package/lib/archive/meta/compare-semver.d.ts +23 -0
  32. package/lib/archive/meta/compare-semver.js +51 -0
  33. package/lib/archive/meta/compute-page-denormalized.d.ts +21 -0
  34. package/lib/archive/meta/compute-page-denormalized.js +35 -0
  35. package/lib/archive/meta/derive-flat-from-meta.d.ts +35 -0
  36. package/lib/archive/meta/derive-flat-from-meta.js +158 -0
  37. package/lib/archive/meta/derive-meta-extras.d.ts +20 -0
  38. package/lib/archive/meta/derive-meta-extras.js +23 -0
  39. package/lib/archive/meta/extract-tags-for-archive.d.ts +18 -0
  40. package/lib/archive/meta/extract-tags-for-archive.js +36 -0
  41. package/lib/archive/meta/summarize-jsonld.d.ts +17 -0
  42. package/lib/archive/meta/summarize-jsonld.js +29 -0
  43. package/lib/archive/meta/summarize-tags.d.ts +16 -0
  44. package/lib/archive/meta/summarize-tags.js +33 -0
  45. package/lib/archive/meta/types.d.ts +207 -0
  46. package/lib/archive/meta/types.js +33 -0
  47. package/lib/archive/migrate-crawl-errors.d.ts +20 -0
  48. package/lib/archive/migrate-crawl-errors.js +38 -0
  49. package/lib/archive/migrate-html-blob-tables.d.ts +24 -0
  50. package/lib/archive/migrate-html-blob-tables.js +53 -0
  51. package/lib/archive/migrate-inventory-runs.d.ts +29 -0
  52. package/lib/archive/migrate-inventory-runs.js +52 -0
  53. package/lib/archive/migrate-page-errors.d.ts +16 -0
  54. package/lib/archive/migrate-page-errors.js +35 -0
  55. package/lib/archive/migrate-pages-resources-source.d.ts +16 -0
  56. package/lib/archive/migrate-pages-resources-source.js +46 -0
  57. package/lib/archive/page.d.ts +187 -49
  58. package/lib/archive/page.js +258 -63
  59. package/lib/archive/peek-archive-lock.d.ts +40 -0
  60. package/lib/archive/peek-archive-lock.js +62 -0
  61. package/lib/archive/resolve-redirect-chain.d.ts +33 -0
  62. package/lib/archive/resolve-redirect-chain.js +27 -0
  63. package/lib/archive/types.d.ts +168 -26
  64. package/lib/classify-error-kind.d.ts +19 -0
  65. package/lib/classify-error-kind.js +122 -0
  66. package/lib/crawler/build-js-redirect-edge.d.ts +68 -0
  67. package/lib/crawler/build-js-redirect-edge.js +57 -0
  68. package/lib/crawler/build-redirect-event.d.ts +24 -0
  69. package/lib/crawler/build-redirect-event.js +28 -0
  70. package/lib/crawler/clear-dns-burned-host-cache.d.ts +6 -0
  71. package/lib/crawler/clear-dns-burned-host-cache.js +11 -0
  72. package/lib/crawler/close-browser-safely.d.ts +64 -0
  73. package/lib/crawler/close-browser-safely.js +73 -0
  74. package/lib/crawler/crawler.d.ts +7 -2
  75. package/lib/crawler/crawler.js +919 -113
  76. package/lib/crawler/create-change-phase-handler.d.ts +54 -0
  77. package/lib/crawler/create-change-phase-handler.js +44 -0
  78. package/lib/crawler/derive-js-redirect-target.d.ts +68 -0
  79. package/lib/crawler/derive-js-redirect-target.js +129 -0
  80. package/lib/crawler/derive-page-source.d.ts +23 -0
  81. package/lib/crawler/derive-page-source.js +28 -0
  82. package/lib/crawler/derive-resource-source.d.ts +33 -0
  83. package/lib/crawler/derive-resource-source.js +37 -0
  84. package/lib/crawler/dns-burned-host-cache.d.ts +26 -0
  85. package/lib/crawler/dns-burned-host-cache.js +25 -0
  86. package/lib/crawler/dns-burned-host-short-circuit-counter.d.ts +13 -0
  87. package/lib/crawler/dns-burned-host-short-circuit-counter.js +11 -0
  88. package/lib/crawler/drain-phase-errors.d.ts +48 -0
  89. package/lib/crawler/drain-phase-errors.js +35 -0
  90. package/lib/crawler/fetch-destination.d.ts +12 -4
  91. package/lib/crawler/fetch-destination.js +132 -18
  92. package/lib/crawler/format-crawl-progress.d.ts +12 -3
  93. package/lib/crawler/format-crawl-progress.js +14 -6
  94. package/lib/crawler/handle-browser-close.d.ts +29 -0
  95. package/lib/crawler/handle-browser-close.js +28 -0
  96. package/lib/crawler/is-html-content-type.d.ts +17 -0
  97. package/lib/crawler/is-html-content-type.js +19 -0
  98. package/lib/crawler/is-js-redirect-error-shape.d.ts +40 -0
  99. package/lib/crawler/is-js-redirect-error-shape.js +53 -0
  100. package/lib/crawler/is-likely-html-url.d.ts +22 -0
  101. package/lib/crawler/is-likely-html-url.js +65 -0
  102. package/lib/crawler/is-puppeteer-fallback-candidate.d.ts +16 -0
  103. package/lib/crawler/is-puppeteer-fallback-candidate.js +63 -0
  104. package/lib/crawler/kill-process-tree.d.ts +94 -0
  105. package/lib/crawler/kill-process-tree.js +178 -0
  106. package/lib/crawler/link-list.d.ts +21 -1
  107. package/lib/crawler/link-list.js +25 -4
  108. package/lib/crawler/link-to-page-data.d.ts +13 -5
  109. package/lib/crawler/link-to-page-data.js +26 -5
  110. package/lib/crawler/log-undrained-phase-errors.d.ts +37 -0
  111. package/lib/crawler/log-undrained-phase-errors.js +34 -0
  112. package/lib/crawler/normalize-content-type.d.ts +14 -0
  113. package/lib/crawler/normalize-content-type.js +20 -0
  114. package/lib/crawler/partition-urls-by-html.d.ts +16 -0
  115. package/lib/crawler/partition-urls-by-html.js +23 -0
  116. package/lib/crawler/plan-sub-resource-emits.d.ts +63 -0
  117. package/lib/crawler/plan-sub-resource-emits.js +44 -0
  118. package/lib/crawler/preload-short-circuit-error.d.ts +22 -0
  119. package/lib/crawler/preload-short-circuit-error.js +25 -0
  120. package/lib/crawler/redirect-dest-key.d.ts +19 -0
  121. package/lib/crawler/redirect-dest-key.js +27 -0
  122. package/lib/crawler/resource-to-page-data.d.ts +28 -0
  123. package/lib/crawler/resource-to-page-data.js +59 -0
  124. package/lib/crawler/should-burn-host.d.ts +78 -0
  125. package/lib/crawler/should-burn-host.js +61 -0
  126. package/lib/crawler/should-get-fallback-on-head-failure.d.ts +38 -0
  127. package/lib/crawler/should-get-fallback-on-head-failure.js +46 -0
  128. package/lib/crawler/types.d.ts +229 -1
  129. package/lib/crawler-orchestrator.d.ts +105 -3
  130. package/lib/crawler-orchestrator.js +620 -20
  131. package/lib/crawler.d.ts +8 -2
  132. package/lib/crawler.js +6 -1
  133. package/lib/permanent-error-kinds.d.ts +43 -0
  134. package/lib/permanent-error-kinds.js +48 -0
  135. package/lib/resource-row-to-lookup-result.d.ts +13 -0
  136. package/lib/resource-row-to-lookup-result.js +20 -0
  137. package/lib/types.d.ts +95 -1
  138. package/lib/utils/compute-file-sha256.d.ts +23 -0
  139. package/lib/utils/compute-file-sha256.js +55 -0
  140. package/lib/utils/error/emit-error-with-retry.d.ts +40 -0
  141. package/lib/utils/error/emit-error-with-retry.js +44 -0
  142. package/lib/utils/error/emit-error.d.ts +39 -0
  143. package/lib/utils/error/emit-error.js +41 -0
  144. package/lib/utils/object/parse-response-headers.d.ts +12 -0
  145. package/lib/utils/object/parse-response-headers.js +26 -0
  146. package/package.json +11 -11
  147. package/lib/utils/error/error-emitter.d.ts +0 -18
  148. package/lib/utils/error/error-emitter.js +0 -29
@@ -0,0 +1,63 @@
1
+ import type { CrawlerEventTypes } from './types.js';
2
+ import type { PageSource } from '../archive/types.js';
3
+ import type { ResourceEntry } from '@d-zero/beholder';
4
+ /**
5
+ * Planned `response` emit produced by {@link planSubResourceEmits}.
6
+ */
7
+ export interface PlannedResponseEmit {
8
+ /** The resource payload to attach to the `response` event. */
9
+ resource: CrawlerEventTypes['response']['resource'];
10
+ /**
11
+ * The `source` field propagated to the `response` event. Resolved from
12
+ * the parent page's lineage via {@link deriveResourceSource} — pinning
13
+ * this through a planning step (rather than computing it inline in
14
+ * `#handleResources`) lets the wire-up be unit-tested without spinning
15
+ * up the puppeteer mock stack.
16
+ */
17
+ source: PageSource | undefined;
18
+ }
19
+ /**
20
+ * Planned `responseReferrers` emit produced by {@link planSubResourceEmits}.
21
+ * Always emitted, regardless of whether the resource is new — `isNew` only
22
+ * gates the `response` event.
23
+ */
24
+ export interface PlannedReferrerEmit {
25
+ /** The page URL that triggered the sub-resource fetch. */
26
+ url: string;
27
+ /** The resource URL (hash stripped to match the storage key). */
28
+ src: string;
29
+ }
30
+ /**
31
+ * Output of {@link planSubResourceEmits}: the deduped `response` plan and
32
+ * the per-resource `responseReferrers` plan, side-by-side.
33
+ */
34
+ export interface SubResourceEmitPlan {
35
+ /** `response` events to emit (new resources only). */
36
+ responseEmits: PlannedResponseEmit[];
37
+ /** `responseReferrers` events to emit (every resource, even seen ones). */
38
+ referrerEmits: PlannedReferrerEmit[];
39
+ }
40
+ /**
41
+ * Decide which sub-resource `response` / `responseReferrers` events the
42
+ * crawler should emit for a page render, with the parent's source lineage
43
+ * baked into every `response` event's `source` field.
44
+ *
45
+ * Pure function — takes the resources captured during the render plus the
46
+ * seen-resource set and the parent's source, returns the emit plan. The
47
+ * caller (`Crawler.#handleResources`) is responsible for iterating the
48
+ * plan through its event emitter. Splitting "decide what to emit" from
49
+ * "actually emit" is what makes the lineage propagation contract
50
+ * unit-testable: the previous shape inlined `emit('response', { ...
51
+ * source: deriveResourceSource(...) })` and could only be exercised via a
52
+ * full scrape with a mocked puppeteer stack, which left the `source`
53
+ * value half of the contract effectively un-pinned.
54
+ *
55
+ * Mutates `seenResources` as a side effect — every captured resource is
56
+ * recorded as seen so the next call dedupes correctly. This mirrors the
57
+ * `Crawler.#resources` Set semantics that the planner is designed to share.
58
+ * @param resources - Sub-resource entries captured during the page render.
59
+ * @param parentSource - Merged source of the page being rendered, as resolved by `Crawler.#resolveParentSource`.
60
+ * @param seenResources - Mutable set of already-seen resource keys (mutated in place).
61
+ * @returns The plan of `response` + `responseReferrers` emits to dispatch.
62
+ */
63
+ export declare function planSubResourceEmits(resources: ResourceEntry[], parentSource: PageSource | undefined, seenResources: Set<string>): SubResourceEmitPlan;
@@ -0,0 +1,44 @@
1
+ import { deriveResourceSource } from './derive-resource-source.js';
2
+ import { handleResourceResponse } from './handle-resource-response.js';
3
+ /**
4
+ * Decide which sub-resource `response` / `responseReferrers` events the
5
+ * crawler should emit for a page render, with the parent's source lineage
6
+ * baked into every `response` event's `source` field.
7
+ *
8
+ * Pure function — takes the resources captured during the render plus the
9
+ * seen-resource set and the parent's source, returns the emit plan. The
10
+ * caller (`Crawler.#handleResources`) is responsible for iterating the
11
+ * plan through its event emitter. Splitting "decide what to emit" from
12
+ * "actually emit" is what makes the lineage propagation contract
13
+ * unit-testable: the previous shape inlined `emit('response', { ...
14
+ * source: deriveResourceSource(...) })` and could only be exercised via a
15
+ * full scrape with a mocked puppeteer stack, which left the `source`
16
+ * value half of the contract effectively un-pinned.
17
+ *
18
+ * Mutates `seenResources` as a side effect — every captured resource is
19
+ * recorded as seen so the next call dedupes correctly. This mirrors the
20
+ * `Crawler.#resources` Set semantics that the planner is designed to share.
21
+ * @param resources - Sub-resource entries captured during the page render.
22
+ * @param parentSource - Merged source of the page being rendered, as resolved by `Crawler.#resolveParentSource`.
23
+ * @param seenResources - Mutable set of already-seen resource keys (mutated in place).
24
+ * @returns The plan of `response` + `responseReferrers` emits to dispatch.
25
+ */
26
+ export function planSubResourceEmits(resources, parentSource, seenResources) {
27
+ const subResourceSource = deriveResourceSource(parentSource);
28
+ const responseEmits = [];
29
+ const referrerEmits = [];
30
+ for (const { resource, pageUrl } of resources) {
31
+ const { isNew } = handleResourceResponse(resource, seenResources);
32
+ if (isNew) {
33
+ responseEmits.push({
34
+ resource: resource,
35
+ source: subResourceSource,
36
+ });
37
+ }
38
+ referrerEmits.push({
39
+ url: pageUrl,
40
+ src: resource.url.withoutHash,
41
+ });
42
+ }
43
+ return { responseEmits, referrerEmits };
44
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Thrown by `Crawler.#sendHeadRequest` when the target URL's hostname is in
3
+ * `dnsBurnedHostCache` — both session-learned and preload-seeded burns
4
+ * land here.
5
+ *
6
+ * The orchestrator's `crawler.on('error', …)` handler tests `instanceof` and
7
+ * skips writing this error to `crawl_errors` / `error.log`, so the same
8
+ * preload data isn't re-amplified on subsequent crawls. `pages.status = -1`
9
+ * still gets set through the normal scrape-error path.
10
+ *
11
+ * The message embeds the `ENOTFOUND` token so any downstream consumer that
12
+ * runs `classifyErrorKind` over it (e.g. dealer log forwarders) still gets
13
+ * the `'dns'` classification.
14
+ */
15
+ export declare class PreloadShortCircuitError extends Error {
16
+ /** Sniffable flag for callers that prefer duck-typing over instanceof. */
17
+ readonly isPreloadShortCircuit: true;
18
+ /**
19
+ * @param host - The DNS-burned hostname (already lowercased / Punycoded).
20
+ */
21
+ constructor(host: string);
22
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Thrown by `Crawler.#sendHeadRequest` when the target URL's hostname is in
3
+ * `dnsBurnedHostCache` — both session-learned and preload-seeded burns
4
+ * land here.
5
+ *
6
+ * The orchestrator's `crawler.on('error', …)` handler tests `instanceof` and
7
+ * skips writing this error to `crawl_errors` / `error.log`, so the same
8
+ * preload data isn't re-amplified on subsequent crawls. `pages.status = -1`
9
+ * still gets set through the normal scrape-error path.
10
+ *
11
+ * The message embeds the `ENOTFOUND` token so any downstream consumer that
12
+ * runs `classifyErrorKind` over it (e.g. dealer log forwarders) still gets
13
+ * the `'dns'` classification.
14
+ */
15
+ export class PreloadShortCircuitError extends Error {
16
+ /** Sniffable flag for callers that prefer duck-typing over instanceof. */
17
+ isPreloadShortCircuit = true;
18
+ /**
19
+ * @param host - The DNS-burned hostname (already lowercased / Punycoded).
20
+ */
21
+ constructor(host) {
22
+ super(`getaddrinfo ENOTFOUND ${host}`);
23
+ this.name = 'PreloadShortCircuitError';
24
+ }
25
+ }
@@ -0,0 +1,19 @@
1
+ import type { ExURL } from '@d-zero/shared/parse-url';
2
+ /**
3
+ * Computes the protocol-agnostic dedup key for the final destination a request
4
+ * lands on after following its redirect chain.
5
+ *
6
+ * Used by the redirect-convergence optimisation (#73): the crawler remembers
7
+ * which final destinations have already been rendered, keyed by this value, so
8
+ * that many source URLs all redirecting to one destination render it only once.
9
+ * When there is no redirect, the destination is the requested URL itself.
10
+ *
11
+ * The key matches the form used elsewhere in the crawler (`protocolAgnosticKey`
12
+ * over the normalised URL without hash/auth) so HTTP and HTTPS variants of the
13
+ * same destination collapse to one entry.
14
+ * @param url - The originally requested URL.
15
+ * @param redirectPaths - The redirect hop URLs captured during the HEAD
16
+ * pre-flight, in order. The last entry is the final destination.
17
+ * @returns The protocol-agnostic key of the final destination.
18
+ */
19
+ export declare function redirectDestKey(url: ExURL, redirectPaths: readonly string[]): string;
@@ -0,0 +1,27 @@
1
+ import { tryParseUrl as parseUrl } from '@d-zero/shared/parse-url';
2
+ import { protocolAgnosticKey } from './protocol-agnostic-key.js';
3
+ /**
4
+ * Computes the protocol-agnostic dedup key for the final destination a request
5
+ * lands on after following its redirect chain.
6
+ *
7
+ * Used by the redirect-convergence optimisation (#73): the crawler remembers
8
+ * which final destinations have already been rendered, keyed by this value, so
9
+ * that many source URLs all redirecting to one destination render it only once.
10
+ * When there is no redirect, the destination is the requested URL itself.
11
+ *
12
+ * The key matches the form used elsewhere in the crawler (`protocolAgnosticKey`
13
+ * over the normalised URL without hash/auth) so HTTP and HTTPS variants of the
14
+ * same destination collapse to one entry.
15
+ * @param url - The originally requested URL.
16
+ * @param redirectPaths - The redirect hop URLs captured during the HEAD
17
+ * pre-flight, in order. The last entry is the final destination.
18
+ * @returns The protocol-agnostic key of the final destination.
19
+ */
20
+ export function redirectDestKey(url, redirectPaths) {
21
+ const last = redirectPaths.at(-1);
22
+ if (last === undefined) {
23
+ return protocolAgnosticKey(url.withoutHashAndAuth);
24
+ }
25
+ const parsed = parseUrl(last);
26
+ return protocolAgnosticKey(parsed ? parsed.withoutHashAndAuth : last);
27
+ }
@@ -0,0 +1,28 @@
1
+ import type { ResourceLookupResult } from './types.js';
2
+ import type { PageData } from '../utils/types/types.js';
3
+ import type { ExURL } from '@d-zero/shared/parse-url';
4
+ /**
5
+ * Parameters for {@link resourceToPageData}.
6
+ */
7
+ interface ResourceToPageDataParams {
8
+ /** The queued URL being processed. */
9
+ readonly url: ExURL;
10
+ /** Whether the URL is external to the crawl scope. */
11
+ readonly isExternal: boolean;
12
+ /** The recorded sub-resource data captured during page rendering. */
13
+ readonly resource: ResourceLookupResult;
14
+ }
15
+ /**
16
+ * Synthesize {@link PageData} from a recorded sub-resource row, or return
17
+ * `null` when the resource is not eligible for reuse.
18
+ *
19
+ * Eligible: the status is 2xx AND the content type is known and not
20
+ * `text/html`. Non-2xx rows (redirect hops, errors, 304s) and HTML rows must
21
+ * fall back to the normal HEAD pre-flight / browser scrape, which also
22
+ * guarantees that `redirectPaths: []` here is accurate — a URL that
23
+ * redirects is recorded with its 3xx status and never reaches this path.
24
+ * @param params - The queued URL, its external flag, and the recorded resource data.
25
+ * @returns The synthesized page data, or `null` when the caller must fall back.
26
+ */
27
+ export declare function resourceToPageData(params: ResourceToPageDataParams): PageData | null;
28
+ export {};
@@ -0,0 +1,59 @@
1
+ import { isHtmlContentType } from './is-html-content-type.js';
2
+ /**
3
+ * Synthesize {@link PageData} from a recorded sub-resource row, or return
4
+ * `null` when the resource is not eligible for reuse.
5
+ *
6
+ * Eligible: the status is 2xx AND the content type is known and not
7
+ * `text/html`. Non-2xx rows (redirect hops, errors, 304s) and HTML rows must
8
+ * fall back to the normal HEAD pre-flight / browser scrape, which also
9
+ * guarantees that `redirectPaths: []` here is accurate — a URL that
10
+ * redirects is recorded with its 3xx status and never reaches this path.
11
+ * @param params - The queued URL, its external flag, and the recorded resource data.
12
+ * @returns The synthesized page data, or `null` when the caller must fall back.
13
+ */
14
+ export function resourceToPageData(params) {
15
+ const { url, isExternal, resource } = params;
16
+ if (resource.status == null ||
17
+ resource.status < 200 ||
18
+ resource.status >= 300 ||
19
+ resource.contentType == null ||
20
+ isHtmlContentType(resource.contentType)) {
21
+ return null;
22
+ }
23
+ return {
24
+ url,
25
+ redirectPaths: [],
26
+ isTarget: !isExternal,
27
+ isExternal,
28
+ status: resource.status,
29
+ statusText: resource.statusText ?? '',
30
+ contentType: resource.contentType,
31
+ contentLength: resource.contentLength,
32
+ responseHeaders: resource.responseHeaders,
33
+ // beholder 3.0.0 made jsonLd / speculationRules / tags / others /
34
+ // originTrial required Meta fields. This fallback wraps a non-HTML
35
+ // resource (PDF, image, etc.) that was discovered via subresource
36
+ // inspection rather than full scraping; populate every required slot
37
+ // so downstream insert/derive helpers iterate without crashing.
38
+ meta: {
39
+ title: '',
40
+ jsonLd: [],
41
+ speculationRules: [],
42
+ tags: { detected: {}, entries: [] },
43
+ others: {
44
+ meta: {},
45
+ property: {},
46
+ httpEquiv: {},
47
+ itemprop: {},
48
+ link: [],
49
+ script: [],
50
+ iframe: [],
51
+ },
52
+ originTrial: [],
53
+ },
54
+ anchorList: [],
55
+ imageList: [],
56
+ html: '',
57
+ isSkipped: false,
58
+ };
59
+ }
@@ -0,0 +1,78 @@
1
+ import type { ErrorKind } from '../types.js';
2
+ /**
3
+ * Inputs to {@link shouldBurnHost}.
4
+ */
5
+ export interface ShouldBurnHostParams {
6
+ /**
7
+ * The {@link ErrorKind} classified from the final-attempt error message
8
+ * (i.e. the error that ended the retry loop in `Crawler.#sendHeadRequest`'s
9
+ * `onGiveUp`).
10
+ */
11
+ errorKind: ErrorKind;
12
+ /**
13
+ * Lower-cased hostname whose URL just exhausted retries. Must already be
14
+ * normalised by the caller — `dnsBurnedHostCache` keys are
15
+ * `url.hostname.toLowerCase()`, so this guard reuses that exact form to
16
+ * stay consistent across the two sites.
17
+ */
18
+ host: string;
19
+ /**
20
+ * Set of hostnames that have had at least one successful
21
+ * `fetchDestination` response in this session. `ReadonlySet` because the
22
+ * decision is read-only — populating the set is the caller's job.
23
+ */
24
+ successfulHosts: ReadonlySet<string>;
25
+ }
26
+ /**
27
+ * Burn the host iff the final-attempt kind is `'dns'` AND the host has no
28
+ * session-success record. Pure function — unit-testable in isolation from
29
+ * the `Crawler` instance, the in-memory caches, and the dealer's retry
30
+ * plumbing.
31
+ *
32
+ * **Why the session-success gate exists**: the first worker to exhaust
33
+ * retries with `getaddrinfo ENOTFOUND` would otherwise burn the host and
34
+ * make every subsequent URL on it short-circuit immediately via
35
+ * `PreloadShortCircuitError`, draining the dealer's work queue in seconds
36
+ * and collapsing the crawl into a degenerate `crawlEnd`. When the cause is
37
+ * a local-network blip (operator's WiFi → tethering / VPN flip / ISP DNS
38
+ * hiccup mid-crawl) rather than a dead domain, the host was demonstrably
39
+ * alive moments earlier — earlier successes on it are recorded in
40
+ * `successfulHosts`, so the cascade is suppressed.
41
+ *
42
+ * **Why `'dns-transient'` is excluded**: `EAI_AGAIN` / `EREFUSED` are
43
+ * absorbed by the retry layer within the session; a final-attempt
44
+ * `'dns-transient'` is rare enough that we'd rather pay the per-URL retry
45
+ * cost than wrongly fast-fail a healthy host that flapped briefly. Only the
46
+ * stronger `'dns'` kind is a candidate for burning.
47
+ *
48
+ * **Known limitation — first-URL false positives**: a host whose very first
49
+ * URL of the session hits a real network blip exhausts its retry budget
50
+ * before any URL has succeeded, so `successfulHosts` is still empty and the
51
+ * host IS burned. That URL's siblings on the same host then short-circuit.
52
+ * Acceptable trade-off: the alternative ("never burn anything") regresses
53
+ * the dead-domain fast-fail behavior that the burn cache exists for. The
54
+ * pause-dealer-on-outage layer (separate issue) covers this gap by
55
+ * detecting the outage BEFORE the first retry budget runs out.
56
+ *
57
+ * **Known limitation — preload-seeded burns are not un-burned**: a host
58
+ * added by `#preloadDnsBurnedHostCache` from the archive's `crawl_errors`
59
+ * trips `PreloadShortCircuitError` at the top of `#sendHeadRequest` and
60
+ * never reaches `fetchDestination`, so `successfulHosts` is never
61
+ * populated for it in this session. This is intentional — preload only
62
+ * seeds hosts whose archive evidence is "DNS-failed with no recovery", so
63
+ * un-burning them on a single transient success could re-introduce the
64
+ * cascade we are trying to prevent.
65
+ *
66
+ * **What does NOT contribute to `successfulHosts`**: external pages
67
+ * traversed via `fetchExternal: false` skip the HEAD pre-flight entirely
68
+ * (the crawler stamps a synthetic `PageData` without touching the
69
+ * network), so the host is not recorded as alive even if the same host
70
+ * appears in the crawled-internal scope. Callers must populate the set
71
+ * from real HTTP-response observations only.
72
+ * @param params - See {@link ShouldBurnHostParams}.
73
+ * @param params.errorKind
74
+ * @param params.host
75
+ * @param params.successfulHosts
76
+ * @returns `true` if the burn cache should record this host, `false` otherwise.
77
+ */
78
+ export declare function shouldBurnHost({ errorKind, host, successfulHosts, }: ShouldBurnHostParams): boolean;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Burn the host iff the final-attempt kind is `'dns'` AND the host has no
3
+ * session-success record. Pure function — unit-testable in isolation from
4
+ * the `Crawler` instance, the in-memory caches, and the dealer's retry
5
+ * plumbing.
6
+ *
7
+ * **Why the session-success gate exists**: the first worker to exhaust
8
+ * retries with `getaddrinfo ENOTFOUND` would otherwise burn the host and
9
+ * make every subsequent URL on it short-circuit immediately via
10
+ * `PreloadShortCircuitError`, draining the dealer's work queue in seconds
11
+ * and collapsing the crawl into a degenerate `crawlEnd`. When the cause is
12
+ * a local-network blip (operator's WiFi → tethering / VPN flip / ISP DNS
13
+ * hiccup mid-crawl) rather than a dead domain, the host was demonstrably
14
+ * alive moments earlier — earlier successes on it are recorded in
15
+ * `successfulHosts`, so the cascade is suppressed.
16
+ *
17
+ * **Why `'dns-transient'` is excluded**: `EAI_AGAIN` / `EREFUSED` are
18
+ * absorbed by the retry layer within the session; a final-attempt
19
+ * `'dns-transient'` is rare enough that we'd rather pay the per-URL retry
20
+ * cost than wrongly fast-fail a healthy host that flapped briefly. Only the
21
+ * stronger `'dns'` kind is a candidate for burning.
22
+ *
23
+ * **Known limitation — first-URL false positives**: a host whose very first
24
+ * URL of the session hits a real network blip exhausts its retry budget
25
+ * before any URL has succeeded, so `successfulHosts` is still empty and the
26
+ * host IS burned. That URL's siblings on the same host then short-circuit.
27
+ * Acceptable trade-off: the alternative ("never burn anything") regresses
28
+ * the dead-domain fast-fail behavior that the burn cache exists for. The
29
+ * pause-dealer-on-outage layer (separate issue) covers this gap by
30
+ * detecting the outage BEFORE the first retry budget runs out.
31
+ *
32
+ * **Known limitation — preload-seeded burns are not un-burned**: a host
33
+ * added by `#preloadDnsBurnedHostCache` from the archive's `crawl_errors`
34
+ * trips `PreloadShortCircuitError` at the top of `#sendHeadRequest` and
35
+ * never reaches `fetchDestination`, so `successfulHosts` is never
36
+ * populated for it in this session. This is intentional — preload only
37
+ * seeds hosts whose archive evidence is "DNS-failed with no recovery", so
38
+ * un-burning them on a single transient success could re-introduce the
39
+ * cascade we are trying to prevent.
40
+ *
41
+ * **What does NOT contribute to `successfulHosts`**: external pages
42
+ * traversed via `fetchExternal: false` skip the HEAD pre-flight entirely
43
+ * (the crawler stamps a synthetic `PageData` without touching the
44
+ * network), so the host is not recorded as alive even if the same host
45
+ * appears in the crawled-internal scope. Callers must populate the set
46
+ * from real HTTP-response observations only.
47
+ * @param params - See {@link ShouldBurnHostParams}.
48
+ * @param params.errorKind
49
+ * @param params.host
50
+ * @param params.successfulHosts
51
+ * @returns `true` if the burn cache should record this host, `false` otherwise.
52
+ */
53
+ export function shouldBurnHost({ errorKind, host, successfulHosts, }) {
54
+ if (errorKind !== 'dns') {
55
+ return false;
56
+ }
57
+ if (successfulHosts.has(host)) {
58
+ return false;
59
+ }
60
+ return true;
61
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Decide whether a HEAD pre-flight failure should trigger a GET retry and
3
+ * stay OUT of `destinationCache`.
4
+ *
5
+ * The two contracts are intentionally tied to one helper: anything we'll GET
6
+ * later because the HEAD answer might be wrong is also the same thing we
7
+ * must NOT freeze into the per-session cache (or the second attempt would
8
+ * hit the stale cached failure and skip the live retry that
9
+ * `Crawler.#sendHeadRequest`'s `HEAD_TIMEOUT_ESCALATION_MS` is supposed to
10
+ * pay for).
11
+ *
12
+ * The eligible kinds are:
13
+ *
14
+ * - **`NetTimeoutError`** — the HEAD pre-flight race fired without a
15
+ * server response. The escalating retry can still succeed against a
16
+ * slow-but-reachable host.
17
+ * - **`parse-error`** — `Parse Error` / `Expected HTTP/` / `Unexpected end
18
+ * of stream`. Usually a middlebox rewriting / truncating the HEAD reply
19
+ * the GET path traverses differently.
20
+ * - **`connection-reset`** — `ECONNRESET` / `ERR_CONNECTION_RESET` etc.
21
+ * middlebox dropping the connection mid-response; a retry frequently
22
+ * succeeds.
23
+ *
24
+ * Everything else — DNS, TLS, refused, blocked, plain timeout — is treated
25
+ * as a persistent within-session verdict and IS cached so repeated calls on
26
+ * the same host pay the network cost once.
27
+ * @param error - The `Error` raised by the HEAD attempt.
28
+ * @returns `true` when the error warrants a GET fallback AND a cache skip.
29
+ * @example
30
+ * ```ts
31
+ * shouldGetFallbackOnHeadFailure(new NetTimeoutError(url)); // true
32
+ * shouldGetFallbackOnHeadFailure(new Error('read ECONNRESET')); // true
33
+ * shouldGetFallbackOnHeadFailure(new Error('Parse Error')); // true
34
+ * shouldGetFallbackOnHeadFailure(new Error('getaddrinfo ENOTFOUND host')); // false
35
+ * shouldGetFallbackOnHeadFailure(new Error('ERR_CERT_DATE_INVALID')); // false
36
+ * ```
37
+ */
38
+ export declare function shouldGetFallbackOnHeadFailure(error: Error): boolean;
@@ -0,0 +1,46 @@
1
+ import { classifyErrorKind } from '../classify-error-kind.js';
2
+ import NetTimeoutError from './net-timeout-error.js';
3
+ /**
4
+ * Decide whether a HEAD pre-flight failure should trigger a GET retry and
5
+ * stay OUT of `destinationCache`.
6
+ *
7
+ * The two contracts are intentionally tied to one helper: anything we'll GET
8
+ * later because the HEAD answer might be wrong is also the same thing we
9
+ * must NOT freeze into the per-session cache (or the second attempt would
10
+ * hit the stale cached failure and skip the live retry that
11
+ * `Crawler.#sendHeadRequest`'s `HEAD_TIMEOUT_ESCALATION_MS` is supposed to
12
+ * pay for).
13
+ *
14
+ * The eligible kinds are:
15
+ *
16
+ * - **`NetTimeoutError`** — the HEAD pre-flight race fired without a
17
+ * server response. The escalating retry can still succeed against a
18
+ * slow-but-reachable host.
19
+ * - **`parse-error`** — `Parse Error` / `Expected HTTP/` / `Unexpected end
20
+ * of stream`. Usually a middlebox rewriting / truncating the HEAD reply
21
+ * the GET path traverses differently.
22
+ * - **`connection-reset`** — `ECONNRESET` / `ERR_CONNECTION_RESET` etc.
23
+ * middlebox dropping the connection mid-response; a retry frequently
24
+ * succeeds.
25
+ *
26
+ * Everything else — DNS, TLS, refused, blocked, plain timeout — is treated
27
+ * as a persistent within-session verdict and IS cached so repeated calls on
28
+ * the same host pay the network cost once.
29
+ * @param error - The `Error` raised by the HEAD attempt.
30
+ * @returns `true` when the error warrants a GET fallback AND a cache skip.
31
+ * @example
32
+ * ```ts
33
+ * shouldGetFallbackOnHeadFailure(new NetTimeoutError(url)); // true
34
+ * shouldGetFallbackOnHeadFailure(new Error('read ECONNRESET')); // true
35
+ * shouldGetFallbackOnHeadFailure(new Error('Parse Error')); // true
36
+ * shouldGetFallbackOnHeadFailure(new Error('getaddrinfo ENOTFOUND host')); // false
37
+ * shouldGetFallbackOnHeadFailure(new Error('ERR_CERT_DATE_INVALID')); // false
38
+ * ```
39
+ */
40
+ export function shouldGetFallbackOnHeadFailure(error) {
41
+ if (error instanceof NetTimeoutError) {
42
+ return true;
43
+ }
44
+ const kind = classifyErrorKind(error.message);
45
+ return kind === 'parse-error' || kind === 'connection-reset';
46
+ }