@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.
- package/lib/archive/archive-accessor.d.ts +87 -14
- package/lib/archive/archive-accessor.js +162 -36
- package/lib/archive/archive.d.ts +262 -24
- package/lib/archive/archive.js +397 -86
- package/lib/archive/cache/compute-archive-cache-key.d.ts +39 -0
- package/lib/archive/cache/compute-archive-cache-key.js +95 -0
- package/lib/archive/cache/extract-archive-to-cache.d.ts +43 -0
- package/lib/archive/cache/extract-archive-to-cache.js +309 -0
- package/lib/archive/cache/get-archive-cache-root.d.ts +20 -0
- package/lib/archive/cache/get-archive-cache-root.js +53 -0
- package/lib/archive/cache/is-archive-cache-disabled.d.ts +24 -0
- package/lib/archive/cache/is-archive-cache-disabled.js +34 -0
- package/lib/archive/cache/resolve-archive-cache-dir.d.ts +26 -0
- package/lib/archive/cache/resolve-archive-cache-dir.js +32 -0
- package/lib/archive/database.d.ts +411 -26
- package/lib/archive/database.js +1881 -540
- package/lib/archive/derive-lineage-from-parent.d.ts +37 -0
- package/lib/archive/derive-lineage-from-parent.js +42 -0
- package/lib/archive/filesystem/peek-tar-top-dir.d.ts +28 -0
- package/lib/archive/filesystem/peek-tar-top-dir.js +65 -0
- package/lib/archive/get-failed-page-messages.d.ts +43 -0
- package/lib/archive/get-failed-page-messages.js +131 -0
- package/lib/archive/init-schema.d.ts +53 -2
- package/lib/archive/init-schema.js +399 -15
- package/lib/archive/is-inventory-source.d.ts +21 -0
- package/lib/archive/is-inventory-source.js +22 -0
- package/lib/archive/meta/assert-compatible-version.d.ts +39 -0
- package/lib/archive/meta/assert-compatible-version.js +72 -0
- package/lib/archive/meta/classify-jsonld-type.d.ts +23 -0
- package/lib/archive/meta/classify-jsonld-type.js +43 -0
- package/lib/archive/meta/compare-semver.d.ts +23 -0
- package/lib/archive/meta/compare-semver.js +51 -0
- package/lib/archive/meta/compute-page-denormalized.d.ts +21 -0
- package/lib/archive/meta/compute-page-denormalized.js +35 -0
- package/lib/archive/meta/derive-flat-from-meta.d.ts +35 -0
- package/lib/archive/meta/derive-flat-from-meta.js +158 -0
- package/lib/archive/meta/derive-meta-extras.d.ts +20 -0
- package/lib/archive/meta/derive-meta-extras.js +23 -0
- package/lib/archive/meta/extract-tags-for-archive.d.ts +18 -0
- package/lib/archive/meta/extract-tags-for-archive.js +36 -0
- package/lib/archive/meta/summarize-jsonld.d.ts +17 -0
- package/lib/archive/meta/summarize-jsonld.js +29 -0
- package/lib/archive/meta/summarize-tags.d.ts +16 -0
- package/lib/archive/meta/summarize-tags.js +33 -0
- package/lib/archive/meta/types.d.ts +207 -0
- package/lib/archive/meta/types.js +33 -0
- package/lib/archive/migrate-crawl-errors.d.ts +20 -0
- package/lib/archive/migrate-crawl-errors.js +38 -0
- package/lib/archive/migrate-html-blob-tables.d.ts +24 -0
- package/lib/archive/migrate-html-blob-tables.js +53 -0
- package/lib/archive/migrate-inventory-runs.d.ts +29 -0
- package/lib/archive/migrate-inventory-runs.js +52 -0
- package/lib/archive/migrate-page-errors.d.ts +16 -0
- package/lib/archive/migrate-page-errors.js +35 -0
- package/lib/archive/migrate-pages-resources-source.d.ts +16 -0
- package/lib/archive/migrate-pages-resources-source.js +46 -0
- package/lib/archive/page.d.ts +187 -49
- package/lib/archive/page.js +258 -63
- package/lib/archive/peek-archive-lock.d.ts +40 -0
- package/lib/archive/peek-archive-lock.js +62 -0
- package/lib/archive/resolve-redirect-chain.d.ts +33 -0
- package/lib/archive/resolve-redirect-chain.js +27 -0
- package/lib/archive/types.d.ts +168 -26
- package/lib/classify-error-kind.d.ts +19 -0
- package/lib/classify-error-kind.js +122 -0
- package/lib/crawler/build-js-redirect-edge.d.ts +68 -0
- package/lib/crawler/build-js-redirect-edge.js +57 -0
- package/lib/crawler/build-redirect-event.d.ts +24 -0
- package/lib/crawler/build-redirect-event.js +28 -0
- package/lib/crawler/clear-dns-burned-host-cache.d.ts +6 -0
- package/lib/crawler/clear-dns-burned-host-cache.js +11 -0
- package/lib/crawler/close-browser-safely.d.ts +64 -0
- package/lib/crawler/close-browser-safely.js +73 -0
- package/lib/crawler/crawler.d.ts +7 -2
- package/lib/crawler/crawler.js +919 -113
- package/lib/crawler/create-change-phase-handler.d.ts +54 -0
- package/lib/crawler/create-change-phase-handler.js +44 -0
- package/lib/crawler/derive-js-redirect-target.d.ts +68 -0
- package/lib/crawler/derive-js-redirect-target.js +129 -0
- package/lib/crawler/derive-page-source.d.ts +23 -0
- package/lib/crawler/derive-page-source.js +28 -0
- package/lib/crawler/derive-resource-source.d.ts +33 -0
- package/lib/crawler/derive-resource-source.js +37 -0
- package/lib/crawler/dns-burned-host-cache.d.ts +26 -0
- package/lib/crawler/dns-burned-host-cache.js +25 -0
- package/lib/crawler/dns-burned-host-short-circuit-counter.d.ts +13 -0
- package/lib/crawler/dns-burned-host-short-circuit-counter.js +11 -0
- package/lib/crawler/drain-phase-errors.d.ts +48 -0
- package/lib/crawler/drain-phase-errors.js +35 -0
- package/lib/crawler/fetch-destination.d.ts +12 -4
- package/lib/crawler/fetch-destination.js +132 -18
- package/lib/crawler/format-crawl-progress.d.ts +12 -3
- package/lib/crawler/format-crawl-progress.js +14 -6
- package/lib/crawler/handle-browser-close.d.ts +29 -0
- package/lib/crawler/handle-browser-close.js +28 -0
- package/lib/crawler/is-html-content-type.d.ts +17 -0
- package/lib/crawler/is-html-content-type.js +19 -0
- package/lib/crawler/is-js-redirect-error-shape.d.ts +40 -0
- package/lib/crawler/is-js-redirect-error-shape.js +53 -0
- package/lib/crawler/is-likely-html-url.d.ts +22 -0
- package/lib/crawler/is-likely-html-url.js +65 -0
- package/lib/crawler/is-puppeteer-fallback-candidate.d.ts +16 -0
- package/lib/crawler/is-puppeteer-fallback-candidate.js +63 -0
- package/lib/crawler/kill-process-tree.d.ts +94 -0
- package/lib/crawler/kill-process-tree.js +178 -0
- package/lib/crawler/link-list.d.ts +21 -1
- package/lib/crawler/link-list.js +25 -4
- package/lib/crawler/link-to-page-data.d.ts +13 -5
- package/lib/crawler/link-to-page-data.js +26 -5
- package/lib/crawler/log-undrained-phase-errors.d.ts +37 -0
- package/lib/crawler/log-undrained-phase-errors.js +34 -0
- package/lib/crawler/normalize-content-type.d.ts +14 -0
- package/lib/crawler/normalize-content-type.js +20 -0
- package/lib/crawler/partition-urls-by-html.d.ts +16 -0
- package/lib/crawler/partition-urls-by-html.js +23 -0
- package/lib/crawler/plan-sub-resource-emits.d.ts +63 -0
- package/lib/crawler/plan-sub-resource-emits.js +44 -0
- package/lib/crawler/preload-short-circuit-error.d.ts +22 -0
- package/lib/crawler/preload-short-circuit-error.js +25 -0
- package/lib/crawler/redirect-dest-key.d.ts +19 -0
- package/lib/crawler/redirect-dest-key.js +27 -0
- package/lib/crawler/resource-to-page-data.d.ts +28 -0
- package/lib/crawler/resource-to-page-data.js +59 -0
- package/lib/crawler/should-burn-host.d.ts +78 -0
- package/lib/crawler/should-burn-host.js +61 -0
- package/lib/crawler/should-get-fallback-on-head-failure.d.ts +38 -0
- package/lib/crawler/should-get-fallback-on-head-failure.js +46 -0
- package/lib/crawler/types.d.ts +229 -1
- package/lib/crawler-orchestrator.d.ts +105 -3
- package/lib/crawler-orchestrator.js +620 -20
- package/lib/crawler.d.ts +8 -2
- package/lib/crawler.js +6 -1
- package/lib/permanent-error-kinds.d.ts +43 -0
- package/lib/permanent-error-kinds.js +48 -0
- package/lib/resource-row-to-lookup-result.d.ts +13 -0
- package/lib/resource-row-to-lookup-result.js +20 -0
- package/lib/types.d.ts +95 -1
- package/lib/utils/compute-file-sha256.d.ts +23 -0
- package/lib/utils/compute-file-sha256.js +55 -0
- package/lib/utils/error/emit-error-with-retry.d.ts +40 -0
- package/lib/utils/error/emit-error-with-retry.js +44 -0
- package/lib/utils/error/emit-error.d.ts +39 -0
- package/lib/utils/error/emit-error.js +41 -0
- package/lib/utils/object/parse-response-headers.d.ts +12 -0
- package/lib/utils/object/parse-response-headers.js +26 -0
- package/package.json +11 -11
- package/lib/utils/error/error-emitter.d.ts +0 -18
- package/lib/utils/error/error-emitter.js +0 -29
package/lib/archive/types.d.ts
CHANGED
|
@@ -46,6 +46,61 @@ export interface Config extends Required<Pick<ParseURLOptions, 'disableQueries'>
|
|
|
46
46
|
/** Whether robots.txt restrictions were ignored during crawling. */
|
|
47
47
|
ignoreRobots: boolean;
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Provenance of a page or resource row — which crawler channel originally
|
|
51
|
+
* inserted it. Stored as `pages.source` / `resources.source` in the
|
|
52
|
+
* SQLite schema (NOT NULL DEFAULT `'crawled'`).
|
|
53
|
+
*
|
|
54
|
+
* - `'crawled'` — discovered via the recursive crawl rooted at `info.roots`.
|
|
55
|
+
* Default for pre-`--inventory` archives after the
|
|
56
|
+
* `migratePagesResourcesSource` runtime migration.
|
|
57
|
+
* - `'inventory-seed'` — supplied directly by a `crawl --inventory` URL
|
|
58
|
+
* list. For pages this is the HTML URL that was rendered; for resources
|
|
59
|
+
* this is a non-HTML URL handed in by the list (HEAD-fetched without
|
|
60
|
+
* rendering).
|
|
61
|
+
* - `'inventory-discovered'` — found by following links from an
|
|
62
|
+
* `inventory-seed` page, OR (for resources) loaded by puppeteer while
|
|
63
|
+
* rendering one of those pages.
|
|
64
|
+
*
|
|
65
|
+
* Used by the viewer as a badge and to indicate why a row was added.
|
|
66
|
+
* Isolation queries (`listIsolatedPages` / `listUnusedResources`) judge
|
|
67
|
+
* orphans by `referrer = 0`, NOT by this value — `source` only labels
|
|
68
|
+
* the row.
|
|
69
|
+
*/
|
|
70
|
+
export type PageSource = 'crawled' | 'inventory-seed' | 'inventory-discovered';
|
|
71
|
+
/**
|
|
72
|
+
* One row written to the `inventory_runs` audit table on each successful
|
|
73
|
+
* `--inventory <list>` invocation.
|
|
74
|
+
*
|
|
75
|
+
* Schema-mirror interface: every column on `inventory_runs` is represented
|
|
76
|
+
* here. Only `ran_at` is required — every other field is nullable so the
|
|
77
|
+
* post-merge raw-SQL backfill path (a one-off `sqlite3 INSERT` for the
|
|
78
|
+
* initial inventory pass that predated this table) can omit summary
|
|
79
|
+
* stats it cannot reconstruct.
|
|
80
|
+
*
|
|
81
|
+
* The audit log is append-only at Phase 1: there is no UPDATE path, no
|
|
82
|
+
* UNIQUE constraint on `source_file_sha256`, and no FK to pages /
|
|
83
|
+
* resources. Phase 2 (`inventory_memberships`) introduces the M:N link
|
|
84
|
+
* to URLs; Phase 3 (`--refresh`) uses `source_file_sha256` for dedupe.
|
|
85
|
+
*/
|
|
86
|
+
export interface InventoryRunMeta {
|
|
87
|
+
/** ISO 8601 timestamp at which the run completed (e.g. `'2026-06-21T11:30:00+09:00'`). */
|
|
88
|
+
ran_at: string;
|
|
89
|
+
/** Human-readable identifier (e.g. `'prod-2026-06-21'`). `null` when the caller did not supply one. */
|
|
90
|
+
list_label?: string | null;
|
|
91
|
+
/** SHA-256 hex digest of the source file. `null` if hashing failed (e.g. file vanished mid-run). */
|
|
92
|
+
source_file_sha256?: string | null;
|
|
93
|
+
/** Number of non-empty lines in the input list (= URL count before scope filtering). */
|
|
94
|
+
total_lines?: number | null;
|
|
95
|
+
/** Number of new HTML seeds inserted as `pages` rows by this run. */
|
|
96
|
+
new_pages?: number | null;
|
|
97
|
+
/** Number of new non-HTML URLs inserted as `resources` rows by this run. */
|
|
98
|
+
new_resources?: number | null;
|
|
99
|
+
/** Number of input URLs dropped because they fell outside the archived scope. */
|
|
100
|
+
scope_skipped?: number | null;
|
|
101
|
+
/** Free-form text for backfill annotations or operator notes. */
|
|
102
|
+
notes?: string | null;
|
|
103
|
+
}
|
|
49
104
|
/**
|
|
50
105
|
* Filter type for querying pages from the database.
|
|
51
106
|
*
|
|
@@ -59,7 +114,13 @@ export interface Config extends Required<Pick<ParseURLOptions, 'disableQueries'>
|
|
|
59
114
|
*/
|
|
60
115
|
export type PageFilter = 'page' | 'page-included-no-target' | 'external-page' | 'internal-page' | 'no-page' | 'external-no-page' | 'internal-no-page';
|
|
61
116
|
/**
|
|
62
|
-
* Raw database row representing a crawled page in the `pages` table.
|
|
117
|
+
* Raw database row representing a crawled page in the `pages` table (v2 schema).
|
|
118
|
+
*
|
|
119
|
+
* Maps 1:1 to the columns defined by `archive/init-schema.ts`. Most meta
|
|
120
|
+
* fields are derived from beholder 3.0.0's nested {@link import('@d-zero/beholder').Meta}
|
|
121
|
+
* via `archive/meta/derive-flat-from-meta.ts` and are stored as plain
|
|
122
|
+
* scalars for SQL-level filter / projection. The catch-all `meta_extras`
|
|
123
|
+
* JSON column preserves nested sub-objects not flattened above.
|
|
63
124
|
*/
|
|
64
125
|
export interface DB_Page {
|
|
65
126
|
/** Auto-incremented primary key. */
|
|
@@ -84,48 +145,118 @@ export interface DB_Page {
|
|
|
84
145
|
contentLength: number | null;
|
|
85
146
|
/** JSON-serialized HTTP response headers. */
|
|
86
147
|
responseHeaders: string;
|
|
87
|
-
/** The `lang` attribute value from the HTML element, or null
|
|
148
|
+
/** The `lang` attribute value from the HTML element, or null. */
|
|
88
149
|
lang: string | null;
|
|
89
|
-
/** The
|
|
150
|
+
/** The `dir` attribute value, or null. */
|
|
151
|
+
dir: string | null;
|
|
152
|
+
/** The `<meta charset>` value, or null. */
|
|
153
|
+
charset: string | null;
|
|
154
|
+
/** Absolutised `<base href>`, or null. */
|
|
155
|
+
baseHref: string | null;
|
|
156
|
+
/** The raw `<meta name="viewport">` content, or null. */
|
|
157
|
+
viewport_raw: string | null;
|
|
158
|
+
/** The primary `<meta name="theme-color">` (no media), or null. */
|
|
159
|
+
themeColor: string | null;
|
|
160
|
+
/** `<meta name="application-name">`, or null. */
|
|
161
|
+
applicationName: string | null;
|
|
162
|
+
/** `<meta name="author">`, or null. */
|
|
163
|
+
author: string | null;
|
|
164
|
+
/** `<meta name="generator">`, or null. */
|
|
165
|
+
generator: string | null;
|
|
166
|
+
/** `<meta name="publisher">`, or null. */
|
|
167
|
+
publisher: string | null;
|
|
168
|
+
/** The page title from the `<title>` element, or null. */
|
|
90
169
|
title: string | null;
|
|
91
|
-
/** The meta description content, or null
|
|
170
|
+
/** The meta description content, or null. */
|
|
92
171
|
description: string | null;
|
|
93
|
-
/** The meta keywords content, or null
|
|
172
|
+
/** The meta keywords content, or null. */
|
|
94
173
|
keywords: string | null;
|
|
95
|
-
/**
|
|
96
|
-
|
|
97
|
-
/** Whether the
|
|
98
|
-
|
|
99
|
-
/** Whether the
|
|
100
|
-
|
|
101
|
-
/**
|
|
174
|
+
/** The raw `<meta name="robots">` content, or null. */
|
|
175
|
+
robots_raw: string | null;
|
|
176
|
+
/** Whether the noindex directive is set (SQLite INTEGER 0/1). */
|
|
177
|
+
robots_noindex: number | null;
|
|
178
|
+
/** Whether the nofollow directive is set (SQLite INTEGER 0/1). */
|
|
179
|
+
robots_nofollow: number | null;
|
|
180
|
+
/** Whether the noarchive directive is set (SQLite INTEGER 0/1). */
|
|
181
|
+
robots_noarchive: number | null;
|
|
182
|
+
/** Whether the noimageindex directive is set (SQLite INTEGER 0/1). */
|
|
183
|
+
robots_noimageindex: number | null;
|
|
184
|
+
/** `<meta name="googlebot">` content, or null. */
|
|
185
|
+
googlebot: string | null;
|
|
186
|
+
/** Absolutised `<link rel="canonical">` href, or null. */
|
|
102
187
|
canonical: string | null;
|
|
103
|
-
/**
|
|
104
|
-
|
|
105
|
-
/**
|
|
188
|
+
/** Absolutised `<link rel="amphtml">` href, or null. */
|
|
189
|
+
amphtml: string | null;
|
|
190
|
+
/** Absolutised `<link rel="manifest">` href, or null. */
|
|
191
|
+
manifest: string | null;
|
|
192
|
+
/** Absolutised `<link rel="icon">` href, or null. */
|
|
193
|
+
icon_href: string | null;
|
|
194
|
+
/** Absolutised `<link rel="apple-touch-icon">` href, or null. */
|
|
195
|
+
appleTouchIcon_href: string | null;
|
|
196
|
+
/** og:type, or null. */
|
|
106
197
|
og_type: string | null;
|
|
107
|
-
/**
|
|
198
|
+
/** og:title, or null. */
|
|
108
199
|
og_title: string | null;
|
|
109
|
-
/**
|
|
200
|
+
/** Absolutised og:url, or null. */
|
|
201
|
+
og_url: string | null;
|
|
202
|
+
/** og:site_name, or null. */
|
|
110
203
|
og_site_name: string | null;
|
|
111
|
-
/**
|
|
204
|
+
/** og:description, or null. */
|
|
112
205
|
og_description: string | null;
|
|
113
|
-
/**
|
|
114
|
-
og_url: string | null;
|
|
115
|
-
/** The Open Graph image URL (`og:image`), or null if not present. */
|
|
206
|
+
/** Absolutised og:image (first if multiple), or null. */
|
|
116
207
|
og_image: string | null;
|
|
117
|
-
/**
|
|
208
|
+
/** og:image:alt, or null. */
|
|
209
|
+
og_image_alt: string | null;
|
|
210
|
+
/** og:image:width as a string (per spec), or null. */
|
|
211
|
+
og_image_width: string | null;
|
|
212
|
+
/** og:image:height as a string (per spec), or null. */
|
|
213
|
+
og_image_height: string | null;
|
|
214
|
+
/** og:locale, or null. */
|
|
215
|
+
og_locale: string | null;
|
|
216
|
+
/** og:article:published_time, or null. */
|
|
217
|
+
og_article_published_time: string | null;
|
|
218
|
+
/** og:article:modified_time, or null. */
|
|
219
|
+
og_article_modified_time: string | null;
|
|
220
|
+
/** twitter:card, or null. */
|
|
118
221
|
twitter_card: string | null;
|
|
222
|
+
/** twitter:site, or null. */
|
|
223
|
+
twitter_site: string | null;
|
|
224
|
+
/** twitter:creator, or null. */
|
|
225
|
+
twitter_creator: string | null;
|
|
226
|
+
/** twitter:title, or null. */
|
|
227
|
+
twitter_title: string | null;
|
|
228
|
+
/** twitter:description, or null. */
|
|
229
|
+
twitter_description: string | null;
|
|
230
|
+
/** Absolutised twitter:image (or twitter:image:src fallback), or null. */
|
|
231
|
+
twitter_image: string | null;
|
|
232
|
+
/** Facebook app id (`fb:app_id`), or null. */
|
|
233
|
+
fb_app_id: string | null;
|
|
234
|
+
/** Google site verification token, or null. */
|
|
235
|
+
verification_google: string | null;
|
|
236
|
+
/** `format-detection` telephone (SQLite INTEGER 0/1), or null. */
|
|
237
|
+
formatDetection_telephone: number | null;
|
|
238
|
+
/** First time this page row was inserted (UNIX ms), or null on legacy rows. */
|
|
239
|
+
firstCrawledAt: number | null;
|
|
240
|
+
/** Last successful re-scrape time (UNIX ms), or null on legacy rows. */
|
|
241
|
+
lastCrawledAt: number | null;
|
|
242
|
+
/** Number of `page_tags` rows belonging to this page. */
|
|
243
|
+
tag_count: number | null;
|
|
244
|
+
/** `meta.jsonLd.length + meta.speculationRules.length` at scrape time. */
|
|
245
|
+
jsonld_count: number | null;
|
|
246
|
+
/** Sorted unique provider names, comma-separated (empty string when none). */
|
|
247
|
+
tags_providers_csv: string | null;
|
|
248
|
+
/** JSON-serialised nested Meta sub-objects not flattened above. */
|
|
249
|
+
meta_extras: string | null;
|
|
119
250
|
/** JSON-serialized network logs captured during scraping, or null if not collected. */
|
|
120
251
|
networkLogs: string | null;
|
|
121
|
-
/** Relative file path to the saved HTML snapshot, or null if not saved. */
|
|
122
|
-
html: string | null;
|
|
123
252
|
/** Whether the page was skipped during crawling (1) or processed normally (0). */
|
|
124
253
|
isSkipped: 0 | 1;
|
|
125
254
|
/** The reason the page was skipped, or null if it was not skipped. */
|
|
126
255
|
skipReason: string | null;
|
|
127
256
|
/** The natural URL sort order index, or null if not yet assigned. */
|
|
128
257
|
order: number | null;
|
|
258
|
+
/** Provenance of the row — see {@link PageSource}. */
|
|
259
|
+
source: PageSource;
|
|
129
260
|
}
|
|
130
261
|
/**
|
|
131
262
|
* Raw database row representing a redirect relationship.
|
|
@@ -283,13 +414,24 @@ export interface DB_Resource {
|
|
|
283
414
|
cdn: string | 0;
|
|
284
415
|
/** JSON-serialized HTTP response headers, or null if not available. */
|
|
285
416
|
responseHeaders: string | null;
|
|
417
|
+
/** Provenance of the row — see {@link PageSource}. */
|
|
418
|
+
source: PageSource;
|
|
286
419
|
}
|
|
287
420
|
/**
|
|
288
421
|
* Connection options for the archive's libsql-backed database.
|
|
289
422
|
*/
|
|
290
423
|
export interface DatabaseOption {
|
|
291
|
-
/** The working directory for the database (used for resolving relative paths). */
|
|
292
|
-
workingDir: string;
|
|
293
424
|
/** The absolute file path to the SQLite database file. */
|
|
294
425
|
filename: string;
|
|
426
|
+
/**
|
|
427
|
+
* When `true`, open the database for read-only inspection:
|
|
428
|
+
*
|
|
429
|
+
* - Skip schema migrations (no `ALTER TABLE` / `UPDATE` on the user's file).
|
|
430
|
+
* - Refuse to mkdir/create the parent dir or the db file — fail loudly if
|
|
431
|
+
* either is missing instead of resurrecting them.
|
|
432
|
+
*
|
|
433
|
+
* Used by the viewer / MCP server when attaching to an in-progress crawl's
|
|
434
|
+
* tmpDir, where any write would race the live crawler.
|
|
435
|
+
*/
|
|
436
|
+
readOnly?: boolean;
|
|
295
437
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ErrorKind } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Classify a raw crawler/scraper error message into a coarse {@link ErrorKind}.
|
|
4
|
+
*
|
|
5
|
+
* Pure and deterministic: the same message always yields the same kind, which
|
|
6
|
+
* is why the kind is derived on read rather than persisted — it can be applied
|
|
7
|
+
* uniformly to freshly captured `crawl_errors`, legacy `error.log` lines, and
|
|
8
|
+
* `page_errors` alike.
|
|
9
|
+
* @param message - The raw error message (a single line is sufficient; the
|
|
10
|
+
* cause token such as `ENOTFOUND` or `Navigation timeout` lives there).
|
|
11
|
+
* @returns The matched kind, or `unknown` when no matcher applies.
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* classifyErrorKind('getaddrinfo ENOTFOUND www.example.com'); // 'dns'
|
|
15
|
+
* classifyErrorKind('gave up after 3 retries — Race 180,000ms'); // 'timeout'
|
|
16
|
+
* classifyErrorKind('Protocol error (Page.reload): Target closed'); // 'protocol'
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare function classifyErrorKind(message: string): ErrorKind;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ordered message matchers. The first pattern that matches wins, so more
|
|
3
|
+
* specific transport causes (DNS, TLS, connection-*) are tested before the
|
|
4
|
+
* broader `protocol` / `timeout` buckets — e.g. `ETIMEDOUT` must classify as
|
|
5
|
+
* `connection-timeout`, not the page-level `timeout`, and a puppeteer
|
|
6
|
+
* `Protocol error` must not be swallowed by the `timeout` matcher.
|
|
7
|
+
*/
|
|
8
|
+
const MATCHERS = [
|
|
9
|
+
// `dns-transient` must be evaluated before `dns`: an `EAI_AGAIN` line also
|
|
10
|
+
// carries the `getaddrinfo` token, so the more specific transient pattern
|
|
11
|
+
// has to win. Splitting it out from `dns` keeps the DNS-burned host cache
|
|
12
|
+
// (which marks on `kind === 'dns'`) from punishing a host whose only sin
|
|
13
|
+
// was a local resolver hiccup.
|
|
14
|
+
{ kind: 'dns-transient', pattern: /EAI_AGAIN|\bEREFUSED\b/i },
|
|
15
|
+
{
|
|
16
|
+
kind: 'dns',
|
|
17
|
+
pattern: /ENOTFOUND|getaddrinfo|ERR_NAME_NOT_RESOLVED|ERR_NAME_RESOLUTION_FAILED/i,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
kind: 'tls',
|
|
21
|
+
// `Hostname/IP does not match certificate's altnames` is Node's
|
|
22
|
+
// node:tls hostname mismatch error and is emphatically a TLS issue;
|
|
23
|
+
// adding it here (alongside the OpenSSL / Chromium tokens) keeps
|
|
24
|
+
// hosts that serve the wrong-name cert (common with misconfigured
|
|
25
|
+
// edge / load-balancer setups) out of `unknown`. `altnames` is
|
|
26
|
+
// anchored to the preceding `certificate` token so a request whose
|
|
27
|
+
// error message merely mentions a path containing the substring
|
|
28
|
+
// `altnames` (e.g. `https://api.example.com/altnames/lookup` in a
|
|
29
|
+
// 5xx body) does NOT get misclassified into `tls` (which is a
|
|
30
|
+
// `PERMANENT_ERROR_KINDS` member — a false-positive would
|
|
31
|
+
// permanently exclude that page from `--retry-failed`).
|
|
32
|
+
pattern: /ERR_CERT|ERR_SSL|\bCERT_|SSL routines|ERR_BAD_SSL|UNABLE_TO_VERIFY|unable to verify|self.signed certificate|certificate has expired\s*$|\bERR_TLS|Hostname\/IP does not match certificate|certificate'?s? altnames/i,
|
|
33
|
+
},
|
|
34
|
+
{ kind: 'connection-refused', pattern: /ECONNREFUSED|ERR_CONNECTION_REFUSED/i },
|
|
35
|
+
{
|
|
36
|
+
kind: 'connection-reset',
|
|
37
|
+
pattern: /ECONNRESET|socket hang up|ERR_CONNECTION_RESET|ERR_CONNECTION_CLOSED|ERR_EMPTY_RESPONSE/i,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
kind: 'connection-timeout',
|
|
41
|
+
pattern: /ETIMEDOUT|ERR_CONNECTION_TIMED_OUT|ERR_TIMED_OUT/i,
|
|
42
|
+
},
|
|
43
|
+
// `local-network` is evaluated AFTER the connection-* matchers so a
|
|
44
|
+
// concrete cause (refused / reset / timeout) wins when both apply. Only
|
|
45
|
+
// "local network is unreachable / changed" symptoms — and the OS-level
|
|
46
|
+
// errors that surface them — land here. Short tokens (`EPIPE`, `EREFUSED`)
|
|
47
|
+
// are word-bounded so unrelated identifiers don't false-positive.
|
|
48
|
+
{
|
|
49
|
+
kind: 'local-network',
|
|
50
|
+
pattern: /ERR_INTERNET_DISCONNECTED|ERR_NETWORK_CHANGED|ERR_NETWORK_IO_SUSPENDED|ERR_ADDRESS_UNREACHABLE|ERR_NETWORK_UNREACHABLE|ENETUNREACH|EHOSTUNREACH|EADDRNOTAVAIL|ENOTCONN|\bEPIPE\b/i,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
kind: 'parse-error',
|
|
54
|
+
pattern: /Parse Error|Expected HTTP\/|Unexpected end of stream/i,
|
|
55
|
+
},
|
|
56
|
+
// `client-blocked` covers Chromium's ERR_BLOCKED_* family — the browser
|
|
57
|
+
// actively decided to reject the request (ad/tracker heuristics, CSP,
|
|
58
|
+
// CORB / ORB, administrator block list, fingerprinting protection,
|
|
59
|
+
// cleartext policy, …). Per the upstream `net/base/net_error_list.h`,
|
|
60
|
+
// `ERR_BLOCKED_BY_CLIENT` is documented as "The client chose to block
|
|
61
|
+
// the request." — i.e. the server was never the deciding party. Listed
|
|
62
|
+
// before `protocol` so puppeteer's generic "Protocol error" wrapper
|
|
63
|
+
// (which sometimes embeds the underlying net error code) is correctly
|
|
64
|
+
// attributed to the blocked layer rather than the protocol layer.
|
|
65
|
+
{
|
|
66
|
+
kind: 'client-blocked',
|
|
67
|
+
pattern: /ERR_BLOCKED_BY_CLIENT|ERR_BLOCKED_BY_ADMINISTRATOR|ERR_BLOCKED_IN_INCOGNITO_BY_ADMINISTRATOR|ERR_BLOCKED_BY_RESPONSE|ERR_BLOCKED_BY_CSP|ERR_BLOCKED_BY_ORB|ERR_BLOCKED_BY_FINGERPRINTING_PROTECTION|ERR_CLEARTEXT_NOT_PERMITTED|ERR_NETWORK_ACCESS_REVOKED/i,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
kind: 'protocol',
|
|
71
|
+
// `detached frame` is anchored to puppeteer's exact prefix
|
|
72
|
+
// `Attempted to use detached Frame` (its current Frame.ts
|
|
73
|
+
// emitter; the `i` flag below catches the lowercase variant
|
|
74
|
+
// automatically), not the bare two-token substring. The bare
|
|
75
|
+
// form would match unrelated diagnostics like a console message
|
|
76
|
+
// "detached frame ref leaked" echoed through a logger. The older
|
|
77
|
+
// Page-domain `frame (?:was |got )?detached` form is kept as a
|
|
78
|
+
// separate alternative because Chromium still surfaces that
|
|
79
|
+
// phrasing in some legacy code paths. Without one of these, the
|
|
80
|
+
// "Attempted to use detached Frame ..." messages observed on a
|
|
81
|
+
// real archive would slip into `unknown`.
|
|
82
|
+
pattern: /Protocol error|Target closed|Session closed|Execution context was destroyed|frame (?:was |got )?detached|Attempted to use detached frame|Navigating frame was detached|Cannot find context|Node with given id|Page\.\w+ returned/i,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
kind: 'timeout',
|
|
86
|
+
// `Timeout: https?:` matches the NetTimeoutError "Timeout: <url>"
|
|
87
|
+
// form. Looking for the URL-shaped tail (rather than anchoring at
|
|
88
|
+
// line start) is what lets us catch the beholder-wrapped variant
|
|
89
|
+
// `[Retried N times] Timeout: https://...` that gets stored in
|
|
90
|
+
// `crawl_errors` / `error.log` after retry exhaustion — the bare
|
|
91
|
+
// `^Timeout:` form would only fire on the immediate failure and
|
|
92
|
+
// miss every retry-exhausted record (the ones that actually land
|
|
93
|
+
// in the archive). Required for slow-server timeouts that
|
|
94
|
+
// previously fell into `unknown`.
|
|
95
|
+
pattern: /Race \d|Navigation timeout|timeout of \d+\s*ms exceeded|TimeoutError|Timed out|Timeout: https?:/i,
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
/**
|
|
99
|
+
* Classify a raw crawler/scraper error message into a coarse {@link ErrorKind}.
|
|
100
|
+
*
|
|
101
|
+
* Pure and deterministic: the same message always yields the same kind, which
|
|
102
|
+
* is why the kind is derived on read rather than persisted — it can be applied
|
|
103
|
+
* uniformly to freshly captured `crawl_errors`, legacy `error.log` lines, and
|
|
104
|
+
* `page_errors` alike.
|
|
105
|
+
* @param message - The raw error message (a single line is sufficient; the
|
|
106
|
+
* cause token such as `ENOTFOUND` or `Navigation timeout` lives there).
|
|
107
|
+
* @returns The matched kind, or `unknown` when no matcher applies.
|
|
108
|
+
* @example
|
|
109
|
+
* ```ts
|
|
110
|
+
* classifyErrorKind('getaddrinfo ENOTFOUND www.example.com'); // 'dns'
|
|
111
|
+
* classifyErrorKind('gave up after 3 retries — Race 180,000ms'); // 'timeout'
|
|
112
|
+
* classifyErrorKind('Protocol error (Page.reload): Target closed'); // 'protocol'
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
export function classifyErrorKind(message) {
|
|
116
|
+
for (const { kind, pattern } of MATCHERS) {
|
|
117
|
+
if (pattern.test(message)) {
|
|
118
|
+
return kind;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return 'unknown';
|
|
122
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { RedirectEdgeResult } from './types.js';
|
|
2
|
+
import type { PageData } from '@d-zero/beholder';
|
|
3
|
+
import type { ExURL } from '@d-zero/shared/parse-url';
|
|
4
|
+
/**
|
|
5
|
+
* Parameters for {@link buildJsRedirectEdge}.
|
|
6
|
+
*/
|
|
7
|
+
export interface BuildJsRedirectEdgeParams {
|
|
8
|
+
/** Source URL puppeteer was asked to navigate to. */
|
|
9
|
+
url: ExURL;
|
|
10
|
+
/**
|
|
11
|
+
* Whether `url` is external to the crawl scope. Propagated onto the
|
|
12
|
+
* synthesised `PageData` so downstream `linkList.done` classifies the
|
|
13
|
+
* source the same way the original scrape would have.
|
|
14
|
+
*/
|
|
15
|
+
isExternal: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Raw error message from `BrowserScrapeResult.error`. The rescue only
|
|
18
|
+
* fires when this matches the narrow `Page.goto returned null` sentinel
|
|
19
|
+
* (see {@link isJsRedirectErrorShape}) — anything else falls through
|
|
20
|
+
* so genuine browser failures surface unchanged.
|
|
21
|
+
*/
|
|
22
|
+
errorMessage: string | null | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Value of `BrowserScrapeResult.postNavigationUrl` — the URL puppeteer's
|
|
25
|
+
* `page.url()` reported after the throw. Passed to
|
|
26
|
+
* {@link deriveJsRedirectTarget} for canonicalisation, credential strip,
|
|
27
|
+
* and scheme filtering.
|
|
28
|
+
*/
|
|
29
|
+
postNavigationUrl: string | null | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* When present, the rescue builds the redirect-edge `PageData` by
|
|
32
|
+
* spreading this HEAD-pre-flight result and overriding `redirectPaths`.
|
|
33
|
+
* Use this on the HEAD-success-then-puppeteer-fail path so the source row
|
|
34
|
+
* carries the real HTTP-level status / content-type from HEAD. When
|
|
35
|
+
* omitted, the rescue builds a synthetic placeholder via
|
|
36
|
+
* {@link linkToPageData} with `status = -1` / `statusText = errorMessage`
|
|
37
|
+
* — used on the HEAD-fail-then-puppeteer-fallback path where there is
|
|
38
|
+
* no HEAD response to draw from.
|
|
39
|
+
*/
|
|
40
|
+
headCheckResult?: PageData;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build a JS-redirect `RedirectEdgeResult` when the browser-scrape error
|
|
44
|
+
* looks like puppeteer's `page.goto() === null` shape AND `page.url()`
|
|
45
|
+
* resolved to a meaningfully-different URL.
|
|
46
|
+
*
|
|
47
|
+
* **Why this helper exists** — both rescue call sites in
|
|
48
|
+
* `Crawler.#scrapePage` (the HEAD-success-then-puppeteer-fail tail and the
|
|
49
|
+
* HEAD-fail-then-puppeteer-fallback branch inside `#sendHeadRequest`) run
|
|
50
|
+
* the same three-step recipe: classify the error shape → derive a JS target
|
|
51
|
+
* from `page.url()` → synthesise a redirect-edge result. Inlining the recipe
|
|
52
|
+
* twice means a future change to `deriveJsRedirectTarget`'s contract or the
|
|
53
|
+
* sentinel string has to be applied to both copies in sync; collapsing into
|
|
54
|
+
* a single helper keeps the rescue's invariants in one place. The
|
|
55
|
+
* intentional difference between the two call sites (whether to fold a
|
|
56
|
+
* HEAD result into the synthesised PageData or to start from a
|
|
57
|
+
* `linkToPageData` placeholder) is reduced to a single optional parameter
|
|
58
|
+
* (`headCheckResult`), preserving both shapes without behavioural drift.
|
|
59
|
+
*
|
|
60
|
+
* The helper logs at `crawlerLog` whenever it fires so operators tailing
|
|
61
|
+
* `DEBUG=Nitpicker:Crawler` see exactly which URL got rescued and to which
|
|
62
|
+
* destination — without this trail the rescue is invisible to anyone
|
|
63
|
+
* debugging "why did this 200-OK source row land in the archive as a 301?".
|
|
64
|
+
* @param params - Inputs gathered at the call site.
|
|
65
|
+
* @returns A `RedirectEdgeResult` when the rescue applies, otherwise
|
|
66
|
+
* `null` (the caller should fall through to the existing error path).
|
|
67
|
+
*/
|
|
68
|
+
export declare function buildJsRedirectEdge(params: BuildJsRedirectEdgeParams): RedirectEdgeResult | null;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { crawlerLog } from '../debug.js';
|
|
2
|
+
import { deriveJsRedirectTarget } from './derive-js-redirect-target.js';
|
|
3
|
+
import { isJsRedirectErrorShape } from './is-js-redirect-error-shape.js';
|
|
4
|
+
import { linkToPageData } from './link-to-page-data.js';
|
|
5
|
+
/**
|
|
6
|
+
* Build a JS-redirect `RedirectEdgeResult` when the browser-scrape error
|
|
7
|
+
* looks like puppeteer's `page.goto() === null` shape AND `page.url()`
|
|
8
|
+
* resolved to a meaningfully-different URL.
|
|
9
|
+
*
|
|
10
|
+
* **Why this helper exists** — both rescue call sites in
|
|
11
|
+
* `Crawler.#scrapePage` (the HEAD-success-then-puppeteer-fail tail and the
|
|
12
|
+
* HEAD-fail-then-puppeteer-fallback branch inside `#sendHeadRequest`) run
|
|
13
|
+
* the same three-step recipe: classify the error shape → derive a JS target
|
|
14
|
+
* from `page.url()` → synthesise a redirect-edge result. Inlining the recipe
|
|
15
|
+
* twice means a future change to `deriveJsRedirectTarget`'s contract or the
|
|
16
|
+
* sentinel string has to be applied to both copies in sync; collapsing into
|
|
17
|
+
* a single helper keeps the rescue's invariants in one place. The
|
|
18
|
+
* intentional difference between the two call sites (whether to fold a
|
|
19
|
+
* HEAD result into the synthesised PageData or to start from a
|
|
20
|
+
* `linkToPageData` placeholder) is reduced to a single optional parameter
|
|
21
|
+
* (`headCheckResult`), preserving both shapes without behavioural drift.
|
|
22
|
+
*
|
|
23
|
+
* The helper logs at `crawlerLog` whenever it fires so operators tailing
|
|
24
|
+
* `DEBUG=Nitpicker:Crawler` see exactly which URL got rescued and to which
|
|
25
|
+
* destination — without this trail the rescue is invisible to anyone
|
|
26
|
+
* debugging "why did this 200-OK source row land in the archive as a 301?".
|
|
27
|
+
* @param params - Inputs gathered at the call site.
|
|
28
|
+
* @returns A `RedirectEdgeResult` when the rescue applies, otherwise
|
|
29
|
+
* `null` (the caller should fall through to the existing error path).
|
|
30
|
+
*/
|
|
31
|
+
export function buildJsRedirectEdge(params) {
|
|
32
|
+
if (!isJsRedirectErrorShape(params.errorMessage)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const jsRedirectTarget = deriveJsRedirectTarget(params.url.withoutHashAndAuth, params.postNavigationUrl);
|
|
36
|
+
if (jsRedirectTarget === null) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const pageData = params.headCheckResult
|
|
40
|
+
? { ...params.headCheckResult, redirectPaths: [jsRedirectTarget] }
|
|
41
|
+
: linkToPageData({
|
|
42
|
+
url: params.url,
|
|
43
|
+
isExternal: params.isExternal,
|
|
44
|
+
isLowerLayer: false,
|
|
45
|
+
dest: {
|
|
46
|
+
redirectPaths: [jsRedirectTarget],
|
|
47
|
+
status: -1,
|
|
48
|
+
statusText: typeof params.errorMessage === 'string' ? params.errorMessage : '',
|
|
49
|
+
contentType: null,
|
|
50
|
+
contentLength: null,
|
|
51
|
+
responseHeaders: null,
|
|
52
|
+
title: '',
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
crawlerLog('JS-redirect rescue fired for %s → %s (HEAD %s)', params.url.href, jsRedirectTarget, params.headCheckResult ? 'available' : 'absent');
|
|
56
|
+
return { type: 'redirect-edge', source: 'js-redirect', pageData };
|
|
57
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CrawlerEventTypes, InventoryMode } from './types.js';
|
|
2
|
+
import type { PageData } from '../utils/types/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Build the payload for the {@link CrawlerEventTypes.redirect} event so the
|
|
5
|
+
* `{ result, source }` shape is constructed in one place. Pure function —
|
|
6
|
+
* no I/O, no side effects, just the wiring that connects
|
|
7
|
+
* {@link derivePageSource} to the emit call.
|
|
8
|
+
*
|
|
9
|
+
* Exists so a unit test can pin the wiring directly. Before extraction
|
|
10
|
+
* the assembly lived inline in `Crawler.#scrape`, which meant the only
|
|
11
|
+
* way to catch "someone deletes the `source` field" or "someone passes
|
|
12
|
+
* `inventoryMode` where it should pass `derivePageSource(...)`" was the
|
|
13
|
+
* inventory E2E. Now a single-purpose spec exercises the construction.
|
|
14
|
+
*
|
|
15
|
+
* The originating URL passed in `pageUrlWithoutHashAndAuth` must be the
|
|
16
|
+
* URL the redirect chain STARTS from (the page being scraped), not the
|
|
17
|
+
* destination — see {@link Database.recordRedirect} JSDoc for why
|
|
18
|
+
* lineage propagates from origin, not from the destination.
|
|
19
|
+
* @param pageData - HEAD-resolved page data carrying the redirect chain.
|
|
20
|
+
* @param inventoryMode - Inventory-mode config from `CrawlerOptions.inventoryMode`, or `null` outside `--inventory`.
|
|
21
|
+
* @param pageUrlWithoutHashAndAuth - The originating page URL keyed by `withoutHashAndAuth`.
|
|
22
|
+
* @returns The exact event payload to feed `emit('redirect', ...)`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildRedirectEvent(pageData: PageData, inventoryMode: InventoryMode | null, pageUrlWithoutHashAndAuth: string): CrawlerEventTypes['redirect'];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { derivePageSource } from './derive-page-source.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build the payload for the {@link CrawlerEventTypes.redirect} event so the
|
|
4
|
+
* `{ result, source }` shape is constructed in one place. Pure function —
|
|
5
|
+
* no I/O, no side effects, just the wiring that connects
|
|
6
|
+
* {@link derivePageSource} to the emit call.
|
|
7
|
+
*
|
|
8
|
+
* Exists so a unit test can pin the wiring directly. Before extraction
|
|
9
|
+
* the assembly lived inline in `Crawler.#scrape`, which meant the only
|
|
10
|
+
* way to catch "someone deletes the `source` field" or "someone passes
|
|
11
|
+
* `inventoryMode` where it should pass `derivePageSource(...)`" was the
|
|
12
|
+
* inventory E2E. Now a single-purpose spec exercises the construction.
|
|
13
|
+
*
|
|
14
|
+
* The originating URL passed in `pageUrlWithoutHashAndAuth` must be the
|
|
15
|
+
* URL the redirect chain STARTS from (the page being scraped), not the
|
|
16
|
+
* destination — see {@link Database.recordRedirect} JSDoc for why
|
|
17
|
+
* lineage propagates from origin, not from the destination.
|
|
18
|
+
* @param pageData - HEAD-resolved page data carrying the redirect chain.
|
|
19
|
+
* @param inventoryMode - Inventory-mode config from `CrawlerOptions.inventoryMode`, or `null` outside `--inventory`.
|
|
20
|
+
* @param pageUrlWithoutHashAndAuth - The originating page URL keyed by `withoutHashAndAuth`.
|
|
21
|
+
* @returns The exact event payload to feed `emit('redirect', ...)`.
|
|
22
|
+
*/
|
|
23
|
+
export function buildRedirectEvent(pageData, inventoryMode, pageUrlWithoutHashAndAuth) {
|
|
24
|
+
return {
|
|
25
|
+
result: pageData,
|
|
26
|
+
source: derivePageSource(inventoryMode, pageUrlWithoutHashAndAuth),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clears the DNS-burned host cache and resets the short-circuit counter.
|
|
3
|
+
* Called between crawl sessions, alongside {@link clearDestinationCache}, to
|
|
4
|
+
* avoid leaking state from a previous session into a fresh one.
|
|
5
|
+
*/
|
|
6
|
+
export declare function clearDnsBurnedHostCache(): void;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { dnsBurnedHostCache } from './dns-burned-host-cache.js';
|
|
2
|
+
import { dnsBurnedHostShortCircuitCounter } from './dns-burned-host-short-circuit-counter.js';
|
|
3
|
+
/**
|
|
4
|
+
* Clears the DNS-burned host cache and resets the short-circuit counter.
|
|
5
|
+
* Called between crawl sessions, alongside {@link clearDestinationCache}, to
|
|
6
|
+
* avoid leaking state from a previous session into a fresh one.
|
|
7
|
+
*/
|
|
8
|
+
export function clearDnsBurnedHostCache() {
|
|
9
|
+
dnsBurnedHostCache.clear();
|
|
10
|
+
dnsBurnedHostShortCircuitCounter.count = 0;
|
|
11
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal structural subset of a Puppeteer `Browser` required to close it safely.
|
|
3
|
+
*
|
|
4
|
+
* Declared structurally (rather than importing Puppeteer's `Browser`) so the
|
|
5
|
+
* function stays unit-testable with a lightweight stub and free of a Puppeteer
|
|
6
|
+
* import.
|
|
7
|
+
*/
|
|
8
|
+
export interface ClosableBrowser {
|
|
9
|
+
/** Gracefully closes the browser and all of its pages over the CDP connection. */
|
|
10
|
+
close(): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Returns the underlying Chromium child process, or `null` when the browser
|
|
13
|
+
* was connected to (rather than launched) and therefore owns no process.
|
|
14
|
+
*/
|
|
15
|
+
process(): {
|
|
16
|
+
/** PID of the Chromium parent process, or `undefined` before spawn settles. */
|
|
17
|
+
readonly pid?: number;
|
|
18
|
+
/** Sends a signal to the process; returns whether it was delivered. */
|
|
19
|
+
kill(signal?: NodeJS.Signals | number): boolean;
|
|
20
|
+
/** Whether a signal has already been successfully sent to the process. */
|
|
21
|
+
readonly killed: boolean;
|
|
22
|
+
} | null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Dependency overrides for {@link closeBrowserSafely}. Used only by tests
|
|
26
|
+
* to substitute the tree-kill orchestration.
|
|
27
|
+
*/
|
|
28
|
+
export interface CloseBrowserSafelyDeps {
|
|
29
|
+
/**
|
|
30
|
+
* Kills a process and every descendant. Defaults to {@link killProcessTree}.
|
|
31
|
+
*/
|
|
32
|
+
killTree?: (pid: number, signal: NodeJS.Signals | number) => Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Closes a Puppeteer browser, falling back to a hard tree-kill if the graceful
|
|
36
|
+
* close hangs.
|
|
37
|
+
*
|
|
38
|
+
* WHY: When a page's Chromium session dies mid-scrape (e.g. a viewport change
|
|
39
|
+
* detaches the frame, surfacing `Attempted to use detached Frame` or
|
|
40
|
+
* `Session closed`), the CDP connection can be left wedged. A bare
|
|
41
|
+
* `await browser.close()` then never settles, stalling the `deal()` worker and
|
|
42
|
+
* hanging the whole crawl. Racing the close against a timeout and SIGKILLing
|
|
43
|
+
* the Chromium process tree (parent + renderer/network/zygote children) on
|
|
44
|
+
* expiry guarantees the worker always completes and no orphan subprocesses are
|
|
45
|
+
* left behind.
|
|
46
|
+
*
|
|
47
|
+
* The losing timer is cleared explicitly in `.finally()` so it never keeps the
|
|
48
|
+
* event loop alive after the race settles (a plain `delay()` in `Promise.race`
|
|
49
|
+
* would leak the timer until it fires).
|
|
50
|
+
*
|
|
51
|
+
* The tree-kill happens via {@link killProcessTree}, which enumerates
|
|
52
|
+
* descendants through `ps` (POSIX) or delegates to `taskkill /T /F` (Windows).
|
|
53
|
+
* `childProcess.kill('SIGKILL')` is still invoked on the parent up-front
|
|
54
|
+
* because Node's `ChildProcess.killed` flag governs how Node treats the
|
|
55
|
+
* spawn handle (reaping etc.); without it the parent would linger in Node's
|
|
56
|
+
* process table even after the OS-level kill.
|
|
57
|
+
* @param browser - The browser to close.
|
|
58
|
+
* @param timeoutMs - Milliseconds to wait for a graceful close before force-killing.
|
|
59
|
+
* Defaults to {@link DEFAULT_CLOSE_TIMEOUT_MS}.
|
|
60
|
+
* @param deps - Test-time overrides (default-free for production callers).
|
|
61
|
+
* @returns `true` if the graceful close timed out (and a tree-kill was
|
|
62
|
+
* attempted), `false` if `close()` settled in time.
|
|
63
|
+
*/
|
|
64
|
+
export declare function closeBrowserSafely(browser: ClosableBrowser, timeoutMs?: number, deps?: CloseBrowserSafelyDeps): Promise<boolean>;
|