@nitpicker/crawler 0.9.0 → 0.12.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 (148) hide show
  1. package/lib/archive/archive-accessor.d.ts +87 -14
  2. package/lib/archive/archive-accessor.js +162 -36
  3. package/lib/archive/archive.d.ts +262 -24
  4. package/lib/archive/archive.js +397 -86
  5. package/lib/archive/cache/compute-archive-cache-key.d.ts +39 -0
  6. package/lib/archive/cache/compute-archive-cache-key.js +95 -0
  7. package/lib/archive/cache/extract-archive-to-cache.d.ts +43 -0
  8. package/lib/archive/cache/extract-archive-to-cache.js +309 -0
  9. package/lib/archive/cache/get-archive-cache-root.d.ts +20 -0
  10. package/lib/archive/cache/get-archive-cache-root.js +53 -0
  11. package/lib/archive/cache/is-archive-cache-disabled.d.ts +24 -0
  12. package/lib/archive/cache/is-archive-cache-disabled.js +34 -0
  13. package/lib/archive/cache/resolve-archive-cache-dir.d.ts +26 -0
  14. package/lib/archive/cache/resolve-archive-cache-dir.js +32 -0
  15. package/lib/archive/database.d.ts +411 -26
  16. package/lib/archive/database.js +1881 -540
  17. package/lib/archive/derive-lineage-from-parent.d.ts +37 -0
  18. package/lib/archive/derive-lineage-from-parent.js +42 -0
  19. package/lib/archive/filesystem/peek-tar-top-dir.d.ts +28 -0
  20. package/lib/archive/filesystem/peek-tar-top-dir.js +65 -0
  21. package/lib/archive/get-failed-page-messages.d.ts +43 -0
  22. package/lib/archive/get-failed-page-messages.js +131 -0
  23. package/lib/archive/init-schema.d.ts +53 -2
  24. package/lib/archive/init-schema.js +399 -15
  25. package/lib/archive/is-inventory-source.d.ts +21 -0
  26. package/lib/archive/is-inventory-source.js +22 -0
  27. package/lib/archive/meta/assert-compatible-version.d.ts +39 -0
  28. package/lib/archive/meta/assert-compatible-version.js +72 -0
  29. package/lib/archive/meta/classify-jsonld-type.d.ts +23 -0
  30. package/lib/archive/meta/classify-jsonld-type.js +43 -0
  31. package/lib/archive/meta/compare-semver.d.ts +23 -0
  32. package/lib/archive/meta/compare-semver.js +51 -0
  33. package/lib/archive/meta/compute-page-denormalized.d.ts +21 -0
  34. package/lib/archive/meta/compute-page-denormalized.js +35 -0
  35. package/lib/archive/meta/derive-flat-from-meta.d.ts +35 -0
  36. package/lib/archive/meta/derive-flat-from-meta.js +158 -0
  37. package/lib/archive/meta/derive-meta-extras.d.ts +20 -0
  38. package/lib/archive/meta/derive-meta-extras.js +23 -0
  39. package/lib/archive/meta/extract-tags-for-archive.d.ts +18 -0
  40. package/lib/archive/meta/extract-tags-for-archive.js +36 -0
  41. package/lib/archive/meta/summarize-jsonld.d.ts +17 -0
  42. package/lib/archive/meta/summarize-jsonld.js +29 -0
  43. package/lib/archive/meta/summarize-tags.d.ts +16 -0
  44. package/lib/archive/meta/summarize-tags.js +33 -0
  45. package/lib/archive/meta/types.d.ts +207 -0
  46. package/lib/archive/meta/types.js +33 -0
  47. package/lib/archive/migrate-crawl-errors.d.ts +20 -0
  48. package/lib/archive/migrate-crawl-errors.js +38 -0
  49. package/lib/archive/migrate-html-blob-tables.d.ts +24 -0
  50. package/lib/archive/migrate-html-blob-tables.js +53 -0
  51. package/lib/archive/migrate-inventory-runs.d.ts +29 -0
  52. package/lib/archive/migrate-inventory-runs.js +52 -0
  53. package/lib/archive/migrate-page-errors.d.ts +16 -0
  54. package/lib/archive/migrate-page-errors.js +35 -0
  55. package/lib/archive/migrate-pages-resources-source.d.ts +16 -0
  56. package/lib/archive/migrate-pages-resources-source.js +46 -0
  57. package/lib/archive/page.d.ts +187 -49
  58. package/lib/archive/page.js +258 -63
  59. package/lib/archive/peek-archive-lock.d.ts +40 -0
  60. package/lib/archive/peek-archive-lock.js +62 -0
  61. package/lib/archive/resolve-redirect-chain.d.ts +33 -0
  62. package/lib/archive/resolve-redirect-chain.js +27 -0
  63. package/lib/archive/types.d.ts +168 -26
  64. package/lib/classify-error-kind.d.ts +19 -0
  65. package/lib/classify-error-kind.js +122 -0
  66. package/lib/crawler/build-js-redirect-edge.d.ts +68 -0
  67. package/lib/crawler/build-js-redirect-edge.js +57 -0
  68. package/lib/crawler/build-redirect-event.d.ts +24 -0
  69. package/lib/crawler/build-redirect-event.js +28 -0
  70. package/lib/crawler/clear-dns-burned-host-cache.d.ts +6 -0
  71. package/lib/crawler/clear-dns-burned-host-cache.js +11 -0
  72. package/lib/crawler/close-browser-safely.d.ts +64 -0
  73. package/lib/crawler/close-browser-safely.js +73 -0
  74. package/lib/crawler/crawler.d.ts +7 -2
  75. package/lib/crawler/crawler.js +919 -113
  76. package/lib/crawler/create-change-phase-handler.d.ts +54 -0
  77. package/lib/crawler/create-change-phase-handler.js +44 -0
  78. package/lib/crawler/derive-js-redirect-target.d.ts +68 -0
  79. package/lib/crawler/derive-js-redirect-target.js +129 -0
  80. package/lib/crawler/derive-page-source.d.ts +23 -0
  81. package/lib/crawler/derive-page-source.js +28 -0
  82. package/lib/crawler/derive-resource-source.d.ts +33 -0
  83. package/lib/crawler/derive-resource-source.js +37 -0
  84. package/lib/crawler/dns-burned-host-cache.d.ts +26 -0
  85. package/lib/crawler/dns-burned-host-cache.js +25 -0
  86. package/lib/crawler/dns-burned-host-short-circuit-counter.d.ts +13 -0
  87. package/lib/crawler/dns-burned-host-short-circuit-counter.js +11 -0
  88. package/lib/crawler/drain-phase-errors.d.ts +48 -0
  89. package/lib/crawler/drain-phase-errors.js +35 -0
  90. package/lib/crawler/fetch-destination.d.ts +12 -4
  91. package/lib/crawler/fetch-destination.js +132 -18
  92. package/lib/crawler/format-crawl-progress.d.ts +12 -3
  93. package/lib/crawler/format-crawl-progress.js +14 -6
  94. package/lib/crawler/handle-browser-close.d.ts +29 -0
  95. package/lib/crawler/handle-browser-close.js +28 -0
  96. package/lib/crawler/is-html-content-type.d.ts +17 -0
  97. package/lib/crawler/is-html-content-type.js +19 -0
  98. package/lib/crawler/is-js-redirect-error-shape.d.ts +40 -0
  99. package/lib/crawler/is-js-redirect-error-shape.js +53 -0
  100. package/lib/crawler/is-likely-html-url.d.ts +22 -0
  101. package/lib/crawler/is-likely-html-url.js +65 -0
  102. package/lib/crawler/is-puppeteer-fallback-candidate.d.ts +16 -0
  103. package/lib/crawler/is-puppeteer-fallback-candidate.js +63 -0
  104. package/lib/crawler/kill-process-tree.d.ts +94 -0
  105. package/lib/crawler/kill-process-tree.js +178 -0
  106. package/lib/crawler/link-list.d.ts +21 -1
  107. package/lib/crawler/link-list.js +25 -4
  108. package/lib/crawler/link-to-page-data.d.ts +13 -5
  109. package/lib/crawler/link-to-page-data.js +26 -5
  110. package/lib/crawler/log-undrained-phase-errors.d.ts +37 -0
  111. package/lib/crawler/log-undrained-phase-errors.js +34 -0
  112. package/lib/crawler/normalize-content-type.d.ts +14 -0
  113. package/lib/crawler/normalize-content-type.js +20 -0
  114. package/lib/crawler/partition-urls-by-html.d.ts +16 -0
  115. package/lib/crawler/partition-urls-by-html.js +23 -0
  116. package/lib/crawler/plan-sub-resource-emits.d.ts +63 -0
  117. package/lib/crawler/plan-sub-resource-emits.js +44 -0
  118. package/lib/crawler/preload-short-circuit-error.d.ts +22 -0
  119. package/lib/crawler/preload-short-circuit-error.js +25 -0
  120. package/lib/crawler/redirect-dest-key.d.ts +19 -0
  121. package/lib/crawler/redirect-dest-key.js +27 -0
  122. package/lib/crawler/resource-to-page-data.d.ts +28 -0
  123. package/lib/crawler/resource-to-page-data.js +59 -0
  124. package/lib/crawler/should-burn-host.d.ts +78 -0
  125. package/lib/crawler/should-burn-host.js +61 -0
  126. package/lib/crawler/should-get-fallback-on-head-failure.d.ts +38 -0
  127. package/lib/crawler/should-get-fallback-on-head-failure.js +46 -0
  128. package/lib/crawler/types.d.ts +229 -1
  129. package/lib/crawler-orchestrator.d.ts +105 -3
  130. package/lib/crawler-orchestrator.js +620 -20
  131. package/lib/crawler.d.ts +8 -2
  132. package/lib/crawler.js +6 -1
  133. package/lib/permanent-error-kinds.d.ts +43 -0
  134. package/lib/permanent-error-kinds.js +48 -0
  135. package/lib/resource-row-to-lookup-result.d.ts +13 -0
  136. package/lib/resource-row-to-lookup-result.js +20 -0
  137. package/lib/types.d.ts +95 -1
  138. package/lib/utils/compute-file-sha256.d.ts +23 -0
  139. package/lib/utils/compute-file-sha256.js +55 -0
  140. package/lib/utils/error/emit-error-with-retry.d.ts +40 -0
  141. package/lib/utils/error/emit-error-with-retry.js +44 -0
  142. package/lib/utils/error/emit-error.d.ts +39 -0
  143. package/lib/utils/error/emit-error.js +41 -0
  144. package/lib/utils/object/parse-response-headers.d.ts +12 -0
  145. package/lib/utils/object/parse-response-headers.js +26 -0
  146. package/package.json +11 -11
  147. package/lib/utils/error/error-emitter.d.ts +0 -18
  148. package/lib/utils/error/error-emitter.js +0 -29
