@nitpicker/crawler 0.14.0 → 0.16.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.d.ts +136 -10
- package/lib/archive/archive.js +147 -9
- package/lib/archive/body-hash/compute-body-hash.d.ts +22 -0
- package/lib/archive/body-hash/compute-body-hash.js +31 -0
- package/lib/archive/body-hash/extract-body.d.ts +18 -0
- package/lib/archive/body-hash/extract-body.js +29 -0
- package/lib/archive/body-hash/mask-dynamic-ids.d.ts +33 -0
- package/lib/archive/body-hash/mask-dynamic-ids.js +44 -0
- package/lib/archive/body-hash/normalize-url-like-strings.d.ts +16 -0
- package/lib/archive/body-hash/normalize-url-like-strings.js +19 -0
- package/lib/archive/cache/clear-archive-cache-entry.d.ts +19 -0
- package/lib/archive/cache/clear-archive-cache-entry.js +25 -0
- package/lib/archive/cache/clear-archive-cache-root.d.ts +26 -0
- package/lib/archive/cache/clear-archive-cache-root.js +32 -0
- package/lib/archive/cache/list-archive-cache-entries.d.ts +23 -0
- package/lib/archive/cache/list-archive-cache-entries.js +125 -0
- package/lib/archive/cache/path-exists.d.ts +9 -0
- package/lib/archive/cache/path-exists.js +18 -0
- package/lib/archive/cache/types.d.ts +33 -0
- package/lib/archive/cache/types.js +1 -0
- package/lib/archive/create-adjunct-tables.d.ts +14 -0
- package/lib/archive/create-adjunct-tables.js +169 -0
- package/lib/archive/create-entity-tables.js +21 -1
- package/lib/archive/database.d.ts +67 -2
- package/lib/archive/database.js +92 -3
- package/lib/archive/db-ops/_shared/clear-write-ref-caches.js +1 -0
- package/lib/archive/db-ops/_shared/compress-payload.d.ts +26 -0
- package/lib/archive/db-ops/_shared/compress-payload.js +30 -0
- package/lib/archive/db-ops/_shared/create-write-ref-caches.js +1 -0
- package/lib/archive/db-ops/_shared/types.d.ts +2 -0
- package/lib/archive/db-ops/_shared/upsert-json-ref.js +3 -3
- package/lib/archive/db-ops/analysis/replace-page-templates.d.ts +12 -3
- package/lib/archive/db-ops/analysis/replace-page-templates.js +35 -3
- package/lib/archive/db-ops/analysis/types.d.ts +61 -0
- package/lib/archive/db-ops/analysis/types.js +1 -0
- package/lib/archive/db-ops/console-logs/compute-console-log-hash.d.ts +19 -0
- package/lib/archive/db-ops/console-logs/compute-console-log-hash.js +30 -0
- package/lib/archive/db-ops/console-logs/replace-console-logs.d.ts +34 -0
- package/lib/archive/db-ops/console-logs/replace-console-logs.js +123 -0
- package/lib/archive/db-ops/console-logs/stringify-console-log-args.d.ts +16 -0
- package/lib/archive/db-ops/console-logs/stringify-console-log-args.js +26 -0
- package/lib/archive/db-ops/console-logs/types.d.ts +38 -0
- package/lib/archive/db-ops/console-logs/types.js +1 -0
- package/lib/archive/db-ops/console-logs/upsert-console-log-item.d.ts +29 -0
- package/lib/archive/db-ops/console-logs/upsert-console-log-item.js +52 -0
- package/lib/archive/db-ops/dedupe-cap/accumulate-dedupe-cap-rejected-count.d.ts +18 -0
- package/lib/archive/db-ops/dedupe-cap/accumulate-dedupe-cap-rejected-count.js +23 -0
- package/lib/archive/db-ops/dedupe-cap/finalize-dedupe-cap-event.d.ts +12 -0
- package/lib/archive/db-ops/dedupe-cap/finalize-dedupe-cap-event.js +15 -0
- package/lib/archive/db-ops/dedupe-cap/insert-dedupe-cap-event.d.ts +14 -0
- package/lib/archive/db-ops/dedupe-cap/insert-dedupe-cap-event.js +30 -0
- package/lib/archive/db-ops/dedupe-cap/list-dedupe-cap-shape-keys.d.ts +21 -0
- package/lib/archive/db-ops/dedupe-cap/list-dedupe-cap-shape-keys.js +27 -0
- package/lib/archive/db-ops/errors/list-dns-burned-host-candidates.d.ts +11 -0
- package/lib/archive/db-ops/errors/list-dns-burned-host-candidates.js +21 -4
- package/lib/archive/db-ops/inventory/record-inventory-run.js +1 -0
- package/lib/archive/db-ops/lifecycle/init.d.ts +11 -2
- package/lib/archive/db-ops/lifecycle/init.js +21 -2
- package/lib/archive/db-ops/outages/close-network-outage.d.ts +12 -0
- package/lib/archive/db-ops/outages/close-network-outage.js +15 -0
- package/lib/archive/db-ops/outages/close-stale-open-network-outages.d.ts +26 -0
- package/lib/archive/db-ops/outages/close-stale-open-network-outages.js +42 -0
- package/lib/archive/db-ops/outages/compute-outage-clamp-timestamp.d.ts +26 -0
- package/lib/archive/db-ops/outages/compute-outage-clamp-timestamp.js +31 -0
- package/lib/archive/db-ops/outages/insert-network-outage.d.ts +14 -0
- package/lib/archive/db-ops/outages/insert-network-outage.js +29 -0
- package/lib/archive/db-ops/outages/list-network-outages.d.ts +28 -0
- package/lib/archive/db-ops/outages/list-network-outages.js +42 -0
- package/lib/archive/db-ops/pages/reset/reset-failed-pages.d.ts +13 -0
- package/lib/archive/db-ops/pages/reset/reset-failed-pages.js +35 -7
- package/lib/archive/db-ops/pages/write/update-page.js +12 -0
- package/lib/archive/db-ops/pages/write/write-page-html-blob.js +6 -6
- package/lib/archive/filesystem/output-binary.d.ts +12 -0
- package/lib/archive/filesystem/output-binary.js +17 -0
- package/lib/archive/get-failed-page-messages.d.ts +20 -8
- package/lib/archive/get-failed-page-messages.js +29 -15
- package/lib/archive/migrate-content-items-alias-of-id.d.ts +47 -0
- package/lib/archive/migrate-content-items-alias-of-id.js +58 -0
- package/lib/archive/migrate-inventory-runs-invalid-skipped.d.ts +11 -0
- package/lib/archive/migrate-inventory-runs-invalid-skipped.js +24 -0
- package/lib/archive/migrate-page-meta-body-hash.d.ts +39 -0
- package/lib/archive/migrate-page-meta-body-hash.js +52 -0
- package/lib/archive/migrate-page-meta-console-error-count.d.ts +22 -0
- package/lib/archive/migrate-page-meta-console-error-count.js +34 -0
- package/lib/archive/page.d.ts +1 -1
- package/lib/archive/types.d.ts +60 -2
- package/lib/archive/url-alias/compute-tier-a-alias-key.d.ts +31 -0
- package/lib/archive/url-alias/compute-tier-a-alias-key.js +36 -0
- package/lib/archive/url-alias/compute-tier-b-alias-key.d.ts +19 -0
- package/lib/archive/url-alias/compute-tier-b-alias-key.js +30 -0
- package/lib/archive/url-alias/format-alias-key.d.ts +13 -0
- package/lib/archive/url-alias/format-alias-key.js +14 -0
- package/lib/archive/url-alias/parse-alias-key-parts.d.ts +15 -0
- package/lib/archive/url-alias/parse-alias-key-parts.js +40 -0
- package/lib/archive/url-alias/types.d.ts +7 -0
- package/lib/archive/url-alias/types.js +1 -0
- package/lib/classify-error-kind.d.ts +1 -0
- package/lib/classify-error-kind.js +14 -0
- package/lib/crawler/assert-chrome-installed.d.ts +24 -0
- package/lib/crawler/assert-chrome-installed.js +43 -0
- package/lib/crawler/choose-probe-host.d.ts +24 -0
- package/lib/crawler/choose-probe-host.js +38 -0
- package/lib/crawler/clear-dns-burned-host-cache.js +2 -0
- package/lib/crawler/crawler.d.ts +12 -0
- package/lib/crawler/crawler.js +496 -29
- package/lib/crawler/decode-auth-credential.d.ts +29 -0
- package/lib/crawler/decode-auth-credential.js +39 -0
- package/lib/crawler/dedupe/compute-meta-signature.d.ts +30 -0
- package/lib/crawler/dedupe/compute-meta-signature.js +0 -0
- package/lib/crawler/dedupe/compute-shape-key.d.ts +37 -0
- package/lib/crawler/dedupe/compute-shape-key.js +56 -0
- package/lib/crawler/dedupe/dedupe-cap-tracker.d.ts +84 -0
- package/lib/crawler/dedupe/dedupe-cap-tracker.js +185 -0
- package/lib/crawler/dedupe/is-predicted-content-duplicate.d.ts +24 -0
- package/lib/crawler/dedupe/is-predicted-content-duplicate.js +26 -0
- package/lib/crawler/dedupe/is-shape-capped.d.ts +10 -0
- package/lib/crawler/dedupe/is-shape-capped.js +12 -0
- package/lib/crawler/dedupe/resolve-og-url-mismatch.d.ts +31 -0
- package/lib/crawler/dedupe/resolve-og-url-mismatch.js +40 -0
- package/lib/crawler/dedupe/types.d.ts +42 -0
- package/lib/crawler/dedupe/types.js +1 -0
- package/lib/crawler/dns-burned-host-burn-timestamps.d.ts +17 -0
- package/lib/crawler/dns-burned-host-burn-timestamps.js +17 -0
- package/lib/crawler/evict-network-classified-destination-cache-entries.d.ts +26 -0
- package/lib/crawler/evict-network-classified-destination-cache-entries.js +34 -0
- package/lib/crawler/evict-outage-tainted-dns-burns.d.ts +23 -0
- package/lib/crawler/evict-outage-tainted-dns-burns.js +26 -0
- package/lib/crawler/fetch-destination.js +14 -2
- package/lib/crawler/generate-predicted-urls.d.ts +12 -0
- package/lib/crawler/generate-predicted-urls.js +33 -2
- package/lib/crawler/is-puppeteer-fallback-candidate.js +3 -0
- package/lib/crawler/network-gate.d.ts +49 -0
- package/lib/crawler/network-gate.js +78 -0
- package/lib/crawler/network-outage-detector.d.ts +51 -0
- package/lib/crawler/network-outage-detector.js +81 -0
- package/lib/crawler/network-outage-summary-counter.d.ts +21 -0
- package/lib/crawler/network-outage-summary-counter.js +18 -0
- package/lib/crawler/probe-network.d.ts +26 -0
- package/lib/crawler/probe-network.js +21 -0
- package/lib/crawler/types.d.ts +185 -1
- package/lib/crawler-orchestrator.d.ts +74 -18
- package/lib/crawler-orchestrator.js +267 -27
- package/lib/crawler.d.ts +22 -0
- package/lib/crawler.js +18 -0
- package/lib/is-within-outage-window.d.ts +49 -0
- package/lib/is-within-outage-window.js +33 -0
- package/lib/network-related-error-kinds.d.ts +23 -0
- package/lib/network-related-error-kinds.js +28 -0
- package/lib/permanent-error-kinds.d.ts +9 -4
- package/lib/permanent-error-kinds.js +10 -4
- package/lib/types.d.ts +11 -2
- package/lib/utils/array/each-splitted.d.ts +1 -1
- package/lib/utils/compute-file-sha256.d.ts +17 -17
- package/lib/utils/compute-file-sha256.js +18 -47
- package/package.json +3 -3
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const INDEX_SUFFIX_PATTERN = /\/index\.\w+/gi;
|
|
2
|
+
/**
|
|
3
|
+
* Normalizes `/index.{ext}` path suffixes (`/index.html`, `/index.php`, ...)
|
|
4
|
+
* to a bare trailing `/` throughout the given text, so that two otherwise
|
|
5
|
+
* identical bodies that differ only in which equivalent URL form a template
|
|
6
|
+
* happened to render (`/about/` vs `/about/index.html`) hash the same.
|
|
7
|
+
*
|
|
8
|
+
* Applied as a blanket string sweep over the whole body — not scoped to
|
|
9
|
+
* `href`/`src` attribute values — because the same trailing-suffix variance
|
|
10
|
+
* can appear anywhere a URL-shaped string is rendered as text (breadcrumbs,
|
|
11
|
+
* "print this page" widgets, JSON embedded in an inline `<script>`), and the
|
|
12
|
+
* body is treated as an opaque string for masking purposes (see
|
|
13
|
+
* `computeBodyHash`).
|
|
14
|
+
* @param body - The `<body>` inner HTML (or any text) to normalize.
|
|
15
|
+
* @returns `body` with every `/index.{ext}` suffix collapsed to `/`.
|
|
16
|
+
*/
|
|
17
|
+
export function normalizeUrlLikeStrings(body) {
|
|
18
|
+
return body.replaceAll(INDEX_SUFFIX_PATTERN, '/');
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remove a single archive cache entry (one directory previously resolved by
|
|
3
|
+
* {@link import('./resolve-archive-cache-dir.js').resolveArchiveCacheDir}).
|
|
4
|
+
*
|
|
5
|
+
* Deliberately scoped to exactly `cacheDir` — it never touches sibling
|
|
6
|
+
* entries under the same cache root (in particular, the `@nitpicker/core`
|
|
7
|
+
* analyze `table` cache is never archive-scoped and must survive a
|
|
8
|
+
* per-archive clear).
|
|
9
|
+
* @param cacheDir - Absolute path to the single cache entry to remove.
|
|
10
|
+
* @returns `true` if `cacheDir` existed and was removed, `false` if it was
|
|
11
|
+
* already absent.
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const cacheKey = await computeArchiveCacheKey(archivePath);
|
|
15
|
+
* const cacheDir = resolveArchiveCacheDir(cacheRoot, cacheKey, archivePath);
|
|
16
|
+
* const removed = await clearArchiveCacheEntry(cacheDir);
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare function clearArchiveCacheEntry(cacheDir: string): Promise<boolean>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { pathExists } from './path-exists.js';
|
|
3
|
+
/**
|
|
4
|
+
* Remove a single archive cache entry (one directory previously resolved by
|
|
5
|
+
* {@link import('./resolve-archive-cache-dir.js').resolveArchiveCacheDir}).
|
|
6
|
+
*
|
|
7
|
+
* Deliberately scoped to exactly `cacheDir` — it never touches sibling
|
|
8
|
+
* entries under the same cache root (in particular, the `@nitpicker/core`
|
|
9
|
+
* analyze `table` cache is never archive-scoped and must survive a
|
|
10
|
+
* per-archive clear).
|
|
11
|
+
* @param cacheDir - Absolute path to the single cache entry to remove.
|
|
12
|
+
* @returns `true` if `cacheDir` existed and was removed, `false` if it was
|
|
13
|
+
* already absent.
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const cacheKey = await computeArchiveCacheKey(archivePath);
|
|
17
|
+
* const cacheDir = resolveArchiveCacheDir(cacheRoot, cacheKey, archivePath);
|
|
18
|
+
* const removed = await clearArchiveCacheEntry(cacheDir);
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export async function clearArchiveCacheEntry(cacheDir) {
|
|
22
|
+
const existedBefore = await pathExists(cacheDir);
|
|
23
|
+
await fs.rm(cacheDir, { recursive: true, force: true });
|
|
24
|
+
return existedBefore;
|
|
25
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remove an entire archive cache root, including every tar-cache entry and
|
|
3
|
+
* any sibling directory living under it (e.g. the `@nitpicker/core` analyze
|
|
4
|
+
* `table` cache, which is resolved as a child of the same root).
|
|
5
|
+
*
|
|
6
|
+
* Pure: takes `cacheRoot` as a parameter and never resolves it itself, so
|
|
7
|
+
* tests can point it at a throwaway directory instead of the real OS temp
|
|
8
|
+
* cache. Mirrors the "manual `rm -rf` is safe" contract already documented
|
|
9
|
+
* for the tar cache (ARCHITECTURE.md) — no confirmation, no lock check.
|
|
10
|
+
*
|
|
11
|
+
* Why not lock-aware: a concurrent `extractArchiveToCache` elsewhere may be
|
|
12
|
+
* holding `<cacheDir>.lock` / writing `<cacheDir>.staging` inside this root
|
|
13
|
+
* when it is removed, which can surface as an ENOENT in that extraction (or,
|
|
14
|
+
* rarely, a second extractor racing into a freshly recreated `.staging`
|
|
15
|
+
* path). This is the same exposure a manual `rm -rf` already has today —
|
|
16
|
+
* this function does not add new risk, it just makes that pre-existing,
|
|
17
|
+
* accepted risk reachable via a single explicit command.
|
|
18
|
+
* @param cacheRoot - Absolute path to the cache root to remove.
|
|
19
|
+
* @returns `true` if `cacheRoot` existed and was removed, `false` if it was
|
|
20
|
+
* already absent.
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const removed = await clearArchiveCacheRoot(getArchiveCacheRoot());
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function clearArchiveCacheRoot(cacheRoot: string): Promise<boolean>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { pathExists } from './path-exists.js';
|
|
3
|
+
/**
|
|
4
|
+
* Remove an entire archive cache root, including every tar-cache entry and
|
|
5
|
+
* any sibling directory living under it (e.g. the `@nitpicker/core` analyze
|
|
6
|
+
* `table` cache, which is resolved as a child of the same root).
|
|
7
|
+
*
|
|
8
|
+
* Pure: takes `cacheRoot` as a parameter and never resolves it itself, so
|
|
9
|
+
* tests can point it at a throwaway directory instead of the real OS temp
|
|
10
|
+
* cache. Mirrors the "manual `rm -rf` is safe" contract already documented
|
|
11
|
+
* for the tar cache (ARCHITECTURE.md) — no confirmation, no lock check.
|
|
12
|
+
*
|
|
13
|
+
* Why not lock-aware: a concurrent `extractArchiveToCache` elsewhere may be
|
|
14
|
+
* holding `<cacheDir>.lock` / writing `<cacheDir>.staging` inside this root
|
|
15
|
+
* when it is removed, which can surface as an ENOENT in that extraction (or,
|
|
16
|
+
* rarely, a second extractor racing into a freshly recreated `.staging`
|
|
17
|
+
* path). This is the same exposure a manual `rm -rf` already has today —
|
|
18
|
+
* this function does not add new risk, it just makes that pre-existing,
|
|
19
|
+
* accepted risk reachable via a single explicit command.
|
|
20
|
+
* @param cacheRoot - Absolute path to the cache root to remove.
|
|
21
|
+
* @returns `true` if `cacheRoot` existed and was removed, `false` if it was
|
|
22
|
+
* already absent.
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* const removed = await clearArchiveCacheRoot(getArchiveCacheRoot());
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export async function clearArchiveCacheRoot(cacheRoot) {
|
|
29
|
+
const existedBefore = await pathExists(cacheRoot);
|
|
30
|
+
await fs.rm(cacheRoot, { recursive: true, force: true });
|
|
31
|
+
return existedBefore;
|
|
32
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ArchiveCacheEntry } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* List the top-level entries found directly under an archive cache root,
|
|
4
|
+
* with a recursive size total and the most recent mtime found in each.
|
|
5
|
+
*
|
|
6
|
+
* Pure: takes `cacheRoot` as a parameter and never resolves it itself (see
|
|
7
|
+
* {@link import('./get-archive-cache-root.js').getArchiveCacheRoot} for the
|
|
8
|
+
* production root), so tests can point it at a throwaway directory instead
|
|
9
|
+
* of the real OS temp cache.
|
|
10
|
+
*
|
|
11
|
+
* Symbolic links are reported but never followed, both at the top level and
|
|
12
|
+
* while walking a directory's contents — this avoids escaping `cacheRoot`
|
|
13
|
+
* and crashing on dangling links.
|
|
14
|
+
* @param cacheRoot - Absolute path to the cache root to inspect.
|
|
15
|
+
* @returns One entry per top-level child of `cacheRoot`, in `fs.readdir`
|
|
16
|
+
* order. Returns `[]` if `cacheRoot` does not exist.
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* const entries = await listArchiveCacheEntries(getArchiveCacheRoot());
|
|
20
|
+
* const totalBytes = entries.reduce((sum, e) => sum + e.sizeBytes, 0);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function listArchiveCacheEntries(cacheRoot: string): Promise<ArchiveCacheEntry[]>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* List the top-level entries found directly under an archive cache root,
|
|
5
|
+
* with a recursive size total and the most recent mtime found in each.
|
|
6
|
+
*
|
|
7
|
+
* Pure: takes `cacheRoot` as a parameter and never resolves it itself (see
|
|
8
|
+
* {@link import('./get-archive-cache-root.js').getArchiveCacheRoot} for the
|
|
9
|
+
* production root), so tests can point it at a throwaway directory instead
|
|
10
|
+
* of the real OS temp cache.
|
|
11
|
+
*
|
|
12
|
+
* Symbolic links are reported but never followed, both at the top level and
|
|
13
|
+
* while walking a directory's contents — this avoids escaping `cacheRoot`
|
|
14
|
+
* and crashing on dangling links.
|
|
15
|
+
* @param cacheRoot - Absolute path to the cache root to inspect.
|
|
16
|
+
* @returns One entry per top-level child of `cacheRoot`, in `fs.readdir`
|
|
17
|
+
* order. Returns `[]` if `cacheRoot` does not exist.
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const entries = await listArchiveCacheEntries(getArchiveCacheRoot());
|
|
21
|
+
* const totalBytes = entries.reduce((sum, e) => sum + e.sizeBytes, 0);
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export async function listArchiveCacheEntries(cacheRoot) {
|
|
25
|
+
let topLevel;
|
|
26
|
+
try {
|
|
27
|
+
topLevel = await fs.readdir(cacheRoot, { withFileTypes: true });
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error.code === 'ENOENT') {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
const entries = [];
|
|
36
|
+
for (const dirent of topLevel) {
|
|
37
|
+
const entryPath = path.join(cacheRoot, dirent.name);
|
|
38
|
+
if (dirent.isSymbolicLink()) {
|
|
39
|
+
entries.push({
|
|
40
|
+
kind: 'unknown',
|
|
41
|
+
name: dirent.name,
|
|
42
|
+
path: entryPath,
|
|
43
|
+
sizeBytes: 0,
|
|
44
|
+
mtimeMs: 0,
|
|
45
|
+
});
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (dirent.isDirectory()) {
|
|
49
|
+
const { sizeBytes, mtimeMs } = await computeDirectoryStats(entryPath);
|
|
50
|
+
entries.push({
|
|
51
|
+
kind: classifyEntryName(dirent.name),
|
|
52
|
+
name: dirent.name,
|
|
53
|
+
path: entryPath,
|
|
54
|
+
sizeBytes,
|
|
55
|
+
mtimeMs,
|
|
56
|
+
});
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const stat = await fs.stat(entryPath);
|
|
60
|
+
entries.push({
|
|
61
|
+
kind: 'unknown',
|
|
62
|
+
name: dirent.name,
|
|
63
|
+
path: entryPath,
|
|
64
|
+
sizeBytes: stat.size,
|
|
65
|
+
mtimeMs: stat.mtimeMs,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return entries;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Matches the corrupt-quarantine suffix `extractArchiveToCache` appends
|
|
72
|
+
* (`${cacheDir}.corrupt.${pid}.${counter}`, both segments always numeric —
|
|
73
|
+
* see `quarantineHalfPopulatedCacheDir` in `extract-archive-to-cache.ts`).
|
|
74
|
+
* Anchored and numeric-specific rather than a loose `includes('.corrupt.')`
|
|
75
|
+
* so an archive whose own (sanitized) basename merely contains the
|
|
76
|
+
* substring `.corrupt.` — e.g. `my.corrupt.report.nitpicker` — is not
|
|
77
|
+
* misclassified as an orphan and offered up for deletion by `cache list`.
|
|
78
|
+
*/
|
|
79
|
+
const CORRUPT_QUARANTINE_SUFFIX = /\.corrupt\.\d+\.\d+$/;
|
|
80
|
+
/**
|
|
81
|
+
* Classify a cache-root child by name pattern alone (no filesystem access).
|
|
82
|
+
*
|
|
83
|
+
* This is a heuristic: `.staging`/corrupt-quarantine suffixes are appended
|
|
84
|
+
* by `extractArchiveToCache` onto an existing tar-cache dir name, so an
|
|
85
|
+
* archive whose own sanitized basename happens to end in exactly one of
|
|
86
|
+
* these literal suffixes is indistinguishable from a real orphan by name
|
|
87
|
+
* alone. Disambiguating fully would require re-deriving each entry's
|
|
88
|
+
* expected name from its source archive, which `list`/`clear` (by design,
|
|
89
|
+
* see grill-me scope) never has access to. Accepted as a rare, low-cost
|
|
90
|
+
* misclassification.
|
|
91
|
+
* @param name - Base name of the top-level entry.
|
|
92
|
+
*/
|
|
93
|
+
function classifyEntryName(name) {
|
|
94
|
+
if (name === 'table') {
|
|
95
|
+
return 'table';
|
|
96
|
+
}
|
|
97
|
+
if (name.endsWith('.staging') || CORRUPT_QUARANTINE_SUFFIX.test(name)) {
|
|
98
|
+
return 'orphan';
|
|
99
|
+
}
|
|
100
|
+
return 'tar-cache';
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Recursively sum file sizes and find the most recent mtime under a
|
|
104
|
+
* directory. Symbolic links are neither followed nor counted.
|
|
105
|
+
* @param dirPath - Absolute path to the directory to walk.
|
|
106
|
+
*/
|
|
107
|
+
async function computeDirectoryStats(dirPath) {
|
|
108
|
+
let sizeBytes = 0;
|
|
109
|
+
let mtimeMs = 0;
|
|
110
|
+
const children = await fs.readdir(dirPath, { recursive: true, withFileTypes: true });
|
|
111
|
+
for (const child of children) {
|
|
112
|
+
if (!child.isFile()) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const childPath = path.join(child.parentPath, child.name);
|
|
116
|
+
const stat = await fs.stat(childPath);
|
|
117
|
+
sizeBytes += stat.size;
|
|
118
|
+
mtimeMs = Math.max(mtimeMs, stat.mtimeMs);
|
|
119
|
+
}
|
|
120
|
+
if (mtimeMs === 0) {
|
|
121
|
+
const dirStat = await fs.stat(dirPath);
|
|
122
|
+
mtimeMs = dirStat.mtimeMs;
|
|
123
|
+
}
|
|
124
|
+
return { sizeBytes, mtimeMs };
|
|
125
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async existence probe — avoids blocking the event loop on the common
|
|
3
|
+
* "check before remove" path shared by
|
|
4
|
+
* {@link import('./clear-archive-cache-root.js').clearArchiveCacheRoot} and
|
|
5
|
+
* {@link import('./clear-archive-cache-entry.js').clearArchiveCacheEntry}.
|
|
6
|
+
* @param targetPath - Absolute path to probe.
|
|
7
|
+
* @returns `true` if the path is reachable via `fs.access`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function pathExists(targetPath: string): Promise<boolean>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
/**
|
|
3
|
+
* Async existence probe — avoids blocking the event loop on the common
|
|
4
|
+
* "check before remove" path shared by
|
|
5
|
+
* {@link import('./clear-archive-cache-root.js').clearArchiveCacheRoot} and
|
|
6
|
+
* {@link import('./clear-archive-cache-entry.js').clearArchiveCacheEntry}.
|
|
7
|
+
* @param targetPath - Absolute path to probe.
|
|
8
|
+
* @returns `true` if the path is reachable via `fs.access`.
|
|
9
|
+
*/
|
|
10
|
+
export async function pathExists(targetPath) {
|
|
11
|
+
try {
|
|
12
|
+
await fs.access(targetPath);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classification of a top-level entry found directly under an archive cache
|
|
3
|
+
* root ({@link import('./get-archive-cache-root.js').getArchiveCacheRoot}).
|
|
4
|
+
*
|
|
5
|
+
* - `'tar-cache'`: a per-archive extraction produced by
|
|
6
|
+
* {@link import('./resolve-archive-cache-dir.js').resolveArchiveCacheDir}
|
|
7
|
+
* (`<cacheKey>-<safeBasename>`).
|
|
8
|
+
* - `'table'`: the `@nitpicker/core` analyze-plugin scratch cache
|
|
9
|
+
* (`getTableCacheRoot()`), a sibling directory literally named `table`.
|
|
10
|
+
* - `'orphan'`: a `.staging` or `.corrupt.<pid>.<n>` leftover from an
|
|
11
|
+
* interrupted {@link import('./extract-archive-to-cache.js').extractArchiveToCache}
|
|
12
|
+
* run (see that file's quarantine/staging logic).
|
|
13
|
+
* - `'unknown'`: anything else (a stray file, a foreign directory a user
|
|
14
|
+
* dropped into the cache root).
|
|
15
|
+
*
|
|
16
|
+
* `'tar-cache'` and `'table'` cannot collide: `resolveArchiveCacheDir` always
|
|
17
|
+
* prefixes the cache key (digits and hyphens), so a tar-cache directory name
|
|
18
|
+
* is never the bare literal `table`.
|
|
19
|
+
*/
|
|
20
|
+
export type ArchiveCacheEntryKind = 'tar-cache' | 'table' | 'orphan' | 'unknown';
|
|
21
|
+
/** A single top-level entry found under an archive cache root. */
|
|
22
|
+
export interface ArchiveCacheEntry {
|
|
23
|
+
/** How this entry was classified by name pattern. */
|
|
24
|
+
readonly kind: ArchiveCacheEntryKind;
|
|
25
|
+
/** Base name of the entry (its final path segment). */
|
|
26
|
+
readonly name: string;
|
|
27
|
+
/** Absolute path to the entry. */
|
|
28
|
+
readonly path: string;
|
|
29
|
+
/** Total size in bytes of all regular files found under this entry (recursive for directories). */
|
|
30
|
+
readonly sizeBytes: number;
|
|
31
|
+
/** Most recent mtime (epoch ms) found among the entry's files, or the entry's own mtime if it has none. */
|
|
32
|
+
readonly mtimeMs: number;
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -14,13 +14,27 @@ import type { Knex } from 'knex';
|
|
|
14
14
|
* `MainContentsData` sub-entity arrays, one row per DOM element, FK →
|
|
15
15
|
* `content_items(id)`
|
|
16
16
|
* - `inventory_runs` — `--inventory` audit log (no FK; append-only)
|
|
17
|
+
* - `network_outages` — operator-network-outage journal (no FK; append-only
|
|
18
|
+
* except `ended_at`, which is written once on recovery)
|
|
19
|
+
* - `dedupe_cap_events` — `--dedupe-cap` same-cluster soft-cap audit log (no
|
|
20
|
+
* FK; append-only except `rejected_count`, which is written once at
|
|
21
|
+
* `crawlEnd`)
|
|
17
22
|
* - `analysis_text_refs` + `analysis_violations` — analyze-phase findings,
|
|
18
23
|
* FK → `content_items(id)`
|
|
19
24
|
* - `page_templates` — DOM-structure template classification (`--templates`,
|
|
20
25
|
* `@nitpicker/core`'s `template-classification/`), one row per classified
|
|
21
26
|
* page, FK → `content_items(id)`
|
|
27
|
+
* - `page_template_clusters` — one row per distinct `page_templates.template_key`,
|
|
28
|
+
* holding `@d-zero/page-cluster`'s cluster-selection evidence (no FK;
|
|
29
|
+
* `template_key` is not a `page_templates` FK target, so consistency is
|
|
30
|
+
* maintained by replacing both tables together, not by a foreign key)
|
|
22
31
|
* - `page_html_blobs` + `page_html_ref` — content-addressable HTML
|
|
23
32
|
* snapshots, FK → `content_items(id)`
|
|
33
|
+
* - `console_log_items` — content-addressable dictionary of distinct
|
|
34
|
+
* console messages / page errors (no FK; hash-deduplicated across every
|
|
35
|
+
* page in the archive, mirroring `text_refs` / `json_refs`)
|
|
36
|
+
* - `page_console_logs` — one row per (page, console log) occurrence, FK →
|
|
37
|
+
* `content_items(id)` and `console_log_items(id)`
|
|
24
38
|
*
|
|
25
39
|
* The DDL is shared between fresh-archive provisioning ({@link initSchema}
|
|
26
40
|
* calls this right after `createEntityTables`) and the migration script
|
|
@@ -13,13 +13,27 @@
|
|
|
13
13
|
* `MainContentsData` sub-entity arrays, one row per DOM element, FK →
|
|
14
14
|
* `content_items(id)`
|
|
15
15
|
* - `inventory_runs` — `--inventory` audit log (no FK; append-only)
|
|
16
|
+
* - `network_outages` — operator-network-outage journal (no FK; append-only
|
|
17
|
+
* except `ended_at`, which is written once on recovery)
|
|
18
|
+
* - `dedupe_cap_events` — `--dedupe-cap` same-cluster soft-cap audit log (no
|
|
19
|
+
* FK; append-only except `rejected_count`, which is written once at
|
|
20
|
+
* `crawlEnd`)
|
|
16
21
|
* - `analysis_text_refs` + `analysis_violations` — analyze-phase findings,
|
|
17
22
|
* FK → `content_items(id)`
|
|
18
23
|
* - `page_templates` — DOM-structure template classification (`--templates`,
|
|
19
24
|
* `@nitpicker/core`'s `template-classification/`), one row per classified
|
|
20
25
|
* page, FK → `content_items(id)`
|
|
26
|
+
* - `page_template_clusters` — one row per distinct `page_templates.template_key`,
|
|
27
|
+
* holding `@d-zero/page-cluster`'s cluster-selection evidence (no FK;
|
|
28
|
+
* `template_key` is not a `page_templates` FK target, so consistency is
|
|
29
|
+
* maintained by replacing both tables together, not by a foreign key)
|
|
21
30
|
* - `page_html_blobs` + `page_html_ref` — content-addressable HTML
|
|
22
31
|
* snapshots, FK → `content_items(id)`
|
|
32
|
+
* - `console_log_items` — content-addressable dictionary of distinct
|
|
33
|
+
* console messages / page errors (no FK; hash-deduplicated across every
|
|
34
|
+
* page in the archive, mirroring `text_refs` / `json_refs`)
|
|
35
|
+
* - `page_console_logs` — one row per (page, console log) occurrence, FK →
|
|
36
|
+
* `content_items(id)` and `console_log_items(id)`
|
|
23
37
|
*
|
|
24
38
|
* The DDL is shared between fresh-archive provisioning ({@link initSchema}
|
|
25
39
|
* calls this right after `createEntityTables`) and the migration script
|
|
@@ -279,10 +293,81 @@ export async function createAdjunctTables(instance) {
|
|
|
279
293
|
t.integer('new_pages').nullable();
|
|
280
294
|
t.integer('new_resources').nullable();
|
|
281
295
|
t.integer('scope_skipped').nullable();
|
|
296
|
+
t.integer('invalid_skipped').nullable();
|
|
282
297
|
t.text('notes').nullable();
|
|
283
298
|
t.index('ran_at');
|
|
284
299
|
});
|
|
285
300
|
}
|
|
301
|
+
if (!(await instance.schema.hasTable('network_outages'))) {
|
|
302
|
+
await instance.schema.createTable('network_outages', (t) => {
|
|
303
|
+
// One row per detected operator-network outage. The archive's
|
|
304
|
+
// evidence that a run of `dns`/`local-network`-shaped failures
|
|
305
|
+
// was caused by the crawl operator's own connectivity, not the
|
|
306
|
+
// target sites — used to retroactively re-classify `crawl_errors`
|
|
307
|
+
// / `page_errors` rows whose `createdAt` falls inside a window
|
|
308
|
+
// (see `is-within-outage-window.ts`). No index: a crawl session
|
|
309
|
+
// produces at most a handful of these rows, so a full in-memory
|
|
310
|
+
// scan by every consumer is cheaper than maintaining a B-tree
|
|
311
|
+
// that few queries would use (`ARCHITECTURE.md`'s
|
|
312
|
+
// perf-index-is-not-free / evidence-before-indexing rules).
|
|
313
|
+
t.increments('id');
|
|
314
|
+
// Backdated to the earliest error still inside the detector's
|
|
315
|
+
// sliding window at trigger time, NOT the trigger instant —
|
|
316
|
+
// see `NetworkOutageDetector`'s `OutageSuspect.startedAt`.
|
|
317
|
+
t.integer('started_at').notNullable();
|
|
318
|
+
// When the sliding window actually crossed both thresholds.
|
|
319
|
+
t.integer('detected_at').notNullable();
|
|
320
|
+
// NULL until a recovery probe succeeds. A row can also be left
|
|
321
|
+
// NULL forever if the crawl process is killed mid-outage; readers
|
|
322
|
+
// MUST NOT treat a NULL `ended_at` as an unbounded window (that
|
|
323
|
+
// would retroactively cover every later error as
|
|
324
|
+
// network-caused) — see `is-within-outage-window.ts` and the
|
|
325
|
+
// writer-side clamp-on-next-open in `db-ops/outages/`.
|
|
326
|
+
t.integer('ended_at').nullable();
|
|
327
|
+
t.string('probe_host').nullable();
|
|
328
|
+
t.integer('trigger_error_count').notNullable();
|
|
329
|
+
t.integer('trigger_host_count').notNullable();
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
if (!(await instance.schema.hasTable('dedupe_cap_events'))) {
|
|
333
|
+
await instance.schema.createTable('dedupe_cap_events', (t) => {
|
|
334
|
+
// One row per URL shape the `--dedupe-cap` same-cluster soft cap
|
|
335
|
+
// (`DedupeCapTracker`) confirmed as a trap during this crawl. No
|
|
336
|
+
// index: a crawl produces at most a handful of these rows (same
|
|
337
|
+
// reasoning as `network_outages`, above).
|
|
338
|
+
t.increments('id');
|
|
339
|
+
// The URL shape key (`computeShapeKey`) that capped — a template
|
|
340
|
+
// with placeholders (e.g. `example.com/news/date/{n}/`), not a
|
|
341
|
+
// literal URL.
|
|
342
|
+
t.string('shape_key').notNullable();
|
|
343
|
+
// One concrete URL matching this shape, captured at cap time so a
|
|
344
|
+
// human reading the audit log can identify what was being
|
|
345
|
+
// crawled — `shape_key` alone is a template, not a navigable URL.
|
|
346
|
+
t.string('sample_url').notNullable();
|
|
347
|
+
// `computeBodyHash` result recorded at cap time. Nullable only in
|
|
348
|
+
// the sense that BLOB columns are nullable by default; every row
|
|
349
|
+
// this feature writes populates it (a page with no rendered
|
|
350
|
+
// `<body>` never reaches the tracker — see `Crawler#handleResult`).
|
|
351
|
+
t.binary('body_hash').nullable();
|
|
352
|
+
// The Misra-Gries threshold that actually triggered the cap,
|
|
353
|
+
// after halving for the `body_hash`-match / `og:url`-mismatch
|
|
354
|
+
// confidence signals — NOT necessarily equal to `--dedupe-cap`'s
|
|
355
|
+
// raw value.
|
|
356
|
+
t.integer('effective_threshold').notNullable();
|
|
357
|
+
// The tracker's Misra-Gries counter value at cap time: a LOWER
|
|
358
|
+
// BOUND on the number of matching-signature pages seen for this
|
|
359
|
+
// shape, not an exact observation count (see `DedupeCapTracker`).
|
|
360
|
+
t.integer('observed_count').notNullable();
|
|
361
|
+
t.integer('detected_at').notNullable();
|
|
362
|
+
// NULL until `crawlEnd` finalizes it (see
|
|
363
|
+
// `Crawler#getDedupeCapRejections`). Unlike `network_outages.ended_at`,
|
|
364
|
+
// a NULL here has no ambiguous "still ongoing" reading — a
|
|
365
|
+
// crawl that never reached `crawlEnd` simply left the count
|
|
366
|
+
// undetermined, so no boot-time reconciliation pass is needed
|
|
367
|
+
// (readers display "unknown", not "0" or "unbounded").
|
|
368
|
+
t.integer('rejected_count').nullable();
|
|
369
|
+
});
|
|
370
|
+
}
|
|
286
371
|
if (!(await instance.schema.hasTable('analysis_text_refs'))) {
|
|
287
372
|
await instance.raw(`
|
|
288
373
|
CREATE TABLE analysis_text_refs (
|
|
@@ -334,6 +419,34 @@ export async function createAdjunctTables(instance) {
|
|
|
334
419
|
) WITHOUT ROWID
|
|
335
420
|
`);
|
|
336
421
|
}
|
|
422
|
+
// One row per distinct `template_key` produced by the same `--templates`
|
|
423
|
+
// classification run, holding `@d-zero/page-cluster`'s cluster-selection
|
|
424
|
+
// evidence (`ClusterReason`, renamed `TemplateClusterReason` on this side)
|
|
425
|
+
// as a zstd-compressed JSON blob — same BLOB+codec+size shape as
|
|
426
|
+
// `page_html_blobs` below. A column on `page_templates` was rejected: that
|
|
427
|
+
// table is one row per *page*, so the same cluster's reason would be
|
|
428
|
+
// duplicated across every member page (multi-GB on a large archive with a
|
|
429
|
+
// few-hundred-member cluster). A `json_refs` row was also rejected: reason
|
|
430
|
+
// payloads differ per cluster (distinct `memberCount`/token sets), so
|
|
431
|
+
// content-address dedup would not pay for itself, and `json_refs` is a
|
|
432
|
+
// shared dictionary that other tables reference — this table's full
|
|
433
|
+
// replace-on-every-run write pattern (see `replacePageTemplates`) would
|
|
434
|
+
// otherwise leave orphaned rows behind with no owner able to delete them.
|
|
435
|
+
// No FK to `page_templates`: `template_key` is not that table's primary
|
|
436
|
+
// key (`page_id` is), so there is nothing to reference — consistency is
|
|
437
|
+
// instead maintained by replacing both tables in the same transaction.
|
|
438
|
+
if (!(await instance.schema.hasTable('page_template_clusters'))) {
|
|
439
|
+
await instance.raw(`
|
|
440
|
+
CREATE TABLE page_template_clusters (
|
|
441
|
+
template_key TEXT PRIMARY KEY,
|
|
442
|
+
member_count INTEGER NOT NULL,
|
|
443
|
+
reason_json BLOB NOT NULL,
|
|
444
|
+
codec TEXT NOT NULL CHECK(codec IN ('zstd', 'none')),
|
|
445
|
+
size_raw INTEGER NOT NULL,
|
|
446
|
+
size_stored INTEGER NOT NULL
|
|
447
|
+
) WITHOUT ROWID
|
|
448
|
+
`);
|
|
449
|
+
}
|
|
337
450
|
// Content-addressable HTML blob storage. Knex's schema builder doesn't
|
|
338
451
|
// expose a WITHOUT ROWID toggle, so the BLOB tables are created via raw
|
|
339
452
|
// SQL. WITHOUT ROWID keeps the rows packed inside the b-tree leaves
|
|
@@ -360,4 +473,60 @@ export async function createAdjunctTables(instance) {
|
|
|
360
473
|
`);
|
|
361
474
|
await instance.raw('CREATE INDEX idx_page_html_ref_hash ON page_html_ref(hash)');
|
|
362
475
|
}
|
|
476
|
+
// Content-addressable dictionary of distinct console messages / page
|
|
477
|
+
// errors (beholder's `ConsoleLogEntry`, issue #228). `hash` is
|
|
478
|
+
// SHA-256 over a canonical tuple of every content field (type, text,
|
|
479
|
+
// args JSON, location, stack) — the same identical warning emitted by
|
|
480
|
+
// a shared framework on every page therefore collapses to one row
|
|
481
|
+
// regardless of how many pages or how many times it fires, mirroring
|
|
482
|
+
// `text_refs` / `json_refs`. `args_json_id` is nullable because a
|
|
483
|
+
// call with no arguments (or one whose args failed to
|
|
484
|
+
// `JSON.stringify`, e.g. a circular reference) has nothing to store.
|
|
485
|
+
// `text_id` is nullable too: `text_refs` never stores the empty
|
|
486
|
+
// string (its dictionary upsert treats `''` as "nothing to dedupe"),
|
|
487
|
+
// so a call like `console.log()` with zero arguments — whose
|
|
488
|
+
// `text` beholder reports as `''` — has no `text_refs` row to point
|
|
489
|
+
// at; `text_id = NULL` there means "empty text", read back as `''`.
|
|
490
|
+
// `type` keeps its own index for the Console Logs view's type filter.
|
|
491
|
+
if (!(await instance.schema.hasTable('console_log_items'))) {
|
|
492
|
+
await instance.raw(`
|
|
493
|
+
CREATE TABLE console_log_items (
|
|
494
|
+
id INTEGER PRIMARY KEY,
|
|
495
|
+
hash BLOB NOT NULL UNIQUE,
|
|
496
|
+
type TEXT NOT NULL,
|
|
497
|
+
text_id INTEGER REFERENCES text_refs(id),
|
|
498
|
+
args_json_id INTEGER REFERENCES json_refs(id),
|
|
499
|
+
loc_url_id INTEGER REFERENCES url_refs(id),
|
|
500
|
+
loc_line INTEGER,
|
|
501
|
+
loc_column INTEGER,
|
|
502
|
+
stack_text_id INTEGER REFERENCES text_refs(id)
|
|
503
|
+
)
|
|
504
|
+
`);
|
|
505
|
+
await instance.raw('CREATE INDEX idx_console_log_items_type ON console_log_items(type)');
|
|
506
|
+
}
|
|
507
|
+
// One row per (page, console log) occurrence — beholder captures a
|
|
508
|
+
// `ts` per firing, so the same message logged 3 times on one page
|
|
509
|
+
// yields 3 rows (unlike `anchor_edges`' first-wins dedup: an
|
|
510
|
+
// occurrence count matters here, not just presence). Replaced
|
|
511
|
+
// wholesale per page on every non-empty scrape by
|
|
512
|
+
// `replaceConsoleLogs`, the same Scoped-Replace pattern as
|
|
513
|
+
// `anchor_edges` / `image_items` — there is no natural key to UPDATE
|
|
514
|
+
// a specific prior occurrence against.
|
|
515
|
+
if (!(await instance.schema.hasTable('page_console_logs'))) {
|
|
516
|
+
await instance.schema.createTable('page_console_logs', (t) => {
|
|
517
|
+
t.increments('id');
|
|
518
|
+
t.integer('pageId')
|
|
519
|
+
.notNullable()
|
|
520
|
+
.unsigned()
|
|
521
|
+
.references('content_items.id')
|
|
522
|
+
.onDelete('CASCADE');
|
|
523
|
+
t.integer('consoleLogId')
|
|
524
|
+
.notNullable()
|
|
525
|
+
.unsigned()
|
|
526
|
+
.references('console_log_items.id');
|
|
527
|
+
t.integer('ts').notNullable();
|
|
528
|
+
t.index('pageId');
|
|
529
|
+
t.index('consoleLogId');
|
|
530
|
+
});
|
|
531
|
+
}
|
|
363
532
|
}
|
|
@@ -197,6 +197,7 @@ export async function createEntityTables(instance) {
|
|
|
197
197
|
content_length INTEGER,
|
|
198
198
|
header_set_id INTEGER REFERENCES header_sets(id),
|
|
199
199
|
redirect_dest_id INTEGER REFERENCES content_items(id) DEFERRABLE INITIALLY DEFERRED,
|
|
200
|
+
alias_of_id INTEGER REFERENCES content_items(id) DEFERRABLE INITIALLY DEFERRED,
|
|
200
201
|
source TEXT NOT NULL DEFAULT 'crawled',
|
|
201
202
|
first_crawled_at INTEGER,
|
|
202
203
|
last_crawled_at INTEGER,
|
|
@@ -208,6 +209,15 @@ export async function createEntityTables(instance) {
|
|
|
208
209
|
await instance.raw('CREATE INDEX IF NOT EXISTS idx_content_items_external ON content_items(is_external)');
|
|
209
210
|
await instance.raw('CREATE INDEX IF NOT EXISTS idx_content_items_scraped ON content_items(scraped)');
|
|
210
211
|
await instance.raw('CREATE INDEX IF NOT EXISTS idx_content_items_redirect_dest_id ON content_items(redirect_dest_id)');
|
|
212
|
+
// `idx_content_items_alias_of_id` is NOT created here even though
|
|
213
|
+
// `alias_of_id` is: this DDL runs unconditionally on every archive open
|
|
214
|
+
// (including legacy archives that still lack the column at this point,
|
|
215
|
+
// before `migrateContentItemsAliasOfId` adds it), so an unconditional
|
|
216
|
+
// `CREATE INDEX ... alias_of_id` here would fail with `no such column`
|
|
217
|
+
// on any archive that predates this feature. The index is created in
|
|
218
|
+
// `migrateContentItemsAliasOfId` instead, which runs after the
|
|
219
|
+
// column-add guard for both fresh and legacy archives (same reasoning as
|
|
220
|
+
// `page_meta.body_hash`'s index).
|
|
211
221
|
await instance.raw('CREATE INDEX IF NOT EXISTS idx_content_items_content_type_id ON content_items(content_type_id)');
|
|
212
222
|
await instance.raw('CREATE INDEX IF NOT EXISTS idx_content_items_crawl_order ON content_items(crawl_order)');
|
|
213
223
|
await instance.raw('CREATE INDEX IF NOT EXISTS idx_content_items_source ON content_items(source)');
|
|
@@ -279,10 +289,20 @@ export async function createEntityTables(instance) {
|
|
|
279
289
|
main_content_audio_count INTEGER,
|
|
280
290
|
main_content_canvas_count INTEGER,
|
|
281
291
|
scroll_height_desktop INTEGER,
|
|
282
|
-
scroll_height_mobile INTEGER
|
|
292
|
+
scroll_height_mobile INTEGER,
|
|
293
|
+
body_hash BLOB,
|
|
294
|
+
console_error_count INTEGER
|
|
283
295
|
)
|
|
284
296
|
`);
|
|
285
297
|
await instance.raw('CREATE INDEX IF NOT EXISTS idx_page_meta_og_type ON page_meta(og_type)');
|
|
298
|
+
// `idx_page_meta_body_hash` is NOT created here even though `body_hash`
|
|
299
|
+
// is: this DDL runs unconditionally on every archive open (including
|
|
300
|
+
// legacy archives that still lack the column at this point, before
|
|
301
|
+
// `migratePageMetaBodyHash` adds it), so an unconditional
|
|
302
|
+
// `CREATE INDEX ... body_hash` here would fail with `no such column` on
|
|
303
|
+
// any archive that predates this feature. The index is created in
|
|
304
|
+
// `migratePageMetaBodyHash` instead, which runs after the column-add
|
|
305
|
+
// guard for both fresh and legacy archives.
|
|
286
306
|
await instance.raw('CREATE INDEX IF NOT EXISTS idx_page_meta_robots_noindex ON page_meta(robots_noindex)');
|
|
287
307
|
await instance.raw(`
|
|
288
308
|
CREATE TABLE IF NOT EXISTS resource_items (
|