@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
@@ -67,6 +67,11 @@ export interface Config extends Required<Pick<ParseURLOptions, 'disableQueries'>
67
67
  userAgent: string;
68
68
  /** Whether robots.txt restrictions were ignored during crawling. */
69
69
  ignoreRobots: boolean;
70
+ /**
71
+ * CSS selector overriding beholder's automatic main-content-region
72
+ * detection, or `null`/omitted to use the automatic heuristic.
73
+ */
74
+ mainContentSelector?: string | null;
70
75
  }
71
76
  /**
72
77
  * Provenance of a page or resource row — which crawler channel originally
@@ -123,9 +128,9 @@ export interface InventoryRunMeta {
123
128
  ran_at: string;
124
129
  /** Human-readable identifier (e.g. `'prod-2026-06-21'`). `null` when the caller did not supply one. */
125
130
  list_label?: string | null;
126
- /** SHA-256 hex digest of the source file. `null` if hashing failed (e.g. file vanished mid-run). */
131
+ /** SHA-256 hex digest of the source file. `null` for programmatic callers that built the URL list in-memory (no source file to hash). */
127
132
  source_file_sha256?: string | null;
128
- /** Number of non-empty lines in the input list (= URL count before scope filtering). */
133
+ /** Number of valid URLs in the input list, after the CLI has warned-and-dropped unparseable-URL lines but before scope filtering. */
129
134
  total_lines?: number | null;
130
135
  /** Number of new HTML seeds inserted as `content_items` rows by this run. */
131
136
  new_pages?: number | null;
@@ -133,9 +138,54 @@ export interface InventoryRunMeta {
133
138
  new_resources?: number | null;
134
139
  /** Number of input URLs dropped because they fell outside the archived scope. */
135
140
  scope_skipped?: number | null;
141
+ /** Number of source-file lines dropped by the CLI for failing URL validation, before this row's `total_lines` was counted. `null` for programmatic callers that built the URL list in-memory (no source file to have invalid lines). */
142
+ invalid_skipped?: number | null;
136
143
  /** Free-form text for backfill annotations or operator notes. */
137
144
  notes?: string | null;
138
145
  }
146
+ /**
147
+ * A row in `network_outages` — one detected operator-network outage.
148
+ *
149
+ * Append-only except `ended_at`: it is written once, `NULL`, when the
150
+ * outage is first detected, and updated exactly once when a recovery probe
151
+ * succeeds. A row can also be left `ended_at = NULL` forever if the crawl
152
+ * process is killed mid-outage — readers must resolve this via a clamp
153
+ * (see `is-within-outage-window.ts` and the `db-ops/outages/` writer that
154
+ * closes stale-open rows on the next writer session), never by treating
155
+ * `NULL` as an unbounded window.
156
+ */
157
+ export interface NetworkOutageRow {
158
+ id: number;
159
+ /** Epoch ms, backdated to the earliest error still inside the detector's sliding window at trigger time. */
160
+ started_at: number;
161
+ /** Epoch ms the sliding window actually crossed both thresholds. */
162
+ detected_at: number;
163
+ /** Epoch ms the recovery probe first succeeded, or `null` while still open / if the session crashed before recovery. */
164
+ ended_at: number | null;
165
+ /** Hostname the recovery probe targeted, or `null` if none was available (see `choose-probe-host.ts`). */
166
+ probe_host: string | null;
167
+ /** Error count in the detector's window at trigger time. */
168
+ trigger_error_count: number;
169
+ /** Distinct host count in the detector's window at trigger time. */
170
+ trigger_host_count: number;
171
+ }
172
+ /**
173
+ * Fields required to record a newly-detected outage via
174
+ * `Database.insertNetworkOutage`. camelCase (unlike {@link NetworkOutageRow}
175
+ * / {@link InventoryRunMeta}) because callers build this directly from
176
+ * `NetworkOutageDetector`'s camelCase `OutageSuspect` plus a probe host —
177
+ * the db-op does the camelCase → snake_case column mapping on write.
178
+ */
179
+ export interface InsertNetworkOutageParams {
180
+ /** Backdated to the earliest error still inside the detector's window at trigger time. */
181
+ startedAt: number;
182
+ /** When the sliding window actually crossed both thresholds. */
183
+ detectedAt: number;
184
+ /** Hostname the recovery probe will target, or `null` if none was available. */
185
+ probeHost: string | null;
186
+ triggerErrorCount: number;
187
+ triggerHostCount: number;
188
+ }
139
189
  /**
140
190
  * Filter type for querying pages from the database.
141
191
  *
@@ -292,6 +342,40 @@ export interface DB_Page {
292
342
  jsonld_count: number | null;
293
343
  /** Sorted unique provider names, comma-separated (empty string when none). */
