@nitpicker/crawler 0.13.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-accessor.d.ts +56 -0
- package/lib/archive/archive-accessor.js +72 -0
- package/lib/archive/archive.d.ts +108 -9
- package/lib/archive/archive.js +109 -7
- 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 +18 -0
- package/lib/archive/create-adjunct-tables.js +277 -0
- package/lib/archive/create-entity-tables.d.ts +14 -0
- package/lib/archive/create-entity-tables.js +52 -1
- package/lib/archive/database.d.ts +91 -2
- package/lib/archive/database.js +128 -0
- 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 +29 -0
- package/lib/archive/db-ops/analysis/replace-page-templates.js +84 -0
- 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/config/info-column-allowlist.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 +25 -10
- package/lib/archive/db-ops/lifecycle/init.js +39 -10
- package/lib/archive/db-ops/meta/get-audios-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-audios-of-page.js +15 -0
- package/lib/archive/db-ops/meta/get-buttons-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-buttons-of-page.js +15 -0
- package/lib/archive/db-ops/meta/get-canvases-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-canvases-of-page.js +15 -0
- package/lib/archive/db-ops/meta/get-headings-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-headings-of-page.js +15 -0
- package/lib/archive/db-ops/meta/get-iframes-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-iframes-of-page.js +15 -0
- package/lib/archive/db-ops/meta/get-main-content-images-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-main-content-images-of-page.js +15 -0
- package/lib/archive/db-ops/meta/get-main-content-tables-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-main-content-tables-of-page.js +15 -0
- package/lib/archive/db-ops/meta/get-videos-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-videos-of-page.js +15 -0
- 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/read/build-page-query.js +1 -1
- package/lib/archive/db-ops/pages/reset/repromote-external-pages.js +19 -10
- package/lib/archive/db-ops/pages/reset/reset-failed-pages.d.ts +18 -3
- package/lib/archive/db-ops/pages/reset/reset-failed-pages.js +48 -10
- package/lib/archive/db-ops/pages/write/insert-audios.d.ts +14 -0
- package/lib/archive/db-ops/pages/write/insert-audios.js +25 -0
- package/lib/archive/db-ops/pages/write/insert-buttons.d.ts +14 -0
- package/lib/archive/db-ops/pages/write/insert-buttons.js +29 -0
- package/lib/archive/db-ops/pages/write/insert-canvases.d.ts +14 -0
- package/lib/archive/db-ops/pages/write/insert-canvases.js +26 -0
- package/lib/archive/db-ops/pages/write/insert-headings.d.ts +15 -0
- package/lib/archive/db-ops/pages/write/insert-headings.js +27 -0
- package/lib/archive/db-ops/pages/write/insert-iframes.d.ts +14 -0
- package/lib/archive/db-ops/pages/write/insert-iframes.js +28 -0
- package/lib/archive/db-ops/pages/write/insert-main-content-images.d.ts +20 -0
- package/lib/archive/db-ops/pages/write/insert-main-content-images.js +32 -0
- package/lib/archive/db-ops/pages/write/insert-main-content-tables.d.ts +14 -0
- package/lib/archive/db-ops/pages/write/insert-main-content-tables.js +29 -0
- package/lib/archive/db-ops/pages/write/insert-page.js +15 -2
- package/lib/archive/db-ops/pages/write/insert-videos.d.ts +14 -0
- package/lib/archive/db-ops/pages/write/insert-videos.js +28 -0
- package/lib/archive/db-ops/pages/write/update-page.js +33 -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/init-schema.js +2 -1
- package/lib/archive/meta/compute-main-contents-denormalized.d.ts +22 -0
- package/lib/archive/meta/compute-main-contents-denormalized.js +63 -0
- package/lib/archive/meta/types.d.ts +263 -0
- 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-info-main-content-selector.d.ts +11 -0
- package/lib/archive/migrate-info-main-content-selector.js +24 -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-main-contents-columns.d.ts +21 -0
- package/lib/archive/migrate-main-contents-columns.js +50 -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 +145 -3
- package/lib/archive/page.js +195 -0
- package/lib/archive/types.d.ts +86 -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 +261 -2
- 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 +2 -0
- package/lib/crawler/link-to-page-data.js +2 -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/resource-to-page-data.js +2 -0
- package/lib/crawler/types.d.ts +152 -1
- package/lib/crawler-orchestrator.d.ts +62 -18
- package/lib/crawler-orchestrator.js +163 -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
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
|
|
@@ -149,10 +196,21 @@ class Crawler extends EventEmitter {
|
|
|
149
196
|
verbose: options?.verbose ?? false,
|
|
150
197
|
userAgent: options?.userAgent || `Nitpicker/${pkg.version}`,
|
|
151
198
|
ignoreRobots: options?.ignoreRobots ?? false,
|
|
199
|
+
mainContentSelector: options?.mainContentSelector ?? null,
|
|
152
200
|
lookupResource: options?.lookupResource ?? null,
|
|
153
201
|
lookupPageSource: options?.lookupPageSource ?? null,
|
|
154
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,
|
|
155
208
|
};
|
|
209
|
+
this.#networkOutageDetector = new NetworkOutageDetector({
|
|
210
|
+
windowMs: this.#options.networkOutageWindowMs,
|
|
211
|
+
errorThreshold: this.#options.networkOutageErrorThreshold,
|
|
212
|
+
hostThreshold: this.#options.networkOutageHostThreshold,
|
|
213
|
+
});
|
|
156
214
|
this.#robotsChecker = new RobotsChecker(this.#options.userAgent, !this.#options.ignoreRobots);
|
|
157
215
|
for (const urlStr of this.#options.roots) {
|
|
158
216
|
const url = parseUrl(urlStr, this.#options);
|
|
@@ -323,6 +381,84 @@ class Crawler extends EventEmitter {
|
|
|
323
381
|
});
|
|
324
382
|
}
|
|
325
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
|
+
}
|
|
326
462
|
/**
|
|
327
463
|
* Processes captured sub-resources from a page scrape, deduplicates them,
|
|
328
464
|
* and emits `response` / `responseReferrers` events for new resources.
|
|
@@ -470,6 +606,54 @@ class Crawler extends EventEmitter {
|
|
|
470
606
|
}
|
|
471
607
|
}
|
|
472
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
|
+
}
|
|
473
657
|
/**
|
|
474
658
|
* Resolve the source label of the page being scraped so sub-resources
|
|
475
659
|
* captured during its render can inherit the correct lineage label
|
|
@@ -540,6 +724,12 @@ class Crawler extends EventEmitter {
|
|
|
540
724
|
// does not inherit "host alive" claims from a prior run that may have
|
|
541
725
|
// happened on an entirely different network.
|
|
542
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();
|
|
543
733
|
// external URL の追跡(target は deal の total/done から導出)
|
|
544
734
|
const externalUrls = new Set();
|
|
545
735
|
const externalDoneUrls = new Set();
|
|
@@ -587,6 +777,12 @@ class Crawler extends EventEmitter {
|
|
|
587
777
|
return Promise.all(ops).then(() => { });
|
|
588
778
|
};
|
|
589
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();
|
|
590
786
|
// Interval delay is handled here instead of by dealer because
|
|
591
787
|
// DNS-burned hosts must skip the wait entirely. Spending the
|
|
592
788
|
// per-URL interval on a host the cache already knows is dead
|
|
@@ -741,6 +937,7 @@ class Crawler extends EventEmitter {
|
|
|
741
937
|
this.#handleResult(result, url, enqueue, paginationState, concurrency);
|
|
742
938
|
const parentSource = await this.#resolveParentSource(url);
|
|
743
939
|
this.#handleResources(result.resources, parentSource);
|
|
940
|
+
this.#handleConsoleLogs(result.consoleLogs, url, result.pageData?.redirectPaths ?? []);
|
|
744
941
|
log(formatResultSummary(result));
|
|
745
942
|
// Phase errors must be emitted AFTER 'page' / 'externalPage'
|
|
746
943
|
// so the orchestrator's WriteQueue sees `setPage` before
|
|
@@ -814,6 +1011,52 @@ class Crawler extends EventEmitter {
|
|
|
814
1011
|
crawlerLog('Crawl End');
|
|
815
1012
|
void this.emit('crawlEnd', {});
|
|
816
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
|
+
}
|
|
817
1060
|
/**
|
|
818
1061
|
* Orchestrates the full scrape pipeline for a single URL.
|
|
819
1062
|
*
|
|
@@ -874,6 +1117,7 @@ class Crawler extends EventEmitter {
|
|
|
874
1117
|
type: 'success',
|
|
875
1118
|
pageData: metadataOnly ? { ...pageData, isTarget: false } : pageData,
|
|
876
1119
|
resources: [],
|
|
1120
|
+
consoleLogs: [],
|
|
877
1121
|
};
|
|
878
1122
|
}
|
|
879
1123
|
}
|
|
@@ -1001,6 +1245,7 @@ class Crawler extends EventEmitter {
|
|
|
1001
1245
|
return {
|
|
1002
1246
|
type: 'error',
|
|
1003
1247
|
resources: [],
|
|
1248
|
+
consoleLogs: [],
|
|
1004
1249
|
error: {
|
|
1005
1250
|
name: error instanceof Error ? error.name : 'Error',
|
|
1006
1251
|
message: errorMessage,
|
|
@@ -1014,6 +1259,7 @@ class Crawler extends EventEmitter {
|
|
|
1014
1259
|
return {
|
|
1015
1260
|
type: 'error',
|
|
1016
1261
|
resources: [],
|
|
1262
|
+
consoleLogs: [],
|
|
1017
1263
|
error: {
|
|
1018
1264
|
name: error instanceof Error ? error.name : 'Error',
|
|
1019
1265
|
message: errorMessage,
|
|
@@ -1061,6 +1307,7 @@ class Crawler extends EventEmitter {
|
|
|
1061
1307
|
type: 'success',
|
|
1062
1308
|
pageData: { ...titleResult, isTarget: false },
|
|
1063
1309
|
resources: [],
|
|
1310
|
+
consoleLogs: [],
|
|
1064
1311
|
};
|
|
1065
1312
|
}
|
|
1066
1313
|
catch (error) {
|
|
@@ -1071,6 +1318,7 @@ class Crawler extends EventEmitter {
|
|
|
1071
1318
|
type: 'success',
|
|
1072
1319
|
pageData: { ...headCheckResult, isTarget: false },
|
|
1073
1320
|
resources: [],
|
|
1321
|
+
consoleLogs: [],
|
|
1074
1322
|
};
|
|
1075
1323
|
}
|
|
1076
1324
|
// Non-HTML content — skip browser
|
|
@@ -1080,6 +1328,7 @@ class Crawler extends EventEmitter {
|
|
|
1080
1328
|
type: 'success',
|
|
1081
1329
|
pageData: headCheckResult,
|
|
1082
1330
|
resources: [],
|
|
1331
|
+
consoleLogs: [],
|
|
1083
1332
|
};
|
|
1084
1333
|
}
|
|
1085
1334
|
// HTML or unknown content type — launch browser with preflight result.
|
|
@@ -1265,9 +1514,11 @@ class Crawler extends EventEmitter {
|
|
|
1265
1514
|
retries: this.#options.retry,
|
|
1266
1515
|
label: 'HEAD request',
|
|
1267
1516
|
onWait: (determinedInterval, retryCount, label, error) => {
|
|
1517
|
+
this.#recordNetworkError(error.message, host);
|
|
1268
1518
|
update(`${label}: ${error.message} — %countdown(${determinedInterval},fetchHead_${laneIndex}_${retryCount},s)%s (retry #${retryCount + 1})`);
|
|
1269
1519
|
},
|
|
1270
1520
|
onGiveUp: (retryCount, error, label) => {
|
|
1521
|
+
this.#recordNetworkError(error.message, host);
|
|
1271
1522
|
// Burn the host so subsequent URLs short-circuit — but ONLY
|
|
1272
1523
|
// when this is the first time we've ever seen the host fail
|
|
1273
1524
|
// in this session. A host that responded earlier is treated
|
|
@@ -1283,6 +1534,12 @@ class Crawler extends EventEmitter {
|
|
|
1283
1534
|
successfulHosts: this.#successfulHosts,
|
|
1284
1535
|
})) {
|
|
1285
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());
|
|
1286
1543
|
}
|
|
1287
1544
|
update(c.red(`${label}: gave up after ${retryCount} retries — ${error.message}`));
|
|
1288
1545
|
},
|
|
@@ -1355,8 +1612,8 @@ class Crawler extends EventEmitter {
|
|
|
1355
1612
|
// sub-resource requests issued from the same page —
|
|
1356
1613
|
// including cross-origin requests to a different hostname
|
|
1357
1614
|
// sharing the same IP / port (e.g. an embedded
|
|
1358
|
-
// `<img src="http://127.0.0.1:
|
|
1359
|
-
// `localhost:
|
|
1615
|
+
// `<img src="http://127.0.0.1:PORT/…">` loaded from a
|
|
1616
|
+
// `localhost:PORT` page) — get the cached `Authorization`
|
|
1360
1617
|
// header re-attached by the network stack. The
|
|
1361
1618
|
// `Fetch.authRequired` event never fires for these
|
|
1362
1619
|
// pre-emptive attachments, so neither `page.authenticate`
|
|
@@ -1399,6 +1656,7 @@ class Crawler extends EventEmitter {
|
|
|
1399
1656
|
metadataOnly,
|
|
1400
1657
|
retries: this.#options.retry,
|
|
1401
1658
|
headCheckResult,
|
|
1659
|
+
mainContentSelector: this.#options.mainContentSelector,
|
|
1402
1660
|
});
|
|
1403
1661
|
// Image dom-path capture runs here — after the scrape completed but
|
|
1404
1662
|
// while `page` is still alive — because beholder's image metadata
|
|
@@ -1477,6 +1735,7 @@ class Crawler extends EventEmitter {
|
|
|
1477
1735
|
return {
|
|
1478
1736
|
type: 'error',
|
|
1479
1737
|
resources: [],
|
|
1738
|
+
consoleLogs: [],
|
|
1480
1739
|
error: {
|
|
1481
1740
|
name: error instanceof Error ? error.name : 'Error',
|
|
1482
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>;
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|