@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,15 @@
1
+ /**
2
+ * Retrieves all `page_main_content_images` rows for the given page id, in
3
+ * DOM traversal order.
4
+ *
5
+ * Read-side counterpart to `insertMainContentImages`.
6
+ * @param knex - Knex query builder connected to the archive DB.
7
+ * @param pageId
8
+ */
9
+ export async function getMainContentImagesOfPage(knex, pageId) {
10
+ return knex
11
+ .select('id', 'pageId', 'order', 'src', 'alt')
12
+ .from('page_main_content_images')
13
+ .where('pageId', pageId)
14
+ .orderBy('order', 'asc');
15
+ }
@@ -0,0 +1,11 @@
1
+ import type { MainContentTableRow } from '../../meta/types.js';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Retrieves all `page_main_content_tables` rows for the given page id, in
5
+ * DOM traversal order.
6
+ *
7
+ * Read-side counterpart to `insertMainContentTables`.
8
+ * @param knex - Knex query builder connected to the archive DB.
9
+ * @param pageId
10
+ */
11
+ export declare function getMainContentTablesOfPage(knex: Knex, pageId: number): Promise<MainContentTableRow[]>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Retrieves all `page_main_content_tables` rows for the given page id, in
3
+ * DOM traversal order.
4
+ *
5
+ * Read-side counterpart to `insertMainContentTables`.
6
+ * @param knex - Knex query builder connected to the archive DB.
7
+ * @param pageId
8
+ */
9
+ export async function getMainContentTablesOfPage(knex, pageId) {
10
+ return knex
11
+ .select('id', 'pageId', 'order', 'rows', 'cols', 'hasHeader', 'hasFooter', 'hasMergedCell')
12
+ .from('page_main_content_tables')
13
+ .where('pageId', pageId)
14
+ .orderBy('order', 'asc');
15
+ }
@@ -0,0 +1,11 @@
1
+ import type { MainContentVideoRow } from '../../meta/types.js';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Retrieves all `page_main_content_videos` rows for the given page id, in
5
+ * DOM traversal order.
6
+ *
7
+ * Read-side counterpart to `insertVideos`.
8
+ * @param knex - Knex query builder connected to the archive DB.
9
+ * @param pageId
10
+ */
11
+ export declare function getVideosOfPage(knex: Knex, pageId: number): Promise<MainContentVideoRow[]>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Retrieves all `page_main_content_videos` rows for the given page id, in
3
+ * DOM traversal order.
4
+ *
5
+ * Read-side counterpart to `insertVideos`.
6
+ * @param knex - Knex query builder connected to the archive DB.
7
+ * @param pageId
8
+ */
9
+ export async function getVideosOfPage(knex, pageId) {
10
+ return knex
11
+ .select('id', 'pageId', 'order', 'src', 'poster', 'width', 'height')
12
+ .from('page_main_content_videos')
13
+ .where('pageId', pageId)
14
+ .orderBy('order', 'asc');
15
+ }
@@ -0,0 +1,12 @@
1
+ import type { Knex } from 'knex';
2
+ /**
3
+ * Close an outage row by stamping `ended_at` — but ONLY if it is still
4
+ * open. The `whereNull('ended_at')` guard is what makes this idempotent: a
5
+ * second call (e.g. a duplicate recovery-probe success racing the first)
6
+ * matches zero rows and is a silent no-op, rather than overwriting an
7
+ * already-recorded `ended_at` with a later timestamp.
8
+ * @param knex - Knex query builder connected to the archive DB.
9
+ * @param id - The `network_outages.id` to close.
10
+ * @param endedAt - Epoch ms the outage is considered to have ended.
11
+ */
12
+ export declare function closeNetworkOutage(knex: Knex, id: number, endedAt: number): Promise<void>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Close an outage row by stamping `ended_at` — but ONLY if it is still
3
+ * open. The `whereNull('ended_at')` guard is what makes this idempotent: a
4
+ * second call (e.g. a duplicate recovery-probe success racing the first)
5
+ * matches zero rows and is a silent no-op, rather than overwriting an
6
+ * already-recorded `ended_at` with a later timestamp.
7
+ * @param knex - Knex query builder connected to the archive DB.
8
+ * @param id - The `network_outages.id` to close.
9
+ * @param endedAt - Epoch ms the outage is considered to have ended.
10
+ */
11
+ export async function closeNetworkOutage(knex, id, endedAt) {
12
+ await knex('network_outages').where({ id }).whereNull('ended_at').update({
13
+ ended_at: endedAt,
14
+ });
15
+ }
@@ -0,0 +1,26 @@
1
+ import type { Knex } from 'knex';
2
+ /**
3
+ * Durably close every `network_outages` row still `ended_at = NULL` at the
4
+ * start of a writer session.
5
+ *
6
+ * A row is left open only when the crawl process was killed (Ctrl-C / OOM
7
+ * / SIGKILL) mid-outage, before a recovery probe could close it. Called
8
+ * once from `db-ops/lifecycle/init.ts` — right after `initSchema` (which
9
+ * guarantees the table exists) and before anything else touches
10
+ * `network_outages` — so that by the time `resetFailedPages` /
11
+ * `listDnsBurnedHostCandidates` / any other reader runs, no row can still
12
+ * be open from a PRIOR session. (A row opened by THIS session cannot exist
13
+ * yet at this point in the boot sequence — the sliding-window detector
14
+ * only starts once the crawl loop begins.)
15
+ *
16
+ * This complements, but does not replace, `list-network-outages.ts`'s
17
+ * on-the-fly clamp: that read-side resolution stays in place as a
18
+ * defensive fallback, but after this runs there should be nothing left for
19
+ * it to resolve.
20
+ *
21
+ * Idempotent: a row closed by a previous call (or by a normal
22
+ * recovery-probe success) is simply absent from the `whereNull('ended_at')`
23
+ * scan, so re-running this on every writer open is always safe.
24
+ * @param knex - Knex query builder connected to the archive DB.
25
+ */
26
+ export declare function closeStaleOpenNetworkOutages(knex: Knex): Promise<void>;
@@ -0,0 +1,42 @@
1
+ import { closeNetworkOutage } from './close-network-outage.js';
2
+ import { computeOutageClampTimestamp } from './compute-outage-clamp-timestamp.js';
3
+ /**
4
+ * Durably close every `network_outages` row still `ended_at = NULL` at the
5
+ * start of a writer session.
6
+ *
7
+ * A row is left open only when the crawl process was killed (Ctrl-C / OOM
8
+ * / SIGKILL) mid-outage, before a recovery probe could close it. Called
9
+ * once from `db-ops/lifecycle/init.ts` — right after `initSchema` (which
10
+ * guarantees the table exists) and before anything else touches
11
+ * `network_outages` — so that by the time `resetFailedPages` /
12
+ * `listDnsBurnedHostCandidates` / any other reader runs, no row can still
13
+ * be open from a PRIOR session. (A row opened by THIS session cannot exist
14
+ * yet at this point in the boot sequence — the sliding-window detector
15
+ * only starts once the crawl loop begins.)
16
+ *
17
+ * This complements, but does not replace, `list-network-outages.ts`'s
18
+ * on-the-fly clamp: that read-side resolution stays in place as a
19
+ * defensive fallback, but after this runs there should be nothing left for
20
+ * it to resolve.
21
+ *
22
+ * Idempotent: a row closed by a previous call (or by a normal
23
+ * recovery-probe success) is simply absent from the `whereNull('ended_at')`
24
+ * scan, so re-running this on every writer open is always safe.
25
+ * @param knex - Knex query builder connected to the archive DB.
26
+ */
27
+ export async function closeStaleOpenNetworkOutages(knex) {
28
+ const hasTable = await knex.schema.hasTable('network_outages');
29
+ if (!hasTable) {
30
+ return;
31
+ }
32
+ const openRows = (await knex('network_outages')
33
+ .whereNull('ended_at')
34
+ .select('id', 'started_at'));
35
+ if (openRows.length === 0) {
36
+ return;
37
+ }
38
+ const clamp = await computeOutageClampTimestamp(knex);
39
+ for (const row of openRows) {
40
+ await closeNetworkOutage(knex, row.id, Math.max(clamp, row.started_at));
41
+ }
42
+ }
@@ -0,0 +1,26 @@
1
+ import type { Knex } from 'knex';
2
+ /**
3
+ * Compute the timestamp used to resolve an outage row whose `ended_at` is
4
+ * still `NULL` (the crawl process was killed before a recovery probe could
5
+ * close it) into a concrete, bounded window.
6
+ *
7
+ * The archive's own most-recent observations — the latest `crawl_errors`
8
+ * timestamp and the latest `content_items.last_crawled_at` — are the only
9
+ * evidence available for "when did activity in this archive last happen",
10
+ * so the later of the two stands in for "the outage cannot have lasted
11
+ * past this point, because the archive shows activity here". Using the
12
+ * LARGER of the two (not just `crawl_errors`) matters because a session
13
+ * that crashed mid-outage may have its last successful page write be more
14
+ * recent than its last recorded error, or vice versa.
15
+ *
16
+ * Both source tables are guaranteed to exist by the time this runs — this
17
+ * helper is only ever called from writer-context code (see
18
+ * `list-network-outages.ts` / the boot-time stale-open finalizer), and
19
+ * `initSchema` creates both `crawl_errors` and `content_items` before any
20
+ * writer session's first query.
21
+ * @param knex - Knex query builder connected to the archive DB.
22
+ * @returns The larger of `MAX(crawl_errors.createdAt)` and
23
+ * `MAX(content_items.last_crawled_at)`, or `0` if the archive has neither
24
+ * (a brand-new archive with no activity yet).
25
+ */
26
+ export declare function computeOutageClampTimestamp(knex: Knex): Promise<number>;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Compute the timestamp used to resolve an outage row whose `ended_at` is
3
+ * still `NULL` (the crawl process was killed before a recovery probe could
4
+ * close it) into a concrete, bounded window.
5
+ *
6
+ * The archive's own most-recent observations — the latest `crawl_errors`
7
+ * timestamp and the latest `content_items.last_crawled_at` — are the only
8
+ * evidence available for "when did activity in this archive last happen",
9
+ * so the later of the two stands in for "the outage cannot have lasted
10
+ * past this point, because the archive shows activity here". Using the
11
+ * LARGER of the two (not just `crawl_errors`) matters because a session
12
+ * that crashed mid-outage may have its last successful page write be more
13
+ * recent than its last recorded error, or vice versa.
14
+ *
15
+ * Both source tables are guaranteed to exist by the time this runs — this
16
+ * helper is only ever called from writer-context code (see
17
+ * `list-network-outages.ts` / the boot-time stale-open finalizer), and
18
+ * `initSchema` creates both `crawl_errors` and `content_items` before any
19
+ * writer session's first query.
20
+ * @param knex - Knex query builder connected to the archive DB.
21
+ * @returns The larger of `MAX(crawl_errors.createdAt)` and
22
+ * `MAX(content_items.last_crawled_at)`, or `0` if the archive has neither
23
+ * (a brand-new archive with no activity yet).
24
+ */
25
+ export async function computeOutageClampTimestamp(knex) {
26
+ const [crawlErrorsRow] = (await knex('crawl_errors').max('createdAt as maxCreatedAt'));
27
+ const [contentItemsRow] = (await knex('content_items').max('last_crawled_at as maxLastCrawledAt'));
28
+ const latestError = crawlErrorsRow?.maxCreatedAt ?? 0;
29
+ const latestCrawl = contentItemsRow?.maxLastCrawledAt ?? 0;
30
+ return Math.max(latestError, latestCrawl);
31
+ }
@@ -0,0 +1,14 @@
1
+ import type { InsertNetworkOutageParams } from '../../types.js';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Appends one row to the `network_outages` journal, with `ended_at` left
5
+ * `NULL` — the row starts life as an open outage.
6
+ *
7
+ * Called the moment a recovery probe CONFIRMS a suspect outage (i.e. the
8
+ * probe itself failed, not merely the sliding-window threshold trip). See
9
+ * `close-network-outage.ts` for how the row is later closed.
10
+ * @param knex - Knex query builder connected to the archive DB.
11
+ * @param params - The confirmed-outage fields to record.
12
+ * @returns The autoincremented `id` of the newly-inserted row.
13
+ */
14
+ export declare function insertNetworkOutage(knex: Knex, params: InsertNetworkOutageParams): Promise<number>;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Appends one row to the `network_outages` journal, with `ended_at` left
3
+ * `NULL` — the row starts life as an open outage.
4
+ *
5
+ * Called the moment a recovery probe CONFIRMS a suspect outage (i.e. the
6
+ * probe itself failed, not merely the sliding-window threshold trip). See
7
+ * `close-network-outage.ts` for how the row is later closed.
8
+ * @param knex - Knex query builder connected to the archive DB.
9
+ * @param params - The confirmed-outage fields to record.
10
+ * @returns The autoincremented `id` of the newly-inserted row.
11
+ */
12
+ export async function insertNetworkOutage(knex, params) {
13
+ const inserted = await knex
14
+ .from('network_outages')
15
+ .insert({
16
+ started_at: params.startedAt,
17
+ detected_at: params.detectedAt,
18
+ ended_at: null,
19
+ probe_host: params.probeHost,
20
+ trigger_error_count: params.triggerErrorCount,
21
+ trigger_host_count: params.triggerHostCount,
22
+ })
23
+ .returning('id');
24
+ const id = inserted[0]?.id;
25
+ if (typeof id !== 'number') {
26
+ throw new TypeError('insertNetworkOutage: INSERT returned no row id');
27
+ }
28
+ return id;
29
+ }
@@ -0,0 +1,28 @@
1
+ import type { OutageWindow } from '../../../is-within-outage-window.js';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * List every recorded outage as a resolved (closed) {@link OutageWindow},
5
+ * suitable for `isWithinOutageWindow`.
6
+ *
7
+ * This is the crawler-internal counterpart to `@nitpicker/query`'s richer
8
+ * `listNetworkOutages` (full row shape, for CLI/MCP display) — this one
9
+ * exists purely to feed the three write-path consumers
10
+ * (`resetFailedPages`, `listDnsBurnedHostCandidates`, the gate itself) that
11
+ * only need "was this timestamp inside an outage", never the row's other
12
+ * columns.
13
+ *
14
+ * Any row whose `ended_at` is still `NULL` (the crawl process was killed
15
+ * before recovery) is resolved on the fly via
16
+ * {@link computeOutageClampTimestamp} rather than ever being returned as an
17
+ * unbounded window — see `is-within-outage-window.ts`'s `OutageWindow`
18
+ * docstring for why an open-ended window would be a correctness bug (every
19
+ * later error would retroactively read as network-caused, forever). This
20
+ * on-the-fly resolution does NOT persist to the row — it is a defensive
21
+ * fallback independent of whichever boot-time finalizer durably closes
22
+ * stale-open rows.
23
+ * @param knex - Knex query builder connected to the archive DB.
24
+ * @returns Resolved outage windows. Empty when the archive predates the
25
+ * `network_outages` table (self-healed on next writer open, so this is
26
+ * never a permanent state) or has recorded no outages.
27
+ */
28
+ export declare function listNetworkOutages(knex: Knex): Promise<OutageWindow[]>;
@@ -0,0 +1,42 @@
1
+ import { computeOutageClampTimestamp } from './compute-outage-clamp-timestamp.js';
2
+ /**
3
+ * List every recorded outage as a resolved (closed) {@link OutageWindow},
4
+ * suitable for `isWithinOutageWindow`.
5
+ *
6
+ * This is the crawler-internal counterpart to `@nitpicker/query`'s richer
7
+ * `listNetworkOutages` (full row shape, for CLI/MCP display) — this one
8
+ * exists purely to feed the three write-path consumers
9
+ * (`resetFailedPages`, `listDnsBurnedHostCandidates`, the gate itself) that
10
+ * only need "was this timestamp inside an outage", never the row's other
11
+ * columns.
12
+ *
13
+ * Any row whose `ended_at` is still `NULL` (the crawl process was killed
14
+ * before recovery) is resolved on the fly via
15
+ * {@link computeOutageClampTimestamp} rather than ever being returned as an
16
+ * unbounded window — see `is-within-outage-window.ts`'s `OutageWindow`
17
+ * docstring for why an open-ended window would be a correctness bug (every
18
+ * later error would retroactively read as network-caused, forever). This
19
+ * on-the-fly resolution does NOT persist to the row — it is a defensive
20
+ * fallback independent of whichever boot-time finalizer durably closes
21
+ * stale-open rows.
22
+ * @param knex - Knex query builder connected to the archive DB.
23
+ * @returns Resolved outage windows. Empty when the archive predates the
24
+ * `network_outages` table (self-healed on next writer open, so this is
25
+ * never a permanent state) or has recorded no outages.
26
+ */
27
+ export async function listNetworkOutages(knex) {
28
+ const hasTable = await knex.schema.hasTable('network_outages');
29
+ if (!hasTable) {
30
+ return [];
31
+ }
32
+ const rows = (await knex('network_outages').select('started_at', 'ended_at'));
33
+ if (rows.length === 0) {
34
+ return [];
35
+ }
36
+ const hasOpenRow = rows.some((row) => row.ended_at === null);
37
+ const clamp = hasOpenRow ? await computeOutageClampTimestamp(knex) : 0;
38
+ return rows.map((row) => ({
39
+ startedAt: row.started_at,
40
+ endedAt: row.ended_at ?? Math.max(clamp, row.started_at),
41
+ }));
42
+ }
@@ -36,5 +36,5 @@ export function buildPageQuery(knex) {
36
36
  .leftJoin('url_refs as og_image_ur', 'og_image_ur.id', 'pm.og_image_url_id')
37
37
  .leftJoin('url_refs as twitter_image_ur', 'twitter_image_ur.id', 'pm.twitter_image_url_id')
38
38
  .leftJoin('json_refs as extras_ref', 'extras_ref.id', 'pm.meta_extras_json_id')
39
- .select('ci.id as id', 'ur.url as url', 'ci.redirect_dest_id as redirectDestId', 'ci.scraped as scraped', 'ci.is_target as isTarget', 'ci.is_external as isExternal', 'ci.status as status', 'ci.status_text as statusText', 'ctr.raw as contentType', 'ci.content_length as contentLength', 'ci.header_set_id as headerSetId', 'pm.lang as lang', 'pm.dir as dir', 'pm.charset as charset', 'pm.base_href as baseHref', 'pm.viewport_raw as viewport_raw', 'pm.theme_color as themeColor', 'pm.application_name as applicationName', 'pm.author as author', 'pm.generator as generator', 'pm.publisher as publisher', 'title_ref.text as title', 'description_ref.text as description', 'keywords_ref.text as keywords', 'robots_raw_ref.text as robots_raw', 'pm.robots_noindex as robots_noindex', 'pm.robots_nofollow as robots_nofollow', 'pm.robots_noarchive as robots_noarchive', 'pm.robots_noimageindex as robots_noimageindex', 'pm.googlebot as googlebot', 'canonical_ur.url as canonical', 'amphtml_ur.url as amphtml', 'manifest_ur.url as manifest', 'icon_ur.url as icon_href', 'apple_ur.url as appleTouchIcon_href', 'pm.og_type as og_type', 'og_title_ref.text as og_title', 'og_url_ur.url as og_url', 'pm.og_site_name as og_site_name', 'og_description_ref.text as og_description', 'og_image_ur.url as og_image', 'pm.og_image_alt as og_image_alt', 'pm.og_image_width as og_image_width', 'pm.og_image_height as og_image_height', 'pm.og_locale as og_locale', 'pm.og_article_published_time as og_article_published_time', 'pm.og_article_modified_time as og_article_modified_time', 'pm.twitter_card as twitter_card', 'pm.twitter_site as twitter_site', 'pm.twitter_creator as twitter_creator', 'twitter_title_ref.text as twitter_title', 'twitter_description_ref.text as twitter_description', 'twitter_image_ur.url as twitter_image', 'pm.fb_app_id as fb_app_id', 'pm.verification_google as verification_google', 'pm.format_detection_telephone as formatDetection_telephone', 'ci.first_crawled_at as firstCrawledAt', 'ci.last_crawled_at as lastCrawledAt', 'pm.tag_count as tag_count', 'pm.jsonld_count as jsonld_count', 'pm.tags_providers_csv as tags_providers_csv', 'extras_ref.json_text as extras_body', 'extras_ref.codec as extras_codec', 'ci.is_skipped as isSkipped', 'ci.skip_reason as skipReason', 'ci.crawl_order as order', 'ci.source as source');
39
+ .select('ci.id as id', 'ur.url as url', 'ci.redirect_dest_id as redirectDestId', 'ci.scraped as scraped', 'ci.is_target as isTarget', 'ci.is_external as isExternal', 'ci.status as status', 'ci.status_text as statusText', 'ctr.raw as contentType', 'ci.content_length as contentLength', 'ci.header_set_id as headerSetId', 'pm.lang as lang', 'pm.dir as dir', 'pm.charset as charset', 'pm.base_href as baseHref', 'pm.viewport_raw as viewport_raw', 'pm.theme_color as themeColor', 'pm.application_name as applicationName', 'pm.author as author', 'pm.generator as generator', 'pm.publisher as publisher', 'title_ref.text as title', 'description_ref.text as description', 'keywords_ref.text as keywords', 'robots_raw_ref.text as robots_raw', 'pm.robots_noindex as robots_noindex', 'pm.robots_nofollow as robots_nofollow', 'pm.robots_noarchive as robots_noarchive', 'pm.robots_noimageindex as robots_noimageindex', 'pm.googlebot as googlebot', 'canonical_ur.url as canonical', 'amphtml_ur.url as amphtml', 'manifest_ur.url as manifest', 'icon_ur.url as icon_href', 'apple_ur.url as appleTouchIcon_href', 'pm.og_type as og_type', 'og_title_ref.text as og_title', 'og_url_ur.url as og_url', 'pm.og_site_name as og_site_name', 'og_description_ref.text as og_description', 'og_image_ur.url as og_image', 'pm.og_image_alt as og_image_alt', 'pm.og_image_width as og_image_width', 'pm.og_image_height as og_image_height', 'pm.og_locale as og_locale', 'pm.og_article_published_time as og_article_published_time', 'pm.og_article_modified_time as og_article_modified_time', 'pm.twitter_card as twitter_card', 'pm.twitter_site as twitter_site', 'pm.twitter_creator as twitter_creator', 'twitter_title_ref.text as twitter_title', 'twitter_description_ref.text as twitter_description', 'twitter_image_ur.url as twitter_image', 'pm.fb_app_id as fb_app_id', 'pm.verification_google as verification_google', 'pm.format_detection_telephone as formatDetection_telephone', 'ci.first_crawled_at as firstCrawledAt', 'ci.last_crawled_at as lastCrawledAt', 'pm.tag_count as tag_count', 'pm.jsonld_count as jsonld_count', 'pm.tags_providers_csv as tags_providers_csv', 'pm.main_content_node_name as main_content_node_name', 'pm.main_content_id as main_content_id', 'pm.main_content_role as main_content_role', 'pm.main_content_selector as main_content_selector', 'pm.main_content_class_list as main_content_class_list', 'pm.main_content_word_count as main_content_word_count', 'pm.main_content_body_word_count as main_content_body_word_count', 'pm.main_content_heading_count as main_content_heading_count', 'pm.main_content_image_count as main_content_image_count', 'pm.main_content_table_count as main_content_table_count', 'pm.main_content_button_count as main_content_button_count', 'pm.main_content_iframe_count as main_content_iframe_count', 'pm.main_content_video_count as main_content_video_count', 'pm.main_content_audio_count as main_content_audio_count', 'pm.main_content_canvas_count as main_content_canvas_count', 'pm.scroll_height_desktop as scroll_height_desktop', 'pm.scroll_height_mobile as scroll_height_mobile', 'extras_ref.json_text as extras_body', 'extras_ref.codec as extras_codec', 'ci.is_skipped as isSkipped', 'ci.skip_reason as skipReason', 'ci.crawl_order as order', 'ci.source as source');
40
40
  }