294
344
  tags_providers_csv: string | null;
345
+ /** Detected main-content element's `nodeName` (e.g. `'MAIN'`), or null. */
346
+ main_content_node_name: string | null;
347
+ /** Detected main-content element's `id`, or null. */
348
+ main_content_id: string | null;
349
+ /** Detected main-content element's `role` attribute, or null. */
350
+ main_content_role: string | null;
351
+ /** Diagnostic tag+id+class selector for the detected element, or null. */
352
+ main_content_selector: string | null;
353
+ /** JSON-encoded array of the detected element's CSS classes, or null. */
354
+ main_content_class_list: string | null;
355
+ /** Character count of the main region's text content, or null. */
356
+ main_content_word_count: number | null;
357
+ /** Character count of `document.body`'s text content, or null. */
358
+ main_content_body_word_count: number | null;
359
+ /** Number of headings within the main region, or null. */
360
+ main_content_heading_count: number | null;
361
+ /** Number of images within the main region, or null. */
362
+ main_content_image_count: number | null;
363
+ /** Number of tables within the main region, or null. */
364
+ main_content_table_count: number | null;
365
+ /** Number of button-like elements within the main region, or null. */
366
+ main_content_button_count: number | null;
367
+ /** Number of iframes within the main region, or null. */
368
+ main_content_iframe_count: number | null;
369
+ /** Number of videos within the main region, or null. */
370
+ main_content_video_count: number | null;
371
+ /** Number of audios within the main region, or null. */
372
+ main_content_audio_count: number | null;
373
+ /** Number of canvases within the main region, or null. */
374
+ main_content_canvas_count: number | null;
375
+ /** `document.body.scrollHeight` at the desktop-compact preset, or null. */
376
+ scroll_height_desktop: number | null;
377
+ /** `document.body.scrollHeight` at the mobile-small preset, or null. */
378
+ scroll_height_mobile: number | null;
295
379
  /** JSON-serialised nested Meta sub-objects not flattened above. */
296
380
  meta_extras: string | null;
