@nitpicker/crawler 0.18.1 → 0.19.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/append-retry-failed-common-setup-phases.d.ts +15 -0
- package/lib/append-retry-failed-common-setup-phases.js +21 -0
- package/lib/append-setup-phases.d.ts +17 -0
- package/lib/append-setup-phases.js +22 -0
- package/lib/archive/archive-accessor.d.ts +31 -5
- package/lib/archive/archive-accessor.js +46 -24
- package/lib/archive/archive.d.ts +115 -37
- package/lib/archive/archive.js +118 -52
- package/lib/archive/cache/extract-archive-to-cache.d.ts +10 -1
- package/lib/archive/cache/extract-archive-to-cache.js +14 -4
- package/lib/archive/create-adjunct-tables.d.ts +12 -2
- package/lib/archive/create-adjunct-tables.js +91 -29
- package/lib/archive/create-entity-tables.d.ts +5 -1
- package/lib/archive/create-entity-tables.js +6 -1
- package/lib/archive/database.d.ts +39 -9
- package/lib/archive/database.js +63 -18
- package/lib/archive/db-ops/lifecycle/init.d.ts +14 -2
- package/lib/archive/db-ops/lifecycle/init.js +29 -11
- package/lib/archive/db-ops/meta/get-custom-elements-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-custom-elements-of-page.js +15 -0
- package/lib/archive/db-ops/meta/get-page-technologies-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-page-technologies-of-page.js +15 -0
- package/lib/archive/db-ops/meta/get-technology-signals-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-technology-signals-of-page.js +15 -0
- package/lib/archive/db-ops/pages/order/set-url-order.d.ts +6 -1
- package/lib/archive/db-ops/pages/order/set-url-order.js +7 -1
- package/lib/archive/db-ops/pages/read/build-page-query.js +1 -1
- package/lib/archive/db-ops/pages/reset/repromote-external-pages.d.ts +6 -1
- package/lib/archive/db-ops/pages/reset/repromote-external-pages.js +12 -3
- package/lib/archive/db-ops/pages/reset/reset-failed-pages.d.ts +6 -1
- package/lib/archive/db-ops/pages/reset/reset-failed-pages.js +10 -2
- package/lib/archive/db-ops/pages/write/insert-custom-elements.d.ts +27 -0
- package/lib/archive/db-ops/pages/write/insert-custom-elements.js +40 -0
- package/lib/archive/db-ops/pages/write/insert-page.d.ts +4 -4
- package/lib/archive/db-ops/pages/write/insert-page.js +3 -2
- package/lib/archive/db-ops/pages/write/insert-technologies.d.ts +25 -0
- package/lib/archive/db-ops/pages/write/insert-technologies.js +45 -0
- package/lib/archive/db-ops/pages/write/update-page.d.ts +4 -1
- package/lib/archive/db-ops/pages/write/update-page.js +38 -16
- package/lib/archive/db-ops/resources/get-resource-url-list.d.ts +12 -1
- package/lib/archive/db-ops/resources/get-resource-url-list.js +43 -5
- package/lib/archive/filesystem/copy-file-with-progress.d.ts +20 -0
- package/lib/archive/filesystem/copy-file-with-progress.js +34 -0
- package/lib/archive/filesystem/parse-pax-path.d.ts +18 -0
- package/lib/archive/filesystem/parse-pax-path.js +47 -0
- package/lib/archive/filesystem/parse-tar-size-field.d.ts +13 -0
- package/lib/archive/filesystem/parse-tar-size-field.js +40 -0
- package/lib/archive/filesystem/peek-tar-top-dir.d.ts +10 -3
- package/lib/archive/filesystem/peek-tar-top-dir.js +166 -16
- package/lib/archive/filesystem/tar.d.ts +13 -1
- package/lib/archive/filesystem/tar.js +63 -7
- package/lib/archive/filesystem/untar.d.ts +13 -0
- package/lib/archive/filesystem/untar.js +68 -8
- package/lib/archive/init-schema.d.ts +1 -1
- package/lib/archive/init-schema.js +6 -4
- package/lib/archive/meta/compute-main-contents-denormalized.d.ts +13 -2
- package/lib/archive/meta/compute-main-contents-denormalized.js +15 -2
- package/lib/archive/meta/technologies/combine-technology-confidence.d.ts +46 -0
- package/lib/archive/meta/technologies/combine-technology-confidence.js +93 -0
- package/lib/archive/meta/technologies/convert-legacy-page-tags-to-inserts.d.ts +45 -0
- package/lib/archive/meta/technologies/convert-legacy-page-tags-to-inserts.js +53 -0
- package/lib/archive/meta/technologies/convert-tag-row-to-wappalyzer-signal.d.ts +33 -0
- package/lib/archive/meta/technologies/convert-tag-row-to-wappalyzer-signal.js +46 -0
- package/lib/archive/meta/technologies/extract-technologies-for-archive.d.ts +30 -0
- package/lib/archive/meta/technologies/extract-technologies-for-archive.js +27 -0
- package/lib/archive/meta/technologies/match-structural-technology-signals.d.ts +13 -0
- package/lib/archive/meta/technologies/match-structural-technology-signals.js +44 -0
- package/lib/archive/meta/technologies/normalize-wappalyzer-entries.d.ts +25 -0
- package/lib/archive/meta/technologies/normalize-wappalyzer-entries.js +35 -0
- package/lib/archive/meta/technologies/technology-signal-definitions.d.ts +43 -0
- package/lib/archive/meta/technologies/technology-signal-definitions.js +193 -0
- package/lib/archive/meta/technologies/types.d.ts +68 -0
- package/lib/archive/meta/technologies/types.js +13 -0
- package/lib/archive/meta/types.d.ts +97 -30
- package/lib/archive/migrate-content-items-alias-of-id.d.ts +6 -1
- package/lib/archive/migrate-content-items-alias-of-id.js +14 -3
- package/lib/archive/migrate-content-items-dedupe-cap-event-id.d.ts +6 -1
- package/lib/archive/migrate-content-items-dedupe-cap-event-id.js +14 -3
- package/lib/archive/migrate-info-main-content-selector.d.ts +6 -1
- package/lib/archive/migrate-info-main-content-selector.js +14 -3
- package/lib/archive/migrate-info-roots.d.ts +6 -1
- package/lib/archive/migrate-info-roots.js +14 -3
- package/lib/archive/migrate-inventory-runs-exclude-skipped.d.ts +6 -1
- package/lib/archive/migrate-inventory-runs-exclude-skipped.js +14 -3
- package/lib/archive/migrate-inventory-runs-invalid-skipped.d.ts +6 -1
- package/lib/archive/migrate-inventory-runs-invalid-skipped.js +14 -3
- package/lib/archive/migrate-main-contents-columns.d.ts +6 -1
- package/lib/archive/migrate-main-contents-columns.js +14 -3
- package/lib/archive/migrate-page-meta-body-hash.d.ts +6 -1
- package/lib/archive/migrate-page-meta-body-hash.js +14 -3
- package/lib/archive/migrate-page-meta-console-error-count.d.ts +6 -1
- package/lib/archive/migrate-page-meta-console-error-count.js +14 -3
- package/lib/archive/migrate-page-meta-custom-element-count.d.ts +33 -0
- package/lib/archive/migrate-page-meta-custom-element-count.js +51 -0
- package/lib/archive/migrate-page-tags-to-page-technologies.d.ts +39 -0
- package/lib/archive/migrate-page-tags-to-page-technologies.js +79 -0
- package/lib/archive/page.d.ts +37 -13
- package/lib/archive/page.js +45 -17
- package/lib/archive/retarget-legacy-fk-tables.d.ts +27 -14
- package/lib/archive/retarget-legacy-fk-tables.js +61 -15
- package/lib/archive/types.d.ts +18 -0
- package/lib/crawler/assert-puppeteer-shared-with-beholder.d.ts +24 -0
- package/lib/crawler/assert-puppeteer-shared-with-beholder.js +36 -0
- package/lib/crawler/capture-custom-elements.d.ts +33 -0
- package/lib/crawler/capture-custom-elements.js +39 -0
- package/lib/crawler/close-browser-safely.d.ts +3 -3
- package/lib/crawler/close-browser-safely.js +8 -17
- package/lib/crawler/collect-custom-elements.d.ts +33 -0
- package/lib/crawler/collect-custom-elements.js +110 -0
- package/lib/crawler/crawler.js +101 -46
- package/lib/crawler/fetch-destination.js +6 -13
- package/lib/crawler/find-package-dir.d.ts +24 -0
- package/lib/crawler/find-package-dir.js +39 -0
- package/lib/crawler/scan-js-resource-for-license-comment.d.ts +32 -0
- package/lib/crawler/scan-js-resource-for-license-comment.js +140 -0
- package/lib/crawler/scan-js-resources-for-technology-signals.d.ts +58 -0
- package/lib/crawler/scan-js-resources-for-technology-signals.js +196 -0
- package/lib/crawler/types.d.ts +23 -0
- package/lib/crawler-orchestrator.d.ts +53 -9
- package/lib/crawler-orchestrator.js +223 -46
- package/lib/crawler.d.ts +11 -1
- package/lib/crawler.js +9 -0
- package/lib/inventory-setup-phases.d.ts +32 -0
- package/lib/inventory-setup-phases.js +45 -0
- package/lib/resume-setup-phases.d.ts +16 -0
- package/lib/resume-setup-phases.js +24 -0
- package/lib/retry-failed-setup-phases.d.ts +19 -0
- package/lib/retry-failed-setup-phases.js +24 -0
- package/lib/setup-recovery-phase-labels.d.ts +21 -0
- package/lib/setup-recovery-phase-labels.js +24 -0
- package/lib/types.d.ts +145 -0
- package/package.json +11 -8
- package/lib/archive/db-ops/meta/get-tags-of-page.d.ts +0 -12
- package/lib/archive/db-ops/meta/get-tags-of-page.js +0 -28
- package/lib/archive/db-ops/pages/write/insert-tags.d.ts +0 -16
- package/lib/archive/db-ops/pages/write/insert-tags.js +0 -34
- package/lib/archive/meta/summarize-tags.d.ts +0 -16
- package/lib/archive/meta/summarize-tags.js +0 -33
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { unlink as unlinkFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { tryParseUrl as parseUrl } from '@d-zero/shared/parse-url';
|
|
4
4
|
import { sortUrl } from '@d-zero/shared/sort-url';
|
|
5
5
|
import { TypedAwaitEventEmitter as EventEmitter } from '@d-zero/shared/typed-await-event-emitter';
|
|
6
6
|
import pkg from '../package.json' with { type: 'json' };
|
|
7
|
+
import { APPEND_SETUP_PHASES } from './append-setup-phases.js';
|
|
7
8
|
import Archive from './archive/archive.js';
|
|
9
|
+
import { copyFileWithProgress } from './archive/filesystem/copy-file-with-progress.js';
|
|
8
10
|
import { REQUIRED_FORMAT_VERSION } from './archive/meta/assert-compatible-version.js';
|
|
9
11
|
import { clearDestinationCache } from './crawler/clear-destination-cache.js';
|
|
10
12
|
import { clearDnsBurnedHostCache } from './crawler/clear-dns-burned-host-cache.js';
|
|
@@ -18,11 +20,16 @@ import { PreloadShortCircuitError } from './crawler/preload-short-circuit-error.
|
|
|
18
20
|
import { protocolAgnosticKey } from './crawler/protocol-agnostic-key.js';
|
|
19
21
|
import { shouldSkipUrl } from './crawler/should-skip-url.js';
|
|
20
22
|
import { crawlerLog, log } from './debug.js';
|
|
23
|
+
import { INVENTORY_SETUP_PHASES } from './inventory-setup-phases.js';
|
|
21
24
|
import { normalizeToArray } from './normalize-to-array.js';
|
|
22
25
|
import { resolveOutputPath } from './resolve-output-path.js';
|
|
23
26
|
import { resourceRowToLookupResult } from './resource-row-to-lookup-result.js';
|
|
27
|
+
import { RESUME_SETUP_PHASES } from './resume-setup-phases.js';
|
|
28
|
+
import { RETRY_FAILED_SETUP_PHASES } from './retry-failed-setup-phases.js';
|
|
29
|
+
import { SETUP_RECOVERY_PHASE_LABELS } from './setup-recovery-phase-labels.js';
|
|
24
30
|
import { cleanObject } from './utils/object/clean-object.js';
|
|
25
31
|
import { WriteQueue } from './write-queue.js';
|
|
32
|
+
const [RECOVERY_RESTORE_FROM_BACKUP, RECOVERY_PERSIST_INGESTED_STATE] = SETUP_RECOVERY_PHASE_LABELS;
|
|
26
33
|
/**
|
|
27
34
|
* Default list of external URL prefixes excluded from crawling.
|
|
28
35
|
* Includes social media sharing endpoints that are commonly linked
|
|
@@ -53,10 +60,13 @@ export const DEFAULT_EXCLUDED_EXTERNAL_URLS = [
|
|
|
53
60
|
* writes the final archive file. It emits events defined by {@link CrawlEvent}.
|
|
54
61
|
*
|
|
55
62
|
* Instances are created via the static factory methods {@link CrawlerOrchestrator.crawling}
|
|
56
|
-
* or {@link CrawlerOrchestrator.resume}; the constructor is private.
|
|
63
|
+
* or {@link CrawlerOrchestrator.resume}; the constructor is private. Implements
|
|
64
|
+
* `Symbol.asyncDispose` so callers can use `await using` to close the archive
|
|
65
|
+
* and reap zombie Chromium processes on scope exit instead of a manual
|
|
66
|
+
* `try`/`finally` around `archive.close()` + `garbageCollect()`.
|
|
57
67
|
* @example
|
|
58
68
|
* ```ts
|
|
59
|
-
*
|
|
69
|
+
* await using orchestrator = await CrawlerOrchestrator.crawling(['https://example.com'], { recursive: true });
|
|
60
70
|
* await orchestrator.write();
|
|
61
71
|
* ```
|
|
62
72
|
*/
|
|
@@ -177,6 +187,36 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
177
187
|
preloadedStickyShapeKeys: options?.preloadedStickyShapeKeys ?? [],
|
|
178
188
|
});
|
|
179
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Enables `await using orchestrator = ...`. Closes the archive (write
|
|
192
|
+
* or remove tmpDir + release the lock, per {@link Archive.close}) and
|
|
193
|
+
* then reaps any zombie Chromium processes via {@link garbageCollect} —
|
|
194
|
+
* the same two-step teardown every CLI crawl command previously
|
|
195
|
+
* repeated by hand in a `finally` block.
|
|
196
|
+
*
|
|
197
|
+
* Relays `Archive.close()`'s recovery-write progress (issue #294) as
|
|
198
|
+
* `recoveringArchiveWrite`/`writeStep`/`writeTarProgress` — the same
|
|
199
|
+
* events `write()` emits — for the rare case where the file doesn't
|
|
200
|
+
* exist on disk yet at dispose time (e.g. an explicit `write()` call
|
|
201
|
+
* threw partway through). A CLI listener whose display is still open at
|
|
202
|
+
* that point (it hadn't yet seen `writeFileEnd`) picks these up for
|
|
203
|
+
* free; one that already tore down after the earlier failure silently
|
|
204
|
+
* drops them, same as any other post-close display update.
|
|
205
|
+
*/
|
|
206
|
+
async [Symbol.asyncDispose]() {
|
|
207
|
+
await this.#archive.close({
|
|
208
|
+
onRecoveryStart: () => {
|
|
209
|
+
void this.emit('recoveringArchiveWrite', {});
|
|
210
|
+
},
|
|
211
|
+
onStep: (step) => {
|
|
212
|
+
void this.emit('writeStep', { step });
|
|
213
|
+
},
|
|
214
|
+
onTarProgress: (writtenBytes, totalBytes) => {
|
|
215
|
+
void this.emit('writeTarProgress', { writtenBytes, totalBytes });
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
this.garbageCollect();
|
|
219
|
+
}
|
|
180
220
|
/**
|
|
181
221
|
* Abort the current crawl operation.
|
|
182
222
|
*
|
|
@@ -228,9 +268,9 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
228
268
|
.catch((writeError) => reject(writeError));
|
|
229
269
|
void this.emit('error', error);
|
|
230
270
|
});
|
|
231
|
-
this.#crawler.on('page', ({ result, source }) => {
|
|
271
|
+
this.#crawler.on('page', ({ result, source, bodyHash }) => {
|
|
232
272
|
writeQueue
|
|
233
|
-
.enqueue(() => this.#archive.setPage(result, source))
|
|
273
|
+
.enqueue(() => this.#archive.setPage(result, source, bodyHash))
|
|
234
274
|
.catch((error) => reject(error));
|
|
235
275
|
});
|
|
236
276
|
this.#crawler.on('externalPage', ({ result, source }) => {
|
|
@@ -256,8 +296,13 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
256
296
|
});
|
|
257
297
|
this.#crawler.on('networkOutageConfirmed', ({ startedAt, detectedAt, probeHost, triggerErrorCount, triggerHostCount }) => {
|
|
258
298
|
crawlerLog('Network outage confirmed: probeHost=%s triggerErrorCount=%d triggerHostCount=%d', probeHost, triggerErrorCount, triggerHostCount);
|
|
259
|
-
//
|
|
260
|
-
// `
|
|
299
|
+
// Rare anomaly notice, printed unconditionally even though this
|
|
300
|
+
// fires while `deal()`'s own crawl-time `Lanes` is actively
|
|
301
|
+
// rendering (issue #294: unlike the crawl-tail notices reported
|
|
302
|
+
// via the `crawlSessionNotice` event, this one has no
|
|
303
|
+
// listener-based route available mid-crawl — visibility during
|
|
304
|
+
// the outage takes priority over the display glitch this
|
|
305
|
+
// causes).
|
|
261
306
|
console.error(`[network] outage suspected — pausing workers (probe host: ${probeHost ?? 'none'})`);
|
|
262
307
|
writeQueue
|
|
263
308
|
.enqueue(async () => {
|
|
@@ -348,6 +393,13 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
348
393
|
.catch((error) => reject(error));
|
|
349
394
|
});
|
|
350
395
|
this.#crawler.on('crawlEnd', () => {
|
|
396
|
+
// Read BEFORE enqueuing the dedupeCap-finalize closure below
|
|
397
|
+
// (issue #294) so this reflects genuine backlog from the
|
|
398
|
+
// crawl's own page/resource writes, not the finalize
|
|
399
|
+
// closure's own, always-present entry.
|
|
400
|
+
if (writeQueue.pending > 0) {
|
|
401
|
+
void this.emit('flushingPendingWrites', { pending: writeQueue.pending });
|
|
402
|
+
}
|
|
351
403
|
// Deferred to INSIDE a queued closure, not read synchronously
|
|
352
404
|
// here, for the same reason `networkOutageRecovered`'s handler
|
|
353
405
|
// defers reading `#openNetworkOutageId`: a `dedupeCap` event's
|
|
@@ -439,14 +491,38 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
439
491
|
* the time `write()` is called those tables are already populated.
|
|
440
492
|
* This method just tars.
|
|
441
493
|
*
|
|
442
|
-
* Emits `writeFileStart` before writing and `writeFileEnd` after
|
|
443
|
-
*
|
|
494
|
+
* Emits `writeFileStart` before writing and `writeFileEnd` after the
|
|
495
|
+
* write completes successfully. Also relays `Archive.write()`'s
|
|
496
|
+
* per-step (`writeStep`) and tar-byte (`writeTarProgress`) progress
|
|
497
|
+
* (issue #294) — tarring a 15 GB+ archive can take minutes, and without
|
|
498
|
+
* these events a CLI listener has no way to show it isn't hung.
|
|
444
499
|
*/
|
|
445
500
|
async write() {
|
|
446
501
|
void this.emit('writeFileStart', { filePath: this.#archive.filePath });
|
|
447
|
-
await this.#archive.write(
|
|
502
|
+
await this.#archive.write({
|
|
503
|
+
onStep: (step) => {
|
|
504
|
+
void this.emit('writeStep', { step });
|
|
505
|
+
},
|
|
506
|
+
onTarProgress: (writtenBytes, totalBytes) => {
|
|
507
|
+
void this.emit('writeTarProgress', { writtenBytes, totalBytes });
|
|
508
|
+
},
|
|
509
|
+
});
|
|
448
510
|
void this.emit('writeFileEnd', { filePath: this.#archive.filePath });
|
|
449
511
|
}
|
|
512
|
+
/**
|
|
513
|
+
* Assign natural URL sort order to every internal page, relaying chunk
|
|
514
|
+
* progress through the `sortingUrls` event (issue #294). Always runs
|
|
515
|
+
* after `crawling()` has returned, i.e. once `initializedCallback` has
|
|
516
|
+
* already had a chance to attach listeners — unlike the setup-phase
|
|
517
|
+
* work in `append`/`inventory`/`retryFailed`, this has an orchestrator
|
|
518
|
+
* instance to emit from, so it goes through the event emitter rather
|
|
519
|
+
* than a `SetupProgressCallbacks` callback.
|
|
520
|
+
*/
|
|
521
|
+
async #setUrlOrder() {
|
|
522
|
+
await this.#archive.setUrlOrder((processed, total) => {
|
|
523
|
+
void this.emit('sortingUrls', { processed, total });
|
|
524
|
+
});
|
|
525
|
+
}
|
|
450
526
|
/**
|
|
451
527
|
* Create a new CrawlerOrchestrator instance and start crawling the given URLs.
|
|
452
528
|
*
|
|
@@ -523,9 +599,9 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
523
599
|
log('Config %O', config);
|
|
524
600
|
await orchestrator.crawling(list);
|
|
525
601
|
log('Crawling completed');
|
|
526
|
-
CrawlerOrchestrator.#finalizeCrawlSession();
|
|
602
|
+
CrawlerOrchestrator.#finalizeCrawlSession(orchestrator);
|
|
527
603
|
log('Set order natural URL sort');
|
|
528
|
-
await
|
|
604
|
+
await orchestrator.#setUrlOrder();
|
|
529
605
|
log('Sorting done');
|
|
530
606
|
return orchestrator;
|
|
531
607
|
}
|
|
@@ -546,10 +622,15 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
546
622
|
* @param newUrls - New root URLs to add and crawl.
|
|
547
623
|
* @param options - Optional config overrides applied on top of the archived config.
|
|
548
624
|
* @param initializedCallback - Optional callback invoked after initialization but before crawling resumes.
|
|
625
|
+
* @param setupProgress - Optional progress callbacks for the setup phase
|
|
626
|
+
* (untar, `.bak` copy, repromote, state rebuild) that runs before
|
|
627
|
+
* `initializedCallback` — see {@link SetupProgressCallbacks} for why
|
|
628
|
+
* this can't go through the orchestrator's event emitter (issue #294).
|
|
549
629
|
* @returns The orchestrator instance after the append crawl completes.
|
|
550
630
|
* @throws {Error} When `newUrls` is empty, the archive is in list mode, or it cannot be parsed.
|
|
551
631
|
*/
|
|
552
|
-
static async append(archivePath, newUrls, options, initializedCallback) {
|
|
632
|
+
static async append(archivePath, newUrls, options, initializedCallback, setupProgress) {
|
|
633
|
+
const [PHASE_EXTRACTING, PHASE_LOADING_CONFIG, PHASE_BACKING_UP, PHASE_REPROMOTING, PHASE_LOADING_DEDUPE_KEYS, PHASE_LOADING_CRAWL_STATE, PHASE_LOADING_RESOURCES, PHASE_LOADING_SCRAPED_COUNT, PHASE_RESTORING_CRAWL_STATE,] = APPEND_SETUP_PHASES;
|
|
553
634
|
if (newUrls.length === 0) {
|
|
554
635
|
throw new Error('append: newUrls is empty');
|
|
555
636
|
}
|
|
@@ -559,16 +640,20 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
559
640
|
: path.resolve(cwd, archivePath);
|
|
560
641
|
// See `ArchiveOpenOptions.openPluginData` for why this must be `true`
|
|
561
642
|
// on every writer path that calls `write()`.
|
|
643
|
+
setupProgress?.onPhase?.(PHASE_EXTRACTING);
|
|
562
644
|
const archive = await Archive.open({
|
|
563
645
|
filePath: absFilePath,
|
|
564
646
|
cwd,
|
|
565
647
|
openPluginData: true,
|
|
648
|
+
onExtractProgress: setupProgress?.onExtractProgress,
|
|
649
|
+
onLog: setupProgress?.onLog,
|
|
566
650
|
});
|
|
567
651
|
// Any throw between here and the successful return must release the
|
|
568
652
|
// archive lock and clean up tmpDir; the caller's `close()` only runs on
|
|
569
653
|
// the happy path. Errors from `close()` itself are intentionally
|
|
570
654
|
// best-effort: the original error is what matters.
|
|
571
655
|
try {
|
|
656
|
+
setupProgress?.onPhase?.(PHASE_LOADING_CONFIG);
|
|
572
657
|
const archived = await archive.getConfig();
|
|
573
658
|
if (archived.fromList) {
|
|
574
659
|
throw new Error('Cannot append to a list-mode archive: this archive was created with --list/--list-file and contains metadata-only pages. Create a fresh archive instead.');
|
|
@@ -588,7 +673,8 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
588
673
|
baseUrl: mergedRoots[0],
|
|
589
674
|
};
|
|
590
675
|
const backupPath = absFilePath + '.bak';
|
|
591
|
-
|
|
676
|
+
setupProgress?.onPhase?.(PHASE_BACKING_UP);
|
|
677
|
+
await copyFileWithProgress(absFilePath, backupPath, setupProgress?.onCopyProgress);
|
|
592
678
|
try {
|
|
593
679
|
await archive.updateConfig(mergedConfig);
|
|
594
680
|
const scopeMap = new Map();
|
|
@@ -599,19 +685,25 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
599
685
|
const existing = scopeMap.get(parsed.hostname) ?? [];
|
|
600
686
|
scopeMap.set(parsed.hostname, [...existing, parsed]);
|
|
601
687
|
}
|
|
602
|
-
|
|
688
|
+
setupProgress?.onPhase?.(PHASE_REPROMOTING);
|
|
689
|
+
await archive.repromoteExternalPages(scopeMap, archived, setupProgress?.onChunkProgress);
|
|
603
690
|
// Seed the sticky set from prior sessions' confirmed traps so
|
|
604
691
|
// `--append` does not pay the cost of re-discovering them (see
|
|
605
692
|
// `DedupeCapTracker`'s constructor JSDoc).
|
|
693
|
+
setupProgress?.onPhase?.(PHASE_LOADING_DEDUPE_KEYS);
|
|
606
694
|
const preloadedStickyShapeKeys = await archive.listDedupeCapShapeKeys();
|
|
607
695
|
const orchestrator = new CrawlerOrchestrator(archive, {
|
|
608
696
|
...mergedConfig,
|
|
609
697
|
roots: mergedRoots,
|
|
610
698
|
preloadedStickyShapeKeys,
|
|
611
699
|
});
|
|
700
|
+
setupProgress?.onPhase?.(PHASE_LOADING_CRAWL_STATE);
|
|
612
701
|
const { scraped, pending } = await archive.getCrawlingState();
|
|
613
|
-
|
|
702
|
+
setupProgress?.onPhase?.(PHASE_LOADING_RESOURCES);
|
|
703
|
+
const resources = await archive.getResourceUrlList(setupProgress?.onChunkProgress);
|
|
704
|
+
setupProgress?.onPhase?.(PHASE_LOADING_SCRAPED_COUNT);
|
|
614
705
|
const pagesScrapedOffset = await archive.getScrapedHtmlPageCount();
|
|
706
|
+
setupProgress?.onPhase?.(PHASE_RESTORING_CRAWL_STATE);
|
|
615
707
|
orchestrator.#crawler.resume(pending, scraped, resources, pagesScrapedOffset);
|
|
616
708
|
if (initializedCallback) {
|
|
617
709
|
await initializedCallback(orchestrator, mergedConfig);
|
|
@@ -622,14 +714,15 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
622
714
|
log('Merged roots %O', mergedRoots);
|
|
623
715
|
await CrawlerOrchestrator.#preloadDnsBurnedHostCache(archive);
|
|
624
716
|
await orchestrator.crawling(newParsed);
|
|
625
|
-
CrawlerOrchestrator.#finalizeCrawlSession();
|
|
626
|
-
await
|
|
717
|
+
CrawlerOrchestrator.#finalizeCrawlSession(orchestrator);
|
|
718
|
+
await orchestrator.#setUrlOrder();
|
|
627
719
|
await ignoreEnoent(unlinkFile(backupPath));
|
|
628
720
|
return orchestrator;
|
|
629
721
|
}
|
|
630
722
|
catch (error) {
|
|
631
723
|
try {
|
|
632
|
-
|
|
724
|
+
setupProgress?.onPhase?.(RECOVERY_RESTORE_FROM_BACKUP);
|
|
725
|
+
await copyFileWithProgress(backupPath, absFilePath, setupProgress?.onCopyProgress);
|
|
633
726
|
await ignoreEnoent(unlinkFile(backupPath));
|
|
634
727
|
}
|
|
635
728
|
catch (restoreError) {
|
|
@@ -723,10 +816,16 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
723
816
|
* URLs. Pass `null` for programmatic callers that built
|
|
724
817
|
* `inventoryUrls` in-memory; the audit row's `source_file_sha256`
|
|
725
818
|
* column will be `NULL` and no source list is archived.
|
|
819
|
+
* @param setupProgress - Optional progress callbacks for the setup phase
|
|
820
|
+
* (untar, scope classification, bulk inserts, state rebuild) that runs
|
|
821
|
+
* before `initializedCallback` — see {@link SetupProgressCallbacks} for
|
|
822
|
+
* why this can't go through the orchestrator's event emitter (issue
|
|
823
|
+
* #294).
|
|
726
824
|
* @returns The orchestrator instance after a successful inventory pass.
|
|
727
825
|
* @throws {Error} When `inventoryUrls` is empty or the archive is in list mode. Unresolved pending URLs from a previous crawl do NOT throw — see step 3.
|
|
728
826
|
*/
|
|
729
|
-
static async inventory(archivePath, inventoryUrls, options, initializedCallback, source = null) {
|
|
827
|
+
static async inventory(archivePath, inventoryUrls, options, initializedCallback, source = null, setupProgress) {
|
|
828
|
+
const [PHASE_EXTRACTING, PHASE_LOADING_CONFIG, PHASE_LOADING_CRAWL_STATE_PRE, PHASE_CHECKING_KNOWN_URLS, PHASE_BACKING_UP, PHASE_RECORDING_NON_HTML, PHASE_RECORDING_HTML_SEEDS, PHASE_RECORDING_EXCLUDED, PHASE_LOADING_CRAWL_STATE_POST, PHASE_LOADING_RESOURCES, PHASE_LOADING_SCRAPED_COUNT, PHASE_RESTORING_CRAWL_STATE,] = INVENTORY_SETUP_PHASES;
|
|
730
829
|
if (inventoryUrls.length === 0) {
|
|
731
830
|
throw new Error('inventory: URL list is empty');
|
|
732
831
|
}
|
|
@@ -736,16 +835,21 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
736
835
|
: path.resolve(cwd, archivePath);
|
|
737
836
|
// See `ArchiveOpenOptions.openPluginData` for why this must be `true`
|
|
738
837
|
// on every writer path that calls `write()`.
|
|
838
|
+
setupProgress?.onPhase?.(PHASE_EXTRACTING);
|
|
739
839
|
const archive = await Archive.open({
|
|
740
840
|
filePath: absFilePath,
|
|
741
841
|
cwd,
|
|
742
842
|
openPluginData: true,
|
|
843
|
+
onExtractProgress: setupProgress?.onExtractProgress,
|
|
844
|
+
onLog: setupProgress?.onLog,
|
|
743
845
|
});
|
|
744
846
|
try {
|
|
847
|
+
setupProgress?.onPhase?.(PHASE_LOADING_CONFIG);
|
|
745
848
|
const archived = await archive.getConfig();
|
|
746
849
|
if (archived.fromList) {
|
|
747
850
|
throw new Error('Cannot run inventory on a list-mode archive: this archive was created with --list/--list-file and contains metadata-only pages. Create a fresh archive instead.');
|
|
748
851
|
}
|
|
852
|
+
setupProgress?.onPhase?.(PHASE_LOADING_CRAWL_STATE_PRE);
|
|
749
853
|
const { pending } = await archive.getCrawlingState();
|
|
750
854
|
if (pending.length > 0) {
|
|
751
855
|
// `getCrawlingState` returns the STRICT pending set — in-scope,
|
|
@@ -759,8 +863,28 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
759
863
|
// enough — the inventory pass continues and the crawled-wins
|
|
760
864
|
// source priority keeps stale labels stable even if some of
|
|
761
865
|
// the strict-pending rows happen to land on inventory seeds.
|
|
762
|
-
//
|
|
763
|
-
|
|
866
|
+
//
|
|
867
|
+
// Routed through `setupProgress.onLog` (issue #294 code
|
|
868
|
+
// review), not a bare `console.warn`: the `'Loading crawl
|
|
869
|
+
// state'` row is active at this exact point (`onPhase` just
|
|
870
|
+
// above), and a direct stream write here corrupts the setup
|
|
871
|
+
// `TaskList`'s cursor tracking the same way self-healing
|
|
872
|
+
// migration notices did. `onLog` sets this row's message,
|
|
873
|
+
// which then freezes as its permanent `done` text once the
|
|
874
|
+
// next `onPhase` call moves past it — still visible in the
|
|
875
|
+
// final terminal output, just not printed as its own
|
|
876
|
+
// interrupting line. Falls back to `console.warn` under
|
|
877
|
+
// `--silent` (`setupProgress` is `null` there, so there is no
|
|
878
|
+
// row to set this on, and the warning would otherwise never
|
|
879
|
+
// surface at all).
|
|
880
|
+
const message = `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.`;
|
|
881
|
+
if (setupProgress?.onLog) {
|
|
882
|
+
setupProgress.onLog(message);
|
|
883
|
+
}
|
|
884
|
+
else {
|
|
885
|
+
// eslint-disable-next-line no-console -- --silent has no TaskList row to report through
|
|
886
|
+
console.warn(message);
|
|
887
|
+
}
|
|
764
888
|
}
|
|
765
889
|
// Archive the exact source bytes before scope classification, so
|
|
766
890
|
// even a run that discards every URL (out-of-scope or already
|
|
@@ -800,6 +924,7 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
800
924
|
// to mirror what `resolveContentItemId` / `insertResource` actually store.
|
|
801
925
|
// Two independent reads — Promise.all halves the wait on large
|
|
802
926
|
// archives where each `WHERE url IN (?)` chunk costs real I/O.
|
|
927
|
+
setupProgress?.onPhase?.(PHASE_CHECKING_KNOWN_URLS);
|
|
803
928
|
const candidateUrls = inScope.map((u) => u.withoutHashAndAuth);
|
|
804
929
|
const [existingPageUrlList, existingResourceUrlList] = await Promise.all([
|
|
805
930
|
archive.getExistingPageUrls(candidateUrls),
|
|
@@ -860,7 +985,8 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
860
985
|
return orchestrator;
|
|
861
986
|
}
|
|
862
987
|
const backupPath = absFilePath + '.bak';
|
|
863
|
-
|
|
988
|
+
setupProgress?.onPhase?.(PHASE_BACKING_UP);
|
|
989
|
+
await copyFileWithProgress(absFilePath, backupPath, setupProgress?.onCopyProgress);
|
|
864
990
|
// Ingestion (pre-insert + audit) is `.bak`-protected — a failure
|
|
865
991
|
// there restores the archive and the operator reruns. Once
|
|
866
992
|
// ingestion completes and the `.bak` is released, the scrape
|
|
@@ -934,6 +1060,7 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
934
1060
|
// inside the `.bak`-protected window on large inventory
|
|
935
1061
|
// lists; the chunked bulk path collapses N round-trips
|
|
936
1062
|
// to N/500.
|
|
1063
|
+
setupProgress?.onPhase?.(PHASE_RECORDING_NON_HTML);
|
|
937
1064
|
await archive.insertInventoryResources(nonHtmlSeeds);
|
|
938
1065
|
// Pre-insert HTML seeds as `scraped = 0`,
|
|
939
1066
|
// `source = 'inventory-seed'` placeholders *before* the
|
|
@@ -941,6 +1068,7 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
941
1068
|
// cannot lose the URL. The strict-pending set picks
|
|
942
1069
|
// these rows up on the next `--resume` via the
|
|
943
1070
|
// `OR p.source != 'crawled'` clause.
|
|
1071
|
+
setupProgress?.onPhase?.(PHASE_RECORDING_HTML_SEEDS);
|
|
944
1072
|
await archive.insertInventorySeeds(htmlSeeds);
|
|
945
1073
|
// Record exclude-matched novel URLs as terminal skipped pages
|
|
946
1074
|
// (`is_skipped=1`, `skip_reason='excluded'`,
|
|
@@ -949,6 +1077,7 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
949
1077
|
// excluded URLs, so the archive looks identical no matter
|
|
950
1078
|
// how the URL was discovered. Inside the `.bak` window for
|
|
951
1079
|
// the same all-or-nothing reason as the seed inserts above.
|
|
1080
|
+
setupProgress?.onPhase?.(PHASE_RECORDING_EXCLUDED);
|
|
952
1081
|
await archive.insertInventorySkippedPages(excludedNovelUrls);
|
|
953
1082
|
log('[inventory] %d HTML seed(s), %d non-HTML resource(s), %d skipped page(s) recorded', htmlSeeds.length, nonHtmlSeeds.length, excludedNovelUrls.length);
|
|
954
1083
|
// Audit row is written *inside* the `.bak` window: a libsql
|
|
@@ -1015,15 +1144,19 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
1015
1144
|
// `retryFailed` uses to drive the dealer from the
|
|
1016
1145
|
// pending set alone (see retryFailed's
|
|
1017
1146
|
// `crawling([], { recursive })` invocation).
|
|
1147
|
+
setupProgress?.onPhase?.(PHASE_LOADING_CRAWL_STATE_POST);
|
|
1018
1148
|
const { scraped: scrapedAfter, pending: pendingAfter } = await archive.getCrawlingState();
|
|
1019
|
-
|
|
1149
|
+
setupProgress?.onPhase?.(PHASE_LOADING_RESOURCES);
|
|
1150
|
+
const resources = await archive.getResourceUrlList(setupProgress?.onChunkProgress);
|
|
1020
1151
|
// Pre-existing rendered HTML page count seeds the
|
|
1021
1152
|
// session-spanning `pagesScraped` counter so the progress
|
|
1022
1153
|
// header reads `internalDone(cumulative pagesScraped)`
|
|
1023
1154
|
// rather than session-only — matches the `append` /
|
|
1024
1155
|
// `retryFailed` / `resume` paths and avoids users reading
|
|
1025
1156
|
// the parenthesised number as "inner pages dropped to N".
|
|
1157
|
+
setupProgress?.onPhase?.(PHASE_LOADING_SCRAPED_COUNT);
|
|
1026
1158
|
const pagesScrapedOffset = await archive.getScrapedHtmlPageCount();
|
|
1159
|
+
setupProgress?.onPhase?.(PHASE_RESTORING_CRAWL_STATE);
|
|
1027
1160
|
orchestrator.#crawler.resume(pendingAfter, scrapedAfter, resources, pagesScrapedOffset);
|
|
1028
1161
|
if (initializedCallback) {
|
|
1029
1162
|
await initializedCallback(orchestrator, baseConfig);
|
|
@@ -1033,8 +1166,8 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
1033
1166
|
log('HTML seeds %O', htmlSeeds.map((u) => u.href));
|
|
1034
1167
|
await CrawlerOrchestrator.#preloadDnsBurnedHostCache(archive);
|
|
1035
1168
|
await orchestrator.crawling([], { recursive: true });
|
|
1036
|
-
CrawlerOrchestrator.#finalizeCrawlSession();
|
|
1037
|
-
await
|
|
1169
|
+
CrawlerOrchestrator.#finalizeCrawlSession(orchestrator);
|
|
1170
|
+
await orchestrator.#setUrlOrder();
|
|
1038
1171
|
return orchestrator;
|
|
1039
1172
|
}
|
|
1040
1173
|
// Only non-HTML URLs were imported — nothing left to render,
|
|
@@ -1043,7 +1176,7 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
1043
1176
|
if (initializedCallback) {
|
|
1044
1177
|
await initializedCallback(orchestrator, baseConfig);
|
|
1045
1178
|
}
|
|
1046
|
-
await
|
|
1179
|
+
await orchestrator.#setUrlOrder();
|
|
1047
1180
|
return orchestrator;
|
|
1048
1181
|
}
|
|
1049
1182
|
catch (error) {
|
|
@@ -1064,6 +1197,7 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
1064
1197
|
// catch's `close()` becomes a no-op for the destructive
|
|
1065
1198
|
// step and only runs `releaseLock` cleanup.
|
|
1066
1199
|
try {
|
|
1200
|
+
setupProgress?.onPhase?.(RECOVERY_PERSIST_INGESTED_STATE);
|
|
1067
1201
|
await archive.write();
|
|
1068
1202
|
await archive.releaseHandle();
|
|
1069
1203
|
}
|
|
@@ -1073,7 +1207,8 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
1073
1207
|
throw error;
|
|
1074
1208
|
}
|
|
1075
1209
|
try {
|
|
1076
|
-
|
|
1210
|
+
setupProgress?.onPhase?.(RECOVERY_RESTORE_FROM_BACKUP);
|
|
1211
|
+
await copyFileWithProgress(backupPath, absFilePath, setupProgress?.onCopyProgress);
|
|
1077
1212
|
await ignoreEnoent(unlinkFile(backupPath));
|
|
1078
1213
|
}
|
|
1079
1214
|
catch (restoreError) {
|
|
@@ -1117,25 +1252,34 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
1117
1252
|
* @param archivePath - Absolute or relative path to the existing `.nitpicker`.
|
|
1118
1253
|
* @param options - Optional config overrides applied on top of the archived config.
|
|
1119
1254
|
* @param initializedCallback - Optional callback invoked after initialization but before crawling resumes.
|
|
1255
|
+
* @param setupProgress - Optional progress callbacks for the setup phase
|
|
1256
|
+
* (untar, `.bak` copy, reset, state rebuild) that runs before
|
|
1257
|
+
* `initializedCallback` — see {@link SetupProgressCallbacks} for why
|
|
1258
|
+
* this can't go through the orchestrator's event emitter (issue #294).
|
|
1120
1259
|
* @returns The orchestrator instance after the retry crawl completes.
|
|
1121
1260
|
* @throws {Error} When the archive is in list mode or has no parseable roots.
|
|
1122
1261
|
*/
|
|
1123
|
-
static async retryFailed(archivePath, options, initializedCallback) {
|
|
1262
|
+
static async retryFailed(archivePath, options, initializedCallback, setupProgress) {
|
|
1263
|
+
const [PHASE_EXTRACTING, PHASE_LOADING_CONFIG, PHASE_BACKING_UP, PHASE_RESETTING_FAILED, PHASE_LOADING_DEDUPE_KEYS, PHASE_LOADING_CRAWL_STATE, PHASE_LOADING_RESOURCES, PHASE_LOADING_SCRAPED_COUNT, PHASE_RESTORING_CRAWL_STATE,] = RETRY_FAILED_SETUP_PHASES;
|
|
1124
1264
|
const cwd = options?.cwd ?? process.cwd();
|
|
1125
1265
|
const absFilePath = path.isAbsolute(archivePath)
|
|
1126
1266
|
? archivePath
|
|
1127
1267
|
: path.resolve(cwd, archivePath);
|
|
1128
1268
|
// See `ArchiveOpenOptions.openPluginData` for why this must be `true`
|
|
1129
1269
|
// on every writer path that calls `write()`.
|
|
1270
|
+
setupProgress?.onPhase?.(PHASE_EXTRACTING);
|
|
1130
1271
|
const archive = await Archive.open({
|
|
1131
1272
|
filePath: absFilePath,
|
|
1132
1273
|
cwd,
|
|
1133
1274
|
openPluginData: true,
|
|
1275
|
+
onExtractProgress: setupProgress?.onExtractProgress,
|
|
1276
|
+
onLog: setupProgress?.onLog,
|
|
1134
1277
|
});
|
|
1135
1278
|
// Any throw between here and the successful return must release the
|
|
1136
1279
|
// archive lock and clean up tmpDir; the caller's `close()` only runs on
|
|
1137
1280
|
// the happy path.
|
|
1138
1281
|
try {
|
|
1282
|
+
setupProgress?.onPhase?.(PHASE_LOADING_CONFIG);
|
|
1139
1283
|
const archived = await archive.getConfig();
|
|
1140
1284
|
if (archived.fromList) {
|
|
1141
1285
|
throw new Error('Cannot retry a list-mode archive: this archive was created with --list/--list-file and contains metadata-only pages. Create a fresh archive instead.');
|
|
@@ -1152,37 +1296,45 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
1152
1296
|
baseUrl: archived.baseUrl,
|
|
1153
1297
|
};
|
|
1154
1298
|
const backupPath = absFilePath + '.bak';
|
|
1155
|
-
|
|
1299
|
+
setupProgress?.onPhase?.(PHASE_BACKING_UP);
|
|
1300
|
+
await copyFileWithProgress(absFilePath, backupPath, setupProgress?.onCopyProgress);
|
|
1156
1301
|
try {
|
|
1157
|
-
|
|
1302
|
+
setupProgress?.onPhase?.(PHASE_RESETTING_FAILED);
|
|
1303
|
+
const resetUrls = await archive.resetFailedPages(setupProgress?.onChunkProgress);
|
|
1158
1304
|
log('Start retrying failed pages');
|
|
1159
1305
|
log('Archive %s', absFilePath);
|
|
1160
1306
|
log('Reset %d failed page(s)', resetUrls.length);
|
|
1161
1307
|
// Seed the sticky set from prior sessions' confirmed traps so
|
|
1162
1308
|
// `--retry-failed` does not pay the cost of re-discovering
|
|
1163
1309
|
// them (see `DedupeCapTracker`'s constructor JSDoc).
|
|
1310
|
+
setupProgress?.onPhase?.(PHASE_LOADING_DEDUPE_KEYS);
|
|
1164
1311
|
const preloadedStickyShapeKeys = await archive.listDedupeCapShapeKeys();
|
|
1165
1312
|
const orchestrator = new CrawlerOrchestrator(archive, {
|
|
1166
1313
|
...config,
|
|
1167
1314
|
preloadedStickyShapeKeys,
|
|
1168
1315
|
});
|
|
1316
|
+
setupProgress?.onPhase?.(PHASE_LOADING_CRAWL_STATE);
|
|
1169
1317
|
const { scraped, pending } = await archive.getCrawlingState();
|
|
1170
|
-
|
|
1318
|
+
setupProgress?.onPhase?.(PHASE_LOADING_RESOURCES);
|
|
1319
|
+
const resources = await archive.getResourceUrlList(setupProgress?.onChunkProgress);
|
|
1320
|
+
setupProgress?.onPhase?.(PHASE_LOADING_SCRAPED_COUNT);
|
|
1171
1321
|
const pagesScrapedOffset = await archive.getScrapedHtmlPageCount();
|
|
1322
|
+
setupProgress?.onPhase?.(PHASE_RESTORING_CRAWL_STATE);
|
|
1172
1323
|
orchestrator.#crawler.resume(pending, scraped, resources, pagesScrapedOffset);
|
|
1173
1324
|
if (initializedCallback) {
|
|
1174
1325
|
await initializedCallback(orchestrator, config);
|
|
1175
1326
|
}
|
|
1176
1327
|
await CrawlerOrchestrator.#preloadDnsBurnedHostCache(archive);
|
|
1177
1328
|
await orchestrator.crawling([], { recursive: config.recursive });
|
|
1178
|
-
CrawlerOrchestrator.#finalizeCrawlSession();
|
|
1179
|
-
await
|
|
1329
|
+
CrawlerOrchestrator.#finalizeCrawlSession(orchestrator);
|
|
1330
|
+
await orchestrator.#setUrlOrder();
|
|
1180
1331
|
await ignoreEnoent(unlinkFile(backupPath));
|
|
1181
1332
|
return orchestrator;
|
|
1182
1333
|
}
|
|
1183
1334
|
catch (error) {
|
|
1184
1335
|
try {
|
|
1185
|
-
|
|
1336
|
+
setupProgress?.onPhase?.(RECOVERY_RESTORE_FROM_BACKUP);
|
|
1337
|
+
await copyFileWithProgress(backupPath, absFilePath, setupProgress?.onCopyProgress);
|
|
1186
1338
|
await ignoreEnoent(unlinkFile(backupPath));
|
|
1187
1339
|
}
|
|
1188
1340
|
catch (restoreError) {
|
|
@@ -1208,15 +1360,27 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
1208
1360
|
* @param stubPath - Path to the existing archive file to resume from.
|
|
1209
1361
|
* @param options - Optional configuration overrides to apply on top of the archived config.
|
|
1210
1362
|
* @param initializedCallback - Optional callback invoked after initialization but before crawling resumes.
|
|
1363
|
+
* @param setupProgress - Optional progress callbacks for the setup phase
|
|
1364
|
+
* (self-healing migrations, state rebuild) that runs before
|
|
1365
|
+
* `initializedCallback` — see {@link SetupProgressCallbacks} for why
|
|
1366
|
+
* this can't go through the orchestrator's event emitter (issue #294).
|
|
1367
|
+
* No `onExtractProgress`/`onCopyProgress`: unlike `append`/`inventory`/
|
|
1368
|
+
* `retryFailed`, `resume` reconnects to an existing tmpDir (no untar)
|
|
1369
|
+
* and takes no `.bak` (nothing to restore — the interrupted crawl's
|
|
1370
|
+
* tmpDir IS the source of truth).
|
|
1211
1371
|
* @returns A promise that resolves to the CrawlerOrchestrator instance after crawling completes.
|
|
1212
1372
|
* @throws {Error} If the archived URL is invalid.
|
|
1213
1373
|
*/
|
|
1214
|
-
static async resume(stubPath, options, initializedCallback) {
|
|
1215
|
-
const
|
|
1374
|
+
static async resume(stubPath, options, initializedCallback, setupProgress) {
|
|
1375
|
+
const [PHASE_RECONNECTING, PHASE_LOADING_CONFIG, PHASE_LOADING_DEDUPE_KEYS, PHASE_LOADING_CRAWL_STATE, PHASE_LOADING_RESOURCES, PHASE_LOADING_SCRAPED_COUNT, PHASE_RESTORING_CRAWL_STATE,] = RESUME_SETUP_PHASES;
|
|
1376
|
+
setupProgress?.onPhase?.(PHASE_RECONNECTING);
|
|
1377
|
+
const archive = await Archive.resume(stubPath, setupProgress?.onLog);
|
|
1378
|
+
setupProgress?.onPhase?.(PHASE_LOADING_CONFIG);
|
|
1216
1379
|
const archivedConfig = await archive.getConfig();
|
|
1217
1380
|
// Seed the sticky set from prior sessions' confirmed traps so
|
|
1218
1381
|
// `--resume` does not pay the cost of re-discovering them (see
|
|
1219
1382
|
// `DedupeCapTracker`'s constructor JSDoc).
|
|
1383
|
+
setupProgress?.onPhase?.(PHASE_LOADING_DEDUPE_KEYS);
|
|
1220
1384
|
const preloadedStickyShapeKeys = await archive.listDedupeCapShapeKeys();
|
|
1221
1385
|
const config = {
|
|
1222
1386
|
...archivedConfig,
|
|
@@ -1229,9 +1393,13 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
1229
1393
|
if (!url) {
|
|
1230
1394
|
throw new Error(`URL (${_url}) is invalid`);
|
|
1231
1395
|
}
|
|
1396
|
+
setupProgress?.onPhase?.(PHASE_LOADING_CRAWL_STATE);
|
|
1232
1397
|
const { scraped, pending } = await archive.getCrawlingState();
|
|
1233
|
-
|
|
1398
|
+
setupProgress?.onPhase?.(PHASE_LOADING_RESOURCES);
|
|
1399
|
+
const resources = await archive.getResourceUrlList(setupProgress?.onChunkProgress);
|
|
1400
|
+
setupProgress?.onPhase?.(PHASE_LOADING_SCRAPED_COUNT);
|
|
1234
1401
|
const pagesScrapedOffset = await archive.getScrapedHtmlPageCount();
|
|
1402
|
+
setupProgress?.onPhase?.(PHASE_RESTORING_CRAWL_STATE);
|
|
1235
1403
|
orchestrator.#crawler.resume(pending, scraped, resources, pagesScrapedOffset);
|
|
1236
1404
|
if (initializedCallback) {
|
|
1237
1405
|
await initializedCallback(orchestrator, config);
|
|
@@ -1242,7 +1410,7 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
1242
1410
|
log('Config %O', config);
|
|
1243
1411
|
await CrawlerOrchestrator.#preloadDnsBurnedHostCache(archive);
|
|
1244
1412
|
await orchestrator.crawling([url]);
|
|
1245
|
-
CrawlerOrchestrator.#finalizeCrawlSession();
|
|
1413
|
+
CrawlerOrchestrator.#finalizeCrawlSession(orchestrator);
|
|
1246
1414
|
return orchestrator;
|
|
1247
1415
|
}
|
|
1248
1416
|
/**
|
|
@@ -1315,21 +1483,30 @@ export class CrawlerOrchestrator extends EventEmitter {
|
|
|
1315
1483
|
});
|
|
1316
1484
|
}
|
|
1317
1485
|
/**
|
|
1318
|
-
* Tears down session-scoped crawler caches and
|
|
1486
|
+
* Tears down session-scoped crawler caches and reports a short-circuit
|
|
1319
1487
|
* summary if any URL fetches were skipped. Invoked at every
|
|
1320
1488
|
* crawl-session boundary (`crawling` / `append` / `inventory` /
|
|
1321
|
-
* `retryFailed` / `resume`)
|
|
1489
|
+
* `retryFailed` / `resume`), in the same crawl-tail window as
|
|
1490
|
+
* `flushingPendingWrites`/`sortingUrls` — reports through the
|
|
1491
|
+
* `crawlSessionNotice` event rather than a bare `console.error` (issue
|
|
1492
|
+
* #294 code review) for the same reason those two do: this runs while a
|
|
1493
|
+
* caller's `Lanes`/`TaskList` display can already be active, and a
|
|
1494
|
+
* direct stream write there corrupts its cursor tracking.
|
|
1495
|
+
* @param orchestrator - The session's orchestrator instance, to emit
|
|
1496
|
+
* `crawlSessionNotice` from.
|
|
1322
1497
|
*/
|
|
1323
|
-
static #finalizeCrawlSession() {
|
|
1498
|
+
static #finalizeCrawlSession(orchestrator) {
|
|
1324
1499
|
const skipped = dnsBurnedHostShortCircuitCounter.count;
|
|
1325
1500
|
if (skipped > 0) {
|
|
1326
|
-
|
|
1327
|
-
|
|
1501
|
+
void orchestrator.emit('crawlSessionNotice', {
|
|
1502
|
+
message: `[preload] Short-circuited ${skipped} URL(s) on DNS-burned hosts`,
|
|
1503
|
+
});
|
|
1328
1504
|
}
|
|
1329
1505
|
const { confirmedCount, totalDurationMs } = networkOutageSummaryCounter;
|
|
1330
1506
|
if (confirmedCount > 0) {
|
|
1331
|
-
|
|
1332
|
-
|
|
1507
|
+
void orchestrator.emit('crawlSessionNotice', {
|
|
1508
|
+
message: `[network] ${confirmedCount} outage(s), ${Math.round(totalDurationMs / 1000)}s total`,
|
|
1509
|
+
});
|
|
1333
1510
|
}
|
|
1334
1511
|
networkOutageSummaryCounter.confirmedCount = 0;
|
|
1335
1512
|
networkOutageSummaryCounter.totalDurationMs = 0;
|