@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.
- 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 +24 -4
- package/lib/archive/db-ops/pages/write/insert-page.js +40 -4
- 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/link-redirect-sources.d.ts +7 -1
- package/lib/archive/db-ops/pages/write/link-redirect-sources.js +8 -2
- package/lib/archive/db-ops/pages/write/update-page.d.ts +4 -1
- package/lib/archive/db-ops/pages/write/update-page.js +38 -16
- package/lib/archive/db-ops/resources/get-resource-url-list.d.ts +12 -1
- package/lib/archive/db-ops/resources/get-resource-url-list.js +43 -5
- package/lib/archive/filesystem/copy-file-with-progress.d.ts +20 -0
- package/lib/archive/filesystem/copy-file-with-progress.js +34 -0
- package/lib/archive/filesystem/parse-pax-path.d.ts +18 -0
- package/lib/archive/filesystem/parse-pax-path.js +47 -0
- package/lib/archive/filesystem/parse-tar-size-field.d.ts +13 -0
- package/lib/archive/filesystem/parse-tar-size-field.js +40 -0
- package/lib/archive/filesystem/peek-tar-top-dir.d.ts +10 -3
- package/lib/archive/filesystem/peek-tar-top-dir.js +166 -16
- package/lib/archive/filesystem/tar.d.ts +13 -1
- package/lib/archive/filesystem/tar.js +63 -7
- package/lib/archive/filesystem/untar.d.ts +13 -0
- package/lib/archive/filesystem/untar.js +68 -8
- package/lib/archive/init-schema.d.ts +1 -1
- package/lib/archive/init-schema.js +6 -4
- package/lib/archive/meta/compute-main-contents-denormalized.d.ts +13 -2
- package/lib/archive/meta/compute-main-contents-denormalized.js +15 -2
- package/lib/archive/meta/technologies/combine-technology-confidence.d.ts +46 -0
- package/lib/archive/meta/technologies/combine-technology-confidence.js +93 -0
- package/lib/archive/meta/technologies/convert-legacy-page-tags-to-inserts.d.ts +45 -0
- package/lib/archive/meta/technologies/convert-legacy-page-tags-to-inserts.js +53 -0
- package/lib/archive/meta/technologies/convert-tag-row-to-wappalyzer-signal.d.ts +33 -0
- package/lib/archive/meta/technologies/convert-tag-row-to-wappalyzer-signal.js +46 -0
- package/lib/archive/meta/technologies/extract-technologies-for-archive.d.ts +30 -0
- package/lib/archive/meta/technologies/extract-technologies-for-archive.js +27 -0
- package/lib/archive/meta/technologies/match-structural-technology-signals.d.ts +13 -0
- package/lib/archive/meta/technologies/match-structural-technology-signals.js +44 -0
- package/lib/archive/meta/technologies/normalize-wappalyzer-entries.d.ts +25 -0
- package/lib/archive/meta/technologies/normalize-wappalyzer-entries.js +35 -0
- package/lib/archive/meta/technologies/technology-signal-definitions.d.ts +43 -0
- package/lib/archive/meta/technologies/technology-signal-definitions.js +193 -0
- package/lib/archive/meta/technologies/types.d.ts +68 -0
- package/lib/archive/meta/technologies/types.js +13 -0
- package/lib/archive/meta/types.d.ts +97 -30
- package/lib/archive/migrate-content-items-alias-of-id.d.ts +6 -1
- package/lib/archive/migrate-content-items-alias-of-id.js +14 -3
- package/lib/archive/migrate-content-items-dedupe-cap-event-id.d.ts +6 -1
- package/lib/archive/migrate-content-items-dedupe-cap-event-id.js +14 -3
- package/lib/archive/migrate-info-main-content-selector.d.ts +6 -1
- package/lib/archive/migrate-info-main-content-selector.js +14 -3
- package/lib/archive/migrate-info-roots.d.ts +6 -1
- package/lib/archive/migrate-info-roots.js +14 -3
- package/lib/archive/migrate-inventory-runs-exclude-skipped.d.ts +6 -1
- package/lib/archive/migrate-inventory-runs-exclude-skipped.js +14 -3
- package/lib/archive/migrate-inventory-runs-invalid-skipped.d.ts +6 -1
- package/lib/archive/migrate-inventory-runs-invalid-skipped.js +14 -3
- package/lib/archive/migrate-main-contents-columns.d.ts +6 -1
- package/lib/archive/migrate-main-contents-columns.js +14 -3
- package/lib/archive/migrate-page-meta-body-hash.d.ts +6 -1
- package/lib/archive/migrate-page-meta-body-hash.js +14 -3
- package/lib/archive/migrate-page-meta-console-error-count.d.ts +6 -1
- package/lib/archive/migrate-page-meta-console-error-count.js +14 -3
- package/lib/archive/migrate-page-meta-custom-element-count.d.ts +33 -0
- package/lib/archive/migrate-page-meta-custom-element-count.js +51 -0
- package/lib/archive/migrate-page-tags-to-page-technologies.d.ts +39 -0
- package/lib/archive/migrate-page-tags-to-page-technologies.js +79 -0
- package/lib/archive/page.d.ts +37 -13
- package/lib/archive/page.js +45 -17
- package/lib/archive/retarget-legacy-fk-tables.d.ts +27 -14
- package/lib/archive/retarget-legacy-fk-tables.js +61 -15
- package/lib/archive/types.d.ts +18 -0
- package/lib/crawler/assert-puppeteer-shared-with-beholder.d.ts +24 -0
- package/lib/crawler/assert-puppeteer-shared-with-beholder.js +36 -0
- package/lib/crawler/capture-custom-elements.d.ts +33 -0
- package/lib/crawler/capture-custom-elements.js +39 -0
- package/lib/crawler/close-browser-safely.d.ts +3 -3
- package/lib/crawler/close-browser-safely.js +8 -17
- package/lib/crawler/collect-custom-elements.d.ts +33 -0
- package/lib/crawler/collect-custom-elements.js +110 -0
- package/lib/crawler/crawler.js +101 -46
- package/lib/crawler/fetch-destination.js +6 -13
- package/lib/crawler/find-package-dir.d.ts +24 -0
- package/lib/crawler/find-package-dir.js +39 -0
- package/lib/crawler/scan-js-resource-for-license-comment.d.ts +32 -0
- package/lib/crawler/scan-js-resource-for-license-comment.js +140 -0
- package/lib/crawler/scan-js-resources-for-technology-signals.d.ts +58 -0
- package/lib/crawler/scan-js-resources-for-technology-signals.js +196 -0
- package/lib/crawler/types.d.ts +23 -0
- package/lib/crawler-orchestrator.d.ts +53 -9
- package/lib/crawler-orchestrator.js +223 -46
- package/lib/crawler.d.ts +11 -1
- package/lib/crawler.js +9 -0
- package/lib/inventory-setup-phases.d.ts +32 -0
- package/lib/inventory-setup-phases.js +45 -0
- package/lib/resume-setup-phases.d.ts +16 -0
- package/lib/resume-setup-phases.js +24 -0
- package/lib/retry-failed-setup-phases.d.ts +19 -0
- package/lib/retry-failed-setup-phases.js +24 -0
- package/lib/setup-recovery-phase-labels.d.ts +21 -0
- package/lib/setup-recovery-phase-labels.js +24 -0
- package/lib/types.d.ts +145 -0
- package/package.json +12 -9
- 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
package/lib/archive/archive.js
CHANGED
|
@@ -31,16 +31,15 @@ import { safePath } from './safe-path.js';
|
|
|
31
31
|
*
|
|
32
32
|
* Use the static factory methods ({@link Archive.create}, {@link Archive.open},
|
|
33
33
|
* {@link Archive.resume}, {@link Archive.connect}) to obtain instances.
|
|
34
|
-
* The constructor is private.
|
|
34
|
+
* The constructor is private. Implements `Symbol.asyncDispose` (inherited
|
|
35
|
+
* from {@link ArchiveAccessor}) so callers can use `await using` instead of
|
|
36
|
+
* a manual `try`/`finally` around {@link close}.
|
|
35
37
|
* @example
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* // Writes the `.nitpicker` tar (if absent), removes tmpDir, releases the lock.
|
|
42
|
-
* await archive.close();
|
|
43
|
-
* }
|
|
38
|
+
* await using archive = await Archive.create({ filePath: '/path/to/site.nitpicker' });
|
|
39
|
+
* await archive.setConfig(config);
|
|
40
|
+
* const pageId = await archive.setPage(pageData);
|
|
41
|
+
* // Writes the `.nitpicker` tar (if absent), removes tmpDir, releases the
|
|
42
|
+
* // lock — all on scope exit, whether by fallthrough or thrown error.
|
|
44
43
|
*/
|
|
45
44
|
export default class Archive extends ArchiveAccessor {
|
|
46
45
|
/**
|
|
@@ -414,22 +413,26 @@ export default class Archive extends ArchiveAccessor {
|
|
|
414
413
|
* internal pages on the next pass.
|
|
415
414
|
* @param scopes - Hostname-indexed scope map representing the new scope.
|
|
416
415
|
* @param options - URL parsing options forwarded to the scope-entry lookup.
|
|
416
|
+
* @param onProgress - Forwarded to {@link Database.repromoteExternalPages}
|
|
417
|
+
* — see that method's docs.
|
|
417
418
|
* @returns The URLs that were repromoted.
|
|
418
419
|
*/
|
|
419
|
-
async repromoteExternalPages(scopes, options) {
|
|
420
|
+
async repromoteExternalPages(scopes, options, onProgress) {
|
|
420
421
|
dbLog('Repromote external pages with %d hostnames in scope', scopes.size);
|
|
421
|
-
return this.#db.repromoteExternalPages(scopes, options);
|
|
422
|
+
return this.#db.repromoteExternalPages(scopes, options, onProgress);
|
|
422
423
|
}
|
|
423
424
|
/**
|
|
424
425
|
* Reset previously-failed pages back to pending so a follow-up crawl re-fetches them.
|
|
425
426
|
*
|
|
426
427
|
* Delegates to {@link Database.resetFailedPages}. See that method for the
|
|
427
428
|
* exact failure criteria (missing status / content type, or a 5xx status).
|
|
429
|
+
* @param onProgress - Forwarded to {@link Database.resetFailedPages} —
|
|
430
|
+
* see that method's docs.
|
|
428
431
|
* @returns The URLs of the pages that were reset to pending.
|
|
429
432
|
*/
|
|
430
|
-
async resetFailedPages() {
|
|
433
|
+
async resetFailedPages(onProgress) {
|
|
431
434
|
dbLog('Reset failed pages back to pending');
|
|
432
|
-
return this.#db.resetFailedPages();
|
|
435
|
+
return this.#db.resetFailedPages(onProgress);
|
|
433
436
|
}
|
|
434
437
|
/**
|
|
435
438
|
* Persists the raw bytes of an `--inventory` source URL list into the
|
|
@@ -498,11 +501,14 @@ export default class Archive extends ArchiveAccessor {
|
|
|
498
501
|
* pages share a single `page_html_blobs` row.
|
|
499
502
|
* @param pageInfo - The page data to store.
|
|
500
503
|
* @param source - Provenance label for new rows. `undefined` leaves the DB DEFAULT (`'crawled'`).
|
|
504
|
+
* @param bodyHash - Precomputed body hash for the page's HTML (see
|
|
505
|
+
* `CrawlerEventTypes.page.bodyHash`). `undefined`/`null` falls back to
|
|
506
|
+
* computing it from the HTML instead.
|
|
501
507
|
* @returns The database ID of the stored page.
|
|
502
508
|
*/
|
|
503
|
-
async setPage(pageInfo, source) {
|
|
509
|
+
async setPage(pageInfo, source, bodyHash) {
|
|
504
510
|
dbLog('Set page: %s', pageInfo.url.href);
|
|
505
|
-
return await this.#db.updatePage(pageInfo, true, pageInfo.isTarget, source);
|
|
511
|
+
return await this.#db.updatePage(pageInfo, true, pageInfo.isTarget, source, bodyHash);
|
|
506
512
|
}
|
|
507
513
|
/**
|
|
508
514
|
* Records a redirect edge without re-storing the destination's content.
|
|
@@ -554,10 +560,12 @@ export default class Archive extends ArchiveAccessor {
|
|
|
554
560
|
/**
|
|
555
561
|
* Assigns natural URL sort order values to all pages in the database
|
|
556
562
|
* that do not yet have an `order` field set.
|
|
563
|
+
* @param onProgress - Forwarded to {@link Database.setUrlOrder} — see that
|
|
564
|
+
* method's docs.
|
|
557
565
|
*/
|
|
558
|
-
async setUrlOrder() {
|
|
566
|
+
async setUrlOrder(onProgress) {
|
|
559
567
|
dbLog("Pages didn't have `order` field. So set URL order.");
|
|
560
|
-
await this.#db.setUrlOrder();
|
|
568
|
+
await this.#db.setUrlOrder(onProgress);
|
|
561
569
|
}
|
|
562
570
|
/**
|
|
563
571
|
* Updates a subset of fields on the archive's `info` row. Used by the append
|
|
@@ -584,16 +592,35 @@ export default class Archive extends ArchiveAccessor {
|
|
|
584
592
|
* `db.sqlite`, so a re-crawl (`append` / `inventory` / `retryFailed`)
|
|
585
593
|
* opened without it would tar back a tmpDir missing those extra files,
|
|
586
594
|
* silently dropping them from the rewritten archive.
|
|
587
|
-
|
|
588
|
-
|
|
595
|
+
* @param options - Optional write settings.
|
|
596
|
+
* @param options.onTarProgress - Called as archive bytes are written
|
|
597
|
+
* during the tar step, with the bytes written so far and the estimated
|
|
598
|
+
* total (issue #294: tarring a large archive takes minutes, and
|
|
599
|
+
* without this the CLI shows nothing until `write` returns). Omit for
|
|
600
|
+
* a silent write (the default).
|
|
601
|
+
* @param options.onStep - Called once at the start of each of this
|
|
602
|
+
* method's four steps (issue #294): `checkpoint` (WAL fold-back —
|
|
603
|
+
* single synchronous PRAGMA, no countable progress) and `remove`
|
|
604
|
+
* (deleting the tarred-away tmpDir) have no progress signal of their
|
|
605
|
+
* own, so without this a large archive's write looks frozen between
|
|
606
|
+
* the `tar` step's byte updates and completion. `rename` is nearly
|
|
607
|
+
* instant (same-filesystem directory move) but included for
|
|
608
|
+
* completeness — a caller displaying phase labels shouldn't have a
|
|
609
|
+
* gap where the operation is silently between named steps.
|
|
610
|
+
*/
|
|
611
|
+
async write(options) {
|
|
589
612
|
saveLog('Starts: %s', this.#filePath);
|
|
613
|
+
options?.onStep?.('checkpoint');
|
|
590
614
|
await this.#db.checkpoint();
|
|
591
615
|
const filePathWithoutExt = path.resolve(path.dirname(this.#filePath), path.basename(this.#filePath, path.extname(this.#filePath)));
|
|
592
616
|
saveLog('Rename temporary dir: %s to %s', this.#tmpDir, filePathWithoutExt);
|
|
617
|
+
options?.onStep?.('rename');
|
|
593
618
|
await rename(this.#tmpDir, filePathWithoutExt, true);
|
|
594
619
|
saveLog('Tar temporary dir to file: %s to %s', filePathWithoutExt, this.#filePath);
|
|
595
|
-
|
|
620
|
+
options?.onStep?.('tar');
|
|
621
|
+
await tar(filePathWithoutExt, this.#filePath, options?.onTarProgress);
|
|
596
622
|
saveLog('Remove temporary dir: %s', filePathWithoutExt);
|
|
623
|
+
options?.onStep?.('remove');
|
|
597
624
|
await remove(filePathWithoutExt);
|
|
598
625
|
saveLog('Done: %s', this.#filePath);
|
|
599
626
|
}
|
|
@@ -602,19 +629,25 @@ export default class Archive extends ArchiveAccessor {
|
|
|
602
629
|
* (write or remove), drops the DB handle via the base class, then
|
|
603
630
|
* releases the lock in a `finally` so the lock never leaks even on
|
|
604
631
|
* partial failure.
|
|
632
|
+
* @param options - See {@link close}.
|
|
633
|
+
* @param options.timeoutMs
|
|
634
|
+
* @param options.onRecoveryStart
|
|
635
|
+
* @param options.onTarProgress
|
|
636
|
+
* @param options.onStep
|
|
605
637
|
*/
|
|
606
|
-
async #runFullClose() {
|
|
638
|
+
async #runFullClose(options) {
|
|
607
639
|
log('Closing');
|
|
608
640
|
try {
|
|
609
641
|
if (!exists(this.#filePath)) {
|
|
610
642
|
log("Save the file because it doesn't exist");
|
|
611
|
-
|
|
643
|
+
options?.onRecoveryStart?.();
|
|
644
|
+
await this.write(options);
|
|
612
645
|
}
|
|
613
646
|
else if (exists(this.#tmpDir)) {
|
|
614
647
|
log('Remove temporary dir');
|
|
615
648
|
await remove(this.#tmpDir);
|
|
616
649
|
}
|
|
617
|
-
await super.close();
|
|
650
|
+
await super.close({ timeoutMs: options?.timeoutMs });
|
|
618
651
|
}
|
|
619
652
|
finally {
|
|
620
653
|
await this.#releaseLock();
|
|
@@ -654,27 +687,38 @@ export default class Archive extends ArchiveAccessor {
|
|
|
654
687
|
* on the user's tmpDir.
|
|
655
688
|
*
|
|
656
689
|
* `options.readOnly: false` is a narrow escape hatch for opening a
|
|
657
|
-
* second, writable connection to a `tmpDir`
|
|
658
|
-
* already
|
|
659
|
-
*
|
|
660
|
-
* read-
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
665
|
-
*
|
|
690
|
+
* second, writable connection to a `tmpDir` the caller's own process
|
|
691
|
+
* already owns and extracted itself. The one production caller is the
|
|
692
|
+
* viewer-read-model worker thread (`@nitpicker/query`'s
|
|
693
|
+
* `viewer-read-model-worker-entry.ts`, issue #294): the parent thread
|
|
694
|
+
* holds the archive via `Archive.open` (lock included — worker threads
|
|
695
|
+
* share the parent's PID, so the PID-based `acquireArchiveLock` guard
|
|
696
|
+
* stays valid), sits idle awaiting the worker, and re-tars the tmpDir
|
|
697
|
+
* afterward. What this hatch must NEVER target is a live/interrupted
|
|
698
|
+
* crawl tmpDir owned by a *different* process (the stub-mode
|
|
699
|
+
* `ArchiveManager.open` path attaches to exactly such directories, and
|
|
700
|
+
* must stay read-only): writable connects run the self-healing
|
|
701
|
+
* migrations, and mutating a directory out from under its owner is how
|
|
702
|
+
* archives corrupt. A read-only open (`Archive.openCached`/
|
|
703
|
+
* `ArchiveManager.open`) must never take this path itself — blocking or
|
|
704
|
+
* writing during what must be a read-only open is forbidden (issue
|
|
705
|
+
* #177). Any new caller is responsible for its own coordination with
|
|
706
|
+
* the tmpDir's owner (see `acquireArchiveLock` for the cross-process
|
|
707
|
+
* case) — this method does not acquire any lock itself.
|
|
666
708
|
* @param tmpDir - The path to the temporary directory containing the database.
|
|
667
709
|
* @param namespace - An optional namespace for scoping data access within the archive.
|
|
668
710
|
* @param options - Connection options.
|
|
669
711
|
* @param options.readOnly - Defaults to `true`. Pass `false` to obtain a
|
|
670
|
-
* writable accessor against
|
|
712
|
+
* writable accessor against a tmpDir the calling process itself owns.
|
|
671
713
|
* @returns An ArchiveAccessor instance for querying the archive data.
|
|
672
714
|
* @example
|
|
673
715
|
* // Default (read-only) — safe for stub mode and cache reads:
|
|
674
716
|
* const accessor = await Archive.connect(tmpDir);
|
|
675
717
|
* @example
|
|
676
|
-
* // Writable escape hatch — only against a
|
|
677
|
-
*
|
|
718
|
+
* // Writable escape hatch — only against a tmpDir this process owns
|
|
719
|
+
* // (e.g. the viewer-read-model worker thread reconnecting to the
|
|
720
|
+
* // parent's Archive.open extraction):
|
|
721
|
+
* const writable = await Archive.connect(ownTmpDir, null, { readOnly: false });
|
|
678
722
|
*/
|
|
679
723
|
static async connect(tmpDir, namespace = null, options = {}) {
|
|
680
724
|
const readOnly = options.readOnly ?? true;
|
|
@@ -715,23 +759,22 @@ export default class Archive extends ArchiveAccessor {
|
|
|
715
759
|
* {@link Archive.open}.
|
|
716
760
|
* @param filePath - Absolute path to the `.nitpicker` file.
|
|
717
761
|
* @param namespace - Optional namespace forwarded to {@link ArchiveAccessor}.
|
|
762
|
+
* @param onExtractProgress - Forwarded to {@link extractArchiveToCache} —
|
|
763
|
+
* see that function's docs for the cache-hit/miss contract.
|
|
718
764
|
* @returns A read-only {@link ArchiveAccessor} backed by the cache directory.
|
|
719
765
|
* @example
|
|
720
766
|
* ```ts
|
|
721
|
-
*
|
|
722
|
-
*
|
|
723
|
-
*
|
|
724
|
-
* } finally {
|
|
725
|
-
* await accessor.close(); // tears down DB handle, cacheDir persists.
|
|
726
|
-
* }
|
|
767
|
+
* await using accessor = await Archive.openCached('/path/to/site.nitpicker');
|
|
768
|
+
* const summary = await getSummary(accessor);
|
|
769
|
+
* // tears down DB handle on scope exit; cacheDir persists.
|
|
727
770
|
* ```
|
|
728
771
|
*/
|
|
729
|
-
static async openCached(filePath, namespace = null) {
|
|
772
|
+
static async openCached(filePath, namespace = null, onExtractProgress) {
|
|
730
773
|
const cacheRoot = getArchiveCacheRoot();
|
|
731
774
|
const cacheKey = await computeArchiveCacheKey(filePath);
|
|
732
775
|
const cacheDir = resolveArchiveCacheDir(cacheRoot, cacheKey, filePath);
|
|
733
776
|
log('Open cached: %s (cacheDir=%s)', filePath, cacheDir);
|
|
734
|
-
await extractArchiveToCache(filePath, cacheRoot, cacheDir, cacheKey);
|
|
777
|
+
await extractArchiveToCache(filePath, cacheRoot, cacheDir, cacheKey, onExtractProgress);
|
|
735
778
|
return await Archive.connect(cacheDir, namespace);
|
|
736
779
|
}
|
|
737
780
|
/**
|
|
@@ -773,7 +816,7 @@ export default class Archive extends ArchiveAccessor {
|
|
|
773
816
|
* @returns An Archive instance with the extracted data loaded.
|
|
774
817
|
*/
|
|
775
818
|
static async open(options) {
|
|
776
|
-
const { filePath, openPluginData } = options;
|
|
819
|
+
const { filePath, openPluginData, onExtractProgress, onLog } = options;
|
|
777
820
|
const cwd = options.cwd ?? process.cwd();
|
|
778
821
|
log('Open: %O', {
|
|
779
822
|
filePath,
|
|
@@ -802,11 +845,12 @@ export default class Archive extends ArchiveAccessor {
|
|
|
802
845
|
await untar(filePath, {
|
|
803
846
|
cwd,
|
|
804
847
|
fileList: openFiles.length > 0 ? openFiles : undefined,
|
|
848
|
+
onProgress: onExtractProgress,
|
|
805
849
|
});
|
|
806
850
|
const extractedDir = path.resolve(cwd, innerDirName);
|
|
807
851
|
log('Move directory: %s to %s', extractedDir, tmpDir);
|
|
808
852
|
await rename(extractedDir, tmpDir, true);
|
|
809
|
-
return await Archive.#init(filePath, tmpDir, releaseLock);
|
|
853
|
+
return await Archive.#init(filePath, tmpDir, releaseLock, onLog);
|
|
810
854
|
}
|
|
811
855
|
catch (error) {
|
|
812
856
|
await releaseLock();
|
|
@@ -817,16 +861,19 @@ export default class Archive extends ArchiveAccessor {
|
|
|
817
861
|
* Resumes an archive from an existing temporary directory
|
|
818
862
|
* (e.g., after an interrupted crawl session).
|
|
819
863
|
* @param targetPath - The path to the temporary directory to resume from.
|
|
864
|
+
* @param onLog - Forwarded to {@link Database.connect} — see
|
|
865
|
+
* {@link ArchiveOpenOptions.onLog}'s docs (this writable reconnect
|
|
866
|
+
* runs the same self-healing migrations `Archive.open` does).
|
|
820
867
|
* @returns An Archive instance reconnected to the existing data.
|
|
821
868
|
* @throws {Error} If the specified path is not a directory.
|
|
822
869
|
*/
|
|
823
|
-
static async resume(targetPath) {
|
|
870
|
+
static async resume(targetPath, onLog) {
|
|
824
871
|
log('Resume: %s', targetPath);
|
|
825
872
|
if (await isDir(targetPath)) {
|
|
826
873
|
const tmpDir = targetPath;
|
|
827
874
|
const releaseLock = await acquireArchiveLock(tmpDir);
|
|
828
875
|
try {
|
|
829
|
-
const db = await Archive.#connectDB(tmpDir);
|
|
876
|
+
const db = await Archive.#connectDB(tmpDir, { onLog });
|
|
830
877
|
const name = (await db.getName()) ||
|
|
831
878
|
path.basename(targetPath).replace(Archive.TMP_DIR_PREFIX, '');
|
|
832
879
|
const filePath = path.resolve(process.cwd(), name + '.' + Archive.FILE_EXTENSION);
|
|
@@ -865,6 +912,7 @@ export default class Archive extends ArchiveAccessor {
|
|
|
865
912
|
* `readOnly: true` so no migrations run and a missing tmpDir is not
|
|
866
913
|
* resurrected.
|
|
867
914
|
* @param options.readOnly
|
|
915
|
+
* @param options.onLog
|
|
868
916
|
*/
|
|
869
917
|
static async #connectDB(tmpDir, options) {
|
|
870
918
|
const dbPath = path.resolve(tmpDir, Archive.SQLITE_DB_FILE_NAME);
|
|
@@ -872,6 +920,7 @@ export default class Archive extends ArchiveAccessor {
|
|
|
872
920
|
return await Database.connect({
|
|
873
921
|
filename: dbPath,
|
|
874
922
|
readOnly: options?.readOnly,
|
|
923
|
+
onLog: options?.onLog,
|
|
875
924
|
});
|
|
876
925
|
}
|
|
877
926
|
/**
|
|
@@ -883,9 +932,11 @@ export default class Archive extends ArchiveAccessor {
|
|
|
883
932
|
* @param filePath - Output `.nitpicker` file path
|
|
884
933
|
* @param tmpDir - Temporary working directory path
|
|
885
934
|
* @param releaseLock - Function returned by {@link acquireArchiveLock}.
|
|
935
|
+
* @param onLog - Forwarded to {@link Database.connect} — see
|
|
936
|
+
* {@link ArchiveOpenOptions.onLog}'s docs.
|
|
886
937
|
*/
|
|
887
|
-
static async #init(filePath, tmpDir, releaseLock) {
|
|
888
|
-
const db = await Archive.#connectDB(tmpDir);
|
|
938
|
+
static async #init(filePath, tmpDir, releaseLock, onLog) {
|
|
939
|
+
const db = await Archive.#connectDB(tmpDir, { onLog });
|
|
889
940
|
const archive = new Archive(filePath, tmpDir, db, releaseLock);
|
|
890
941
|
return archive;
|
|
891
942
|
}
|
|
@@ -909,12 +960,27 @@ export default class Archive extends ArchiveAccessor {
|
|
|
909
960
|
* {@link ArchiveAccessor} (not an `Archive`), so `close()` resolves to
|
|
910
961
|
* the safe base implementation — no `write()`, no `remove()`, no lock
|
|
911
962
|
* release — leaving the tmpDir intact for the live crawler.
|
|
912
|
-
|
|
913
|
-
|
|
963
|
+
* @param options - Optional close settings. `timeoutMs` is accepted for
|
|
964
|
+
* compatibility with {@link ArchiveAccessor.close}'s signature (forwarded
|
|
965
|
+
* to the base `super.close()` call below); the rest are progress
|
|
966
|
+
* callbacks (issue #294) forwarded to {@link write} when this call ends
|
|
967
|
+
* up taking the recovery-write branch (the archive file doesn't exist
|
|
968
|
+
* yet).
|
|
969
|
+
* @param options.timeoutMs - See {@link ArchiveAccessor.close}.
|
|
970
|
+
* @param options.onRecoveryStart - Called once, only when this `close()`
|
|
971
|
+
* is about to write the archive because the file doesn't exist on
|
|
972
|
+
* disk yet — e.g. a caller's own explicit `write()` threw before
|
|
973
|
+
* finishing, or was never called at all. Without this, a listener
|
|
974
|
+
* that already tore down its display after that earlier failure has
|
|
975
|
+
* no way to know a second, recovery write is happening.
|
|
976
|
+
* @param options.onTarProgress - See {@link write}.
|
|
977
|
+
* @param options.onStep - See {@link write}.
|
|
978
|
+
*/
|
|
979
|
+
async close(options) {
|
|
914
980
|
if (this.#closeOnce) {
|
|
915
981
|
return this.#closeOnce;
|
|
916
982
|
}
|
|
917
|
-
this.#closeOnce = this.#runFullClose();
|
|
983
|
+
this.#closeOnce = this.#runFullClose(options);
|
|
918
984
|
return this.#closeOnce;
|
|
919
985
|
}
|
|
920
986
|
/**
|
|
@@ -38,6 +38,15 @@
|
|
|
38
38
|
* @param cacheDir - Absolute path the extracted contents should end up at.
|
|
39
39
|
* @param cacheKey - The cache key used to derive `cacheDir`. Recomputed
|
|
40
40
|
* after extraction to detect concurrent writers; must match.
|
|
41
|
+
* @param onExtractProgress - Called during the untar step with bytes read
|
|
42
|
+
* so far and the archive's total size (issue #294: a cold cache on a
|
|
43
|
+
* large archive can take tens of seconds with no other signal it isn't
|
|
44
|
+
* hung). Never called on a cache hit — {@link isCacheDirReady} short-
|
|
45
|
+
* circuits before `untar` runs — so callers can treat "never invoked" as
|
|
46
|
+
* the definition of a hit and skip printing a phase label until the
|
|
47
|
+
* first call actually arrives. Only the first concurrent caller for a
|
|
48
|
+
* given `cacheDir` sees callbacks; same-`cacheDir` callers deduped
|
|
49
|
+
* through {@link inFlightByCacheDir} just await the shared promise.
|
|
41
50
|
* @returns Resolves once `cacheDir` is ready to be opened read-only.
|
|
42
51
|
*/
|
|
43
|
-
export declare function extractArchiveToCache(archivePath: string, cacheRoot: string, cacheDir: string, cacheKey: string): Promise<void>;
|
|
52
|
+
export declare function extractArchiveToCache(archivePath: string, cacheRoot: string, cacheDir: string, cacheKey: string, onExtractProgress?: (readBytes: number, totalBytes: number) => void): Promise<void>;
|
|
@@ -86,9 +86,18 @@ const inFlightByCacheDir = new Map();
|
|
|
86
86
|
* @param cacheDir - Absolute path the extracted contents should end up at.
|
|
87
87
|
* @param cacheKey - The cache key used to derive `cacheDir`. Recomputed
|
|
88
88
|
* after extraction to detect concurrent writers; must match.
|
|
89
|
+
* @param onExtractProgress - Called during the untar step with bytes read
|
|
90
|
+
* so far and the archive's total size (issue #294: a cold cache on a
|
|
91
|
+
* large archive can take tens of seconds with no other signal it isn't
|
|
92
|
+
* hung). Never called on a cache hit — {@link isCacheDirReady} short-
|
|
93
|
+
* circuits before `untar` runs — so callers can treat "never invoked" as
|
|
94
|
+
* the definition of a hit and skip printing a phase label until the
|
|
95
|
+
* first call actually arrives. Only the first concurrent caller for a
|
|
96
|
+
* given `cacheDir` sees callbacks; same-`cacheDir` callers deduped
|
|
97
|
+
* through {@link inFlightByCacheDir} just await the shared promise.
|
|
89
98
|
* @returns Resolves once `cacheDir` is ready to be opened read-only.
|
|
90
99
|
*/
|
|
91
|
-
export async function extractArchiveToCache(archivePath, cacheRoot, cacheDir, cacheKey) {
|
|
100
|
+
export async function extractArchiveToCache(archivePath, cacheRoot, cacheDir, cacheKey, onExtractProgress) {
|
|
92
101
|
if (await isCacheDirReady(cacheDir)) {
|
|
93
102
|
return;
|
|
94
103
|
}
|
|
@@ -96,7 +105,7 @@ export async function extractArchiveToCache(archivePath, cacheRoot, cacheDir, ca
|
|
|
96
105
|
if (existing) {
|
|
97
106
|
return existing;
|
|
98
107
|
}
|
|
99
|
-
const promise = runExtraction(archivePath, cacheRoot, cacheDir, cacheKey).finally(() => {
|
|
108
|
+
const promise = runExtraction(archivePath, cacheRoot, cacheDir, cacheKey, onExtractProgress).finally(() => {
|
|
100
109
|
inFlightByCacheDir.delete(cacheDir);
|
|
101
110
|
});
|
|
102
111
|
inFlightByCacheDir.set(cacheDir, promise);
|
|
@@ -110,8 +119,9 @@ export async function extractArchiveToCache(archivePath, cacheRoot, cacheDir, ca
|
|
|
110
119
|
* @param cacheRoot - Absolute path to the cache root directory.
|
|
111
120
|
* @param cacheDir - Absolute path the extracted contents should end up at.
|
|
112
121
|
* @param cacheKey - Pre-extraction cache key, re-verified post-extraction.
|
|
122
|
+
* @param onExtractProgress - See {@link extractArchiveToCache}.
|
|
113
123
|
*/
|
|
114
|
-
async function runExtraction(archivePath, cacheRoot, cacheDir, cacheKey) {
|
|
124
|
+
async function runExtraction(archivePath, cacheRoot, cacheDir, cacheKey, onExtractProgress) {
|
|
115
125
|
await fs.mkdir(cacheRoot, { recursive: true });
|
|
116
126
|
const releaseLock = await acquireLockWithPeerWait(cacheDir);
|
|
117
127
|
try {
|
|
@@ -131,7 +141,7 @@ async function runExtraction(archivePath, cacheRoot, cacheDir, cacheKey) {
|
|
|
131
141
|
// it (with `newer:true` cherrypicking entries) so wipe first.
|
|
132
142
|
await fs.rm(stagingDir, { recursive: true, force: true });
|
|
133
143
|
await fs.mkdir(stagingDir, { recursive: true });
|
|
134
|
-
await untar(archivePath, { cwd: stagingDir });
|
|
144
|
+
await untar(archivePath, { cwd: stagingDir, onProgress: onExtractProgress });
|
|
135
145
|
// Concurrent-writer detection: if the source archive changed
|
|
136
146
|
// during our untar, the contents we just landed do NOT match the
|
|
137
147
|
// `cacheKey` that named the directory. Drop the staging dir and
|
|
@@ -7,12 +7,22 @@ import type { Knex } from 'knex';
|
|
|
7
7
|
* - `page_errors` — partial scrape failures, FK → `content_items(id)`
|
|
8
8
|
* - `crawl_errors` — crawler-level error channel (no FK; the URL may be
|
|
9
9
|
* an external link that failed DNS, or null for a process-level error)
|
|
10
|
-
* - `
|
|
10
|
+
* - `technology_signals` — one un-combined technology-detection signal
|
|
11
|
+
* (Wappalyzer or structural) per row, FK → `content_items(id)`
|
|
12
|
+
* - `page_technologies` — confidence-combined roll-up of
|
|
13
|
+
* `technology_signals`, one row per (page, technology), FK →
|
|
14
|
+
* `content_items(id)`. Supersedes the removed `page_tags` (Wappalyzer-only)
|
|
15
|
+
* - `technology_js_scan_cache` — resource-scoped cache for the post-crawl
|
|
16
|
+
* JS license-comment scan, FK → `resource_items(id)`
|
|
11
17
|
* - `page_jsonld` — JSON-LD / SpeculationRules, FK → `content_items(id)`
|
|
12
18
|
* - `page_main_content_headings` / `_images` / `_tables` / `_buttons` /
|
|
13
19
|
* `_iframes` / `_videos` / `_audios` / `_canvases` — beholder
|
|
14
20
|
* `MainContentsData` sub-entity arrays, one row per DOM element, FK →
|
|
15
21
|
* `content_items(id)`
|
|
22
|
+
* - `page_main_content_custom_elements` — Web Components (custom elements)
|
|
23
|
+
* found in the main-content region, one row per DOM element, FK →
|
|
24
|
+
* `content_items(id)`. Unlike its eight siblings above, captured by
|
|
25
|
+
* nitpicker itself (`crawler/capture-custom-elements.ts`), not beholder
|
|
16
26
|
* - `inventory_runs` — `--inventory` audit log (no FK; append-only)
|
|
17
27
|
* - `network_outages` — operator-network-outage journal (no FK; append-only
|
|
18
28
|
* except `ended_at`, which is written once on recovery)
|
|
@@ -50,7 +60,7 @@ import type { Knex } from 'knex';
|
|
|
50
60
|
* Unlike `createRefTables` / `createEntityTables` (whose callers guard with
|
|
51
61
|
* a single sentinel table), each table here is guarded individually because
|
|
52
62
|
* the migration-script caller sees archives where any subset may already
|
|
53
|
-
* exist (e.g. `
|
|
63
|
+
* exist (e.g. `page_jsonld` from the 0.10 migration but no `inventory_runs`).
|
|
54
64
|
* Index creation stays inside each guard: an existing table keeps whatever
|
|
55
65
|
* indexes its creation path declared.
|
|
56
66
|
* @param instance - The Knex query builder instance connected to the database.
|
|
@@ -6,12 +6,22 @@
|
|
|
6
6
|
* - `page_errors` — partial scrape failures, FK → `content_items(id)`
|
|
7
7
|
* - `crawl_errors` — crawler-level error channel (no FK; the URL may be
|
|
8
8
|
* an external link that failed DNS, or null for a process-level error)
|
|
9
|
-
* - `
|
|
9
|
+
* - `technology_signals` — one un-combined technology-detection signal
|
|
10
|
+
* (Wappalyzer or structural) per row, FK → `content_items(id)`
|
|
11
|
+
* - `page_technologies` — confidence-combined roll-up of
|
|
12
|
+
* `technology_signals`, one row per (page, technology), FK →
|
|
13
|
+
* `content_items(id)`. Supersedes the removed `page_tags` (Wappalyzer-only)
|
|
14
|
+
* - `technology_js_scan_cache` — resource-scoped cache for the post-crawl
|
|
15
|
+
* JS license-comment scan, FK → `resource_items(id)`
|
|
10
16
|
* - `page_jsonld` — JSON-LD / SpeculationRules, FK → `content_items(id)`
|
|
11
17
|
* - `page_main_content_headings` / `_images` / `_tables` / `_buttons` /
|
|
12
18
|
* `_iframes` / `_videos` / `_audios` / `_canvases` — beholder
|
|
13
19
|
* `MainContentsData` sub-entity arrays, one row per DOM element, FK →
|
|
14
20
|
* `content_items(id)`
|
|
21
|
+
* - `page_main_content_custom_elements` — Web Components (custom elements)
|
|
22
|
+
* found in the main-content region, one row per DOM element, FK →
|
|
23
|
+
* `content_items(id)`. Unlike its eight siblings above, captured by
|
|
24
|
+
* nitpicker itself (`crawler/capture-custom-elements.ts`), not beholder
|
|
15
25
|
* - `inventory_runs` — `--inventory` audit log (no FK; append-only)
|
|
16
26
|
* - `network_outages` — operator-network-outage journal (no FK; append-only
|
|
17
27
|
* except `ended_at`, which is written once on recovery)
|
|
@@ -49,7 +59,7 @@
|
|
|
49
59
|
* Unlike `createRefTables` / `createEntityTables` (whose callers guard with
|
|
50
60
|
* a single sentinel table), each table here is guarded individually because
|
|
51
61
|
* the migration-script caller sees archives where any subset may already
|
|
52
|
-
* exist (e.g. `
|
|
62
|
+
* exist (e.g. `page_jsonld` from the 0.10 migration but no `inventory_runs`).
|
|
53
63
|
* Index creation stays inside each guard: an existing table keeps whatever
|
|
54
64
|
* indexes its creation path declared.
|
|
55
65
|
* @param instance - The Knex query builder instance connected to the database.
|
|
@@ -93,36 +103,69 @@ export async function createAdjunctTables(instance) {
|
|
|
93
103
|
t.integer('createdAt').notNullable();
|
|
94
104
|
});
|
|
95
105
|
}
|
|
96
|
-
if (!(await instance.schema.hasTable('
|
|
97
|
-
await instance.schema.createTable('
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
106
|
+
if (!(await instance.schema.hasTable('technology_signals'))) {
|
|
107
|
+
await instance.schema.createTable('technology_signals', (t) => {
|
|
108
|
+
// One un-combined signal for one technology on one page — the
|
|
109
|
+
// source-of-truth granular evidence `page_technologies` rolls up.
|
|
110
|
+
// Supersedes `page_tags` (Wappalyzer-only; removed): `signalType`
|
|
111
|
+
// covers Wappalyzer AND nitpicker's own structural detections
|
|
112
|
+
// (URL patterns, HTML markers, scoped attributes, meta generator,
|
|
113
|
+
// JS license comments) in one unified model. See
|
|
114
|
+
// `archive/meta/technologies/`.
|
|
103
115
|
t.increments('id');
|
|
104
116
|
t.integer('pageId')
|
|
105
117
|
.notNullable()
|
|
106
118
|
.unsigned()
|
|
107
119
|
.references('content_items.id')
|
|
108
120
|
.onDelete('CASCADE');
|
|
109
|
-
t.string('
|
|
121
|
+
t.string('technology').notNullable();
|
|
122
|
+
t.string('signalType').notNullable();
|
|
123
|
+
t.text('evidence');
|
|
124
|
+
t.integer('weight').notNullable();
|
|
125
|
+
t.index('pageId');
|
|
126
|
+
});
|
|
127
|
+
await instance.raw('CREATE INDEX technology_signals_tech_type ON technology_signals(technology, signalType)');
|
|
128
|
+
}
|
|
129
|
+
if (!(await instance.schema.hasTable('page_technologies'))) {
|
|
130
|
+
await instance.schema.createTable('page_technologies', (t) => {
|
|
131
|
+
// Confidence-combined roll-up of `technology_signals`, one row
|
|
132
|
+
// per (page, technology) — the read-optimised projection every
|
|
133
|
+
// query/viewer/MCP consumer reads instead of re-combining
|
|
134
|
+
// `technology_signals` on every request. Always written in the
|
|
135
|
+
// same transaction as its `technology_signals` rows; see
|
|
136
|
+
// ARCHITECTURE.md's invariant that the two tables are updated
|
|
137
|
+
// as a pair.
|
|
138
|
+
t.increments('id');
|
|
139
|
+
t.integer('pageId')
|
|
140
|
+
.notNullable()
|
|
141
|
+
.unsigned()
|
|
142
|
+
.references('content_items.id')
|
|
143
|
+
.onDelete('CASCADE');
|
|
144
|
+
t.string('technology').notNullable();
|
|
110
145
|
t.string('category');
|
|
111
|
-
t.string('externalId');
|
|
112
146
|
t.string('version');
|
|
113
|
-
t.integer('confidence');
|
|
114
|
-
t.
|
|
115
|
-
t.
|
|
116
|
-
t.index('
|
|
117
|
-
|
|
118
|
-
|
|
147
|
+
t.integer('confidence').notNullable();
|
|
148
|
+
t.integer('signalCount').notNullable();
|
|
149
|
+
t.unique(['pageId', 'technology']);
|
|
150
|
+
t.index('technology');
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (!(await instance.schema.hasTable('technology_js_scan_cache'))) {
|
|
154
|
+
await instance.schema.createTable('technology_js_scan_cache', (t) => {
|
|
155
|
+
// Resource-scoped cache preventing "JSスキャン・エンリッチメント"
|
|
156
|
+
// (post-crawl network enrichment, see ARCHITECTURE.md) from
|
|
157
|
+
// re-fetching the same JS resource on `--append`/`--retry-failed`.
|
|
158
|
+
// `resourceId` is the PK (one scan outcome per resource, ever);
|
|
159
|
+
// `technology`/`evidence` are `null` on a non-match.
|
|
160
|
+
t.integer('resourceId')
|
|
161
|
+
.primary()
|
|
162
|
+
.unsigned()
|
|
163
|
+
.references('resource_items.id')
|
|
164
|
+
.onDelete('CASCADE');
|
|
165
|
+
t.integer('scannedAt').notNullable();
|
|
166
|
+
t.string('technology');
|
|
167
|
+
t.text('evidence');
|
|
119
168
|
});
|
|
120
|
-
// Compound indexes for the "find duplicate IDs across pages" and
|
|
121
|
-
// "list pages using provider X" hot paths. Knex's schema builder
|
|
122
|
-
// can't express compound indexes inline in a way that round-trips
|
|
123
|
-
// through libsql consistently, so raw SQL is used.
|
|
124
|
-
await instance.raw('CREATE INDEX page_tags_provider_extId ON page_tags(provider, externalId)');
|
|
125
|
-
await instance.raw('CREATE INDEX page_tags_provider_pageId ON page_tags(provider, pageId)');
|
|
126
169
|
}
|
|
127
170
|
if (!(await instance.schema.hasTable('page_jsonld'))) {
|
|
128
171
|
await instance.schema.createTable('page_jsonld', (t) => {
|
|
@@ -151,12 +194,16 @@ export async function createAdjunctTables(instance) {
|
|
|
151
194
|
await instance.raw('CREATE INDEX page_jsonld_type_pageId ON page_jsonld(type, pageId)');
|
|
152
195
|
}
|
|
153
196
|
// Beholder `MainContentsData` sub-entities, one adjunct table per array
|
|
154
|
-
// (headings/images/tables/buttons/iframes/videos/audios/canvases)
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
// `
|
|
158
|
-
//
|
|
159
|
-
//
|
|
197
|
+
// (headings/images/tables/buttons/iframes/videos/audios/canvases), plus
|
|
198
|
+
// `page_main_content_custom_elements` (Web Components) — the sole
|
|
199
|
+
// exception, captured by nitpicker itself rather than beholder (see
|
|
200
|
+
// `create-entity-tables.ts`'s `main_content_custom_element_count` JSDoc
|
|
201
|
+
// for why). Same shape as `page_tags` / `page_jsonld`: `pageId` FK →
|
|
202
|
+
// `content_items(id)` ON DELETE CASCADE, individually guarded so any
|
|
203
|
+
// subset can pre-exist. `order` preserves the DOM traversal order the
|
|
204
|
+
// source array is captured in (0-based); it is not itself an index
|
|
205
|
+
// target since these tables are always read whole-page via
|
|
206
|
+
// `WHERE pageId = ? ORDER BY "order"`.
|
|
160
207
|
if (!(await instance.schema.hasTable('page_main_content_headings'))) {
|
|
161
208
|
await instance.schema.createTable('page_main_content_headings', (t) => {
|
|
162
209
|
t.increments('id');
|
|
@@ -278,6 +325,21 @@ export async function createAdjunctTables(instance) {
|
|
|
278
325
|
t.index('pageId');
|
|
279
326
|
});
|
|
280
327
|
}
|
|
328
|
+
if (!(await instance.schema.hasTable('page_main_content_custom_elements'))) {
|
|
329
|
+
await instance.schema.createTable('page_main_content_custom_elements', (t) => {
|
|
330
|
+
t.increments('id');
|
|
331
|
+
t.integer('pageId')
|
|
332
|
+
.notNullable()
|
|
333
|
+
.unsigned()
|
|
334
|
+
.references('content_items.id')
|
|
335
|
+
.onDelete('CASCADE');
|
|
336
|
+
t.integer('order').notNullable();
|
|
337
|
+
t.string('nodeName').notNullable();
|
|
338
|
+
t.string('elementId');
|
|
339
|
+
t.text('classList');
|
|
340
|
+
t.index('pageId');
|
|
341
|
+
});
|
|
342
|
+
}
|
|
281
343
|
if (!(await instance.schema.hasTable('inventory_runs'))) {
|
|
282
344
|
await instance.schema.createTable('inventory_runs', (t) => {
|
|
283
345
|
// One row per successful `--inventory <list>` invocation. The
|
|
@@ -148,7 +148,11 @@ import type { Knex } from 'knex';
|
|
|
148
148
|
* through `text_refs` — the values are page-specific diagnostics with low
|
|
149
149
|
* cross-page reuse, so the ref-table dedup machinery would add write-path
|
|
150
150
|
* cost without a corresponding storage win. `main_content_class_list` holds
|
|
151
|
-
* a JSON-encoded string array.
|
|
151
|
+
* a JSON-encoded string array. `main_content_custom_element_count` is the
|
|
152
|
+
* one exception to "derived from beholder's `MainContentsData`" — beholder
|
|
153
|
+
* has no `customElements` category, so nitpicker captures Web Components
|
|
154
|
+
* itself (`crawler/capture-custom-elements.ts`) after `scrapeStart`
|
|
155
|
+
* returns, the same way it captures image dom-paths.
|
|
152
156
|
*
|
|
153
157
|
* ### Index rationale
|
|
154
158
|
*
|
|
@@ -147,7 +147,11 @@
|
|
|
147
147
|
* through `text_refs` — the values are page-specific diagnostics with low
|
|
148
148
|
* cross-page reuse, so the ref-table dedup machinery would add write-path
|
|
149
149
|
* cost without a corresponding storage win. `main_content_class_list` holds
|
|
150
|
-
* a JSON-encoded string array.
|
|
150
|
+
* a JSON-encoded string array. `main_content_custom_element_count` is the
|
|
151
|
+
* one exception to "derived from beholder's `MainContentsData`" — beholder
|
|
152
|
+
* has no `customElements` category, so nitpicker captures Web Components
|
|
153
|
+
* itself (`crawler/capture-custom-elements.ts`) after `scrapeStart`
|
|
154
|
+
* returns, the same way it captures image dom-paths.
|
|
151
155
|
*
|
|
152
156
|
* ### Index rationale
|
|
153
157
|
*
|
|
@@ -298,6 +302,7 @@ export async function createEntityTables(instance) {
|
|
|
298
302
|
main_content_video_count INTEGER,
|
|
299
303
|
main_content_audio_count INTEGER,
|
|
300
304
|
main_content_canvas_count INTEGER,
|
|
305
|
+
main_content_custom_element_count INTEGER,
|
|
301
306
|
scroll_height_desktop INTEGER,
|
|
302
307
|
scroll_height_mobile INTEGER,
|
|
303
308
|
body_hash BLOB,
|