@nitpicker/crawler 0.13.0 → 0.15.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 (171) hide show
  1. package/lib/archive/archive-accessor.d.ts +56 -0
  2. package/lib/archive/archive-accessor.js +72 -0
  3. package/lib/archive/archive.d.ts +108 -9
  4. package/lib/archive/archive.js +109 -7
  5. package/lib/archive/body-hash/compute-body-hash.d.ts +22 -0
  6. package/lib/archive/body-hash/compute-body-hash.js +31 -0
  7. package/lib/archive/body-hash/extract-body.d.ts +18 -0
  8. package/lib/archive/body-hash/extract-body.js +29 -0
  9. package/lib/archive/body-hash/mask-dynamic-ids.d.ts +33 -0
  10. package/lib/archive/body-hash/mask-dynamic-ids.js +44 -0
  11. package/lib/archive/body-hash/normalize-url-like-strings.d.ts +16 -0
  12. package/lib/archive/body-hash/normalize-url-like-strings.js +19 -0
  13. package/lib/archive/cache/clear-archive-cache-entry.d.ts +19 -0
  14. package/lib/archive/cache/clear-archive-cache-entry.js +25 -0
  15. package/lib/archive/cache/clear-archive-cache-root.d.ts +26 -0
  16. package/lib/archive/cache/clear-archive-cache-root.js +32 -0
  17. package/lib/archive/cache/list-archive-cache-entries.d.ts +23 -0
  18. package/lib/archive/cache/list-archive-cache-entries.js +125 -0
  19. package/lib/archive/cache/path-exists.d.ts +9 -0
  20. package/lib/archive/cache/path-exists.js +18 -0
  21. package/lib/archive/cache/types.d.ts +33 -0
  22. package/lib/archive/cache/types.js +1 -0
  23. package/lib/archive/create-adjunct-tables.d.ts +18 -0
  24. package/lib/archive/create-adjunct-tables.js +277 -0
  25. package/lib/archive/create-entity-tables.d.ts +14 -0
  26. package/lib/archive/create-entity-tables.js +52 -1
  27. package/lib/archive/database.d.ts +91 -2
  28. package/lib/archive/database.js +128 -0
  29. package/lib/archive/db-ops/_shared/clear-write-ref-caches.js +1 -0
  30. package/lib/archive/db-ops/_shared/compress-payload.d.ts +26 -0
  31. package/lib/archive/db-ops/_shared/compress-payload.js +30 -0
  32. package/lib/archive/db-ops/_shared/create-write-ref-caches.js +1 -0
  33. package/lib/archive/db-ops/_shared/types.d.ts +2 -0
  34. package/lib/archive/db-ops/_shared/upsert-json-ref.js +3 -3
  35. package/lib/archive/db-ops/analysis/replace-page-templates.d.ts +29 -0
  36. package/lib/archive/db-ops/analysis/replace-page-templates.js +84 -0
  37. package/lib/archive/db-ops/analysis/types.d.ts +61 -0
  38. package/lib/archive/db-ops/analysis/types.js +1 -0
  39. package/lib/archive/db-ops/config/info-column-allowlist.js +1 -0
  40. package/lib/archive/db-ops/console-logs/compute-console-log-hash.d.ts +19 -0
  41. package/lib/archive/db-ops/console-logs/compute-console-log-hash.js +30 -0
  42. package/lib/archive/db-ops/console-logs/replace-console-logs.d.ts +34 -0
  43. package/lib/archive/db-ops/console-logs/replace-console-logs.js +123 -0
  44. package/lib/archive/db-ops/console-logs/stringify-console-log-args.d.ts +16 -0
  45. package/lib/archive/db-ops/console-logs/stringify-console-log-args.js +26 -0
  46. package/lib/archive/db-ops/console-logs/types.d.ts +38 -0
  47. package/lib/archive/db-ops/console-logs/types.js +1 -0
  48. package/lib/archive/db-ops/console-logs/upsert-console-log-item.d.ts +29 -0
  49. package/lib/archive/db-ops/console-logs/upsert-console-log-item.js +52 -0
  50. package/lib/archive/db-ops/errors/list-dns-burned-host-candidates.d.ts +11 -0
  51. package/lib/archive/db-ops/errors/list-dns-burned-host-candidates.js +21 -4
  52. package/lib/archive/db-ops/inventory/record-inventory-run.js +1 -0
  53. package/lib/archive/db-ops/lifecycle/init.d.ts +25 -10
  54. package/lib/archive/db-ops/lifecycle/init.js +39 -10
  55. package/lib/archive/db-ops/meta/get-audios-of-page.d.ts +11 -0
  56. package/lib/archive/db-ops/meta/get-audios-of-page.js +15 -0
  57. package/lib/archive/db-ops/meta/get-buttons-of-page.d.ts +11 -0
  58. package/lib/archive/db-ops/meta/get-buttons-of-page.js +15 -0
  59. package/lib/archive/db-ops/meta/get-canvases-of-page.d.ts +11 -0
  60. package/lib/archive/db-ops/meta/get-canvases-of-page.js +15 -0
  61. package/lib/archive/db-ops/meta/get-headings-of-page.d.ts +11 -0
  62. package/lib/archive/db-ops/meta/get-headings-of-page.js +15 -0
  63. package/lib/archive/db-ops/meta/get-iframes-of-page.d.ts +11 -0
  64. package/lib/archive/db-ops/meta/get-iframes-of-page.js +15 -0
  65. package/lib/archive/db-ops/meta/get-main-content-images-of-page.d.ts +11 -0
  66. package/lib/archive/db-ops/meta/get-main-content-images-of-page.js +15 -0
  67. package/lib/archive/db-ops/meta/get-main-content-tables-of-page.d.ts +11 -0
  68. package/lib/archive/db-ops/meta/get-main-content-tables-of-page.js +15 -0
  69. package/lib/archive/db-ops/meta/get-videos-of-page.d.ts +11 -0
  70. package/lib/archive/db-ops/meta/get-videos-of-page.js +15 -0
  71. package/lib/archive/db-ops/outages/close-network-outage.d.ts +12 -0
  72. package/lib/archive/db-ops/outages/close-network-outage.js +15 -0
  73. package/lib/archive/db-ops/outages/close-stale-open-network-outages.d.ts +26 -0
  74. package/lib/archive/db-ops/outages/close-stale-open-network-outages.js +42 -0
  75. package/lib/archive/db-ops/outages/compute-outage-clamp-timestamp.d.ts +26 -0
  76. package/lib/archive/db-ops/outages/compute-outage-clamp-timestamp.js +31 -0
  77. package/lib/archive/db-ops/outages/insert-network-outage.d.ts +14 -0
  78. package/lib/archive/db-ops/outages/insert-network-outage.js +29 -0
  79. package/lib/archive/db-ops/outages/list-network-outages.d.ts +28 -0
  80. package/lib/archive/db-ops/outages/list-network-outages.js +42 -0
  81. package/lib/archive/db-ops/pages/read/build-page-query.js +1 -1
  82. package/lib/archive/db-ops/pages/reset/repromote-external-pages.js +19 -10
  83. package/lib/archive/db-ops/pages/reset/reset-failed-pages.d.ts +18 -3
  84. package/lib/archive/db-ops/pages/reset/reset-failed-pages.js +48 -10
  85. package/lib/archive/db-ops/pages/write/insert-audios.d.ts +14 -0
  86. package/lib/archive/db-ops/pages/write/insert-audios.js +25 -0
  87. package/lib/archive/db-ops/pages/write/insert-buttons.d.ts +14 -0
  88. package/lib/archive/db-ops/pages/write/insert-buttons.js +29 -0
  89. package/lib/archive/db-ops/pages/write/insert-canvases.d.ts +14 -0
  90. package/lib/archive/db-ops/pages/write/insert-canvases.js +26 -0
  91. package/lib/archive/db-ops/pages/write/insert-headings.d.ts +15 -0
  92. package/lib/archive/db-ops/pages/write/insert-headings.js +27 -0
  93. package/lib/archive/db-ops/pages/write/insert-iframes.d.ts +14 -0
  94. package/lib/archive/db-ops/pages/write/insert-iframes.js +28 -0
  95. package/lib/archive/db-ops/pages/write/insert-main-content-images.d.ts +20 -0
  96. package/lib/archive/db-ops/pages/write/insert-main-content-images.js +32 -0
  97. package/lib/archive/db-ops/pages/write/insert-main-content-tables.d.ts +14 -0
  98. package/lib/archive/db-ops/pages/write/insert-main-content-tables.js +29 -0
  99. package/lib/archive/db-ops/pages/write/insert-page.js +15 -2
  100. package/lib/archive/db-ops/pages/write/insert-videos.d.ts +14 -0
  101. package/lib/archive/db-ops/pages/write/insert-videos.js +28 -0
  102. package/lib/archive/db-ops/pages/write/update-page.js +33 -0
  103. package/lib/archive/db-ops/pages/write/write-page-html-blob.js +6 -6
  104. package/lib/archive/filesystem/output-binary.d.ts +12 -0
  105. package/lib/archive/filesystem/output-binary.js +17 -0
  106. package/lib/archive/get-failed-page-messages.d.ts +20 -8
  107. package/lib/archive/get-failed-page-messages.js +29 -15
  108. package/lib/archive/init-schema.js +2 -1
  109. package/lib/archive/meta/compute-main-contents-denormalized.d.ts +22 -0
  110. package/lib/archive/meta/compute-main-contents-denormalized.js +63 -0
  111. package/lib/archive/meta/types.d.ts +263 -0
  112. package/lib/archive/migrate-content-items-alias-of-id.d.ts +47 -0
  113. package/lib/archive/migrate-content-items-alias-of-id.js +58 -0
  114. package/lib/archive/migrate-info-main-content-selector.d.ts +11 -0
  115. package/lib/archive/migrate-info-main-content-selector.js +24 -0
  116. package/lib/archive/migrate-inventory-runs-invalid-skipped.d.ts +11 -0
  117. package/lib/archive/migrate-inventory-runs-invalid-skipped.js +24 -0
  118. package/lib/archive/migrate-main-contents-columns.d.ts +21 -0
  119. package/lib/archive/migrate-main-contents-columns.js +50 -0
  120. package/lib/archive/migrate-page-meta-body-hash.d.ts +39 -0
  121. package/lib/archive/migrate-page-meta-body-hash.js +52 -0
  122. package/lib/archive/migrate-page-meta-console-error-count.d.ts +22 -0
  123. package/lib/archive/migrate-page-meta-console-error-count.js +34 -0
  124. package/lib/archive/page.d.ts +145 -3
  125. package/lib/archive/page.js +195 -0
  126. package/lib/archive/types.d.ts +86 -2
  127. package/lib/archive/url-alias/compute-tier-a-alias-key.d.ts +31 -0
  128. package/lib/archive/url-alias/compute-tier-a-alias-key.js +36 -0
  129. package/lib/archive/url-alias/compute-tier-b-alias-key.d.ts +19 -0
  130. package/lib/archive/url-alias/compute-tier-b-alias-key.js +30 -0
  131. package/lib/archive/url-alias/format-alias-key.d.ts +13 -0
  132. package/lib/archive/url-alias/format-alias-key.js +14 -0
  133. package/lib/archive/url-alias/parse-alias-key-parts.d.ts +15 -0
  134. package/lib/archive/url-alias/parse-alias-key-parts.js +40 -0
  135. package/lib/archive/url-alias/types.d.ts +7 -0
  136. package/lib/archive/url-alias/types.js +1 -0
  137. package/lib/crawler/choose-probe-host.d.ts +24 -0
  138. package/lib/crawler/choose-probe-host.js +38 -0
  139. package/lib/crawler/clear-dns-burned-host-cache.js +2 -0
  140. package/lib/crawler/crawler.js +261 -2
  141. package/lib/crawler/dns-burned-host-burn-timestamps.d.ts +17 -0
  142. package/lib/crawler/dns-burned-host-burn-timestamps.js +17 -0
  143. package/lib/crawler/evict-network-classified-destination-cache-entries.d.ts +26 -0
  144. package/lib/crawler/evict-network-classified-destination-cache-entries.js +34 -0
  145. package/lib/crawler/evict-outage-tainted-dns-burns.d.ts +23 -0
  146. package/lib/crawler/evict-outage-tainted-dns-burns.js +26 -0
  147. package/lib/crawler/fetch-destination.js +2 -0
  148. package/lib/crawler/link-to-page-data.js +2 -0
  149. package/lib/crawler/network-gate.d.ts +49 -0
  150. package/lib/crawler/network-gate.js +78 -0
  151. package/lib/crawler/network-outage-detector.d.ts +51 -0
  152. package/lib/crawler/network-outage-detector.js +81 -0
  153. package/lib/crawler/network-outage-summary-counter.d.ts +21 -0
  154. package/lib/crawler/network-outage-summary-counter.js +18 -0
  155. package/lib/crawler/probe-network.d.ts +26 -0
  156. package/lib/crawler/probe-network.js +21 -0
  157. package/lib/crawler/resource-to-page-data.js +2 -0
  158. package/lib/crawler/types.d.ts +152 -1
  159. package/lib/crawler-orchestrator.d.ts +62 -18
  160. package/lib/crawler-orchestrator.js +163 -26
  161. package/lib/crawler.d.ts +21 -0
  162. package/lib/crawler.js +17 -0
  163. package/lib/is-within-outage-window.d.ts +49 -0
  164. package/lib/is-within-outage-window.js +33 -0
  165. package/lib/network-related-error-kinds.d.ts +23 -0
  166. package/lib/network-related-error-kinds.js +28 -0
  167. package/lib/types.d.ts +9 -1
  168. package/lib/utils/array/each-splitted.d.ts +1 -1
  169. package/lib/utils/compute-file-sha256.d.ts +17 -17
  170. package/lib/utils/compute-file-sha256.js +18 -47
  171. package/package.json +3 -3
