@nitpicker/crawler 0.18.0 → 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.
Files changed (140) hide show
  1. package/lib/append-retry-failed-common-setup-phases.d.ts +15 -0
  2. package/lib/append-retry-failed-common-setup-phases.js +21 -0
  3. package/lib/append-setup-phases.d.ts +17 -0
  4. package/lib/append-setup-phases.js +22 -0
  5. package/lib/archive/archive-accessor.d.ts +31 -5
  6. package/lib/archive/archive-accessor.js +46 -24
  7. package/lib/archive/archive.d.ts +115 -37
  8. package/lib/archive/archive.js +118 -52
  9. package/lib/archive/cache/extract-archive-to-cache.d.ts +10 -1
  10. package/lib/archive/cache/extract-archive-to-cache.js +14 -4
  11. package/lib/archive/create-adjunct-tables.d.ts +12 -2
  12. package/lib/archive/create-adjunct-tables.js +91 -29
  13. package/lib/archive/create-entity-tables.d.ts +5 -1
  14. package/lib/archive/create-entity-tables.js +6 -1
  15. package/lib/archive/database.d.ts +39 -9
  16. package/lib/archive/database.js +63 -18
  17. package/lib/archive/db-ops/lifecycle/init.d.ts +14 -2
  18. package/lib/archive/db-ops/lifecycle/init.js +29 -11
  19. package/lib/archive/db-ops/meta/get-custom-elements-of-page.d.ts +11 -0
  20. package/lib/archive/db-ops/meta/get-custom-elements-of-page.js +15 -0
  21. package/lib/archive/db-ops/meta/get-page-technologies-of-page.d.ts +11 -0
  22. package/lib/archive/db-ops/meta/get-page-technologies-of-page.js +15 -0
  23. package/lib/archive/db-ops/meta/get-technology-signals-of-page.d.ts +11 -0
  24. package/lib/archive/db-ops/meta/get-technology-signals-of-page.js +15 -0
  25. package/lib/archive/db-ops/pages/order/set-url-order.d.ts +6 -1
  26. package/lib/archive/db-ops/pages/order/set-url-order.js +7 -1
  27. package/lib/archive/db-ops/pages/read/build-page-query.js +1 -1
  28. package/lib/archive/db-ops/pages/reset/repromote-external-pages.d.ts +6 -1
  29. package/lib/archive/db-ops/pages/reset/repromote-external-pages.js +12 -3
  30. package/lib/archive/db-ops/pages/reset/reset-failed-pages.d.ts +6 -1
  31. package/lib/archive/db-ops/pages/reset/reset-failed-pages.js +10 -2
  32. package/lib/archive/db-ops/pages/write/insert-custom-elements.d.ts +27 -0
  33. package/lib/archive/db-ops/pages/write/insert-custom-elements.js +40 -0
  34. package/lib/archive/db-ops/pages/write/insert-page.d.ts +24 -4
  35. package/lib/archive/db-ops/pages/write/insert-page.js +40 -4
  36. package/lib/archive/db-ops/pages/write/insert-technologies.d.ts +25 -0
  37. package/lib/archive/db-ops/pages/write/insert-technologies.js +45 -0
  38. package/lib/archive/db-ops/pages/write/link-redirect-sources.d.ts +7 -1
  39. package/lib/archive/db-ops/pages/write/link-redirect-sources.js +8 -2
  40. package/lib/archive/db-ops/pages/write/update-page.d.ts +4 -1
  41. package/lib/archive/db-ops/pages/write/update-page.js +38 -16
  42. package/lib/archive/db-ops/resources/get-resource-url-list.d.ts +12 -1
  43. package/lib/archive/db-ops/resources/get-resource-url-list.js +43 -5
  44. package/lib/archive/filesystem/copy-file-with-progress.d.ts +20 -0
  45. package/lib/archive/filesystem/copy-file-with-progress.js +34 -0
  46. package/lib/archive/filesystem/parse-pax-path.d.ts +18 -0
  47. package/lib/archive/filesystem/parse-pax-path.js +47 -0
  48. package/lib/archive/filesystem/parse-tar-size-field.d.ts +13 -0
  49. package/lib/archive/filesystem/parse-tar-size-field.js +40 -0
  50. package/lib/archive/filesystem/peek-tar-top-dir.d.ts +10 -3
  51. package/lib/archive/filesystem/peek-tar-top-dir.js +166 -16
  52. package/lib/archive/filesystem/tar.d.ts +13 -1
  53. package/lib/archive/filesystem/tar.js +63 -7
  54. package/lib/archive/filesystem/untar.d.ts +13 -0
  55. package/lib/archive/filesystem/untar.js +68 -8
  56. package/lib/archive/init-schema.d.ts +1 -1
  57. package/lib/archive/init-schema.js +6 -4
  58. package/lib/archive/meta/compute-main-contents-denormalized.d.ts +13 -2
  59. package/lib/archive/meta/compute-main-contents-denormalized.js +15 -2
  60. package/lib/archive/meta/technologies/combine-technology-confidence.d.ts +46 -0
  61. package/lib/archive/meta/technologies/combine-technology-confidence.js +93 -0
  62. package/lib/archive/meta/technologies/convert-legacy-page-tags-to-inserts.d.ts +45 -0
  63. package/lib/archive/meta/technologies/convert-legacy-page-tags-to-inserts.js +53 -0
  64. package/lib/archive/meta/technologies/convert-tag-row-to-wappalyzer-signal.d.ts +33 -0
  65. package/lib/archive/meta/technologies/convert-tag-row-to-wappalyzer-signal.js +46 -0
  66. package/lib/archive/meta/technologies/extract-technologies-for-archive.d.ts +30 -0
  67. package/lib/archive/meta/technologies/extract-technologies-for-archive.js +27 -0
  68. package/lib/archive/meta/technologies/match-structural-technology-signals.d.ts +13 -0
  69. package/lib/archive/meta/technologies/match-structural-technology-signals.js +44 -0
  70. package/lib/archive/meta/technologies/normalize-wappalyzer-entries.d.ts +25 -0
  71. package/lib/archive/meta/technologies/normalize-wappalyzer-entries.js +35 -0
  72. package/lib/archive/meta/technologies/technology-signal-definitions.d.ts +43 -0
  73. package/lib/archive/meta/technologies/technology-signal-definitions.js +193 -0
  74. package/lib/archive/meta/technologies/types.d.ts +68 -0
  75. package/lib/archive/meta/technologies/types.js +13 -0
  76. package/lib/archive/meta/types.d.ts +97 -30
  77. package/lib/archive/migrate-content-items-alias-of-id.d.ts +6 -1
  78. package/lib/archive/migrate-content-items-alias-of-id.js +14 -3
  79. package/lib/archive/migrate-content-items-dedupe-cap-event-id.d.ts +6 -1
  80. package/lib/archive/migrate-content-items-dedupe-cap-event-id.js +14 -3
  81. package/lib/archive/migrate-info-main-content-selector.d.ts +6 -1
  82. package/lib/archive/migrate-info-main-content-selector.js +14 -3
  83. package/lib/archive/migrate-info-roots.d.ts +6 -1
  84. package/lib/archive/migrate-info-roots.js +14 -3
  85. package/lib/archive/migrate-inventory-runs-exclude-skipped.d.ts +6 -1
  86. package/lib/archive/migrate-inventory-runs-exclude-skipped.js +14 -3
  87. package/lib/archive/migrate-inventory-runs-invalid-skipped.d.ts +6 -1
  88. package/lib/archive/migrate-inventory-runs-invalid-skipped.js +14 -3
  89. package/lib/archive/migrate-main-contents-columns.d.ts +6 -1
  90. package/lib/archive/migrate-main-contents-columns.js +14 -3
  91. package/lib/archive/migrate-page-meta-body-hash.d.ts +6 -1
  92. package/lib/archive/migrate-page-meta-body-hash.js +14 -3
  93. package/lib/archive/migrate-page-meta-console-error-count.d.ts +6 -1
  94. package/lib/archive/migrate-page-meta-console-error-count.js +14 -3
  95. package/lib/archive/migrate-page-meta-custom-element-count.d.ts +33 -0
  96. package/lib/archive/migrate-page-meta-custom-element-count.js +51 -0
  97. package/lib/archive/migrate-page-tags-to-page-technologies.d.ts +39 -0
  98. package/lib/archive/migrate-page-tags-to-page-technologies.js +79 -0
  99. package/lib/archive/page.d.ts +37 -13
  100. package/lib/archive/page.js +45 -17
  101. package/lib/archive/retarget-legacy-fk-tables.d.ts +27 -14
  102. package/lib/archive/retarget-legacy-fk-tables.js +61 -15
  103. package/lib/archive/types.d.ts +18 -0
  104. package/lib/crawler/assert-puppeteer-shared-with-beholder.d.ts +24 -0
  105. package/lib/crawler/assert-puppeteer-shared-with-beholder.js +36 -0
  106. package/lib/crawler/capture-custom-elements.d.ts +33 -0
  107. package/lib/crawler/capture-custom-elements.js +39 -0
  108. package/lib/crawler/close-browser-safely.d.ts +3 -3
  109. package/lib/crawler/close-browser-safely.js +8 -17
  110. package/lib/crawler/collect-custom-elements.d.ts +33 -0
  111. package/lib/crawler/collect-custom-elements.js +110 -0
  112. package/lib/crawler/crawler.js +101 -46
  113. package/lib/crawler/fetch-destination.js +6 -13
  114. package/lib/crawler/find-package-dir.d.ts +24 -0
  115. package/lib/crawler/find-package-dir.js +39 -0
  116. package/lib/crawler/scan-js-resource-for-license-comment.d.ts +32 -0
  117. package/lib/crawler/scan-js-resource-for-license-comment.js +140 -0
  118. package/lib/crawler/scan-js-resources-for-technology-signals.d.ts +58 -0
  119. package/lib/crawler/scan-js-resources-for-technology-signals.js +196 -0
  120. package/lib/crawler/types.d.ts +23 -0
  121. package/lib/crawler-orchestrator.d.ts +53 -9
  122. package/lib/crawler-orchestrator.js +223 -46
  123. package/lib/crawler.d.ts +11 -1
  124. package/lib/crawler.js +9 -0
  125. package/lib/inventory-setup-phases.d.ts +32 -0
  126. package/lib/inventory-setup-phases.js +45 -0
  127. package/lib/resume-setup-phases.d.ts +16 -0
  128. package/lib/resume-setup-phases.js +24 -0
  129. package/lib/retry-failed-setup-phases.d.ts +19 -0
  130. package/lib/retry-failed-setup-phases.js +24 -0
  131. package/lib/setup-recovery-phase-labels.d.ts +21 -0
  132. package/lib/setup-recovery-phase-labels.js +24 -0
  133. package/lib/types.d.ts +145 -0
  134. package/package.json +12 -9
  135. package/lib/archive/db-ops/meta/get-tags-of-page.d.ts +0 -12
  136. package/lib/archive/db-ops/meta/get-tags-of-page.js +0 -28
  137. package/lib/archive/db-ops/pages/write/insert-tags.d.ts +0 -16
  138. package/lib/archive/db-ops/pages/write/insert-tags.js +0 -34
  139. package/lib/archive/meta/summarize-tags.d.ts +0 -16
  140. package/lib/archive/meta/summarize-tags.js +0 -33