@@ -65,19 +65,20 @@ export async function repromoteExternalPages(knex, scopes, options) {
65
65
  // demotion.
66
66
  });
67
67
  // Clear the prior crawl's data for the repromoted pages. `updatePage`
68
- // also replaces anchor_edges/image_items/tags/jsonld when it
69
- // re-scrapes them, but only when the new scrape is non-empty — so
70
- // this pre-clear is still load-bearing for pages that get
68
+ // also replaces anchor_edges/image_items/tags/jsonld/page_main_content_*
69
+ // when it re-scrapes them, but only when the new scrape is non-empty —
70
+ // so this pre-clear is still load-bearing for pages that get
71
71
  // repromoted but then re-scrape to nothing (or are never reached
72
72
  // again), and it is the only place `resource_ref_edges` is cleared.
73
73
  // Deleting the `page_meta` row (rather than nulling every column)
74
- // clears title / description / og:* / twitter:* / meta_extras in
75
- // one statement; a re-scrape re-inserts it via
76
- // `ON CONFLICT(page_id) DO UPDATE`. `page_tags` / `page_jsonld` are
77
- // cleared explicitly even though both tables also carry ON DELETE
78
- // CASCADE — we keep the existing pattern of explicit chunked
79
- // DELETEs rather than relying on CASCADE indirectly (and would not
80
- // cascade anyway: the parent `content_items` row is updated, not
74
+ // clears title / description / og:* / twitter:* / meta_extras /
75
+ // main_content_* in one statement; a re-scrape re-inserts it via
76
+ // `ON CONFLICT(page_id) DO UPDATE`. `page_tags` / `page_jsonld` /
77
+ // `page_main_content_*` are cleared explicitly even though all of
78
+ // them also carry ON DELETE CASCADE — we keep the existing pattern
79
+ // of explicit chunked DELETEs rather than relying on CASCADE
80
+ // indirectly (and would not cascade anyway: the parent
81
+ // `content_items` row is updated, not
81
82
  // deleted). Orphan blobs in `page_html_blobs` are left behind; #23
