@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
@@ -8,12 +8,14 @@ import { emitError } from '../utils/error/emit-error.js';
8
8
  import { createWriteRefCaches } from './db-ops/_shared/create-write-ref-caches.js';
9
9
  import { retrySetting } from './db-ops/_shared/retry-setting.js';
10
10
  import { replaceAnalysisViolations as replaceAnalysisViolationsOp } from './db-ops/analysis/replace-analysis-violations.js';
11
+ import { replacePageTemplates as replacePageTemplatesOp } from './db-ops/analysis/replace-page-templates.js';
11
12
  import { getAnchorsOnPage as getAnchorsOnPageOp } from './db-ops/anchors/get-anchors-on-page.js';
12
13
  import { getBaseUrl as getBaseUrlOp } from './db-ops/config/get-base-url.js';
13
14
  import { getConfig as getConfigOp } from './db-ops/config/get-config.js';
14
15
  import { getName as getNameOp } from './db-ops/config/get-name.js';
15
16
  import { setConfig as setConfigOp } from './db-ops/config/set-config.js';
16
17
  import { updateConfig as updateConfigOp } from './db-ops/config/update-config.js';
18
+ import { replaceConsoleLogs as replaceConsoleLogsOp } from './db-ops/console-logs/replace-console-logs.js';
17
19
  import { insertCrawlError as insertCrawlErrorOp } from './db-ops/errors/insert-crawl-error.js';
18
20
  import { insertPageError as insertPageErrorOp } from './db-ops/errors/insert-page-error.js';
19
21
  import { listDnsBurnedHostCandidates as listDnsBurnedHostCandidatesOp } from './db-ops/errors/list-dns-burned-host-candidates.js';
@@ -22,8 +24,19 @@ import { recordInventoryRun as recordInventoryRunOp } from './db-ops/inventory/r
22
24
  import { checkpoint as checkpointOp } from './db-ops/lifecycle/checkpoint.js';
23
25
  import { destroy as destroyOp } from './db-ops/lifecycle/destroy.js';
24
26
  import { init as initOp } from './db-ops/lifecycle/init.js';
27
+ import { getAudiosOfPage as getAudiosOfPageOp } from './db-ops/meta/get-audios-of-page.js';
28
+ import { getButtonsOfPage as getButtonsOfPageOp } from './db-ops/meta/get-buttons-of-page.js';
29
+ import { getCanvasesOfPage as getCanvasesOfPageOp } from './db-ops/meta/get-canvases-of-page.js';
30
+ import { getHeadingsOfPage as getHeadingsOfPageOp } from './db-ops/meta/get-headings-of-page.js';
31
+ import { getIframesOfPage as getIframesOfPageOp } from './db-ops/meta/get-iframes-of-page.js';
25
32
  import { getJsonLdOfPage as getJsonLdOfPageOp } from './db-ops/meta/get-jsonld-of-page.js';
33
+ import { getMainContentImagesOfPage as getMainContentImagesOfPageOp } from './db-ops/meta/get-main-content-images-of-page.js';
34
+ import { getMainContentTablesOfPage as getMainContentTablesOfPageOp } from './db-ops/meta/get-main-content-tables-of-page.js';
26
35
  import { getTagsOfPage as getTagsOfPageOp } from './db-ops/meta/get-tags-of-page.js';
36
+ import { getVideosOfPage as getVideosOfPageOp } from './db-ops/meta/get-videos-of-page.js';
37
+ import { closeNetworkOutage as closeNetworkOutageOp } from './db-ops/outages/close-network-outage.js';
38
+ import { insertNetworkOutage as insertNetworkOutageOp } from './db-ops/outages/insert-network-outage.js';
39
+ import { listNetworkOutages as listNetworkOutagesOp } from './db-ops/outages/list-network-outages.js';
27
40
  import { setUrlOrder as setUrlOrderOp } from './db-ops/pages/order/set-url-order.js';
28
41
  import { getCrawlingState as getCrawlingStateOp } from './db-ops/pages/read/get-crawling-state.js';
29
42
  import { getExistingPageUrls as getExistingPageUrlsOp } from './db-ops/pages/read/get-existing-page-urls.js';
