@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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Config } from './archive/types.js';
|
|
2
|
+
import type { NetworkProbe } from './crawler/probe-network.js';
|
|
2
3
|
import type { InventoryMode } from './crawler/types.js';
|
|
3
4
|
import type { CrawlEvent } from './types.js';
|
|
4
5
|
import type { ExURL } from '@d-zero/shared/parse-url';
|
|
@@ -48,6 +49,26 @@ interface CrawlConfig extends Config {
|
|
|
48
49
|
* this `null` so new rows are labelled `'crawled'` by the DB DEFAULT.
|
|
49
50
|
*/
|
|
50
51
|
inventoryMode: InventoryMode | null;
|
|
52
|
+
/**
|
|
53
|
+
* See {@link CrawlerOptions.networkOutageWindowMs}. Omitted (`undefined`
|
|
54
|
+
* on the `Partial<CrawlConfig>` callers actually pass) falls through to
|
|
55
|
+
* `Crawler`'s own default — this field exists so tests can shrink the
|
|
56
|
+
* window for a fast, deterministic outage-detection cycle.
|
|
57
|
+
*/
|
|
58
|
+
networkOutageWindowMs: number;
|
|
59
|
+
/** See {@link CrawlerOptions.networkOutageErrorThreshold}. */
|
|
60
|
+
networkOutageErrorThreshold: number;
|
|
61
|
+
/** See {@link CrawlerOptions.networkOutageHostThreshold}. */
|
|
62
|
+
networkOutageHostThreshold: number;
|
|
63
|
+
/** See {@link CrawlerOptions.networkOutageProbeIntervalMs}. */
|
|
64
|
+
networkOutageProbeIntervalMs: number;
|
|
65
|
+
/**
|
|
66
|
+
* See {@link CrawlerOptions.networkProbe}. The seam tests use to simulate
|
|
67
|
+
* confirmed outages and recoveries deterministically without touching
|
|
68
|
+
* the real network — plumbed through from `CrawlerOrchestrator.crawling`'s
|
|
69
|
+
* `options` so an E2E test can inject it via the public API.
|
|
70
|
+
*/
|
|
71
|
+
networkProbe: NetworkProbe | null;
|
|
51
72
|
}
|
|
52
73
|
/**
|
|
53
74
|
* Callback invoked after the CrawlerOrchestrator instance is fully initialized
|
|
@@ -56,6 +77,20 @@ interface CrawlConfig extends Config {
|
|
|
56
77
|
* @param config - The resolved archive configuration.
|
|
57
78
|
*/
|
|
58
79
|
type CrawlInitializedCallback = (orchestrator: CrawlerOrchestrator, config: Config) => void | Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* The CLI's already-read `--inventory` source list, passed to
|
|
82
|
+
* {@link CrawlerOrchestrator.inventory} instead of a file path — see that
|
|
83
|
+
* method's `source` param for why the path itself never crosses this
|
|
84
|
+
* boundary.
|
|
85
|
+
*/
|
|
86
|
+
interface InventorySource {
|
|
87
|
+
/** Lower-case hex SHA-256 digest of `bytes` (`computeFileSha256(bytes)`). */
|
|
88
|
+
sha256: string;
|
|
89
|
+
/** The exact bytes of the source list file, archived verbatim. */
|
|
90
|
+
bytes: Buffer;
|
|
91
|
+
/** Number of source-file lines the CLI warned-and-dropped for failing URL validation, before `inventoryUrls` was ever built. Recorded on the audit row as `inventory_runs.invalid_skipped`. */
|
|
92
|
+
invalidLineCount: number;
|
|
93
|
+
}
|
|
59
94
|
/**
|
|
60
95
|
* The main entry point for Nitpicker web crawling and archiving.
|
|
61
96
|
*
|
|
@@ -184,22 +219,29 @@ export declare class CrawlerOrchestrator extends EventEmitter<CrawlEvent> {
|
|
|
184
219
|
* 3. Reject archives with unfinished `pending` URLs — those would inherit
|
|
185
220
|
* the inventory `source` label by mistake. Operator must resume /
|
|
186
221
|
* retry-failed first.
|
|
187
|
-
* 4.
|
|
188
|
-
*
|
|
189
|
-
*
|
|
222
|
+
* 4. If `source` is given, archive its exact bytes under
|
|
223
|
+
* `inventory/<sha256>.txt` (see {@link Archive.saveInventorySourceList}).
|
|
224
|
+
* Done before scope classification so even a run that discards every
|
|
225
|
+
* URL (out of scope or already known) still leaves a recoverable
|
|
226
|
+
* copy of what was fed in.
|
|
227
|
+
* 5. Parse the URL list — the CLI has already warned-and-dropped
|
|
228
|
+
* unparseable-URL lines before calling this method, so every
|
|
229
|
+
* remaining entry parses. Anything outside the archived scope is
|
|
230
|
+
* warned and skipped (inventory is per-server by design).
|
|
231
|
+
* 6. Subtract URLs that already exist in `pages` or `resources` so the
|
|
190
232
|
* second (and N-th) inventory pass is a no-op for known rows — keeps
|
|
191
233
|
* `'inventory-seed'` rows from being silently demoted.
|
|
192
|
-
*
|
|
234
|
+
* 7. Make `<archive>.bak`. Anything thrown beyond this point restores
|
|
193
235
|
* from the backup.
|
|
194
|
-
*
|
|
236
|
+
* 8. HEAD-probe each novel URL. Responses classified as HTML are queued
|
|
195
237
|
* as Crawler seeds (`'inventory-seed'`); everything else is recorded
|
|
196
238
|
* in `resources` directly as `'inventory-seed'` (no browser launch).
|
|
197
|
-
*
|
|
239
|
+
* 9. If any HTML seeds exist, start a Crawler with
|
|
198
240
|
* `inventoryMode = { seedUrls }` so the rendered page and every newly
|
|
199
241
|
* discovered downstream link is labelled correctly. `resume` is fed
|
|
200
242
|
* the existing `scraped` / `resources` sets so links into already-
|
|
201
243
|
* crawled pages stop at the seen-gate without re-rendering.
|
|
202
|
-
*
|
|
244
|
+
* 10. Drop the backup on success; restore it on any throw.
|
|
203
245
|
*
|
|
204
246
|
* Mutually exclusive with `--append` / `--retry-failed` / `--resume` /
|
|
205
247
|
* `--diff` / `--list` / `--list-file` / `--single` / `--output` — the
|
|
@@ -209,20 +251,22 @@ export declare class CrawlerOrchestrator extends EventEmitter<CrawlEvent> {
|
|
|
209
251
|
* @param inventoryUrls - Pre-read URL list (one URL per element).
|
|
210
252
|
* @param options - Optional config overrides — most callers leave this blank and let the archived config flow through.
|
|
211
253
|
* @param initializedCallback - Hook invoked once the orchestrator is constructed but before `crawling` runs (the CLI uses it to attach progress reporting).
|
|
212
|
-
* @param
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
* `
|
|
254
|
+
* @param source - The CLI's already-read source list, as `{ sha256, bytes }`.
|
|
255
|
+
* The orchestrator deliberately does NOT receive the file path: the
|
|
256
|
+
* path is privacy-sensitive (leaks user-home / OS structure when
|
|
257
|
+
* archives are shared) and we want it lifted off this boundary so no
|
|
258
|
+
* future log line / breadcrumb / error message inside the orchestrator
|
|
259
|
+
* can accidentally re-leak it. `bytes` is archived verbatim under
|
|
260
|
+
* `inventory/<sha256>.txt` (see {@link Archive.saveInventorySourceList})
|
|
261
|
+
* before scope classification, so a later `--inventory` run against
|
|
262
|
+
* the same list is an audit no-op even when it discards zero new
|
|
263
|
+
* URLs. Pass `null` for programmatic callers that built
|
|
264
|
+
* `inventoryUrls` in-memory; the audit row's `source_file_sha256`
|
|
265
|
+
* column will be `NULL` and no source list is archived.
|
|
222
266
|
* @returns The orchestrator instance after a successful inventory pass.
|
|
223
267
|
* @throws {Error} When `inventoryUrls` is empty, the archive is in list mode, or pending URLs from a previous crawl remain unresolved.
|
|
224
268
|
*/
|
|
225
|
-
static inventory(archivePath: string, inventoryUrls: string[], options?: Partial<CrawlConfig>, initializedCallback?: CrawlInitializedCallback,
|
|
269
|
+
static inventory(archivePath: string, inventoryUrls: string[], options?: Partial<CrawlConfig>, initializedCallback?: CrawlInitializedCallback, source?: InventorySource | null): Promise<CrawlerOrchestrator>;
|
|
226
270
|
/**
|
|
227
271
|
* Re-fetch previously-failed pages in an existing `.nitpicker` archive.
|
|
228
272
|
*
|
|
@@ -13,6 +13,7 @@ import { dnsBurnedHostCache } from './crawler/dns-burned-host-cache.js';
|
|
|
13
13
|
import { dnsBurnedHostShortCircuitCounter } from './crawler/dns-burned-host-short-circuit-counter.js';
|
|
14
14
|
import { findScopeEntry } from './crawler/find-scope-entry.js';
|
|
15
15
|
import { isLikelyHtmlUrl } from './crawler/is-likely-html-url.js';
|
|
16
|
+
import { networkOutageSummaryCounter } from './crawler/network-outage-summary-counter.js';
|
|
16
17
|
import { PreloadShortCircuitError } from './crawler/preload-short-circuit-error.js';
|
|
17
18
|
import { protocolAgnosticKey } from './crawler/protocol-agnostic-key.js';
|
|
18
19
|
import { crawlerLog, log } from './debug.js';
|
|
@@ -65,6 +66,17 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
65
66
|
#crawler;
|
|
66
67
|
/** Whether the crawl was started from a pre-defined URL list (non-recursive mode). */
|
|
67
68
|
#fromList;
|
|
69
|
+
/**
|
|
70
|
+
* The `network_outages` row id for the currently-open outage, or `null`
|
|
71
|
+
* when none is open. Set by the `networkOutageConfirmed` handler (once
|
|
72
|
+
* the INSERT resolves) and consumed by `networkOutageRecovered` — the
|
|
73
|
+
* `Crawler` class never touches the archive itself and has no way to
|
|
74
|
+
* know the row's id, so the orchestrator is the only place that can
|
|
75
|
+
* bridge the two events for the same outage.
|
|
76
|
+
*/
|
|
77
|
+
#openNetworkOutageId = null;
|
|
78
|
+
/** `startedAt` of the currently-open outage, tracked alongside {@link #openNetworkOutageId} so `networkOutageRecovered` can compute a duration for {@link networkOutageSummaryCounter}. */
|
|
79
|
+
#openNetworkOutageStartedAt = null;
|
|
68
80
|
/** Serializes archive writes from crawler event handlers (FIFO). */
|
|
69
81
|
#writeQueue = new WriteQueue();
|
|
70
82
|
/**
|
|
@@ -138,6 +150,14 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
138
150
|
// rows continue to land in pages/resources with the DB DEFAULT
|
|
139
151
|
// `'crawled'` provenance label.
|
|
140
152
|
inventoryMode: options?.inventoryMode ?? null,
|
|
153
|
+
// Forwarded as-is (including `undefined`) — `Crawler`'s own
|
|
154
|
+
// constructor merges each against its `DEFAULT_NETWORK_OUTAGE_*`
|
|
155
|
+
// constant, so omitting them here is exactly "use the default".
|
|
156
|
+
networkOutageWindowMs: options?.networkOutageWindowMs,
|
|
157
|
+
networkOutageErrorThreshold: options?.networkOutageErrorThreshold,
|
|
158
|
+
networkOutageHostThreshold: options?.networkOutageHostThreshold,
|
|
159
|
+
networkOutageProbeIntervalMs: options?.networkOutageProbeIntervalMs,
|
|
160
|
+
networkProbe: options?.networkProbe ?? null,
|
|
141
161
|
});
|
|
142
162
|
}
|
|
143
163
|
/**
|
|
@@ -166,6 +186,11 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
166
186
|
*/
|
|
167
187
|
async crawling(list, opts) {
|
|
168
188
|
const writeQueue = this.#writeQueue;
|
|
189
|
+
// Per-session state, like `Crawler`'s own `#successfulHosts.clear()` /
|
|
190
|
+
// `#networkGate.open()` reset at the start of `#runDeal` — a fresh
|
|
191
|
+
// session must not inherit a dangling outage id from a prior one.
|
|
192
|
+
this.#openNetworkOutageId = null;
|
|
193
|
+
this.#openNetworkOutageStartedAt = null;
|
|
169
194
|
return new Promise((resolve, reject) => {
|
|
170
195
|
this.#crawler.on('error', (error) => {
|
|
171
196
|
if (error.error instanceof PreloadShortCircuitError) {
|
|
@@ -212,6 +237,67 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
212
237
|
.catch((error) => reject(error));
|
|
213
238
|
void this.emit('redirect', { result });
|
|
214
239
|
});
|
|
240
|
+
this.#crawler.on('networkOutageConfirmed', ({ startedAt, detectedAt, probeHost, triggerErrorCount, triggerHostCount }) => {
|
|
241
|
+
crawlerLog('Network outage confirmed: probeHost=%s triggerErrorCount=%d triggerHostCount=%d', probeHost, triggerErrorCount, triggerHostCount);
|
|
242
|
+
// event notice; mirrors `#finalizeCrawlSession`'s unconditional
|
|
243
|
+
// `console.error` for the DNS-burn short-circuit summary.
|
|
244
|
+
console.error(`[network] outage suspected — pausing workers (probe host: ${probeHost ?? 'none'})`);
|
|
245
|
+
writeQueue
|
|
246
|
+
.enqueue(async () => {
|
|
247
|
+
// Both fields are set together, inside this single
|
|
248
|
+
// closure, so the pair can never fall out of sync
|
|
249
|
+
// (e.g. one set synchronously above while the other
|
|
250
|
+
// waits on the INSERT) — `networkOutageRecovered`'s
|
|
251
|
+
// queued closure always sees either both set or
|
|
252
|
+
// neither.
|
|
253
|
+
const id = await this.#archive.insertNetworkOutage({
|
|
254
|
+
startedAt,
|
|
255
|
+
detectedAt,
|
|
256
|
+
probeHost,
|
|
257
|
+
triggerErrorCount,
|
|
258
|
+
triggerHostCount,
|
|
259
|
+
});
|
|
260
|
+
this.#openNetworkOutageId = id;
|
|
261
|
+
this.#openNetworkOutageStartedAt = startedAt;
|
|
262
|
+
})
|
|
263
|
+
.catch((error) => reject(error));
|
|
264
|
+
});
|
|
265
|
+
this.#crawler.on('networkOutageRecovered', ({ endedAt }) => {
|
|
266
|
+
// The `id` read is deferred to INSIDE the queued closure, not
|
|
267
|
+
// read synchronously here, because `networkOutageConfirmed`'s
|
|
268
|
+
// INSERT is itself only queued (not awaited) when that event
|
|
269
|
+
// fires — `#openNetworkOutageId` is not guaranteed to be set
|
|
270
|
+
// yet at the instant `networkOutageRecovered` fires (the two
|
|
271
|
+
// events can arrive in quick succession, e.g. in tests that
|
|
272
|
+
// drive them back-to-back with no real probe-interval delay
|
|
273
|
+
// between them). `WriteQueue` runs enqueued operations in
|
|
274
|
+
// submission order, so by the time THIS closure actually
|
|
275
|
+
// executes, the confirm's INSERT closure (enqueued first) has
|
|
276
|
+
// already completed and `#openNetworkOutageId` is reliably set.
|
|
277
|
+
writeQueue
|
|
278
|
+
.enqueue(() => {
|
|
279
|
+
const id = this.#openNetworkOutageId;
|
|
280
|
+
const startedAt = this.#openNetworkOutageStartedAt;
|
|
281
|
+
if (id === null) {
|
|
282
|
+
// Defensive: `networkOutageConfirmed` always
|
|
283
|
+
// precedes `networkOutageRecovered` on the same
|
|
284
|
+
// `Crawler` instance. If this fires anyway, there
|
|
285
|
+
// is no row to close.
|
|
286
|
+
crawlerLog('Network outage recovered but no open outage id was tracked');
|
|
287
|
+
return Promise.resolve();
|
|
288
|
+
}
|
|
289
|
+
this.#openNetworkOutageId = null;
|
|
290
|
+
this.#openNetworkOutageStartedAt = null;
|
|
291
|
+
const durationMs = endedAt - (startedAt ?? endedAt);
|
|
292
|
+
networkOutageSummaryCounter.confirmedCount++;
|
|
293
|
+
networkOutageSummaryCounter.totalDurationMs += durationMs;
|
|
294
|
+
crawlerLog('Network outage recovered: id=%d endedAt=%d', id, endedAt);
|
|
295
|
+
// eslint-disable-next-line no-console -- see the confirmed handler above
|
|
296
|
+
console.error(`[network] recovered after ${Math.round(durationMs / 1000)}s`);
|
|
297
|
+
return this.#archive.closeNetworkOutage(id, endedAt);
|
|
298
|
+
})
|
|
299
|
+
.catch((error) => reject(error));
|
|
300
|
+
});
|
|
215
301
|
this.#crawler.on('response', ({ resource, source }) => {
|
|
216
302
|
writeQueue
|
|
217
303
|
.enqueue(() => this.#archive.setResources(resource, source))
|
|
@@ -222,6 +308,11 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
222
308
|
.enqueue(() => this.#archive.setResourcesReferrers(resource))
|
|
223
309
|
.catch((error) => reject(error));
|
|
224
310
|
});
|
|
311
|
+
this.#crawler.on('consoleLogs', ({ pageUrl, redirectPaths, entries }) => {
|
|
312
|
+
writeQueue
|
|
313
|
+
.enqueue(() => this.#archive.setConsoleLogs(pageUrl, redirectPaths, entries))
|
|
314
|
+
.catch((error) => reject(error));
|
|
315
|
+
});
|
|
225
316
|
this.#crawler.on('crawlEnd', () => {
|
|
226
317
|
writeQueue
|
|
227
318
|
.drain()
|
|
@@ -387,7 +478,13 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
387
478
|
const absFilePath = path.isAbsolute(archivePath)
|
|
388
479
|
? archivePath
|
|
389
480
|
: path.resolve(cwd, archivePath);
|
|
390
|
-
|
|
481
|
+
// See `ArchiveOpenOptions.openPluginData` for why this must be `true`
|
|
482
|
+
// on every writer path that calls `write()`.
|
|
483
|
+
const archive = await Archive.open({
|
|
484
|
+
filePath: absFilePath,
|
|
485
|
+
cwd,
|
|
486
|
+
openPluginData: true,
|
|
487
|
+
});
|
|
391
488
|
// Any throw between here and the successful return must release the
|
|
392
489
|
// archive lock and clean up tmpDir; the caller's `close()` only runs on
|
|
393
490
|
// the happy path. Errors from `close()` itself are intentionally
|
|
@@ -481,22 +578,29 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
481
578
|
* 3. Reject archives with unfinished `pending` URLs — those would inherit
|
|
482
579
|
* the inventory `source` label by mistake. Operator must resume /
|
|
483
580
|
* retry-failed first.
|
|
484
|
-
* 4.
|
|
485
|
-
*
|
|
486
|
-
*
|
|
581
|
+
* 4. If `source` is given, archive its exact bytes under
|
|
582
|
+
* `inventory/<sha256>.txt` (see {@link Archive.saveInventorySourceList}).
|
|
583
|
+
* Done before scope classification so even a run that discards every
|
|
584
|
+
* URL (out of scope or already known) still leaves a recoverable
|
|
585
|
+
* copy of what was fed in.
|
|
586
|
+
* 5. Parse the URL list — the CLI has already warned-and-dropped
|
|
587
|
+
* unparseable-URL lines before calling this method, so every
|
|
588
|
+
* remaining entry parses. Anything outside the archived scope is
|
|
589
|
+
* warned and skipped (inventory is per-server by design).
|
|
590
|
+
* 6. Subtract URLs that already exist in `pages` or `resources` so the
|
|
487
591
|
* second (and N-th) inventory pass is a no-op for known rows — keeps
|
|
488
592
|
* `'inventory-seed'` rows from being silently demoted.
|
|
489
|
-
*
|
|
593
|
+
* 7. Make `<archive>.bak`. Anything thrown beyond this point restores
|
|
490
594
|
* from the backup.
|
|
491
|
-
*
|
|
595
|
+
* 8. HEAD-probe each novel URL. Responses classified as HTML are queued
|
|
492
596
|
* as Crawler seeds (`'inventory-seed'`); everything else is recorded
|
|
493
597
|
* in `resources` directly as `'inventory-seed'` (no browser launch).
|
|
494
|
-
*
|
|
598
|
+
* 9. If any HTML seeds exist, start a Crawler with
|
|
495
599
|
* `inventoryMode = { seedUrls }` so the rendered page and every newly
|
|
496
600
|
* discovered downstream link is labelled correctly. `resume` is fed
|
|
497
601
|
* the existing `scraped` / `resources` sets so links into already-
|
|
498
602
|
* crawled pages stop at the seen-gate without re-rendering.
|
|
499
|
-
*
|
|
603
|
+
* 10. Drop the backup on success; restore it on any throw.
|
|
500
604
|
*
|
|
501
605
|
* Mutually exclusive with `--append` / `--retry-failed` / `--resume` /
|
|
502
606
|
* `--diff` / `--list` / `--list-file` / `--single` / `--output` — the
|
|
@@ -506,20 +610,22 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
506
610
|
* @param inventoryUrls - Pre-read URL list (one URL per element).
|
|
507
611
|
* @param options - Optional config overrides — most callers leave this blank and let the archived config flow through.
|
|
508
612
|
* @param initializedCallback - Hook invoked once the orchestrator is constructed but before `crawling` runs (the CLI uses it to attach progress reporting).
|
|
509
|
-
* @param
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
* `
|
|
613
|
+
* @param source - The CLI's already-read source list, as `{ sha256, bytes }`.
|
|
614
|
+
* The orchestrator deliberately does NOT receive the file path: the
|
|
615
|
+
* path is privacy-sensitive (leaks user-home / OS structure when
|
|
616
|
+
* archives are shared) and we want it lifted off this boundary so no
|
|
617
|
+
* future log line / breadcrumb / error message inside the orchestrator
|
|
618
|
+
* can accidentally re-leak it. `bytes` is archived verbatim under
|
|
619
|
+
* `inventory/<sha256>.txt` (see {@link Archive.saveInventorySourceList})
|
|
620
|
+
* before scope classification, so a later `--inventory` run against
|
|
621
|
+
* the same list is an audit no-op even when it discards zero new
|
|
622
|
+
* URLs. Pass `null` for programmatic callers that built
|
|
623
|
+
* `inventoryUrls` in-memory; the audit row's `source_file_sha256`
|
|
624
|
+
* column will be `NULL` and no source list is archived.
|
|
519
625
|
* @returns The orchestrator instance after a successful inventory pass.
|
|
520
626
|
* @throws {Error} When `inventoryUrls` is empty, the archive is in list mode, or pending URLs from a previous crawl remain unresolved.
|
|
521
627
|
*/
|
|
522
|
-
static async inventory(archivePath, inventoryUrls, options, initializedCallback,
|
|
628
|
+
static async inventory(archivePath, inventoryUrls, options, initializedCallback, source = null) {
|
|
523
629
|
if (inventoryUrls.length === 0) {
|
|
524
630
|
throw new Error('inventory: URL list is empty');
|
|
525
631
|
}
|
|
@@ -527,7 +633,13 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
527
633
|
const absFilePath = path.isAbsolute(archivePath)
|
|
528
634
|
? archivePath
|
|
529
635
|
: path.resolve(cwd, archivePath);
|
|
530
|
-
|
|
636
|
+
// See `ArchiveOpenOptions.openPluginData` for why this must be `true`
|
|
637
|
+
// on every writer path that calls `write()`.
|
|
638
|
+
const archive = await Archive.open({
|
|
639
|
+
filePath: absFilePath,
|
|
640
|
+
cwd,
|
|
641
|
+
openPluginData: true,
|
|
642
|
+
});
|
|
531
643
|
try {
|
|
532
644
|
const archived = await archive.getConfig();
|
|
533
645
|
if (archived.fromList) {
|
|
@@ -549,6 +661,14 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
549
661
|
// eslint-disable-next-line no-console -- operator-facing warning, must be visible regardless of DEBUG filters
|
|
550
662
|
console.warn(`inventory: archive has ${pending.length} pending URLs from a previous crawl. Proceeding — crawled-wins priority keeps their labels stable. Consider \`--resume\` first if you want the prior work finalized.`);
|
|
551
663
|
}
|
|
664
|
+
// Archive the exact source bytes before scope classification, so
|
|
665
|
+
// even a run that discards every URL (out-of-scope or already
|
|
666
|
+
// known — see `novelUrls.length === 0` below) still leaves a
|
|
667
|
+
// recoverable copy of what was fed in. Content-hash-named, so a
|
|
668
|
+
// second `--inventory` pass with the same list is a no-op write.
|
|
669
|
+
if (source) {
|
|
670
|
+
await archive.saveInventorySourceList(source.sha256, source.bytes);
|
|
671
|
+
}
|
|
552
672
|
// Parse + scope-classify the candidate URLs. sortUrl drops
|
|
553
673
|
// unparseable strings; findScopeEntry separates in-scope from
|
|
554
674
|
// out-of-scope.
|
|
@@ -703,7 +823,8 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
703
823
|
htmlSeedsCount: htmlSeeds.length,
|
|
704
824
|
nonHtmlCount: nonHtmlSeeds.length,
|
|
705
825
|
outOfScope,
|
|
706
|
-
sourceFileSha256,
|
|
826
|
+
sourceFileSha256: source?.sha256 ?? null,
|
|
827
|
+
invalidSkipped: source?.invalidLineCount ?? null,
|
|
707
828
|
});
|
|
708
829
|
// Ingestion's DB writes are now committed. From here on a
|
|
709
830
|
// throw must NOT trigger the `.bak` restore (it would wipe
|
|
@@ -853,7 +974,13 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
853
974
|
const absFilePath = path.isAbsolute(archivePath)
|
|
854
975
|
? archivePath
|
|
855
976
|
: path.resolve(cwd, archivePath);
|
|
856
|
-
|
|
977
|
+
// See `ArchiveOpenOptions.openPluginData` for why this must be `true`
|
|
978
|
+
// on every writer path that calls `write()`.
|
|
979
|
+
const archive = await Archive.open({
|
|
980
|
+
filePath: absFilePath,
|
|
981
|
+
cwd,
|
|
982
|
+
openPluginData: true,
|
|
983
|
+
});
|
|
857
984
|
// Any throw between here and the successful return must release the
|
|
858
985
|
// archive lock and clean up tmpDir; the caller's `close()` only runs on
|
|
859
986
|
// the happy path.
|
|
@@ -990,10 +1117,10 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
990
1117
|
* no `--label` flag, so this is always the auto form.
|
|
991
1118
|
* `source_file_sha256` arrives pre-computed via
|
|
992
1119
|
* `aggregates.sourceFileSha256` (the CLI's `inventoryCrawl` ran
|
|
993
|
-
* `computeFileSha256` against the
|
|
994
|
-
* was even invoked). The orchestrator boundary
|
|
995
|
-
* the absolute path — see
|
|
996
|
-
* privacy rationale.
|
|
1120
|
+
* `computeFileSha256` against the bytes it read from the input txt,
|
|
1121
|
+
* before the orchestrator was even invoked). The orchestrator boundary
|
|
1122
|
+
* deliberately never sees the absolute path — see
|
|
1123
|
+
* {@link InventoryRunAggregates} for the privacy rationale.
|
|
997
1124
|
*
|
|
998
1125
|
* **Audit-write failures abort the ingestion phase.** Swallowing them
|
|
999
1126
|
* would only be justified if the audit were the last write after the
|
|
@@ -1020,6 +1147,7 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
1020
1147
|
new_pages: aggregates.htmlSeedsCount,
|
|
1021
1148
|
new_resources: aggregates.nonHtmlCount,
|
|
1022
1149
|
scope_skipped: aggregates.outOfScope,
|
|
1150
|
+
invalid_skipped: aggregates.invalidSkipped,
|
|
1023
1151
|
});
|
|
1024
1152
|
}
|
|
1025
1153
|
/**
|
|
@@ -1034,6 +1162,13 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
1034
1162
|
// eslint-disable-next-line no-console
|
|
1035
1163
|
console.error(`[preload] Short-circuited ${skipped} URL(s) on DNS-burned hosts`);
|
|
1036
1164
|
}
|
|
1165
|
+
const { confirmedCount, totalDurationMs } = networkOutageSummaryCounter;
|
|
1166
|
+
if (confirmedCount > 0) {
|
|
1167
|
+
// eslint-disable-next-line no-console
|
|
1168
|
+
console.error(`[network] ${confirmedCount} outage(s), ${Math.round(totalDurationMs / 1000)}s total`);
|
|
1169
|
+
}
|
|
1170
|
+
networkOutageSummaryCounter.confirmedCount = 0;
|
|
1171
|
+
networkOutageSummaryCounter.totalDurationMs = 0;
|
|
1037
1172
|
clearDestinationCache();
|
|
1038
1173
|
clearDnsBurnedHostCache();
|
|
1039
1174
|
}
|
package/lib/crawler.d.ts
CHANGED
|
@@ -16,16 +16,36 @@ export { default as ArchiveResource } from './archive/resource.js';
|
|
|
16
16
|
export * from './archive/types.js';
|
|
17
17
|
export { default as Archive } from './archive/archive.js';
|
|
18
18
|
export { isArchiveCacheDisabled } from './archive/cache/is-archive-cache-disabled.js';
|
|
19
|
+
export { getArchiveCacheRoot } from './archive/cache/get-archive-cache-root.js';
|
|
20
|
+
export { computeArchiveCacheKey } from './archive/cache/compute-archive-cache-key.js';
|
|
21
|
+
export { resolveArchiveCacheDir } from './archive/cache/resolve-archive-cache-dir.js';
|
|
22
|
+
export { listArchiveCacheEntries } from './archive/cache/list-archive-cache-entries.js';
|
|
23
|
+
export { clearArchiveCacheRoot } from './archive/cache/clear-archive-cache-root.js';
|
|
24
|
+
export { clearArchiveCacheEntry } from './archive/cache/clear-archive-cache-entry.js';
|
|
25
|
+
export type { ArchiveCacheEntry, ArchiveCacheEntryKind } from './archive/cache/types.js';
|
|
19
26
|
export { acquireArchiveLock, ArchiveLockError } from './archive/archive-lock.js';
|
|
20
27
|
export { peekArchiveLockHolder } from './archive/peek-archive-lock.js';
|
|
21
28
|
export type { ArchiveLockHolder } from './archive/peek-archive-lock.js';
|
|
22
29
|
export type { FlatPageMetaColumns, JsonLdRow, JsonLdRowForInsert, TagRow, TagRowForInsert, JsonLdSummary, TagsSummary, TagInventoryEntry, PageDenormalizedColumns, } from './archive/meta/types.js';
|
|
23
30
|
export { IncompatibleArchiveError } from './archive/meta/types.js';
|
|
24
31
|
export { REQUIRED_FORMAT_VERSION } from './archive/meta/assert-compatible-version.js';
|
|
32
|
+
export { computeBodyHash } from './archive/body-hash/compute-body-hash.js';
|
|
33
|
+
export { decodeStoredBlob } from './archive/decode-html-blob.js';
|
|
34
|
+
export { computeTierAAliasKey } from './archive/url-alias/compute-tier-a-alias-key.js';
|
|
35
|
+
export { computeTierBAliasKey } from './archive/url-alias/compute-tier-b-alias-key.js';
|
|
25
36
|
export { DEFAULT_EXCLUDED_EXTERNAL_URLS, CrawlerOrchestrator, } from './crawler-orchestrator.js';
|
|
26
37
|
export * from './types.js';
|
|
27
38
|
export * from './crawler/types.js';
|
|
28
39
|
export { classifyErrorKind } from './classify-error-kind.js';
|
|
40
|
+
export { NETWORK_RELATED_ERROR_KINDS } from './network-related-error-kinds.js';
|
|
41
|
+
export type { OutageWindow } from './is-within-outage-window.js';
|
|
42
|
+
export { isWithinOutageWindow } from './is-within-outage-window.js';
|
|
43
|
+
export { default as NetworkOutageDetector } from './crawler/network-outage-detector.js';
|
|
44
|
+
export { default as NetworkGate } from './crawler/network-gate.js';
|
|
45
|
+
export type { NetworkProbe } from './crawler/probe-network.js';
|
|
46
|
+
export { probeNetwork } from './crawler/probe-network.js';
|
|
47
|
+
export { computeOutageClampTimestamp } from './archive/db-ops/outages/compute-outage-clamp-timestamp.js';
|
|
48
|
+
export { chooseProbeHost } from './crawler/choose-probe-host.js';
|
|
29
49
|
export { computeFileSha256 } from './utils/compute-file-sha256.js';
|
|
30
50
|
export { populateEntityTables } from './archive/populate-entity-tables/populate-entities.js';
|
|
31
51
|
export type { PageDomPathResolver } from './archive/populate-entity-tables/populate-image-items.js';
|
|
@@ -38,3 +58,4 @@ export { populateBlobRefs } from './archive/populate-ref-tables/populate-blob-re
|
|
|
38
58
|
export { populateHeaderTables } from './archive/populate-ref-tables/populate-header-tables.js';
|
|
39
59
|
export { loadResponseHeadersBySetIds } from './archive/db-ops/_shared/load-response-headers-by-set-ids.js';
|
|
40
60
|
export { decodeJsonRef } from './archive/db-ops/_shared/decode-json-ref.js';
|
|
61
|
+
export type { TemplateClusterBlockingEvidence, TemplateClusterBlockingReason, TemplateClusterLandmarkProfile, TemplateClusterLandmarkType, TemplateClusterReason, } from './archive/db-ops/analysis/types.js';
|
package/lib/crawler.js
CHANGED
|
@@ -17,15 +17,32 @@ export { default as ArchiveResource } from './archive/resource.js';
|
|
|
17
17
|
export * from './archive/types.js';
|
|
18
18
|
export { default as Archive } from './archive/archive.js';
|
|
19
19
|
export { isArchiveCacheDisabled } from './archive/cache/is-archive-cache-disabled.js';
|
|
20
|
+
export { getArchiveCacheRoot } from './archive/cache/get-archive-cache-root.js';
|
|
21
|
+
export { computeArchiveCacheKey } from './archive/cache/compute-archive-cache-key.js';
|
|
22
|
+
export { resolveArchiveCacheDir } from './archive/cache/resolve-archive-cache-dir.js';
|
|
23
|
+
export { listArchiveCacheEntries } from './archive/cache/list-archive-cache-entries.js';
|
|
24
|
+
export { clearArchiveCacheRoot } from './archive/cache/clear-archive-cache-root.js';
|
|
25
|
+
export { clearArchiveCacheEntry } from './archive/cache/clear-archive-cache-entry.js';
|
|
20
26
|
export { acquireArchiveLock, ArchiveLockError } from './archive/archive-lock.js';
|
|
21
27
|
export { peekArchiveLockHolder } from './archive/peek-archive-lock.js';
|
|
22
28
|
export { IncompatibleArchiveError } from './archive/meta/types.js';
|
|
23
29
|
export { REQUIRED_FORMAT_VERSION } from './archive/meta/assert-compatible-version.js';
|
|
30
|
+
export { computeBodyHash } from './archive/body-hash/compute-body-hash.js';
|
|
31
|
+
export { decodeStoredBlob } from './archive/decode-html-blob.js';
|
|
32
|
+
export { computeTierAAliasKey } from './archive/url-alias/compute-tier-a-alias-key.js';
|
|
33
|
+
export { computeTierBAliasKey } from './archive/url-alias/compute-tier-b-alias-key.js';
|
|
24
34
|
// Core
|
|
25
35
|
export { DEFAULT_EXCLUDED_EXTERNAL_URLS, CrawlerOrchestrator, } from './crawler-orchestrator.js';
|
|
26
36
|
export * from './types.js';
|
|
27
37
|
export * from './crawler/types.js';
|
|
28
38
|
export { classifyErrorKind } from './classify-error-kind.js';
|
|
39
|
+
export { NETWORK_RELATED_ERROR_KINDS } from './network-related-error-kinds.js';
|
|
40
|
+
export { isWithinOutageWindow } from './is-within-outage-window.js';
|
|
41
|
+
export { default as NetworkOutageDetector } from './crawler/network-outage-detector.js';
|
|
42
|
+
export { default as NetworkGate } from './crawler/network-gate.js';
|
|
43
|
+
export { probeNetwork } from './crawler/probe-network.js';
|
|
44
|
+
export { computeOutageClampTimestamp } from './archive/db-ops/outages/compute-outage-clamp-timestamp.js';
|
|
45
|
+
export { chooseProbeHost } from './crawler/choose-probe-host.js';
|
|
29
46
|
export { computeFileSha256 } from './utils/compute-file-sha256.js';
|
|
30
47
|
// 0.13 ref-table population (issue #191, epic #103). Exposed as the
|
|
31
48
|
// public seam that the migration script (`scripts/migrate-to-0.13.mjs`)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A resolved (closed) network-outage interval, as consumed by
|
|
3
|
+
* {@link isWithinOutageWindow}.
|
|
4
|
+
*
|
|
5
|
+
* This is deliberately NOT the raw `network_outages` row shape: a row whose
|
|
6
|
+
* `ended_at` is still `NULL` (the crawl session crashed before recovery was
|
|
7
|
+
* observed) must be resolved to a concrete `endedAt` by the caller — e.g.
|
|
8
|
+
* `listNetworkOutages` clamping it to the latest observed timestamp in the
|
|
9
|
+
* archive — before it reaches this function. Accepting a nullable `endedAt`
|
|
10
|
+
* here would let a crashed session's outage silently swallow every
|
|
11
|
+
* subsequent timestamp as "network-caused" forever.
|
|
12
|
+
*/
|
|
13
|
+
export interface OutageWindow {
|
|
14
|
+
/** Epoch ms the outage is considered to have started (already backdated to the earliest triggering error). */
|
|
15
|
+
readonly startedAt: number;
|
|
16
|
+
/** Epoch ms the outage is considered to have ended. Never `NULL` — see the interface docstring. */
|
|
17
|
+
readonly endedAt: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Decide whether a timestamp falls inside any recorded network-outage
|
|
21
|
+
* window — i.e. whether an error observed at that instant is more likely
|
|
22
|
+
* attributable to the operator's own network than to the target site.
|
|
23
|
+
*
|
|
24
|
+
* Pure and dependency-free by design: no DB handle, no `Crawler` instance.
|
|
25
|
+
* This is the single predicate every consumer (`resetFailedPages`,
|
|
26
|
+
* `listDnsBurnedHostCandidates`, `getSummary`'s attribution split) is
|
|
27
|
+
* expected to call, so the inclusive/exclusive boundary decision below is
|
|
28
|
+
* made exactly once.
|
|
29
|
+
*
|
|
30
|
+
* **Boundaries are inclusive on both ends.** A timestamp equal to
|
|
31
|
+
* `startedAt` or `endedAt` counts as inside the window. This errs toward
|
|
32
|
+
* attributing borderline errors to the outage rather than to the site:
|
|
33
|
+
* classifying a genuinely network-caused failure as "unknown/site kills it
|
|
34
|
+
* forever" (a false negative) is worse than the reverse (a false positive
|
|
35
|
+
* merely costs one extra retry pass before the kind classifier sorts it out
|
|
36
|
+
* again).
|
|
37
|
+
* @param timestamp - Epoch ms to test (typically an error's `createdAt`).
|
|
38
|
+
* @param windows - Resolved outage windows to test against. An empty array
|
|
39
|
+
* always yields `false` — this is what makes an archive with no
|
|
40
|
+
* `network_outages` rows (every archive created before this feature, or
|
|
41
|
+
* any crawl with no detected outage) behave identically to today.
|
|
42
|
+
* @returns `true` if `timestamp` falls within any window.
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* isWithinOutageWindow(1_000, [{ startedAt: 500, endedAt: 1_500 }]); // true
|
|
46
|
+
* isWithinOutageWindow(1_000, []); // false — no recorded outages
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare function isWithinOutageWindow(timestamp: number, windows: readonly OutageWindow[]): boolean;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide whether a timestamp falls inside any recorded network-outage
|
|
3
|
+
* window — i.e. whether an error observed at that instant is more likely
|
|
4
|
+
* attributable to the operator's own network than to the target site.
|
|
5
|
+
*
|
|
6
|
+
* Pure and dependency-free by design: no DB handle, no `Crawler` instance.
|
|
7
|
+
* This is the single predicate every consumer (`resetFailedPages`,
|
|
8
|
+
* `listDnsBurnedHostCandidates`, `getSummary`'s attribution split) is
|
|
9
|
+
* expected to call, so the inclusive/exclusive boundary decision below is
|
|
10
|
+
* made exactly once.
|
|
11
|
+
*
|
|
12
|
+
* **Boundaries are inclusive on both ends.** A timestamp equal to
|
|
13
|
+
* `startedAt` or `endedAt` counts as inside the window. This errs toward
|
|
14
|
+
* attributing borderline errors to the outage rather than to the site:
|
|
15
|
+
* classifying a genuinely network-caused failure as "unknown/site kills it
|
|
16
|
+
* forever" (a false negative) is worse than the reverse (a false positive
|
|
17
|
+
* merely costs one extra retry pass before the kind classifier sorts it out
|
|
18
|
+
* again).
|
|
19
|
+
* @param timestamp - Epoch ms to test (typically an error's `createdAt`).
|
|
20
|
+
* @param windows - Resolved outage windows to test against. An empty array
|
|
21
|
+
* always yields `false` — this is what makes an archive with no
|
|
22
|
+
* `network_outages` rows (every archive created before this feature, or
|
|
23
|
+
* any crawl with no detected outage) behave identically to today.
|
|
24
|
+
* @returns `true` if `timestamp` falls within any window.
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* isWithinOutageWindow(1_000, [{ startedAt: 500, endedAt: 1_500 }]); // true
|
|
28
|
+
* isWithinOutageWindow(1_000, []); // false — no recorded outages
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function isWithinOutageWindow(timestamp, windows) {
|
|
32
|
+
return windows.some((window) => timestamp >= window.startedAt && timestamp <= window.endedAt);
|
|
33
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ErrorKind } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* `ErrorKind`s that plausibly indicate trouble with the operator's own
|
|
4
|
+
* network path rather than a genuinely dead or misconfigured target site.
|
|
5
|
+
*
|
|
6
|
+
* Single source of truth shared by two consumers that must agree on
|
|
7
|
+
* exactly the same set:
|
|
8
|
+
*
|
|
9
|
+
* - `NetworkOutageDetector` — only these kinds contribute to the sliding
|
|
10
|
+
* window that detects a suspect outage.
|
|
11
|
+
* - `evict-network-classified-destination-cache-entries.ts` — on recovery,
|
|
12
|
+
* only `destinationCache` entries whose cached `Error` classifies into
|
|
13
|
+
* one of these kinds are evicted (a cached `tls` or `client-blocked`
|
|
14
|
+
* failure is a site-specific fact and must survive an outage recovery
|
|
15
|
+
* unrelated to it).
|
|
16
|
+
*
|
|
17
|
+
* `dns` is included despite `permanent-error-kinds.ts` treating it as a
|
|
18
|
+
* permanent, site-specific verdict in isolation — that classification is
|
|
19
|
+
* exactly what outage detection exists to override when the surrounding
|
|
20
|
+
* evidence (many hosts, tight time window) points to the local network
|
|
21
|
+
* instead.
|
|
22
|
+
*/
|
|
23
|
+
export declare const NETWORK_RELATED_ERROR_KINDS: ReadonlySet<ErrorKind>;
|