82
83
  // will add GC.
83
84
  await knex('page_meta').whereIn('page_id', chunk).delete();
@@ -87,6 +88,14 @@ export async function repromoteExternalPages(knex, scopes, options) {
87
88
  await knex('page_html_ref').whereIn('page_id', chunk).delete();
88
89
  await knex('page_tags').whereIn('pageId', chunk).delete();
89
90
  await knex('page_jsonld').whereIn('pageId', chunk).delete();
91
+ await knex('page_main_content_headings').whereIn('pageId', chunk).delete();
92
+ await knex('page_main_content_images').whereIn('pageId', chunk).delete();
93
+ await knex('page_main_content_tables').whereIn('pageId', chunk).delete();
94
+ await knex('page_main_content_buttons').whereIn('pageId', chunk).delete();
95
+ await knex('page_main_content_iframes').whereIn('pageId', chunk).delete();
96
+ await knex('page_main_content_videos').whereIn('pageId', chunk).delete();
97
+ await knex('page_main_content_audios').whereIn('pageId', chunk).delete();
98
+ await knex('page_main_content_canvases').whereIn('pageId', chunk).delete();
90
99
  }
91
100
  dbLog('Repromoted %d external pages back to pending', promotedUrls.length);
92
101
  return promotedUrls;
@@ -29,15 +29,30 @@ import type { Knex } from 'knex';
29
29
  * target shrinking across `--retry-failed` passes by leaving deterministic