package/lib/crawler.d.ts CHANGED
@@ -7,8 +7,6 @@
7
7
  export * from './utils/types/types.js';
8
8
  export { eachSplitted } from './utils/array/each-splitted.js';
9
9
  export { DOMEvaluationError } from './utils/error/dom-evaluation-error.js';
10
- export { ErrorEmitter } from './utils/error/error-emitter.js';
11
- export type { ErrorEvent } from './utils/error/error-emitter.js';
12
10
  export * from './utils/object/clean-object.js';
13
11
  export { globalLog as log } from './utils/debug.js';
14
12
  export { ArchiveAccessor } from './archive/archive-accessor.js';
@@ -17,6 +15,14 @@ export { default as Page } from './archive/page.js';
17
15
  export { default as ArchiveResource } from './archive/resource.js';
18
16
  export * from './archive/types.js';
19
17
  export { default as Archive } from './archive/archive.js';
18
+ export { isArchiveCacheDisabled } from './archive/cache/is-archive-cache-disabled.js';
19
+ export { peekArchiveLockHolder } from './archive/peek-archive-lock.js';
20
+ export type { ArchiveLockHolder } from './archive/peek-archive-lock.js';
21
+ export type { FlatPageMetaColumns, JsonLdRow, JsonLdRowForInsert, TagRow, TagRowForInsert, JsonLdSummary, TagsSummary, TagInventoryEntry, PageDenormalizedColumns, } from './archive/meta/types.js';
22
+ export { IncompatibleArchiveError } from './archive/meta/types.js';
23
+ export { REQUIRED_FORMAT_VERSION } from './archive/meta/assert-compatible-version.js';
20
24
  export { DEFAULT_EXCLUDED_EXTERNAL_URLS, CrawlerOrchestrator, } from './crawler-orchestrator.js';
