@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
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { PageSource } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Decide which {@link PageSource} label a child row reached through the
|
|
4
|
+
* crawl graph (anchor placeholder, sub-resource, redirect chain
|
|
5
|
+
* intermediate) should inherit from its parent.
|
|
6
|
+
*
|
|
7
|
+
* Two simple rules, expressed once so anchor / redirect / sub-resource
|
|
8
|
+
* call sites stay in lockstep:
|
|
9
|
+
*
|
|
10
|
+
* 1. If the parent is in the inventory chain
|
|
11
|
+
* ({@link isInventorySource}) → propagate
|
|
12
|
+
* `'inventory-discovered'`. The child is itself a transitively
|
|
13
|
+
* reached node in the inventory chain; it is NOT a new seed (the
|
|
14
|
+
* seed label is reserved for URLs the operator listed in
|
|
15
|
+
* `--inventory ./list.txt`).
|
|
16
|
+
*
|
|
17
|
+
* 2. Otherwise → return `fallback`. The two production fallbacks differ
|
|
18
|
+
* by call site:
|
|
19
|
+
*
|
|
20
|
+
* - Anchor lineage passes `'crawled'` explicitly so the crawled-wins
|
|
21
|
+
* downgrade inside `#getIdByUrl` fires when the anchor reaches an
|
|
22
|
+
* existing `'inventory-*'` row.
|
|
23
|
+
* - Sub-resource emit passes `undefined` so the DB DEFAULT
|
|
24
|
+
* `'crawled'` lands on the freshly INSERTed `resources` row (the
|
|
25
|
+
* `setResources` path is INSERT-only with `onConflict.ignore()`, so
|
|
26
|
+
* no downgrade is needed).
|
|
27
|
+
* - Redirect chain intermediate uses `'crawled'` (same reason as
|
|
28
|
+
* anchor): an existing inventory-* intermediate reached by a
|
|
29
|
+
* crawled redirect chain must be downgraded.
|
|
30
|
+
*
|
|
31
|
+
* Pure function — keeps the lineage decision testable in isolation from
|
|
32
|
+
* the database transaction / event-emitter wiring that consumes it.
|
|
33
|
+
* @param parentSource - The parent page's stored `source` column (or `undefined` when no parent row exists).
|
|
34
|
+
* @param fallback - The label to return when the parent is NOT in the inventory chain. Pass `'crawled'` to enable the crawled-wins downgrade, or `undefined` to let the DB DEFAULT apply.
|
|
35
|
+
* @returns The lineage label to attach to the child row.
|
|
36
|
+
*/
|
|
37
|
+
export declare function deriveLineageFromParent(parentSource: PageSource | undefined, fallback: PageSource | undefined): PageSource | undefined;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { isInventorySource } from './is-inventory-source.js';
|
|
2
|
+
/**
|
|
3
|
+
* Decide which {@link PageSource} label a child row reached through the
|
|
4
|
+
* crawl graph (anchor placeholder, sub-resource, redirect chain
|
|
5
|
+
* intermediate) should inherit from its parent.
|
|
6
|
+
*
|
|
7
|
+
* Two simple rules, expressed once so anchor / redirect / sub-resource
|
|
8
|
+
* call sites stay in lockstep:
|
|
9
|
+
*
|
|
10
|
+
* 1. If the parent is in the inventory chain
|
|
11
|
+
* ({@link isInventorySource}) → propagate
|
|
12
|
+
* `'inventory-discovered'`. The child is itself a transitively
|
|
13
|
+
* reached node in the inventory chain; it is NOT a new seed (the
|
|
14
|
+
* seed label is reserved for URLs the operator listed in
|
|
15
|
+
* `--inventory ./list.txt`).
|
|
16
|
+
*
|
|
17
|
+
* 2. Otherwise → return `fallback`. The two production fallbacks differ
|
|
18
|
+
* by call site:
|
|
19
|
+
*
|
|
20
|
+
* - Anchor lineage passes `'crawled'` explicitly so the crawled-wins
|
|
21
|
+
* downgrade inside `#getIdByUrl` fires when the anchor reaches an
|
|
22
|
+
* existing `'inventory-*'` row.
|
|
23
|
+
* - Sub-resource emit passes `undefined` so the DB DEFAULT
|
|
24
|
+
* `'crawled'` lands on the freshly INSERTed `resources` row (the
|
|
25
|
+
* `setResources` path is INSERT-only with `onConflict.ignore()`, so
|
|
26
|
+
* no downgrade is needed).
|
|
27
|
+
* - Redirect chain intermediate uses `'crawled'` (same reason as
|
|
28
|
+
* anchor): an existing inventory-* intermediate reached by a
|
|
29
|
+
* crawled redirect chain must be downgraded.
|
|
30
|
+
*
|
|
31
|
+
* Pure function — keeps the lineage decision testable in isolation from
|
|
32
|
+
* the database transaction / event-emitter wiring that consumes it.
|
|
33
|
+
* @param parentSource - The parent page's stored `source` column (or `undefined` when no parent row exists).
|
|
34
|
+
* @param fallback - The label to return when the parent is NOT in the inventory chain. Pass `'crawled'` to enable the crawled-wins downgrade, or `undefined` to let the DB DEFAULT apply.
|
|
35
|
+
* @returns The lineage label to attach to the child row.
|
|
36
|
+
*/
|
|
37
|
+
export function deriveLineageFromParent(parentSource, fallback) {
|
|
38
|
+
if (isInventorySource(parentSource)) {
|
|
39
|
+
return 'inventory-discovered';
|
|
40
|
+
}
|
|
41
|
+
return fallback;
|
|
42
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads the top-level directory name from a `.nitpicker` tar archive.
|
|
3
|
+
*
|
|
4
|
+
* Why: a `.nitpicker` is a tar that wraps `<basename>/db.sqlite` (and
|
|
5
|
+
* historically other files). `Archive.write()` names the inner directory
|
|
6
|
+
* from the file's basename at write time, but users routinely rename
|
|
7
|
+
* `.nitpicker` files after the fact (e.g. `mv X.migrated.nitpicker
|
|
8
|
+
* X.nitpicker`) — a perfectly reasonable filesystem operation that breaks
|
|
9
|
+
* any code that recomputes the inner-dir name from the outer filename.
|
|
10
|
+
*
|
|
11
|
+
* This helper scans the tar's entry list and returns the first top-level
|
|
12
|
+
* directory it sees, so callers (`Archive.open`, the migration script) can
|
|
13
|
+
* use the actual inner name regardless of what the outer file is called.
|
|
14
|
+
*
|
|
15
|
+
* Reads the tar's central directory only; does not extract any data. Runs
|
|
16
|
+
* in O(number of entries) but stops at the first match, so a typical
|
|
17
|
+
* archive (1–2 top-level entries) completes in a single read.
|
|
18
|
+
* @param tarFilePath - Path to the `.nitpicker` tar file.
|
|
19
|
+
* @returns The first top-level directory name found in the archive.
|
|
20
|
+
* @throws {Error} When the tar contains no top-level directory entry
|
|
21
|
+
* (e.g. file is empty, corrupted, or non-tar). The error message
|
|
22
|
+
* identifies the path for diagnostics.
|
|
23
|
+
* @example
|
|
24
|
+
* // The user renamed `original.nitpicker` to `renamed.nitpicker`. The
|
|
25
|
+
* // inner directory was baked in at write time and is unaffected:
|
|
26
|
+
* await peekTarTopDir('renamed.nitpicker'); // → 'original'
|
|
27
|
+
*/
|
|
28
|
+
export declare function peekTarTopDir(tarFilePath: string): Promise<string>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { list } from 'tar';
|
|
2
|
+
/**
|
|
3
|
+
* Reads the top-level directory name from a `.nitpicker` tar archive.
|
|
4
|
+
*
|
|
5
|
+
* Why: a `.nitpicker` is a tar that wraps `<basename>/db.sqlite` (and
|
|
6
|
+
* historically other files). `Archive.write()` names the inner directory
|
|
7
|
+
* from the file's basename at write time, but users routinely rename
|
|
8
|
+
* `.nitpicker` files after the fact (e.g. `mv X.migrated.nitpicker
|
|
9
|
+
* X.nitpicker`) — a perfectly reasonable filesystem operation that breaks
|
|
10
|
+
* any code that recomputes the inner-dir name from the outer filename.
|
|
11
|
+
*
|
|
12
|
+
* This helper scans the tar's entry list and returns the first top-level
|
|
13
|
+
* directory it sees, so callers (`Archive.open`, the migration script) can
|
|
14
|
+
* use the actual inner name regardless of what the outer file is called.
|
|
15
|
+
*
|
|
16
|
+
* Reads the tar's central directory only; does not extract any data. Runs
|
|
17
|
+
* in O(number of entries) but stops at the first match, so a typical
|
|
18
|
+
* archive (1–2 top-level entries) completes in a single read.
|
|
19
|
+
* @param tarFilePath - Path to the `.nitpicker` tar file.
|
|
20
|
+
* @returns The first top-level directory name found in the archive.
|
|
21
|
+
* @throws {Error} When the tar contains no top-level directory entry
|
|
22
|
+
* (e.g. file is empty, corrupted, or non-tar). The error message
|
|
23
|
+
* identifies the path for diagnostics.
|
|
24
|
+
* @example
|
|
25
|
+
* // The user renamed `original.nitpicker` to `renamed.nitpicker`. The
|
|
26
|
+
* // inner directory was baked in at write time and is unaffected:
|
|
27
|
+
* await peekTarTopDir('renamed.nitpicker'); // → 'original'
|
|
28
|
+
*/
|
|
29
|
+
export async function peekTarTopDir(tarFilePath) {
|
|
30
|
+
let found = null;
|
|
31
|
+
await list({
|
|
32
|
+
file: tarFilePath,
|
|
33
|
+
onReadEntry: (entry) => {
|
|
34
|
+
if (found !== null)
|
|
35
|
+
return;
|
|
36
|
+
// Only Directory entries count. Files at the tar root would
|
|
37
|
+
// otherwise win — and macOS BSD tar embeds AppleDouble (`._*`)
|
|
38
|
+
// sidecar files at the tar root for resource forks; those are
|
|
39
|
+
// File entries that BSD `tar -tf` hides but Node's `tar`
|
|
40
|
+
// surfaces verbatim, so without this filter the resource-fork
|
|
41
|
+
// of the real top-level dir would win instead of the dir itself.
|
|
42
|
+
if (entry.type !== 'Directory')
|
|
43
|
+
return;
|
|
44
|
+
// `entry.path` is the path inside the tar. Strip any leading
|
|
45
|
+
// `./` (some tar producers prepend it) before splitting.
|
|
46
|
+
const cleaned = entry.path.replace(/^\.\//, '');
|
|
47
|
+
const top = cleaned.split('/')[0];
|
|
48
|
+
if (!top || top === '')
|
|
49
|
+
return;
|
|
50
|
+
// Defense in depth: skip AppleDouble directory entries
|
|
51
|
+
// (`._foo/`) and pax extended header entries
|
|
52
|
+
// (`PaxHeaders.NNN/...` / `@PaxHeader`). Real `.nitpicker`
|
|
53
|
+
// archives never name their top dir with these prefixes.
|
|
54
|
+
if (top.startsWith('._'))
|
|
55
|
+
return;
|
|
56
|
+
if (top.startsWith('PaxHeaders') || top.startsWith('@PaxHeader'))
|
|
57
|
+
return;
|
|
58
|
+
found = top;
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
if (found === null) {
|
|
62
|
+
throw new Error(`Tar contains no top-level directory entry: ${tarFilePath}`);
|
|
63
|
+
}
|
|
64
|
+
return found;
|
|
65
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Knex } from 'knex';
|
|
2
|
+
/**
|
|
3
|
+
* Bulk-resolve a raw error message for each given page id, using only sources
|
|
4
|
+
* reachable from a {@link Knex} handle. Read order: `page_errors` (keyed by
|
|
5
|
+
* `pageId`, the most direct signal a scrape attempt recorded), then
|
|
6
|
+
* `crawl_errors` (keyed by `url`, the crawler-channel record for failures
|
|
7
|
+
* that happened before a page row was scraped).
|
|
8
|
+
*
|
|
9
|
+
* **Known limitation — pre-`crawl_errors` archives**: This helper does NOT
|
|
10
|
+
* read `error.log`. `migrateCrawlErrors` creates the `crawl_errors` table on
|
|
11
|
+
* writer connect but does NOT back-fill historical lines from `error.log`,
|
|
12
|
+
* so an archive that predates the `crawl_errors` schema and whose failures
|
|
13
|
+
* live only in `error.log` will resolve every id to "no message" here. The
|
|
14
|
+
* downstream `Database.resetFailedPages` treats absence as `unknown` (still
|
|
15
|
+
* retryable), so legacy archives lose the permanent-kind exclusion until a
|
|
16
|
+
* fresh crawl run populates `crawl_errors` / `page_errors`. The trade-off
|
|
17
|
+
* (no error.log parsing in the writer path) keeps the writer dependency
|
|
18
|
+
* surface narrow and avoids re-implementing the parser already living in
|
|
19
|
+
* `@nitpicker/query`'s `resolveFailedPageMessages` — which the crawler
|
|
20
|
+
* package cannot import (reverse-direction dependency). When this matters
|
|
21
|
+
* in practice, run the archive through one fresh `crawl --retry-failed`
|
|
22
|
+
* pass first to populate the structured tables.
|
|
23
|
+
*
|
|
24
|
+
* Pages with no message in any consulted source are simply absent from the
|
|
25
|
+
* returned map; callers treat the absence as "unclassifiable, keep retrying"
|
|
26
|
+
* (i.e. `unknown`).
|
|
27
|
+
* @param instance - The {@link Knex} handle.
|
|
28
|
+
* @param ids - Candidate `pages.id` values.
|
|
29
|
+
* @param urls - The corresponding `pages.url` values, in the same order as
|
|
30
|
+
* `ids`. Length and indexing MUST match `ids` so the page → url join can be
|
|
31
|
+
* reconstructed without a second `pages` round-trip.
|
|
32
|
+
* @returns `Map<pageId, message>` populated only for ids whose message was
|
|
33
|
+
* found in one of the consulted tables.
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* const messages = await getFailedPageMessages(
|
|
37
|
+
* instance,
|
|
38
|
+
* candidates.map(c => c.id),
|
|
39
|
+
* candidates.map(c => c.url),
|
|
40
|
+
* );
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare function getFailedPageMessages(instance: Knex, ids: readonly number[], urls: readonly string[]): Promise<Map<number, string>>;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/** chunk size for `WHERE … IN (?)` SQLite parameter packing. */
|
|
2
|
+
const CHUNK_SIZE = 500;
|
|
3
|
+
/**
|
|
4
|
+
* Bulk-resolve a raw error message for each given page id, using only sources
|
|
5
|
+
* reachable from a {@link Knex} handle. Read order: `page_errors` (keyed by
|
|
6
|
+
* `pageId`, the most direct signal a scrape attempt recorded), then
|
|
7
|
+
* `crawl_errors` (keyed by `url`, the crawler-channel record for failures
|
|
8
|
+
* that happened before a page row was scraped).
|
|
9
|
+
*
|
|
10
|
+
* **Known limitation — pre-`crawl_errors` archives**: This helper does NOT
|
|
11
|
+
* read `error.log`. `migrateCrawlErrors` creates the `crawl_errors` table on
|
|
12
|
+
* writer connect but does NOT back-fill historical lines from `error.log`,
|
|
13
|
+
* so an archive that predates the `crawl_errors` schema and whose failures
|
|
14
|
+
* live only in `error.log` will resolve every id to "no message" here. The
|
|
15
|
+
* downstream `Database.resetFailedPages` treats absence as `unknown` (still
|
|
16
|
+
* retryable), so legacy archives lose the permanent-kind exclusion until a
|
|
17
|
+
* fresh crawl run populates `crawl_errors` / `page_errors`. The trade-off
|
|
18
|
+
* (no error.log parsing in the writer path) keeps the writer dependency
|
|
19
|
+
* surface narrow and avoids re-implementing the parser already living in
|
|
20
|
+
* `@nitpicker/query`'s `resolveFailedPageMessages` — which the crawler
|
|
21
|
+
* package cannot import (reverse-direction dependency). When this matters
|
|
22
|
+
* in practice, run the archive through one fresh `crawl --retry-failed`
|
|
23
|
+
* pass first to populate the structured tables.
|
|
24
|
+
*
|
|
25
|
+
* Pages with no message in any consulted source are simply absent from the
|
|
26
|
+
* returned map; callers treat the absence as "unclassifiable, keep retrying"
|
|
27
|
+
* (i.e. `unknown`).
|
|
28
|
+
* @param instance - The {@link Knex} handle.
|
|
29
|
+
* @param ids - Candidate `pages.id` values.
|
|
30
|
+
* @param urls - The corresponding `pages.url` values, in the same order as
|
|
31
|
+
* `ids`. Length and indexing MUST match `ids` so the page → url join can be
|
|
32
|
+
* reconstructed without a second `pages` round-trip.
|
|
33
|
+
* @returns `Map<pageId, message>` populated only for ids whose message was
|
|
34
|
+
* found in one of the consulted tables.
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* const messages = await getFailedPageMessages(
|
|
38
|
+
* instance,
|
|
39
|
+
* candidates.map(c => c.id),
|
|
40
|
+
* candidates.map(c => c.url),
|
|
41
|
+
* );
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export async function getFailedPageMessages(instance, ids, urls) {
|
|
45
|
+
if (ids.length === 0) {
|
|
46
|
+
return new Map();
|
|
47
|
+
}
|
|
48
|
+
if (ids.length !== urls.length) {
|
|
49
|
+
throw new Error(`getFailedPageMessages: ids.length (${ids.length}) !== urls.length (${urls.length}) — must be 1:1`);
|
|
50
|
+
}
|
|
51
|
+
const messageByPageId = new Map();
|
|
52
|
+
if (await instance.schema.hasTable('page_errors')) {
|
|
53
|
+
for (let i = 0; i < ids.length; i += CHUNK_SIZE) {
|
|
54
|
+
const chunk = ids.slice(i, i + CHUNK_SIZE);
|
|
55
|
+
// `orderBy('id', 'asc')` makes the per-pageId "first row seen"
|
|
56
|
+
// behavior deterministic regardless of SQLite's natural ROWID
|
|
57
|
+
// scan order, which is otherwise implementation-defined under
|
|
58
|
+
// concurrent / migrated archives. Without the explicit order, a
|
|
59
|
+
// reset that classified a page as `unknown` once could classify
|
|
60
|
+
// it as `parse-error` on the next run when the rows happen to be
|
|
61
|
+
// returned in a different order.
|
|
62
|
+
const rows = (await instance('page_errors')
|
|
63
|
+
.select('pageId', 'message')
|
|
64
|
+
.whereIn('pageId', chunk)
|
|
65
|
+
.orderBy('id', 'asc'));
|
|
66
|
+
for (const row of rows) {
|
|
67
|
+
// Earliest-id wins. Schema permits multiple rows per pageId
|
|
68
|
+
// (the same scrape can record several phase errors); the
|
|
69
|
+
// first row inserted is usually the trigger cause, later
|
|
70
|
+
// rows are follow-on noise from the same failure cascade.
|
|
71
|
+
//
|
|
72
|
+
// An empty `message` is treated as "no signal" and ignored
|
|
73
|
+
// so the crawl_errors lookup can fill it in. Without this,
|
|
74
|
+
// a page_errors row with `message=''` (recorded by a
|
|
75
|
+
// scraper phase that fired its trigger but had no error
|
|
76
|
+
// text) would short-circuit and we'd lose access to the
|
|
77
|
+
// crawl_errors row that classifies the failure as
|
|
78
|
+
// `dns` / `tls` / `client-blocked` etc. — defeating
|
|
79
|
+
// `--retry-failed`'s permanent-kind exclusion.
|
|
80
|
+
if (row.message !== '' && !messageByPageId.has(row.pageId)) {
|
|
81
|
+
messageByPageId.set(row.pageId, row.message);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const idsMissing = ids.filter((id) => !messageByPageId.has(id));
|
|
87
|
+
if (idsMissing.length === 0 || !(await instance.schema.hasTable('crawl_errors'))) {
|
|
88
|
+
// Early-exit short-circuits BOTH the idToUrl Map construction and
|
|
89
|
+
// the crawl_errors round-trip. On a 1M-page archive where every
|
|
90
|
+
// failed page already has a `page_errors` row, this avoids walking
|
|
91
|
+
// the candidate list a second time.
|
|
92
|
+
return messageByPageId;
|
|
93
|
+
}
|
|
94
|
+
const idToUrl = new Map();
|
|
95
|
+
for (const [i, id] of ids.entries()) {
|
|
96
|
+
const url = urls[i];
|
|
97
|
+
if (url !== undefined) {
|
|
98
|
+
idToUrl.set(id, url);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const missingUrls = [];
|
|
102
|
+
for (const id of idsMissing) {
|
|
103
|
+
const url = idToUrl.get(id);
|
|
104
|
+
if (url !== undefined) {
|
|
105
|
+
missingUrls.push(url);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const urlToMessage = new Map();
|
|
109
|
+
for (let i = 0; i < missingUrls.length; i += CHUNK_SIZE) {
|
|
110
|
+
const chunk = missingUrls.slice(i, i + CHUNK_SIZE);
|
|
111
|
+
const rows = (await instance('crawl_errors')
|
|
112
|
+
.select('url', 'message')
|
|
113
|
+
.whereIn('url', chunk));
|
|
114
|
+
for (const row of rows) {
|
|
115
|
+
if (row.url !== null && !urlToMessage.has(row.url)) {
|
|
116
|
+
urlToMessage.set(row.url, row.message);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const id of idsMissing) {
|
|
121
|
+
const url = idToUrl.get(id);
|
|
122
|
+
if (url === undefined) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const message = urlToMessage.get(url);
|
|
126
|
+
if (message !== undefined) {
|
|
127
|
+
messageByPageId.set(id, message);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return messageByPageId;
|
|
131
|
+
}
|
|
@@ -1,8 +1,59 @@
|
|
|
1
1
|
import type { Knex } from 'knex';
|
|
2
|
+
/**
|
|
3
|
+
* Applies the connection-level PRAGMAs that govern foreign-key enforcement
|
|
4
|
+
* and BLOB-read performance. These are **per-connection** settings (libsql
|
|
5
|
+
* resets them when a new connection is opened), so they must be reapplied
|
|
6
|
+
* every time `Database.connect` runs — not just on first-time schema
|
|
7
|
+
* initialization. Keeping them separate from `initSchema`'s one-shot path
|
|
8
|
+
* also lets `page_size` (which only takes effect against an empty DB)
|
|
9
|
+
* stay gated behind the existence check.
|
|
10
|
+
* @param instance - The Knex query builder instance connected to the database.
|
|
11
|
+
*/
|
|
12
|
+
export declare function applyConnectionPragmas(instance: Knex): Promise<void>;
|
|
2
13
|
/**
|
|
3
14
|
* Initializes the archive database schema if tables do not exist.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
15
|
+
*
|
|
16
|
+
* Schema notes:
|
|
17
|
+
*
|
|
18
|
+
* - **Meta columns (v2)**: pages carries ~47 flat columns derived from
|
|
19
|
+
* beholder 3.0.0's nested Meta shape (`canonical`, `og_*`, `twitter_*`,
|
|
20
|
+
* `robots_*`, document basics, editorial fields) plus a `meta_extras`
|
|
21
|
+
* JSON column for everything not flattened. URL-shaped columns are
|
|
22
|
+
* absolutised against the page URL before write (see
|
|
23
|
+
* `archive/meta/derive-flat-from-meta.ts`).
|
|
24
|
+
* - **Denormalised aggregates** (`tag_count`, `jsonld_count`,
|
|
25
|
+
* `tags_providers_csv`): computed at write time from `meta.tags` /
|
|
26
|
+
* `meta.jsonLd` to avoid N+1 GROUP BY at Sheets-render / page-detail time.
|
|
27
|
+
* Plan: "ファイルサイズが多少増えてもいいから取り出しパフォーマンスを優先".
|
|
28
|
+
* - **Per-page timestamps** (`firstCrawledAt`, `lastCrawledAt`): UNIX ms.
|
|
29
|
+
* Written by `#insertPage` on INSERT (`first = last = now`) and UPDATE
|
|
30
|
+
* (`last = now`, `first` preserved). `resetFailedPages` deliberately
|
|
31
|
+
* leaves them alone so failure-reset does not erase the last-success
|
|
32
|
+
* record.
|
|
33
|
+
* - **`page_tags`** (Wappalyzer): per-provider × external-id row shape, plus
|
|
34
|
+
* `categories`/`sources` JSON columns. Compound indexes
|
|
35
|
+
* `(provider, externalId)` / `(provider, pageId)` are pre-built for the
|
|
36
|
+
* Phase 2+ "find duplicate IDs across pages" and "list pages using
|
|
37
|
+
* provider X" hot paths — Phase 1 read perf > storage cost trade-off.
|
|
38
|
+
* - **`page_jsonld`** (JSON-LD / SpeculationRules): one row per
|
|
39
|
+
* `<script type="application/ld+json">` or `<script type="speculationrules">`.
|
|
40
|
+
* `raw` is stored uncompressed (SQLite overflow pages handle large rows);
|
|
41
|
+
* if cross-archive bulk export becomes a use case, add a `codec` column
|
|
42
|
+
* à la `page_html_blobs`. Compound `(type, pageId)` accelerates streaming
|
|
43
|
+
* `list_pages_by_jsonld_type` JOINs.
|
|
44
|
+
* - **HTML snapshots** (`page_html_blobs` + `page_html_ref`): unchanged
|
|
45
|
+
* from v1. zstd-compressed BLOBs keyed by SHA-256 for content-addressable
|
|
46
|
+
* dedup. WITHOUT ROWID via raw SQL because knex's schema builder cannot
|
|
47
|
+
* express it.
|
|
48
|
+
* - **PRAGMA `page_size` and `journal_mode`** are set BEFORE any
|
|
49
|
+
* `CREATE TABLE` because SQLite only honors `page_size` changes against
|
|
50
|
+
* an empty database, and `journal_mode = WAL` is persistent. Other
|
|
51
|
+
* per-connection PRAGMAs live in {@link applyConnectionPragmas}.
|
|
52
|
+
*
|
|
53
|
+
* Pre-0.10 → 0.10 migration is intentionally absent. `assertCompatibleVersion`
|
|
54
|
+
* (called before `initSchema`) rejects pre-0.10 archives with a friendly
|
|
55
|
+
* error pointing the operator at `scripts/migrate-to-0.10.mjs`; `v0.x`
|
|
56
|
+
* policy allows breaking changes.
|
|
6
57
|
* @param instance - The Knex query builder instance connected to the database.
|
|
7
58
|
*/
|
|
8
59
|
export declare function initSchema(instance: Knex): Promise<void>;
|