30
30
  * dead-ends alone.
31
31
  *
32
+ * **Outage override**: before applying the permanent-kind exclusion, the
33
+ * message's `createdAt` is checked against every recorded
34
+ * `network_outages` window (see `is-within-outage-window.ts`). A `dns` (or
35
+ * any other permanent-kind) failure whose timestamp falls inside a window
36
+ * is treated as retryable regardless — `dns` is only a permanent,
37
+ * site-specific verdict when nothing else explains it; inside a confirmed
38
+ * operator-network outage, the same `getaddrinfo ENOTFOUND` message is
39
+ * evidence about the CRAWLER's connectivity, not the target site, and
40
+ * excluding it from retry would strand a perfectly reachable host as a
41
+ * false permanent failure for the rest of the archive's life. An archive
42
+ * with no recorded outages (`listNetworkOutages` returns `[]`) behaves
43
+ * exactly as before this override existed.
44
+ *
32
45
  * Matching rows — internal and external alike — are demoted back to pending
33
46
  * (`scraped = 0`) and have their stale scrape metadata cleared (the
34
47
  * `page_meta` row is deleted outright rather than nulled column-by-column).
35
48
  * The page row itself is kept (id preserved) so existing
36
49
  * `anchor_edges.href_page_id` referrers stay valid, and `is_external` is