21
25
  export * from './types.js';
22
26
  export * from './crawler/types.js';
27
+ export { classifyErrorKind } from './classify-error-kind.js';
28
+ export { computeFileSha256 } from './utils/compute-file-sha256.js';
package/lib/crawler.js CHANGED
@@ -8,7 +8,6 @@
8
8
  export * from './utils/types/types.js';
9
9
  export { eachSplitted } from './utils/array/each-splitted.js';
10
10
  export { DOMEvaluationError } from './utils/error/dom-evaluation-error.js';
11
- export { ErrorEmitter } from './utils/error/error-emitter.js';
12
11
  export * from './utils/object/clean-object.js';
13
12
  export { globalLog as log } from './utils/debug.js';
14
13
  // Archive
@@ -17,7 +16,13 @@ export { default as Page } from './archive/page.js';
17
16
  export { default as ArchiveResource } from './archive/resource.js';
18
17
  export * from './archive/types.js';
19
18
  export { default as Archive } from './archive/archive.js';
19
+ export { isArchiveCacheDisabled } from './archive/cache/is-archive-cache-disabled.js';
20
+ export { peekArchiveLockHolder } from './archive/peek-archive-lock.js';
21
+ export { IncompatibleArchiveError } from './archive/meta/types.js';
22
+ export { REQUIRED_FORMAT_VERSION } from './archive/meta/assert-compatible-version.js';
20
23
  // Core
21
24
  export { DEFAULT_EXCLUDED_EXTERNAL_URLS, CrawlerOrchestrator, } from './crawler-orchestrator.js';
22
25
  export * from './types.js';
23
26
  export * from './crawler/types.js';