@@ -112,6 +125,15 @@ export class Database extends EventEmitter {
112
125
  async checkpoint() {
113
126
  await checkpointOp(this.#instance);
114
127
  }
128
+ /**
129
+ * Closes an outage row by stamping `ended_at` — a no-op if the row is
130
+ * already closed. Delegates to {@link closeNetworkOutageOp}.
131
+ * @param id - The `network_outages.id` to close.
132
+ * @param endedAt - Epoch ms the outage is considered to have ended.
133
+ */
134
+ async closeNetworkOutage(id, endedAt) {
135
+ return emitErrorAndRetry(this, 'Database.closeNetworkOutage', async () => await closeNetworkOutageOp(this.#instance, id, endedAt), retrySetting);
136
+ }
115
137
  /**
116
138
  * Destroys the database connection, releasing all pooled resources.
117
139
  * Delegates to {@link destroyOp}.
@@ -128,6 +150,14 @@ export class Database extends EventEmitter {
128
150
  async getAnchorsOnPage(pageId) {
129
151
  return emitErrorAndRetry(this, 'Database.getAnchorsOnPage', async () => await getAnchorsOnPageOp(this.#instance, pageId), retrySetting);
130
152
  }
153
+ /**
154
+ * Retrieves all `page_main_content_audios` rows for the given page id.
155
+ * Delegates to {@link getAudiosOfPageOp}.
156
+ * @param pageId
157
+ */
158
+ async getAudiosOfPage(pageId) {
159
+ return emitErrorAndRetry(this, 'Database.getAudiosOfPage', async () => await getAudiosOfPageOp(this.#instance, pageId), retrySetting);
160
+ }
131
161
  /**
132
162
  * Retrieves the base URL of the crawl session from the `info` table.
133
163
  * Delegates to {@link getBaseUrlOp}.
@@ -137,6 +167,22 @@ export class Database extends EventEmitter {
137
167
  async getBaseUrl() {
138
168
  return emitErrorAndRetry(this, 'Database.getBaseUrl', async () => await getBaseUrlOp(this.#instance), retrySetting);
139
169
  }
170
+ /**
171
+ * Retrieves all `page_main_content_buttons` rows for the given page id.
172
+ * Delegates to {@link getButtonsOfPageOp}.
173
+ * @param pageId
174
+ */
175
+ async getButtonsOfPage(pageId) {
176
+ return emitErrorAndRetry(this, 'Database.getButtonsOfPage', async () => await getButtonsOfPageOp(this.#instance, pageId), retrySetting);
177
+ }
178
+ /**
179
+ * Retrieves all `page_main_content_canvases` rows for the given page id.
180
+ * Delegates to {@link getCanvasesOfPageOp}.
181
+ * @param pageId
182
+ */
183
+ async getCanvasesOfPage(pageId) {
184
+ return emitErrorAndRetry(this, 'Database.getCanvasesOfPage', async () => await getCanvasesOfPageOp(this.#instance, pageId), retrySetting);
185
+ }
140
186
  /**
141
187
  * Retrieves the full crawl configuration from the `info` table.
142
188
  * Delegates to {@link getConfigOp}.
@@ -174,6 +220,14 @@ export class Database extends EventEmitter {
174
220
  async getExistingResourceUrls(urls) {
175
221
  return emitError(this, 'Database.getExistingResourceUrls', async () => await getExistingResourceUrlsOp(this.#instance, urls));
176
222
  }
223
+ /**
224
+ * Retrieves all `page_main_content_headings` rows for the given page id.
225
+ * Delegates to {@link getHeadingsOfPageOp}.
226
+ * @param pageId
227
+ */
228
+ async getHeadingsOfPage(pageId) {
229
+ return emitErrorAndRetry(this, 'Database.getHeadingsOfPage', async () => await getHeadingsOfPageOp(this.#instance, pageId), retrySetting);
230
+ }
177
231
  /**
178
232
  * Reads the HTML snapshot stored as a zstd-compressed BLOB for the given page.
179
233
  * Delegates to {@link getHtmlOfPageByIdOp}.
@@ -183,6 +237,14 @@ export class Database extends EventEmitter {
183
237
  async getHtmlOfPageById(pageId) {
184
238
  return emitErrorAndRetry(this, 'Database.getHtmlOfPageById', async () => await getHtmlOfPageByIdOp(this.#instance, pageId), retrySetting);
185
239
  }
240
+ /**
241
+ * Retrieves all `page_main_content_iframes` rows for the given page id.
242
+ * Delegates to {@link getIframesOfPageOp}.
243
+ * @param pageId
244
+ */
245
+ async getIframesOfPage(pageId) {
246
+ return emitErrorAndRetry(this, 'Database.getIframesOfPage', async () => await getIframesOfPageOp(this.#instance, pageId), retrySetting);
247
+ }
186
248
  /**
187
249
  * Retrieves all `page_jsonld` rows for the given page id, parsed back into
188
250
  * {@link JsonLdRow} shape. Delegates to {@link getJsonLdOfPageOp}.
@@ -200,6 +262,22 @@ export class Database extends EventEmitter {
200
262
  getKnex() {
201
263
  return this.#instance;
202
264
  }
265
+ /**
266
+ * Retrieves all `page_main_content_images` rows for the given page id.
267
+ * Delegates to {@link getMainContentImagesOfPageOp}.
268
+ * @param pageId
269
+ */
270
+ async getMainContentImagesOfPage(pageId) {
271
+ return emitErrorAndRetry(this, 'Database.getMainContentImagesOfPage', async () => await getMainContentImagesOfPageOp(this.#instance, pageId), retrySetting);
272
+ }
273
+ /**
274
+ * Retrieves all `page_main_content_tables` rows for the given page id.
275
+ * Delegates to {@link getMainContentTablesOfPageOp}.
276
+ * @param pageId
277
+ */
278
+ async getMainContentTablesOfPage(pageId) {
279
+ return emitErrorAndRetry(this, 'Database.getMainContentTablesOfPage', async () => await getMainContentTablesOfPageOp(this.#instance, pageId), retrySetting);
280
+ }
203
281
  /**
204
282
  * Retrieves the crawl session name from the `info` table.
205
283
  * Delegates to {@link getNameOp}.
@@ -326,6 +404,14 @@ export class Database extends EventEmitter {
326
404
  async getTagsOfPage(pageId) {
327
405
  return emitErrorAndRetry(this, 'Database.getTagsOfPage', async () => await getTagsOfPageOp(this.#instance, pageId), retrySetting);
328
406
  }
407
+ /**
408
+ * Retrieves all `page_main_content_videos` rows for the given page id.
409
+ * Delegates to {@link getVideosOfPageOp}.
410
+ * @param pageId
411
+ */
412
+ async getVideosOfPage(pageId) {
413
+ return emitErrorAndRetry(this, 'Database.getVideosOfPage', async () => await getVideosOfPageOp(this.#instance, pageId), retrySetting);
414
+ }
329
415
  /**
330
416
  * Records a crawler-level (`error` channel) failure into `crawl_errors`.
331
417
  * Delegates to {@link insertCrawlErrorOp}.
@@ -354,6 +440,15 @@ export class Database extends EventEmitter {
354
440
  async insertInventorySeeds(urls) {
355
441
  return emitErrorAndRetry(this, 'Database.insertInventorySeeds', async () => await insertInventorySeedsOp(this.#instance, this.#writeRefCaches, urls), retrySetting);
356
442
  }
443
+ /**
444
+ * Appends one open (`ended_at = NULL`) row to the `network_outages`
445
+ * journal. Delegates to {@link insertNetworkOutageOp}.
446
+ * @param params - The confirmed-outage fields to record.
447
+ * @returns The autoincremented `id` of the newly-inserted row.
448
+ */
449
+ async insertNetworkOutage(params) {
450
+ return emitErrorAndRetry(this, 'Database.insertNetworkOutage', async () => await insertNetworkOutageOp(this.#instance, params), retrySetting);
451
+ }
357
452
  /**
358
453
  * Records a partial scrape failure against the page identified by `url`.
359
454
  * Delegates to {@link insertPageErrorOp}.
@@ -392,6 +487,15 @@ export class Database extends EventEmitter {
392
487
  async listDnsBurnedHostCandidates() {
393
488
  return emitErrorAndRetry(this, 'Database.listDnsBurnedHostCandidates', async () => await listDnsBurnedHostCandidatesOp(this.#instance), retrySetting);
394
489
  }
490
+ /**
491
+ * Lists every recorded outage as a resolved {@link OutageWindow}.
492
+ * Delegates to {@link listNetworkOutagesOp}.
493
+ * @returns Resolved outage windows, or `[]` on an archive that predates
494
+ * `network_outages` or has recorded no outages.
495
+ */
496
+ async listNetworkOutages() {
497
+ return emitErrorAndRetry(this, 'Database.listNetworkOutages', async () => await listNetworkOutagesOp(this.#instance), retrySetting);
498
+ }
395
499
  /**
396
500
  * Appends one row to the `inventory_runs` audit log.
397
501
  * Delegates to {@link recordInventoryRunOp}.
@@ -421,6 +525,30 @@ export class Database extends EventEmitter {
421
525
  async replaceAnalysisViolations(violations) {
422
526
  return emitErrorAndRetry(this, 'Database.replaceAnalysisViolations', async () => await replaceAnalysisViolationsOp(this.#instance, violations), retrySetting);
423
527
  }
528
+ /**
529
+ * Replaces one page's `page_console_logs` rows with a freshly captured
530
+ * set of console messages / page errors. Delegates to
531
+ * {@link replaceConsoleLogsOp}.
532
+ * @param pageUrl - The originally-requested URL, normalised (`withoutHashAndAuth` form).
533
+ * @param redirectPaths - The redirect chain hops captured during fetch, in order.
534
+ * @param entries - The console log entries to persist.
535
+ */
536
+ async replaceConsoleLogs(pageUrl, redirectPaths, entries) {
537
+ return emitErrorAndRetry(this, 'Database.replaceConsoleLogs', async () => await replaceConsoleLogsOp(this.#instance, this.#writeRefCaches, pageUrl, redirectPaths, entries), retrySetting);
538
+ }
539
+ /**
540
+ * Replaces the stored DOM-structure template classification with a
541
+ * freshly generated set. Delegates to {@link replacePageTemplatesOp}.
542
+ * @param templateKeysByUrl - Page URL → template key.
543
+ * @param clusterReasonsByTemplateKey - Template key → cluster-selection
544
+ * evidence, if the caller captured it.
545
+ */
546
+ async replacePageTemplates(templateKeysByUrl, clusterReasonsByTemplateKey) {
547
+ return emitErrorAndRetry(this, 'Database.replacePageTemplates', async () => await replacePageTemplatesOp(this.#instance, {
548
+ templateKeysByUrl,
549
+ clusterReasonsByTemplateKey,
550
+ }), retrySetting);
551
+ }
424
552
  /**
425
553
  * Promote previously-external pages whose URL falls under any of the new
426
554
  * scope entries back to a "needs scraping" state.
@@ -30,5 +30,6 @@ export function clearWriteRefCaches(caches) {
30
30
  caches.contentTypeIds.clear();
31
31
  caches.jsonIds.clear();
32
32
  caches.blobIds.clear();
33
+ caches.consoleLogIds.clear();
33
34
  caches.headers = null;
34
35
  }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Compresses raw bytes for storage in a `codec`/`size_raw`/`size_stored`
3
+ * BLOB column, matching the read side of
4
+ * {@link import('./decode-json-ref.js').decodeJsonRef} and
5
+ * {@link import('../../decode-html-blob.js').decodeStoredBlob}.
6
+ *
7
+ * The one shared primitive behind every `codec: 'zstd'` writer in the
8
+ * archive (`upsert-json-ref.ts`, `write-page-html-blob.ts`,
9
+ * `replace-page-templates.ts`) — a codec change (e.g. a brotli fallback, a
10
+ * different zstd level) only has to happen here. Callers that also need a
11
+ * content hash (`upsert-json-ref.ts`, `write-page-html-blob.ts`) compute it
12
+ * themselves from the same `rawBytes` they pass in here, rather than this
13
+ * function computing one not every caller needs.
14
+ * @param rawBytes - The raw (uncompressed) payload bytes.
15
+ * @returns The compressed body alongside the codec and size bookkeeping the
16
+ * BLOB column's schema expects.
17
+ * @example
18
+ * const rawBytes = Buffer.from(JSON.stringify(reason), 'utf8');
19
+ * const { body, codec, sizeRaw, sizeStored } = compressPayload(rawBytes);
20
+ */
21
+ export declare function compressPayload(rawBytes: Buffer): {
22
+ readonly body: Buffer;
23
+ readonly codec: 'zstd';
24
+ readonly sizeRaw: number;
25
+ readonly sizeStored: number;
26
+ };
@@ -0,0 +1,30 @@
1
+ import { zstdCompressSync } from 'node:zlib';
2
+ /**
3
+ * Compresses raw bytes for storage in a `codec`/`size_raw`/`size_stored`
4
+ * BLOB column, matching the read side of
5
+ * {@link import('./decode-json-ref.js').decodeJsonRef} and
6
+ * {@link import('../../decode-html-blob.js').decodeStoredBlob}.
7
+ *
8
+ * The one shared primitive behind every `codec: 'zstd'` writer in the
9
+ * archive (`upsert-json-ref.ts`, `write-page-html-blob.ts`,
10
+ * `replace-page-templates.ts`) — a codec change (e.g. a brotli fallback, a
11
+ * different zstd level) only has to happen here. Callers that also need a
12
+ * content hash (`upsert-json-ref.ts`, `write-page-html-blob.ts`) compute it
13
+ * themselves from the same `rawBytes` they pass in here, rather than this
14
+ * function computing one not every caller needs.
15
+ * @param rawBytes - The raw (uncompressed) payload bytes.
16
+ * @returns The compressed body alongside the codec and size bookkeeping the
17
+ * BLOB column's schema expects.
18
+ * @example
19
+ * const rawBytes = Buffer.from(JSON.stringify(reason), 'utf8');
20
+ * const { body, codec, sizeRaw, sizeStored } = compressPayload(rawBytes);
21
+ */
22
+ export function compressPayload(rawBytes) {
23
+ const compressed = zstdCompressSync(rawBytes);
24
+ return {
25
+ body: compressed,
26
+ codec: 'zstd',
27
+ sizeRaw: rawBytes.byteLength,
28
+ sizeStored: compressed.byteLength,
29
+ };
30
+ }
@@ -21,6 +21,7 @@ export function createWriteRefCaches() {
21
21
  contentTypeIds: new Map(),
22
22
  jsonIds: new Map(),
23
23
  blobIds: new Map(),
24
+ consoleLogIds: new Map(),
24
25
  headers: null,
25
26
  };
26
27
  }
@@ -42,6 +42,8 @@ export interface WriteRefCaches {
42
42
  readonly jsonIds: Map<string, number>;
43
43
  /** `hex(content hash) → blob_refs.id`. */
44
44
  readonly blobIds: Map<string, number>;
45
+ /** `hex(content hash) → console_log_items.id`. */
46
+ readonly consoleLogIds: Map<string, number>;
45
47
  /**
46
48
  * Header dictionary caches (`header_name_refs` / `header_value_refs` /
47
49
  * `header_sets` id maps). `null` until the first header-set upsert
@@ -1,5 +1,5 @@
1
- import { zstdCompressSync } from 'node:zlib';
2
1
  import { computeContentHash } from '../../populate-ref-tables/compute-content-hash.js';
2
+ import { compressPayload } from './compress-payload.js';
3
3
  /**
4
4
  * Resolves the `json_refs.id` for one JSON payload string, inserting the
5
5
  * deduplicated (hash-keyed) row when the payload is new.
@@ -27,11 +27,11 @@ export async function upsertJsonRef(qb, caches, jsonText) {
27
27
  if (cached !== undefined) {
28
28
  return cached;
29
29
  }
30
- const compressed = zstdCompressSync(rawBytes);
30
+ const { body, sizeRaw, sizeStored } = compressPayload(rawBytes);
31
31
  const rows = await qb.raw(`INSERT INTO json_refs (hash, json_text, codec, size_raw, size_stored)
32
32
  VALUES (?, ?, 'zstd', ?, ?)
33
33
  ON CONFLICT(hash) DO UPDATE SET hash = hash
34
- RETURNING id`, [hash, compressed, rawBytes.byteLength, compressed.byteLength]);
34
+ RETURNING id`, [hash, body, sizeRaw, sizeStored]);
35
35
  const first = rows[0];
36
36
  if (first === undefined) {
37
37
  throw new Error('upsertJsonRef: RETURNING yielded no row');
@@ -0,0 +1,29 @@
1
+ import type { ReplacePageTemplatesParams } from './types.js';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Replaces the stored DOM-structure template classification (`--templates`)
5
+ * with a freshly generated set.
6
+ *
7
+ * Every classified page is a full-archive, all-or-nothing recomputation
8
+ * (see `@nitpicker/core`'s `classifyPageTemplates`), so this always deletes
9
+ * every existing row before inserting the new set — there is no per-page
10
+ * incremental update path, matching `replaceAnalysisViolations`'s
11
+ * whole-table replace shape. Unlike violations, a page whose URL can't be
12
+ * resolved back to a `content_items` row is silently skipped rather than
13
+ * treated as a hard failure: losing one page's template classification
14
+ * (e.g. a URL-normalization mismatch between the in-memory `Page.url.href`
15
+ * and the stored `url_refs.url`) should not discard the rest of a
16
+ * potentially multi-thousand-page classification run.
17
+ *
18
+ * `page_template_clusters` is always cleared alongside `page_templates`
19
+ * regardless of whether `clusterReasonsByTemplateKey` is passed — "no
20
+ * reason" must mean "not captured for this run", never "carry over the
21
+ * previous run's reason". Reason rows are inserted for every key in
22
+ * `clusterReasonsByTemplateKey` even if some have no surviving member page
23
+ * in `templateKeysByUrl` after URL-resolution skips above — harmless
24
+ * (nothing joins `page_template_clusters` back to `page_templates` by FK;
25
+ * see the table's own JSDoc), and simpler than cross-filtering the two maps.
26
+ * @param knex - Knex query builder connected to the archive DB.
27
+ * @param params - See {@link ReplacePageTemplatesParams}.
28
+ */
29
+ export declare function replacePageTemplates(knex: Knex, params: ReplacePageTemplatesParams): Promise<void>;
@@ -0,0 +1,84 @@
1
+ import { eachSplitted } from '../../../utils/array/each-splitted.js';
2
+ import { compressPayload } from '../_shared/compress-payload.js';
3
+ /**
4
+ * Replaces the stored DOM-structure template classification (`--templates`)
5
+ * with a freshly generated set.
6
+ *
7
+ * Every classified page is a full-archive, all-or-nothing recomputation
8
+ * (see `@nitpicker/core`'s `classifyPageTemplates`), so this always deletes
9
+ * every existing row before inserting the new set — there is no per-page
10
+ * incremental update path, matching `replaceAnalysisViolations`'s
11
+ * whole-table replace shape. Unlike violations, a page whose URL can't be
12
+ * resolved back to a `content_items` row is silently skipped rather than
13
+ * treated as a hard failure: losing one page's template classification
14
+ * (e.g. a URL-normalization mismatch between the in-memory `Page.url.href`
15
+ * and the stored `url_refs.url`) should not discard the rest of a
16
+ * potentially multi-thousand-page classification run.
17
+ *
18
+ * `page_template_clusters` is always cleared alongside `page_templates`
19
+ * regardless of whether `clusterReasonsByTemplateKey` is passed — "no
20
+ * reason" must mean "not captured for this run", never "carry over the
21
+ * previous run's reason". Reason rows are inserted for every key in
22
+ * `clusterReasonsByTemplateKey` even if some have no surviving member page
23
+ * in `templateKeysByUrl` after URL-resolution skips above — harmless
24
+ * (nothing joins `page_template_clusters` back to `page_templates` by FK;
25
+ * see the table's own JSDoc), and simpler than cross-filtering the two maps.
26
+ * @param knex - Knex query builder connected to the archive DB.
27
+ * @param params - See {@link ReplacePageTemplatesParams}.
28
+ */
29
+ export async function replacePageTemplates(knex, params) {
30
+ const { templateKeysByUrl, clusterReasonsByTemplateKey } = params;
31
+ // Compressing every reason is pure CPU work independent of the DB — done
32
+ // before opening the transaction below so it doesn't extend how long the
33
+ // SQLite write-lock is held for.
34
+ const reasonRows = clusterReasonsByTemplateKey && clusterReasonsByTemplateKey.size > 0
35
+ ? [...clusterReasonsByTemplateKey].map(([templateKey, reason]) => {
36
+ const { body, codec, sizeRaw, sizeStored } = compressPayload(Buffer.from(JSON.stringify(reason), 'utf8'));
37
+ return {
38
+ template_key: templateKey,
39
+ member_count: reason.memberCount,
40
+ reason_json: body,
41
+ codec,
42
+ size_raw: sizeRaw,
43
+ size_stored: sizeStored,
44
+ };
45
+ })
46
+ : [];
47
+ await knex.transaction(async (trx) => {
48
+ await trx('page_templates').delete();
49
+ await trx('page_template_clusters').delete();
50
+ if (reasonRows.length > 0) {
51
+ await eachSplitted(reasonRows, 100, async (chunk) => {
52
+ await trx('page_template_clusters').insert(chunk);
53
+ });
54
+ }
55
+ if (templateKeysByUrl.size === 0) {
56
+ return;
57
+ }
58
+ const urls = [...templateKeysByUrl.keys()];
59
+ const pageIdByUrl = new Map();
60
+ await eachSplitted(urls, 500, async (chunk) => {
61
+ const pageRows = await trx('content_items')
62
+ .join('url_refs', 'url_refs.id', 'content_items.url_id')
63
+ .select('content_items.id as id', 'url_refs.url as url')
64
+ .whereIn('url_refs.url', chunk);
65
+ for (const row of pageRows) {
66
+ pageIdByUrl.set(row.url, row.id);
67
+ }
68
+ });
69
+ const rows = [];
70
+ for (const [url, templateKey] of templateKeysByUrl) {
71
+ const pageId = pageIdByUrl.get(url);
72
+ if (pageId == null) {
73
+ continue;
74
+ }
75
+ rows.push({ page_id: pageId, template_key: templateKey });
76
+ }
77
+ if (rows.length === 0) {
78
+ return;
79
+ }
80
+ await eachSplitted(rows, 500, async (chunk) => {
81
+ await trx('page_templates').insert(chunk);
82
+ });
83
+ });
84
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The six landmark types `@d-zero/page-cluster`'s `extractLandmarks` and
3
+ * `ClusterReason.landmarks` recognize.
4
+ */
5
+ export type TemplateClusterLandmarkType = 'header' | 'footer' | 'nav' | 'aside' | 'form' | 'search';
6
+ /**
7
+ * Why a Pass-0 block (one of possibly several that merged into a final
8
+ * cluster) was formed — mirrors `@d-zero/page-cluster`'s `BlockingReason`
9
+ * discriminated union.
10
+ */
11
+ export type TemplateClusterBlockingReason = {
12
+ readonly kind: 'css';
13
+ readonly distinctiveStylesheetHrefs: readonly string[];
14
+ } | {
15
+ readonly kind: 'path';
16
+ readonly pathKey: string;
17
+ } | {
18
+ readonly kind: 'orphanMerge';
19
+ readonly pathKey: string;
20
+ };
21
+ /** One block's blocking key and the reason it was formed. */
22
+ export interface TemplateClusterBlockingEvidence {
23
+ readonly blockKey: string;
24
+ readonly reason: TemplateClusterBlockingReason;
25
+ }
26
+ /** How common one landmark type is across a cluster's member pages. */
27
+ export interface TemplateClusterLandmarkProfile {
28
+ readonly presenceRate: number;
29
+ readonly chromeRate: number;
30
+ readonly shellTokens: readonly string[];
31
+ readonly memberCountWithInstance: number;
32
+ }
33
+ /**
34
+ * nitpicker's own copy of `@d-zero/page-cluster`'s `ClusterReason` shape.
35
+ * Kept independent of the `@d-zero/page-cluster` package (rather than
36
+ * importing its type directly) so `@nitpicker/query` — which does not
37
+ * depend on `@d-zero/page-cluster` and is consumed by the browser-side
38
+ * viewer build — never has to add that dependency just to type a value
39
+ * read back out of the archive. `@d-zero/page-cluster`'s `ClusterReason`
40
+ * is structurally assignable to this type.
41
+ */
42
+ export interface TemplateClusterReason {
43
+ readonly memberCount: number;
44
+ readonly blocking: readonly TemplateClusterBlockingEvidence[];
45
+ readonly structuralCoreTokens: readonly string[];
46
+ readonly landmarks: Partial<Record<TemplateClusterLandmarkType, TemplateClusterLandmarkProfile>>;
47
+ readonly siblingClusterKeys: readonly string[];
48
+ }
49
+ /**
50
+ * Params for {@link import('./replace-page-templates.js').replacePageTemplates}.
51
+ */
52
+ export interface ReplacePageTemplatesParams {
53
+ /** Page URL → template key, as produced by `classifyPageTemplates`. */
54
+ readonly templateKeysByUrl: ReadonlyMap<string, string>;
55
+ /**
56
+ * Template key → `@d-zero/page-cluster`'s cluster-selection evidence for
57
+ * that key, as produced by `classifyPageTemplates`. Omitted entirely (not
58
+ * just empty) when the caller didn't request reasons.
59
+ */
60
+ readonly clusterReasonsByTemplateKey?: ReadonlyMap<string, TemplateClusterReason>;
61
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -23,4 +23,5 @@ export const INFO_COLUMN_ALLOWLIST = new Set([
23
23
  'disableQueries',
24
24
  'userAgent',
25
25
  'ignoreRobots',
26
+ 'mainContentSelector',
26
27
  ]);
@@ -0,0 +1,19 @@
1
+ import type { ConsoleLogContent } from './types.js';
2
+ /**
3
+ * Computes the `console_log_items.hash` for one console log entry's content.
4
+ *
5
+ * Hashing over the raw content fields (not their resolved ref ids) keeps the
6
+ * dictionary row's identity independent of ref-table state, matching
7
+ * `text_refs` / `json_refs` / `url_refs`, whose hash columns are likewise
8
+ * computed from source content rather than downstream ids. The same
9
+ * message logged by a shared framework on every page therefore hashes to
10
+ * the same value regardless of insertion order or which page's scrape
11
+ * resolves the dictionary row first.
12
+ * @param entry - The content fields to hash. `argsJson` must already be
13
+ * `stringifyConsoleLogArgs`'s output — this function does not
14
+ * re-serialize `args` itself.
15
+ * @returns 32-byte SHA-256 hash, ready for the `hash BLOB UNIQUE` column.
16
+ * @example
17
+ * const hash = computeConsoleLogHash({ type: 'error', text: 'boom', argsJson: null });
18
+ */
19
+ export declare function computeConsoleLogHash(entry: ConsoleLogContent): Buffer;
@@ -0,0 +1,30 @@
1
+ import { computeContentHash } from '../../populate-ref-tables/compute-content-hash.js';
2
+ /**
3
+ * Computes the `console_log_items.hash` for one console log entry's content.
4
+ *
5
+ * Hashing over the raw content fields (not their resolved ref ids) keeps the
6
+ * dictionary row's identity independent of ref-table state, matching
7
+ * `text_refs` / `json_refs` / `url_refs`, whose hash columns are likewise
8
+ * computed from source content rather than downstream ids. The same
9
+ * message logged by a shared framework on every page therefore hashes to
10
+ * the same value regardless of insertion order or which page's scrape
11
+ * resolves the dictionary row first.
12
+ * @param entry - The content fields to hash. `argsJson` must already be
13
+ * `stringifyConsoleLogArgs`'s output — this function does not
14
+ * re-serialize `args` itself.
15
+ * @returns 32-byte SHA-256 hash, ready for the `hash BLOB UNIQUE` column.
16
+ * @example
17
+ * const hash = computeConsoleLogHash({ type: 'error', text: 'boom', argsJson: null });
18
+ */
19
+ export function computeConsoleLogHash(entry) {
20
+ const canonical = JSON.stringify([
21
+ entry.type,
22
+ entry.text,
23
+ entry.argsJson,
24
+ entry.location?.url ?? null,
25
+ entry.location?.lineNumber ?? null,
26
+ entry.location?.columnNumber ?? null,
27
+ entry.stack ?? null,
28
+ ]);
29
+ return computeContentHash(canonical);
30
+ }
@@ -0,0 +1,34 @@
1
+ import type { WriteRefCaches } from '../_shared/types.js';
2
+ import type { ConsoleLogEntry } from '@d-zero/beholder';
3
+ import type { Knex } from 'knex';
4
+ /**
5
+ * Replaces one page's `page_console_logs` rows wholesale with a freshly
6
+ * captured set of console messages / page errors (issue #228).
7
+ *
8
+ * Scoped-Replace, the same pattern as `replaceAnchorEdges` /
9
+ * `replaceImageItems`: the page's existing rows are deleted and the new
10
+ * set is inserted in the same transaction, so a re-scrape (`--append` /
11
+ * `--retry-failed` / re-render) never accumulates duplicate occurrences.
12
+ * Unlike those two, there is no non-empty guard on the DELETE here — the
13
+ * caller (`Crawler#handleConsoleLogs`) already skips emitting the event
14
+ * entirely when `entries` is empty, which is what keeps a degraded
15
+ * re-scrape from wiping out a prior good capture.
16
+ *
17
+ * `pageUrl` is the originally-requested URL (matching `updatePage`'s
18
+ * `page.url.withoutHashAndAuth` contract), not necessarily the page that
19
+ * ends up holding the content: `resolveRedirectChain` derives the same
20
+ * redirect destination `updatePage` writes content under, so console logs
21
+ * attach to the row that actually carries the page's `page_meta`, not to
22
+ * an empty placeholder for the pre-redirect URL.
23
+ * @param knex - Knex query builder connected to the archive DB.
24
+ * @param caches - The connection's write-side id caches.
25
+ * @param pageUrl - The originally-requested URL, normalised
26
+ * (`withoutHashAndAuth` form).
27
+ * @param redirectPaths - The redirect chain hops captured during fetch, in
28
+ * order (empty when the page was not redirected).
29
+ * @param entries - The console log entries to persist. Must be non-empty —
30
+ * callers should skip calling this function entirely for an empty list.
31
+ * @example
32
+ * await replaceConsoleLogs(knex, caches, page.url.withoutHashAndAuth, page.redirectPaths, entries);
33
+ */
34
+ export declare function replaceConsoleLogs(knex: Knex, caches: WriteRefCaches, pageUrl: string, redirectPaths: readonly string[], entries: readonly ConsoleLogEntry[]): Promise<void>;