37
50
  * left untouched so the next pass re-classifies each page from the crawl
38
- * scope. Related `anchor_edges`, `image_items`, `resource_ref_edges`, and
39
- * `page_errors` rows are deleted so the re-scrape can re-insert fresh data
40
- * without duplicates.
51
+ * scope. Related `anchor_edges`, `image_items`, `resource_ref_edges`,
52
+ * `page_errors`, and the `page_main_content_*` child tables are deleted so
53
+ * the re-scrape can re-insert fresh data without duplicates — kept in sync
54
+ * with the `page_meta` row deletion above so a reset page's `main_content_*`
55
+ * counts and its child-table detail never disagree.
41
56
  *
42
57
  * SELECT and UPDATE/DELETE statements are chunked to stay below SQLite's
43
58
  * `SQLITE_LIMIT_VARIABLE_NUMBER`.
@@ -1,7 +1,9 @@
1
1
  import { classifyErrorKind } from '../../../../classify-error-kind.js';
2
+ import { isWithinOutageWindow } from '../../../../is-within-outage-window.js';
2
3
  import { PERMANENT_ERROR_KINDS } from '../../../../permanent-error-kinds.js';
3
4
  import { dbLog } from '../../../debug.js';
4
5
  import { getFailedPageMessages } from '../../../get-failed-page-messages.js';
