@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/crawler/types.d.ts
CHANGED
|
@@ -1,6 +1,93 @@
|
|
|
1
|
+
import type { PageSource } from '../archive/types.js';
|
|
1
2
|
import type { PageData, CrawlerError, Resource } from '../utils/types/types.js';
|
|
2
|
-
import type { ChangePhaseEvent } from '@d-zero/beholder';
|
|
3
|
+
import type { ChangePhaseEvent, ScrapeResult } from '@d-zero/beholder';
|
|
3
4
|
import type { ParseURLOptions } from '@d-zero/shared/parse-url';
|
|
5
|
+
/**
|
|
6
|
+
* Result of resolving a URL that redirects to a destination already rendered
|
|
7
|
+
* during this crawl (#73). The crawler records the redirect edge only and skips
|
|
8
|
+
* launching the browser, so the destination is never re-rendered.
|
|
9
|
+
*/
|
|
10
|
+
export interface RedirectEdgeResult {
|
|
11
|
+
/** Discriminant marking this as a redirect-edge-only outcome. */
|
|
12
|
+
type: 'redirect-edge';
|
|
13
|
+
/** HEAD-resolved page data carrying the redirect chain (source → destination). */
|
|
14
|
+
pageData: PageData;
|
|
15
|
+
/**
|
|
16
|
+
* Where the chain came from. The caller needs this to decide whether the
|
|
17
|
+
* URLs in `pageData.redirectPaths` are already-known (HTTP chain — every
|
|
18
|
+
* hop was followed by the browser/HEAD pre-flight and the destination is
|
|
19
|
+
* already rendered) or brand-new (JS redirect — only the source was
|
|
20
|
+
* processed, the destination came out of `page.url()` and has never been
|
|
21
|
+
* touched).
|
|
22
|
+
*
|
|
23
|
+
* - `'http-chain'` — Returned when the HEAD pre-flight resolved a real
|
|
24
|
+
* 3xx chain and the destination has already been claimed via
|
|
25
|
+
* `#scrapedDestinations`. The crawler folds every URL in
|
|
26
|
+
* `redirectPaths` into the link-list's done-set; the destination is not
|
|
27
|
+
* re-enqueued because it is already in the archive.
|
|
28
|
+
* - `'js-redirect'` — Returned when `scraper.scrapeStart` threw because
|
|
29
|
+
* `page.goto()` resolved to `null` (client-side
|
|
30
|
+
* `window.location.replace()` / `<meta http-equiv="refresh">`) and
|
|
31
|
+
* `page.url()` exposed a different destination. `redirectPaths`
|
|
32
|
+
* contains exactly one URL: the JS-redirect target. The crawler MUST
|
|
33
|
+
* enqueue that destination so it reaches the browser, and MUST NOT
|
|
34
|
+
* fold it into the done-set (otherwise the dealer's `seen` rejects
|
|
35
|
+
* the push and the destination is silently lost from the archive).
|
|
36
|
+
*/
|
|
37
|
+
source: 'http-chain' | 'js-redirect';
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The outcome of {@link Crawler.#scrapePage}: either a normal scrape result from
|
|
41
|
+
* the browser/HEAD pipeline, or a {@link RedirectEdgeResult} when the URL's
|
|
42
|
+
* redirect destination was already rendered and only the edge needs recording.
|
|
43
|
+
*/
|
|
44
|
+
export type ScrapeOutcome = ScrapeResult | RedirectEdgeResult;
|
|
45
|
+
/**
|
|
46
|
+
* Internal envelope returned by {@link Crawler.#launchBrowserAndScrape} that
|
|
47
|
+
* augments beholder's {@link ScrapeResult} with the puppeteer-side
|
|
48
|
+
* post-navigation URL.
|
|
49
|
+
*
|
|
50
|
+
* **Why:** when `scraper.scrapeStart` throws because `page.goto()` resolved to
|
|
51
|
+
* `null` (the classic puppeteer symptom of a client-side
|
|
52
|
+
* `window.location.replace()` / meta-refresh firing mid-navigation), the only
|
|
53
|
+
* authoritative source for the URL the browser actually landed on is
|
|
54
|
+
* `page.url()` — neither the HEAD pre-flight nor the thrown error carries it.
|
|
55
|
+
* Capturing it here lets `#scrapePage` fold the source into a redirect edge
|
|
56
|
+
* instead of recording a hard `status = -1` that `--retry-failed` would chase
|
|
57
|
+
* forever (`Page.goto returned null` classifies as `protocol`, which is neither
|
|
58
|
+
* permanent nor a puppeteer-fallback kind — so the SQL filter resets it every
|
|
59
|
+
* pass and the next pass replays the same failure).
|
|
60
|
+
*
|
|
61
|
+
* `postNavigationUrl` is optional because:
|
|
62
|
+
* - successful / skipped outcomes do not need it (the success path already
|
|
63
|
+
* exposes the final URL via `pageData.url` + `redirectPaths`);
|
|
64
|
+
* - capturing can itself fail when the underlying browser context is already
|
|
65
|
+
* torn down (target closed, session killed) — we treat that as "no extra
|
|
66
|
+
* information" and fall through to the existing error path.
|
|
67
|
+
*/
|
|
68
|
+
export type BrowserScrapeResult = ScrapeResult & {
|
|
69
|
+
/**
|
|
70
|
+
* URL puppeteer reports via `page.url()` *after* a thrown navigation.
|
|
71
|
+
*
|
|
72
|
+
* Semantically only meaningful when the parent result is `type: 'error'`
|
|
73
|
+
* — `#launchBrowserAndScrape` sets it from inside its catch arm, and the
|
|
74
|
+
* success / skipped paths never write to it. The field is typed as
|
|
75
|
+
* optional on the whole envelope rather than narrowed to the error
|
|
76
|
+
* variant because beholder's `ScrapeResult` is not a discriminated
|
|
77
|
+
* union (all variants share the same shape and disambiguate via
|
|
78
|
+
* `type`), so narrowing here would force a parallel ad-hoc union with
|
|
79
|
+
* no compile-time payoff. Consumers MUST therefore check
|
|
80
|
+
* `result.type === 'error'` before reading `postNavigationUrl` — and in
|
|
81
|
+
* practice the only consumer is the JS-redirect rescue, which does
|
|
82
|
+
* exactly that.
|
|
83
|
+
*
|
|
84
|
+
* Consumers should also confirm the URL is meaningful via
|
|
85
|
+
* {@link deriveJsRedirectTarget} — `about:blank`, identity values,
|
|
86
|
+
* case-only or trailing-slash variants are all filtered there, not
|
|
87
|
+
* here.
|
|
88
|
+
*/
|
|
89
|
+
postNavigationUrl?: string;
|
|
90
|
+
};
|
|
4
91
|
/**
|
|
5
92
|
* Configuration options that control crawler behavior.
|
|
6
93
|
*
|
|
@@ -42,6 +129,89 @@ export interface CrawlerOptions extends Required<Pick<ParseURLOptions, 'disableQ
|
|
|
42
129
|
userAgent: string;
|
|
43
130
|
/** Whether to ignore robots.txt restrictions. */
|
|
44
131
|
ignoreRobots: boolean;
|
|
132
|
+
/**
|
|
133
|
+
* Lookup for previously captured sub-resources, or `null` to disable the
|
|
134
|
+
* resource-reuse optimization. See {@link ResourceLookup}.
|
|
135
|
+
*/
|
|
136
|
+
lookupResource: ResourceLookup | null;
|
|
137
|
+
/**
|
|
138
|
+
* Lookup for an already-persisted page's `source` column, or `null` when
|
|
139
|
+
* lineage propagation across sessions is not required. See
|
|
140
|
+
* {@link PageSourceLookup}.
|
|
141
|
+
*
|
|
142
|
+
* Injected by the orchestrator so that `#scrapePage` can resolve the
|
|
143
|
+
* parent's source on `--resume` / `--retry-failed` paths, where
|
|
144
|
+
* `inventoryMode` is not persisted but the page's `source` column is.
|
|
145
|
+
* Without this, sub-resources captured during a re-render of an
|
|
146
|
+
* inventory-labelled page would fall back to the DB DEFAULT `'crawled'`
|
|
147
|
+
* and lose their `'inventory-discovered'` provenance.
|
|
148
|
+
*/
|
|
149
|
+
lookupPageSource: PageSourceLookup | null;
|
|
150
|
+
/**
|
|
151
|
+
* When non-null, the crawler is running in `--inventory` mode. New page
|
|
152
|
+
* rows whose URL matches `seedUrls` are labelled `'inventory-seed'`;
|
|
153
|
+
* every other newly-inserted page or sub-resource is labelled
|
|
154
|
+
* `'inventory-discovered'`. When `null`, no source label is emitted —
|
|
155
|
+
* the DB DEFAULT `'crawled'` applies.
|
|
156
|
+
*/
|
|
157
|
+
inventoryMode: InventoryMode | null;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Inventory-mode runtime configuration. Passed from
|
|
161
|
+
* `CrawlerOrchestrator.inventory` into the Crawler so the emit pipeline can
|
|
162
|
+
* label new rows with the correct {@link PageSource}.
|
|
163
|
+
*/
|
|
164
|
+
export interface InventoryMode {
|
|
165
|
+
/**
|
|
166
|
+
* URLs explicitly listed in the user-supplied URL file, keyed by their
|
|
167
|
+
* `withoutHashAndAuth` form (so credentials in the URL don't break the
|
|
168
|
+
* match). Membership decides `inventory-seed` vs `inventory-discovered`
|
|
169
|
+
* for HTML pages.
|
|
170
|
+
*/
|
|
171
|
+
seedUrls: ReadonlySet<string>;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Looks up a previously captured sub-resource by URL.
|
|
175
|
+
*
|
|
176
|
+
* Injected by the orchestrator so that the crawler can reuse network data
|
|
177
|
+
* recorded during page rendering instead of issuing a redundant HEAD
|
|
178
|
+
* pre-flight request. Implementations must serialize the read against any
|
|
179
|
+
* pending resource writes (e.g., via the orchestrator's WriteQueue).
|
|
180
|
+
* @param urls - URL candidates to match (e.g., with and without auth credentials).
|
|
181
|
+
* @returns The recorded resource data, or `null` when no row matches.
|
|
182
|
+
*/
|
|
183
|
+
export type ResourceLookup = (urls: readonly string[]) => Promise<ResourceLookupResult | null>;
|
|
184
|
+
/**
|
|
185
|
+
* Looks up the `source` column of a previously persisted page by URL.
|
|
186
|
+
*
|
|
187
|
+
* Returns `undefined` when no row matches (e.g. a freshly-discovered URL
|
|
188
|
+
* that has not been INSERTed yet) so the caller can fall through to its
|
|
189
|
+
* default behaviour.
|
|
190
|
+
*
|
|
191
|
+
* Used by {@link Crawler} during sub-resource lineage propagation:
|
|
192
|
+
* `#scrapePage` consults this once per page to resolve the parent's
|
|
193
|
+
* lineage when the in-memory `inventoryMode` is unavailable (i.e. on
|
|
194
|
+
* `--resume` / `--retry-failed` sessions where inventory state lives only
|
|
195
|
+
* in the DB).
|
|
196
|
+
* @param urlWithoutHashAndAuth - The URL key (`url.withoutHashAndAuth` form) to look up.
|
|
197
|
+
* @returns The recorded `source`, or `undefined` when no matching row exists.
|
|
198
|
+
*/
|
|
199
|
+
export type PageSourceLookup = (urlWithoutHashAndAuth: string) => Promise<PageSource | undefined>;
|
|
200
|
+
/**
|
|
201
|
+
* Minimal sub-resource data needed to synthesize {@link PageData}
|
|
202
|
+
* without performing a network fetch.
|
|
203
|
+
*/
|
|
204
|
+
export interface ResourceLookupResult {
|
|
205
|
+
/** HTTP status code of the recorded response, or `null` if unknown. */
|
|
206
|
+
status: number | null;
|
|
207
|
+
/** HTTP status text of the recorded response, or `null` if unknown. */
|
|
208
|
+
statusText: string | null;
|
|
209
|
+
/** The Content-Type header value (media type only), or `null` if unknown. */
|
|
210
|
+
contentType: string | null;
|
|
211
|
+
/** The Content-Length header value in bytes, or `null` if unknown. */
|
|
212
|
+
contentLength: number | null;
|
|
213
|
+
/** Raw HTTP response headers, or `null` if unavailable. */
|
|
214
|
+
responseHeaders: Record<string, string | string[] | undefined> | null;
|
|
45
215
|
}
|
|
46
216
|
/**
|
|
47
217
|
* Describes a detected pagination pattern between two consecutive URLs.
|
|
@@ -67,6 +237,12 @@ export interface CrawlerEventTypes {
|
|
|
67
237
|
page: {
|
|
68
238
|
/** The scraped page data including HTML, metadata, anchors, and images. */
|
|
69
239
|
result: PageData;
|
|
240
|
+
/**
|
|
241
|
+
* Inventory provenance to write to `pages.source` when this row is new.
|
|
242
|
+
* `undefined` means the DB default (`'crawled'`) applies, which is the
|
|
243
|
+
* common case outside `crawl --inventory`. See {@link PageSource}.
|
|
244
|
+
*/
|
|
245
|
+
source?: PageSource;
|
|
70
246
|
};
|
|
71
247
|
/**
|
|
72
248
|
* Emitted when an external page (outside the crawl scope) has been scraped.
|
|
@@ -74,6 +250,8 @@ export interface CrawlerEventTypes {
|
|
|
74
250
|
externalPage: {
|
|
75
251
|
/** The scraped page data for the external page. */
|
|
76
252
|
result: PageData;
|
|
253
|
+
/** Inventory provenance for new rows — see {@link CrawlerEventTypes.page.source}. */
|
|
254
|
+
source?: PageSource;
|
|
77
255
|
};
|
|
78
256
|
/**
|
|
79
257
|
* Emitted when a URL is skipped due to exclusion rules, robots.txt restrictions,
|
|
@@ -93,6 +271,14 @@ export interface CrawlerEventTypes {
|
|
|
93
271
|
response: {
|
|
94
272
|
/** The captured resource data including URL, status, content type, and headers. */
|
|
95
273
|
resource: Resource;
|
|
274
|
+
/**
|
|
275
|
+
* Inventory provenance to write to `resources.source` when this row is new.
|
|
276
|
+
* Sub-resources discovered while puppeteer renders an inventory-seed
|
|
277
|
+
* page are always `'inventory-discovered'` (a sub-resource is never
|
|
278
|
+
* itself a seed). `undefined` means the DB default (`'crawled'`)
|
|
279
|
+
* applies. See {@link PageSource}.
|
|
280
|
+
*/
|
|
281
|
+
source?: PageSource;
|
|
96
282
|
};
|
|
97
283
|
/**
|
|
98
284
|
* Emitted to record the relationship between a page and a resource it references.
|
|
@@ -116,4 +302,46 @@ export interface CrawlerEventTypes {
|
|
|
116
302
|
* (e.g., scrapeStart, headRequest, openPage, success).
|
|
117
303
|
*/
|
|
118
304
|
changePhase: ChangePhaseEvent;
|
|
305
|
+
/**
|
|
306
|
+
* Emitted when a secondary scrape step fails for a URL but the page itself
|
|
307
|
+
* is otherwise scraped successfully (e.g. a viewport switch in
|
|
308
|
+
* `#fetchImages` detaches the frame and `@retryable` gives up). The
|
|
309
|
+
* orchestrator persists these as `page_errors` rows so the failure is
|
|
310
|
+
* visible in the archive instead of being lost to stdout logs.
|
|
311
|
+
*
|
|
312
|
+
* For ordering, this event is always emitted AFTER `page` / `externalPage`
|
|
313
|
+
* for the same URL, so the orchestrator's WriteQueue serialises the
|
|
314
|
+
* `pages` upsert before the `page_errors` insert and the FK resolution
|
|
315
|
+
* via URL succeeds.
|
|
316
|
+
*/
|
|
317
|
+
pageError: {
|
|
318
|
+
/** URL of the affected page. */
|
|
319
|
+
url: string;
|
|
320
|
+
/** Scrape phase name (typically `'retryExhausted'`). */
|
|
321
|
+
phase: string;
|
|
322
|
+
/** Human-readable failure message. */
|
|
323
|
+
message: string;
|
|
324
|
+
/** Whether the URL is external to the crawl scope. */
|
|
325
|
+
isExternal: boolean;
|
|
326
|
+
};
|
|
327
|
+
/**
|
|
328
|
+
* Emitted when a URL redirects to a destination that has already been
|
|
329
|
+
* rendered during this crawl, so only the redirect edge is recorded and the
|
|
330
|
+
* destination is not re-rendered (#73). The orchestrator persists this via
|
|
331
|
+
* `Archive.setRedirect`, which writes the edge without overwriting the
|
|
332
|
+
* destination's content.
|
|
333
|
+
*/
|
|
334
|
+
redirect: {
|
|
335
|
+
/** HEAD-resolved page data carrying the redirect chain (source → destination). */
|
|
336
|
+
result: PageData;
|
|
337
|
+
/**
|
|
338
|
+
* Inventory provenance for the redirect-edge call. Forwarded by the
|
|
339
|
+
* orchestrator to `Archive.setRedirect` → `Database.recordRedirect`
|
|
340
|
+
* so brand-new destination rows INSERTed by the edge-only path pick
|
|
341
|
+
* up the inventory label (and propagate it to intermediates) when
|
|
342
|
+
* the originating chain is in the inventory chain. `undefined` keeps
|
|
343
|
+
* the DB DEFAULT `'crawled'`.
|
|
344
|
+
*/
|
|
345
|
+
source: PageSource | undefined;
|
|
346
|
+
};
|
|
119
347
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Config } from './archive/types.js';
|
|
2
|
+
import type { InventoryMode } from './crawler/types.js';
|
|
2
3
|
import type { CrawlEvent } from './types.js';
|
|
3
4
|
import type { ExURL } from '@d-zero/shared/parse-url';
|
|
4
5
|
import { TypedAwaitEventEmitter as EventEmitter } from '@d-zero/shared/typed-await-event-emitter';
|
|
@@ -38,6 +39,12 @@ interface CrawlConfig extends Config {
|
|
|
38
39
|
userAgent: string;
|
|
39
40
|
/** Whether to ignore robots.txt restrictions. */
|
|
40
41
|
ignoreRobots: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Inventory-mode runtime configuration (see {@link InventoryMode}). Set
|
|
44
|
+
* by {@link CrawlerOrchestrator.inventory}; the default crawl path leaves
|
|
45
|
+
* this `null` so new rows are labelled `'crawled'` by the DB DEFAULT.
|
|
46
|
+
*/
|
|
47
|
+
inventoryMode: InventoryMode | null;
|
|
41
48
|
}
|
|
42
49
|
/**
|
|
43
50
|
* Callback invoked after the CrawlerOrchestrator instance is fully initialized
|
|
@@ -82,11 +89,17 @@ export declare class CrawlerOrchestrator extends EventEmitter<CrawlEvent> {
|
|
|
82
89
|
* Sets up event listeners on the crawler, starts crawling, and resolves
|
|
83
90
|
* when the crawl completes. Discovered pages, external pages, skipped pages,
|
|
84
91
|
* and resources are forwarded to the archive for storage.
|
|
85
|
-
* @param list - The list of parsed URLs to crawl.
|
|
92
|
+
* @param list - The list of parsed URLs to crawl. May be empty when a resumed
|
|
93
|
+
* session already has pending pages queued (for example `--retry-failed`).
|
|
94
|
+
* @param opts - Optional crawl overrides.
|
|
95
|
+
* @param opts.recursive - Whether discovered URLs are followed. Defaults to
|
|
96
|
+
* `!fromList` (recursive unless the archive was created from a URL list), so
|
|
97
|
+
* existing callers keep their behaviour; the retry flow passes it explicitly.
|
|
86
98
|
* @returns A promise that resolves when crawling is complete.
|
|
87
|
-
* @throws {Error} If the URL list is empty.
|
|
88
99
|
*/
|
|
89
|
-
crawling(list: ExURL[]
|
|
100
|
+
crawling(list: ExURL[], opts?: {
|
|
101
|
+
recursive?: boolean;
|
|
102
|
+
}): Promise<void>;
|
|
90
103
|
/**
|
|
91
104
|
* Kill any zombie Chromium processes that were not properly cleaned up.
|
|
92
105
|
*
|
|
@@ -146,6 +159,95 @@ export declare class CrawlerOrchestrator extends EventEmitter<CrawlEvent> {
|
|
|
146
159
|
* @throws {Error} When `newUrls` is empty, the archive is in list mode, or it cannot be parsed.
|
|
147
160
|
*/
|
|
148
161
|
static append(archivePath: string, newUrls: string[], options?: Partial<CrawlConfig>, initializedCallback?: CrawlInitializedCallback): Promise<CrawlerOrchestrator>;
|
|
162
|
+
/**
|
|
163
|
+
* Inventory mode: cross-reference a user-supplied URL list against an
|
|
164
|
+
* existing `.nitpicker` archive and import ONLY the URLs that are not yet
|
|
165
|
+
* tracked there. Designed to surface "orphan" landing pages that link
|
|
166
|
+
* graph traversal could not reach, and "unused" server-side files that
|
|
167
|
+
* no crawled page references — both of which the
|
|
168
|
+
* `listIsolatedPages` / `listUnusedResources` queries can then list.
|
|
169
|
+
*
|
|
170
|
+
* Flow:
|
|
171
|
+
*
|
|
172
|
+
* 1. Open the archive (writer mode, takes the archive lock).
|
|
173
|
+
* 2. Reject list-mode archives — they hold metadata-only rows that
|
|
174
|
+
* inventory has no business touching.
|
|
175
|
+
* 3. Reject archives with unfinished `pending` URLs — those would inherit
|
|
176
|
+
* the inventory `source` label by mistake. Operator must resume /
|
|
177
|
+
* retry-failed first.
|
|
178
|
+
* 4. Parse the URL list. Anything outside the archived scope is warned
|
|
179
|
+
* and skipped (inventory is per-server by design).
|
|
180
|
+
* 5. Subtract URLs that already exist in `pages` or `resources` so the
|
|
181
|
+
* second (and N-th) inventory pass is a no-op for known rows — keeps
|
|
182
|
+
* `'inventory-seed'` rows from being silently demoted.
|
|
183
|
+
* 6. Make `<archive>.bak`. Anything thrown beyond this point restores
|
|
184
|
+
* from the backup.
|
|
185
|
+
* 7. HEAD-probe each novel URL. Responses classified as HTML are queued
|
|
186
|
+
* as Crawler seeds (`'inventory-seed'`); everything else is recorded
|
|
187
|
+
* in `resources` directly as `'inventory-seed'` (no browser launch).
|
|
188
|
+
* 8. If any HTML seeds exist, start a Crawler with
|
|
189
|
+
* `inventoryMode = { seedUrls }` so the rendered page and every newly
|
|
190
|
+
* discovered downstream link is labelled correctly. `resume` is fed
|
|
191
|
+
* the existing `scraped` / `resources` sets so links into already-
|
|
192
|
+
* crawled pages stop at the seen-gate without re-rendering.
|
|
193
|
+
* 9. Drop the backup on success; restore it on any throw.
|
|
194
|
+
*
|
|
195
|
+
* Mutually exclusive with `--append` / `--retry-failed` / `--resume` /
|
|
196
|
+
* `--diff` / `--list` / `--list-file` / `--single` / `--output` — the
|
|
197
|
+
* CLI dispatch enforces this; this method assumes the caller honoured
|
|
198
|
+
* the contract.
|
|
199
|
+
* @param archivePath - Absolute or cwd-relative path to the `.nitpicker` archive.
|
|
200
|
+
* @param inventoryUrls - Pre-read URL list (one URL per element).
|
|
201
|
+
* @param options - Optional config overrides — most callers leave this blank and let the archived config flow through.
|
|
202
|
+
* @param initializedCallback - Hook invoked once the orchestrator is constructed but before `crawling` runs (the CLI uses it to attach progress reporting).
|
|
203
|
+
* @param sourceFileSha256 - **Pre-computed** SHA-256 hex digest of the
|
|
204
|
+
* source URL list. The orchestrator deliberately does NOT receive
|
|
205
|
+
* the file path: the path is privacy-sensitive (leaks user-home /
|
|
206
|
+
* OS structure when archives are shared) and we want it lifted off
|
|
207
|
+
* this boundary so no future log line / breadcrumb / error message
|
|
208
|
+
* inside the orchestrator can accidentally re-leak it. The CLI
|
|
209
|
+
* computes the digest via `computeFileSha256(resolvedListFile)`
|
|
210
|
+
* and passes it through here. Pass `null` for programmatic
|
|
211
|
+
* callers that built `inventoryUrls` in-memory; the audit row's
|
|
212
|
+
* `source_file_sha256` column will be `NULL`.
|
|
213
|
+
* @returns The orchestrator instance after a successful inventory pass.
|
|
214
|
+
* @throws {Error} When `inventoryUrls` is empty, the archive is in list mode, or pending URLs from a previous crawl remain unresolved.
|
|
215
|
+
*/
|
|
216
|
+
static inventory(archivePath: string, inventoryUrls: string[], options?: Partial<CrawlConfig>, initializedCallback?: CrawlInitializedCallback, sourceFileSha256?: string | null): Promise<CrawlerOrchestrator>;
|
|
217
|
+
/**
|
|
218
|
+
* Re-fetch previously-failed pages in an existing `.nitpicker` archive.
|
|
219
|
+
*
|
|
220
|
+
* Opens the archive, resets every page whose previous attempt ended in a
|
|
221
|
+
* recoverable failure (missing status / content type, or a 5xx status — see
|
|
222
|
+
* {@link Archive.resetFailedPages}) back to pending, and resumes crawling.
|
|
223
|
+
* The archived crawl configuration is reused — scopes, excludes, keywords,
|
|
224
|
+
* user agent, etc. — so the retry honours the original crawl boundaries
|
|
225
|
+
* unless a field is explicitly overridden via `options`. The exception is
|
|
226
|
+
* `recursive`: it is taken from `options` (the CLI flag defaults it to
|
|
227
|
+
* `true`) rather than inherited from the archive, so a retry decides afresh
|
|
228
|
+
* whether to follow newly-discovered URLs regardless of how the original
|
|
229
|
+
* crawl was run.
|
|
230
|
+
*
|
|
231
|
+
* When `recursive` is enabled (the default), newly-discovered URLs from the
|
|
232
|
+
* re-fetched pages are followed and crawled from scratch; when disabled, only
|
|
233
|
+
* the failed pages themselves are re-fetched. The archived roots seed the
|
|
234
|
+
* crawl scope while the reset pages are picked up through the resumed pending
|
|
235
|
+
* set, so failed external pages stay external (metadata-only) instead of being
|
|
236
|
+
* promoted into scope, and a failed root is re-fetched in place.
|
|
237
|
+
*
|
|
238
|
+
* A `<archive>.bak` is created before any DB mutation and removed on success;
|
|
239
|
+
* if the crawl throws, the backup is restored to keep the original archive
|
|
240
|
+
* intact.
|
|
241
|
+
*
|
|
242
|
+
* List-mode archives (`info.fromList === true`) are rejected for the same
|
|
243
|
+
* reason as {@link CrawlerOrchestrator.append}: their pages are metadata-only.
|
|
244
|
+
* @param archivePath - Absolute or relative path to the existing `.nitpicker`.
|
|
245
|
+
* @param options - Optional config overrides applied on top of the archived config.
|
|
246
|
+
* @param initializedCallback - Optional callback invoked after initialization but before crawling resumes.
|
|
247
|
+
* @returns The orchestrator instance after the retry crawl completes.
|
|
248
|
+
* @throws {Error} When the archive is in list mode or has no parseable roots.
|
|
249
|
+
*/
|
|
250
|
+
static retryFailed(archivePath: string, options?: Partial<CrawlConfig>, initializedCallback?: CrawlInitializedCallback): Promise<CrawlerOrchestrator>;
|
|
149
251
|
/**
|
|
150
252
|
* Resume a previously interrupted crawl from an existing archive file.
|
|
151
253
|
*
|