297
381
  /** JSON-serialized network logs captured during scraping, or null if not collected. */
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Computes the Tier A alias key for `url`: two URLs share a Tier A key when
3
+ * they differ only by scheme (`http` vs `https` are folded together),
4
+ * host letter-casing, or a `/index.{ext}` path suffix — differences that
5
+ * are safe to treat as "the same resource" from the URL string alone, with
6
+ * no need to compare rendered content.
7
+ *
8
+ * This is a canonical-key function, not a pairwise comparator: grouping
9
+ * rows by exact string equality of this function's output is definitionally
10
+ * an equivalence relation (reflexive, symmetric, transitive), unlike
11
+ * `compareUrlSortKeys`'s pairwise "roughly equal" comparison (see
12
+ * ARCHITECTURE.md's "URL natural-sort comparator は推移律を保証しない" —
13
+ * that non-transitivity problem is inherent to pairwise comparators, and
14
+ * does not apply to a canonical-key partition like this one).
15
+ *
16
+ * Deliberately does not touch the query string or fragment: a query-string
17
+ * difference is not addressed by this feature (out of scope — see the
18
+ * `body_hash` masking, a separate content-hash-based mechanism, for
19
+ * detecting meaningless-parameter duplicates), and fragments are
20
+ * client-side only and never affect server-rendered content.
21
+ * @param url - The URL string to compute a key for.
22
+ * @returns The Tier A key, or `null` if `url` is not a parseable http(s) URL.
23
+ * @example
24
+ * ```ts
25
+ * computeTierAAliasKey('http://Example.com/about/index.html');
26
+ * // 'example.com/about/'
27
+ * computeTierAAliasKey('https://example.com/about/');
28
+ * // 'example.com/about/' -- same key: scheme and /index.html are folded
29
+ * ```
30
+ */
31
+ export declare function computeTierAAliasKey(url: string): string | null;
@@ -0,0 +1,36 @@
1
+ import { formatAliasKey } from './format-alias-key.js';
2
+ import { parseAliasKeyParts } from './parse-alias-key-parts.js';
3
+ /**
4
+ * Computes the Tier A alias key for `url`: two URLs share a Tier A key when
5
+ * they differ only by scheme (`http` vs `https` are folded together),
6
+ * host letter-casing, or a `/index.{ext}` path suffix — differences that
7
+ * are safe to treat as "the same resource" from the URL string alone, with
8
+ * no need to compare rendered content.
9
+ *
10
+ * This is a canonical-key function, not a pairwise comparator: grouping
11
+ * rows by exact string equality of this function's output is definitionally
12
+ * an equivalence relation (reflexive, symmetric, transitive), unlike
13
+ * `compareUrlSortKeys`'s pairwise "roughly equal" comparison (see
14
+ * ARCHITECTURE.md's "URL natural-sort comparator は推移律を保証しない" —
15
+ * that non-transitivity problem is inherent to pairwise comparators, and
16
+ * does not apply to a canonical-key partition like this one).
17
+ *
18
+ * Deliberately does not touch the query string or fragment: a query-string
19
+ * difference is not addressed by this feature (out of scope — see the
20
+ * `body_hash` masking, a separate content-hash-based mechanism, for
21
+ * detecting meaningless-parameter duplicates), and fragments are
22
+ * client-side only and never affect server-rendered content.
23
+ * @param url - The URL string to compute a key for.
24
+ * @returns The Tier A key, or `null` if `url` is not a parseable http(s) URL.
25
+ * @example
26
+ * ```ts
27
+ * computeTierAAliasKey('http://Example.com/about/index.html');
28
+ * // 'example.com/about/'
29
+ * computeTierAAliasKey('https://example.com/about/');
30
+ * // 'example.com/about/' -- same key: scheme and /index.html are folded
31
+ * ```
32
+ */
33
+ export function computeTierAAliasKey(url) {
34
+ const parts = parseAliasKeyParts(url);
35
+ return parts ? formatAliasKey(parts) : null;
36
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Computes the Tier B alias key for `url`: like `computeTierAAliasKey`,
3
+ * but additionally strips one trailing `/` from the path (the bare root
4
+ * path `/` is left alone). Two URLs sharing a Tier B key differ only by
5
+ * trailing-slash presence — a resource-identity signal considerably weaker
6
+ * than Tier A's (a web server can legitimately serve different content at
7
+ * `/foo` and `/foo/`), so callers must additionally require a matching
8
+ * `page_meta.body_hash` before treating a Tier B match as the same page.
9
+ * @param url - The URL string to compute a key for.
10
+ * @returns The Tier B key, or `null` if `url` is not a parseable http(s) URL.
11
+ * @example
12
+ * ```ts
13
+ * computeTierBAliasKey('https://example.com/foo');
14
+ * computeTierBAliasKey('https://example.com/foo/');
15
+ * // both: 'example.com/foo' -- same key, but the caller must still confirm
16
+ * // a matching body_hash before merging these as the same page.
17
+ * ```
18
+ */
19
+ export declare function computeTierBAliasKey(url: string): string | null;
@@ -0,0 +1,30 @@
1
+ import { formatAliasKey } from './format-alias-key.js';
2
+ import { parseAliasKeyParts } from './parse-alias-key-parts.js';
3
+ /**
4
+ * Computes the Tier B alias key for `url`: like `computeTierAAliasKey`,
5
+ * but additionally strips one trailing `/` from the path (the bare root
6
+ * path `/` is left alone). Two URLs sharing a Tier B key differ only by
7
+ * trailing-slash presence — a resource-identity signal considerably weaker
8
+ * than Tier A's (a web server can legitimately serve different content at
9
+ * `/foo` and `/foo/`), so callers must additionally require a matching
10
+ * `page_meta.body_hash` before treating a Tier B match as the same page.
11
+ * @param url - The URL string to compute a key for.
12
+ * @returns The Tier B key, or `null` if `url` is not a parseable http(s) URL.
13
+ * @example
14
+ * ```ts
15
+ * computeTierBAliasKey('https://example.com/foo');
16
+ * computeTierBAliasKey('https://example.com/foo/');
17
+ * // both: 'example.com/foo' -- same key, but the caller must still confirm
18
+ * // a matching body_hash before merging these as the same page.
19
+ * ```
20
+ */
21
+ export function computeTierBAliasKey(url) {
22
+ const parts = parseAliasKeyParts(url);
23
+ if (!parts) {
24
+ return null;
25
+ }
26
+ const path = parts.path.length > 1 && parts.path.endsWith('/')
27
+ ? parts.path.slice(0, -1)
28
+ : parts.path;
29
+ return formatAliasKey({ ...parts, path });
30
+ }
@@ -0,0 +1,13 @@
1
+ import type { AliasKeyParts } from './types.js';
2
+ /**
3
+ * Formats parsed alias-key parts into a single string key. `host`/`port`
4
+ * never contain `/` or `?`, and `path`/`search` always start with those
5
+ * characters respectively (or `search` is empty), so concatenation without
6
+ * an explicit delimiter is unambiguous.
7
+ * @param parts - The parts to format.
8
+ * @returns The formatted key string.
9
+ * @example
10
+ * formatAliasKey({ host: 'example.com', port: '', path: '/about/', search: '' });
11
+ * // 'example.com/about/'
12
+ */
13
+ export declare function formatAliasKey(parts: AliasKeyParts): string;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Formats parsed alias-key parts into a single string key. `host`/`port`
3
+ * never contain `/` or `?`, and `path`/`search` always start with those
4
+ * characters respectively (or `search` is empty), so concatenation without
5
+ * an explicit delimiter is unambiguous.
6
+ * @param parts - The parts to format.
7
+ * @returns The formatted key string.
8
+ * @example
9
+ * formatAliasKey({ host: 'example.com', port: '', path: '/about/', search: '' });
10
+ * // 'example.com/about/'
11
+ */
12
+ export function formatAliasKey(parts) {
13
+ return `${parts.host}${parts.port ? `:${parts.port}` : ''}${parts.path}${parts.search}`;
14
+ }
@@ -0,0 +1,15 @@
1
+ import type { AliasKeyParts } from './types.js';
2
+ /**
3
+ * Parses `url` and extracts the components Tier A / Tier B keys are built
4
+ * from. Returns `null` for anything that isn't a parseable `http`/`https`
5
+ * URL — such a row is defensively excluded from alias candidacy entirely
6
+ * (a non-http(s) URL should not occur for anything the crawler itself
7
+ * stored, since every `url_refs.url` was already parsed once during
8
+ * crawling).
9
+ * @param url - The URL string to parse.
10
+ * @returns The extracted parts, or `null` if unparseable / not http(s).
11
+ * @example
12
+ * parseAliasKeyParts('https://Example.com/about/index.html');
13
+ * // { host: 'example.com', port: '', path: '/about/', search: '' }
14
+ */
15
+ export declare function parseAliasKeyParts(url: string): AliasKeyParts | null;
@@ -0,0 +1,40 @@
1
+ const INDEX_SUFFIX_PATTERN = /\/index\.\w+$/i;
2
+ /**
3
+ * Parses `url` and extracts the components Tier A / Tier B keys are built
4
+ * from. Returns `null` for anything that isn't a parseable `http`/`https`
5
+ * URL — such a row is defensively excluded from alias candidacy entirely
6
+ * (a non-http(s) URL should not occur for anything the crawler itself
7
+ * stored, since every `url_refs.url` was already parsed once during
8
+ * crawling).
9
+ * @param url - The URL string to parse.
10
+ * @returns The extracted parts, or `null` if unparseable / not http(s).
11
+ * @example
12
+ * parseAliasKeyParts('https://Example.com/about/index.html');
13
+ * // { host: 'example.com', port: '', path: '/about/', search: '' }
14
+ */
15
+ export function parseAliasKeyParts(url) {
16
+ let parsed;
17
+ try {
18
+ parsed = new URL(url);
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
24
+ return null;
25
+ }
26
+ return {
27
+ // WHATWG URL already lowercases `hostname` during parsing; the
28
+ // explicit `.toLowerCase()` documents that intent rather than
29
+ // relying on it silently.
30
+ host: parsed.hostname.toLowerCase(),
31
+ // Empty string when the port is the scheme's default (or
32
+ // unspecified) — this is why `http://example.com/` and
33
+ // `https://example.com/` fold to the same key below (both have
34
+ // `port === ''`) while an explicit non-default port on either
35
+ // scheme is preserved and kept distinct.
36
+ port: parsed.port,
37
+ path: parsed.pathname.replace(INDEX_SUFFIX_PATTERN, '/'),
38
+ search: parsed.search,
39
+ };
40
+ }
@@ -0,0 +1,7 @@
1
+ /** Components a Tier A / Tier B alias key is built from. */
2
+ export interface AliasKeyParts {
3
+ host: string;
4
+ port: string;
5
+ path: string;
6
+ search: string;
7
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Pick the hostname a network-outage probe should target.
3
+ *
4
+ * Prefers a host that has already answered successfully during this crawl
5
+ * session (`Crawler.#successfulHosts` — the same "proven alive" evidence
6
+ * `shouldBurnHost` uses) over a root URL's hostname, and never falls back to
7
+ * a hardcoded external address (e.g. `1.1.1.1`): the probe should depend
8
+ * only on infrastructure the crawl is already touching. `ReadonlySet`
9
+ * iteration order in JS is insertion order, so this deterministically picks
10
+ * the first host to have succeeded this session.
11
+ * @param successfulHosts - Hostnames observed to respond in this session.
12
+ * @param roots - The crawl's configured root URLs (`CrawlerOptions.roots`),
13
+ * used as a fallback before any host has succeeded yet.
14
+ * @returns A probe target hostname, or `null` if neither source yields one
15
+ * (e.g. a fresh session with a malformed/empty roots list and no
16
+ * successes yet — the caller should treat this as "cannot probe").
17
+ * @example
18
+ * ```ts
19
+ * chooseProbeHost(new Set(['a.example']), ['https://b.example/']); // 'a.example'
20
+ * chooseProbeHost(new Set(), ['https://b.example/']); // 'b.example'
21
+ * chooseProbeHost(new Set(), []); // null
22
+ * ```
23
+ */
24
+ export declare function chooseProbeHost(successfulHosts: ReadonlySet<string>, roots: readonly string[]): string | null;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Pick the hostname a network-outage probe should target.
3
+ *
4
+ * Prefers a host that has already answered successfully during this crawl
5
+ * session (`Crawler.#successfulHosts` — the same "proven alive" evidence
6
+ * `shouldBurnHost` uses) over a root URL's hostname, and never falls back to
7
+ * a hardcoded external address (e.g. `1.1.1.1`): the probe should depend
8
+ * only on infrastructure the crawl is already touching. `ReadonlySet`
9
+ * iteration order in JS is insertion order, so this deterministically picks
10
+ * the first host to have succeeded this session.
11
+ * @param successfulHosts - Hostnames observed to respond in this session.
12
+ * @param roots - The crawl's configured root URLs (`CrawlerOptions.roots`),
13
+ * used as a fallback before any host has succeeded yet.
14
+ * @returns A probe target hostname, or `null` if neither source yields one
15
+ * (e.g. a fresh session with a malformed/empty roots list and no
16
+ * successes yet — the caller should treat this as "cannot probe").
17
+ * @example
18
+ * ```ts
19
+ * chooseProbeHost(new Set(['a.example']), ['https://b.example/']); // 'a.example'
20
+ * chooseProbeHost(new Set(), ['https://b.example/']); // 'b.example'
21
+ * chooseProbeHost(new Set(), []); // null
22
+ * ```
23
+ */
24
+ export function chooseProbeHost(successfulHosts, roots) {
25
+ const [firstSuccessfulHost] = successfulHosts;
26
+ if (firstSuccessfulHost !== undefined) {
27
+ return firstSuccessfulHost;
28
+ }
29
+ for (const root of roots) {
30
+ try {
31
+ return new URL(root).hostname;
32
+ }
33
+ catch {
34
+ continue;
35
+ }
36
+ }
37
+ return null;
38
+ }
@@ -1,3 +1,4 @@
1
+ import { dnsBurnedHostBurnTimestamps } from './dns-burned-host-burn-timestamps.js';
1
2
  import { dnsBurnedHostCache } from './dns-burned-host-cache.js';
2
3
  import { dnsBurnedHostShortCircuitCounter } from './dns-burned-host-short-circuit-counter.js';
3
4
  /**
@@ -8,4 +9,5 @@ import { dnsBurnedHostShortCircuitCounter } from './dns-burned-host-short-circui
8
9
  export function clearDnsBurnedHostCache() {
9
10
  dnsBurnedHostCache.clear();
10
11
  dnsBurnedHostShortCircuitCounter.count = 0;
12
+ dnsBurnedHostBurnTimestamps.clear();
11
13
  }