@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,123 @@
1
+ import { upsertTextRefs } from '../../populate-entity-tables/upsert-text-refs.js';
2
+ import { resolveRedirectChain } from '../../resolve-redirect-chain.js';
3
+ import { clearWriteRefCaches } from '../_shared/clear-write-ref-caches.js';
4
+ import { resolveContentItemId } from '../_shared/resolve-content-item-id.js';
5
+ import { upsertJsonRef } from '../_shared/upsert-json-ref.js';
6
+ import { upsertUrlRef } from '../_shared/upsert-url-ref.js';
7
+ import { computeConsoleLogHash } from './compute-console-log-hash.js';
8
+ import { stringifyConsoleLogArgs } from './stringify-console-log-args.js';
9
+ import { upsertConsoleLogItem } from './upsert-console-log-item.js';
10
+ /** Rows sent per `INSERT INTO page_console_logs ... VALUES (...)` statement. */
11
+ const INSERT_CHUNK_SIZE = 500;
12
+ /**
13
+ * Replaces one page's `page_console_logs` rows wholesale with a freshly
14
+ * captured set of console messages / page errors (issue #228).
15
+ *
16
+ * Scoped-Replace, the same pattern as `replaceAnchorEdges` /
17
+ * `replaceImageItems`: the page's existing rows are deleted and the new
18
+ * set is inserted in the same transaction, so a re-scrape (`--append` /
19
+ * `--retry-failed` / re-render) never accumulates duplicate occurrences.
20
+ * Unlike those two, there is no non-empty guard on the DELETE here — the
21
+ * caller (`Crawler#handleConsoleLogs`) already skips emitting the event
22
+ * entirely when `entries` is empty, which is what keeps a degraded
23
+ * re-scrape from wiping out a prior good capture.
24
+ *
25
+ * `pageUrl` is the originally-requested URL (matching `updatePage`'s
26
+ * `page.url.withoutHashAndAuth` contract), not necessarily the page that
27
+ * ends up holding the content: `resolveRedirectChain` derives the same
28
+ * redirect destination `updatePage` writes content under, so console logs
29
+ * attach to the row that actually carries the page's `page_meta`, not to
30
+ * an empty placeholder for the pre-redirect URL.
31
+ * @param knex - Knex query builder connected to the archive DB.
32
+ * @param caches - The connection's write-side id caches.
33
+ * @param pageUrl - The originally-requested URL, normalised
34
+ * (`withoutHashAndAuth` form).
35
+ * @param redirectPaths - The redirect chain hops captured during fetch, in
36
+ * order (empty when the page was not redirected).
37
+ * @param entries - The console log entries to persist. Must be non-empty —
38
+ * callers should skip calling this function entirely for an empty list.
39
+ * @example
40
+ * await replaceConsoleLogs(knex, caches, page.url.withoutHashAndAuth, page.redirectPaths, entries);
41
+ */
42
+ export async function replaceConsoleLogs(knex, caches, pageUrl, redirectPaths, entries) {
43
+ const { destUrl } = resolveRedirectChain(pageUrl, redirectPaths);
44
+ try {
45
+ await knex.transaction(async (trx) => {
46
+ const pageId = await resolveContentItemId(trx, caches, destUrl);
47
+ await trx('page_console_logs').where('pageId', pageId).delete();
48
+ const texts = new Set();
49
+ for (const entry of entries) {
50
+ // `text_refs` never stores the empty string (its upsert
51
+ // treats `''` as "nothing to dedupe" and skips it) — a
52
+ // `console.log()` call with zero arguments reports `text:
53
+ // ''`, so that entry resolves to `textId: null` below
54
+ // instead of looking it up.
55
+ if (entry.text !== '') {
56
+ texts.add(entry.text);
57
+ }
58
+ if (entry.stack) {
59
+ texts.add(entry.stack);
60
+ }
61
+ }
62
+ const textIds = await upsertTextRefs(trx, texts);
63
+ const rows = [];
64
+ for (const entry of entries) {
65
+ let textId = null;
66
+ if (entry.text !== '') {
67
+ const resolved = textIds.get(entry.text);
68
+ if (resolved === undefined) {
69
+ throw new Error(`replaceConsoleLogs: text_refs id not resolved for "${entry.text}"`);
70
+ }
71
+ textId = resolved;
72
+ }
73
+ const stackTextId = entry.stack ? (textIds.get(entry.stack) ?? null) : null;
74
+ const argsJson = stringifyConsoleLogArgs(entry.args);
75
+ const argsJsonId = argsJson === null ? null : await upsertJsonRef(trx, caches, argsJson);
76
+ const locUrlId = entry.location?.url === undefined
77
+ ? null
78
+ : await upsertUrlRef(trx, caches, entry.location.url);
79
+ const consoleLogId = await upsertConsoleLogItem(trx, caches, {
80
+ hash: computeConsoleLogHash({
81
+ type: entry.type,
82
+ text: entry.text,
83
+ argsJson,
84
+ location: entry.location,
85
+ stack: entry.stack,
86
+ }),
87
+ type: entry.type,
88
+ textId,
89
+ argsJsonId,
90
+ locUrlId,
91
+ locLine: entry.location?.lineNumber ?? null,
92
+ locColumn: entry.location?.columnNumber ?? null,
93
+ stackTextId,
94
+ });
95
+ rows.push({ pageId, consoleLogId, ts: entry.ts });
96
+ }
97
+ for (let index = 0; index < rows.length; index += INSERT_CHUNK_SIZE) {
98
+ await trx('page_console_logs').insert(rows.slice(index, index + INSERT_CHUNK_SIZE));
99
+ }
100
+ // Denormalised onto `page_meta` (the same write-once-at-scrape-time
101
+ // pattern as `tag_count` / `jsonld_count`) so the Pages list can
102
+ // display and sort by it without a live JOIN + COUNT per row.
103
+ // Computed from `entries` directly rather than re-querying the rows
104
+ // just inserted above — the data is already in hand. A `page_meta`
105
+ // row may not exist yet for a `'skipped'` / `'error'` scrape; the
106
+ // UPDATE then simply affects zero rows rather than throwing.
107
+ const errorCount = entries.filter((entry) => entry.type === 'error' || entry.type === 'pageerror').length;
108
+ await trx('page_meta').where('page_id', pageId).update({
109
+ console_error_count: errorCount,
110
+ });
111
+ });
112
+ }
113
+ catch (error) {
114
+ // A rolled-back transaction can leave ids cached that no longer
115
+ // correspond to any row (AUTOINCREMENT never rewinds) — same
116
+ // cache-poisoning hazard `updatePage` / `recordRedirect` guard
117
+ // against. A full clear (not a partial one) is required because
118
+ // this function shares `caches` with every other write path on
119
+ // the same connection.
120
+ clearWriteRefCaches(caches);
121
+ throw error;
122
+ }
123
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Serializes a console call's argument array to a JSON string, or `null`
3
+ * when there is nothing worth storing.
4
+ *
5
+ * Two cases collapse to `null` rather than an empty/degenerate string:
6
+ * an empty `args` array (most commonly a `"pageerror"` entry, which
7
+ * beholder always reports with `args: []`) and a `JSON.stringify` failure
8
+ * (e.g. a circular reference in a logged object) — both mean "no
9
+ * additional structured payload beyond `text`", not "a malformed one".
10
+ * @param args - The `ConsoleLogEntry.args` array to serialize.
11
+ * @returns The JSON string, or `null` when empty or unserializable.
12
+ * @example
13
+ * stringifyConsoleLogArgs(['a', 1]); // '["a",1]'
14
+ * stringifyConsoleLogArgs([]); // null
15
+ */
16
+ export declare function stringifyConsoleLogArgs(args: readonly unknown[]): string | null;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Serializes a console call's argument array to a JSON string, or `null`
3
+ * when there is nothing worth storing.
4
+ *
5
+ * Two cases collapse to `null` rather than an empty/degenerate string:
6
+ * an empty `args` array (most commonly a `"pageerror"` entry, which
7
+ * beholder always reports with `args: []`) and a `JSON.stringify` failure
8
+ * (e.g. a circular reference in a logged object) — both mean "no
9
+ * additional structured payload beyond `text`", not "a malformed one".
10
+ * @param args - The `ConsoleLogEntry.args` array to serialize.
11
+ * @returns The JSON string, or `null` when empty or unserializable.
12
+ * @example
13
+ * stringifyConsoleLogArgs(['a', 1]); // '["a",1]'
14
+ * stringifyConsoleLogArgs([]); // null
15
+ */
16
+ export function stringifyConsoleLogArgs(args) {
17
+ if (args.length === 0) {
18
+ return null;
19
+ }
20
+ try {
21
+ return JSON.stringify(args);
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The content fields of a `ConsoleLogEntry` that determine its identity in
3
+ * `console_log_items` — everything except `pageUrl` / `ts`, which are
4
+ * per-occurrence rather than per-content (see `page_console_logs`).
5
+ */
6
+ export interface ConsoleLogContent {
7
+ type: string;
8
+ text: string;
9
+ /**
10
+ * Pre-stringified `args` (via `stringifyConsoleLogArgs`), or `null` for
11
+ * an empty/unserializable args array. Callers compute this once and
12
+ * reuse it both for hashing here and for the `json_refs` storage
13
+ * decision, rather than calling `stringifyConsoleLogArgs` on the same
14
+ * `args` array twice.
15
+ */
16
+ argsJson: string | null;
17
+ location?: {
18
+ url?: string;
19
+ lineNumber?: number;
20
+ columnNumber?: number;
21
+ };
22
+ stack?: string;
23
+ }
24
+ /**
25
+ * The resolved ref ids and scalar fields that make up one
26
+ * `console_log_items` row, computed by the caller before the upsert runs.
27
+ */
28
+ export interface ConsoleLogItemRow {
29
+ hash: Buffer;
30
+ type: string;
31
+ /** `null` when `text` is the empty string — `text_refs` never stores `''`. */
32
+ textId: number | null;
33
+ argsJsonId: number | null;
34
+ locUrlId: number | null;
35
+ locLine: number | null;
36
+ locColumn: number | null;
37
+ stackTextId: number | null;
38
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ import type { ConsoleLogItemRow } from './types.js';
2
+ import type { WriteRefCaches } from '../_shared/types.js';
3
+ import type { Knex } from 'knex';
4
+ /**
5
+ * Resolves the `console_log_items.id` for one console log entry's content,
6
+ * inserting the dictionary row when the hash is not yet known.
7
+ *
8
+ * Same no-op-update-for-`RETURNING` idiom as `upsertUrlRef` / `upsertJsonRef`:
9
+ * `INSERT ... ON CONFLICT(hash) DO UPDATE SET hash = hash RETURNING id`
10
+ * yields the existing row's id on a hash collision without a separate
11
+ * SELECT round trip, and (unlike `INSERT OR IGNORE ... RETURNING`) still
12
+ * returns a row when the insert itself is the no-op.
13
+ * @param qb - Knex instance or transaction connected to the archive DB.
14
+ * @param caches - The connection's write-side id caches; mutated in place.
15
+ * @param row - The content hash plus every resolved ref id / scalar column.
16
+ * @returns The `console_log_items.id` of the existing or newly inserted row.
17
+ * @example
18
+ * const id = await upsertConsoleLogItem(trx, caches, {
19
+ * hash: computeConsoleLogHash(entry),
20
+ * type: entry.type,
21
+ * textId,
22
+ * argsJsonId: null,
23
+ * locUrlId: null,
24
+ * locLine: null,
25
+ * locColumn: null,
26
+ * stackTextId: null,
27
+ * });
28
+ */
29
+ export declare function upsertConsoleLogItem(qb: Knex | Knex.Transaction, caches: WriteRefCaches, row: ConsoleLogItemRow): Promise<number>;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Resolves the `console_log_items.id` for one console log entry's content,
3
+ * inserting the dictionary row when the hash is not yet known.
4
+ *
5
+ * Same no-op-update-for-`RETURNING` idiom as `upsertUrlRef` / `upsertJsonRef`:
6
+ * `INSERT ... ON CONFLICT(hash) DO UPDATE SET hash = hash RETURNING id`
7
+ * yields the existing row's id on a hash collision without a separate
8
+ * SELECT round trip, and (unlike `INSERT OR IGNORE ... RETURNING`) still
9
+ * returns a row when the insert itself is the no-op.
10
+ * @param qb - Knex instance or transaction connected to the archive DB.
11
+ * @param caches - The connection's write-side id caches; mutated in place.
12
+ * @param row - The content hash plus every resolved ref id / scalar column.
13
+ * @returns The `console_log_items.id` of the existing or newly inserted row.
14
+ * @example
15
+ * const id = await upsertConsoleLogItem(trx, caches, {
16
+ * hash: computeConsoleLogHash(entry),
17
+ * type: entry.type,
18
+ * textId,
19
+ * argsJsonId: null,
20
+ * locUrlId: null,
21
+ * locLine: null,
22
+ * locColumn: null,
23
+ * stackTextId: null,
24
+ * });
25
+ */
26
+ export async function upsertConsoleLogItem(qb, caches, row) {
27
+ const hex = row.hash.toString('hex');
28
+ const cached = caches.consoleLogIds.get(hex);
29
+ if (cached !== undefined) {
30
+ return cached;
31
+ }
32
+ const rows = await qb.raw(`INSERT INTO console_log_items
33
+ (hash, type, text_id, args_json_id, loc_url_id, loc_line, loc_column, stack_text_id)
34
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
35
+ ON CONFLICT(hash) DO UPDATE SET hash = hash
36
+ RETURNING id`, [
37
+ row.hash,
38
+ row.type,
39
+ row.textId,
40
+ row.argsJsonId,
41
+ row.locUrlId,
42
+ row.locLine,
43
+ row.locColumn,
44
+ row.stackTextId,
45
+ ]);
46
+ const first = rows[0];
47
+ if (first === undefined) {
48
+ throw new Error('upsertConsoleLogItem: RETURNING yielded no row');
49
+ }
50
+ caches.consoleLogIds.set(hex, first.id);
51
+ return first.id;
52
+ }
@@ -16,6 +16,17 @@ import type { Knex } from 'knex';
16
16
  *
17
17
  * Returns `[]` on legacy archives that pre-date the `crawl_errors`
18
18
  * table — the `hasTable` guard keeps the call non-destructive.
19
+ *
20
+ * **Outage exclusion**: a host whose latest DNS error's `createdAt` falls
21
+ * inside a recorded `network_outages` window is dropped from the result
22
+ * regardless of the other checks. A host with no prior success this
23
+ * archive has ever seen (the case this whole exclusion-bag scheme cannot
24
+ * otherwise catch) would, without this check, get preload-seeded into
25
+ * `dnsBurnedHostCache` and short-circuit on EVERY subsequent session
26
+ * forever — the exact "damage 3" this function's caller
27
+ * (`#preloadDnsBurnedHostCache`) exists to prevent for hosts genuinely
28
+ * proven dead, not to inflict on hosts merely unlucky enough to be first
29
+ * contacted during an operator-side network blip.
19
30
  * @param knex - Knex query builder connected to the archive DB.
20
31
  * @returns Lower-cased hostnames safe to short-circuit.
21
32
  */
@@ -1,4 +1,6 @@
1
1
  import { classifyErrorKind } from '../../../classify-error-kind.js';
2
+ import { isWithinOutageWindow } from '../../../is-within-outage-window.js';
3
+ import { listNetworkOutages } from '../outages/list-network-outages.js';
2
4
  /**
3
5
  * Hostnames whose `crawl_errors` history is consistently DNS failures and
4
6
  * for which no recent 2xx-3xx page or resource is recorded — i.e. hosts
@@ -16,6 +18,17 @@ import { classifyErrorKind } from '../../../classify-error-kind.js';
16
18
  *
17
19
  * Returns `[]` on legacy archives that pre-date the `crawl_errors`
18
20
  * table — the `hasTable` guard keeps the call non-destructive.
21
+ *
22
+ * **Outage exclusion**: a host whose latest DNS error's `createdAt` falls
23
+ * inside a recorded `network_outages` window is dropped from the result
24
+ * regardless of the other checks. A host with no prior success this
25
+ * archive has ever seen (the case this whole exclusion-bag scheme cannot
26
+ * otherwise catch) would, without this check, get preload-seeded into
27
+ * `dnsBurnedHostCache` and short-circuit on EVERY subsequent session
28
+ * forever — the exact "damage 3" this function's caller
29
+ * (`#preloadDnsBurnedHostCache`) exists to prevent for hosts genuinely
30
+ * proven dead, not to inflict on hosts merely unlucky enough to be first
31
+ * contacted during an operator-side network blip.
19
32
  * @param knex - Knex query builder connected to the archive DB.
20
33
  * @returns Lower-cased hostnames safe to short-circuit.
21
34
  */
@@ -118,11 +131,12 @@ export async function listDnsBurnedHostCandidates(knex) {
118
131
  }
119
132
  resourceOkHosts.add(host);
120
133
  }
134
+ const outageWindows = await listNetworkOutages(knex);
121
135
  // A candidate host is burned only if neither pages nor resources hold a
122
- // 2xx-3xx for it, AND its latest 2xx page (if any) is not newer than
123
- // the latest DNS error. The third check guards against re-burning a
124
- // host that recovered between the last DNS failure and the most recent
125
- // crawl.
136
+ // 2xx-3xx for it, its latest 2xx page (if any) is not newer than the
137
+ // latest DNS error (guards against re-burning a host that recovered
138
+ // between the last DNS failure and the most recent crawl), AND that
139
+ // latest DNS error did not happen during a recorded network outage.
126
140
  const burned = [];
127
141
  for (const [host, latestErrorAt] of candidateLatestErrorAt) {
128
142
  if (pageOkHosts.has(host)) {
@@ -135,6 +149,9 @@ export async function listDnsBurnedHostCandidates(knex) {
135
149
  if (typeof latestOkAt === 'number' && latestOkAt > latestErrorAt) {
136
150
  continue;
137
151
  }
152
+ if (isWithinOutageWindow(latestErrorAt, outageWindows)) {
153
+ continue;
154
+ }
138
155
  burned.push(host);
139
156
  }
140
157
  return burned;
@@ -27,6 +27,7 @@ export async function recordInventoryRun(knex, meta) {
27
27
  new_pages: meta.new_pages ?? null,
28
28
  new_resources: meta.new_resources ?? null,
29
29
  scope_skipped: meta.scope_skipped ?? null,
30
+ invalid_skipped: meta.invalid_skipped ?? null,
30
31
  notes: meta.notes ?? null,
31
32
  })
32
33
  .returning('id');
@@ -1,17 +1,32 @@
1
1
  import type { Knex } from 'knex';
2
2
  /**
3
3
  * Initializes the database schema if tables do not exist, then runs the
4
- * one remaining lightweight migration (`info.roots`).
4
+ * remaining lightweight migrations (`info.roots`, `info.mainContentSelector`,
5
+ * `page_meta.main_content_*`, `inventory_runs.invalid_skipped`).
5
6
  *
6
- * There is deliberately no per-table lazy-migration chain here:
7
- * `assertCompatibleVersion` (called below, before any schema work)
8
- * rejects every archive older than the current format, so a connection
9
- * that reaches `initSchema` is either brand new (initSchema provisions
10
- * the full schema) or was produced by `scripts/migrate-to-0.13.mjs`
11
- * (which guarantees the full table set before it repacks). A
12
- * `hasTable`-guarded catch-up migration could therefore never fire —
13
- * schema catch-up for old archives is the migration script's job, not
14
- * the open path's.
7
+ * There is deliberately no per-table *table-creation* migration chain here:
8
+ * `assertCompatibleVersion` (called below, before any schema work) rejects
9
+ * every archive older than the current format, so a connection that reaches
10
+ * `initSchema` is either brand new (`initSchema` provisions the full schema)
11
+ * or was produced by `scripts/migrate-to-0.13.mjs` (which guarantees the
12
+ * full table set before it repacks) — and `initSchema` itself re-runs
13
+ * `createEntityTables` / `createAdjunctTables` unconditionally on every
14
+ * open, self-healing any *missing table* via their internal
15
+ * `IF NOT EXISTS` / `hasTable` guards. What that self-healing cannot do is
16
+ * retrofit a *new column* onto an entity table that already exists —
17
+ * `CREATE TABLE IF NOT EXISTS` is a no-op once the table is present. Column
18
+ * additions to an existing 0.13 table are therefore the one case that still
19
+ * needs an explicit `hasColumn`-guarded `ALTER TABLE` here (`migrateInfoRoots`,
20
+ * `migrateMainContentsColumns`, `migratePageMetaBodyHash`,
21
+ * `migratePageMetaConsoleErrorCount`, `migrateContentItemsAliasOfId`,
22
+ * `migrateInventoryRunsInvalidSkipped`) rather than a DDL-string change alone.
23
+ *
24
+ * `closeStaleOpenNetworkOutages` is not a schema migration (no columns
25
+ * change) but belongs at this same boot phase for the same reason the
26
+ * others do: it must run before ANY reader (`resetFailedPages`,
27
+ * `listDnsBurnedHostCandidates`, …) can observe a `network_outages` row
28
+ * left `ended_at = NULL` by a crawl process that was killed mid-outage in a
29
+ * prior session.
15
30
  *
16
31
  * In read-only mode schema init + migration are SKIPPED so the same DB
17
32
  * can be opened safely by a viewer attached to a live (or interrupted)
@@ -1,19 +1,41 @@
1
1
  import { applyConnectionPragmas, initSchema } from '../../init-schema.js';
2
2
  import { assertCompatibleVersion } from '../../meta/assert-compatible-version.js';
3
+ import { migrateContentItemsAliasOfId } from '../../migrate-content-items-alias-of-id.js';
4
+ import { migrateInfoMainContentSelector } from '../../migrate-info-main-content-selector.js';
3
5
  import { migrateInfoRoots } from '../../migrate-info-roots.js';
6
+ import { migrateInventoryRunsInvalidSkipped } from '../../migrate-inventory-runs-invalid-skipped.js';
7
+ import { migrateMainContentsColumns } from '../../migrate-main-contents-columns.js';
8
+ import { migratePageMetaBodyHash } from '../../migrate-page-meta-body-hash.js';
9
+ import { migratePageMetaConsoleErrorCount } from '../../migrate-page-meta-console-error-count.js';
10
+ import { closeStaleOpenNetworkOutages } from '../outages/close-stale-open-network-outages.js';
4
11
  /**
5
12
  * Initializes the database schema if tables do not exist, then runs the
6
- * one remaining lightweight migration (`info.roots`).
13
+ * remaining lightweight migrations (`info.roots`, `info.mainContentSelector`,
14
+ * `page_meta.main_content_*`, `inventory_runs.invalid_skipped`).
7
15
  *
8
- * There is deliberately no per-table lazy-migration chain here:
9
- * `assertCompatibleVersion` (called below, before any schema work)
10
- * rejects every archive older than the current format, so a connection
11
- * that reaches `initSchema` is either brand new (initSchema provisions
12
- * the full schema) or was produced by `scripts/migrate-to-0.13.mjs`
13
- * (which guarantees the full table set before it repacks). A
14
- * `hasTable`-guarded catch-up migration could therefore never fire —
15
- * schema catch-up for old archives is the migration script's job, not
16
- * the open path's.
16
+ * There is deliberately no per-table *table-creation* migration chain here:
17
+ * `assertCompatibleVersion` (called below, before any schema work) rejects
18
+ * every archive older than the current format, so a connection that reaches
19
+ * `initSchema` is either brand new (`initSchema` provisions the full schema)
20
+ * or was produced by `scripts/migrate-to-0.13.mjs` (which guarantees the
21
+ * full table set before it repacks) — and `initSchema` itself re-runs
22
+ * `createEntityTables` / `createAdjunctTables` unconditionally on every
23
+ * open, self-healing any *missing table* via their internal
24
+ * `IF NOT EXISTS` / `hasTable` guards. What that self-healing cannot do is
25
+ * retrofit a *new column* onto an entity table that already exists —
26
+ * `CREATE TABLE IF NOT EXISTS` is a no-op once the table is present. Column
27
+ * additions to an existing 0.13 table are therefore the one case that still
28
+ * needs an explicit `hasColumn`-guarded `ALTER TABLE` here (`migrateInfoRoots`,
29
+ * `migrateMainContentsColumns`, `migratePageMetaBodyHash`,
30
+ * `migratePageMetaConsoleErrorCount`, `migrateContentItemsAliasOfId`,
31
+ * `migrateInventoryRunsInvalidSkipped`) rather than a DDL-string change alone.
32
+ *
33
+ * `closeStaleOpenNetworkOutages` is not a schema migration (no columns
34
+ * change) but belongs at this same boot phase for the same reason the
35
+ * others do: it must run before ANY reader (`resetFailedPages`,
36
+ * `listDnsBurnedHostCandidates`, …) can observe a `network_outages` row
37
+ * left `ended_at = NULL` by a crawl process that was killed mid-outage in a
38
+ * prior session.
17
39
  *
18
40
  * In read-only mode schema init + migration are SKIPPED so the same DB
19
41
  * can be opened safely by a viewer attached to a live (or interrupted)
@@ -39,4 +61,11 @@ export async function init(knex, readOnly) {
39
61
  }
40
62
  await initSchema(knex);
41
63
  await migrateInfoRoots(knex);
64
+ await migrateInfoMainContentSelector(knex);
65
+ await migrateMainContentsColumns(knex);
66
+ await migratePageMetaBodyHash(knex);
67
+ await migratePageMetaConsoleErrorCount(knex);
68
+ await migrateContentItemsAliasOfId(knex);
69
+ await migrateInventoryRunsInvalidSkipped(knex);
70
+ await closeStaleOpenNetworkOutages(knex);
42
71
  }
@@ -0,0 +1,11 @@
1
+ import type { MainContentAudioRow } from '../../meta/types.js';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Retrieves all `page_main_content_audios` rows for the given page id, in
5
+ * DOM traversal order.
6
+ *
7
+ * Read-side counterpart to `insertAudios`.
8
+ * @param knex - Knex query builder connected to the archive DB.
9
+ * @param pageId
10
+ */
11
+ export declare function getAudiosOfPage(knex: Knex, pageId: number): Promise<MainContentAudioRow[]>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Retrieves all `page_main_content_audios` rows for the given page id, in
3
+ * DOM traversal order.
4
+ *
5
+ * Read-side counterpart to `insertAudios`.
6
+ * @param knex - Knex query builder connected to the archive DB.
7
+ * @param pageId
8
+ */
9
+ export async function getAudiosOfPage(knex, pageId) {
10
+ return knex
11
+ .select('id', 'pageId', 'order', 'src')
12
+ .from('page_main_content_audios')
13
+ .where('pageId', pageId)
14
+ .orderBy('order', 'asc');
15
+ }
@@ -0,0 +1,11 @@
1
+ import type { MainContentButtonRow } from '../../meta/types.js';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Retrieves all `page_main_content_buttons` rows for the given page id, in
5
+ * DOM traversal order.
6
+ *
7
+ * Read-side counterpart to `insertButtons`.
8
+ * @param knex - Knex query builder connected to the archive DB.
9
+ * @param pageId
10
+ */
11
+ export declare function getButtonsOfPage(knex: Knex, pageId: number): Promise<MainContentButtonRow[]>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Retrieves all `page_main_content_buttons` rows for the given page id, in
3
+ * DOM traversal order.
4
+ *
5
+ * Read-side counterpart to `insertButtons`.
6
+ * @param knex - Knex query builder connected to the archive DB.
7
+ * @param pageId
8
+ */
9
+ export async function getButtonsOfPage(knex, pageId) {
10
+ return knex
11
+ .select('id', 'pageId', 'order', 'nodeName', 'role', 'type', 'text', 'disabled')
12
+ .from('page_main_content_buttons')
13
+ .where('pageId', pageId)
14
+ .orderBy('order', 'asc');
15
+ }
@@ -0,0 +1,11 @@
1
+ import type { MainContentCanvasRow } from '../../meta/types.js';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Retrieves all `page_main_content_canvases` rows for the given page id, in
5
+ * DOM traversal order.
6
+ *
7
+ * Read-side counterpart to `insertCanvases`.
8
+ * @param knex - Knex query builder connected to the archive DB.
9
+ * @param pageId
10
+ */
11
+ export declare function getCanvasesOfPage(knex: Knex, pageId: number): Promise<MainContentCanvasRow[]>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Retrieves all `page_main_content_canvases` rows for the given page id, in
3
+ * DOM traversal order.
4
+ *
5
+ * Read-side counterpart to `insertCanvases`.
6
+ * @param knex - Knex query builder connected to the archive DB.
7
+ * @param pageId
8
+ */
9
+ export async function getCanvasesOfPage(knex, pageId) {
10
+ return knex
11
+ .select('id', 'pageId', 'order', 'width', 'height')
12
+ .from('page_main_content_canvases')
13
+ .where('pageId', pageId)
14
+ .orderBy('order', 'asc');
15
+ }
@@ -0,0 +1,11 @@
1
+ import type { MainContentHeadingRow } from '../../meta/types.js';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Retrieves all `page_main_content_headings` rows for the given page id, in
5
+ * DOM traversal order.
6
+ *
7
+ * Read-side counterpart to `insertHeadings`.
8
+ * @param knex - Knex query builder connected to the archive DB.
9
+ * @param pageId
10
+ */
11
+ export declare function getHeadingsOfPage(knex: Knex, pageId: number): Promise<MainContentHeadingRow[]>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Retrieves all `page_main_content_headings` rows for the given page id, in
3
+ * DOM traversal order.
4
+ *
5
+ * Read-side counterpart to `insertHeadings`.
6
+ * @param knex - Knex query builder connected to the archive DB.
7
+ * @param pageId
8
+ */
9
+ export async function getHeadingsOfPage(knex, pageId) {
10
+ return knex
11
+ .select('id', 'pageId', 'order', 'text', 'level')
12
+ .from('page_main_content_headings')
13
+ .where('pageId', pageId)
14
+ .orderBy('order', 'asc');
15
+ }
@@ -0,0 +1,11 @@
1
+ import type { MainContentIframeRow } from '../../meta/types.js';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Retrieves all `page_main_content_iframes` rows for the given page id, in
5
+ * DOM traversal order.
6
+ *
7
+ * Read-side counterpart to `insertIframes`.
8
+ * @param knex - Knex query builder connected to the archive DB.
9
+ * @param pageId
10
+ */
11
+ export declare function getIframesOfPage(knex: Knex, pageId: number): Promise<MainContentIframeRow[]>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Retrieves all `page_main_content_iframes` rows for the given page id, in
3
+ * DOM traversal order.
4
+ *
5
+ * Read-side counterpart to `insertIframes`.
6
+ * @param knex - Knex query builder connected to the archive DB.
7
+ * @param pageId
8
+ */
9
+ export async function getIframesOfPage(knex, pageId) {
10
+ return knex
11
+ .select('id', 'pageId', 'order', 'src', 'title', 'width', 'height')
12
+ .from('page_main_content_iframes')
13
+ .where('pageId', pageId)
14
+ .orderBy('order', 'asc');
15
+ }
@@ -0,0 +1,11 @@
1
+ import type { MainContentImageRow } from '../../meta/types.js';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Retrieves all `page_main_content_images` rows for the given page id, in
5
+ * DOM traversal order.
6
+ *
7
+ * Read-side counterpart to `insertMainContentImages`.
8
+ * @param knex - Knex query builder connected to the archive DB.
9
+ * @param pageId
10
+ */
11
+ export declare function getMainContentImagesOfPage(knex: Knex, pageId: number): Promise<MainContentImageRow[]>;