@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,15 @@
1
+ /**
2
+ * The setup-phase labels `append()` and `retryFailed()` announce identically
3
+ * — everything except the one step in between that names what each mode is
4
+ * actually doing (`'Repromoting external pages'` vs `'Resetting failed
5
+ * pages'`) — split into a `prefix` (before that step) and `suffix` (after
6
+ * it). Single source of truth for `APPEND_SETUP_PHASES` /
7
+ * `RETRY_FAILED_SETUP_PHASES` (issue #294): the two were previously
8
+ * hand-written as independent nine-entry literal arrays sharing eight of
9
+ * nine entries verbatim, with nothing to catch one drifting from the other
10
+ * if a shared phase were added, renamed, or reordered in only one place.
11
+ */
12
+ export declare const APPEND_RETRY_FAILED_COMMON_SETUP_PHASES: {
13
+ readonly prefix: readonly ["Extracting archive", "Loading archive config", "Backing up archive"];
14
+ readonly suffix: readonly ["Loading dedupe-cap shape keys", "Loading crawl state", "Loading resource list", "Loading scraped page count", "Restoring crawl state"];
15
+ };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The setup-phase labels `append()` and `retryFailed()` announce identically
3
+ * — everything except the one step in between that names what each mode is
4
+ * actually doing (`'Repromoting external pages'` vs `'Resetting failed
5
+ * pages'`) — split into a `prefix` (before that step) and `suffix` (after
6
+ * it). Single source of truth for `APPEND_SETUP_PHASES` /
7
+ * `RETRY_FAILED_SETUP_PHASES` (issue #294): the two were previously
8
+ * hand-written as independent nine-entry literal arrays sharing eight of
9
+ * nine entries verbatim, with nothing to catch one drifting from the other
10
+ * if a shared phase were added, renamed, or reordered in only one place.
11
+ */
12
+ export const APPEND_RETRY_FAILED_COMMON_SETUP_PHASES = {
13
+ prefix: ['Extracting archive', 'Loading archive config', 'Backing up archive'],
14
+ suffix: [
15
+ 'Loading dedupe-cap shape keys',
16
+ 'Loading crawl state',
17
+ 'Loading resource list',
18
+ 'Loading scraped page count',
19
+ 'Restoring crawl state',
20
+ ],
21
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Ordered `SetupProgressCallbacks.onPhase` labels `CrawlerOrchestrator.append`
3
+ * announces, in call order, before its `initializedCallback` fires. Single
4
+ * source of truth for both the `onPhase` call sites in
5
+ * `crawler-orchestrator.ts` and the CLI's setup task list (`create-setup-task-list.ts`),
6
+ * which pre-builds one task-list row per entry — duplicating this sequence by
7
+ * hand at the CLI layer would silently drift the moment a phase is added,
8
+ * renamed, or reordered here. Shares its prefix/suffix with
9
+ * `RETRY_FAILED_SETUP_PHASES` via `APPEND_RETRY_FAILED_COMMON_SETUP_PHASES` —
10
+ * `'Repromoting external pages'` is the one step unique to `append()`.
11
+ *
12
+ * Linear on the success path (no branching before `initializedCallback`). On
13
+ * failure — anywhere from `archive.updateConfig` through the crawl itself —
14
+ * a `SETUP_RECOVERY_PHASE_LABELS` entry (`'Restoring archive from backup'`)
15
+ * is announced instead of continuing this sequence.
16
+ */
17
+ export declare const APPEND_SETUP_PHASES: readonly ["Extracting archive", "Loading archive config", "Backing up archive", "Repromoting external pages", "Loading dedupe-cap shape keys", "Loading crawl state", "Loading resource list", "Loading scraped page count", "Restoring crawl state"];
@@ -0,0 +1,22 @@
1
+ import { APPEND_RETRY_FAILED_COMMON_SETUP_PHASES } from './append-retry-failed-common-setup-phases.js';
2
+ /**
3
+ * Ordered `SetupProgressCallbacks.onPhase` labels `CrawlerOrchestrator.append`
4
+ * announces, in call order, before its `initializedCallback` fires. Single
5
+ * source of truth for both the `onPhase` call sites in
6
+ * `crawler-orchestrator.ts` and the CLI's setup task list (`create-setup-task-list.ts`),
7
+ * which pre-builds one task-list row per entry — duplicating this sequence by
8
+ * hand at the CLI layer would silently drift the moment a phase is added,
9
+ * renamed, or reordered here. Shares its prefix/suffix with
10
+ * `RETRY_FAILED_SETUP_PHASES` via `APPEND_RETRY_FAILED_COMMON_SETUP_PHASES` —
11
+ * `'Repromoting external pages'` is the one step unique to `append()`.
12
+ *
13
+ * Linear on the success path (no branching before `initializedCallback`). On
14
+ * failure — anywhere from `archive.updateConfig` through the crawl itself —
15
+ * a `SETUP_RECOVERY_PHASE_LABELS` entry (`'Restoring archive from backup'`)
16
+ * is announced instead of continuing this sequence.
17
+ */
18
+ export const APPEND_SETUP_PHASES = [
19
+ ...APPEND_RETRY_FAILED_COMMON_SETUP_PHASES.prefix,
20
+ 'Repromoting external pages',
21
+ ...APPEND_RETRY_FAILED_COMMON_SETUP_PHASES.suffix,
22
+ ];
@@ -29,6 +29,16 @@ export declare class ArchiveAccessor extends EventEmitter<DatabaseEvent> {
29
29
  constructor(tmpDir: string, db: Database, namespace?: string | null, options?: {
30
30
  readOnly?: boolean;
31
31
  });
32
+ /**
33
+ * Enables `await using accessor = ...`. Delegates to {@link close} with
34
+ * the default timeout — callers that need a non-default `timeoutMs`
35
+ * must call `close` explicitly instead of relying on disposal.
36
+ *
37
+ * Dispatches through the instance's own `close`, so an `Archive`
38
+ * (which overrides `close`) gets its full teardown here too — this
39
+ * method does not need to be re-implemented on subclasses.
40
+ */
41
+ [Symbol.asyncDispose](): Promise<void>;
32
42
  /**
33
43
  * Closes the underlying database connection.
34
44
  *
@@ -94,6 +104,13 @@ export declare class ArchiveAccessor extends EventEmitter<DatabaseEvent> {
94
104
  * @returns The parsed {@link Config} object.
95
105
  */
96
106
  getConfig(): Promise<Config>;
107
+ /**
108
+ * Retrieves the Web Components (custom elements) within the given page's
109
+ * detected main content region, from `page_main_content_custom_elements`.
110
+ * @param pageId - The database id of the page.
111
+ * @returns Ordered custom-element rows.
112
+ */
113
+ getCustomElementsOfPage(pageId: number): Promise<import("./meta/types.js").MainContentCustomElementRow[]>;
97
114
  /**
98
115
  * Reads custom data stored in the archive by name.
99
116
  * @param name - The base name of the data file (without extension).
@@ -199,6 +216,13 @@ export declare class ArchiveAccessor extends EventEmitter<DatabaseEvent> {
199
216
  getPagesWithRefs(limit: number, callback: (pages: Page[], currentOffset: number, max: number) => void | Promise<void>, options?: ParseURLOptions & {
200
217
  withRefs?: boolean;
201
218
  }): Promise<void>;
219
+ /**
220
+ * Retrieves the confidence-combined technology roll-up for the given
221
+ * page, from the `page_technologies` table.
222
+ * @param pageId - The database id of the page.
223
+ * @returns Technology rows with category, version, confidence, signalCount.
224
+ */
225
+ getPageTechnologiesOfPage(pageId: number): Promise<import("./meta/types.js").PageTechnologyRow[]>;
202
226
  /**
203
227
  * Retrieves pages that link to the specified page (incoming links).
204
228
  * @param pageId - The database ID of the target page.
@@ -218,16 +242,18 @@ export declare class ArchiveAccessor extends EventEmitter<DatabaseEvent> {
218
242
  getResources(): Promise<Resource[]>;
219
243
  /**
220
244
  * Retrieves a flat list of all resource URLs stored in the archive.
245
+ * @param onProgress - Forwarded to {@link Database.getResourceUrlList} —
246
+ * see that method's docs.
221
247
  * @returns An array of resource URL strings.
222
248
  */
223
- getResourceUrlList(): Promise<string[]>;
249
+ getResourceUrlList(onProgress?: (scannedUpToId: number, maxId: number) => void): Promise<string[]>;
224
250
  /**
225
- * Retrieves the Wappalyzer tag entries for the given page, parsed back
226
- * from the `page_tags` table.
251
+ * Retrieves the raw technology-detection signals for the given page,
252
+ * from the `technology_signals` table.
227
253
  * @param pageId - The database id of the page.
228
- * @returns Ordered entries with provider, category, externalId, etc.
254
+ * @returns Ordered signal rows with technology, signalType, evidence, weight.
229
255
  */
230
- getTagsOfPage(pageId: number): Promise<import("./meta/types.js").TagRow[]>;
256
+ getTechnologySignalsOfPage(pageId: number): Promise<import("./meta/types.js").TechnologySignalRow[]>;
231
257
  /**
232
258
  * Retrieves the videos within the given page's detected main content
233
259
  * region, from `page_main_content_videos`.
@@ -1,4 +1,5 @@
1
1
  import path from 'node:path';
2
+ import { raceWithTimeout } from '@d-zero/shared/race-with-timeout';
2
3
  import { TypedAwaitEventEmitter as EventEmitter } from '@d-zero/shared/typed-await-event-emitter';
3
4
  import { log } from './debug.js';
4
5
  import { outputJSON } from './filesystem/output-json.js';
@@ -81,6 +82,18 @@ export class ArchiveAccessor extends EventEmitter {
81
82
  void this.emit('error', e);
82
83
  });
83
84
  }
85
+ /**
86
+ * Enables `await using accessor = ...`. Delegates to {@link close} with
87
+ * the default timeout — callers that need a non-default `timeoutMs`
88
+ * must call `close` explicitly instead of relying on disposal.
89
+ *
90
+ * Dispatches through the instance's own `close`, so an `Archive`
91
+ * (which overrides `close`) gets its full teardown here too — this
92
+ * method does not need to be re-implemented on subclasses.
93
+ */
94
+ async [Symbol.asyncDispose]() {
95
+ await this.close();
96
+ }
84
97
  /**
85
98
  * Closes the underlying database connection.
86
99
  *
@@ -162,6 +175,15 @@ export class ArchiveAccessor extends EventEmitter {
162
175
  async getConfig() {
163
176
  return this.#db.getConfig();
164
177
  }
178
+ /**
179
+ * Retrieves the Web Components (custom elements) within the given page's
180
+ * detected main content region, from `page_main_content_custom_elements`.
181
+ * @param pageId - The database id of the page.
182
+ * @returns Ordered custom-element rows.
183
+ */
184
+ async getCustomElementsOfPage(pageId) {
185
+ return this.#db.getCustomElementsOfPage(pageId);
186
+ }
165
187
  async getData(name, format = 'json') {
166
188
  const namespace = this.#namespace || '';
167
189
  const filePath = safePath(this.#tmpDir, namespace, `${name}.${format}`);
@@ -310,6 +332,15 @@ export class ArchiveAccessor extends EventEmitter {
310
332
  times++;
311
333
  }
312
334
  }
335
+ /**
336
+ * Retrieves the confidence-combined technology roll-up for the given
337
+ * page, from the `page_technologies` table.
338
+ * @param pageId - The database id of the page.
339
+ * @returns Technology rows with category, version, confidence, signalCount.
340
+ */
341
+ async getPageTechnologiesOfPage(pageId) {
342
+ return this.#db.getPageTechnologiesOfPage(pageId);
343
+ }
313
344
  /**
314
345
  * Retrieves pages that link to the specified page (incoming links).
315
346
  * @param pageId - The database ID of the target page.
@@ -338,19 +369,21 @@ export class ArchiveAccessor extends EventEmitter {
338
369
  }
339
370
  /**
340
371
  * Retrieves a flat list of all resource URLs stored in the archive.
372
+ * @param onProgress - Forwarded to {@link Database.getResourceUrlList} —
373
+ * see that method's docs.
341
374
  * @returns An array of resource URL strings.
342
375
  */
343
- async getResourceUrlList() {
344
- return this.#db.getResourceUrlList();
376
+ async getResourceUrlList(onProgress) {
377
+ return this.#db.getResourceUrlList(onProgress);
345
378
  }
346
379
  /**
347
- * Retrieves the Wappalyzer tag entries for the given page, parsed back
348
- * from the `page_tags` table.
380
+ * Retrieves the raw technology-detection signals for the given page,
381
+ * from the `technology_signals` table.
349
382
  * @param pageId - The database id of the page.
350
- * @returns Ordered entries with provider, category, externalId, etc.
383
+ * @returns Ordered signal rows with technology, signalType, evidence, weight.
351
384
  */
352
- async getTagsOfPage(pageId) {
353
- return this.#db.getTagsOfPage(pageId);
385
+ async getTechnologySignalsOfPage(pageId) {
386
+ return this.#db.getTechnologySignalsOfPage(pageId);
354
387
  }
355
388
  /**
356
389
  * Retrieves the videos within the given page's detected main content
@@ -457,27 +490,16 @@ export class ArchiveAccessor extends EventEmitter {
457
490
  await this.#db.destroy();
458
491
  return;
459
492
  }
460
- let timer = null;
461
- const timeout = new Promise((resolve) => {
462
- timer = setTimeout(() => resolve('timeout'), timeoutMs);
463
- });
464
493
  // Track destroy() so we can attach an error-suppressing handler if we
465
494
  // give up waiting — otherwise a late rejection becomes an unhandled
466
495
  // promise rejection on the process.
467
496
  const destroy = this.#db.destroy().then(() => 'done');
468
- try {
469
- const result = await Promise.race([destroy, timeout]);
470
- if (result === 'timeout') {
471
- log('ArchiveAccessor.close: db.destroy() did not settle within %dms — giving up', timeoutMs);
472
- destroy.catch((error) => {
473
- log('ArchiveAccessor.close: late db.destroy() rejection (post-timeout): %O', error);
474
- });
475
- }
476
- }
477
- finally {
478
- if (timer) {
479
- clearTimeout(timer);
480
- }
497
+ const { timeout } = await raceWithTimeout(() => destroy, timeoutMs);
498
+ if (timeout) {
499
+ log('ArchiveAccessor.close: db.destroy() did not settle within %dms — giving up', timeoutMs);
500
+ destroy.catch((error) => {
501
+ log('ArchiveAccessor.close: late db.destroy() rejection (post-timeout): %O', error);
502
+ });
481
503
  }
482
504
  }
483
505
  }
@@ -19,16 +19,15 @@ import { ArchiveAccessor } from './archive-accessor.js';
19
19
  *
20
20
  * Use the static factory methods ({@link Archive.create}, {@link Archive.open},
21
21
  * {@link Archive.resume}, {@link Archive.connect}) to obtain instances.
22
- * The constructor is private.
22
+ * The constructor is private. Implements `Symbol.asyncDispose` (inherited
23
+ * from {@link ArchiveAccessor}) so callers can use `await using` instead of
24
+ * a manual `try`/`finally` around {@link close}.
23
25
  * @example
24
- * const archive = await Archive.create({ filePath: '/path/to/site.nitpicker' });
25
- * try {
26
- * await archive.setConfig(config);
27
- * const pageId = await archive.setPage(pageData);
28
- * } finally {
29
- * // Writes the `.nitpicker` tar (if absent), removes tmpDir, releases the lock.
30
- * await archive.close();
31
- * }
26
+ * await using archive = await Archive.create({ filePath: '/path/to/site.nitpicker' });
27
+ * await archive.setConfig(config);
28
+ * const pageId = await archive.setPage(pageData);
29
+ * // Writes the `.nitpicker` tar (if absent), removes tmpDir, releases the
30
+ * // lock — all on scope exit, whether by fallthrough or thrown error.
32
31
  */
33
32
  export default class Archive extends ArchiveAccessor {
34
33
  #private;
@@ -308,17 +307,21 @@ export default class Archive extends ArchiveAccessor {
308
307
  * internal pages on the next pass.
309
308
  * @param scopes - Hostname-indexed scope map representing the new scope.
310
309
  * @param options - URL parsing options forwarded to the scope-entry lookup.
310
+ * @param onProgress - Forwarded to {@link Database.repromoteExternalPages}
311
+ * — see that method's docs.
311
312
  * @returns The URLs that were repromoted.
312
313
  */
313
- repromoteExternalPages(scopes: ReadonlyMap<string, readonly ExURL[]>, options?: ParseURLOptions): Promise<string[]>;
314
+ repromoteExternalPages(scopes: ReadonlyMap<string, readonly ExURL[]>, options?: ParseURLOptions, onProgress?: (processed: number, total: number) => void): Promise<string[]>;
314
315
  /**
315
316
  * Reset previously-failed pages back to pending so a follow-up crawl re-fetches them.
316
317
  *
317
318
  * Delegates to {@link Database.resetFailedPages}. See that method for the
318
319
  * exact failure criteria (missing status / content type, or a 5xx status).
320
+ * @param onProgress - Forwarded to {@link Database.resetFailedPages} —
321
+ * see that method's docs.
319
322
  * @returns The URLs of the pages that were reset to pending.
320
323
  */
321
- resetFailedPages(): Promise<string[]>;
324
+ resetFailedPages(onProgress?: (processed: number, total: number) => void): Promise<string[]>;
322
325
  /**
323
326
  * Persists the raw bytes of an `--inventory` source URL list into the
324
327
  * archive's tar payload, at `inventory/<sha256>.txt`.
@@ -374,9 +377,12 @@ export default class Archive extends ArchiveAccessor {
374
377
  * pages share a single `page_html_blobs` row.
375
378
  * @param pageInfo - The page data to store.
376
379
  * @param source - Provenance label for new rows. `undefined` leaves the DB DEFAULT (`'crawled'`).
380
+ * @param bodyHash - Precomputed body hash for the page's HTML (see
381
+ * `CrawlerEventTypes.page.bodyHash`). `undefined`/`null` falls back to
382
+ * computing it from the HTML instead.
377
383
  * @returns The database ID of the stored page.
378
384
  */
379
- setPage(pageInfo: PageData, source?: PageSource): Promise<number>;
385
+ setPage(pageInfo: PageData, source?: PageSource, bodyHash?: Buffer | null): Promise<number>;
380
386
  /**
381
387
  * Records a redirect edge without re-storing the destination's content.
382
388
  *
@@ -418,8 +424,10 @@ export default class Archive extends ArchiveAccessor {
418
424
  /**
419
425
  * Assigns natural URL sort order values to all pages in the database
420
426
  * that do not yet have an `order` field set.
427
+ * @param onProgress - Forwarded to {@link Database.setUrlOrder} — see that
428
+ * method's docs.
421
429
  */
422
- setUrlOrder(): Promise<void>;
430
+ setUrlOrder(onProgress?: (processed: number, total: number) => void): Promise<void>;
423
431
  /**
424
432
  * Updates a subset of fields on the archive's `info` row. Used by the append
425
433
  * flow to extend `roots` / `scope` without rewriting the entire config.
@@ -442,8 +450,26 @@ export default class Archive extends ArchiveAccessor {
442
450
  * `db.sqlite`, so a re-crawl (`append` / `inventory` / `retryFailed`)
443
451
  * opened without it would tar back a tmpDir missing those extra files,
444
452
  * silently dropping them from the rewritten archive.
445
- */
446
- write(): Promise<void>;
453
+ * @param options - Optional write settings.
454
+ * @param options.onTarProgress - Called as archive bytes are written
455
+ * during the tar step, with the bytes written so far and the estimated
456
+ * total (issue #294: tarring a large archive takes minutes, and
457
+ * without this the CLI shows nothing until `write` returns). Omit for
458
+ * a silent write (the default).
459
+ * @param options.onStep - Called once at the start of each of this
460
+ * method's four steps (issue #294): `checkpoint` (WAL fold-back —
461
+ * single synchronous PRAGMA, no countable progress) and `remove`
462
+ * (deleting the tarred-away tmpDir) have no progress signal of their
463
+ * own, so without this a large archive's write looks frozen between
464
+ * the `tar` step's byte updates and completion. `rename` is nearly
465
+ * instant (same-filesystem directory move) but included for
466
+ * completeness — a caller displaying phase labels shouldn't have a
467
+ * gap where the operation is silently between named steps.
468
+ */
469
+ write(options?: {
470
+ onTarProgress?: (writtenBytes: number, totalBytes: number) => void;
471
+ onStep?: (step: 'checkpoint' | 'rename' | 'tar' | 'remove') => void;
472
+ }): Promise<void>;
447
473
  /** The file extension for Nitpicker archive files (without the leading dot). */
448
474
  static FILE_EXTENSION: string;
449
475
  /** The filename of the SQLite database within the archive. */
@@ -464,27 +490,38 @@ export default class Archive extends ArchiveAccessor {
464
490
  * on the user's tmpDir.
465
491
  *
466
492
  * `options.readOnly: false` is a narrow escape hatch for opening a
467
- * second, writable connection to a `tmpDir` that {@link Archive.openCached}
468
- * already extracted (and migrated) into an OS-temp cache directory
469
- * never the caller's live/interrupted crawl tmpDir, which must stay
470
- * read-only. A read-only open (`Archive.openCached`/`ArchiveManager.open`)
471
- * must never take this path itselfblocking or writing during what
472
- * must be a read-only open is forbidden (issue #177). This escape
473
- * hatch has no current production caller; any future
474
- * one is responsible for its own cross-process coordination (see
475
- * `acquireArchiveLock`) this method does not acquire any lock itself.
493
+ * second, writable connection to a `tmpDir` the caller's own process
494
+ * already owns and extracted itself. The one production caller is the
495
+ * viewer-read-model worker thread (`@nitpicker/query`'s
496
+ * `viewer-read-model-worker-entry.ts`, issue #294): the parent thread
497
+ * holds the archive via `Archive.open` (lock included worker threads
498
+ * share the parent's PID, so the PID-based `acquireArchiveLock` guard
499
+ * stays valid), sits idle awaiting the worker, and re-tars the tmpDir
500
+ * afterward. What this hatch must NEVER target is a live/interrupted
501
+ * crawl tmpDir owned by a *different* process (the stub-mode
502
+ * `ArchiveManager.open` path attaches to exactly such directories, and
503
+ * must stay read-only): writable connects run the self-healing
504
+ * migrations, and mutating a directory out from under its owner is how
505
+ * archives corrupt. A read-only open (`Archive.openCached`/
506
+ * `ArchiveManager.open`) must never take this path itself — blocking or
507
+ * writing during what must be a read-only open is forbidden (issue
508
+ * #177). Any new caller is responsible for its own coordination with
509
+ * the tmpDir's owner (see `acquireArchiveLock` for the cross-process
510
+ * case) — this method does not acquire any lock itself.
476
511
  * @param tmpDir - The path to the temporary directory containing the database.
477
512
  * @param namespace - An optional namespace for scoping data access within the archive.
478
513
  * @param options - Connection options.
479
514
  * @param options.readOnly - Defaults to `true`. Pass `false` to obtain a
480
- * writable accessor against an already-extracted cache directory.
515
+ * writable accessor against a tmpDir the calling process itself owns.
481
516
  * @returns An ArchiveAccessor instance for querying the archive data.
482
517
  * @example
483
518
  * // Default (read-only) — safe for stub mode and cache reads:
484
519
  * const accessor = await Archive.connect(tmpDir);
485
520
  * @example
486
- * // Writable escape hatch — only against a tar-cache extraction:
487
- * const writable = await Archive.connect(cacheDir, null, { readOnly: false });
521
+ * // Writable escape hatch — only against a tmpDir this process owns
522
+ * // (e.g. the viewer-read-model worker thread reconnecting to the
523
+ * // parent's Archive.open extraction):
524
+ * const writable = await Archive.connect(ownTmpDir, null, { readOnly: false });
488
525
  */
489
526
  static connect(tmpDir: string, namespace?: string | null, options?: {
490
527
  readOnly?: boolean;
@@ -522,18 +559,17 @@ export default class Archive extends ArchiveAccessor {
522
559
  * {@link Archive.open}.
523
560
  * @param filePath - Absolute path to the `.nitpicker` file.
524
561
  * @param namespace - Optional namespace forwarded to {@link ArchiveAccessor}.
562
+ * @param onExtractProgress - Forwarded to {@link extractArchiveToCache} —
563
+ * see that function's docs for the cache-hit/miss contract.
525
564
  * @returns A read-only {@link ArchiveAccessor} backed by the cache directory.
526
565
  * @example
527
566
  * ```ts
528
- * const accessor = await Archive.openCached('/path/to/site.nitpicker');
529
- * try {
530
- * const summary = await getSummary(accessor);
531
- * } finally {
532
- * await accessor.close(); // tears down DB handle, cacheDir persists.
533
- * }
567
+ * await using accessor = await Archive.openCached('/path/to/site.nitpicker');
568
+ * const summary = await getSummary(accessor);
569
+ * // tears down DB handle on scope exit; cacheDir persists.
534
570
  * ```
535
571
  */
536
- static openCached(filePath: string, namespace?: string | null): Promise<ArchiveAccessor>;
572
+ static openCached(filePath: string, namespace?: string | null, onExtractProgress?: (readBytes: number, totalBytes: number) => void): Promise<ArchiveAccessor>;
537
573
  /**
538
574
  * Creates a new archive at the specified file path.
539
575
  * Initializes a temporary working directory and a fresh SQLite database.
@@ -558,10 +594,13 @@ export default class Archive extends ArchiveAccessor {
558
594
  * Resumes an archive from an existing temporary directory
559
595
  * (e.g., after an interrupted crawl session).
560
596
  * @param targetPath - The path to the temporary directory to resume from.
597
+ * @param onLog - Forwarded to {@link Database.connect} — see
598
+ * {@link ArchiveOpenOptions.onLog}'s docs (this writable reconnect
599
+ * runs the same self-healing migrations `Archive.open` does).
561
600
  * @returns An Archive instance reconnected to the existing data.
562
601
  * @throws {Error} If the specified path is not a directory.
563
602
  */
564
- static resume(targetPath: string): Promise<Archive>;
603
+ static resume(targetPath: string, onLog?: (message: string) => void): Promise<Archive>;
565
604
  /**
566
605
  * Generates a timestamp string in the format `YYYYMMDDHHmmssSSS`
567
606
  * suitable for use in file names.
@@ -588,8 +627,28 @@ export default class Archive extends ArchiveAccessor {
588
627
  * {@link ArchiveAccessor} (not an `Archive`), so `close()` resolves to
589
628
  * the safe base implementation — no `write()`, no `remove()`, no lock
590
629
  * release — leaving the tmpDir intact for the live crawler.
591
- */
592
- close(): Promise<void>;
630
+ * @param options - Optional close settings. `timeoutMs` is accepted for
631
+ * compatibility with {@link ArchiveAccessor.close}'s signature (forwarded
632
+ * to the base `super.close()` call below); the rest are progress
633
+ * callbacks (issue #294) forwarded to {@link write} when this call ends
634
+ * up taking the recovery-write branch (the archive file doesn't exist
635
+ * yet).
636
+ * @param options.timeoutMs - See {@link ArchiveAccessor.close}.
637
+ * @param options.onRecoveryStart - Called once, only when this `close()`
638
+ * is about to write the archive because the file doesn't exist on
639
+ * disk yet — e.g. a caller's own explicit `write()` threw before
640
+ * finishing, or was never called at all. Without this, a listener
641
+ * that already tore down its display after that earlier failure has
642
+ * no way to know a second, recovery write is happening.
643
+ * @param options.onTarProgress - See {@link write}.
644
+ * @param options.onStep - See {@link write}.
645
+ */
646
+ close(options?: {
647
+ timeoutMs?: number;
648
+ onRecoveryStart?: () => void;
649
+ onTarProgress?: (writtenBytes: number, totalBytes: number) => void;
650
+ onStep?: (step: 'checkpoint' | 'rename' | 'tar' | 'remove') => void;
651
+ }): Promise<void>;
593
652
  /**
594
653
  * Retrieves the crawl configuration stored in the archive database.
595
654
  * @returns The configuration object.
@@ -623,5 +682,24 @@ type ArchiveOpenOptions = {
623
682
  * dropping them from the rewritten archive.
624
683
  */
625
684
  openPluginData?: boolean;
685
+ /**
686
+ * Called as archive bytes are consumed during the initial tar
687
+ * extraction, with the bytes read so far and the archive's total size
688
+ * (issue #294: a large archive takes minutes to extract, and without
689
+ * this the CLI shows nothing at all until `open` returns). Byte
690
+ * granularity is the read-stream chunk size — throttle in the callback
691
+ * for coarser display updates. Omit for a silent extraction (the
692
+ * default).
693
+ */
694
+ onExtractProgress?: (readBytes: number, totalBytes: number) => void;
695
+ /**
696
+ * Called instead of `console.error` for self-healing schema migration
697
+ * notices that fire while opening a legacy archive (issue #294) —
698
+ * forwarded to {@link Database.connect}'s `onLog`. Without this, a
699
+ * migration notice can print mid-redraw of a caller's `Lanes`/`TaskList`
700
+ * display, corrupting its cursor tracking. Omit to fall back to
701
+ * `console.error` (the pre-#294 behavior).
702
+ */
703
+ onLog?: (message: string) => void;
626
704
  };
627
705
  export {};