27
+ export { classifyErrorKind } from './classify-error-kind.js';
28
+ export { computeFileSha256 } from './utils/compute-file-sha256.js';
@@ -0,0 +1,43 @@
1
+ import type { ErrorKind } from './types.js';
2
+ /**
3
+ * Error kinds whose failure mode is **persistent** — re-fetching the same URL
4
+ * (without changing the network, the certificate, the destination host, or the
5
+ * browser's block-list state) will almost certainly produce the same failure.
6
+ *
7
+ * Used by {@link resetFailedPages} to exclude pages whose latest recorded error
8
+ * falls in this set, so `--retry-failed` actually converges: without the
9
+ * exclusion, NXDOMAIN / TLS mismatch / `ERR_BLOCKED_BY_CLIENT` /
10
+ * `ECONNREFUSED` / HTTP parse-error pages would be reset to pending on every
11
+ * iteration, the crawler would re-attempt them, they would fail again the
12
+ * same way, and the retry-target count would stay constant forever.
13
+ *
14
+ * Why these five and not others:
15
+ * - **dns** — `ENOTFOUND` / `ERR_NAME_NOT_RESOLVED` are authoritative DNS
16
+ * answers; the host is gone (or never existed). EAI_AGAIN is split out as
17
+ * `dns-transient` precisely so it is NOT in this set.
18
+ * - **tls** — cert expiry / SAN mismatch / SSL protocol errors require the
19
+ * server operator to fix something; retrying within the same archive run
20
+ * cannot succeed.
21
+ * - **client-blocked** — Chromium's `ERR_BLOCKED_*` family is a deliberate
22
+ * browser-side rejection; the request will be blocked identically every
23
+ * time the browser sees the same URL.
24
+ * - **parse-error** — the server's HTTP response is malformed (`Expected
25
+ * HTTP/, RTSP/ or ICE/`, `Unexpected end of stream`). Retrying the same
26
+ * request hits the same parser failure.
27
+ * - **connection-refused** — `ECONNREFUSED` is an authoritative TCP RST from
28
+ * the listener; either no process is listening on the port or its accept
29
+ * queue rejected the connection. Either way the answer is final until the
30
+ * server operator intervenes.
31
+ *
32
+ * Notably absent (intentionally retryable):
33
+ * - `connection-reset` / `connection-timeout` — could be middlebox or
34
+ * transient overload
35
+ * - `dns-transient` (EAI_AGAIN / EREFUSED) — local resolver hiccup
36
+ * - `local-network` — operator-side connectivity loss
37
+ * - `timeout` — slow but reachable server (HEAD-timeout escalation gives
38
+ * these a real chance)
39
+ * - `protocol` — puppeteer lifecycle race, often recovers on retry
40
+ * - `unknown` — by definition we don't know it's permanent, so we keep
41
+ * retrying (errs on the side of investigating)
42
+ */
43
+ export declare const PERMANENT_ERROR_KINDS: ReadonlySet<ErrorKind>;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Error kinds whose failure mode is **persistent** — re-fetching the same URL
3
+ * (without changing the network, the certificate, the destination host, or the
4
+ * browser's block-list state) will almost certainly produce the same failure.
5
+ *
6
+ * Used by {@link resetFailedPages} to exclude pages whose latest recorded error
7
+ * falls in this set, so `--retry-failed` actually converges: without the
8
+ * exclusion, NXDOMAIN / TLS mismatch / `ERR_BLOCKED_BY_CLIENT` /
9
+ * `ECONNREFUSED` / HTTP parse-error pages would be reset to pending on every
10
+ * iteration, the crawler would re-attempt them, they would fail again the
11
+ * same way, and the retry-target count would stay constant forever.
12
+ *
13
+ * Why these five and not others:
14
+ * - **dns** — `ENOTFOUND` / `ERR_NAME_NOT_RESOLVED` are authoritative DNS
15
+ * answers; the host is gone (or never existed). EAI_AGAIN is split out as
16
+ * `dns-transient` precisely so it is NOT in this set.
17
+ * - **tls** — cert expiry / SAN mismatch / SSL protocol errors require the
18
+ * server operator to fix something; retrying within the same archive run
19
+ * cannot succeed.
20
+ * - **client-blocked** — Chromium's `ERR_BLOCKED_*` family is a deliberate
21
+ * browser-side rejection; the request will be blocked identically every
22
+ * time the browser sees the same URL.
23
+ * - **parse-error** — the server's HTTP response is malformed (`Expected
24
+ * HTTP/, RTSP/ or ICE/`, `Unexpected end of stream`). Retrying the same
25
+ * request hits the same parser failure.
26
+ * - **connection-refused** — `ECONNREFUSED` is an authoritative TCP RST from
27
+ * the listener; either no process is listening on the port or its accept
28
+ * queue rejected the connection. Either way the answer is final until the
29
+ * server operator intervenes.
30
+ *
31
+ * Notably absent (intentionally retryable):
32
+ * - `connection-reset` / `connection-timeout` — could be middlebox or
33
+ * transient overload
34
+ * - `dns-transient` (EAI_AGAIN / EREFUSED) — local resolver hiccup
35
+ * - `local-network` — operator-side connectivity loss
36
+ * - `timeout` — slow but reachable server (HEAD-timeout escalation gives
37
+ * these a real chance)
38
+ * - `protocol` — puppeteer lifecycle race, often recovers on retry
39
+ * - `unknown` — by definition we don't know it's permanent, so we keep
40
+ * retrying (errs on the side of investigating)
41
+ */
42
+ export const PERMANENT_ERROR_KINDS = new Set([
43
+ 'dns',
44
+ 'tls',
45
+ 'client-blocked',
46
+ 'parse-error',
47
+ 'connection-refused',
48
+ ]);
@@ -0,0 +1,13 @@
1
+ import type { DB_Resource } from './archive/types.js';
2
+ import type { ResourceLookupResult } from './crawler/types.js';
3
+ /**
4
+ * Convert a raw `resources` table row into the minimal lookup result the
5
+ * crawler needs to reuse captured sub-resource data.
6
+ *
7
+ * Header parsing degrades to `null` on malformed JSON instead of throwing
8
+ * because a missing header set only loses fidelity — the reuse path stays
9
+ * valid.
10
+ * @param row - The raw database row.
11
+ * @returns The lookup result consumed by the crawler's resource-reuse hook.
12
+ */
13
+ export declare function resourceRowToLookupResult(row: DB_Resource): ResourceLookupResult;
@@ -0,0 +1,20 @@
1
+ import { parseResponseHeaders } from './utils/object/parse-response-headers.js';
2
+ /**
3
+ * Convert a raw `resources` table row into the minimal lookup result the
4
+ * crawler needs to reuse captured sub-resource data.
5
+ *
6
+ * Header parsing degrades to `null` on malformed JSON instead of throwing
7
+ * because a missing header set only loses fidelity — the reuse path stays
8
+ * valid.
9
+ * @param row - The raw database row.
10
+ * @returns The lookup result consumed by the crawler's resource-reuse hook.
11
+ */
12
+ export function resourceRowToLookupResult(row) {
13
+ return {
14
+ status: row.status,
15
+ statusText: row.statusText,
16
+ contentType: row.contentType,
17
+ contentLength: row.contentLength,
18
+ responseHeaders: parseResponseHeaders(row.responseHeaders),
19
+ };
20
+ }
package/lib/types.d.ts CHANGED
@@ -1,4 +1,88 @@
1
- import type { CrawlerError } from './utils/types/types.js';
1
+ import type { CrawlerError, PageData } from './utils/types/types.js';
2
+ /**
3
+ * Aggregate counts captured during a `--inventory` invocation, forwarded to
4
+ * `#writeInventoryRunRow` so the audit log row is consistent between the
5
+ * HTML-seed branch and the non-HTML-only branch of
6
+ * {@link CrawlerOrchestrator.inventory}.
7
+ *
8
+ * Spelled out here (not inlined at the call site) so a new field added to
9
+ * the audit row has a single edit point and so each field's semantics are
10
+ * documented per-property rather than scattered across the two emit sites.
11
+ */
12
+ export interface InventoryRunAggregates {
13
+ /** Total non-empty lines in the input list (= `inventoryUrls.length` before any filtering). Stored verbatim as `inventory_runs.total_lines`. */
14
+ inventoryUrlsCount: number;
15
+ /** Number of novel URLs classified as HTML and queued for render. Stored as `new_pages` (excludes anchor-discovered descendants — those add later via the crawler graph and are NOT counted here). */
16
+ htmlSeedsCount: number;
17
+ /** Number of novel URLs classified as non-HTML and written directly into `resources`. Stored as `new_resources`. */
18
+ nonHtmlCount: number;
19
+ /** URLs dropped because they fell outside the archived scope. Stored as `scope_skipped`. */
20
+ outOfScope: number;
21
+ /**
22
+ * SHA-256 hex digest of the source `.txt`, **pre-computed by the caller**
23
+ * (typically the CLI's `inventoryCrawl`). Stored verbatim as
24
+ * `inventory_runs.source_file_sha256`.
25
+ *
26
+ * Pre-computation lifts the absolute path off the orchestrator
27
+ * boundary entirely — the path is privacy-sensitive (leaks
28
+ * user-home / OS structure when archives are shared), and the
29
+ * orchestrator has no business handling it after the audit-row
30
+ * column was dropped. Pass `null` for programmatic callers that
31
+ * built `inventoryUrls` in-memory; the audit row's
32
+ * `source_file_sha256` will be `NULL`.
33
+ */
34
+ sourceFileSha256: string | null;
35
+ }
36
+ /**
37
+ * Coarse cause of a crawl/scrape failure.
38
+ *
39
+ * The crawler stores only the raw error message (in `crawl_errors`,
40
+ * `page_errors`, or `error.log`); the cause is derived on read by
41
+ * `classifyErrorKind`, so existing archives gain classification without a
42
+ * re-crawl.
43
+ *
44
+ * Owned by the crawler package because both the crawler (for DNS-burned host
45
+ * caching) and `@nitpicker/query` (for `getErrorKinds` / `getSummary`) need to
46
+ * classify error messages, and crawler cannot depend on query.
47
+ *
48
+ * ### transient vs persistent
49
+ *
50
+ * | kind | transient? | DNS-burn? | notes |
51
+ * | --- | --- | --- | --- |
52
+ * | `dns` | no | yes | NXDOMAIN; the host does not resolve at all |
53
+ * | `dns-transient` | **yes** | no | `EAI_AGAIN`; local resolver hiccup, retry often recovers |
54
+ * | `tls` | no | no | certificate issue, usually persistent until cert rotates |
55
+ * | `connection-refused` | mostly persistent | no | server actively rejecting on this port |
56
+ * | `connection-reset` | yes | no | TCP reset mid-stream, often transient |
57
+ * | `connection-timeout` | yes | no | TCP-level timeout (`ETIMEDOUT`); slow but reachable |
58
+ * | `local-network` | **yes** | no | local machine's network is unreachable / changed (WiFi, sleep, ICMP-unreachable, …) |
59
+ * | `parse-error` | mostly persistent | no | HTTP response could not be parsed (proxy, garbage, MITM) |
60
+ * | `client-blocked` | persistent (per browser) | no | Chromium-side `ERR_BLOCKED_BY_*` family — the browser actively refused the request (ad/tracker heuristics, CSP, CORP, administrator block list, …) |
61
+ * | `protocol` | yes | no | puppeteer protocol layer (frame detached, target closed, …) |
62
+ * | `timeout` | yes | no | puppeteer navigation timeout or HEAD pre-flight race timeout (`Timeout: <url>`) |
63
+ * | `unknown` | unknown | no | catch-all for messages no matcher recognised |
64
+ *
65
+ * Only `dns` is mark-target for the DNS-burned host cache; everything else is
66
+ * either too transient to burn (network glitch / browser hiccup) or too
67
+ * server-specific to extrapolate to "this whole host is dead."
68
+ *
69
+ * ### Derived constants that MUST be reviewed when this union changes
70
+ *
71
+ * - `PERMANENT_ERROR_KINDS` (`permanent-error-kinds.ts`) — the set of kinds
72
+ * excluded from `--retry-failed` so retry iterations actually converge.
73
+ * A new kind that is deterministically permanent (server-state, cert,
74
+ * browser-block, …) likely belongs here.
75
+ * - `PUPPETEER_FALLBACK_KINDS` (`crawler/is-puppeteer-fallback-candidate.ts`)
76
+ * — the set of kinds where one puppeteer attempt has a realistic chance
77
+ * of succeeding after HEAD+GET pre-flight exhausted retries. A new kind
78
+ * modelling a middlebox / WAF / slow-server quirk likely belongs here.
79
+ *
80
+ * Adding a kind without reviewing both sets risks a silent regression:
81
+ * `--retry-failed` re-trying a permanent failure forever (no PERMANENT
82
+ * entry), or a recoverable URL never reaching the puppeteer fallback (no
83
+ * PUPPETEER_FALLBACK entry).
84
+ */
85
+ export type ErrorKind = 'dns' | 'dns-transient' | 'connection-refused' | 'connection-reset' | 'connection-timeout' | 'tls' | 'local-network' | 'parse-error' | 'client-blocked' | 'timeout' | 'protocol' | 'unknown';
2
86
  /**
3
87
  * Event map for the `CrawlerOrchestrator` class.
4
88
  *
@@ -24,4 +108,14 @@ export interface CrawlEvent {
24
108
  * Emitted when an error occurs during crawling or archiving.
25
109
  */
26
110
  error: CrawlerError;
111
+ /**
112
+ * Emitted when a URL redirects to a destination already rendered during this
113
+ * crawl, so only the redirect edge is recorded and the destination is not
114
+ * re-rendered (#73). Mirrors the crawler's `redirect` event; useful for
115
+ * observing how much redirect-convergence work was skipped.
116
+ */
117
+ redirect: {
118
+ /** HEAD-resolved page data carrying the redirect chain (source → destination). */
119
+ result: PageData;
120
+ };
27
121
  }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Compute the SHA-256 digest of a file's bytes, streaming so memory stays