@@ -0,0 +1,29 @@
1
+ import { eachSplitted } from '../../../../utils/array/each-splitted.js';
2
+ /**
3
+ * Replaces the page's `page_main_content_buttons` rows with the freshly
4
+ * captured set. Called inside `updatePage`'s transaction, gated on
5
+ * `mainContents !== null`.
6
+ *
7
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
8
+ * wipe prior rows on a degraded re-scrape.
9
+ * @param pageId - The owning `content_items.id`.
10
+ * @param mainContents - Beholder's per-page main-content metrics.
11
+ * @param trx - The active transaction.
12
+ */
13
+ export async function insertButtons(pageId, mainContents, trx) {
14
+ if (mainContents.buttons.length === 0)
15
+ return;
16
+ const rows = mainContents.buttons.map((button, order) => ({
17
+ pageId,
18
+ order,
19
+ nodeName: button.nodeName,
20
+ role: button.role,
21
+ type: button.type,
22
+ text: button.text,
23
+ disabled: button.disabled,
24
+ }));
25
+ await trx('page_main_content_buttons').where('pageId', pageId).delete();
26
+ await eachSplitted(rows, 100, async (chunk) => {
27
+ await trx('page_main_content_buttons').insert(chunk);
28
+ });
29
+ }
@@ -0,0 +1,14 @@
1
+ import type { MainContentsData } from '@d-zero/beholder';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Replaces the page's `page_main_content_canvases` rows with the freshly
5
+ * captured set. Called inside `updatePage`'s transaction, gated on
6
+ * `mainContents !== null`.
7
+ *
8
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
9
+ * wipe prior rows on a degraded re-scrape.
10
+ * @param pageId - The owning `content_items.id`.
11
+ * @param mainContents - Beholder's per-page main-content metrics.
12
+ * @param trx - The active transaction.
13
+ */
14
+ export declare function insertCanvases(pageId: number, mainContents: MainContentsData, trx: Knex.Transaction): Promise<void>;
@@ -0,0 +1,26 @@
1
+ import { eachSplitted } from '../../../../utils/array/each-splitted.js';
2
+ /**
3
+ * Replaces the page's `page_main_content_canvases` rows with the freshly
4
+ * captured set. Called inside `updatePage`'s transaction, gated on
5
+ * `mainContents !== null`.
6
+ *
7
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
8
+ * wipe prior rows on a degraded re-scrape.
9
+ * @param pageId - The owning `content_items.id`.
10
+ * @param mainContents - Beholder's per-page main-content metrics.
11
+ * @param trx - The active transaction.
12
+ */
13
+ export async function insertCanvases(pageId, mainContents, trx) {
14
+ if (mainContents.canvases.length === 0)
15
+ return;
16
+ const rows = mainContents.canvases.map((canvas, order) => ({
17
+ pageId,
18
+ order,
19
+ width: canvas.width,
20
+ height: canvas.height,
21
+ }));
22
+ await trx('page_main_content_canvases').where('pageId', pageId).delete();
23
+ await eachSplitted(rows, 100, async (chunk) => {
24
+ await trx('page_main_content_canvases').insert(chunk);
25
+ });
26
+ }
@@ -0,0 +1,15 @@
1
+ import type { MainContentsData } from '@d-zero/beholder';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Replaces the page's `page_main_content_headings` rows with the freshly
5
+ * captured set. Called inside `updatePage`'s transaction, gated on
6
+ * `mainContents !== null` (non-HTML / external / metadata-only scrapes have
7
+ * no main-content data to write).
8
+ *
9
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
10
+ * wipe prior rows on a degraded re-scrape.
11
+ * @param pageId - The owning `content_items.id`.
12
+ * @param mainContents - Beholder's per-page main-content metrics.
13
+ * @param trx - The active transaction.
14
+ */
15
+ export declare function insertHeadings(pageId: number, mainContents: MainContentsData, trx: Knex.Transaction): Promise<void>;
@@ -0,0 +1,27 @@
1
+ import { eachSplitted } from '../../../../utils/array/each-splitted.js';
2
+ /**
3
+ * Replaces the page's `page_main_content_headings` rows with the freshly
4
+ * captured set. Called inside `updatePage`'s transaction, gated on
5
+ * `mainContents !== null` (non-HTML / external / metadata-only scrapes have
6
+ * no main-content data to write).
7
+ *
8
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
9
+ * wipe prior rows on a degraded re-scrape.
10
+ * @param pageId - The owning `content_items.id`.
11
+ * @param mainContents - Beholder's per-page main-content metrics.
12
+ * @param trx - The active transaction.
13
+ */
14
+ export async function insertHeadings(pageId, mainContents, trx) {
15
+ if (mainContents.headings.length === 0)
16
+ return;
17
+ const rows = mainContents.headings.map((heading, order) => ({
18
+ pageId,
19
+ order,
20
+ text: heading.text,
21
+ level: heading.level,
22
+ }));
23
+ await trx('page_main_content_headings').where('pageId', pageId).delete();
24
+ await eachSplitted(rows, 100, async (chunk) => {
25
+ await trx('page_main_content_headings').insert(chunk);
26
+ });
27
+ }
@@ -0,0 +1,14 @@
1
+ import type { MainContentsData } from '@d-zero/beholder';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Replaces the page's `page_main_content_iframes` rows with the freshly
5
+ * captured set. Called inside `updatePage`'s transaction, gated on
6
+ * `mainContents !== null`.
7
+ *
8
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
9
+ * wipe prior rows on a degraded re-scrape.
10
+ * @param pageId - The owning `content_items.id`.
11
+ * @param mainContents - Beholder's per-page main-content metrics.
12
+ * @param trx - The active transaction.
13
+ */
14
+ export declare function insertIframes(pageId: number, mainContents: MainContentsData, trx: Knex.Transaction): Promise<void>;
@@ -0,0 +1,28 @@
1
+ import { eachSplitted } from '../../../../utils/array/each-splitted.js';
2
+ /**
3
+ * Replaces the page's `page_main_content_iframes` rows with the freshly
4
+ * captured set. Called inside `updatePage`'s transaction, gated on
5
+ * `mainContents !== null`.
6
+ *
7
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
8
+ * wipe prior rows on a degraded re-scrape.
9
+ * @param pageId - The owning `content_items.id`.
10
+ * @param mainContents - Beholder's per-page main-content metrics.
11
+ * @param trx - The active transaction.
12
+ */
13
+ export async function insertIframes(pageId, mainContents, trx) {
14
+ if (mainContents.iframes.length === 0)
15
+ return;
16
+ const rows = mainContents.iframes.map((iframe, order) => ({
17
+ pageId,
18
+ order,
19
+ src: iframe.src,
20
+ title: iframe.title,
21
+ width: iframe.width,
22
+ height: iframe.height,
23
+ }));
24
+ await trx('page_main_content_iframes').where('pageId', pageId).delete();
25
+ await eachSplitted(rows, 100, async (chunk) => {
26
+ await trx('page_main_content_iframes').insert(chunk);
27
+ });
28
+ }
@@ -0,0 +1,20 @@
1
+ import type { MainContentsData } from '@d-zero/beholder';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Replaces the page's `page_main_content_images` rows with the freshly
5
+ * captured set. Called inside `updatePage`'s transaction, gated on
6
+ * `mainContents !== null`.
7
+ *
8
+ * Distinct from `image_items` (the whole-page image scan, `replaceImageItems`):
9
+ * this table only covers images inside the detected main-content region, so
10
+ * `src` is stored as a plain string rather than routed through
11
+ * `url_refs` / `blob_refs` — same low-cross-page-reuse rationale as the
12
+ * `page_meta.main_content_*` columns.
13
+ *
14
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
15
+ * wipe prior rows on a degraded re-scrape.
16
+ * @param pageId - The owning `content_items.id`.
17
+ * @param mainContents - Beholder's per-page main-content metrics.
18
+ * @param trx - The active transaction.
19
+ */
20
+ export declare function insertMainContentImages(pageId: number, mainContents: MainContentsData, trx: Knex.Transaction): Promise<void>;
@@ -0,0 +1,32 @@
1
+ import { eachSplitted } from '../../../../utils/array/each-splitted.js';
2
+ /**
3
+ * Replaces the page's `page_main_content_images` rows with the freshly
4
+ * captured set. Called inside `updatePage`'s transaction, gated on
5
+ * `mainContents !== null`.
6
+ *
7
+ * Distinct from `image_items` (the whole-page image scan, `replaceImageItems`):
8
+ * this table only covers images inside the detected main-content region, so
9
+ * `src` is stored as a plain string rather than routed through
10
+ * `url_refs` / `blob_refs` — same low-cross-page-reuse rationale as the
11
+ * `page_meta.main_content_*` columns.
12
+ *
13
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
14
+ * wipe prior rows on a degraded re-scrape.
15
+ * @param pageId - The owning `content_items.id`.
16
+ * @param mainContents - Beholder's per-page main-content metrics.
17
+ * @param trx - The active transaction.
18
+ */
19
+ export async function insertMainContentImages(pageId, mainContents, trx) {
20
+ if (mainContents.images.length === 0)
21
+ return;
22
+ const rows = mainContents.images.map((image, order) => ({
23
+ pageId,
24
+ order,
25
+ src: image.src,
26
+ alt: image.alt,
27
+ }));
28
+ await trx('page_main_content_images').where('pageId', pageId).delete();
29
+ await eachSplitted(rows, 100, async (chunk) => {
30
+ await trx('page_main_content_images').insert(chunk);
31
+ });
32
+ }
@@ -0,0 +1,14 @@
1
+ import type { MainContentsData } from '@d-zero/beholder';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Replaces the page's `page_main_content_tables` rows with the freshly
5
+ * captured set. Called inside `updatePage`'s transaction, gated on
6
+ * `mainContents !== null`.
7
+ *
8
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
9
+ * wipe prior rows on a degraded re-scrape.
10
+ * @param pageId - The owning `content_items.id`.
11
+ * @param mainContents - Beholder's per-page main-content metrics.
12
+ * @param trx - The active transaction.
13
+ */
14
+ export declare function insertMainContentTables(pageId: number, mainContents: MainContentsData, trx: Knex.Transaction): Promise<void>;
@@ -0,0 +1,29 @@
1
+ import { eachSplitted } from '../../../../utils/array/each-splitted.js';
2
+ /**
3
+ * Replaces the page's `page_main_content_tables` rows with the freshly
4
+ * captured set. Called inside `updatePage`'s transaction, gated on
5
+ * `mainContents !== null`.
6
+ *
7
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
8
+ * wipe prior rows on a degraded re-scrape.
9
+ * @param pageId - The owning `content_items.id`.
10
+ * @param mainContents - Beholder's per-page main-content metrics.
11
+ * @param trx - The active transaction.
12
+ */
13
+ export async function insertMainContentTables(pageId, mainContents, trx) {
14
+ if (mainContents.tables.length === 0)
15
+ return;
16
+ const rows = mainContents.tables.map((table, order) => ({
17
+ pageId,
18
+ order,
19
+ rows: table.rows,
20
+ cols: table.cols,
21
+ hasHeader: table.hasHeader,
22
+ hasFooter: table.hasFooter,
23
+ hasMergedCell: table.hasMergedCell,
24
+ }));
25
+ await trx('page_main_content_tables').where('pageId', pageId).delete();
26
+ await eachSplitted(rows, 100, async (chunk) => {
27
+ await trx('page_main_content_tables').insert(chunk);
28
+ });
29
+ }
@@ -1,4 +1,5 @@
1
1
  import { normalizeContentType } from '../../../../crawler/normalize-content-type.js';
