@nitpicker/crawler 0.14.0 → 0.16.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 +136 -10
- package/lib/archive/archive.js +147 -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 +14 -0
- package/lib/archive/create-adjunct-tables.js +169 -0
- package/lib/archive/create-entity-tables.js +21 -1
- package/lib/archive/database.d.ts +67 -2
- package/lib/archive/database.js +92 -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/dedupe-cap/accumulate-dedupe-cap-rejected-count.d.ts +18 -0
- package/lib/archive/db-ops/dedupe-cap/accumulate-dedupe-cap-rejected-count.js +23 -0
- package/lib/archive/db-ops/dedupe-cap/finalize-dedupe-cap-event.d.ts +12 -0
- package/lib/archive/db-ops/dedupe-cap/finalize-dedupe-cap-event.js +15 -0
- package/lib/archive/db-ops/dedupe-cap/insert-dedupe-cap-event.d.ts +14 -0
- package/lib/archive/db-ops/dedupe-cap/insert-dedupe-cap-event.js +30 -0
- package/lib/archive/db-ops/dedupe-cap/list-dedupe-cap-shape-keys.d.ts +21 -0
- package/lib/archive/db-ops/dedupe-cap/list-dedupe-cap-shape-keys.js +27 -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 +60 -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/classify-error-kind.d.ts +1 -0
- package/lib/classify-error-kind.js +14 -0
- package/lib/crawler/assert-chrome-installed.d.ts +24 -0
- package/lib/crawler/assert-chrome-installed.js +43 -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.d.ts +12 -0
- package/lib/crawler/crawler.js +496 -29
- package/lib/crawler/decode-auth-credential.d.ts +29 -0
- package/lib/crawler/decode-auth-credential.js +39 -0
- package/lib/crawler/dedupe/compute-meta-signature.d.ts +30 -0
- package/lib/crawler/dedupe/compute-meta-signature.js +0 -0
- package/lib/crawler/dedupe/compute-shape-key.d.ts +37 -0
- package/lib/crawler/dedupe/compute-shape-key.js +56 -0
- package/lib/crawler/dedupe/dedupe-cap-tracker.d.ts +84 -0
- package/lib/crawler/dedupe/dedupe-cap-tracker.js +185 -0
- package/lib/crawler/dedupe/is-predicted-content-duplicate.d.ts +24 -0
- package/lib/crawler/dedupe/is-predicted-content-duplicate.js +26 -0
- package/lib/crawler/dedupe/is-shape-capped.d.ts +10 -0
- package/lib/crawler/dedupe/is-shape-capped.js +12 -0
- package/lib/crawler/dedupe/resolve-og-url-mismatch.d.ts +31 -0
- package/lib/crawler/dedupe/resolve-og-url-mismatch.js +40 -0
- package/lib/crawler/dedupe/types.d.ts +42 -0
- package/lib/crawler/dedupe/types.js +1 -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/fetch-destination.js +14 -2
- package/lib/crawler/generate-predicted-urls.d.ts +12 -0
- package/lib/crawler/generate-predicted-urls.js +33 -2
- package/lib/crawler/is-puppeteer-fallback-candidate.js +3 -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 +185 -1
- package/lib/crawler-orchestrator.d.ts +74 -18
- package/lib/crawler-orchestrator.js +267 -27
- package/lib/crawler.d.ts +22 -0
- package/lib/crawler.js +18 -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/permanent-error-kinds.d.ts +9 -4
- package/lib/permanent-error-kinds.js +10 -4
- package/lib/types.d.ts +11 -2
- 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,42 @@
|
|
|
1
|
+
/** Options controlling the opt-in same-cluster soft cap (`--dedupe-cap` / `--dedupe-map-cap`). */
|
|
2
|
+
export interface DedupeCapOptions {
|
|
3
|
+
/** Base Misra-Gries majority-vote threshold, before confidence-signal halving. */
|
|
4
|
+
cap: number;
|
|
5
|
+
/** Hard cap on the number of distinct shapes tracked at once; the least-recently-touched shape is evicted beyond this. */
|
|
6
|
+
mapCap: number;
|
|
7
|
+
}
|
|
8
|
+
/** Per-shape Misra-Gries majority-vote slot. */
|
|
9
|
+
export interface DedupeSlot {
|
|
10
|
+
/** The dominant meta signature currently "winning" for this shape. */
|
|
11
|
+
metaSig: string;
|
|
12
|
+
/** Majority-vote counter: incremented on a matching `metaSig`, decremented otherwise. Never exceeds the true count of matching observations. */
|
|
13
|
+
count: number;
|
|
14
|
+
/** The `computeBodyHash` result recorded when this slot was (re)created. */
|
|
15
|
+
bodyHash: Buffer;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* One page's observation fed into `DedupeCapTracker#observe`. Callers are
|
|
19
|
+
* responsible for excluding pages with no signal (empty `computeMetaSignature`
|
|
20
|
+
* result, external, or metadata-only) before constructing this — the tracker
|
|
21
|
+
* itself does not special-case them.
|
|
22
|
+
*/
|
|
23
|
+
export interface DedupeCapObservation {
|
|
24
|
+
/** The page's URL shape key (see `computeShapeKey`). */
|
|
25
|
+
shapeKey: string;
|
|
26
|
+
/** The page's meta signature (see `computeMetaSignature`). */
|
|
27
|
+
metaSig: string;
|
|
28
|
+
/** The page's `computeBodyHash` result. */
|
|
29
|
+
bodyHash: Buffer;
|
|
30
|
+
/** Whether the page's (absolutised) `og:url` differs from its own URL (see `resolveOgUrlMismatch`). */
|
|
31
|
+
ogUrlMismatch: boolean;
|
|
32
|
+
/** The page's own URL — recorded as the resulting event's `sampleUrl` if this observation caps the shape. */
|
|
33
|
+
url: string;
|
|
34
|
+
}
|
|
35
|
+
/** Emitted the moment a shape's effective threshold is first reached. */
|
|
36
|
+
export interface DedupeCapEvent {
|
|
37
|
+
shapeKey: string;
|
|
38
|
+
sampleUrl: string;
|
|
39
|
+
bodyHash: Buffer;
|
|
40
|
+
effectiveThreshold: number;
|
|
41
|
+
observedCount: number;
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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>;
|
|
@@ -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 const dnsBurnedHostBurnTimestamps = new Map();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PageData } from '@d-zero/beholder';
|
|
2
|
+
/**
|
|
3
|
+
* Delete every `destinationCache` entry whose cached value is an `Error`
|
|
4
|
+
* that classifies as a {@link NETWORK_RELATED_ERROR_KINDS network-related}
|
|
5
|
+
* kind, leaving successes and site-specific failures (`tls`,
|
|
6
|
+
* `client-blocked`, `connection-refused`, …) untouched.
|
|
7
|
+
*
|
|
8
|
+
* Called by `Crawler` whenever its network gate transitions from closed to
|
|
9
|
+
* open (whether via a successful recovery probe or an abort), so a URL
|
|
10
|
+
* that failed with `getaddrinfo ENOTFOUND` only because the operator's own
|
|
11
|
+
* network was down does not stay wrongly cached as "this host is dead" for
|
|
12
|
+
* the rest of the session.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately does NOT distinguish "cached during THIS specific outage"
|
|
15
|
+
* from "cached during an earlier blip this session" — any cached error
|
|
16
|
+
* whose KIND looks network-related is, by definition, potentially stale
|
|
17
|
+
* evidence about the operator's network rather than the target site, so it
|
|
18
|
+
* is always safe to re-test after any recovery. The trade-off is a few
|
|
19
|
+
* redundant HEAD attempts for hosts that were already dead independent of
|
|
20
|
+
* the outage; the alternative (a stale "outage-tainted" failure verdict
|
|
21
|
+
* surviving the rest of the session) is strictly worse.
|
|
22
|
+
* @param cache - The cache to sweep. Takes the `Map` explicitly (rather
|
|
23
|
+
* than importing the `destinationCache` singleton directly) so this stays
|
|
24
|
+
* unit-testable without touching module-level state.
|
|
25
|
+
*/
|
|
26
|
+
export declare function evictNetworkClassifiedDestinationCacheEntries(cache: Map<string, PageData | Error>): void;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { classifyErrorKind } from '../classify-error-kind.js';
|
|
2
|
+
import { NETWORK_RELATED_ERROR_KINDS } from '../network-related-error-kinds.js';
|
|
3
|
+
/**
|
|
4
|
+
* Delete every `destinationCache` entry whose cached value is an `Error`
|
|
5
|
+
* that classifies as a {@link NETWORK_RELATED_ERROR_KINDS network-related}
|
|
6
|
+
* kind, leaving successes and site-specific failures (`tls`,
|
|
7
|
+
* `client-blocked`, `connection-refused`, …) untouched.
|
|
8
|
+
*
|
|
9
|
+
* Called by `Crawler` whenever its network gate transitions from closed to
|
|
10
|
+
* open (whether via a successful recovery probe or an abort), so a URL
|
|
11
|
+
* that failed with `getaddrinfo ENOTFOUND` only because the operator's own
|
|
12
|
+
* network was down does not stay wrongly cached as "this host is dead" for
|
|
13
|
+
* the rest of the session.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately does NOT distinguish "cached during THIS specific outage"
|
|
16
|
+
* from "cached during an earlier blip this session" — any cached error
|
|
17
|
+
* whose KIND looks network-related is, by definition, potentially stale
|
|
18
|
+
* evidence about the operator's network rather than the target site, so it
|
|
19
|
+
* is always safe to re-test after any recovery. The trade-off is a few
|
|
20
|
+
* redundant HEAD attempts for hosts that were already dead independent of
|
|
21
|
+
* the outage; the alternative (a stale "outage-tainted" failure verdict
|
|
22
|
+
* surviving the rest of the session) is strictly worse.
|
|
23
|
+
* @param cache - The cache to sweep. Takes the `Map` explicitly (rather
|
|
24
|
+
* than importing the `destinationCache` singleton directly) so this stays
|
|
25
|
+
* unit-testable without touching module-level state.
|
|
26
|
+
*/
|
|
27
|
+
export function evictNetworkClassifiedDestinationCacheEntries(cache) {
|
|
28
|
+
for (const [key, value] of cache) {
|
|
29
|
+
if (value instanceof Error &&
|
|
30
|
+
NETWORK_RELATED_ERROR_KINDS.has(classifyErrorKind(value.message))) {
|
|
31
|
+
cache.delete(key);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { OutageWindow } from '../is-within-outage-window.js';
|
|
2
|
+
import type { ErrorKind } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Undo `dnsBurnedHostCache` burns whose recorded timestamp (see
|
|
5
|
+
* `dns-burned-host-burn-timestamps.ts`) falls inside `window` — i.e. hosts
|
|
6
|
+
* THIS session burned because a HEAD request failed with a
|
|
7
|
+
* `dns`-classified error while (or just before) the operator's own network
|
|
8
|
+
* was down, not because the host is actually dead.
|
|
9
|
+
*
|
|
10
|
+
* Preload-seeded burns are structurally immune: they never appear in
|
|
11
|
+
* `burnTimestamps` (see that module's docstring), so this function can
|
|
12
|
+
* never touch them regardless of the window.
|
|
13
|
+
* @param options - Named parameters.
|
|
14
|
+
* @param options.cache - `dnsBurnedHostCache` (or a test double with the same shape).
|
|
15
|
+
* @param options.burnTimestamps - `dnsBurnedHostBurnTimestamps` (or a test double).
|
|
16
|
+
* @param options.window - The just-resolved outage window (`{ startedAt, endedAt }`)
|
|
17
|
+
* to test each burn's timestamp against.
|
|
18
|
+
*/
|
|
19
|
+
export declare function evictOutageTaintedDnsBurns(options: {
|
|
20
|
+
cache: Map<string, ErrorKind>;
|
|
21
|
+
burnTimestamps: Map<string, number>;
|
|
22
|
+
window: OutageWindow;
|
|
23
|
+
}): void;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { isWithinOutageWindow } from '../is-within-outage-window.js';
|
|
2
|
+
/**
|
|
3
|
+
* Undo `dnsBurnedHostCache` burns whose recorded timestamp (see
|
|
4
|
+
* `dns-burned-host-burn-timestamps.ts`) falls inside `window` — i.e. hosts
|
|
5
|
+
* THIS session burned because a HEAD request failed with a
|
|
6
|
+
* `dns`-classified error while (or just before) the operator's own network
|
|
7
|
+
* was down, not because the host is actually dead.
|
|
8
|
+
*
|
|
9
|
+
* Preload-seeded burns are structurally immune: they never appear in
|
|
10
|
+
* `burnTimestamps` (see that module's docstring), so this function can
|
|
11
|
+
* never touch them regardless of the window.
|
|
12
|
+
* @param options - Named parameters.
|
|
13
|
+
* @param options.cache - `dnsBurnedHostCache` (or a test double with the same shape).
|
|
14
|
+
* @param options.burnTimestamps - `dnsBurnedHostBurnTimestamps` (or a test double).
|
|
15
|
+
* @param options.window - The just-resolved outage window (`{ startedAt, endedAt }`)
|
|
16
|
+
* to test each burn's timestamp against.
|
|
17
|
+
*/
|
|
18
|
+
export function evictOutageTaintedDnsBurns(options) {
|
|
19
|
+
const { cache, burnTimestamps, window } = options;
|
|
20
|
+
for (const [host, burnedAt] of burnTimestamps) {
|
|
21
|
+
if (isWithinOutageWindow(burnedAt, [window])) {
|
|
22
|
+
cache.delete(host);
|
|
23
|
+
burnTimestamps.delete(host);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { delay } from '@d-zero/shared/delay';
|
|
2
2
|
import redirects from 'follow-redirects';
|
|
3
|
+
import { decodeAuthCredential } from './decode-auth-credential.js';
|
|
3
4
|
import { destinationCache } from './destination-cache.js';
|
|
4
5
|
import NetTimeoutError from './net-timeout-error.js';
|
|
5
6
|
import { shouldGetFallbackOnHeadFailure } from './should-get-fallback-on-head-failure.js';
|
|
@@ -140,8 +141,19 @@ async function _fetchHead(url, isExternal, method, titleBytesLimit, userAgent, t
|
|
|
140
141
|
// Range: url.extname?.toLowerCase() === 'pdf' ? 'bytes=0-0' : undefined,
|
|
141
142
|
},
|
|
142
143
|
};
|
|
143
|
-
|
|
144
|
-
|
|
144
|
+
// `||`, not `&&`: ExURL rounds an empty userinfo component to null,
|
|
145
|
+
// so `http://user:@host/` (legal empty-password Basic auth) has
|
|
146
|
+
// `password: null`. Requiring both would leave this pre-flight
|
|
147
|
+
// unauthenticated while the browser path (`page.authenticate`, which
|
|
148
|
+
// always runs and treats null as '') succeeds — and for non-HTML
|
|
149
|
+
// content the browser never runs, so the HEAD 401 would become the
|
|
150
|
+
// page's final recorded status.
|
|
151
|
+
if (url.username || url.password) {
|
|
152
|
+
// ExURL fields are WHATWG percent-encoded; `auth` is base64'd
|
|
153
|
+
// verbatim into the Authorization header, so decode first —
|
|
154
|
+
// mirroring what Node's own `urlToOptions` does for
|
|
155
|
+
// `http.request(url)` (see `decode-auth-credential.ts`).
|
|
156
|
+
request.auth = `${decodeAuthCredential(url.username)}:${decodeAuthCredential(url.password)}`;
|
|
145
157
|
}
|
|
146
158
|
let req;
|
|
147
159
|
let destroyed = false;
|
|
@@ -5,6 +5,18 @@ import type { PaginationPattern } from './types.js';
|
|
|
5
5
|
* Starting from `currentUrl`, applies the pattern's step `count` times to produce
|
|
6
6
|
* future page URLs (e.g. if step=1 and currentNumber=2, generates page 3, 4, ...).
|
|
7
7
|
* These URLs are pushed into the crawl queue and discarded later if they 404.
|
|
8
|
+
*
|
|
9
|
+
* Generation stops at the first token that would lose its original digit-string
|
|
10
|
+
* shape, rather than skipping it and continuing: beyond `Number.MAX_SAFE_INTEGER`,
|
|
11
|
+
* `String()` renders scientific notation (e.g. `"1e+21"`), which is not a valid
|
|
12
|
+
* path/query token and — left unguarded — becomes a self-generated URL that no
|
|
13
|
+
* page on the target site ever linked to (observed in production: a
|
|
14
|
+
* `/news/date/{year}/` pager whose per-anchor `step` was miscalculated from
|
|
15
|
+
* unrelated pages, compounding across rounds until it emitted
|
|
16
|
+
* `1.7715854126052197e+120`). A token growing far beyond its original digit
|
|
17
|
+
* count is equally implausible as a next page number. Both trends are
|
|
18
|
+
* monotonic as `i` increases, so once one prediction is rejected, every later
|
|
19
|
+
* one in the same batch would be too — there is nothing to skip past.
|
|
8
20
|
* @param pattern - The detected pagination pattern from `detectPaginationPattern()`
|
|
9
21
|
* @param currentUrl - The URL to extrapolate from (protocol-agnostic, without hash/auth)
|
|
10
22
|
* @param count - Number of predicted URLs to generate (typically equals concurrency)
|
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import { decomposeUrl } from './decompose-url.js';
|
|
2
2
|
import { reconstructUrl } from './reconstruct-url.js';
|
|
3
|
+
const DIGITS_ONLY_PATTERN = /^\d+$/;
|
|
3
4
|
/**
|
|
4
5
|
* Generates predicted URLs by extrapolating the detected pagination pattern.
|
|
5
6
|
*
|
|
6
7
|
* Starting from `currentUrl`, applies the pattern's step `count` times to produce
|
|
7
8
|
* future page URLs (e.g. if step=1 and currentNumber=2, generates page 3, 4, ...).
|
|
8
9
|
* These URLs are pushed into the crawl queue and discarded later if they 404.
|
|
10
|
+
*
|
|
11
|
+
* Generation stops at the first token that would lose its original digit-string
|
|
12
|
+
* shape, rather than skipping it and continuing: beyond `Number.MAX_SAFE_INTEGER`,
|
|
13
|
+
* `String()` renders scientific notation (e.g. `"1e+21"`), which is not a valid
|
|
14
|
+
* path/query token and — left unguarded — becomes a self-generated URL that no
|
|
15
|
+
* page on the target site ever linked to (observed in production: a
|
|
16
|
+
* `/news/date/{year}/` pager whose per-anchor `step` was miscalculated from
|
|
17
|
+
* unrelated pages, compounding across rounds until it emitted
|
|
18
|
+
* `1.7715854126052197e+120`). A token growing far beyond its original digit
|
|
19
|
+
* count is equally implausible as a next page number. Both trends are
|
|
20
|
+
* monotonic as `i` increases, so once one prediction is rejected, every later
|
|
21
|
+
* one in the same batch would be too — there is nothing to skip past.
|
|
9
22
|
* @param pattern - The detected pagination pattern from `detectPaginationPattern()`
|
|
10
23
|
* @param currentUrl - The URL to extrapolate from (protocol-agnostic, without hash/auth)
|
|
11
24
|
* @param count - Number of predicted URLs to generate (typically equals concurrency)
|
|
@@ -17,11 +30,29 @@ export function generatePredictedUrls(pattern, currentUrl, count) {
|
|
|
17
30
|
const decomposed = decomposeUrl(currentUrl);
|
|
18
31
|
if (!decomposed)
|
|
19
32
|
return [];
|
|
33
|
+
const { pathSegments, queryValues } = decomposed;
|
|
34
|
+
const originalToken = pattern.tokenIndex < pathSegments.length
|
|
35
|
+
? pathSegments[pattern.tokenIndex]
|
|
36
|
+
: queryValues[pattern.tokenIndex - pathSegments.length];
|
|
37
|
+
if (originalToken === undefined)
|
|
38
|
+
return [];
|
|
39
|
+
// A pager jumping from 4 digits to 5 (e.g. year 9999 → 10000) is plausible;
|
|
40
|
+
// jumping straight to 6+ digits within the same predicted batch is not.
|
|
41
|
+
const maxDigits = originalToken.length + 1;
|
|
20
42
|
const results = [];
|
|
21
43
|
for (let i = 1; i <= count; i++) {
|
|
22
44
|
const nextNum = pattern.currentNumber + pattern.step * i;
|
|
23
|
-
|
|
24
|
-
|
|
45
|
+
if (!Number.isSafeInteger(nextNum))
|
|
46
|
+
break;
|
|
47
|
+
const rendered = String(nextNum);
|
|
48
|
+
if (!DIGITS_ONLY_PATTERN.test(rendered))
|
|
49
|
+
break;
|
|
50
|
+
// Preserve zero-padding width (e.g. "01" → "02"), without truncating a
|
|
51
|
+
// value that has legitimately grown past the original width.
|
|
52
|
+
const padded = rendered.padStart(originalToken.length, '0');
|
|
53
|
+
if (padded.length > maxDigits)
|
|
54
|
+
break;
|
|
55
|
+
results.push(reconstructUrl(decomposed, pattern.tokenIndex, padded));
|
|
25
56
|
}
|
|
26
57
|
return results;
|
|
27
58
|
}
|
|
@@ -26,6 +26,9 @@ import { classifyErrorKind } from '../classify-error-kind.js';
|
|
|
26
26
|
* as `timeout` via the `NetTimeoutError "Timeout: <url>"` shape.
|
|
27
27
|
* - **local-network** — operator-side connectivity loss; nothing on this
|
|
28
28
|
* machine will reach the host.
|
|
29
|
+
* - **redirect-loop** — the redirect chain itself never terminates; a
|
|
30
|
+
* puppeteer navigation follows the identical Location-header chain (or
|
|
31
|
+
* hits Chromium's own `ERR_TOO_MANY_REDIRECTS`) and loops the same way.
|
|
29
32
|
* - **protocol** — puppeteer lifecycle race; bouncing back to puppeteer
|
|
30
33
|
* reproduces the same race.
|
|
31
34
|
* - **unknown** — by design. Spinning up a fresh Chromium for every
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Level-triggered gate that dealer worker callbacks await before doing any
|
|
3
|
+
* network work. Achieves "pause the crawl until the network recovers"
|
|
4
|
+
* without touching `@d-zero/dealer` internals — a closed gate just makes
|
|
5
|
+
* the worker's task body a long-running `await`, which is all dealer ever
|
|
6
|
+
* sees.
|
|
7
|
+
*
|
|
8
|
+
* **`epoch`** counts outage generations: it increments on every `close()`
|
|
9
|
+
* transition (open → closed) and holds that value for the remainder of the
|
|
10
|
+
* closed period AND through the following open period, until the next
|
|
11
|
+
* `close()`. This is the hook `destinationCache` / `dnsBurnedHostCache`
|
|
12
|
+
* eviction uses: an entry written while the gate was closed is tagged with
|
|
13
|
+
* `gate.epoch` at write time; after `open()`, the caller purges entries
|
|
14
|
+
* tagged with that same epoch value. Because `epoch` only advances on
|
|
15
|
+
* `close()` (never on `open()`), entries written during ordinary (open)
|
|
16
|
+
* operation are never mistakenly tagged with a soon-to-be-purged epoch —
|
|
17
|
+
* only writes that happen while `isOpen` is `false` get tagged at all.
|
|
18
|
+
*/
|
|
19
|
+
export default class NetworkGate {
|
|
20
|
+
#private;
|
|
21
|
+
/** `true` when the gate is open (the default). */
|
|
22
|
+
get isOpen(): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Current outage-generation counter. See the class docstring for how
|
|
25
|
+
* cache-eviction callers are expected to use this value.
|
|
26
|
+
*/
|
|
27
|
+
get epoch(): number;
|
|
28
|
+
/**
|
|
29
|
+
* Close the gate, blocking future {@link wait} callers until the next
|
|
30
|
+
* {@link open}. Idempotent — calling `close()` while already closed does
|
|
31
|
+
* nothing (in particular, it does NOT bump {@link epoch} a second time,
|
|
32
|
+
* which would otherwise let a still-open outage's cache entries escape
|
|
33
|
+
* eviction under a stale epoch number).
|
|
34
|
+
*/
|
|
35
|
+
close(): void;
|
|
36
|
+
/**
|
|
37
|
+
* Open the gate, resolving every {@link wait} caller currently pending —
|
|
38
|
+
* including ones that subscribed after `close()` but before this call.
|
|
39
|
+
* Idempotent — calling `open()` while already open does nothing.
|
|
40
|
+
*/
|
|
41
|
+
open(): void;
|
|
42
|
+
/**
|
|
43
|
+
* Resolve immediately if the gate is open; otherwise resolve on the next
|
|
44
|
+
* {@link open}. Multiple concurrent callers all resolve on the same
|
|
45
|
+
* `open()` call — no awaiter is dropped regardless of when it subscribed
|
|
46
|
+
* relative to others.
|
|
47
|
+
*/
|
|
48
|
+
wait(): Promise<void>;
|
|
49
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Level-triggered gate that dealer worker callbacks await before doing any
|
|
3
|
+
* network work. Achieves "pause the crawl until the network recovers"
|
|
4
|
+
* without touching `@d-zero/dealer` internals — a closed gate just makes
|
|
5
|
+
* the worker's task body a long-running `await`, which is all dealer ever
|
|
6
|
+
* sees.
|
|
7
|
+
*
|
|
8
|
+
* **`epoch`** counts outage generations: it increments on every `close()`
|
|
9
|
+
* transition (open → closed) and holds that value for the remainder of the
|
|
10
|
+
* closed period AND through the following open period, until the next
|
|
11
|
+
* `close()`. This is the hook `destinationCache` / `dnsBurnedHostCache`
|
|
12
|
+
* eviction uses: an entry written while the gate was closed is tagged with
|
|
13
|
+
* `gate.epoch` at write time; after `open()`, the caller purges entries
|
|
14
|
+
* tagged with that same epoch value. Because `epoch` only advances on
|
|
15
|
+
* `close()` (never on `open()`), entries written during ordinary (open)
|
|
16
|
+
* operation are never mistakenly tagged with a soon-to-be-purged epoch —
|
|
17
|
+
* only writes that happen while `isOpen` is `false` get tagged at all.
|
|
18
|
+
*/
|
|
19
|
+
export default class NetworkGate {
|
|
20
|
+
#epoch = 0;
|
|
21
|
+
#open = true;
|
|
22
|
+
#waiters = [];
|
|
23
|
+
/** `true` when the gate is open (the default). */
|
|
24
|
+
get isOpen() {
|
|
25
|
+
return this.#open;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Current outage-generation counter. See the class docstring for how
|
|
29
|
+
* cache-eviction callers are expected to use this value.
|
|
30
|
+
*/
|
|
31
|
+
get epoch() {
|
|
32
|
+
return this.#epoch;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Close the gate, blocking future {@link wait} callers until the next
|
|
36
|
+
* {@link open}. Idempotent — calling `close()` while already closed does
|
|
37
|
+
* nothing (in particular, it does NOT bump {@link epoch} a second time,
|
|
38
|
+
* which would otherwise let a still-open outage's cache entries escape
|
|
39
|
+
* eviction under a stale epoch number).
|
|
40
|
+
*/
|
|
41
|
+
close() {
|
|
42
|
+
if (!this.#open) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
this.#open = false;
|
|
46
|
+
this.#epoch += 1;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Open the gate, resolving every {@link wait} caller currently pending —
|
|
50
|
+
* including ones that subscribed after `close()` but before this call.
|
|
51
|
+
* Idempotent — calling `open()` while already open does nothing.
|
|
52
|
+
*/
|
|
53
|
+
open() {
|
|
54
|
+
if (this.#open) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
this.#open = true;
|
|
58
|
+
const waiters = this.#waiters;
|
|
59
|
+
this.#waiters = [];
|
|
60
|
+
for (const resolve of waiters) {
|
|
61
|
+
resolve();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Resolve immediately if the gate is open; otherwise resolve on the next
|
|
66
|
+
* {@link open}. Multiple concurrent callers all resolve on the same
|
|
67
|
+
* `open()` call — no awaiter is dropped regardless of when it subscribed
|
|
68
|
+
* relative to others.
|
|
69
|
+
*/
|
|
70
|
+
wait() {
|
|
71
|
+
if (this.#open) {
|
|
72
|
+
return Promise.resolve();
|
|
73
|
+
}
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
this.#waiters.push(resolve);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { NetworkErrorRecord, NetworkOutageDetectorOptions, OutageSuspect } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Sliding-window detector for "this looks like the crawl operator's own
|
|
4
|
+
* network, not the target sites, is having a bad time" — the trigger that
|
|
5
|
+
* hands off to an active probe (see `probe-network.ts`) before the crawler
|
|
6
|
+
* commits to pausing.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately dependency-free: no timers, no DB, no `Crawler` instance.
|
|
9
|
+
* Time flows in via {@link NetworkErrorRecord.at} only, which is what makes
|
|
10
|
+
* window-boundary behaviour testable with exact values instead of
|
|
11
|
+
* `vi.useFakeTimers()`.
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const detector = new NetworkOutageDetector({
|
|
15
|
+
* windowMs: 10_000,
|
|
16
|
+
* errorThreshold: 5,
|
|
17
|
+
* hostThreshold: 2,
|
|
18
|
+
* });
|
|
19
|
+
* const suspect = detector.record({ kind: 'dns', host: 'a.example', at: Date.now() });
|
|
20
|
+
* if (suspect) {
|
|
21
|
+
* // probe before closing the gate
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export default class NetworkOutageDetector {
|
|
26
|
+
#private;
|
|
27
|
+
constructor(options: NetworkOutageDetectorOptions);
|
|
28
|
+
/**
|
|
29
|
+
* Record one observed error and check whether it tips the sliding window
|
|
30
|
+
* over both thresholds.
|
|
31
|
+
*
|
|
32
|
+
* Non-network `kind`s (see `NETWORK_RELATED_ERROR_KINDS`) are silently
|
|
33
|
+
* ignored: they never enter the window and can never contribute to a
|
|
34
|
+
* trigger, regardless of how many arrive.
|
|
35
|
+
* @param record - The observed error.
|
|
36
|
+
* @param record.kind
|
|
37
|
+
* @param record.host
|
|
38
|
+
* @param record.at
|
|
39
|
+
* @returns An {@link OutageSuspect} the instant both thresholds are met,
|
|
40
|
+
* or `null` otherwise. On a trigger, the window is cleared so the same
|
|
41
|
+
* batch of errors cannot fire a second time on the next call.
|
|
42
|
+
*/
|
|
43
|
+
record({ kind, host, at }: NetworkErrorRecord): OutageSuspect | null;
|
|
44
|
+
/**
|
|
45
|
+
* Clear the sliding window. Called at the start of a fresh crawl session
|
|
46
|
+
* (`Crawler.#runDeal`) so error observations from a prior session on the
|
|
47
|
+
* same `Crawler` instance never carry over — mirrors `#successfulHosts
|
|
48
|
+
* .clear()` / `#scrapedDestinations.clear()`'s per-session reset.
|
|
49
|
+
*/
|
|
50
|
+
reset(): void;
|
|
51
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { NETWORK_RELATED_ERROR_KINDS } from '../network-related-error-kinds.js';
|
|
2
|
+
/**
|
|
3
|
+
* Sliding-window detector for "this looks like the crawl operator's own
|
|
4
|
+
* network, not the target sites, is having a bad time" — the trigger that
|
|
5
|
+
* hands off to an active probe (see `probe-network.ts`) before the crawler
|
|
6
|
+
* commits to pausing.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately dependency-free: no timers, no DB, no `Crawler` instance.
|
|
9
|
+
* Time flows in via {@link NetworkErrorRecord.at} only, which is what makes
|
|
10
|
+
* window-boundary behaviour testable with exact values instead of
|
|
11
|
+
* `vi.useFakeTimers()`.
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const detector = new NetworkOutageDetector({
|
|
15
|
+
* windowMs: 10_000,
|
|
16
|
+
* errorThreshold: 5,
|
|
17
|
+
* hostThreshold: 2,
|
|
18
|
+
* });
|
|
19
|
+
* const suspect = detector.record({ kind: 'dns', host: 'a.example', at: Date.now() });
|
|
20
|
+
* if (suspect) {
|
|
21
|
+
* // probe before closing the gate
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export default class NetworkOutageDetector {
|
|
26
|
+
#entries = [];
|
|
27
|
+
#errorThreshold;
|
|
28
|
+
#hostThreshold;
|
|
29
|
+
#windowMs;
|
|
30
|
+
constructor(options) {
|
|
31
|
+
this.#windowMs = options.windowMs;
|
|
32
|
+
this.#errorThreshold = options.errorThreshold;
|
|
33
|
+
this.#hostThreshold = options.hostThreshold;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Record one observed error and check whether it tips the sliding window
|
|
37
|
+
* over both thresholds.
|
|
38
|
+
*
|
|
39
|
+
* Non-network `kind`s (see `NETWORK_RELATED_ERROR_KINDS`) are silently
|
|
40
|
+
* ignored: they never enter the window and can never contribute to a
|
|
41
|
+
* trigger, regardless of how many arrive.
|
|
42
|
+
* @param record - The observed error.
|
|
43
|
+
* @param record.kind
|
|
44
|
+
* @param record.host
|
|
45
|
+
* @param record.at
|
|
46
|
+
* @returns An {@link OutageSuspect} the instant both thresholds are met,
|
|
47
|
+
* or `null` otherwise. On a trigger, the window is cleared so the same
|
|
48
|
+
* batch of errors cannot fire a second time on the next call.
|
|
49
|
+
*/
|
|
50
|
+
record({ kind, host, at }) {
|
|
51
|
+
if (!NETWORK_RELATED_ERROR_KINDS.has(kind)) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const cutoff = at - this.#windowMs;
|
|
55
|
+
this.#entries = this.#entries.filter((entry) => entry.at >= cutoff);
|
|
56
|
+
this.#entries.push({ host, at });
|
|
57
|
+
const distinctHosts = new Set(this.#entries.map((entry) => entry.host)).size;
|
|
58
|
+
if (this.#entries.length < this.#errorThreshold ||
|
|
59
|
+
distinctHosts < this.#hostThreshold) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const startedAt = Math.min(...this.#entries.map((entry) => entry.at));
|
|
63
|
+
const suspect = {
|
|
64
|
+
startedAt,
|
|
65
|
+
detectedAt: at,
|
|
66
|
+
triggerErrorCount: this.#entries.length,
|
|
67
|
+
triggerHostCount: distinctHosts,
|
|
68
|
+
};
|
|
69
|
+
this.#entries = [];
|
|
70
|
+
return suspect;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Clear the sliding window. Called at the start of a fresh crawl session
|
|
74
|
+
* (`Crawler.#runDeal`) so error observations from a prior session on the
|
|
75
|
+
* same `Crawler` instance never carry over — mirrors `#successfulHosts
|
|
76
|
+
* .clear()` / `#scrapedDestinations.clear()`'s per-session reset.
|
|
77
|
+
*/
|
|
78
|
+
reset() {
|
|
79
|
+
this.#entries = [];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-scoped counter for how many operator-network outages were
|
|
3
|
+
* confirmed, and their total duration. The orchestrator accumulates into
|
|
4
|
+
* this at every `networkOutageRecovered` event, reads it at crawl shutdown
|
|
5
|
+
* to emit a `[network] N outage(s), Ms total` summary line, then zeroes it
|
|
6
|
+
* for the next session (mirrors `dnsBurnedHostShortCircuitCounter`'s
|
|
7
|
+
* lifecycle, including its known limitation: two `CrawlerOrchestrator`
|
|
8
|
+
* crawls running concurrently in the same process would accumulate into and
|
|
9
|
+
* zero the same shared counter, cross-contaminating each other's summary.
|
|
10
|
+
* The CLI only ever runs one crawl per process, so this has never been
|
|
11
|
+
* observed in practice; fixing it would mean threading per-session state
|
|
12
|
+
* through both counters together, out of scope here).
|
|
13
|
+
*
|
|
14
|
+
* Exposed as a mutable object (not a plain `let`) so the counter remains a
|
|
15
|
+
* single shared reference across crawler / orchestrator imports — `let`
|
|
16
|
+
* bindings cannot be mutated from another module.
|
|
17
|
+
*/
|
|
18
|
+
export declare const networkOutageSummaryCounter: {
|
|
19
|
+
confirmedCount: number;
|
|
20
|
+
totalDurationMs: number;
|
|
21
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-scoped counter for how many operator-network outages were
|
|
3
|
+
* confirmed, and their total duration. The orchestrator accumulates into
|
|
4
|
+
* this at every `networkOutageRecovered` event, reads it at crawl shutdown
|
|
5
|
+
* to emit a `[network] N outage(s), Ms total` summary line, then zeroes it
|
|
6
|
+
* for the next session (mirrors `dnsBurnedHostShortCircuitCounter`'s
|
|
7
|
+
* lifecycle, including its known limitation: two `CrawlerOrchestrator`
|
|
8
|
+
* crawls running concurrently in the same process would accumulate into and
|
|
9
|
+
* zero the same shared counter, cross-contaminating each other's summary.
|
|
10
|
+
* The CLI only ever runs one crawl per process, so this has never been
|
|
11
|
+
* observed in practice; fixing it would mean threading per-session state
|
|
12
|
+
* through both counters together, out of scope here).
|
|
13
|
+
*
|
|
14
|
+
* Exposed as a mutable object (not a plain `let`) so the counter remains a
|
|
15
|
+
* single shared reference across crawler / orchestrator imports — `let`
|
|
16
|
+
* bindings cannot be mutated from another module.
|
|
17
|
+
*/
|
|
18
|
+
export const networkOutageSummaryCounter = { confirmedCount: 0, totalDurationMs: 0 };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Function shape for an injectable network-reachability probe: given a
|
|
3
|
+
* hostname, resolve to `true` if the network path to it appears healthy,
|
|
4
|
+
* `false` otherwise. Never rejects — a probe failure is a normal outcome
|
|
5
|
+
* (that's the whole point of probing), not an exceptional one.
|
|
6
|
+
*
|
|
7
|
+
* Injected via `CrawlerOptions.networkProbe` so tests can simulate
|
|
8
|
+
* confirmed outages and recoveries deterministically (a function that fails
|
|
9
|
+
* N times then starts succeeding) without touching the real network — the
|
|
10
|
+
* same injection pattern as `ResourceLookup` / `PageSourceLookup`.
|
|
11
|
+
*/
|
|
12
|
+
export type NetworkProbe = (host: string) => Promise<boolean>;
|
|
13
|
+
/**
|
|
14
|
+
* Default {@link NetworkProbe}: an active `dns.lookup` against `host`.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately does not hit a hardcoded external address (e.g. `1.1.1.1`) —
|
|
17
|
+
* probing a host that has already answered successfully during this crawl
|
|
18
|
+
* session (see `choose-probe-host.ts`) avoids depending on infrastructure
|
|
19
|
+
* outside the sites actually being crawled. `dns.lookup` alone is enough:
|
|
20
|
+
* it exercises the operator's local resolver / network path, which is
|
|
21
|
+
* exactly the layer an operator-side outage breaks.
|
|
22
|
+
* @param host - Hostname to resolve.
|
|
23
|
+
* @returns `true` if the lookup succeeds, `false` on any error (including
|
|
24
|
+
* NXDOMAIN, timeout, or resolver unavailability).
|
|
25
|
+
*/
|
|
26
|
+
export declare function probeNetwork(host: string): Promise<boolean>;
|