@@ -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
+ }
@@ -301,6 +301,14 @@ export interface CrawlerEventTypes {
301
301
  * common case outside `crawl --inventory`. See {@link PageSource}.
302
302
  */
303
303
  source?: PageSource;
304
+ /**
305
+ * This page's body hash, precomputed by the crawler from `result.html`
306
+ * (non-null whenever `result.html.length > 0`, since this event is only
307
+ * emitted for internal pages). Forwarded through `Archive.setPage` to
308
+ * `update-page.ts` so `page_meta.body_hash` is written from this value
309
+ * instead of hashing the same html a second time.
310
+ */
311
+ bodyHash?: Buffer | null;
304
312
  };
305
313
  /**
306
314
  * Emitted when an external page (outside the crawl scope) has been scraped.
@@ -475,6 +483,21 @@ export interface CrawlerEventTypes {
475
483
  observedCount: number;
476
484
  };
477
485
  }
486
+ /**
487
+ * One Web Component (custom element) found inside a page's main-content
488
+ * region by {@link ./collect-custom-elements.ts}. Unlike beholder's eight
489
+ * `MainContentsData` categories (headings/images/tables/buttons/iframes/
490
+ * videos/audios/canvases), this is captured independently by nitpicker
491
+ * itself — see {@link ./capture-custom-elements.ts} for why.
492
+ */
493
+ export interface MainContentCustomElementCandidate {
494
+ /** The element's `nodeName` (always upper-cased in an HTML document, e.g. `MY-WIDGET`). */
495
+ nodeName: string;
496
+ /** The element's `id` attribute, or `null` when absent. */
497
+ elementId: string | null;
498
+ /** The element's class list, in DOM order. */
499
+ classList: string[];
500
+ }
478
501
  /**
479
502
  * Tunables for `NetworkOutageDetector`.
480
503
  */
@@ -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
- * const orchestrator = await CrawlerOrchestrator.crawling(['https://example.com'], { recursive: true });
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
- * the write completes successfully.
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 {};