@nitpicker/crawler 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/archive/archive.d.ts +98 -10
- package/lib/archive/archive.js +98 -9
- package/lib/archive/body-hash/compute-body-hash.d.ts +22 -0
- package/lib/archive/body-hash/compute-body-hash.js +31 -0
- package/lib/archive/body-hash/extract-body.d.ts +18 -0
- package/lib/archive/body-hash/extract-body.js +29 -0
- package/lib/archive/body-hash/mask-dynamic-ids.d.ts +33 -0
- package/lib/archive/body-hash/mask-dynamic-ids.js +44 -0
- package/lib/archive/body-hash/normalize-url-like-strings.d.ts +16 -0
- package/lib/archive/body-hash/normalize-url-like-strings.js +19 -0
- package/lib/archive/cache/clear-archive-cache-entry.d.ts +19 -0
- package/lib/archive/cache/clear-archive-cache-entry.js +25 -0
- package/lib/archive/cache/clear-archive-cache-root.d.ts +26 -0
- package/lib/archive/cache/clear-archive-cache-root.js +32 -0
- package/lib/archive/cache/list-archive-cache-entries.d.ts +23 -0
- package/lib/archive/cache/list-archive-cache-entries.js +125 -0
- package/lib/archive/cache/path-exists.d.ts +9 -0
- package/lib/archive/cache/path-exists.js +18 -0
- package/lib/archive/cache/types.d.ts +33 -0
- package/lib/archive/cache/types.js +1 -0
- package/lib/archive/create-adjunct-tables.d.ts +11 -0
- package/lib/archive/create-adjunct-tables.js +127 -0
- package/lib/archive/create-entity-tables.js +21 -1
- package/lib/archive/database.d.ts +37 -2
- package/lib/archive/database.js +50 -3
- package/lib/archive/db-ops/_shared/clear-write-ref-caches.js +1 -0
- package/lib/archive/db-ops/_shared/compress-payload.d.ts +26 -0
- package/lib/archive/db-ops/_shared/compress-payload.js +30 -0
- package/lib/archive/db-ops/_shared/create-write-ref-caches.js +1 -0
- package/lib/archive/db-ops/_shared/types.d.ts +2 -0
- package/lib/archive/db-ops/_shared/upsert-json-ref.js +3 -3
- package/lib/archive/db-ops/analysis/replace-page-templates.d.ts +12 -3
- package/lib/archive/db-ops/analysis/replace-page-templates.js +35 -3
- package/lib/archive/db-ops/analysis/types.d.ts +61 -0
- package/lib/archive/db-ops/analysis/types.js +1 -0
- package/lib/archive/db-ops/console-logs/compute-console-log-hash.d.ts +19 -0
- package/lib/archive/db-ops/console-logs/compute-console-log-hash.js +30 -0
- package/lib/archive/db-ops/console-logs/replace-console-logs.d.ts +34 -0
- package/lib/archive/db-ops/console-logs/replace-console-logs.js +123 -0
- package/lib/archive/db-ops/console-logs/stringify-console-log-args.d.ts +16 -0
- package/lib/archive/db-ops/console-logs/stringify-console-log-args.js +26 -0
- package/lib/archive/db-ops/console-logs/types.d.ts +38 -0
- package/lib/archive/db-ops/console-logs/types.js +1 -0
- package/lib/archive/db-ops/console-logs/upsert-console-log-item.d.ts +29 -0
- package/lib/archive/db-ops/console-logs/upsert-console-log-item.js +52 -0
- package/lib/archive/db-ops/errors/list-dns-burned-host-candidates.d.ts +11 -0
- package/lib/archive/db-ops/errors/list-dns-burned-host-candidates.js +21 -4
- package/lib/archive/db-ops/inventory/record-inventory-run.js +1 -0
- package/lib/archive/db-ops/lifecycle/init.d.ts +11 -2
- package/lib/archive/db-ops/lifecycle/init.js +21 -2
- package/lib/archive/db-ops/outages/close-network-outage.d.ts +12 -0
- package/lib/archive/db-ops/outages/close-network-outage.js +15 -0
- package/lib/archive/db-ops/outages/close-stale-open-network-outages.d.ts +26 -0
- package/lib/archive/db-ops/outages/close-stale-open-network-outages.js +42 -0
- package/lib/archive/db-ops/outages/compute-outage-clamp-timestamp.d.ts +26 -0
- package/lib/archive/db-ops/outages/compute-outage-clamp-timestamp.js +31 -0
- package/lib/archive/db-ops/outages/insert-network-outage.d.ts +14 -0
- package/lib/archive/db-ops/outages/insert-network-outage.js +29 -0
- package/lib/archive/db-ops/outages/list-network-outages.d.ts +28 -0
- package/lib/archive/db-ops/outages/list-network-outages.js +42 -0
- package/lib/archive/db-ops/pages/reset/reset-failed-pages.d.ts +13 -0
- package/lib/archive/db-ops/pages/reset/reset-failed-pages.js +35 -7
- package/lib/archive/db-ops/pages/write/update-page.js +12 -0
- package/lib/archive/db-ops/pages/write/write-page-html-blob.js +6 -6
- package/lib/archive/filesystem/output-binary.d.ts +12 -0
- package/lib/archive/filesystem/output-binary.js +17 -0
- package/lib/archive/get-failed-page-messages.d.ts +20 -8
- package/lib/archive/get-failed-page-messages.js +29 -15
- package/lib/archive/migrate-content-items-alias-of-id.d.ts +47 -0
- package/lib/archive/migrate-content-items-alias-of-id.js +58 -0
- package/lib/archive/migrate-inventory-runs-invalid-skipped.d.ts +11 -0
- package/lib/archive/migrate-inventory-runs-invalid-skipped.js +24 -0
- package/lib/archive/migrate-page-meta-body-hash.d.ts +39 -0
- package/lib/archive/migrate-page-meta-body-hash.js +52 -0
- package/lib/archive/migrate-page-meta-console-error-count.d.ts +22 -0
- package/lib/archive/migrate-page-meta-console-error-count.js +34 -0
- package/lib/archive/page.d.ts +1 -1
- package/lib/archive/types.d.ts +47 -2
- package/lib/archive/url-alias/compute-tier-a-alias-key.d.ts +31 -0
- package/lib/archive/url-alias/compute-tier-a-alias-key.js +36 -0
- package/lib/archive/url-alias/compute-tier-b-alias-key.d.ts +19 -0
- package/lib/archive/url-alias/compute-tier-b-alias-key.js +30 -0
- package/lib/archive/url-alias/format-alias-key.d.ts +13 -0
- package/lib/archive/url-alias/format-alias-key.js +14 -0
- package/lib/archive/url-alias/parse-alias-key-parts.d.ts +15 -0
- package/lib/archive/url-alias/parse-alias-key-parts.js +40 -0
- package/lib/archive/url-alias/types.d.ts +7 -0
- package/lib/archive/url-alias/types.js +1 -0
- package/lib/crawler/choose-probe-host.d.ts +24 -0
- package/lib/crawler/choose-probe-host.js +38 -0
- package/lib/crawler/clear-dns-burned-host-cache.js +2 -0
- package/lib/crawler/crawler.js +257 -0
- package/lib/crawler/dns-burned-host-burn-timestamps.d.ts +17 -0
- package/lib/crawler/dns-burned-host-burn-timestamps.js +17 -0
- package/lib/crawler/evict-network-classified-destination-cache-entries.d.ts +26 -0
- package/lib/crawler/evict-network-classified-destination-cache-entries.js +34 -0
- package/lib/crawler/evict-outage-tainted-dns-burns.d.ts +23 -0
- package/lib/crawler/evict-outage-tainted-dns-burns.js +26 -0
- package/lib/crawler/network-gate.d.ts +49 -0
- package/lib/crawler/network-gate.js +78 -0
- package/lib/crawler/network-outage-detector.d.ts +51 -0
- package/lib/crawler/network-outage-detector.js +81 -0
- package/lib/crawler/network-outage-summary-counter.d.ts +21 -0
- package/lib/crawler/network-outage-summary-counter.js +18 -0
- package/lib/crawler/probe-network.d.ts +26 -0
- package/lib/crawler/probe-network.js +21 -0
- package/lib/crawler/types.d.ts +147 -1
- package/lib/crawler-orchestrator.d.ts +62 -18
- package/lib/crawler-orchestrator.js +161 -26
- package/lib/crawler.d.ts +21 -0
- package/lib/crawler.js +17 -0
- package/lib/is-within-outage-window.d.ts +49 -0
- package/lib/is-within-outage-window.js +33 -0
- package/lib/network-related-error-kinds.d.ts +23 -0
- package/lib/network-related-error-kinds.js +28 -0
- package/lib/types.d.ts +9 -1
- package/lib/utils/array/each-splitted.d.ts +1 -1
- package/lib/utils/compute-file-sha256.d.ts +17 -17
- package/lib/utils/compute-file-sha256.js +18 -47
- package/package.json +3 -3
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Computes the Tier B alias key for `url`: like `computeTierAAliasKey`,
|
|
3
|
+
* but additionally strips one trailing `/` from the path (the bare root
|
|
4
|
+
* path `/` is left alone). Two URLs sharing a Tier B key differ only by
|
|
5
|
+
* trailing-slash presence — a resource-identity signal considerably weaker
|
|
6
|
+
* than Tier A's (a web server can legitimately serve different content at
|
|
7
|
+
* `/foo` and `/foo/`), so callers must additionally require a matching
|
|
8
|
+
* `page_meta.body_hash` before treating a Tier B match as the same page.
|
|
9
|
+
* @param url - The URL string to compute a key for.
|
|
10
|
+
* @returns The Tier B key, or `null` if `url` is not a parseable http(s) URL.
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* computeTierBAliasKey('https://example.com/foo');
|
|
14
|
+
* computeTierBAliasKey('https://example.com/foo/');
|
|
15
|
+
* // both: 'example.com/foo' -- same key, but the caller must still confirm
|
|
16
|
+
* // a matching body_hash before merging these as the same page.
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare function computeTierBAliasKey(url: string): string | null;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { formatAliasKey } from './format-alias-key.js';
|
|
2
|
+
import { parseAliasKeyParts } from './parse-alias-key-parts.js';
|
|
3
|
+
/**
|
|
4
|
+
* Computes the Tier B alias key for `url`: like `computeTierAAliasKey`,
|
|
5
|
+
* but additionally strips one trailing `/` from the path (the bare root
|
|
6
|
+
* path `/` is left alone). Two URLs sharing a Tier B key differ only by
|
|
7
|
+
* trailing-slash presence — a resource-identity signal considerably weaker
|
|
8
|
+
* than Tier A's (a web server can legitimately serve different content at
|
|
9
|
+
* `/foo` and `/foo/`), so callers must additionally require a matching
|
|
10
|
+
* `page_meta.body_hash` before treating a Tier B match as the same page.
|
|
11
|
+
* @param url - The URL string to compute a key for.
|
|
12
|
+
* @returns The Tier B key, or `null` if `url` is not a parseable http(s) URL.
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* computeTierBAliasKey('https://example.com/foo');
|
|
16
|
+
* computeTierBAliasKey('https://example.com/foo/');
|
|
17
|
+
* // both: 'example.com/foo' -- same key, but the caller must still confirm
|
|
18
|
+
* // a matching body_hash before merging these as the same page.
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function computeTierBAliasKey(url) {
|
|
22
|
+
const parts = parseAliasKeyParts(url);
|
|
23
|
+
if (!parts) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const path = parts.path.length > 1 && parts.path.endsWith('/')
|
|
27
|
+
? parts.path.slice(0, -1)
|
|
28
|
+
: parts.path;
|
|
29
|
+
return formatAliasKey({ ...parts, path });
|
|
30
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AliasKeyParts } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Formats parsed alias-key parts into a single string key. `host`/`port`
|
|
4
|
+
* never contain `/` or `?`, and `path`/`search` always start with those
|
|
5
|
+
* characters respectively (or `search` is empty), so concatenation without
|
|
6
|
+
* an explicit delimiter is unambiguous.
|
|
7
|
+
* @param parts - The parts to format.
|
|
8
|
+
* @returns The formatted key string.
|
|
9
|
+
* @example
|
|
10
|
+
* formatAliasKey({ host: 'example.com', port: '', path: '/about/', search: '' });
|
|
11
|
+
* // 'example.com/about/'
|
|
12
|
+
*/
|
|
13
|
+
export declare function formatAliasKey(parts: AliasKeyParts): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats parsed alias-key parts into a single string key. `host`/`port`
|
|
3
|
+
* never contain `/` or `?`, and `path`/`search` always start with those
|
|
4
|
+
* characters respectively (or `search` is empty), so concatenation without
|
|
5
|
+
* an explicit delimiter is unambiguous.
|
|
6
|
+
* @param parts - The parts to format.
|
|
7
|
+
* @returns The formatted key string.
|
|
8
|
+
* @example
|
|
9
|
+
* formatAliasKey({ host: 'example.com', port: '', path: '/about/', search: '' });
|
|
10
|
+
* // 'example.com/about/'
|
|
11
|
+
*/
|
|
12
|
+
export function formatAliasKey(parts) {
|
|
13
|
+
return `${parts.host}${parts.port ? `:${parts.port}` : ''}${parts.path}${parts.search}`;
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AliasKeyParts } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parses `url` and extracts the components Tier A / Tier B keys are built
|
|
4
|
+
* from. Returns `null` for anything that isn't a parseable `http`/`https`
|
|
5
|
+
* URL — such a row is defensively excluded from alias candidacy entirely
|
|
6
|
+
* (a non-http(s) URL should not occur for anything the crawler itself
|
|
7
|
+
* stored, since every `url_refs.url` was already parsed once during
|
|
8
|
+
* crawling).
|
|
9
|
+
* @param url - The URL string to parse.
|
|
10
|
+
* @returns The extracted parts, or `null` if unparseable / not http(s).
|
|
11
|
+
* @example
|
|
12
|
+
* parseAliasKeyParts('https://Example.com/about/index.html');
|
|
13
|
+
* // { host: 'example.com', port: '', path: '/about/', search: '' }
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseAliasKeyParts(url: string): AliasKeyParts | null;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const INDEX_SUFFIX_PATTERN = /\/index\.\w+$/i;
|
|
2
|
+
/**
|
|
3
|
+
* Parses `url` and extracts the components Tier A / Tier B keys are built
|
|
4
|
+
* from. Returns `null` for anything that isn't a parseable `http`/`https`
|
|
5
|
+
* URL — such a row is defensively excluded from alias candidacy entirely
|
|
6
|
+
* (a non-http(s) URL should not occur for anything the crawler itself
|
|
7
|
+
* stored, since every `url_refs.url` was already parsed once during
|
|
8
|
+
* crawling).
|
|
9
|
+
* @param url - The URL string to parse.
|
|
10
|
+
* @returns The extracted parts, or `null` if unparseable / not http(s).
|
|
11
|
+
* @example
|
|
12
|
+
* parseAliasKeyParts('https://Example.com/about/index.html');
|
|
13
|
+
* // { host: 'example.com', port: '', path: '/about/', search: '' }
|
|
14
|
+
*/
|
|
15
|
+
export function parseAliasKeyParts(url) {
|
|
16
|
+
let parsed;
|
|
17
|
+
try {
|
|
18
|
+
parsed = new URL(url);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
// WHATWG URL already lowercases `hostname` during parsing; the
|
|
28
|
+
// explicit `.toLowerCase()` documents that intent rather than
|
|
29
|
+
// relying on it silently.
|
|
30
|
+
host: parsed.hostname.toLowerCase(),
|
|
31
|
+
// Empty string when the port is the scheme's default (or
|
|
32
|
+
// unspecified) — this is why `http://example.com/` and
|
|
33
|
+
// `https://example.com/` fold to the same key below (both have
|
|
34
|
+
// `port === ''`) while an explicit non-default port on either
|
|
35
|
+
// scheme is preserved and kept distinct.
|
|
36
|
+
port: parsed.port,
|
|
37
|
+
path: parsed.pathname.replace(INDEX_SUFFIX_PATTERN, '/'),
|
|
38
|
+
search: parsed.search,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pick the hostname a network-outage probe should target.
|
|
3
|
+
*
|
|
4
|
+
* Prefers a host that has already answered successfully during this crawl
|
|
5
|
+
* session (`Crawler.#successfulHosts` — the same "proven alive" evidence
|
|
6
|
+
* `shouldBurnHost` uses) over a root URL's hostname, and never falls back to
|
|
7
|
+
* a hardcoded external address (e.g. `1.1.1.1`): the probe should depend
|
|
8
|
+
* only on infrastructure the crawl is already touching. `ReadonlySet`
|
|
9
|
+
* iteration order in JS is insertion order, so this deterministically picks
|
|
10
|
+
* the first host to have succeeded this session.
|
|
11
|
+
* @param successfulHosts - Hostnames observed to respond in this session.
|
|
12
|
+
* @param roots - The crawl's configured root URLs (`CrawlerOptions.roots`),
|
|
13
|
+
* used as a fallback before any host has succeeded yet.
|
|
14
|
+
* @returns A probe target hostname, or `null` if neither source yields one
|
|
15
|
+
* (e.g. a fresh session with a malformed/empty roots list and no
|
|
16
|
+
* successes yet — the caller should treat this as "cannot probe").
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* chooseProbeHost(new Set(['a.example']), ['https://b.example/']); // 'a.example'
|
|
20
|
+
* chooseProbeHost(new Set(), ['https://b.example/']); // 'b.example'
|
|
21
|
+
* chooseProbeHost(new Set(), []); // null
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function chooseProbeHost(successfulHosts: ReadonlySet<string>, roots: readonly string[]): string | null;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pick the hostname a network-outage probe should target.
|
|
3
|
+
*
|
|
4
|
+
* Prefers a host that has already answered successfully during this crawl
|
|
5
|
+
* session (`Crawler.#successfulHosts` — the same "proven alive" evidence
|
|
6
|
+
* `shouldBurnHost` uses) over a root URL's hostname, and never falls back to
|
|
7
|
+
* a hardcoded external address (e.g. `1.1.1.1`): the probe should depend
|
|
8
|
+
* only on infrastructure the crawl is already touching. `ReadonlySet`
|
|
9
|
+
* iteration order in JS is insertion order, so this deterministically picks
|
|
10
|
+
* the first host to have succeeded this session.
|
|
11
|
+
* @param successfulHosts - Hostnames observed to respond in this session.
|
|
12
|
+
* @param roots - The crawl's configured root URLs (`CrawlerOptions.roots`),
|
|
13
|
+
* used as a fallback before any host has succeeded yet.
|
|
14
|
+
* @returns A probe target hostname, or `null` if neither source yields one
|
|
15
|
+
* (e.g. a fresh session with a malformed/empty roots list and no
|
|
16
|
+
* successes yet — the caller should treat this as "cannot probe").
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* chooseProbeHost(new Set(['a.example']), ['https://b.example/']); // 'a.example'
|
|
20
|
+
* chooseProbeHost(new Set(), ['https://b.example/']); // 'b.example'
|
|
21
|
+
* chooseProbeHost(new Set(), []); // null
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function chooseProbeHost(successfulHosts, roots) {
|
|
25
|
+
const [firstSuccessfulHost] = successfulHosts;
|
|
26
|
+
if (firstSuccessfulHost !== undefined) {
|
|
27
|
+
return firstSuccessfulHost;
|
|
28
|
+
}
|
|
29
|
+
for (const root of roots) {
|
|
30
|
+
try {
|
|
31
|
+
return new URL(root).hostname;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dnsBurnedHostBurnTimestamps } from './dns-burned-host-burn-timestamps.js';
|
|
1
2
|
import { dnsBurnedHostCache } from './dns-burned-host-cache.js';
|
|
2
3
|
import { dnsBurnedHostShortCircuitCounter } from './dns-burned-host-short-circuit-counter.js';
|
|
3
4
|
/**
|
|
@@ -8,4 +9,5 @@ import { dnsBurnedHostShortCircuitCounter } from './dns-burned-host-short-circui
|
|
|
8
9
|
export function clearDnsBurnedHostCache() {
|
|
9
10
|
dnsBurnedHostCache.clear();
|
|
10
11
|
dnsBurnedHostShortCircuitCounter.count = 0;
|
|
12
|
+
dnsBurnedHostBurnTimestamps.clear();
|
|
11
13
|
}
|
package/lib/crawler/crawler.js
CHANGED
|
@@ -14,12 +14,17 @@ import { crawlerLog } from '../debug.js';
|
|
|
14
14
|
import { buildJsRedirectEdge } from './build-js-redirect-edge.js';
|
|
15
15
|
import { buildRedirectEvent } from './build-redirect-event.js';
|
|
16
16
|
import { captureImageDomPaths } from './capture-image-dom-paths.js';
|
|
17
|
+
import { chooseProbeHost } from './choose-probe-host.js';
|
|
17
18
|
import { createChangePhaseHandler } from './create-change-phase-handler.js';
|
|
18
19
|
import { derivePageSource } from './derive-page-source.js';
|
|
20
|
+
import { destinationCache } from './destination-cache.js';
|
|
19
21
|
import { detectPaginationPattern } from './detect-pagination-pattern.js';
|
|
22
|
+
import { dnsBurnedHostBurnTimestamps } from './dns-burned-host-burn-timestamps.js';
|
|
20
23
|
import { dnsBurnedHostCache } from './dns-burned-host-cache.js';
|
|
21
24
|
import { dnsBurnedHostShortCircuitCounter } from './dns-burned-host-short-circuit-counter.js';
|
|
22
25
|
import { drainPhaseErrors } from './drain-phase-errors.js';
|
|
26
|
+
import { evictNetworkClassifiedDestinationCacheEntries } from './evict-network-classified-destination-cache-entries.js';
|
|
27
|
+
import { evictOutageTaintedDnsBurns } from './evict-outage-tainted-dns-burns.js';
|
|
23
28
|
import { fetchDestination } from './fetch-destination.js';
|
|
24
29
|
import { findScopeEntry } from './find-scope-entry.js';
|
|
25
30
|
import { formatCrawlProgress } from './format-crawl-progress.js';
|
|
@@ -35,9 +40,12 @@ import { isPuppeteerFallbackCandidate } from './is-puppeteer-fallback-candidate.
|
|
|
35
40
|
import LinkList from './link-list.js';
|
|
36
41
|
import { linkToPageData } from './link-to-page-data.js';
|
|
37
42
|
import { logUndrainedPhaseErrors } from './log-undrained-phase-errors.js';
|
|
43
|
+
import NetworkGate from './network-gate.js';
|
|
44
|
+
import NetworkOutageDetector from './network-outage-detector.js';
|
|
38
45
|
import { partitionUrlsByHtml } from './partition-urls-by-html.js';
|
|
39
46
|
import { planSubResourceEmits } from './plan-sub-resource-emits.js';
|
|
40
47
|
import { PreloadShortCircuitError } from './preload-short-circuit-error.js';
|
|
48
|
+
import { probeNetwork } from './probe-network.js';
|
|
41
49
|
import { protocolAgnosticKey } from './protocol-agnostic-key.js';
|
|
42
50
|
import { redirectDestKey } from './redirect-dest-key.js';
|
|
43
51
|
import { resourceToPageData } from './resource-to-page-data.js';
|
|
@@ -56,6 +64,14 @@ import { shouldSkipUrl } from './should-skip-url.js';
|
|
|
56
64
|
* stay on the final (longest) timeout for any additional attempts.
|
|
57
65
|
*/
|
|
58
66
|
const HEAD_TIMEOUT_ESCALATION_MS = [10_000, 30_000, 60_000];
|
|
67
|
+
/** Default {@link CrawlerOptions.networkOutageWindowMs}. */
|
|
68
|
+
const DEFAULT_NETWORK_OUTAGE_WINDOW_MS = 10_000;
|
|
69
|
+
/** Default {@link CrawlerOptions.networkOutageErrorThreshold}. */
|
|
70
|
+
const DEFAULT_NETWORK_OUTAGE_ERROR_THRESHOLD = 5;
|
|
71
|
+
/** Default {@link CrawlerOptions.networkOutageHostThreshold}. */
|
|
72
|
+
const DEFAULT_NETWORK_OUTAGE_HOST_THRESHOLD = 2;
|
|
73
|
+
/** Default {@link CrawlerOptions.networkOutageProbeIntervalMs}. */
|
|
74
|
+
const DEFAULT_NETWORK_OUTAGE_PROBE_INTERVAL_MS = 10_000;
|
|
59
75
|
/**
|
|
60
76
|
* The core crawler engine that discovers and scrapes web pages.
|
|
61
77
|
*
|
|
@@ -72,8 +88,39 @@ class Crawler extends EventEmitter {
|
|
|
72
88
|
#abortController = new AbortController();
|
|
73
89
|
/** Tracks discovered URLs, their scrape status, and deduplication. */
|
|
74
90
|
#linkList = new LinkList();
|
|
91
|
+
/**
|
|
92
|
+
* Gate every worker callback awaits before doing network work (see the
|
|
93
|
+
* worker body inside {@link #runDeal}). Open by default; closed by
|
|
94
|
+
* {@link #handleOutageSuspect} once a recovery probe confirms a suspect
|
|
95
|
+
* outage, reopened once a later probe succeeds. Re-opened defensively at
|
|
96
|
+
* the start of {@link #runDeal} (a no-op if already open) so a fresh
|
|
97
|
+
* session never inherits a closed gate from a prior anomalous one.
|
|
98
|
+
*/
|
|
99
|
+
#networkGate = new NetworkGate();
|
|
100
|
+
/**
|
|
101
|
+
* Sliding-window detector for "the operator's own network, not the
|
|
102
|
+
* target sites, looks like it is down". Fed from {@link #sendHeadRequest}'s
|
|
103
|
+
* `onWait` / `onGiveUp`; a non-null {@link OutageSuspect} triggers
|
|
104
|
+
* {@link #handleOutageSuspect}. Reset at the start of {@link #runDeal}.
|
|
105
|
+
* Assigned in the constructor (not a field initializer) because it
|
|
106
|
+
* needs `this.#options`'s network-outage tunables.
|
|
107
|
+
*/
|
|
108
|
+
#networkOutageDetector;
|
|
75
109
|
/** Merged crawler configuration (user overrides + defaults). */
|
|
76
110
|
#options;
|
|
111
|
+
/**
|
|
112
|
+
* Synchronous claim flag guarding the async gap between "a suspect
|
|
113
|
+
* outage arrived" and "the confirming probe settled" in
|
|
114
|
+
* {@link #handleOutageSuspect}. Without it, two workers whose HEAD
|
|
115
|
+
* requests both exhaust retries in quick succession could each start
|
|
116
|
+
* their own confirming probe while the gate is still open, and if both
|
|
117
|
+
* probes fail, both would close the gate and emit
|
|
118
|
+
* `networkOutageConfirmed` — creating two simultaneously-open
|
|
119
|
+
* `network_outages` rows for one ongoing outage. Checked and set
|
|
120
|
+
* synchronously (no `await` between the check and the set), which is
|
|
121
|
+
* race-free because JS has no thread-level interleaving.
|
|
122
|
+
*/
|
|
123
|
+
#outageHandlingInProgress = false;
|
|
77
124
|
/**
|
|
78
125
|
* Phase errors observed during {@link Crawler._launchBrowserAndScrape},
|
|
79
126
|
* buffered per URL href so they can be emitted as `pageError` events
|
|
@@ -153,7 +200,17 @@ class Crawler extends EventEmitter {
|
|
|
153
200
|
lookupResource: options?.lookupResource ?? null,
|
|
154
201
|
lookupPageSource: options?.lookupPageSource ?? null,
|
|
155
202
|
inventoryMode: options?.inventoryMode ?? null,
|
|
203
|
+
networkOutageWindowMs: options?.networkOutageWindowMs ?? DEFAULT_NETWORK_OUTAGE_WINDOW_MS,
|
|
204
|
+
networkOutageErrorThreshold: options?.networkOutageErrorThreshold ?? DEFAULT_NETWORK_OUTAGE_ERROR_THRESHOLD,
|
|
205
|
+
networkOutageHostThreshold: options?.networkOutageHostThreshold ?? DEFAULT_NETWORK_OUTAGE_HOST_THRESHOLD,
|
|
206
|
+
networkOutageProbeIntervalMs: options?.networkOutageProbeIntervalMs ?? DEFAULT_NETWORK_OUTAGE_PROBE_INTERVAL_MS,
|
|
207
|
+
networkProbe: options?.networkProbe ?? null,
|
|
156
208
|
};
|
|
209
|
+
this.#networkOutageDetector = new NetworkOutageDetector({
|
|
210
|
+
windowMs: this.#options.networkOutageWindowMs,
|
|
211
|
+
errorThreshold: this.#options.networkOutageErrorThreshold,
|
|
212
|
+
hostThreshold: this.#options.networkOutageHostThreshold,
|
|
213
|
+
});
|
|
157
214
|
this.#robotsChecker = new RobotsChecker(this.#options.userAgent, !this.#options.ignoreRobots);
|
|
158
215
|
for (const urlStr of this.#options.roots) {
|
|
159
216
|
const url = parseUrl(urlStr, this.#options);
|
|
@@ -324,6 +381,84 @@ class Crawler extends EventEmitter {
|
|
|
324
381
|
});
|
|
325
382
|
}
|
|
326
383
|
}
|
|
384
|
+
/**
|
|
385
|
+
* Emits captured console messages / page errors for a scrape (issue
|
|
386
|
+
* #228), skipping the emit entirely when `entries` is empty.
|
|
387
|
+
*
|
|
388
|
+
* The empty-skip is deliberate, not an optimization: `replaceConsoleLogs`
|
|
389
|
+
* replaces a page's rows wholesale (Scoped-Replace, like
|
|
390
|
+
* `anchor_edges` / `image_items`), so emitting on an empty capture would
|
|
391
|
+
* wipe out a prior good result on a degraded re-scrape (navigation
|
|
392
|
+
* timeout, partial render) that legitimately produced nothing this
|
|
393
|
+
* time — the same trade-off `updatePage` documents for anchors/images.
|
|
394
|
+
* @param entries - Console log entries captured during the page load.
|
|
395
|
+
* @param url - The originally-requested URL (not necessarily the page
|
|
396
|
+
* that ends up holding the content — see `CrawlerEventTypes.consoleLogs`).
|
|
397
|
+
* @param redirectPaths - The redirect chain hops captured during fetch,
|
|
398
|
+
* in order. Empty when the scrape produced no `pageData` (a
|
|
399
|
+
* `'skipped'` / `'error'` result).
|
|
400
|
+
*/
|
|
401
|
+
#handleConsoleLogs(entries, url, redirectPaths) {
|
|
402
|
+
if (entries.length === 0) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
void this.emit('consoleLogs', {
|
|
406
|
+
pageUrl: url.withoutHashAndAuth,
|
|
407
|
+
redirectPaths,
|
|
408
|
+
entries,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Confirm a sliding-window suspect via an active probe, and if
|
|
413
|
+
* confirmed, close {@link #networkGate} and start
|
|
414
|
+
* {@link #runRecoveryProbeLoop}.
|
|
415
|
+
*
|
|
416
|
+
* Guarded by {@link #outageHandlingInProgress} (a synchronous
|
|
417
|
+
* check-then-set, race-free under JS's single-threaded execution) AND
|
|
418
|
+
* by `#networkGate.isOpen` — the latter covers the entire duration a
|
|
419
|
+
* recovery loop is running (no new suspect should re-confirm or
|
|
420
|
+
* re-probe while one outage is already open), the former covers only
|
|
421
|
+
* the narrow async gap between "decided to investigate" and "the
|
|
422
|
+
* confirming probe settled", which the gate-open check alone cannot see
|
|
423
|
+
* since the gate has not closed yet at that point.
|
|
424
|
+
* @param suspect - The trigger emitted by {@link NetworkOutageDetector.record}.
|
|
425
|
+
*/
|
|
426
|
+
async #handleOutageSuspect(suspect) {
|
|
427
|
+
if (!this.#networkGate.isOpen || this.#outageHandlingInProgress) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
this.#outageHandlingInProgress = true;
|
|
431
|
+
try {
|
|
432
|
+
// No usable probe target at all (no session successes yet AND no
|
|
433
|
+
// parseable root URL) — cannot confirm, and cannot ever detect
|
|
434
|
+
// recovery either, so there is nothing safe to do but leave the
|
|
435
|
+
// gate open and treat this as inconclusive.
|
|
436
|
+
const probeHost = chooseProbeHost(this.#successfulHosts, this.#options.roots);
|
|
437
|
+
if (probeHost === null) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const probe = this.#options.networkProbe ?? probeNetwork;
|
|
441
|
+
const initiallyReachable = await probe(probeHost);
|
|
442
|
+
if (initiallyReachable) {
|
|
443
|
+
// False alarm: the sliding window tripped (e.g. several
|
|
444
|
+
// unrelated hosts happened to fail close together) but the
|
|
445
|
+
// probe host answers fine. Leave the gate open.
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
this.#networkGate.close();
|
|
449
|
+
void this.emit('networkOutageConfirmed', {
|
|
450
|
+
startedAt: suspect.startedAt,
|
|
451
|
+
detectedAt: suspect.detectedAt,
|
|
452
|
+
probeHost,
|
|
453
|
+
triggerErrorCount: suspect.triggerErrorCount,
|
|
454
|
+
triggerHostCount: suspect.triggerHostCount,
|
|
455
|
+
});
|
|
456
|
+
void this.#runRecoveryProbeLoop(probeHost, suspect.startedAt);
|
|
457
|
+
}
|
|
458
|
+
finally {
|
|
459
|
+
this.#outageHandlingInProgress = false;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
327
462
|
/**
|
|
328
463
|
* Processes captured sub-resources from a page scrape, deduplicates them,
|
|
329
464
|
* and emits `response` / `responseReferrers` events for new resources.
|
|
@@ -471,6 +606,54 @@ class Crawler extends EventEmitter {
|
|
|
471
606
|
}
|
|
472
607
|
}
|
|
473
608
|
}
|
|
609
|
+
/**
|
|
610
|
+
* Undo cache damage from the outage window `[startedAt, endedAt]`:
|
|
611
|
+
* evict `destinationCache` entries whose cached error looks
|
|
612
|
+
* network-related (any such entry may be stale evidence about the
|
|
613
|
+
* operator's network, not the target site), and un-burn any
|
|
614
|
+
* `dnsBurnedHostCache` host THIS session burned during that window
|
|
615
|
+
* (preload-seeded burns are structurally immune — see
|
|
616
|
+
* `evict-outage-tainted-dns-burns.ts`).
|
|
617
|
+
*
|
|
618
|
+
* Called on every closed→open gate transition, whether triggered by a
|
|
619
|
+
* successful recovery probe or by an abort — the cached failures are
|
|
620
|
+
* stale either way, and the eviction itself has no failure mode that
|
|
621
|
+
* depends on why the gate reopened.
|
|
622
|
+
* @param startedAt - The outage's `startedAt` (from the triggering `OutageSuspect`).
|
|
623
|
+
* @param endedAt - The moment the gate is reopening.
|
|
624
|
+
*/
|
|
625
|
+
#onGateReopened(startedAt, endedAt) {
|
|
626
|
+
evictNetworkClassifiedDestinationCacheEntries(destinationCache);
|
|
627
|
+
evictOutageTaintedDnsBurns({
|
|
628
|
+
cache: dnsBurnedHostCache,
|
|
629
|
+
burnTimestamps: dnsBurnedHostBurnTimestamps,
|
|
630
|
+
window: { startedAt, endedAt },
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Feed one observed network-layer error into
|
|
635
|
+
* {@link #networkOutageDetector} and hand off to
|
|
636
|
+
* {@link #handleOutageSuspect} the instant its sliding window trips.
|
|
637
|
+
*
|
|
638
|
+
* Called from BOTH `onWait` (every non-final retry attempt) and
|
|
639
|
+
* `onGiveUp` (the final attempt) inside {@link #sendHeadRequest}, so a
|
|
640
|
+
* single URL's retry storm contributes every attempt's error, not just
|
|
641
|
+
* its terminal one — a real network-wide outage is expected to trip the
|
|
642
|
+
* `hostThreshold` gate from many DIFFERENT hosts' attempts arriving in
|
|
643
|
+
* the same short window, not from one URL retrying against one host.
|
|
644
|
+
* @param message - The raw error message to classify.
|
|
645
|
+
* @param host - Lower-cased hostname the error occurred on.
|
|
646
|
+
*/
|
|
647
|
+
#recordNetworkError(message, host) {
|
|
648
|
+
const suspect = this.#networkOutageDetector.record({
|
|
649
|
+
kind: classifyErrorKind(message),
|
|
650
|
+
host,
|
|
651
|
+
at: Date.now(),
|
|
652
|
+
});
|
|
653
|
+
if (suspect) {
|
|
654
|
+
void this.#handleOutageSuspect(suspect);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
474
657
|
/**
|
|
475
658
|
* Resolve the source label of the page being scraped so sub-resources
|
|
476
659
|
* captured during its render can inherit the correct lineage label
|
|
@@ -541,6 +724,12 @@ class Crawler extends EventEmitter {
|
|
|
541
724
|
// does not inherit "host alive" claims from a prior run that may have
|
|
542
725
|
// happened on an entirely different network.
|
|
543
726
|
this.#successfulHosts.clear();
|
|
727
|
+
// Network-outage state is per-crawl too: a sliding window of errors
|
|
728
|
+
// (or a gate left closed) from a prior run on this same `Crawler`
|
|
729
|
+
// instance must not leak into a fresh session. `#networkGate.open()`
|
|
730
|
+
// is a no-op if already open.
|
|
731
|
+
this.#networkOutageDetector.reset();
|
|
732
|
+
this.#networkGate.open();
|
|
544
733
|
// external URL の追跡(target は deal の total/done から導出)
|
|
545
734
|
const externalUrls = new Set();
|
|
546
735
|
const externalDoneUrls = new Set();
|
|
@@ -588,6 +777,12 @@ class Crawler extends EventEmitter {
|
|
|
588
777
|
return Promise.all(ops).then(() => { });
|
|
589
778
|
};
|
|
590
779
|
return async () => {
|
|
780
|
+
// Pause here, not inside `fetchDestination` or deeper, so a
|
|
781
|
+
// paused worker shows as a long-running dealer task instead
|
|
782
|
+
// of requiring any change to `@d-zero/dealer` itself — a
|
|
783
|
+
// closed gate resolves the instant `#handleOutageSuspect`'s
|
|
784
|
+
// recovery probe succeeds (see `network-gate.ts`).
|
|
785
|
+
await this.#networkGate.wait();
|
|
591
786
|
// Interval delay is handled here instead of by dealer because
|
|
592
787
|
// DNS-burned hosts must skip the wait entirely. Spending the
|
|
593
788
|
// per-URL interval on a host the cache already knows is dead
|
|
@@ -742,6 +937,7 @@ class Crawler extends EventEmitter {
|
|
|
742
937
|
this.#handleResult(result, url, enqueue, paginationState, concurrency);
|
|
743
938
|
const parentSource = await this.#resolveParentSource(url);
|
|
744
939
|
this.#handleResources(result.resources, parentSource);
|
|
940
|
+
this.#handleConsoleLogs(result.consoleLogs, url, result.pageData?.redirectPaths ?? []);
|
|
745
941
|
log(formatResultSummary(result));
|
|
746
942
|
// Phase errors must be emitted AFTER 'page' / 'externalPage'
|
|
747
943
|
// so the orchestrator's WriteQueue sees `setPage` before
|
|
@@ -815,6 +1011,52 @@ class Crawler extends EventEmitter {
|
|
|
815
1011
|
crawlerLog('Crawl End');
|
|
816
1012
|
void this.emit('crawlEnd', {});
|
|
817
1013
|
}
|
|
1014
|
+
/**
|
|
1015
|
+
* While {@link #networkGate} is closed, probe every
|
|
1016
|
+
* `networkOutageProbeIntervalMs` until one succeeds, then reopen the
|
|
1017
|
+
* gate and emit `networkOutageRecovered`.
|
|
1018
|
+
*
|
|
1019
|
+
* If the crawl is aborted while this loop is running, the gate is
|
|
1020
|
+
* opened anyway (so any worker stuck on `#networkGate.wait()` can
|
|
1021
|
+
* unblock and `deal()` can resolve) but `networkOutageRecovered` is NOT
|
|
1022
|
+
* emitted — an abort says nothing about whether the network actually
|
|
1023
|
+
* recovered, so the `network_outages` row is deliberately left open for
|
|
1024
|
+
* the next writer session's boot-time finalizer
|
|
1025
|
+
* (`close-stale-open-network-outages.ts`) to resolve. Either way,
|
|
1026
|
+
* {@link #onGateReopened} still runs — the cached failures are stale
|
|
1027
|
+
* regardless of why the gate reopened.
|
|
1028
|
+
* @param probeHost - The hostname to probe, chosen once by
|
|
1029
|
+
* {@link #handleOutageSuspect} and reused for every attempt in this loop.
|
|
1030
|
+
* @param startedAt - The outage's `startedAt`, forwarded to {@link #onGateReopened}.
|
|
1031
|
+
*/
|
|
1032
|
+
async #runRecoveryProbeLoop(probeHost, startedAt) {
|
|
1033
|
+
const probe = this.#options.networkProbe ?? probeNetwork;
|
|
1034
|
+
const bailIfAborted = () => {
|
|
1035
|
+
if (!this.#abortController.signal.aborted) {
|
|
1036
|
+
return false;
|
|
1037
|
+
}
|
|
1038
|
+
this.#networkGate.open();
|
|
1039
|
+
this.#onGateReopened(startedAt, Date.now());
|
|
1040
|
+
return true;
|
|
1041
|
+
};
|
|
1042
|
+
if (bailIfAborted()) {
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
for (;;) {
|
|
1046
|
+
await delay(this.#options.networkOutageProbeIntervalMs);
|
|
1047
|
+
if (bailIfAborted()) {
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
const recovered = await probe(probeHost);
|
|
1051
|
+
if (recovered) {
|
|
1052
|
+
const endedAt = Date.now();
|
|
1053
|
+
this.#networkGate.open();
|
|
1054
|
+
this.#onGateReopened(startedAt, endedAt);
|
|
1055
|
+
void this.emit('networkOutageRecovered', { endedAt });
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
818
1060
|
/**
|
|
819
1061
|
* Orchestrates the full scrape pipeline for a single URL.
|
|
820
1062
|
*
|
|
@@ -875,6 +1117,7 @@ class Crawler extends EventEmitter {
|
|
|
875
1117
|
type: 'success',
|
|
876
1118
|
pageData: metadataOnly ? { ...pageData, isTarget: false } : pageData,
|
|
877
1119
|
resources: [],
|
|
1120
|
+
consoleLogs: [],
|
|
878
1121
|
};
|
|
879
1122
|
}
|
|
880
1123
|
}
|
|
@@ -1002,6 +1245,7 @@ class Crawler extends EventEmitter {
|
|
|
1002
1245
|
return {
|
|
1003
1246
|
type: 'error',
|
|
1004
1247
|
resources: [],
|
|
1248
|
+
consoleLogs: [],
|
|
1005
1249
|
error: {
|
|
1006
1250
|
name: error instanceof Error ? error.name : 'Error',
|
|
1007
1251
|
message: errorMessage,
|
|
@@ -1015,6 +1259,7 @@ class Crawler extends EventEmitter {
|
|
|
1015
1259
|
return {
|
|
1016
1260
|
type: 'error',
|
|
1017
1261
|
resources: [],
|
|
1262
|
+
consoleLogs: [],
|
|
1018
1263
|
error: {
|
|
1019
1264
|
name: error instanceof Error ? error.name : 'Error',
|
|
1020
1265
|
message: errorMessage,
|
|
@@ -1062,6 +1307,7 @@ class Crawler extends EventEmitter {
|
|
|
1062
1307
|
type: 'success',
|
|
1063
1308
|
pageData: { ...titleResult, isTarget: false },
|
|
1064
1309
|
resources: [],
|
|
1310
|
+
consoleLogs: [],
|
|
1065
1311
|
};
|
|
1066
1312
|
}
|
|
1067
1313
|
catch (error) {
|
|
@@ -1072,6 +1318,7 @@ class Crawler extends EventEmitter {
|
|
|
1072
1318
|
type: 'success',
|
|
1073
1319
|
pageData: { ...headCheckResult, isTarget: false },
|
|
1074
1320
|
resources: [],
|
|
1321
|
+
consoleLogs: [],
|
|
1075
1322
|
};
|
|
1076
1323
|
}
|
|
1077
1324
|
// Non-HTML content — skip browser
|
|
@@ -1081,6 +1328,7 @@ class Crawler extends EventEmitter {
|
|
|
1081
1328
|
type: 'success',
|
|
1082
1329
|
pageData: headCheckResult,
|
|
1083
1330
|
resources: [],
|
|
1331
|
+
consoleLogs: [],
|
|
1084
1332
|
};
|
|
1085
1333
|
}
|
|
1086
1334
|
// HTML or unknown content type — launch browser with preflight result.
|
|
@@ -1266,9 +1514,11 @@ class Crawler extends EventEmitter {
|
|
|
1266
1514
|
retries: this.#options.retry,
|
|
1267
1515
|
label: 'HEAD request',
|
|
1268
1516
|
onWait: (determinedInterval, retryCount, label, error) => {
|
|
1517
|
+
this.#recordNetworkError(error.message, host);
|
|
1269
1518
|
update(`${label}: ${error.message} — %countdown(${determinedInterval},fetchHead_${laneIndex}_${retryCount},s)%s (retry #${retryCount + 1})`);
|
|
1270
1519
|
},
|
|
1271
1520
|
onGiveUp: (retryCount, error, label) => {
|
|
1521
|
+
this.#recordNetworkError(error.message, host);
|
|
1272
1522
|
// Burn the host so subsequent URLs short-circuit — but ONLY
|
|
1273
1523
|
// when this is the first time we've ever seen the host fail
|
|
1274
1524
|
// in this session. A host that responded earlier is treated
|
|
@@ -1284,6 +1534,12 @@ class Crawler extends EventEmitter {
|
|
|
1284
1534
|
successfulHosts: this.#successfulHosts,
|
|
1285
1535
|
})) {
|
|
1286
1536
|
dnsBurnedHostCache.set(host, 'dns');
|
|
1537
|
+
// Recorded so a later outage recovery can tell THIS
|
|
1538
|
+
// burn (possibly outage-caused) apart from a
|
|
1539
|
+
// preload-seeded one (a cross-session, confirmed-dead
|
|
1540
|
+
// verdict that must never be undone by an in-session
|
|
1541
|
+
// recovery) — see `evict-outage-tainted-dns-burns.ts`.
|
|
1542
|
+
dnsBurnedHostBurnTimestamps.set(host, Date.now());
|
|
1287
1543
|
}
|
|
1288
1544
|
update(c.red(`${label}: gave up after ${retryCount} retries — ${error.message}`));
|
|
1289
1545
|
},
|
|
@@ -1479,6 +1735,7 @@ class Crawler extends EventEmitter {
|
|
|
1479
1735
|
return {
|
|
1480
1736
|
type: 'error',
|
|
1481
1737
|
resources: [],
|
|
1738
|
+
consoleLogs: [],
|
|
1482
1739
|
error: {
|
|
1483
1740
|
name: error instanceof Error ? error.name : 'Error',
|
|
1484
1741
|
message: error instanceof Error ? error.message : String(error),
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Companion to `dnsBurnedHostCache`: records the epoch-ms timestamp of each
|
|
3
|
+
* host burned by THIS session's `#sendHeadRequest` `onGiveUp` path (the
|
|
4
|
+
* `shouldBurnHost`-gated write in `crawler.ts`).
|
|
5
|
+
*
|
|
6
|
+
* Deliberately NOT populated by `#preloadDnsBurnedHostCache`'s seeding from
|
|
7
|
+
* a previous session's `crawl_errors` — those entries represent hosts
|
|
8
|
+
* already proven dead across sessions and must never be evicted just
|
|
9
|
+
* because THIS session's network happened to recover from an outage. Since
|
|
10
|
+
* `dnsBurnedHostCache`'s value (`ErrorKind`) carries no provenance of its
|
|
11
|
+
* own (preload-seeded and session-learned burns are indistinguishable by
|
|
12
|
+
* value alone), this separate timestamp map is what lets
|
|
13
|
+
* `evict-outage-tainted-dns-burns.ts` tell them apart: only hosts present
|
|
14
|
+
* HERE are eligible for eviction, and only if their burn timestamp falls
|
|
15
|
+
* inside the just-recovered outage's window.
|
|
16
|
+
*/
|
|
17
|
+
export declare const dnsBurnedHostBurnTimestamps: Map<string, number>;
|