2
+ import { computeMainContentsDenormalized } from '../../../meta/compute-main-contents-denormalized.js';
2
3
  import { computePageDenormalized } from '../../../meta/compute-page-denormalized.js';
3
4
  import { deriveFlatFromMeta } from '../../../meta/derive-flat-from-meta.js';
4
5
  import { deriveMetaExtras } from '../../../meta/derive-meta-extras.js';
@@ -46,6 +47,7 @@ export async function insertPage(knex, caches, page, isTarget, trx, source) {
46
47
  const pageId = await resolveContentItemId(qb, caches, page.url.withoutHashAndAuth, undefined, source);
47
48
  const flat = deriveFlatFromMeta(page.meta, page.url.href);
48
49
  const denorm = computePageDenormalized(page.meta);
50
+ const mainContentsDenorm = computeMainContentsDenormalized(page.mainContents, page.scrollHeight);
49
51
  const extras = deriveMetaExtras(page.meta);
50
52
  const now = Date.now();
51
53
  // Canonicalize so the stored dictionary value matches the exact-string
@@ -112,7 +114,7 @@ export async function insertPage(knex, caches, page, isTarget, trx, source) {
112
114
  entry.source = source;
113
115
  }
114
116
  }
115
- await upsertPageMeta(qb, caches, pageId, flat, denorm, extras);
117
+ await upsertPageMeta(qb, caches, pageId, flat, denorm, page.mainContents == null ? null : mainContentsDenorm, extras);
116
118
  return pageId;