6
+ import { listNetworkOutages } from '../../outages/list-network-outages.js';
5
7
  /**
6
8
  * Reset previously-attempted pages that ended in a recoverable failure so a
7
9
  * follow-up crawl can re-fetch them from scratch.
@@ -32,15 +34,30 @@ import { getFailedPageMessages } from '../../../get-failed-page-messages.js';
32
34
  * target shrinking across `--retry-failed` passes by leaving deterministic
33
35
  * dead-ends alone.
34
36
  *
37
+ * **Outage override**: before applying the permanent-kind exclusion, the
38
+ * message's `createdAt` is checked against every recorded
39
+ * `network_outages` window (see `is-within-outage-window.ts`). A `dns` (or
40
+ * any other permanent-kind) failure whose timestamp falls inside a window
41
+ * is treated as retryable regardless — `dns` is only a permanent,
42
+ * site-specific verdict when nothing else explains it; inside a confirmed
43
+ * operator-network outage, the same `getaddrinfo ENOTFOUND` message is
44
+ * evidence about the CRAWLER's connectivity, not the target site, and
45
+ * excluding it from retry would strand a perfectly reachable host as a
46
+ * false permanent failure for the rest of the archive's life. An archive
47
+ * with no recorded outages (`listNetworkOutages` returns `[]`) behaves
48
+ * exactly as before this override existed.
49
+ *
35
50
  * Matching rows — internal and external alike — are demoted back to pending
36
51
  * (`scraped = 0`) and have their stale scrape metadata cleared (the
37
52
  * `page_meta` row is deleted outright rather than nulled column-by-column).
38
53
  * The page row itself is kept (id preserved) so existing
39
54
  * `anchor_edges.href_page_id` referrers stay valid, and `is_external` is
40
55
  * left untouched so the next pass re-classifies each page from the crawl
41
- * scope. Related `anchor_edges`, `image_items`, `resource_ref_edges`, and
42
- * `page_errors` rows are deleted so the re-scrape can re-insert fresh data
43
- * without duplicates.
56
+ * scope. Related `anchor_edges`, `image_items`, `resource_ref_edges`,
57
+ * `page_errors`, and the `page_main_content_*` child tables are deleted so
58
+ * the re-scrape can re-insert fresh data without duplicates — kept in sync
59
+ * with the `page_meta` row deletion above so a reset page's `main_content_*`
60
+ * counts and its child-table detail never disagree.
44
61
  *
45
62
  * SELECT and UPDATE/DELETE statements are chunked to stay below SQLite's
46
63
  * `SQLITE_LIMIT_VARIABLE_NUMBER`.
@@ -67,16 +84,29 @@ export async function resetFailedPages(knex) {
67
84
  }
68
85
  const candidateIds = candidates.map((row) => row.id);
69
86
  const candidateUrls = candidates.map((row) => row.url);
70
- const messages = await getFailedPageMessages(knex, candidateIds, candidateUrls);
71
- // Drop candidates whose latest recorded message classifies as permanent.
72
- // An empty/absent message stays in the retry pool — we keep retrying when
73
- // we don't know it's permanent, erring on the side of investigation.
87
+ // Unrelated tables (page_errors/crawl_errors vs network_outages), no data
88
+ // dependency between them run concurrently instead of paying two
89
+ // sequential round-trips on every `--retry-failed` pass.
90
+ const [messages, outageWindows] = await Promise.all([
91
+ getFailedPageMessages(knex, candidateIds, candidateUrls),
92
+ listNetworkOutages(knex),
93
+ ]);
94
+ // Drop candidates whose latest recorded message classifies as permanent —
95
+ // UNLESS that message's timestamp falls inside a recorded network outage,
96
+ // in which case the permanent-kind verdict is overridden (see the
97
+ // "Outage override" section of this function's docstring). An
98
+ // empty/absent message stays in the retry pool regardless — we keep
99
+ // retrying when we don't know it's permanent, erring on the side of
100
+ // investigation.
74
101
  const retryable = candidates.filter((row) => {
75
- const message = messages.get(row.id) ?? '';
76
- if (message === '') {
102
+ const resolved = messages.get(row.id);
103
+ if (resolved === undefined || resolved.message === '') {
104
+ return true;
105
+ }
106
+ if (!PERMANENT_ERROR_KINDS.has(classifyErrorKind(resolved.message))) {
77
107
  return true;
78
108
  }
79
- return !PERMANENT_ERROR_KINDS.has(classifyErrorKind(message));
109
+ return isWithinOutageWindow(resolved.createdAt, outageWindows);
80
110
  });
81
111
  const excludedCount = candidates.length - retryable.length;
82
112
  if (excludedCount > 0) {
@@ -118,6 +148,14 @@ export async function resetFailedPages(knex) {
118
148
  await knex('page_html_ref').whereIn('page_id', chunk).delete();
119
149
  await knex('page_tags').whereIn('pageId', chunk).delete();
120
150
  await knex('page_jsonld').whereIn('pageId', chunk).delete();
151
+ await knex('page_main_content_headings').whereIn('pageId', chunk).delete();
152
+ await knex('page_main_content_images').whereIn('pageId', chunk).delete();
153
+ await knex('page_main_content_tables').whereIn('pageId', chunk).delete();
154
+ await knex('page_main_content_buttons').whereIn('pageId', chunk).delete();
155
+ await knex('page_main_content_iframes').whereIn('pageId', chunk).delete();
156
+ await knex('page_main_content_videos').whereIn('pageId', chunk).delete();
157
+ await knex('page_main_content_audios').whereIn('pageId', chunk).delete();
158
+ await knex('page_main_content_canvases').whereIn('pageId', chunk).delete();
121
159
  }
122
160
  dbLog('Reset %d failed pages back to pending', urls.length);
123
161
  return urls;
@@ -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_audios` 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 insertAudios(pageId: number, mainContents: MainContentsData, trx: Knex.Transaction): Promise<void>;
@@ -0,0 +1,25 @@
1
+ import { eachSplitted } from '../../../../utils/array/each-splitted.js';
2
+ /**
3
+ * Replaces the page's `page_main_content_audios` 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 insertAudios(pageId, mainContents, trx) {
14
+ if (mainContents.audios.length === 0)
15
+ return;
16
+ const rows = mainContents.audios.map((audio, order) => ({
17
+ pageId,
18
+ order,
19
+ src: audio.src,
20
+ }));
21
+ await trx('page_main_content_audios').where('pageId', pageId).delete();
22
+ await eachSplitted(rows, 100, async (chunk) => {
23
+ await trx('page_main_content_audios').insert(chunk);
24
+ });
25
+ }
@@ -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_buttons` 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 insertButtons(pageId: number, mainContents: MainContentsData, trx: Knex.Transaction): Promise<void>;