@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.
- package/lib/archive/archive-accessor.d.ts +87 -14
- package/lib/archive/archive-accessor.js +162 -36
- package/lib/archive/archive.d.ts +262 -24
- package/lib/archive/archive.js +397 -86
- 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 +411 -26
- package/lib/archive/database.js +1881 -540
- package/lib/archive/derive-lineage-from-parent.d.ts +37 -0
- package/lib/archive/derive-lineage-from-parent.js +42 -0
- package/lib/archive/filesystem/peek-tar-top-dir.d.ts +28 -0
- package/lib/archive/filesystem/peek-tar-top-dir.js +65 -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.d.ts +53 -2
- package/lib/archive/init-schema.js +399 -15
- package/lib/archive/is-inventory-source.d.ts +21 -0
- package/lib/archive/is-inventory-source.js +22 -0
- package/lib/archive/meta/assert-compatible-version.d.ts +39 -0
- package/lib/archive/meta/assert-compatible-version.js +72 -0
- package/lib/archive/meta/classify-jsonld-type.d.ts +23 -0
- package/lib/archive/meta/classify-jsonld-type.js +43 -0
- package/lib/archive/meta/compare-semver.d.ts +23 -0
- package/lib/archive/meta/compare-semver.js +51 -0
- package/lib/archive/meta/compute-page-denormalized.d.ts +21 -0
- package/lib/archive/meta/compute-page-denormalized.js +35 -0
- package/lib/archive/meta/derive-flat-from-meta.d.ts +35 -0
- package/lib/archive/meta/derive-flat-from-meta.js +158 -0
- package/lib/archive/meta/derive-meta-extras.d.ts +20 -0
- package/lib/archive/meta/derive-meta-extras.js +23 -0
- package/lib/archive/meta/extract-tags-for-archive.d.ts +18 -0
- package/lib/archive/meta/extract-tags-for-archive.js +36 -0
- package/lib/archive/meta/summarize-jsonld.d.ts +17 -0
- package/lib/archive/meta/summarize-jsonld.js +29 -0
- package/lib/archive/meta/summarize-tags.d.ts +16 -0
- package/lib/archive/meta/summarize-tags.js +33 -0
- package/lib/archive/meta/types.d.ts +207 -0
- package/lib/archive/meta/types.js +33 -0
- package/lib/archive/migrate-crawl-errors.d.ts +20 -0
- package/lib/archive/migrate-crawl-errors.js +38 -0
- package/lib/archive/migrate-html-blob-tables.d.ts +24 -0
- package/lib/archive/migrate-html-blob-tables.js +53 -0
- package/lib/archive/migrate-inventory-runs.d.ts +29 -0
- package/lib/archive/migrate-inventory-runs.js +52 -0
- package/lib/archive/migrate-page-errors.d.ts +16 -0
- package/lib/archive/migrate-page-errors.js +35 -0
- package/lib/archive/migrate-pages-resources-source.d.ts +16 -0
- package/lib/archive/migrate-pages-resources-source.js +46 -0
- package/lib/archive/page.d.ts +187 -49
- package/lib/archive/page.js +258 -63
- package/lib/archive/peek-archive-lock.d.ts +40 -0
- package/lib/archive/peek-archive-lock.js +62 -0
- package/lib/archive/resolve-redirect-chain.d.ts +33 -0
- package/lib/archive/resolve-redirect-chain.js +27 -0
- package/lib/archive/types.d.ts +168 -26
- 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/close-browser-safely.d.ts +64 -0
- package/lib/crawler/close-browser-safely.js +73 -0
- package/lib/crawler/crawler.d.ts +7 -2
- package/lib/crawler/crawler.js +919 -113
- package/lib/crawler/create-change-phase-handler.d.ts +54 -0
- package/lib/crawler/create-change-phase-handler.js +44 -0
- 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-page-source.d.ts +23 -0
- package/lib/crawler/derive-page-source.js +28 -0
- package/lib/crawler/derive-resource-source.d.ts +33 -0
- package/lib/crawler/derive-resource-source.js +37 -0
- 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/drain-phase-errors.d.ts +48 -0
- package/lib/crawler/drain-phase-errors.js +35 -0
- package/lib/crawler/fetch-destination.d.ts +12 -4
- package/lib/crawler/fetch-destination.js +132 -18
- package/lib/crawler/format-crawl-progress.d.ts +12 -3
- package/lib/crawler/format-crawl-progress.js +14 -6
- package/lib/crawler/handle-browser-close.d.ts +29 -0
- package/lib/crawler/handle-browser-close.js +28 -0
- package/lib/crawler/is-html-content-type.d.ts +17 -0
- package/lib/crawler/is-html-content-type.js +19 -0
- 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-likely-html-url.d.ts +22 -0
- package/lib/crawler/is-likely-html-url.js +65 -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/kill-process-tree.d.ts +94 -0
- package/lib/crawler/kill-process-tree.js +178 -0
- package/lib/crawler/link-list.d.ts +21 -1
- package/lib/crawler/link-list.js +25 -4
- package/lib/crawler/link-to-page-data.d.ts +13 -5
- package/lib/crawler/link-to-page-data.js +26 -5
- package/lib/crawler/log-undrained-phase-errors.d.ts +37 -0
- package/lib/crawler/log-undrained-phase-errors.js +34 -0
- package/lib/crawler/normalize-content-type.d.ts +14 -0
- package/lib/crawler/normalize-content-type.js +20 -0
- package/lib/crawler/partition-urls-by-html.d.ts +16 -0
- package/lib/crawler/partition-urls-by-html.js +23 -0
- 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/redirect-dest-key.d.ts +19 -0
- package/lib/crawler/redirect-dest-key.js +27 -0
- package/lib/crawler/resource-to-page-data.d.ts +28 -0
- package/lib/crawler/resource-to-page-data.js +59 -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 +229 -1
- package/lib/crawler-orchestrator.d.ts +105 -3
- package/lib/crawler-orchestrator.js +620 -20
- package/lib/crawler.d.ts +8 -2
- package/lib/crawler.js +6 -1
- package/lib/permanent-error-kinds.d.ts +43 -0
- package/lib/permanent-error-kinds.js +48 -0
- package/lib/resource-row-to-lookup-result.d.ts +13 -0
- package/lib/resource-row-to-lookup-result.js +20 -0
- package/lib/types.d.ts +95 -1
- 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/lib/utils/object/parse-response-headers.d.ts +12 -0
- package/lib/utils/object/parse-response-headers.js +26 -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,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
|
+
}
|