@nitpicker/crawler 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/archive/archive.d.ts +136 -10
- package/lib/archive/archive.js +147 -9
- package/lib/archive/body-hash/compute-body-hash.d.ts +22 -0
- package/lib/archive/body-hash/compute-body-hash.js +31 -0
- package/lib/archive/body-hash/extract-body.d.ts +18 -0
- package/lib/archive/body-hash/extract-body.js +29 -0
- package/lib/archive/body-hash/mask-dynamic-ids.d.ts +33 -0
- package/lib/archive/body-hash/mask-dynamic-ids.js +44 -0
- package/lib/archive/body-hash/normalize-url-like-strings.d.ts +16 -0
- package/lib/archive/body-hash/normalize-url-like-strings.js +19 -0
- package/lib/archive/cache/clear-archive-cache-entry.d.ts +19 -0
- package/lib/archive/cache/clear-archive-cache-entry.js +25 -0
- package/lib/archive/cache/clear-archive-cache-root.d.ts +26 -0
- package/lib/archive/cache/clear-archive-cache-root.js +32 -0
- package/lib/archive/cache/list-archive-cache-entries.d.ts +23 -0
- package/lib/archive/cache/list-archive-cache-entries.js +125 -0
- package/lib/archive/cache/path-exists.d.ts +9 -0
- package/lib/archive/cache/path-exists.js +18 -0
- package/lib/archive/cache/types.d.ts +33 -0
- package/lib/archive/cache/types.js +1 -0
- package/lib/archive/create-adjunct-tables.d.ts +14 -0
- package/lib/archive/create-adjunct-tables.js +169 -0
- package/lib/archive/create-entity-tables.js +21 -1
- package/lib/archive/database.d.ts +67 -2
- package/lib/archive/database.js +92 -3
- package/lib/archive/db-ops/_shared/clear-write-ref-caches.js +1 -0
- package/lib/archive/db-ops/_shared/compress-payload.d.ts +26 -0
- package/lib/archive/db-ops/_shared/compress-payload.js +30 -0
- package/lib/archive/db-ops/_shared/create-write-ref-caches.js +1 -0
- package/lib/archive/db-ops/_shared/types.d.ts +2 -0
- package/lib/archive/db-ops/_shared/upsert-json-ref.js +3 -3
- package/lib/archive/db-ops/analysis/replace-page-templates.d.ts +12 -3
- package/lib/archive/db-ops/analysis/replace-page-templates.js +35 -3
- package/lib/archive/db-ops/analysis/types.d.ts +61 -0
- package/lib/archive/db-ops/analysis/types.js +1 -0
- package/lib/archive/db-ops/console-logs/compute-console-log-hash.d.ts +19 -0
- package/lib/archive/db-ops/console-logs/compute-console-log-hash.js +30 -0
- package/lib/archive/db-ops/console-logs/replace-console-logs.d.ts +34 -0
- package/lib/archive/db-ops/console-logs/replace-console-logs.js +123 -0
- package/lib/archive/db-ops/console-logs/stringify-console-log-args.d.ts +16 -0
- package/lib/archive/db-ops/console-logs/stringify-console-log-args.js +26 -0
- package/lib/archive/db-ops/console-logs/types.d.ts +38 -0
- package/lib/archive/db-ops/console-logs/types.js +1 -0
- package/lib/archive/db-ops/console-logs/upsert-console-log-item.d.ts +29 -0
- package/lib/archive/db-ops/console-logs/upsert-console-log-item.js +52 -0
- package/lib/archive/db-ops/dedupe-cap/accumulate-dedupe-cap-rejected-count.d.ts +18 -0
- package/lib/archive/db-ops/dedupe-cap/accumulate-dedupe-cap-rejected-count.js +23 -0
- package/lib/archive/db-ops/dedupe-cap/finalize-dedupe-cap-event.d.ts +12 -0
- package/lib/archive/db-ops/dedupe-cap/finalize-dedupe-cap-event.js +15 -0
- package/lib/archive/db-ops/dedupe-cap/insert-dedupe-cap-event.d.ts +14 -0
- package/lib/archive/db-ops/dedupe-cap/insert-dedupe-cap-event.js +30 -0
- package/lib/archive/db-ops/dedupe-cap/list-dedupe-cap-shape-keys.d.ts +21 -0
- package/lib/archive/db-ops/dedupe-cap/list-dedupe-cap-shape-keys.js +27 -0
- package/lib/archive/db-ops/errors/list-dns-burned-host-candidates.d.ts +11 -0
- package/lib/archive/db-ops/errors/list-dns-burned-host-candidates.js +21 -4
- package/lib/archive/db-ops/inventory/record-inventory-run.js +1 -0
- package/lib/archive/db-ops/lifecycle/init.d.ts +11 -2
- package/lib/archive/db-ops/lifecycle/init.js +21 -2
- package/lib/archive/db-ops/outages/close-network-outage.d.ts +12 -0
- package/lib/archive/db-ops/outages/close-network-outage.js +15 -0
- package/lib/archive/db-ops/outages/close-stale-open-network-outages.d.ts +26 -0
- package/lib/archive/db-ops/outages/close-stale-open-network-outages.js +42 -0
- package/lib/archive/db-ops/outages/compute-outage-clamp-timestamp.d.ts +26 -0
- package/lib/archive/db-ops/outages/compute-outage-clamp-timestamp.js +31 -0
- package/lib/archive/db-ops/outages/insert-network-outage.d.ts +14 -0
- package/lib/archive/db-ops/outages/insert-network-outage.js +29 -0
- package/lib/archive/db-ops/outages/list-network-outages.d.ts +28 -0
- package/lib/archive/db-ops/outages/list-network-outages.js +42 -0
- package/lib/archive/db-ops/pages/reset/reset-failed-pages.d.ts +13 -0
- package/lib/archive/db-ops/pages/reset/reset-failed-pages.js +35 -7
- package/lib/archive/db-ops/pages/write/update-page.js +12 -0
- package/lib/archive/db-ops/pages/write/write-page-html-blob.js +6 -6
- package/lib/archive/filesystem/output-binary.d.ts +12 -0
- package/lib/archive/filesystem/output-binary.js +17 -0
- package/lib/archive/get-failed-page-messages.d.ts +20 -8
- package/lib/archive/get-failed-page-messages.js +29 -15
- package/lib/archive/migrate-content-items-alias-of-id.d.ts +47 -0
- package/lib/archive/migrate-content-items-alias-of-id.js +58 -0
- package/lib/archive/migrate-inventory-runs-invalid-skipped.d.ts +11 -0
- package/lib/archive/migrate-inventory-runs-invalid-skipped.js +24 -0
- package/lib/archive/migrate-page-meta-body-hash.d.ts +39 -0
- package/lib/archive/migrate-page-meta-body-hash.js +52 -0
- package/lib/archive/migrate-page-meta-console-error-count.d.ts +22 -0
- package/lib/archive/migrate-page-meta-console-error-count.js +34 -0
- package/lib/archive/page.d.ts +1 -1
- package/lib/archive/types.d.ts +60 -2
- package/lib/archive/url-alias/compute-tier-a-alias-key.d.ts +31 -0
- package/lib/archive/url-alias/compute-tier-a-alias-key.js +36 -0
- package/lib/archive/url-alias/compute-tier-b-alias-key.d.ts +19 -0
- package/lib/archive/url-alias/compute-tier-b-alias-key.js +30 -0
- package/lib/archive/url-alias/format-alias-key.d.ts +13 -0
- package/lib/archive/url-alias/format-alias-key.js +14 -0
- package/lib/archive/url-alias/parse-alias-key-parts.d.ts +15 -0
- package/lib/archive/url-alias/parse-alias-key-parts.js +40 -0
- package/lib/archive/url-alias/types.d.ts +7 -0
- package/lib/archive/url-alias/types.js +1 -0
- package/lib/classify-error-kind.d.ts +1 -0
- package/lib/classify-error-kind.js +14 -0
- package/lib/crawler/assert-chrome-installed.d.ts +24 -0
- package/lib/crawler/assert-chrome-installed.js +43 -0
- package/lib/crawler/choose-probe-host.d.ts +24 -0
- package/lib/crawler/choose-probe-host.js +38 -0
- package/lib/crawler/clear-dns-burned-host-cache.js +2 -0
- package/lib/crawler/crawler.d.ts +12 -0
- package/lib/crawler/crawler.js +496 -29
- package/lib/crawler/decode-auth-credential.d.ts +29 -0
- package/lib/crawler/decode-auth-credential.js +39 -0
- package/lib/crawler/dedupe/compute-meta-signature.d.ts +30 -0
- package/lib/crawler/dedupe/compute-meta-signature.js +0 -0
- package/lib/crawler/dedupe/compute-shape-key.d.ts +37 -0
- package/lib/crawler/dedupe/compute-shape-key.js +56 -0
- package/lib/crawler/dedupe/dedupe-cap-tracker.d.ts +84 -0
- package/lib/crawler/dedupe/dedupe-cap-tracker.js +185 -0
- package/lib/crawler/dedupe/is-predicted-content-duplicate.d.ts +24 -0
- package/lib/crawler/dedupe/is-predicted-content-duplicate.js +26 -0
- package/lib/crawler/dedupe/is-shape-capped.d.ts +10 -0
- package/lib/crawler/dedupe/is-shape-capped.js +12 -0
- package/lib/crawler/dedupe/resolve-og-url-mismatch.d.ts +31 -0
- package/lib/crawler/dedupe/resolve-og-url-mismatch.js +40 -0
- package/lib/crawler/dedupe/types.d.ts +42 -0
- package/lib/crawler/dedupe/types.js +1 -0
- package/lib/crawler/dns-burned-host-burn-timestamps.d.ts +17 -0
- package/lib/crawler/dns-burned-host-burn-timestamps.js +17 -0
- package/lib/crawler/evict-network-classified-destination-cache-entries.d.ts +26 -0
- package/lib/crawler/evict-network-classified-destination-cache-entries.js +34 -0
- package/lib/crawler/evict-outage-tainted-dns-burns.d.ts +23 -0
- package/lib/crawler/evict-outage-tainted-dns-burns.js +26 -0
- package/lib/crawler/fetch-destination.js +14 -2
- package/lib/crawler/generate-predicted-urls.d.ts +12 -0
- package/lib/crawler/generate-predicted-urls.js +33 -2
- package/lib/crawler/is-puppeteer-fallback-candidate.js +3 -0
- package/lib/crawler/network-gate.d.ts +49 -0
- package/lib/crawler/network-gate.js +78 -0
- package/lib/crawler/network-outage-detector.d.ts +51 -0
- package/lib/crawler/network-outage-detector.js +81 -0
- package/lib/crawler/network-outage-summary-counter.d.ts +21 -0
- package/lib/crawler/network-outage-summary-counter.js +18 -0
- package/lib/crawler/probe-network.d.ts +26 -0
- package/lib/crawler/probe-network.js +21 -0
- package/lib/crawler/types.d.ts +185 -1
- package/lib/crawler-orchestrator.d.ts +74 -18
- package/lib/crawler-orchestrator.js +267 -27
- package/lib/crawler.d.ts +22 -0
- package/lib/crawler.js +18 -0
- package/lib/is-within-outage-window.d.ts +49 -0
- package/lib/is-within-outage-window.js +33 -0
- package/lib/network-related-error-kinds.d.ts +23 -0
- package/lib/network-related-error-kinds.js +28 -0
- package/lib/permanent-error-kinds.d.ts +9 -4
- package/lib/permanent-error-kinds.js +10 -4
- package/lib/types.d.ts +11 -2
- package/lib/utils/array/each-splitted.d.ts +1 -1
- package/lib/utils/compute-file-sha256.d.ts +17 -17
- package/lib/utils/compute-file-sha256.js +18 -47
- package/package.json +3 -3
|
@@ -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';
|
|
@@ -63,8 +64,28 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
63
64
|
#archive;
|
|
64
65
|
/** The crawler engine that discovers and scrapes pages. */
|
|
65
66
|
#crawler;
|
|
67
|
+
/**
|
|
68
|
+
* `dedupe_cap_events.id` for each shape confirmed capped this session, so
|
|
69
|
+
* `crawlEnd` can look up the right row to finalize with
|
|
70
|
+
* `Crawler#getDedupeCapRejections`'s counts. A `Map` (not a single
|
|
71
|
+
* scalar like {@link #openNetworkOutageId}) because, unlike a network
|
|
72
|
+
* outage, more than one shape can be capped simultaneously within one
|
|
73
|
+
* crawl.
|
|
74
|
+
*/
|
|
75
|
+
#dedupeCapEventIds = new Map();
|
|
66
76
|
/** Whether the crawl was started from a pre-defined URL list (non-recursive mode). */
|
|
67
77
|
#fromList;
|
|
78
|
+
/**
|
|
79
|
+
* The `network_outages` row id for the currently-open outage, or `null`
|
|
80
|
+
* when none is open. Set by the `networkOutageConfirmed` handler (once
|
|
81
|
+
* the INSERT resolves) and consumed by `networkOutageRecovered` — the
|
|
82
|
+
* `Crawler` class never touches the archive itself and has no way to
|
|
83
|
+
* know the row's id, so the orchestrator is the only place that can
|
|
84
|
+
* bridge the two events for the same outage.
|
|
85
|
+
*/
|
|
86
|
+
#openNetworkOutageId = null;
|
|
87
|
+
/** `startedAt` of the currently-open outage, tracked alongside {@link #openNetworkOutageId} so `networkOutageRecovered` can compute a duration for {@link networkOutageSummaryCounter}. */
|
|
88
|
+
#openNetworkOutageStartedAt = null;
|
|
68
89
|
/** Serializes archive writes from crawler event handlers (FIFO). */
|
|
69
90
|
#writeQueue = new WriteQueue();
|
|
70
91
|
/**
|
|
@@ -138,6 +159,21 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
138
159
|
// rows continue to land in pages/resources with the DB DEFAULT
|
|
139
160
|
// `'crawled'` provenance label.
|
|
140
161
|
inventoryMode: options?.inventoryMode ?? null,
|
|
162
|
+
// Forwarded as-is (including `undefined`) — `Crawler`'s own
|
|
163
|
+
// constructor merges each against its `DEFAULT_NETWORK_OUTAGE_*`
|
|
164
|
+
// constant, so omitting them here is exactly "use the default".
|
|
165
|
+
networkOutageWindowMs: options?.networkOutageWindowMs,
|
|
166
|
+
networkOutageErrorThreshold: options?.networkOutageErrorThreshold,
|
|
167
|
+
networkOutageHostThreshold: options?.networkOutageHostThreshold,
|
|
168
|
+
networkOutageProbeIntervalMs: options?.networkOutageProbeIntervalMs,
|
|
169
|
+
networkProbe: options?.networkProbe ?? null,
|
|
170
|
+
dedupeCap: options?.dedupeCap ?? null,
|
|
171
|
+
dedupeMapCap: options?.dedupeMapCap,
|
|
172
|
+
// Only the four resuming-session static methods
|
|
173
|
+
// (`append`/`inventory`/`retryFailed`/`resume`) pass this — a
|
|
174
|
+
// fresh `crawling()` has no archive history to seed from (see
|
|
175
|
+
// `CrawlConfig.preloadedStickyShapeKeys`'s JSDoc).
|
|
176
|
+
preloadedStickyShapeKeys: options?.preloadedStickyShapeKeys ?? [],
|
|
141
177
|
});
|
|
142
178
|
}
|
|
143
179
|
/**
|
|
@@ -166,6 +202,11 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
166
202
|
*/
|
|
167
203
|
async crawling(list, opts) {
|
|
168
204
|
const writeQueue = this.#writeQueue;
|
|
205
|
+
// Per-session state, like `Crawler`'s own `#successfulHosts.clear()` /
|
|
206
|
+
// `#networkGate.open()` reset at the start of `#runDeal` — a fresh
|
|
207
|
+
// session must not inherit a dangling outage id from a prior one.
|
|
208
|
+
this.#openNetworkOutageId = null;
|
|
209
|
+
this.#openNetworkOutageStartedAt = null;
|
|
169
210
|
return new Promise((resolve, reject) => {
|
|
170
211
|
this.#crawler.on('error', (error) => {
|
|
171
212
|
if (error.error instanceof PreloadShortCircuitError) {
|
|
@@ -212,6 +253,84 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
212
253
|
.catch((error) => reject(error));
|
|
213
254
|
void this.emit('redirect', { result });
|
|
214
255
|
});
|
|
256
|
+
this.#crawler.on('networkOutageConfirmed', ({ startedAt, detectedAt, probeHost, triggerErrorCount, triggerHostCount }) => {
|
|
257
|
+
crawlerLog('Network outage confirmed: probeHost=%s triggerErrorCount=%d triggerHostCount=%d', probeHost, triggerErrorCount, triggerHostCount);
|
|
258
|
+
// event notice; mirrors `#finalizeCrawlSession`'s unconditional
|
|
259
|
+
// `console.error` for the DNS-burn short-circuit summary.
|
|
260
|
+
console.error(`[network] outage suspected — pausing workers (probe host: ${probeHost ?? 'none'})`);
|
|
261
|
+
writeQueue
|
|
262
|
+
.enqueue(async () => {
|
|
263
|
+
// Both fields are set together, inside this single
|
|
264
|
+
// closure, so the pair can never fall out of sync
|
|
265
|
+
// (e.g. one set synchronously above while the other
|
|
266
|
+
// waits on the INSERT) — `networkOutageRecovered`'s
|
|
267
|
+
// queued closure always sees either both set or
|
|
268
|
+
// neither.
|
|
269
|
+
const id = await this.#archive.insertNetworkOutage({
|
|
270
|
+
startedAt,
|
|
271
|
+
detectedAt,
|
|
272
|
+
probeHost,
|
|
273
|
+
triggerErrorCount,
|
|
274
|
+
triggerHostCount,
|
|
275
|
+
});
|
|
276
|
+
this.#openNetworkOutageId = id;
|
|
277
|
+
this.#openNetworkOutageStartedAt = startedAt;
|
|
278
|
+
})
|
|
279
|
+
.catch((error) => reject(error));
|
|
280
|
+
});
|
|
281
|
+
this.#crawler.on('networkOutageRecovered', ({ endedAt }) => {
|
|
282
|
+
// The `id` read is deferred to INSIDE the queued closure, not
|
|
283
|
+
// read synchronously here, because `networkOutageConfirmed`'s
|
|
284
|
+
// INSERT is itself only queued (not awaited) when that event
|
|
285
|
+
// fires — `#openNetworkOutageId` is not guaranteed to be set
|
|
286
|
+
// yet at the instant `networkOutageRecovered` fires (the two
|
|
287
|
+
// events can arrive in quick succession, e.g. in tests that
|
|
288
|
+
// drive them back-to-back with no real probe-interval delay
|
|
289
|
+
// between them). `WriteQueue` runs enqueued operations in
|
|
290
|
+
// submission order, so by the time THIS closure actually
|
|
291
|
+
// executes, the confirm's INSERT closure (enqueued first) has
|
|
292
|
+
// already completed and `#openNetworkOutageId` is reliably set.
|
|
293
|
+
writeQueue
|
|
294
|
+
.enqueue(() => {
|
|
295
|
+
const id = this.#openNetworkOutageId;
|
|
296
|
+
const startedAt = this.#openNetworkOutageStartedAt;
|
|
297
|
+
if (id === null) {
|
|
298
|
+
// Defensive: `networkOutageConfirmed` always
|
|
299
|
+
// precedes `networkOutageRecovered` on the same
|
|
300
|
+
// `Crawler` instance. If this fires anyway, there
|
|
301
|
+
// is no row to close.
|
|
302
|
+
crawlerLog('Network outage recovered but no open outage id was tracked');
|
|
303
|
+
return Promise.resolve();
|
|
304
|
+
}
|
|
305
|
+
this.#openNetworkOutageId = null;
|
|
306
|
+
this.#openNetworkOutageStartedAt = null;
|
|
307
|
+
const durationMs = endedAt - (startedAt ?? endedAt);
|
|
308
|
+
networkOutageSummaryCounter.confirmedCount++;
|
|
309
|
+
networkOutageSummaryCounter.totalDurationMs += durationMs;
|
|
310
|
+
crawlerLog('Network outage recovered: id=%d endedAt=%d', id, endedAt);
|
|
311
|
+
// eslint-disable-next-line no-console -- see the confirmed handler above
|
|
312
|
+
console.error(`[network] recovered after ${Math.round(durationMs / 1000)}s`);
|
|
313
|
+
return this.#archive.closeNetworkOutage(id, endedAt);
|
|
314
|
+
})
|
|
315
|
+
.catch((error) => reject(error));
|
|
316
|
+
});
|
|
317
|
+
this.#crawler.on('dedupeCap', ({ shapeKey, sampleUrl, bodyHash, effectiveThreshold, observedCount }) => {
|
|
318
|
+
crawlerLog('Dedupe cap reached: shapeKey=%s effectiveThreshold=%d observedCount=%d', shapeKey, effectiveThreshold, observedCount);
|
|
319
|
+
console.error(`[dedupe-cap] same-cluster trap confirmed: ${shapeKey} (sample: ${sampleUrl})`);
|
|
320
|
+
writeQueue
|
|
321
|
+
.enqueue(async () => {
|
|
322
|
+
const id = await this.#archive.insertDedupeCapEvent({
|
|
323
|
+
shapeKey,
|
|
324
|
+
sampleUrl,
|
|
325
|
+
bodyHash,
|
|
326
|
+
effectiveThreshold,
|
|
327
|
+
observedCount,
|
|
328
|
+
detectedAt: Date.now(),
|
|
329
|
+
});
|
|
330
|
+
this.#dedupeCapEventIds.set(shapeKey, id);
|
|
331
|
+
})
|
|
332
|
+
.catch((error) => reject(error));
|
|
333
|
+
});
|
|
215
334
|
this.#crawler.on('response', ({ resource, source }) => {
|
|
216
335
|
writeQueue
|
|
217
336
|
.enqueue(() => this.#archive.setResources(resource, source))
|
|
@@ -222,7 +341,57 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
222
341
|
.enqueue(() => this.#archive.setResourcesReferrers(resource))
|
|
223
342
|
.catch((error) => reject(error));
|
|
224
343
|
});
|
|
344
|
+
this.#crawler.on('consoleLogs', ({ pageUrl, redirectPaths, entries }) => {
|
|
345
|
+
writeQueue
|
|
346
|
+
.enqueue(() => this.#archive.setConsoleLogs(pageUrl, redirectPaths, entries))
|
|
347
|
+
.catch((error) => reject(error));
|
|
348
|
+
});
|
|
225
349
|
this.#crawler.on('crawlEnd', () => {
|
|
350
|
+
// Deferred to INSIDE a queued closure, not read synchronously
|
|
351
|
+
// here, for the same reason `networkOutageRecovered`'s handler
|
|
352
|
+
// defers reading `#openNetworkOutageId`: a `dedupeCap` event's
|
|
353
|
+
// INSERT closure may still be queued (not yet executed) at the
|
|
354
|
+
// instant `crawlEnd` fires. `WriteQueue` runs enqueued
|
|
355
|
+
// operations in submission order, so by the time THIS closure
|
|
356
|
+
// executes, every earlier-queued `dedupeCap` INSERT has
|
|
357
|
+
// already completed and `#dedupeCapEventIds` is reliably
|
|
358
|
+
// populated.
|
|
359
|
+
writeQueue
|
|
360
|
+
.enqueue(async () => {
|
|
361
|
+
const rejections = this.#crawler.getDedupeCapRejections();
|
|
362
|
+
// Finalize every shape capped THIS session (has an id in
|
|
363
|
+
// `#dedupeCapEventIds`), not just the ones with a nonzero
|
|
364
|
+
// rejection count — a shape that capped near the end of the
|
|
365
|
+
// crawl (or whose remaining anchors all happened to be
|
|
366
|
+
// discovered before it capped) never enters `rejections` at
|
|
367
|
+
// all, and would otherwise stay `rejected_count: NULL` forever
|
|
368
|
+
// despite the crawl completing normally, corrupting the "NULL
|
|
369
|
+
// means the crawl never reached crawlEnd" contract
|
|
370
|
+
// `list-dedupe-cap-events.ts` documents.
|
|
371
|
+
const shapeKeysToFinalize = new Set([
|
|
372
|
+
...this.#dedupeCapEventIds.keys(),
|
|
373
|
+
...rejections.keys(),
|
|
374
|
+
]);
|
|
375
|
+
await Promise.all([...shapeKeysToFinalize].map((shapeKey) => {
|
|
376
|
+
const rejectedCount = rejections.get(shapeKey) ?? 0;
|
|
377
|
+
const id = this.#dedupeCapEventIds.get(shapeKey);
|
|
378
|
+
// A shape capped THIS session has an id here (the
|
|
379
|
+
// `dedupeCap` event always enqueues an INSERT before any
|
|
380
|
+
// rejection for that shape can be counted) and is
|
|
381
|
+
// finalized once via its row id. A shape with no id was
|
|
382
|
+
// never observed this session at all — it was preloaded
|
|
383
|
+
// into `DedupeCapTracker`'s sticky set from an EARLIER
|
|
384
|
+
// session's `dedupe_cap_events` row (see
|
|
385
|
+
// `CrawlConfig.preloadedStickyShapeKeys`'s JSDoc), so gate
|
|
386
|
+
// rejections still accumulate for it but no new row (and
|
|
387
|
+
// thus no id) is ever created. That earlier row's count is
|
|
388
|
+
// accumulated onto by shape_key instead of overwritten.
|
|
389
|
+
return id === undefined
|
|
390
|
+
? this.#archive.accumulateDedupeCapRejectedCount(shapeKey, rejectedCount)
|
|
391
|
+
: this.#archive.finalizeDedupeCapEvent(id, rejectedCount);
|
|
392
|
+
}));
|
|
393
|
+
})
|
|
394
|
+
.catch((error) => reject(error));
|
|
226
395
|
writeQueue
|
|
227
396
|
.drain()
|
|
228
397
|
.then(() => resolve())
|
|
@@ -387,7 +556,13 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
387
556
|
const absFilePath = path.isAbsolute(archivePath)
|
|
388
557
|
? archivePath
|
|
389
558
|
: path.resolve(cwd, archivePath);
|
|
390
|
-
|
|
559
|
+
// See `ArchiveOpenOptions.openPluginData` for why this must be `true`
|
|
560
|
+
// on every writer path that calls `write()`.
|
|
561
|
+
const archive = await Archive.open({
|
|
562
|
+
filePath: absFilePath,
|
|
563
|
+
cwd,
|
|
564
|
+
openPluginData: true,
|
|
565
|
+
});
|
|
391
566
|
// Any throw between here and the successful return must release the
|
|
392
567
|
// archive lock and clean up tmpDir; the caller's `close()` only runs on
|
|
393
568
|
// the happy path. Errors from `close()` itself are intentionally
|
|
@@ -424,9 +599,14 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
424
599
|
scopeMap.set(parsed.hostname, [...existing, parsed]);
|
|
425
600
|
}
|
|
426
601
|
await archive.repromoteExternalPages(scopeMap, archived);
|
|
602
|
+
// Seed the sticky set from prior sessions' confirmed traps so
|
|
603
|
+
// `--append` does not pay the cost of re-discovering them (see
|
|
604
|
+
// `DedupeCapTracker`'s constructor JSDoc).
|
|
605
|
+
const preloadedStickyShapeKeys = await archive.listDedupeCapShapeKeys();
|
|
427
606
|
const orchestrator = new CrawlerOrchestrator(archive, {
|
|
428
607
|
...mergedConfig,
|
|
429
608
|
roots: mergedRoots,
|
|
609
|
+
preloadedStickyShapeKeys,
|
|
430
610
|
});
|
|
431
611
|
const { scraped, pending } = await archive.getCrawlingState();
|
|
432
612
|
const resources = await archive.getResourceUrlList();
|
|
@@ -481,22 +661,29 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
481
661
|
* 3. Reject archives with unfinished `pending` URLs — those would inherit
|
|
482
662
|
* the inventory `source` label by mistake. Operator must resume /
|
|
483
663
|
* retry-failed first.
|
|
484
|
-
* 4.
|
|
485
|
-
*
|
|
486
|
-
*
|
|
664
|
+
* 4. If `source` is given, archive its exact bytes under
|
|
665
|
+
* `inventory/<sha256>.txt` (see {@link Archive.saveInventorySourceList}).
|
|
666
|
+
* Done before scope classification so even a run that discards every
|
|
667
|
+
* URL (out of scope or already known) still leaves a recoverable
|
|
668
|
+
* copy of what was fed in.
|
|
669
|
+
* 5. Parse the URL list — the CLI has already warned-and-dropped
|
|
670
|
+
* unparseable-URL lines before calling this method, so every
|
|
671
|
+
* remaining entry parses. Anything outside the archived scope is
|
|
672
|
+
* warned and skipped (inventory is per-server by design).
|
|
673
|
+
* 6. Subtract URLs that already exist in `pages` or `resources` so the
|
|
487
674
|
* second (and N-th) inventory pass is a no-op for known rows — keeps
|
|
488
675
|
* `'inventory-seed'` rows from being silently demoted.
|
|
489
|
-
*
|
|
676
|
+
* 7. Make `<archive>.bak`. Anything thrown beyond this point restores
|
|
490
677
|
* from the backup.
|
|
491
|
-
*
|
|
678
|
+
* 8. HEAD-probe each novel URL. Responses classified as HTML are queued
|
|
492
679
|
* as Crawler seeds (`'inventory-seed'`); everything else is recorded
|
|
493
680
|
* in `resources` directly as `'inventory-seed'` (no browser launch).
|
|
494
|
-
*
|
|
681
|
+
* 9. If any HTML seeds exist, start a Crawler with
|
|
495
682
|
* `inventoryMode = { seedUrls }` so the rendered page and every newly
|
|
496
683
|
* discovered downstream link is labelled correctly. `resume` is fed
|
|
497
684
|
* the existing `scraped` / `resources` sets so links into already-
|
|
498
685
|
* crawled pages stop at the seen-gate without re-rendering.
|
|
499
|
-
*
|
|
686
|
+
* 10. Drop the backup on success; restore it on any throw.
|
|
500
687
|
*
|
|
501
688
|
* Mutually exclusive with `--append` / `--retry-failed` / `--resume` /
|
|
502
689
|
* `--diff` / `--list` / `--list-file` / `--single` / `--output` — the
|
|
@@ -506,20 +693,22 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
506
693
|
* @param inventoryUrls - Pre-read URL list (one URL per element).
|
|
507
694
|
* @param options - Optional config overrides — most callers leave this blank and let the archived config flow through.
|
|
508
695
|
* @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
|
-
* `
|
|
696
|
+
* @param source - The CLI's already-read source list, as `{ sha256, bytes }`.
|
|
697
|
+
* The orchestrator deliberately does NOT receive the file path: the
|
|
698
|
+
* path is privacy-sensitive (leaks user-home / OS structure when
|
|
699
|
+
* archives are shared) and we want it lifted off this boundary so no
|
|
700
|
+
* future log line / breadcrumb / error message inside the orchestrator
|
|
701
|
+
* can accidentally re-leak it. `bytes` is archived verbatim under
|
|
702
|
+
* `inventory/<sha256>.txt` (see {@link Archive.saveInventorySourceList})
|
|
703
|
+
* before scope classification, so a later `--inventory` run against
|
|
704
|
+
* the same list is an audit no-op even when it discards zero new
|
|
705
|
+
* URLs. Pass `null` for programmatic callers that built
|
|
706
|
+
* `inventoryUrls` in-memory; the audit row's `source_file_sha256`
|
|
707
|
+
* column will be `NULL` and no source list is archived.
|
|
519
708
|
* @returns The orchestrator instance after a successful inventory pass.
|
|
520
709
|
* @throws {Error} When `inventoryUrls` is empty, the archive is in list mode, or pending URLs from a previous crawl remain unresolved.
|
|
521
710
|
*/
|
|
522
|
-
static async inventory(archivePath, inventoryUrls, options, initializedCallback,
|
|
711
|
+
static async inventory(archivePath, inventoryUrls, options, initializedCallback, source = null) {
|
|
523
712
|
if (inventoryUrls.length === 0) {
|
|
524
713
|
throw new Error('inventory: URL list is empty');
|
|
525
714
|
}
|
|
@@ -527,7 +716,13 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
527
716
|
const absFilePath = path.isAbsolute(archivePath)
|
|
528
717
|
? archivePath
|
|
529
718
|
: path.resolve(cwd, archivePath);
|
|
530
|
-
|
|
719
|
+
// See `ArchiveOpenOptions.openPluginData` for why this must be `true`
|
|
720
|
+
// on every writer path that calls `write()`.
|
|
721
|
+
const archive = await Archive.open({
|
|
722
|
+
filePath: absFilePath,
|
|
723
|
+
cwd,
|
|
724
|
+
openPluginData: true,
|
|
725
|
+
});
|
|
531
726
|
try {
|
|
532
727
|
const archived = await archive.getConfig();
|
|
533
728
|
if (archived.fromList) {
|
|
@@ -549,6 +744,14 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
549
744
|
// eslint-disable-next-line no-console -- operator-facing warning, must be visible regardless of DEBUG filters
|
|
550
745
|
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
746
|
}
|
|
747
|
+
// Archive the exact source bytes before scope classification, so
|
|
748
|
+
// even a run that discards every URL (out-of-scope or already
|
|
749
|
+
// known — see `novelUrls.length === 0` below) still leaves a
|
|
750
|
+
// recoverable copy of what was fed in. Content-hash-named, so a
|
|
751
|
+
// second `--inventory` pass with the same list is a no-op write.
|
|
752
|
+
if (source) {
|
|
753
|
+
await archive.saveInventorySourceList(source.sha256, source.bytes);
|
|
754
|
+
}
|
|
552
755
|
// Parse + scope-classify the candidate URLs. sortUrl drops
|
|
553
756
|
// unparseable strings; findScopeEntry separates in-scope from
|
|
554
757
|
// out-of-scope.
|
|
@@ -703,7 +906,8 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
703
906
|
htmlSeedsCount: htmlSeeds.length,
|
|
704
907
|
nonHtmlCount: nonHtmlSeeds.length,
|
|
705
908
|
outOfScope,
|
|
706
|
-
sourceFileSha256,
|
|
909
|
+
sourceFileSha256: source?.sha256 ?? null,
|
|
910
|
+
invalidSkipped: source?.invalidLineCount ?? null,
|
|
707
911
|
});
|
|
708
912
|
// Ingestion's DB writes are now committed. From here on a
|
|
709
913
|
// throw must NOT trigger the `.bak` restore (it would wipe
|
|
@@ -735,6 +939,16 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
735
939
|
inventoryMode: { seedUrls: seedSet },
|
|
736
940
|
};
|
|
737
941
|
if (htmlSeeds.length > 0) {
|
|
942
|
+
// Seed the sticky set from prior sessions' confirmed traps
|
|
943
|
+
// so `--inventory` does not pay the cost of
|
|
944
|
+
// re-discovering them (see `DedupeCapTracker`'s
|
|
945
|
+
// constructor JSDoc). Scoped to this branch only,
|
|
946
|
+
// matching `#preloadDnsBurnedHostCache`'s scoping below —
|
|
947
|
+
// the fallback (non-HTML-only) branch never calls
|
|
948
|
+
// `orchestrator.crawling(...)`, so the tracker is never
|
|
949
|
+
// consulted there.
|
|
950
|
+
orchestratorOptions.preloadedStickyShapeKeys =
|
|
951
|
+
await archive.listDedupeCapShapeKeys();
|
|
738
952
|
const orchestrator = new CrawlerOrchestrator(archive, orchestratorOptions);
|
|
739
953
|
// Re-read pending *after* the pre-insert so the strict-
|
|
740
954
|
// pending set includes the freshly inserted
|
|
@@ -853,7 +1067,13 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
853
1067
|
const absFilePath = path.isAbsolute(archivePath)
|
|
854
1068
|
? archivePath
|
|
855
1069
|
: path.resolve(cwd, archivePath);
|
|
856
|
-
|
|
1070
|
+
// See `ArchiveOpenOptions.openPluginData` for why this must be `true`
|
|
1071
|
+
// on every writer path that calls `write()`.
|
|
1072
|
+
const archive = await Archive.open({
|
|
1073
|
+
filePath: absFilePath,
|
|
1074
|
+
cwd,
|
|
1075
|
+
openPluginData: true,
|
|
1076
|
+
});
|
|
857
1077
|
// Any throw between here and the successful return must release the
|
|
858
1078
|
// archive lock and clean up tmpDir; the caller's `close()` only runs on
|
|
859
1079
|
// the happy path.
|
|
@@ -880,7 +1100,14 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
880
1100
|
log('Start retrying failed pages');
|
|
881
1101
|
log('Archive %s', absFilePath);
|
|
882
1102
|
log('Reset %d failed page(s)', resetUrls.length);
|
|
883
|
-
|
|
1103
|
+
// Seed the sticky set from prior sessions' confirmed traps so
|
|
1104
|
+
// `--retry-failed` does not pay the cost of re-discovering
|
|
1105
|
+
// them (see `DedupeCapTracker`'s constructor JSDoc).
|
|
1106
|
+
const preloadedStickyShapeKeys = await archive.listDedupeCapShapeKeys();
|
|
1107
|
+
const orchestrator = new CrawlerOrchestrator(archive, {
|
|
1108
|
+
...config,
|
|
1109
|
+
preloadedStickyShapeKeys,
|
|
1110
|
+
});
|
|
884
1111
|
const { scraped, pending } = await archive.getCrawlingState();
|
|
885
1112
|
const resources = await archive.getResourceUrlList();
|
|
886
1113
|
const pagesScrapedOffset = await archive.getScrapedHtmlPageCount();
|
|
@@ -929,9 +1156,14 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
929
1156
|
static async resume(stubPath, options, initializedCallback) {
|
|
930
1157
|
const archive = await Archive.resume(stubPath);
|
|
931
1158
|
const archivedConfig = await archive.getConfig();
|
|
1159
|
+
// Seed the sticky set from prior sessions' confirmed traps so
|
|
1160
|
+
// `--resume` does not pay the cost of re-discovering them (see
|
|
1161
|
+
// `DedupeCapTracker`'s constructor JSDoc).
|
|
1162
|
+
const preloadedStickyShapeKeys = await archive.listDedupeCapShapeKeys();
|
|
932
1163
|
const config = {
|
|
933
1164
|
...archivedConfig,
|
|
934
1165
|
...cleanObject(options),
|
|
1166
|
+
preloadedStickyShapeKeys,
|
|
935
1167
|
};
|
|
936
1168
|
const orchestrator = new CrawlerOrchestrator(archive, config);
|
|
937
1169
|
const _url = await archive.getUrl();
|
|
@@ -990,10 +1222,10 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
990
1222
|
* no `--label` flag, so this is always the auto form.
|
|
991
1223
|
* `source_file_sha256` arrives pre-computed via
|
|
992
1224
|
* `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.
|
|
1225
|
+
* `computeFileSha256` against the bytes it read from the input txt,
|
|
1226
|
+
* before the orchestrator was even invoked). The orchestrator boundary
|
|
1227
|
+
* deliberately never sees the absolute path — see
|
|
1228
|
+
* {@link InventoryRunAggregates} for the privacy rationale.
|
|
997
1229
|
*
|
|
998
1230
|
* **Audit-write failures abort the ingestion phase.** Swallowing them
|
|
999
1231
|
* would only be justified if the audit were the last write after the
|
|
@@ -1020,6 +1252,7 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
1020
1252
|
new_pages: aggregates.htmlSeedsCount,
|
|
1021
1253
|
new_resources: aggregates.nonHtmlCount,
|
|
1022
1254
|
scope_skipped: aggregates.outOfScope,
|
|
1255
|
+
invalid_skipped: aggregates.invalidSkipped,
|
|
1023
1256
|
});
|
|
1024
1257
|
}
|
|
1025
1258
|
/**
|
|
@@ -1034,6 +1267,13 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
1034
1267
|
// eslint-disable-next-line no-console
|
|
1035
1268
|
console.error(`[preload] Short-circuited ${skipped} URL(s) on DNS-burned hosts`);
|
|
1036
1269
|
}
|
|
1270
|
+
const { confirmedCount, totalDurationMs } = networkOutageSummaryCounter;
|
|
1271
|
+
if (confirmedCount > 0) {
|
|
1272
|
+
// eslint-disable-next-line no-console
|
|
1273
|
+
console.error(`[network] ${confirmedCount} outage(s), ${Math.round(totalDurationMs / 1000)}s total`);
|
|
1274
|
+
}
|
|
1275
|
+
networkOutageSummaryCounter.confirmedCount = 0;
|
|
1276
|
+
networkOutageSummaryCounter.totalDurationMs = 0;
|
|
1037
1277
|
clearDestinationCache();
|
|
1038
1278
|
clearDnsBurnedHostCache();
|
|
1039
1279
|
}
|
package/lib/crawler.d.ts
CHANGED
|
@@ -16,16 +16,37 @@ 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';
|
|
49
|
+
export { assertChromeIsInstalled } from './crawler/assert-chrome-installed.js';
|
|
29
50
|
export { computeFileSha256 } from './utils/compute-file-sha256.js';
|
|
30
51
|
export { populateEntityTables } from './archive/populate-entity-tables/populate-entities.js';
|
|
31
52
|
export type { PageDomPathResolver } from './archive/populate-entity-tables/populate-image-items.js';
|
|
@@ -38,3 +59,4 @@ export { populateBlobRefs } from './archive/populate-ref-tables/populate-blob-re
|
|
|
38
59
|
export { populateHeaderTables } from './archive/populate-ref-tables/populate-header-tables.js';
|
|
39
60
|
export { loadResponseHeadersBySetIds } from './archive/db-ops/_shared/load-response-headers-by-set-ids.js';
|
|
40
61
|
export { decodeJsonRef } from './archive/db-ops/_shared/decode-json-ref.js';
|
|
62
|
+
export type { TemplateClusterBlockingEvidence, TemplateClusterBlockingReason, TemplateClusterLandmarkProfile, TemplateClusterLandmarkType, TemplateClusterReason, } from './archive/db-ops/analysis/types.js';
|
package/lib/crawler.js
CHANGED
|
@@ -17,15 +17,33 @@ 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';
|
|
46
|
+
export { assertChromeIsInstalled } from './crawler/assert-chrome-installed.js';
|
|
29
47
|
export { computeFileSha256 } from './utils/compute-file-sha256.js';
|
|
30
48
|
// 0.13 ref-table population (issue #191, epic #103). Exposed as the
|
|
31
49
|
// 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>;
|