117
119
  }
118
120
  /**
@@ -125,9 +127,16 @@ export async function insertPage(knex, caches, page, isTarget, trx, source) {
125
127
  * @param pageId - The owning `content_items.id`.
126
128
  * @param flat - Flat meta columns from `deriveFlatFromMeta`.
127
129
  * @param denorm - Denormalised aggregates from `computePageDenormalized`.
130
+ * @param mainContentsDenorm - Denormalised `main_content_*` / `scroll_height_*`
131
+ * aggregates from `computeMainContentsDenormalized`, or `null` when the
132
+ * page's `mainContents` was `null` (degraded / non-HTML scrape). `null`
133
+ * here omits all seventeen columns from the upsert entirely — on
134
+ * `ON CONFLICT DO UPDATE` this leaves a previous full scrape's values
135
+ * untouched rather than overwriting them with NULL, mirroring the
136
+ * `page_main_content_*` child-table writers' same no-op-on-null guard.
128
137
  * @param extras - Nested-Meta catch-all from `deriveMetaExtras`.
129
138
  */
130
- async function upsertPageMeta(qb, caches, pageId, flat, denorm, extras) {
139
+ async function upsertPageMeta(qb, caches, pageId, flat, denorm, mainContentsDenorm, extras) {
131
140
  const texts = new Set();
132
141
  for (const { source } of PAGE_META_COLUMN_MAPS.text) {
133
142
  const value = flat[source];
@@ -174,6 +183,10 @@ async function upsertPageMeta(qb, caches, pageId, flat, denorm, extras) {
174
183
  tag_count: denorm.tag_count,
175
184
  jsonld_count: denorm.jsonld_count,
176
185
  tags_providers_csv: denorm.tags_providers_csv,
186
+ // Omitted entirely (not set to null) when mainContentsDenorm is null —
187
+ // see the parameter doc above for why this preserves prior values on
188
+ // ON CONFLICT DO UPDATE instead of erasing them.
189
+ ...mainContentsDenorm,
177
190
  };
178
191
  for (const { source, target } of PAGE_META_COLUMN_MAPS.text) {
179
192
  const value = flat[source];
@@ -0,0 +1,14 @@
1
+ import type { MainContentsData } from '@d-zero/beholder';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Replaces the page's `page_main_content_videos` rows with the freshly
5
+ * captured set. Called inside `updatePage`'s transaction, gated on
6
+ * `mainContents !== null`.
7
+ *
8
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
9
+ * wipe prior rows on a degraded re-scrape.
10
+ * @param pageId - The owning `content_items.id`.
11
+ * @param mainContents - Beholder's per-page main-content metrics.
12
+ * @param trx - The active transaction.
13
+ */
14
+ export declare function insertVideos(pageId: number, mainContents: MainContentsData, trx: Knex.Transaction): Promise<void>;
@@ -0,0 +1,28 @@
1
+ import { eachSplitted } from '../../../../utils/array/each-splitted.js';
2
+ /**
3
+ * Replaces the page's `page_main_content_videos` rows with the freshly
4
+ * captured set. Called inside `updatePage`'s transaction, gated on
5
+ * `mainContents !== null`.
6
+ *
7
+ * Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does not
8
+ * wipe prior rows on a degraded re-scrape.
9
+ * @param pageId - The owning `content_items.id`.
10
+ * @param mainContents - Beholder's per-page main-content metrics.
11
+ * @param trx - The active transaction.
12
+ */
13
+ export async function insertVideos(pageId, mainContents, trx) {
14
+ if (mainContents.videos.length === 0)
15
+ return;
16
+ const rows = mainContents.videos.map((video, order) => ({
17
+ pageId,
18
+ order,
19
+ src: video.src,
20
+ poster: video.poster,
21
+ width: video.width,
22
+ height: video.height,
23
+ }));
24
+ await trx('page_main_content_videos').where('pageId', pageId).delete();
25
+ await eachSplitted(rows, 100, async (chunk) => {
26
+ await trx('page_main_content_videos').insert(chunk);
27
+ });
28
+ }
@@ -1,6 +1,7 @@
1
1
  import { tryParseUrl as parseUrl } from '@d-zero/shared/parse-url';
2
2
  import { isHtmlContentType } from '../../../../crawler/is-html-content-type.js';
3
3
  import { eachSplitted } from '../../../../utils/array/each-splitted.js';
4
+ import { computeBodyHash } from '../../../body-hash/compute-body-hash.js';
4
5
  import { dbLog } from '../../../debug.js';
5
6
  import { deriveLineageFromParent } from '../../../derive-lineage-from-parent.js';
6
7
  import { matchImagesToDomPaths } from '../../../populate-entity-tables/match-images-to-dom-paths.js';
@@ -9,9 +10,17 @@ import { resolveRedirectChain } from '../../../resolve-redirect-chain.js';
9
10
  import { clearWriteRefCaches } from '../../_shared/clear-write-ref-caches.js';
10
11
  import { resolveContentItemId } from '../../_shared/resolve-content-item-id.js';
11
12
  import { resolveUrlOrBlob } from '../../_shared/resolve-url-or-blob.js';
13
+ import { insertAudios } from './insert-audios.js';
14
+ import { insertButtons } from './insert-buttons.js';
15
+ import { insertCanvases } from './insert-canvases.js';
16
+ import { insertHeadings } from './insert-headings.js';
17
+ import { insertIframes } from './insert-iframes.js';
12
18
  import { insertJsonLd } from './insert-jsonld.js';
19
+ import { insertMainContentImages } from './insert-main-content-images.js';
20
+ import { insertMainContentTables } from './insert-main-content-tables.js';
13
21
  import { insertPage } from './insert-page.js';
14
22
  import { insertTags } from './insert-tags.js';
23
+ import { insertVideos } from './insert-videos.js';
15
24
  import { linkRedirectSources } from './link-redirect-sources.js';
16
25
  import { writePageHtmlBlob } from './write-page-html-blob.js';
17
26
  /**
@@ -92,6 +101,19 @@ async function updatePageInTransaction(trx, knex, caches, page, destUrlObject, s
92
101
  if (writeHtml) {
93
102
  await insertJsonLd(pageId, page.meta, trx);
94
103
  }
104
+ // beholder's MainContentsData is captured in the same render pass as the
105
+ // HTML body / JSON-LD, so it shares the same `writeHtml` + non-null gate:
106
+ // external / non-HTML / metadata-only scrapes never populate it.
107
+ if (writeHtml && page.mainContents) {
108
+ await insertHeadings(pageId, page.mainContents, trx);
109
+ await insertMainContentImages(pageId, page.mainContents, trx);
110
+ await insertMainContentTables(pageId, page.mainContents, trx);
111
+ await insertButtons(pageId, page.mainContents, trx);
112
+ await insertIframes(pageId, page.mainContents, trx);
113
+ await insertVideos(pageId, page.mainContents, trx);
114
+ await insertAudios(pageId, page.mainContents, trx);
115
+ await insertCanvases(pageId, page.mainContents, trx);
116
+ }
95
117
  // Chain lineage propagates FROM the originating URL
96
118
  // (`page.url`), NOT from the destination. See the matching
97
119
  // rationale in `recordRedirect`: intermediates are reached
@@ -120,6 +142,12 @@ async function updatePageInTransaction(trx, knex, caches, page, destUrlObject, s
120
142
  // content check alone expresses the intent without a redundant term.
121
143
  if (writeHtml && page.html.length > 0) {
122
144
  await writePageHtmlBlob(pageId, page.html, trx);
145
+ // Computed from the same `page.html` written above, in the same
146
+ // transaction, so `page_meta.body_hash` never observably lags the
147
+ // snapshot it was derived from.
148
+ await trx('page_meta')
149
+ .where('page_id', pageId)
150
+ .update({ body_hash: computeBodyHash(page.html) });
123
151
  }
124
152
  else if (writeHtml &&
125
153
  page.contentType !== null &&
@@ -134,7 +162,12 @@ async function updatePageInTransaction(trx, knex, caches, page, destUrlObject, s
134
162
  // stale ref can only have been written by a snapshot-capable call
135
163
  // (`setPage`); `setExternalPage` passes `writeHtml = false` and never
136
164
  // sets `html`, so it has nothing to clear.
165
+ //
166
+ // `page_meta.body_hash` is cleared alongside it for the same reason —
167
+ // it is derived from the same HTML `page_html_ref` points at, so it
168
+ // must not outlive the ref it was computed from.
137
169
  await trx('page_html_ref').where('page_id', pageId).delete();
170
+ await trx('page_meta').where('page_id', pageId).update({ body_hash: null });
138
171
  }
139
172
  // Re-scrape semantics: the same URL can be scraped more than once
140
173
  // (e.g. `crawl --resume`, re-visits, `--append` re-promotion). Edges
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { zstdCompressSync } from 'node:zlib';
2
+ import { compressPayload } from '../../_shared/compress-payload.js';
3
3
  /**
4
4
  * Encodes, dedups, and persists a page's HTML snapshot.
5
5
  *
@@ -20,14 +20,14 @@ import { zstdCompressSync } from 'node:zlib';
20
20
  export async function writePageHtmlBlob(pageId, html, trx) {
21
21
  const rawBytes = Buffer.from(html, 'utf8');
22
22
  const hash = createHash('sha256').update(rawBytes).digest();
23
- const compressed = zstdCompressSync(rawBytes);
23
+ const { body, codec, sizeRaw, sizeStored } = compressPayload(rawBytes);
24
24
  await trx('page_html_blobs')
25
25
  .insert({
26
26
  hash,
27
- body: compressed,
28
- codec: 'zstd',
29
- size_raw: rawBytes.byteLength,
30
- size_stored: compressed.byteLength,
27
+ body,
28
+ codec,
29
+ size_raw: sizeRaw,
30
+ size_stored: sizeStored,
31
31
  })
32
32
  .onConflict('hash')
33
33
  .ignore();
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Writes raw bytes to a file at the specified path, creating parent
3
+ * directories if needed.
4
+ *
5
+ * Unlike {@link outputText}, the buffer is written verbatim with no UTF-8
6
+ * re-encoding, so callers that need byte-for-byte fidelity (e.g. archiving
7
+ * a source file of unknown or mixed encoding for audit purposes) are not
8
+ * exposed to lossy round-tripping through a JS string.
9
+ * @param filePath - The absolute or relative path to the file to write.
10
+ * @param data - The raw bytes to write.
11
+ */
12
+ export declare function outputBinary(filePath: string, data: Buffer): Promise<void>;
@@ -0,0 +1,17 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { mkdir } from './mkdir.js';
3
+ /**
4
+ * Writes raw bytes to a file at the specified path, creating parent
5
+ * directories if needed.
6
+ *
7
+ * Unlike {@link outputText}, the buffer is written verbatim with no UTF-8
8
+ * re-encoding, so callers that need byte-for-byte fidelity (e.g. archiving
9
+ * a source file of unknown or mixed encoding for audit purposes) are not
10
+ * exposed to lossy round-tripping through a JS string.
11
+ * @param filePath - The absolute or relative path to the file to write.
12
+ * @param data - The raw bytes to write.
13
+ */
14
+ export async function outputBinary(filePath, data) {
15
+ mkdir(filePath);
16
+ await fs.writeFile(filePath, data);
17
+ }
@@ -1,10 +1,22 @@
1
1
  import type { Knex } from 'knex';
2
2
  /**
3
- * Bulk-resolve a raw error message for each given page id, using only sources
4
- * reachable from a {@link Knex} handle. Read order: `page_errors` (keyed by
5
- * `pageId`, the most direct signal a scrape attempt recorded), then
6
- * `crawl_errors` (keyed by `url`, the crawler-channel record for failures
7
- * that happened before a page row was scraped).
3
+ * One resolved failure message, with the timestamp it was recorded at.
4
+ * `createdAt` is what lets a caller (`resetFailedPages`) decide whether the
5
+ * failure falls inside a `network_outages` window and should be treated as
6
+ * retryable regardless of its classified `ErrorKind` see
7
+ * `is-within-outage-window.ts`.
8
+ */
9
+ export interface FailedPageMessage {
10
+ message: string;
11
+ /** Epoch ms the message was recorded (`page_errors.createdAt` or `crawl_errors.createdAt`). */
12
+ createdAt: number;
13
+ }
14
+ /**
15
+ * Bulk-resolve a raw error message (plus its timestamp) for each given page
16
+ * id, using only sources reachable from a {@link Knex} handle. Read order:
17
+ * `page_errors` (keyed by `pageId`, the most direct signal a scrape attempt
18
+ * recorded), then `crawl_errors` (keyed by `url`, the crawler-channel
19
+ * record for failures that happened before a page row was scraped).
8
20
  *
9
21
  * **Known limitation — pre-`crawl_errors` archives**: This helper does NOT
10
22
  * read `error.log`. The `crawl_errors` table is created empty (by
@@ -30,8 +42,8 @@ import type { Knex } from 'knex';
30
42
  * @param urls - The corresponding `pages.url` values, in the same order as
31
43
  * `ids`. Length and indexing MUST match `ids` so the page → url join can be
32
44
  * reconstructed without a second `pages` round-trip.
33
- * @returns `Map<pageId, message>` populated only for ids whose message was
34
- * found in one of the consulted tables.
45
+ * @returns `Map<pageId, FailedPageMessage>` populated only for ids whose
46
+ * message was found in one of the consulted tables.
35
47
  * @example
36
48
  * ```ts
37
49
  * const messages = await getFailedPageMessages(
@@ -41,4 +53,4 @@ import type { Knex } from 'knex';
41
53
  * );
42
54
  * ```
43
55
  */
44
- export declare function getFailedPageMessages(instance: Knex, ids: readonly number[], urls: readonly string[]): Promise<Map<number, string>>;
56
+ export declare function getFailedPageMessages(instance: Knex, ids: readonly number[], urls: readonly string[]): Promise<Map<number, FailedPageMessage>>;
@@ -1,11 +1,11 @@
1
1
  /** chunk size for `WHERE … IN (?)` SQLite parameter packing. */
2
2
  const CHUNK_SIZE = 500;
3
3
  /**
4
- * Bulk-resolve a raw error message for each given page id, using only sources
5
- * reachable from a {@link Knex} handle. Read order: `page_errors` (keyed by
6
- * `pageId`, the most direct signal a scrape attempt recorded), then
7
- * `crawl_errors` (keyed by `url`, the crawler-channel record for failures
8
- * that happened before a page row was scraped).
4
+ * Bulk-resolve a raw error message (plus its timestamp) for each given page
5
+ * id, using only sources reachable from a {@link Knex} handle. Read order:
6
+ * `page_errors` (keyed by `pageId`, the most direct signal a scrape attempt
7
+ * recorded), then `crawl_errors` (keyed by `url`, the crawler-channel
8
+ * record for failures that happened before a page row was scraped).
9
9
  *
10
10
  * **Known limitation — pre-`crawl_errors` archives**: This helper does NOT
11
11
  * read `error.log`. The `crawl_errors` table is created empty (by
@@ -31,8 +31,8 @@ const CHUNK_SIZE = 500;
31
31
  * @param urls - The corresponding `pages.url` values, in the same order as
32
32
  * `ids`. Length and indexing MUST match `ids` so the page → url join can be
33
33
  * reconstructed without a second `pages` round-trip.
34
- * @returns `Map<pageId, message>` populated only for ids whose message was
35
- * found in one of the consulted tables.
34
+ * @returns `Map<pageId, FailedPageMessage>` populated only for ids whose
35
+ * message was found in one of the consulted tables.
36
36
  * @example
37
37
  * ```ts
38
38
  * const messages = await getFailedPageMessages(
@@ -61,7 +61,7 @@ export async function getFailedPageMessages(instance, ids, urls) {
61
61
  // it as `parse-error` on the next run when the rows happen to be
62
62
  // returned in a different order.
63
63
  const rows = (await instance('page_errors')
64
- .select('pageId', 'message')
64
+ .select('pageId', 'message', 'createdAt')
65
65
  .whereIn('pageId', chunk)
66
66
  .orderBy('id', 'asc'));
67
67
  for (const row of rows) {
@@ -79,7 +79,10 @@ export async function getFailedPageMessages(instance, ids, urls) {
79
79
  // `dns` / `tls` / `client-blocked` etc. — defeating
80
80
  // `--retry-failed`'s permanent-kind exclusion.
81
81
  if (row.message !== '' && !messageByPageId.has(row.pageId)) {
82
- messageByPageId.set(row.pageId, row.message);
82
+ messageByPageId.set(row.pageId, {
83
+ message: row.message,
84
+ createdAt: row.createdAt,
85
+ });
83
86
  }
84
87
  }
85
88
  }
@@ -110,11 +113,22 @@ export async function getFailedPageMessages(instance, ids, urls) {
110
113
  for (let i = 0; i < missingUrls.length; i += CHUNK_SIZE) {
111
114
  const chunk = missingUrls.slice(i, i + CHUNK_SIZE);
112
115
  const rows = (await instance('crawl_errors')
113
- .select('url', 'message')
116
+ .select('url', 'message', 'createdAt')
114
117
  .whereIn('url', chunk));
115
118
  for (const row of rows) {
116
- if (row.url !== null && !urlToMessage.has(row.url)) {
117
- urlToMessage.set(row.url, row.message);
119
+ if (row.url === null) {
120
+ continue;
121
+ }
122
+ // Latest-createdAt wins (fixes a previously-undefined
123
+ // selection among duplicate URLs — SQLite's natural scan
124
+ // order is implementation-defined). The most recent message
125
+ // is the most relevant one for both classification and outage
126
+ // attribution: an old NXDOMAIN followed by a network-outage
127
+ // blip should resolve to the outage-era message, not whichever
128
+ // happened to be inserted first.
129
+ const existing = urlToMessage.get(row.url);
130
+ if (existing === undefined || row.createdAt > existing.createdAt) {
131
+ urlToMessage.set(row.url, { message: row.message, createdAt: row.createdAt });
118
132
  }
119
133
  }
120
134
  }
@@ -123,9 +137,9 @@ export async function getFailedPageMessages(instance, ids, urls) {
123
137
  if (url === undefined) {
124
138
  continue;
125
139
  }
126
- const message = urlToMessage.get(url);
127
- if (message !== undefined) {
128
- messageByPageId.set(id, message);
140
+ const resolved = urlToMessage.get(url);
141
+ if (resolved !== undefined) {
142
+ messageByPageId.set(id, resolved);
129
143
  }
130
144
  }
131
145
  return messageByPageId;
@@ -14,7 +14,7 @@ import { createRefTables } from './create-ref-tables.js';
14
14
  export async function applyConnectionPragmas(instance) {
15
15
  // Foreign-key enforcement defaults to OFF on every new SQLite
16
16
  // connection. Required for ON DELETE CASCADE on `page_html_ref`,
17
- // `page_tags`, and `page_jsonld` to fire.
17
+ // `page_tags`, `page_jsonld`, and the `page_main_content_*` tables to fire.
18
18
  await instance.raw('PRAGMA foreign_keys = ON');
19
19
  await instance.raw('PRAGMA wal_autocheckpoint = 1000');
20
20
  // Negative value = KiB of memory (64 MiB). Helps large BLOB scans.
@@ -111,6 +111,7 @@ export async function initSchema(instance) {
111
111
  t.boolean('disableQueries');
112
112
  t.string('userAgent');
113
113
  t.boolean('ignoreRobots');
114
+ t.string('mainContentSelector');
114
115
  });
115
116
  }
116
117
  // 0.13 ref / header dictionary tables. DDL + column-level rationale