@nitpicker/crawler 0.18.1 → 0.20.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 +140 -56
- 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/handle-scrape-end.js +19 -10
- package/lib/crawler/resolve-result-went-off-host.d.ts +34 -0
- package/lib/crawler/resolve-result-went-off-host.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 +34 -1
- 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
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import redirects from 'follow-redirects';
|
|
2
|
+
import { TECHNOLOGY_SIGNAL_DEFINITIONS } from '../archive/meta/technologies/technology-signal-definitions.js';
|
|
3
|
+
/** Default cap on bytes read from a JS resource before giving up on a match. */
|
|
4
|
+
const DEFAULT_BYTE_LIMIT = 8192;
|
|
5
|
+
/** Default network timeout, shorter than `fetchDestination`'s HEAD budget — this is a best-effort enrichment pass, not the crawl's critical path. */
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 8000;
|
|
7
|
+
const EVIDENCE_MAX_LENGTH = 200;
|
|
8
|
+
/** Definitions this scan tests against — only the `js-license-comment` signal type applies to JS resource bodies (the rest match HTML). */
|
|
9
|
+
const JS_LICENSE_COMMENT_DEFINITIONS = TECHNOLOGY_SIGNAL_DEFINITIONS.filter((def) => def.signalType === 'js-license-comment');
|
|
10
|
+
/**
|
|
11
|
+
* Reads up to `byteLimit` bytes from a JS resource's leading bytes and tests
|
|
12
|
+
* them against {@link TECHNOLOGY_SIGNAL_DEFINITIONS}' `js-license-comment`
|
|
13
|
+
* patterns (e.g. Vue's leading license-banner comment). Best-effort: any network
|
|
14
|
+
* failure, timeout, or non-2xx response resolves `null` rather than
|
|
15
|
+
* throwing — a single unreachable JS resource must not abort the enrichment
|
|
16
|
+
* pass over the rest of the archive's resources (see
|
|
17
|
+
* `scanJsResourcesForTechnologySignals`, this function's only caller).
|
|
18
|
+
*
|
|
19
|
+
* Not cached and not routed through `destinationCache` (unlike
|
|
20
|
+
* `fetchDestination`): callers are expected to persist the outcome in
|
|
21
|
+
* `technology_js_scan_cache`, keyed by `resourceId`, so a resource is never
|
|
22
|
+
* scanned twice across the archive's lifetime.
|
|
23
|
+
* @param url - The JS resource's absolute URL.
|
|
24
|
+
* @param options - Byte cap, timeout, and User-Agent overrides.
|
|
25
|
+
* @returns The first matching signal, or `null` when nothing matched (or
|
|
26
|
+
* the fetch failed).
|
|
27
|
+
* @example
|
|
28
|
+
* const signal = await scanJsResourceForLicenseComment('https://example.com/_astro/app.js');
|
|
29
|
+
* // { technology: 'Vue', signalType: 'js-license-comment', evidence: 'Vue.js license banner text', weight: 55, category: 'JavaScript frameworks' }
|
|
30
|
+
*/
|
|
31
|
+
export async function scanJsResourceForLicenseComment(url, options = {}) {
|
|
32
|
+
const byteLimit = options.byteLimit ?? DEFAULT_BYTE_LIMIT;
|
|
33
|
+
const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
34
|
+
const body = await readLeadingBytes(url, byteLimit, timeout, options.userAgent).catch(() => null);
|
|
35
|
+
if (body == null)
|
|
36
|
+
return null;
|
|
37
|
+
for (const def of JS_LICENSE_COMMENT_DEFINITIONS) {
|
|
38
|
+
const match = def.pattern.exec(body);
|
|
39
|
+
if (match) {
|
|
40
|
+
return {
|
|
41
|
+
technology: def.technology,
|
|
42
|
+
signalType: def.signalType,
|
|
43
|
+
evidence: match[0].slice(0, EVIDENCE_MAX_LENGTH),
|
|
44
|
+
weight: def.weight,
|
|
45
|
+
category: def.category ?? null,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Streams up to `byteLimit` bytes of a GET response body, destroying the
|
|
53
|
+
* connection once the cap is reached (or the stream ends first). Mirrors
|
|
54
|
+
* `fetchDestination`'s `titleBytesLimit` byte-cap technique (same
|
|
55
|
+
* `follow-redirects` + manual chunk accumulation + `req.destroy()` shape),
|
|
56
|
+
* but generic over "leading bytes as text" rather than title extraction —
|
|
57
|
+
* this module has no `PageData` to build.
|
|
58
|
+
* @param url - The absolute URL to fetch.
|
|
59
|
+
* @param byteLimit - Byte cap on the accumulated body.
|
|
60
|
+
* @param timeout - Milliseconds before the request is aborted.
|
|
61
|
+
* @param userAgent - Optional User-Agent header.
|
|
62
|
+
* @returns The accumulated body text, or `null` on a non-2xx response.
|
|
63
|
+
*/
|
|
64
|
+
async function readLeadingBytes(url, byteLimit, timeout, userAgent) {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const parsed = new URL(url);
|
|
67
|
+
const request = {
|
|
68
|
+
protocol: parsed.protocol,
|
|
69
|
+
hostname: parsed.hostname,
|
|
70
|
+
port: parsed.port || undefined,
|
|
71
|
+
path: `${parsed.pathname}${parsed.search}`,
|
|
72
|
+
method: 'GET',
|
|
73
|
+
trackRedirects: true,
|
|
74
|
+
headers: {
|
|
75
|
+
host: parsed.host,
|
|
76
|
+
...(userAgent ? { 'User-Agent': userAgent } : {}),
|
|
77
|
+
Accept: '*/*',
|
|
78
|
+
'Accept-Encoding': 'identity',
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
let req;
|
|
82
|
+
let destroyed = false;
|
|
83
|
+
let settled = false;
|
|
84
|
+
const timeoutHandle = setTimeout(() => {
|
|
85
|
+
if (settled)
|
|
86
|
+
return;
|
|
87
|
+
settled = true;
|
|
88
|
+
destroyed = true;
|
|
89
|
+
req.destroy();
|
|
90
|
+
resolve(null);
|
|
91
|
+
}, timeout);
|
|
92
|
+
const response = (res) => {
|
|
93
|
+
const status = res.statusCode ?? 0;
|
|
94
|
+
if (status < 200 || status >= 300) {
|
|
95
|
+
settled = true;
|
|
96
|
+
clearTimeout(timeoutHandle);
|
|
97
|
+
destroyed = true;
|
|
98
|
+
req.destroy();
|
|
99
|
+
resolve(null);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const chunks = [];
|
|
103
|
+
let totalBytes = 0;
|
|
104
|
+
res.on('data', (chunk) => {
|
|
105
|
+
if (settled)
|
|
106
|
+
return;
|
|
107
|
+
chunks.push(chunk);
|
|
108
|
+
totalBytes += chunk.length;
|
|
109
|
+
if (totalBytes >= byteLimit) {
|
|
110
|
+
settled = true;
|
|
111
|
+
clearTimeout(timeoutHandle);
|
|
112
|
+
destroyed = true;
|
|
113
|
+
req.destroy();
|
|
114
|
+
resolve(Buffer.concat(chunks).toString('utf8'));
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
res.on('end', () => {
|
|
118
|
+
if (settled)
|
|
119
|
+
return;
|
|
120
|
+
settled = true;
|
|
121
|
+
clearTimeout(timeoutHandle);
|
|
122
|
+
resolve(Buffer.concat(chunks).toString('utf8'));
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
if (parsed.protocol === 'https:') {
|
|
126
|
+
req = redirects.https.request(request, response);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
req = redirects.http.request(request, response);
|
|
130
|
+
}
|
|
131
|
+
req.on('error', (error) => {
|
|
132
|
+
clearTimeout(timeoutHandle);
|
|
133
|
+
// Ignore errors caused by our own intentional req.destroy() above.
|
|
134
|
+
if (destroyed)
|
|
135
|
+
return;
|
|
136
|
+
reject(error);
|
|
137
|
+
});
|
|
138
|
+
req.end();
|
|
139
|
+
});
|
|
140
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ArchiveAccessor } from '../archive/archive-accessor.js';
|
|
2
|
+
/** Options for {@link scanJsResourcesForTechnologySignals}. */
|
|
3
|
+
export interface ScanJsResourcesForTechnologySignalsOptions {
|
|
4
|
+
/** Maximum concurrent network fetches. Defaults to {@link DEFAULT_CONCURRENCY}. */
|
|
5
|
+
concurrency?: number;
|
|
6
|
+
/** Forwarded to `scanJsResourceForLicenseComment`. */
|
|
7
|
+
byteLimit?: number;
|
|
8
|
+
/** Forwarded to `scanJsResourceForLicenseComment`. */
|
|
9
|
+
timeout?: number;
|
|
10
|
+
/** Forwarded to `scanJsResourceForLicenseComment`. */
|
|
11
|
+
userAgent?: string;
|
|
12
|
+
/** Called after each resource finishes scanning (matched or not). */
|
|
13
|
+
onProgress?: (done: number, total: number) => void;
|
|
14
|
+
}
|
|
15
|
+
/** Outcome counters returned by {@link scanJsResourcesForTechnologySignals}. */
|
|
16
|
+
export interface ScanJsResourcesForTechnologySignalsResult {
|
|
17
|
+
/** JS resources eligible for scanning (internal, not yet cached). */
|
|
18
|
+
candidateCount: number;
|
|
19
|
+
/** Resources actually scanned this run (equals `candidateCount` barring a mid-run crash). */
|
|
20
|
+
scannedCount: number;
|
|
21
|
+
/** Resources whose leading bytes matched a known license comment. */
|
|
22
|
+
matchedCount: number;
|
|
23
|
+
/** Distinct pages whose `technology_signals` / `page_technologies` were updated as a result. */
|
|
24
|
+
pagesUpdatedCount: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Post-crawl network enrichment (distinct from crawl-time extraction and
|
|
28
|
+
* from read-model-time backfill — see ARCHITECTURE.md): re-fetches the
|
|
29
|
+
* leading bytes of every not-yet-scanned internal JS resource, tests them
|
|
30
|
+
* for a known technology's license comment, and folds any match into the
|
|
31
|
+
* referencing pages' `technology_signals` / `page_technologies`.
|
|
32
|
+
*
|
|
33
|
+
* Each resource is scanned at most once ever, across the archive's whole
|
|
34
|
+
* lifetime — outcomes (including non-matches) are recorded in
|
|
35
|
+
* `technology_js_scan_cache` keyed by `resourceId`, so a later
|
|
36
|
+
* `--append`/`--retry-failed` run only pays the network cost for resources
|
|
37
|
+
* discovered since the last run.
|
|
38
|
+
*
|
|
39
|
+
* A single resource can be referenced by many pages (a shared bundle); a
|
|
40
|
+
* match is applied to every one of them independently. Network scanning
|
|
41
|
+
* runs at bounded concurrency; the per-page DB recombination that follows
|
|
42
|
+
* runs after every scan has settled, never concurrently for the same page,
|
|
43
|
+
* so two resources that both resolve to the same page cannot race each
|
|
44
|
+
* other's read-modify-write.
|
|
45
|
+
*
|
|
46
|
+
* Best-effort like `scanJsResourceForLicenseComment`: an unreachable
|
|
47
|
+
* resource is recorded as a non-match (cached as scanned, `technology:
|
|
48
|
+
* null`) rather than retried or surfaced as an error — a flaky CDN must not
|
|
49
|
+
* block the rest of the archive's enrichment, and the resource will not be
|
|
50
|
+
* retried until `technology_js_scan_cache` itself is cleared.
|
|
51
|
+
* @param accessor - The archive to enrich.
|
|
52
|
+
* @param options - Concurrency, byte-cap, timeout, and progress overrides.
|
|
53
|
+
* @returns Counters describing what was scanned, matched, and updated.
|
|
54
|
+
* @example
|
|
55
|
+
* const result = await scanJsResourcesForTechnologySignals(archive);
|
|
56
|
+
* // { candidateCount: 42, scannedCount: 42, matchedCount: 3, pagesUpdatedCount: 57 }
|
|
57
|
+
*/
|
|
58
|
+
export declare function scanJsResourcesForTechnologySignals(accessor: ArchiveAccessor, options?: ScanJsResourcesForTechnologySignalsOptions): Promise<ScanJsResourcesForTechnologySignalsResult>;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { combineTechnologyConfidence } from '../archive/meta/technologies/combine-technology-confidence.js';
|
|
2
|
+
import { scanJsResourceForLicenseComment } from './scan-js-resource-for-license-comment.js';
|
|
3
|
+
/**
|
|
4
|
+
* Content-Type strings this scan treats as JavaScript. Deliberately
|
|
5
|
+
* duplicated (not imported) from `@nitpicker/query`'s `content-type-rules.ts`
|
|
6
|
+
* `'javascript'` category rule: the crawler package must not depend back on
|
|
7
|
+
* query (see `ensure-viewer-read-model-quietly.ts`'s docs for the same
|
|
8
|
+
* boundary), and this list is small and stable enough that duplication is
|
|
9
|
+
* cheaper than introducing a shared third package for it.
|
|
10
|
+
*/
|
|
11
|
+
const JS_CONTENT_TYPES = [
|
|
12
|
+
'text/javascript',
|
|
13
|
+
'application/javascript',
|
|
14
|
+
'application/x-javascript',
|
|
15
|
+
'application/ecmascript',
|
|
16
|
+
];
|
|
17
|
+
const DEFAULT_CONCURRENCY = 4;
|
|
18
|
+
/**
|
|
19
|
+
* Runs `worker` over `items` with at most `concurrency` calls in flight at
|
|
20
|
+
* once. A minimal worker-pool, not a chunk-then-parallelize helper like
|
|
21
|
+
* `eachSplitted` (which parallelizes across chunks, not within one) — this
|
|
22
|
+
* module needs a true cap on simultaneous outbound HTTP connections.
|
|
23
|
+
* @param items - The items to process.
|
|
24
|
+
* @param concurrency - Maximum simultaneous `worker` calls.
|
|
25
|
+
* @param worker - Called once per item; errors propagate to the caller.
|
|
26
|
+
*/
|
|
27
|
+
async function runWithConcurrency(items, concurrency, worker) {
|
|
28
|
+
let nextIndex = 0;
|
|
29
|
+
/**
|
|
30
|
+
*
|
|
31
|
+
*/
|
|
32
|
+
async function runNext() {
|
|
33
|
+
const index = nextIndex++;
|
|
34
|
+
if (index >= items.length)
|
|
35
|
+
return;
|
|
36
|
+
await worker(items[index]);
|
|
37
|
+
await runNext();
|
|
38
|
+
}
|
|
39
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => runNext()));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Re-derives one page's `technology_signals` + `page_technologies` rows from
|
|
43
|
+
* its existing persisted signals plus newly-discovered `js-license-comment`
|
|
44
|
+
* signals, then scoped-replaces both tables — the same full-per-page-replace
|
|
45
|
+
* invariant `insertTechnologies` follows (see its docs: the two tables are
|
|
46
|
+
* never updated independently).
|
|
47
|
+
*
|
|
48
|
+
* `technology_signals` does not persist `category`/`version` (only
|
|
49
|
+
* `page_technologies` does — see `create-adjunct-tables.ts`), so a technology
|
|
50
|
+
* whose category/version came from a signal not re-derivable from the
|
|
51
|
+
* persisted rows (a `wappalyzer` or `meta-generator` signal, both computed at
|
|
52
|
+
* crawl time from data this function does not have) would otherwise regress
|
|
53
|
+
* to `null` here. The existing `page_technologies` row is read first and its
|
|
54
|
+
* `category`/`version` fall back in wherever the freshly combined result has
|
|
55
|
+
* none.
|
|
56
|
+
* @param knex - The archive's Knex instance.
|
|
57
|
+
* @param pageId - The page to update.
|
|
58
|
+
* @param newSignals - Newly-discovered signals for this page (from JS
|
|
59
|
+
* resources it references).
|
|
60
|
+
*/
|
|
61
|
+
async function applyNewSignalsToPage(knex, pageId, newSignals) {
|
|
62
|
+
await knex.transaction(async (trx) => {
|
|
63
|
+
const existingSignalRows = await trx('technology_signals')
|
|
64
|
+
.where('pageId', pageId)
|
|
65
|
+
.select('technology', 'signalType', 'evidence', 'weight');
|
|
66
|
+
const existingTechnologyRows = await trx('page_technologies')
|
|
67
|
+
.where('pageId', pageId)
|
|
68
|
+
.select('technology', 'category', 'version');
|
|
69
|
+
const existingMetaByTechnology = new Map(existingTechnologyRows.map((row) => [row.technology, row]));
|
|
70
|
+
const allSignals = [...existingSignalRows, ...newSignals];
|
|
71
|
+
const technologies = combineTechnologyConfidence(allSignals).map((t) => {
|
|
72
|
+
const previous = existingMetaByTechnology.get(t.technology);
|
|
73
|
+
return {
|
|
74
|
+
...t,
|
|
75
|
+
category: t.category ?? previous?.category ?? null,
|
|
76
|
+
version: t.version ?? previous?.version ?? null,
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
await trx('technology_signals').where('pageId', pageId).delete();
|
|
80
|
+
await trx('page_technologies').where('pageId', pageId).delete();
|
|
81
|
+
await trx('technology_signals').insert(allSignals.map((s) => ({
|
|
82
|
+
pageId,
|
|
83
|
+
technology: s.technology,
|
|
84
|
+
signalType: s.signalType,
|
|
85
|
+
evidence: s.evidence,
|
|
86
|
+
weight: s.weight,
|
|
87
|
+
})));
|
|
88
|
+
if (technologies.length > 0) {
|
|
89
|
+
await trx('page_technologies').insert(technologies.map((t) => ({ pageId, ...t })));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Post-crawl network enrichment (distinct from crawl-time extraction and
|
|
95
|
+
* from read-model-time backfill — see ARCHITECTURE.md): re-fetches the
|
|
96
|
+
* leading bytes of every not-yet-scanned internal JS resource, tests them
|
|
97
|
+
* for a known technology's license comment, and folds any match into the
|
|
98
|
+
* referencing pages' `technology_signals` / `page_technologies`.
|
|
99
|
+
*
|
|
100
|
+
* Each resource is scanned at most once ever, across the archive's whole
|
|
101
|
+
* lifetime — outcomes (including non-matches) are recorded in
|
|
102
|
+
* `technology_js_scan_cache` keyed by `resourceId`, so a later
|
|
103
|
+
* `--append`/`--retry-failed` run only pays the network cost for resources
|
|
104
|
+
* discovered since the last run.
|
|
105
|
+
*
|
|
106
|
+
* A single resource can be referenced by many pages (a shared bundle); a
|
|
107
|
+
* match is applied to every one of them independently. Network scanning
|
|
108
|
+
* runs at bounded concurrency; the per-page DB recombination that follows
|
|
109
|
+
* runs after every scan has settled, never concurrently for the same page,
|
|
110
|
+
* so two resources that both resolve to the same page cannot race each
|
|
111
|
+
* other's read-modify-write.
|
|
112
|
+
*
|
|
113
|
+
* Best-effort like `scanJsResourceForLicenseComment`: an unreachable
|
|
114
|
+
* resource is recorded as a non-match (cached as scanned, `technology:
|
|
115
|
+
* null`) rather than retried or surfaced as an error — a flaky CDN must not
|
|
116
|
+
* block the rest of the archive's enrichment, and the resource will not be
|
|
117
|
+
* retried until `technology_js_scan_cache` itself is cleared.
|
|
118
|
+
* @param accessor - The archive to enrich.
|
|
119
|
+
* @param options - Concurrency, byte-cap, timeout, and progress overrides.
|
|
120
|
+
* @returns Counters describing what was scanned, matched, and updated.
|
|
121
|
+
* @example
|
|
122
|
+
* const result = await scanJsResourcesForTechnologySignals(archive);
|
|
123
|
+
* // { candidateCount: 42, scannedCount: 42, matchedCount: 3, pagesUpdatedCount: 57 }
|
|
124
|
+
*/
|
|
125
|
+
export async function scanJsResourcesForTechnologySignals(accessor, options = {}) {
|
|
126
|
+
const knex = accessor.getKnex();
|
|
127
|
+
const concurrency = Math.max(options.concurrency ?? DEFAULT_CONCURRENCY, 1);
|
|
128
|
+
const candidates = await knex('resource_items as ri')
|
|
129
|
+
.join('url_refs as ur', 'ur.id', 'ri.url_id')
|
|
130
|
+
.leftJoin('content_type_refs as ctr', 'ctr.id', 'ri.content_type_id')
|
|
131
|
+
.leftJoin('technology_js_scan_cache as cache', 'cache.resourceId', 'ri.id')
|
|
132
|
+
.where('ri.is_external', 0)
|
|
133
|
+
.whereNull('cache.resourceId')
|
|
134
|
+
.where((qb) => {
|
|
135
|
+
qb.whereIn('ctr.raw', JS_CONTENT_TYPES)
|
|
136
|
+
.orWhere('ur.url', 'like', '%.js')
|
|
137
|
+
.orWhere('ur.url', 'like', '%.js?%')
|
|
138
|
+
.orWhere('ur.url', 'like', '%.mjs')
|
|
139
|
+
.orWhere('ur.url', 'like', '%.mjs?%');
|
|
140
|
+
})
|
|
141
|
+
.select('ri.id as resourceId', 'ur.url as url');
|
|
142
|
+
let scannedCount = 0;
|
|
143
|
+
const matchesByResourceId = new Map();
|
|
144
|
+
await runWithConcurrency(candidates, concurrency, async (candidate) => {
|
|
145
|
+
const signal = await scanJsResourceForLicenseComment(candidate.url, {
|
|
146
|
+
byteLimit: options.byteLimit,
|
|
147
|
+
timeout: options.timeout,
|
|
148
|
+
userAgent: options.userAgent,
|
|
149
|
+
});
|
|
150
|
+
scannedCount++;
|
|
151
|
+
options.onProgress?.(scannedCount, candidates.length);
|
|
152
|
+
await knex('technology_js_scan_cache').insert({
|
|
153
|
+
resourceId: candidate.resourceId,
|
|
154
|
+
scannedAt: Date.now(),
|
|
155
|
+
technology: signal?.technology ?? null,
|
|
156
|
+
evidence: signal?.evidence ?? null,
|
|
157
|
+
});
|
|
158
|
+
if (signal) {
|
|
159
|
+
matchesByResourceId.set(candidate.resourceId, signal);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
if (matchesByResourceId.size === 0) {
|
|
163
|
+
return {
|
|
164
|
+
candidateCount: candidates.length,
|
|
165
|
+
scannedCount,
|
|
166
|
+
matchedCount: 0,
|
|
167
|
+
pagesUpdatedCount: 0,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const edges = await knex('resource_ref_edges')
|
|
171
|
+
.whereIn('resource_id', [...matchesByResourceId.keys()])
|
|
172
|
+
.select('resource_id', 'page_id');
|
|
173
|
+
const newSignalsByPageId = new Map();
|
|
174
|
+
for (const edge of edges) {
|
|
175
|
+
const signal = matchesByResourceId.get(edge.resource_id);
|
|
176
|
+
if (!signal)
|
|
177
|
+
continue;
|
|
178
|
+
const list = newSignalsByPageId.get(edge.page_id);
|
|
179
|
+
if (list) {
|
|
180
|
+
list.push(signal);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
newSignalsByPageId.set(edge.page_id, [signal]);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const affectedPageIds = [...newSignalsByPageId.keys()];
|
|
187
|
+
await runWithConcurrency(affectedPageIds, concurrency, async (pageId) => {
|
|
188
|
+
await applyNewSignalsToPage(knex, pageId, newSignalsByPageId.get(pageId));
|
|
189
|
+
});
|
|
190
|
+
return {
|
|
191
|
+
candidateCount: candidates.length,
|
|
192
|
+
scannedCount,
|
|
193
|
+
matchedCount: matchesByResourceId.size,
|
|
194
|
+
pagesUpdatedCount: affectedPageIds.length,
|
|
195
|
+
};
|
|
196
|
+
}
|
package/lib/crawler/types.d.ts
CHANGED
|
@@ -42,8 +42,18 @@ export interface RedirectEdgeResult {
|
|
|
42
42
|
* The outcome of {@link Crawler.#scrapePage}: either a normal scrape result from
|
|
43
43
|
* the browser/HEAD pipeline, or a {@link RedirectEdgeResult} when the URL's
|
|
44
44
|
* redirect destination was already rendered and only the edge needs recording.
|
|
45
|
+
*
|
|
46
|
+
* The non-redirect-edge member is {@link BrowserScrapeResult}, not bare
|
|
47
|
+
* beholder `ScrapeResult` — every `#scrapePage` return site is either a
|
|
48
|
+
* `_launchBrowserAndScrape` call (which returns `BrowserScrapeResult`) or a
|
|
49
|
+
* plain `ScrapeResult`-shaped literal (structurally compatible, since
|
|
50
|
+
* `postNavigationUrl` is optional). Callers that reach a `type: 'error'`
|
|
51
|
+
* result and need to know whether the browser ended up off-host before
|
|
52
|
+
* failing (`pageData` is absent on error, so `pageData.isExternal` is not
|
|
53
|
+
* available) read `postNavigationUrl` for that signal — see
|
|
54
|
+
* `resolveResultWentOffHost`.
|
|
45
55
|
*/
|
|
46
|
-
export type ScrapeOutcome =
|
|
56
|
+
export type ScrapeOutcome = BrowserScrapeResult | RedirectEdgeResult;
|
|
47
57
|
/**
|
|
48
58
|
* Internal envelope returned by {@link Crawler.#launchBrowserAndScrape} that
|
|
49
59
|
* augments beholder's {@link ScrapeResult} with the puppeteer-side
|
|
@@ -301,6 +311,14 @@ export interface CrawlerEventTypes {
|
|
|
301
311
|
* common case outside `crawl --inventory`. See {@link PageSource}.
|
|
302
312
|
*/
|
|
303
313
|
source?: PageSource;
|
|
314
|
+
/**
|
|
315
|
+
* This page's body hash, precomputed by the crawler from `result.html`
|
|
316
|
+
* (non-null whenever `result.html.length > 0`, since this event is only
|
|
317
|
+
* emitted for internal pages). Forwarded through `Archive.setPage` to
|
|
318
|
+
* `update-page.ts` so `page_meta.body_hash` is written from this value
|
|
319
|
+
* instead of hashing the same html a second time.
|
|
320
|
+
*/
|
|
321
|
+
bodyHash?: Buffer | null;
|
|
304
322
|
};
|
|
305
323
|
/**
|
|
306
324
|
* Emitted when an external page (outside the crawl scope) has been scraped.
|
|
@@ -475,6 +493,21 @@ export interface CrawlerEventTypes {
|
|
|
475
493
|
observedCount: number;
|
|
476
494
|
};
|
|
477
495
|
}
|
|
496
|
+
/**
|
|
497
|
+
* One Web Component (custom element) found inside a page's main-content
|
|
498
|
+
* region by {@link ./collect-custom-elements.ts}. Unlike beholder's eight
|
|
499
|
+
* `MainContentsData` categories (headings/images/tables/buttons/iframes/
|
|
500
|
+
* videos/audios/canvases), this is captured independently by nitpicker
|
|
501
|
+
* itself — see {@link ./capture-custom-elements.ts} for why.
|
|
502
|
+
*/
|
|
503
|
+
export interface MainContentCustomElementCandidate {
|
|
504
|
+
/** The element's `nodeName` (always upper-cased in an HTML document, e.g. `MY-WIDGET`). */
|
|
505
|
+
nodeName: string;
|
|
506
|
+
/** The element's `id` attribute, or `null` when absent. */
|
|
507
|
+
elementId: string | null;
|
|
508
|
+
/** The element's class list, in DOM order. */
|
|
509
|
+
classList: string[];
|
|
510
|
+
}
|
|
478
511
|
/**
|
|
479
512
|
* Tunables for `NetworkOutageDetector`.
|
|
480
513
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Config } from './archive/types.js';
|
|
2
2
|
import type { NetworkProbe } from './crawler/probe-network.js';
|
|
3
3
|
import type { InventoryMode } from './crawler/types.js';
|
|
4
|
-
import type { CrawlEvent } from './types.js';
|
|
4
|
+
import type { CrawlEvent, SetupProgressCallbacks } from './types.js';
|
|
5
5
|
import type { ExURL } from '@d-zero/shared/parse-url';
|
|
6
6
|
import { TypedAwaitEventEmitter as EventEmitter } from '@d-zero/shared/typed-await-event-emitter';
|
|
7
7
|
import Archive from './archive/archive.js';
|
|
@@ -111,10 +111,13 @@ interface InventorySource {
|
|
|
111
111
|
* writes the final archive file. It emits events defined by {@link CrawlEvent}.
|
|
112
112
|
*
|
|
113
113
|
* Instances are created via the static factory methods {@link CrawlerOrchestrator.crawling}
|
|
114
|
-
* or {@link CrawlerOrchestrator.resume}; the constructor is private.
|
|
114
|
+
* or {@link CrawlerOrchestrator.resume}; the constructor is private. Implements
|
|
115
|
+
* `Symbol.asyncDispose` so callers can use `await using` to close the archive
|
|
116
|
+
* and reap zombie Chromium processes on scope exit instead of a manual
|
|
117
|
+
* `try`/`finally` around `archive.close()` + `garbageCollect()`.
|
|
115
118
|
* @example
|
|
116
119
|
* ```ts
|
|
117
|
-
*
|
|
120
|
+
* await using orchestrator = await CrawlerOrchestrator.crawling(['https://example.com'], { recursive: true });
|
|
118
121
|
* await orchestrator.write();
|
|
119
122
|
* ```
|
|
120
123
|
*/
|
|
@@ -125,6 +128,23 @@ export declare class CrawlerOrchestrator extends EventEmitter<CrawlEvent> {
|
|
|
125
128
|
*/
|
|
126
129
|
get archive(): Archive;
|
|
127
130
|
private constructor();
|
|
131
|
+
/**
|
|
132
|
+
* Enables `await using orchestrator = ...`. Closes the archive (write
|
|
133
|
+
* or remove tmpDir + release the lock, per {@link Archive.close}) and
|
|
134
|
+
* then reaps any zombie Chromium processes via {@link garbageCollect} —
|
|
135
|
+
* the same two-step teardown every CLI crawl command previously
|
|
136
|
+
* repeated by hand in a `finally` block.
|
|
137
|
+
*
|
|
138
|
+
* Relays `Archive.close()`'s recovery-write progress (issue #294) as
|
|
139
|
+
* `recoveringArchiveWrite`/`writeStep`/`writeTarProgress` — the same
|
|
140
|
+
* events `write()` emits — for the rare case where the file doesn't
|
|
141
|
+
* exist on disk yet at dispose time (e.g. an explicit `write()` call
|
|
142
|
+
* threw partway through). A CLI listener whose display is still open at
|
|
143
|
+
* that point (it hadn't yet seen `writeFileEnd`) picks these up for
|
|
144
|
+
* free; one that already tore down after the earlier failure silently
|
|
145
|
+
* drops them, same as any other post-close display update.
|
|
146
|
+
*/
|
|
147
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
128
148
|
/**
|
|
129
149
|
* Abort the current crawl operation.
|
|
130
150
|
*
|
|
@@ -173,8 +193,11 @@ export declare class CrawlerOrchestrator extends EventEmitter<CrawlEvent> {
|
|
|
173
193
|
* the time `write()` is called those tables are already populated.
|
|
174
194
|
* This method just tars.
|
|
175
195
|
*
|
|
176
|
-
* Emits `writeFileStart` before writing and `writeFileEnd` after
|
|
177
|
-
*
|
|
196
|
+
* Emits `writeFileStart` before writing and `writeFileEnd` after the
|
|
197
|
+
* write completes successfully. Also relays `Archive.write()`'s
|
|
198
|
+
* per-step (`writeStep`) and tar-byte (`writeTarProgress`) progress
|
|
199
|
+
* (issue #294) — tarring a 15 GB+ archive can take minutes, and without
|
|
200
|
+
* these events a CLI listener has no way to show it isn't hung.
|
|
178
201
|
*/
|
|
179
202
|
write(): Promise<void>;
|
|
180
203
|
/**
|
|
@@ -211,10 +234,14 @@ export declare class CrawlerOrchestrator extends EventEmitter<CrawlEvent> {
|
|
|
211
234
|
* @param newUrls - New root URLs to add and crawl.
|
|
212
235
|
* @param options - Optional config overrides applied on top of the archived config.
|
|
213
236
|
* @param initializedCallback - Optional callback invoked after initialization but before crawling resumes.
|
|
237
|
+
* @param setupProgress - Optional progress callbacks for the setup phase
|
|
238
|
+
* (untar, `.bak` copy, repromote, state rebuild) that runs before
|
|
239
|
+
* `initializedCallback` — see {@link SetupProgressCallbacks} for why
|
|
240
|
+
* this can't go through the orchestrator's event emitter (issue #294).
|
|
214
241
|
* @returns The orchestrator instance after the append crawl completes.
|
|
215
242
|
* @throws {Error} When `newUrls` is empty, the archive is in list mode, or it cannot be parsed.
|
|
216
243
|
*/
|
|
217
|
-
static append(archivePath: string, newUrls: string[], options?: Partial<CrawlConfig>, initializedCallback?: CrawlInitializedCallback): Promise<CrawlerOrchestrator>;
|
|
244
|
+
static append(archivePath: string, newUrls: string[], options?: Partial<CrawlConfig>, initializedCallback?: CrawlInitializedCallback, setupProgress?: SetupProgressCallbacks): Promise<CrawlerOrchestrator>;
|
|
218
245
|
/**
|
|
219
246
|
* Inventory mode: cross-reference a user-supplied URL list against an
|
|
220
247
|
* existing `.nitpicker` archive and import ONLY the URLs that are not yet
|
|
@@ -292,10 +319,15 @@ export declare class CrawlerOrchestrator extends EventEmitter<CrawlEvent> {
|
|
|
292
319
|
* URLs. Pass `null` for programmatic callers that built
|
|
293
320
|
* `inventoryUrls` in-memory; the audit row's `source_file_sha256`
|
|
294
321
|
* column will be `NULL` and no source list is archived.
|
|
322
|
+
* @param setupProgress - Optional progress callbacks for the setup phase
|
|
323
|
+
* (untar, scope classification, bulk inserts, state rebuild) that runs
|
|
324
|
+
* before `initializedCallback` — see {@link SetupProgressCallbacks} for
|
|
325
|
+
* why this can't go through the orchestrator's event emitter (issue
|
|
326
|
+
* #294).
|
|
295
327
|
* @returns The orchestrator instance after a successful inventory pass.
|
|
296
328
|
* @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.
|
|
297
329
|
*/
|
|
298
|
-
static inventory(archivePath: string, inventoryUrls: string[], options?: Partial<CrawlConfig>, initializedCallback?: CrawlInitializedCallback, source?: InventorySource | null): Promise<CrawlerOrchestrator>;
|
|
330
|
+
static inventory(archivePath: string, inventoryUrls: string[], options?: Partial<CrawlConfig>, initializedCallback?: CrawlInitializedCallback, source?: InventorySource | null, setupProgress?: SetupProgressCallbacks): Promise<CrawlerOrchestrator>;
|
|
299
331
|
/**
|
|
300
332
|
* Re-fetch previously-failed pages in an existing `.nitpicker` archive.
|
|
301
333
|
*
|
|
@@ -326,10 +358,14 @@ export declare class CrawlerOrchestrator extends EventEmitter<CrawlEvent> {
|
|
|
326
358
|
* @param archivePath - Absolute or relative path to the existing `.nitpicker`.
|
|
327
359
|
* @param options - Optional config overrides applied on top of the archived config.
|
|
328
360
|
* @param initializedCallback - Optional callback invoked after initialization but before crawling resumes.
|
|
361
|
+
* @param setupProgress - Optional progress callbacks for the setup phase
|
|
362
|
+
* (untar, `.bak` copy, reset, state rebuild) that runs before
|
|
363
|
+
* `initializedCallback` — see {@link SetupProgressCallbacks} for why
|
|
364
|
+
* this can't go through the orchestrator's event emitter (issue #294).
|
|
329
365
|
* @returns The orchestrator instance after the retry crawl completes.
|
|
330
366
|
* @throws {Error} When the archive is in list mode or has no parseable roots.
|
|
331
367
|
*/
|
|
332
|
-
static retryFailed(archivePath: string, options?: Partial<CrawlConfig>, initializedCallback?: CrawlInitializedCallback): Promise<CrawlerOrchestrator>;
|
|
368
|
+
static retryFailed(archivePath: string, options?: Partial<CrawlConfig>, initializedCallback?: CrawlInitializedCallback, setupProgress?: SetupProgressCallbacks): Promise<CrawlerOrchestrator>;
|
|
333
369
|
/**
|
|
334
370
|
* Resume a previously interrupted crawl from an existing archive file.
|
|
335
371
|
*
|
|
@@ -339,9 +375,17 @@ export declare class CrawlerOrchestrator extends EventEmitter<CrawlEvent> {
|
|
|
339
375
|
* @param stubPath - Path to the existing archive file to resume from.
|
|
340
376
|
* @param options - Optional configuration overrides to apply on top of the archived config.
|
|
341
377
|
* @param initializedCallback - Optional callback invoked after initialization but before crawling resumes.
|
|
378
|
+
* @param setupProgress - Optional progress callbacks for the setup phase
|
|
379
|
+
* (self-healing migrations, state rebuild) that runs before
|
|
380
|
+
* `initializedCallback` — see {@link SetupProgressCallbacks} for why
|
|
381
|
+
* this can't go through the orchestrator's event emitter (issue #294).
|
|
382
|
+
* No `onExtractProgress`/`onCopyProgress`: unlike `append`/`inventory`/
|
|
383
|
+
* `retryFailed`, `resume` reconnects to an existing tmpDir (no untar)
|
|
384
|
+
* and takes no `.bak` (nothing to restore — the interrupted crawl's
|
|
385
|
+
* tmpDir IS the source of truth).
|
|
342
386
|
* @returns A promise that resolves to the CrawlerOrchestrator instance after crawling completes.
|
|
343
387
|
* @throws {Error} If the archived URL is invalid.
|
|
344
388
|
*/
|
|
345
|
-
static resume(stubPath: string, options?: Partial<CrawlConfig>, initializedCallback?: CrawlInitializedCallback): Promise<CrawlerOrchestrator>;
|
|
389
|
+
static resume(stubPath: string, options?: Partial<CrawlConfig>, initializedCallback?: CrawlInitializedCallback, setupProgress?: SetupProgressCallbacks): Promise<CrawlerOrchestrator>;
|
|
346
390
|
}
|
|
347
391
|
export {};
|