@nitpicker/crawler 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/archive/archive-accessor.d.ts +56 -0
- package/lib/archive/archive-accessor.js +72 -0
- package/lib/archive/archive.d.ts +108 -9
- package/lib/archive/archive.js +109 -7
- package/lib/archive/body-hash/compute-body-hash.d.ts +22 -0
- package/lib/archive/body-hash/compute-body-hash.js +31 -0
- package/lib/archive/body-hash/extract-body.d.ts +18 -0
- package/lib/archive/body-hash/extract-body.js +29 -0
- package/lib/archive/body-hash/mask-dynamic-ids.d.ts +33 -0
- package/lib/archive/body-hash/mask-dynamic-ids.js +44 -0
- package/lib/archive/body-hash/normalize-url-like-strings.d.ts +16 -0
- package/lib/archive/body-hash/normalize-url-like-strings.js +19 -0
- package/lib/archive/cache/clear-archive-cache-entry.d.ts +19 -0
- package/lib/archive/cache/clear-archive-cache-entry.js +25 -0
- package/lib/archive/cache/clear-archive-cache-root.d.ts +26 -0
- package/lib/archive/cache/clear-archive-cache-root.js +32 -0
- package/lib/archive/cache/list-archive-cache-entries.d.ts +23 -0
- package/lib/archive/cache/list-archive-cache-entries.js +125 -0
- package/lib/archive/cache/path-exists.d.ts +9 -0
- package/lib/archive/cache/path-exists.js +18 -0
- package/lib/archive/cache/types.d.ts +33 -0
- package/lib/archive/cache/types.js +1 -0
- package/lib/archive/create-adjunct-tables.d.ts +18 -0
- package/lib/archive/create-adjunct-tables.js +277 -0
- package/lib/archive/create-entity-tables.d.ts +14 -0
- package/lib/archive/create-entity-tables.js +52 -1
- package/lib/archive/database.d.ts +91 -2
- package/lib/archive/database.js +128 -0
- package/lib/archive/db-ops/_shared/clear-write-ref-caches.js +1 -0
- package/lib/archive/db-ops/_shared/compress-payload.d.ts +26 -0
- package/lib/archive/db-ops/_shared/compress-payload.js +30 -0
- package/lib/archive/db-ops/_shared/create-write-ref-caches.js +1 -0
- package/lib/archive/db-ops/_shared/types.d.ts +2 -0
- package/lib/archive/db-ops/_shared/upsert-json-ref.js +3 -3
- package/lib/archive/db-ops/analysis/replace-page-templates.d.ts +29 -0
- package/lib/archive/db-ops/analysis/replace-page-templates.js +84 -0
- package/lib/archive/db-ops/analysis/types.d.ts +61 -0
- package/lib/archive/db-ops/analysis/types.js +1 -0
- package/lib/archive/db-ops/config/info-column-allowlist.js +1 -0
- package/lib/archive/db-ops/console-logs/compute-console-log-hash.d.ts +19 -0
- package/lib/archive/db-ops/console-logs/compute-console-log-hash.js +30 -0
- package/lib/archive/db-ops/console-logs/replace-console-logs.d.ts +34 -0
- package/lib/archive/db-ops/console-logs/replace-console-logs.js +123 -0
- package/lib/archive/db-ops/console-logs/stringify-console-log-args.d.ts +16 -0
- package/lib/archive/db-ops/console-logs/stringify-console-log-args.js +26 -0
- package/lib/archive/db-ops/console-logs/types.d.ts +38 -0
- package/lib/archive/db-ops/console-logs/types.js +1 -0
- package/lib/archive/db-ops/console-logs/upsert-console-log-item.d.ts +29 -0
- package/lib/archive/db-ops/console-logs/upsert-console-log-item.js +52 -0
- package/lib/archive/db-ops/errors/list-dns-burned-host-candidates.d.ts +11 -0
- package/lib/archive/db-ops/errors/list-dns-burned-host-candidates.js +21 -4
- package/lib/archive/db-ops/inventory/record-inventory-run.js +1 -0
- package/lib/archive/db-ops/lifecycle/init.d.ts +25 -10
- package/lib/archive/db-ops/lifecycle/init.js +39 -10
- package/lib/archive/db-ops/meta/get-audios-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-audios-of-page.js +15 -0
- package/lib/archive/db-ops/meta/get-buttons-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-buttons-of-page.js +15 -0
- package/lib/archive/db-ops/meta/get-canvases-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-canvases-of-page.js +15 -0
- package/lib/archive/db-ops/meta/get-headings-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-headings-of-page.js +15 -0
- package/lib/archive/db-ops/meta/get-iframes-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-iframes-of-page.js +15 -0
- package/lib/archive/db-ops/meta/get-main-content-images-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-main-content-images-of-page.js +15 -0
- package/lib/archive/db-ops/meta/get-main-content-tables-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-main-content-tables-of-page.js +15 -0
- package/lib/archive/db-ops/meta/get-videos-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-videos-of-page.js +15 -0
- package/lib/archive/db-ops/outages/close-network-outage.d.ts +12 -0
- package/lib/archive/db-ops/outages/close-network-outage.js +15 -0
- package/lib/archive/db-ops/outages/close-stale-open-network-outages.d.ts +26 -0
- package/lib/archive/db-ops/outages/close-stale-open-network-outages.js +42 -0
- package/lib/archive/db-ops/outages/compute-outage-clamp-timestamp.d.ts +26 -0
- package/lib/archive/db-ops/outages/compute-outage-clamp-timestamp.js +31 -0
- package/lib/archive/db-ops/outages/insert-network-outage.d.ts +14 -0
- package/lib/archive/db-ops/outages/insert-network-outage.js +29 -0
- package/lib/archive/db-ops/outages/list-network-outages.d.ts +28 -0
- package/lib/archive/db-ops/outages/list-network-outages.js +42 -0
- package/lib/archive/db-ops/pages/read/build-page-query.js +1 -1
- package/lib/archive/db-ops/pages/reset/repromote-external-pages.js +19 -10
- package/lib/archive/db-ops/pages/reset/reset-failed-pages.d.ts +18 -3
- package/lib/archive/db-ops/pages/reset/reset-failed-pages.js +48 -10
- package/lib/archive/db-ops/pages/write/insert-audios.d.ts +14 -0
- package/lib/archive/db-ops/pages/write/insert-audios.js +25 -0
- package/lib/archive/db-ops/pages/write/insert-buttons.d.ts +14 -0
- package/lib/archive/db-ops/pages/write/insert-buttons.js +29 -0
- package/lib/archive/db-ops/pages/write/insert-canvases.d.ts +14 -0
- package/lib/archive/db-ops/pages/write/insert-canvases.js +26 -0
- package/lib/archive/db-ops/pages/write/insert-headings.d.ts +15 -0
- package/lib/archive/db-ops/pages/write/insert-headings.js +27 -0
- package/lib/archive/db-ops/pages/write/insert-iframes.d.ts +14 -0
- package/lib/archive/db-ops/pages/write/insert-iframes.js +28 -0
- package/lib/archive/db-ops/pages/write/insert-main-content-images.d.ts +20 -0
- package/lib/archive/db-ops/pages/write/insert-main-content-images.js +32 -0
- package/lib/archive/db-ops/pages/write/insert-main-content-tables.d.ts +14 -0
- package/lib/archive/db-ops/pages/write/insert-main-content-tables.js +29 -0
- package/lib/archive/db-ops/pages/write/insert-page.js +15 -2
- package/lib/archive/db-ops/pages/write/insert-videos.d.ts +14 -0
- package/lib/archive/db-ops/pages/write/insert-videos.js +28 -0
- package/lib/archive/db-ops/pages/write/update-page.js +33 -0
- package/lib/archive/db-ops/pages/write/write-page-html-blob.js +6 -6
- package/lib/archive/filesystem/output-binary.d.ts +12 -0
- package/lib/archive/filesystem/output-binary.js +17 -0
- package/lib/archive/get-failed-page-messages.d.ts +20 -8
- package/lib/archive/get-failed-page-messages.js +29 -15
- package/lib/archive/init-schema.js +2 -1
- package/lib/archive/meta/compute-main-contents-denormalized.d.ts +22 -0
- package/lib/archive/meta/compute-main-contents-denormalized.js +63 -0
- package/lib/archive/meta/types.d.ts +263 -0
- package/lib/archive/migrate-content-items-alias-of-id.d.ts +47 -0
- package/lib/archive/migrate-content-items-alias-of-id.js +58 -0
- package/lib/archive/migrate-info-main-content-selector.d.ts +11 -0
- package/lib/archive/migrate-info-main-content-selector.js +24 -0
- package/lib/archive/migrate-inventory-runs-invalid-skipped.d.ts +11 -0
- package/lib/archive/migrate-inventory-runs-invalid-skipped.js +24 -0
- package/lib/archive/migrate-main-contents-columns.d.ts +21 -0
- package/lib/archive/migrate-main-contents-columns.js +50 -0
- package/lib/archive/migrate-page-meta-body-hash.d.ts +39 -0
- package/lib/archive/migrate-page-meta-body-hash.js +52 -0
- package/lib/archive/migrate-page-meta-console-error-count.d.ts +22 -0
- package/lib/archive/migrate-page-meta-console-error-count.js +34 -0
- package/lib/archive/page.d.ts +145 -3
- package/lib/archive/page.js +195 -0
- package/lib/archive/types.d.ts +86 -2
- package/lib/archive/url-alias/compute-tier-a-alias-key.d.ts +31 -0
- package/lib/archive/url-alias/compute-tier-a-alias-key.js +36 -0
- package/lib/archive/url-alias/compute-tier-b-alias-key.d.ts +19 -0
- package/lib/archive/url-alias/compute-tier-b-alias-key.js +30 -0
- package/lib/archive/url-alias/format-alias-key.d.ts +13 -0
- package/lib/archive/url-alias/format-alias-key.js +14 -0
- package/lib/archive/url-alias/parse-alias-key-parts.d.ts +15 -0
- package/lib/archive/url-alias/parse-alias-key-parts.js +40 -0
- package/lib/archive/url-alias/types.d.ts +7 -0
- package/lib/archive/url-alias/types.js +1 -0
- package/lib/crawler/choose-probe-host.d.ts +24 -0
- package/lib/crawler/choose-probe-host.js +38 -0
- package/lib/crawler/clear-dns-burned-host-cache.js +2 -0
- package/lib/crawler/crawler.js +261 -2
- package/lib/crawler/dns-burned-host-burn-timestamps.d.ts +17 -0
- package/lib/crawler/dns-burned-host-burn-timestamps.js +17 -0
- package/lib/crawler/evict-network-classified-destination-cache-entries.d.ts +26 -0
- package/lib/crawler/evict-network-classified-destination-cache-entries.js +34 -0
- package/lib/crawler/evict-outage-tainted-dns-burns.d.ts +23 -0
- package/lib/crawler/evict-outage-tainted-dns-burns.js +26 -0
- package/lib/crawler/fetch-destination.js +2 -0
- package/lib/crawler/link-to-page-data.js +2 -0
- package/lib/crawler/network-gate.d.ts +49 -0
- package/lib/crawler/network-gate.js +78 -0
- package/lib/crawler/network-outage-detector.d.ts +51 -0
- package/lib/crawler/network-outage-detector.js +81 -0
- package/lib/crawler/network-outage-summary-counter.d.ts +21 -0
- package/lib/crawler/network-outage-summary-counter.js +18 -0
- package/lib/crawler/probe-network.d.ts +26 -0
- package/lib/crawler/probe-network.js +21 -0
- package/lib/crawler/resource-to-page-data.js +2 -0
- package/lib/crawler/types.d.ts +152 -1
- package/lib/crawler-orchestrator.d.ts +62 -18
- package/lib/crawler-orchestrator.js +163 -26
- package/lib/crawler.d.ts +21 -0
- package/lib/crawler.js +17 -0
- package/lib/is-within-outage-window.d.ts +49 -0
- package/lib/is-within-outage-window.js +33 -0
- package/lib/network-related-error-kinds.d.ts +23 -0
- package/lib/network-related-error-kinds.js +28 -0
- package/lib/types.d.ts +9 -1
- package/lib/utils/array/each-splitted.d.ts +1 -1
- package/lib/utils/compute-file-sha256.d.ts +17 -17
- package/lib/utils/compute-file-sha256.js +18 -47
- package/package.json +3 -3
|
@@ -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
|
/**
|
|
@@ -109,6 +121,7 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
109
121
|
verbose: options?.verbose ?? false,
|
|
110
122
|
userAgent: options?.userAgent || defaultUserAgent,
|
|
111
123
|
ignoreRobots: options?.ignoreRobots ?? false,
|
|
124
|
+
mainContentSelector: options?.mainContentSelector ?? null,
|
|
112
125
|
// Let the crawler reuse sub-resource data captured during page
|
|
113
126
|
// rendering instead of issuing a redundant HEAD pre-flight.
|
|
114
127
|
lookupResource: async (urls) => {
|
|
@@ -137,6 +150,14 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
137
150
|
// rows continue to land in pages/resources with the DB DEFAULT
|
|
138
151
|
// `'crawled'` provenance label.
|
|
139
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,
|
|
140
161
|
});
|
|
141
162
|
}
|
|
142
163
|
/**
|
|
@@ -165,6 +186,11 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
165
186
|
*/
|
|
166
187
|
async crawling(list, opts) {
|
|
167
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;
|
|
168
194
|
return new Promise((resolve, reject) => {
|
|
169
195
|
this.#crawler.on('error', (error) => {
|
|
170
196
|
if (error.error instanceof PreloadShortCircuitError) {
|
|
@@ -211,6 +237,67 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
211
237
|
.catch((error) => reject(error));
|
|
212
238
|
void this.emit('redirect', { result });
|
|
213
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
|
+
});
|
|
214
301
|
this.#crawler.on('response', ({ resource, source }) => {
|
|
215
302
|
writeQueue
|
|
216
303
|
.enqueue(() => this.#archive.setResources(resource, source))
|
|
@@ -221,6 +308,11 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
221
308
|
.enqueue(() => this.#archive.setResourcesReferrers(resource))
|
|
222
309
|
.catch((error) => reject(error));
|
|
223
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
|
+
});
|
|
224
316
|
this.#crawler.on('crawlEnd', () => {
|
|
225
317
|
writeQueue
|
|
226
318
|
.drain()
|
|
@@ -337,6 +429,7 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
337
429
|
disableQueries,
|
|
338
430
|
userAgent: options?.userAgent || defaultUserAgent,
|
|
339
431
|
ignoreRobots: options?.ignoreRobots ?? false,
|
|
432
|
+
mainContentSelector: options?.mainContentSelector ?? null,
|
|
340
433
|
});
|
|
341
434
|
const orchestrator = new CrawlerOrchestrator(archive, {
|
|
342
435
|
...options,
|
|
@@ -385,7 +478,13 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
385
478
|
const absFilePath = path.isAbsolute(archivePath)
|
|
386
479
|
? archivePath
|
|
387
480
|
: path.resolve(cwd, archivePath);
|
|
388
|
-
|
|
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
|
+
});
|
|
389
488
|
// Any throw between here and the successful return must release the
|
|
390
489
|
// archive lock and clean up tmpDir; the caller's `close()` only runs on
|
|
391
490
|
// the happy path. Errors from `close()` itself are intentionally
|
|
@@ -479,22 +578,29 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
479
578
|
* 3. Reject archives with unfinished `pending` URLs — those would inherit
|
|
480
579
|
* the inventory `source` label by mistake. Operator must resume /
|
|
481
580
|
* retry-failed first.
|
|
482
|
-
* 4.
|
|
483
|
-
*
|
|
484
|
-
*
|
|
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
|
|
485
591
|
* second (and N-th) inventory pass is a no-op for known rows — keeps
|
|
486
592
|
* `'inventory-seed'` rows from being silently demoted.
|
|
487
|
-
*
|
|
593
|
+
* 7. Make `<archive>.bak`. Anything thrown beyond this point restores
|
|
488
594
|
* from the backup.
|
|
489
|
-
*
|
|
595
|
+
* 8. HEAD-probe each novel URL. Responses classified as HTML are queued
|
|
490
596
|
* as Crawler seeds (`'inventory-seed'`); everything else is recorded
|
|
491
597
|
* in `resources` directly as `'inventory-seed'` (no browser launch).
|
|
492
|
-
*
|
|
598
|
+
* 9. If any HTML seeds exist, start a Crawler with
|
|
493
599
|
* `inventoryMode = { seedUrls }` so the rendered page and every newly
|
|
494
600
|
* discovered downstream link is labelled correctly. `resume` is fed
|
|
495
601
|
* the existing `scraped` / `resources` sets so links into already-
|
|
496
602
|
* crawled pages stop at the seen-gate without re-rendering.
|
|
497
|
-
*
|
|
603
|
+
* 10. Drop the backup on success; restore it on any throw.
|
|
498
604
|
*
|
|
499
605
|
* Mutually exclusive with `--append` / `--retry-failed` / `--resume` /
|
|
500
606
|
* `--diff` / `--list` / `--list-file` / `--single` / `--output` — the
|
|
@@ -504,20 +610,22 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
504
610
|
* @param inventoryUrls - Pre-read URL list (one URL per element).
|
|
505
611
|
* @param options - Optional config overrides — most callers leave this blank and let the archived config flow through.
|
|
506
612
|
* @param initializedCallback - Hook invoked once the orchestrator is constructed but before `crawling` runs (the CLI uses it to attach progress reporting).
|
|
507
|
-
* @param
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
* `
|
|
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.
|
|
517
625
|
* @returns The orchestrator instance after a successful inventory pass.
|
|
518
626
|
* @throws {Error} When `inventoryUrls` is empty, the archive is in list mode, or pending URLs from a previous crawl remain unresolved.
|
|
519
627
|
*/
|
|
520
|
-
static async inventory(archivePath, inventoryUrls, options, initializedCallback,
|
|
628
|
+
static async inventory(archivePath, inventoryUrls, options, initializedCallback, source = null) {
|
|
521
629
|
if (inventoryUrls.length === 0) {
|
|
522
630
|
throw new Error('inventory: URL list is empty');
|
|
523
631
|
}
|
|
@@ -525,7 +633,13 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
525
633
|
const absFilePath = path.isAbsolute(archivePath)
|
|
526
634
|
? archivePath
|
|
527
635
|
: path.resolve(cwd, archivePath);
|
|
528
|
-
|
|
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
|
+
});
|
|
529
643
|
try {
|
|
530
644
|
const archived = await archive.getConfig();
|
|
531
645
|
if (archived.fromList) {
|
|
@@ -547,6 +661,14 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
547
661
|
// eslint-disable-next-line no-console -- operator-facing warning, must be visible regardless of DEBUG filters
|
|
548
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.`);
|
|
549
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
|
+
}
|
|
550
672
|
// Parse + scope-classify the candidate URLs. sortUrl drops
|
|
551
673
|
// unparseable strings; findScopeEntry separates in-scope from
|
|
552
674
|
// out-of-scope.
|
|
@@ -701,7 +823,8 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
701
823
|
htmlSeedsCount: htmlSeeds.length,
|
|
702
824
|
nonHtmlCount: nonHtmlSeeds.length,
|
|
703
825
|
outOfScope,
|
|
704
|
-
sourceFileSha256,
|
|
826
|
+
sourceFileSha256: source?.sha256 ?? null,
|
|
827
|
+
invalidSkipped: source?.invalidLineCount ?? null,
|
|
705
828
|
});
|
|
706
829
|
// Ingestion's DB writes are now committed. From here on a
|
|
707
830
|
// throw must NOT trigger the `.bak` restore (it would wipe
|
|
@@ -851,7 +974,13 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
851
974
|
const absFilePath = path.isAbsolute(archivePath)
|
|
852
975
|
? archivePath
|
|
853
976
|
: path.resolve(cwd, archivePath);
|
|
854
|
-
|
|
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
|
+
});
|
|
855
984
|
// Any throw between here and the successful return must release the
|
|
856
985
|
// archive lock and clean up tmpDir; the caller's `close()` only runs on
|
|
857
986
|
// the happy path.
|
|
@@ -988,10 +1117,10 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
988
1117
|
* no `--label` flag, so this is always the auto form.
|
|
989
1118
|
* `source_file_sha256` arrives pre-computed via
|
|
990
1119
|
* `aggregates.sourceFileSha256` (the CLI's `inventoryCrawl` ran
|
|
991
|
-
* `computeFileSha256` against the
|
|
992
|
-
* was even invoked). The orchestrator boundary
|
|
993
|
-
* the absolute path — see
|
|
994
|
-
* 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.
|
|
995
1124
|
*
|
|
996
1125
|
* **Audit-write failures abort the ingestion phase.** Swallowing them
|
|
997
1126
|
* would only be justified if the audit were the last write after the
|
|
@@ -1018,6 +1147,7 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
1018
1147
|
new_pages: aggregates.htmlSeedsCount,
|
|
1019
1148
|
new_resources: aggregates.nonHtmlCount,
|
|
1020
1149
|
scope_skipped: aggregates.outOfScope,
|
|
1150
|
+
invalid_skipped: aggregates.invalidSkipped,
|
|
1021
1151
|
});
|
|
1022
1152
|
}
|
|
1023
1153
|
/**
|
|
@@ -1032,6 +1162,13 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
1032
1162
|
// eslint-disable-next-line no-console
|
|
1033
1163
|
console.error(`[preload] Short-circuited ${skipped} URL(s) on DNS-burned hosts`);
|
|
1034
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;
|
|
1035
1172
|
clearDestinationCache();
|
|
1036
1173
|
clearDnsBurnedHostCache();
|
|
1037
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>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ErrorKind`s that plausibly indicate trouble with the operator's own
|
|
3
|
+
* network path rather than a genuinely dead or misconfigured target site.
|
|
4
|
+
*
|
|
5
|
+
* Single source of truth shared by two consumers that must agree on
|
|
6
|
+
* exactly the same set:
|
|
7
|
+
*
|
|
8
|
+
* - `NetworkOutageDetector` — only these kinds contribute to the sliding
|
|
9
|
+
* window that detects a suspect outage.
|
|
10
|
+
* - `evict-network-classified-destination-cache-entries.ts` — on recovery,
|
|
11
|
+
* only `destinationCache` entries whose cached `Error` classifies into
|
|
12
|
+
* one of these kinds are evicted (a cached `tls` or `client-blocked`
|
|
13
|
+
* failure is a site-specific fact and must survive an outage recovery
|
|
14
|
+
* unrelated to it).
|
|
15
|
+
*
|
|
16
|
+
* `dns` is included despite `permanent-error-kinds.ts` treating it as a
|
|
17
|
+
* permanent, site-specific verdict in isolation — that classification is
|
|
18
|
+
* exactly what outage detection exists to override when the surrounding
|
|
19
|
+
* evidence (many hosts, tight time window) points to the local network
|
|
20
|
+
* instead.
|
|
21
|
+
*/
|
|
22
|
+
export const NETWORK_RELATED_ERROR_KINDS = new Set([
|
|
23
|
+
'dns',
|
|
24
|
+
'dns-transient',
|
|
25
|
+
'local-network',
|
|
26
|
+
'connection-timeout',
|
|
27
|
+
'connection-reset',
|
|
28
|
+
]);
|
package/lib/types.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { CrawlerError, PageData } from './utils/types/types.js';
|
|
|
10
10
|
* documented per-property rather than scattered across the two emit sites.
|
|
11
11
|
*/
|
|
12
12
|
export interface InventoryRunAggregates {
|
|
13
|
-
/**
|
|
13
|
+
/** `inventoryUrls.length` as received by `CrawlerOrchestrator.inventory` — the CLI (`inventoryCrawl`) has already warned-and-dropped unparseable-URL lines before this point, so this counts valid URLs, not raw source-file lines. Stored verbatim as `inventory_runs.total_lines`. */
|
|
14
14
|
inventoryUrlsCount: number;
|
|
15
15
|
/** Number of novel URLs classified as HTML and queued for render. Stored as `new_pages` (excludes anchor-discovered descendants — those add later via the crawler graph and are NOT counted here). */
|
|
16
16
|
htmlSeedsCount: number;
|
|
@@ -32,6 +32,14 @@ export interface InventoryRunAggregates {
|
|
|
32
32
|
* `source_file_sha256` will be `NULL`.
|
|
33
33
|
*/
|
|
34
34
|
sourceFileSha256: string | null;
|
|
35
|
+
/**
|
|
36
|
+
* Number of source-file lines the CLI warned-and-dropped for failing
|
|
37
|
+
* URL validation, before `inventoryUrlsCount` was ever counted. Stored
|
|
38
|
+
* verbatim as `inventory_runs.invalid_skipped`. `null` for programmatic
|
|
39
|
+
* callers that built `inventoryUrls` in-memory — there is no source
|
|
40
|
+
* file, so no line was ever dropped as invalid.
|
|
41
|
+
*/
|
|
42
|
+
invalidSkipped: number | null;
|
|
35
43
|
}
|
|
36
44
|
/**
|
|
37
45
|
* Coarse cause of a crawl/scrape failure.
|
|
@@ -7,4 +7,4 @@
|
|
|
7
7
|
* @param callback - A function to invoke on each chunk. May be synchronous or asynchronous.
|
|
8
8
|
* @returns A promise that resolves when all chunk callbacks have completed.
|
|
9
9
|
*/
|
|
10
|
-
export declare function eachSplitted<T>(a: T[], count: number, callback: (items: T[]) => void | Promise<void>): Promise<void>;
|
|
10
|
+
export declare function eachSplitted<T>(a: readonly T[], count: number, callback: (items: T[]) => void | Promise<void>): Promise<void>;
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Compute the SHA-256 digest of
|
|
3
|
-
* O(1) regardless of file size.
|
|
2
|
+
* Compute the SHA-256 digest of an in-memory byte buffer.
|
|
4
3
|
*
|
|
5
|
-
* Used by `CrawlerOrchestrator.inventory` to fingerprint the source
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Used by `CrawlerOrchestrator.inventory` to fingerprint the source URL
|
|
5
|
+
* list — the digest is both recorded on the `inventory_runs` audit row
|
|
6
|
+
* (the content-identity key for "was this exact list applied before") and
|
|
7
|
+
* used as the file name under which the raw list is archived
|
|
8
|
+
* (`Archive.saveInventorySourceList`).
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* @param
|
|
17
|
-
* @returns Lower-case hex digest (64 chars)
|
|
10
|
+
* Takes an already-read `Buffer` rather than a file path: the CLI reads the
|
|
11
|
+
* source file exactly once (`inventoryCrawl`) and derives the digest, the
|
|
12
|
+
* parsed URL list, and the archived copy all from that single buffer. A
|
|
13
|
+
* separate read-then-hash pass would let the file change between the two
|
|
14
|
+
* reads and desync the archived bytes from the hash naming them.
|
|
15
|
+
* @param bytes - The exact bytes to hash.
|
|
16
|
+
* @returns Lower-case hex digest (64 chars).
|
|
18
17
|
* @example
|
|
19
18
|
* ```ts
|
|
20
|
-
* const
|
|
21
|
-
*
|
|
19
|
+
* const bytes = await fs.readFile('/tmp/list.txt');
|
|
20
|
+
* const sha = computeFileSha256(bytes);
|
|
21
|
+
* console.log(sha); // 64-char hex string
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
|
-
export declare function computeFileSha256(
|
|
24
|
+
export declare function computeFileSha256(bytes: Buffer): string;
|