3
+ * O(1) regardless of file size.
4
+ *
5
+ * Used by {@link CrawlerOrchestrator.inventory} to fingerprint the source
6
+ * URL list `.txt` and store the digest on the `inventory_runs` row.
7
+ * Phase 3 (`--refresh`) will key dedupe on this column; Phase 1 just
8
+ * records it for operator audit.
9
+ *
10
+ * Returns `null` instead of throwing when the file cannot be read (e.g.
11
+ * vanished mid-run, permissions issue) so the inventory run's audit row
12
+ * can still be written with a NULL digest. Hashing failure is an audit
13
+ * loss, not a correctness failure — the actual ingestion has already
14
+ * succeeded by the time this is called.
15
+ * @param filePath - Absolute or relative path to the file to hash.
16
+ * @returns Lower-case hex digest (64 chars), or `null` if reading failed.
17
+ * @example
18
+ * ```ts
19
+ * const sha = await computeFileSha256('/tmp/list.txt');
20
+ * if (sha) console.log(sha); // 64-char hex string
21
+ * ```
22
+ */
23
+ export declare function computeFileSha256(filePath: string): Promise<string | null>;
@@ -0,0 +1,55 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { createReadStream } from 'node:fs';
3
+ import { crawlerLog } from '../debug.js';
4
+ /**
5
+ * Compute the SHA-256 digest of a file's bytes, streaming so memory stays
6
+ * O(1) regardless of file size.
7
+ *
8
+ * Used by {@link CrawlerOrchestrator.inventory} to fingerprint the source
9
+ * URL list `.txt` and store the digest on the `inventory_runs` row.
10
+ * Phase 3 (`--refresh`) will key dedupe on this column; Phase 1 just
11
+ * records it for operator audit.
12
+ *
13
+ * Returns `null` instead of throwing when the file cannot be read (e.g.
14
+ * vanished mid-run, permissions issue) so the inventory run's audit row
15
+ * can still be written with a NULL digest. Hashing failure is an audit
16
+ * loss, not a correctness failure — the actual ingestion has already
17
+ * succeeded by the time this is called.
18
+ * @param filePath - Absolute or relative path to the file to hash.
19
+ * @returns Lower-case hex digest (64 chars), or `null` if reading failed.
20
+ * @example
21
+ * ```ts
22
+ * const sha = await computeFileSha256('/tmp/list.txt');
23
+ * if (sha) console.log(sha); // 64-char hex string
24
+ * ```
25
+ */
26
+ export async function computeFileSha256(filePath) {
27
+ return await new Promise((resolve) => {
28
+ const hash = createHash('sha256');
29
+ const stream = createReadStream(filePath);
30
+ let settled = false;
31
+ const settle = (value) => {
32
+ if (settled) {
33
+ return;
34
+ }
35
+ settled = true;
36
+ resolve(value);
37
+ };
38
+ stream.on('data', (chunk) => hash.update(chunk));
39
+ stream.on('end', () => settle(hash.digest('hex')));
40
+ stream.on('error', (error) => {
41
+ // Trace the failure so operators inspecting `--inventory`
42
+ // audit rows with `source_file_sha256 = NULL` can recover
43
+ // the underlying cause via `DEBUG=Nitpicker:Crawler:*`.
44
+ crawlerLog('compute-file-sha256 failed for %s: %s', filePath, error.message);
45
+ settle(null);
46
+ });
47
+ // `'close'` is the last-resort settler. If the stream is
48
+ // destroyed externally between `'data'` and `'end'` (e.g. a
49
+ // test or signal handler), neither `'end'` nor `'error'` may
50
+ // fire — leaving the promise pending forever. Falling back to
51
+ // `null` matches the documented contract ("hashing failure
52
+ // yields null").
53
+ stream.on('close', () => settle(null));
54
+ });
55
+ }
@@ -0,0 +1,40 @@
1
+ import type { RetryCallOptions } from '@d-zero/shared/retry';
2
+ /**
3
+ * Minimal object shape required by {@link emitErrorAndRetry}: an emitter
4
+ * able to publish an `'error'` event carrying an `Error` payload. Same
5
+ * contract as `emitError` — see {@link ./emit-error.js} for details.
6
+ */
7
+ interface Emitter {
8
+ /**
9
+ * Emits the `'error'` event with the given `Error` payload.
10
+ * @param event - Event name; always `'error'` here.
11
+ * @param payload - The error instance to publish.
12
+ * @returns Anything; the caller ignores it.
13
+ */
14
+ emit(event: 'error', payload: Error): unknown;
15
+ }
16
+ /**
17
+ * Retries `fn` per `retryOptions`, then — if retries are exhausted with a
18
+ * final `Error` — logs and re-emits it as an `'error'` event on `emitter`
19
+ * before re-throwing. `label` is forwarded into `retryCall` so its own
20
+ * onWait / onGiveUp / timeout messages identify the call site (previously
21
+ * derived automatically by the `@retry` decorator via
22
+ * `ClassMethodDecoratorContext.name`).
23
+ *
24
+ * Inlined try/catch rather than composing {@link emitError} with a
25
+ * `() => retryCall(...)` closure to keep the per-invocation closure count
26
+ * at 1 — this method is called on the DB write hot path (`updatePage`,
27
+ * `insertResource`), where wrapping decorators previously ran with zero
28
+ * per-call allocation.
29
+ * @template T - The return type of `fn`.
30
+ * @param emitter - The event emitter that receives the `'error'` event.
31
+ * @param label - Human-readable label; forwarded into `retryCall`
32
+ * (visible in retry timeouts / onWait / onGiveUp) and used
33
+ * as the debug log prefix on final failure.
34
+ * @param fn - The async operation to retry.
35
+ * @param retryOptions - Retry configuration; `label` is overwritten
36
+ * with the `label` argument here.
37
+ * @returns The resolved value of `fn`.
38
+ */
39
+ export declare function emitErrorAndRetry<T>(emitter: Emitter, label: string, fn: () => Promise<T>, retryOptions: RetryCallOptions): Promise<T>;
40
+ export {};
@@ -0,0 +1,44 @@
1
+ import { retryCall } from '@d-zero/shared/retry';
2
+ import { log } from '../debug.js';
3
+ /**
4
+ * Debug logger for the ErrorEmitter helper family.
5
+ * Namespace: `Nitpicker:Utils:ErrorEmitter`. Kept identical to the namespace
6
+ * used by the deleted `@ErrorEmitter()` decorator so existing `DEBUG` env
7
+ * filters keep working after the decorator-to-HOF migration.
8
+ */
9
+ const errorLog = log.extend('ErrorEmitter');
10
+ /**
11
+ * Retries `fn` per `retryOptions`, then — if retries are exhausted with a
12
+ * final `Error` — logs and re-emits it as an `'error'` event on `emitter`
13
+ * before re-throwing. `label` is forwarded into `retryCall` so its own
14
+ * onWait / onGiveUp / timeout messages identify the call site (previously
15
+ * derived automatically by the `@retry` decorator via
16
+ * `ClassMethodDecoratorContext.name`).
17
+ *
18
+ * Inlined try/catch rather than composing {@link emitError} with a
19
+ * `() => retryCall(...)` closure to keep the per-invocation closure count
20
+ * at 1 — this method is called on the DB write hot path (`updatePage`,
21
+ * `insertResource`), where wrapping decorators previously ran with zero
22
+ * per-call allocation.
23
+ * @template T - The return type of `fn`.
24
+ * @param emitter - The event emitter that receives the `'error'` event.
25
+ * @param label - Human-readable label; forwarded into `retryCall`
26
+ * (visible in retry timeouts / onWait / onGiveUp) and used
27
+ * as the debug log prefix on final failure.
28
+ * @param fn - The async operation to retry.
29
+ * @param retryOptions - Retry configuration; `label` is overwritten
30
+ * with the `label` argument here.
31
+ * @returns The resolved value of `fn`.
32
+ */
33
+ export async function emitErrorAndRetry(emitter, label, fn, retryOptions) {
34
+ try {
35
+ return await retryCall(fn, { ...retryOptions, label });
36
+ }
37
+ catch (error) {
38
+ if (error instanceof Error) {
39
+ errorLog('%s: %O', label, error);
40
+ void emitter.emit('error', error);
41
+ }
42
+ throw error;
43
+ }
44
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Minimal object shape required by {@link emitError}: an emitter able to
3
+ * publish an `'error'` event carrying an `Error` payload. Any class that
4
+ * extends `TypedAwaitEventEmitter<{ error: Error, ... }>` satisfies this,
5
+ * and so does Node's built-in `EventEmitter`.
6
+ */
7
+ interface Emitter {
8
+ /**
9
+ * Emits the `'error'` event with the given `Error` payload.
10
+ * The return value is intentionally unspecified — callers ignore it.
11
+ * @param event - Event name; always `'error'` here.
12
+ * @param payload - The error instance to publish.
13
+ * @returns Anything; the caller ignores it.
14
+ */
15
+ emit(event: 'error', payload: Error): unknown;
16
+ }
17
+ /**
18
+ * Wraps `fn` so that any thrown `Error` is logged with `label` and
19
+ * re-emitted as an `'error'` event on `emitter` before being re-thrown.
20
+ * Non-`Error` throws pass through unchanged — no log, no emit — matching
21
+ * the behaviour of the deleted `@ErrorEmitter()` decorator so that
22
+ * downstream `'error'` listeners (e.g. the crawler orchestrator that
23
+ * aborts on any emitted error) are only invoked with real `Error`
24
+ * instances.
25
+ *
26
+ * Kept as a standalone HOF (instead of a decorator) because Vite 8 /
27
+ * Vitest 4.1 switched from esbuild to Rolldown + oxc, which emit
28
+ * TC39 Stage 3 decorator syntax as-is and let it reach Node, causing
29
+ * `SyntaxError: Invalid or unexpected token` at test runtime.
30
+ * @template T - The return type of `fn`.
31
+ * @param emitter - The event emitter that receives the `'error'` event.
32
+ * @param label - Human-readable label included in the debug log line
33
+ * (typically `'ClassName.methodName'`). Also useful as the
34
+ * grep target when triaging failed crawls.
35
+ * @param fn - The async operation to wrap.
36
+ * @returns The resolved value of `fn`.
37
+ */
38
+ export declare function emitError<T>(emitter: Emitter, label: string, fn: () => Promise<T>): Promise<T>;
39
+ export {};
@@ -0,0 +1,41 @@
1
+ import { log } from '../debug.js';
2
+ /**
3
+ * Debug logger for the ErrorEmitter helper family.
4
+ * Namespace: `Nitpicker:Utils:ErrorEmitter`. Kept identical to the namespace
5
+ * used by the deleted `@ErrorEmitter()` decorator so existing `DEBUG` env
6
+ * filters keep working after the decorator-to-HOF migration.
7
+ */
8
+ const errorLog = log.extend('ErrorEmitter');
9
+ /**
10
+ * Wraps `fn` so that any thrown `Error` is logged with `label` and
11
+ * re-emitted as an `'error'` event on `emitter` before being re-thrown.
12
+ * Non-`Error` throws pass through unchanged — no log, no emit — matching
13
+ * the behaviour of the deleted `@ErrorEmitter()` decorator so that
14
+ * downstream `'error'` listeners (e.g. the crawler orchestrator that
15
+ * aborts on any emitted error) are only invoked with real `Error`
16
+ * instances.
17
+ *
18
+ * Kept as a standalone HOF (instead of a decorator) because Vite 8 /
19
+ * Vitest 4.1 switched from esbuild to Rolldown + oxc, which emit
20
+ * TC39 Stage 3 decorator syntax as-is and let it reach Node, causing
21
+ * `SyntaxError: Invalid or unexpected token` at test runtime.
22
+ * @template T - The return type of `fn`.
23
+ * @param emitter - The event emitter that receives the `'error'` event.
24
+ * @param label - Human-readable label included in the debug log line
25
+ * (typically `'ClassName.methodName'`). Also useful as the
26
+ * grep target when triaging failed crawls.
27
+ * @param fn - The async operation to wrap.
28
+ * @returns The resolved value of `fn`.
29
+ */
30
+ export async function emitError(emitter, label, fn) {
31
+ try {
32
+ return await fn();
33
+ }
34
+ catch (error) {
35
+ if (error instanceof Error) {
36
+ errorLog('%s: %O', label, error);
37
+ void emitter.emit('error', error);
38
+ }
39
+ throw error;
40
+ }
41
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Parse a JSON-serialized HTTP response headers column from the archive
3
+ * database.
4
+ *
5
+ * The columns hold JSON produced by `JSON.stringify` at insert time; absent,
6
+ * malformed, or non-object JSON (the string `"null"`, arrays, primitives)
7
+ * degrades to `null` instead of throwing because a missing header set only
8
+ * loses fidelity — callers decide their own fallback (`?? {}` etc.).
9
+ * @param json - The raw column value.
10
+ * @returns The parsed header record, or `null` when absent or malformed.
11
+ */
12
+ export declare function parseResponseHeaders(json: string | null): Record<string, string | string[] | undefined> | null;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Parse a JSON-serialized HTTP response headers column from the archive
3
+ * database.
4
+ *
5
+ * The columns hold JSON produced by `JSON.stringify` at insert time; absent,
6
+ * malformed, or non-object JSON (the string `"null"`, arrays, primitives)
7
+ * degrades to `null` instead of throwing because a missing header set only
8
+ * loses fidelity — callers decide their own fallback (`?? {}` etc.).
9
+ * @param json - The raw column value.
10
+ * @returns The parsed header record, or `null` when absent or malformed.
11
+ */
12
+ export function parseResponseHeaders(json) {
13
+ if (json == null) {
14
+ return null;
15
+ }
16
+ try {
17
+ const parsed = JSON.parse(json);
18
+ if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) {
19
+ return null;
20
+ }
21
+ return parsed;
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitpicker/crawler",
3
- "version": "0.9.0",
3
+ "version": "0.12.0",
4
4
  "description": "Web crawler engine with headless browser rendering and archive storage",
5
5
  "author": "D-ZERO",
6
6
  "license": "Apache-2.0",
@@ -27,26 +27,26 @@
27
27
  "clean": "tsc --build --clean"
28
28
  },
29
29
  "dependencies": {
30
- "@d-zero/beholder": "2.1.2",
31
- "@d-zero/dealer": "1.7.0",
32
- "@d-zero/fs": "0.2.2",
33
- "@d-zero/shared": "0.20.1",
30
+ "@d-zero/beholder": "3.1.3",
31
+ "@d-zero/dealer": "1.9.3",
32
+ "@d-zero/fs": "0.2.5",
33
+ "@d-zero/shared": "0.22.1",
34
34
  "ansi-colors": "4.1.3",
35
35
  "debug": "4.4.3",
36
- "follow-redirects": "1.15.11",
37
- "fs-extra": "11.3.3",
38
- "knex": "3.1.0",
36
+ "follow-redirects": "1.16.0",
37
+ "fs-extra": "11.3.5",
38
+ "knex": "3.2.10",
39
39
  "libsql": "0.5.29",
40
40
  "puppeteer": "24.37.5",
41
41
  "robots-parser": "3.0.1",
42
- "tar": "7.5.9"
42
+ "tar": "7.5.16"
43
43
  },
44
44
  "devDependencies": {
45
- "@types/debug": "4.1.12",
45
+ "@types/debug": "4.1.13",
46
46
  "@types/follow-redirects": "1.14.4",
47
47
  "@types/fs-extra": "11.0.4",
48
48
  "@types/tar": "7.0.87",
49
49
  "@types/unzipper": "0.10.11"
50
50
  },
51
- "gitHead": "2d194c881f323de7555e0025f6291ded65450dae"
51
+ "gitHead": "58b68416e0b6e3dcdd92772bee754f186a35505f"
52
52
  }
@@ -1,18 +0,0 @@
1
- import type { TypedAwaitEventEmitter as EventEmitter } from '@d-zero/shared/typed-await-event-emitter';
2
- /**
3
- * Event payload type for error events emitted by classes using the {@link ErrorEmitter} decorator.
4
- * @template E - The specific error type, defaults to `Error`.
5
- */
6
- export type ErrorEvent<E extends Error = Error> = {
7
- /** The error instance that was caught. */
8
- error: E;
9
- };
10
- /**
11
- * A class method decorator factory that wraps the decorated method with error handling.
12
- * When the method throws an `Error`, it emits an `'error'` event on the class instance
13
- * (which must extend {@link EventEmitter}) with the caught error, then re-throws the error.
14
- * @template C - The class type, which must be an EventEmitter capable of emitting error events.
15
- * @template E - The error event type, defaults to {@link ErrorEvent}.
16
- * @returns A decorator function that wraps the target method with error-emitting behavior.
17
- */
18
- export declare function ErrorEmitter<C extends EventEmitter<E>, E extends ErrorEvent = ErrorEvent>(): (method: Function, context: ClassMethodDecoratorContext) => (this: C, ...args: unknown[]) => Promise<any>;