@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,26 @@
|
|
|
1
|
+
import type { Knex } from 'knex';
|
|
2
|
+
/**
|
|
3
|
+
* Durably close every `network_outages` row still `ended_at = NULL` at the
|
|
4
|
+
* start of a writer session.
|
|
5
|
+
*
|
|
6
|
+
* A row is left open only when the crawl process was killed (Ctrl-C / OOM
|
|
7
|
+
* / SIGKILL) mid-outage, before a recovery probe could close it. Called
|
|
8
|
+
* once from `db-ops/lifecycle/init.ts` — right after `initSchema` (which
|
|
9
|
+
* guarantees the table exists) and before anything else touches
|
|
10
|
+
* `network_outages` — so that by the time `resetFailedPages` /
|
|
11
|
+
* `listDnsBurnedHostCandidates` / any other reader runs, no row can still
|
|
12
|
+
* be open from a PRIOR session. (A row opened by THIS session cannot exist
|
|
13
|
+
* yet at this point in the boot sequence — the sliding-window detector
|
|
14
|
+
* only starts once the crawl loop begins.)
|
|
15
|
+
*
|
|
16
|
+
* This complements, but does not replace, `list-network-outages.ts`'s
|
|
17
|
+
* on-the-fly clamp: that read-side resolution stays in place as a
|
|
18
|
+
* defensive fallback, but after this runs there should be nothing left for
|
|
19
|
+
* it to resolve.
|
|
20
|
+
*
|
|
21
|
+
* Idempotent: a row closed by a previous call (or by a normal
|
|
22
|
+
* recovery-probe success) is simply absent from the `whereNull('ended_at')`
|
|
23
|
+
* scan, so re-running this on every writer open is always safe.
|
|
24
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
25
|
+
*/
|
|
26
|
+
export declare function closeStaleOpenNetworkOutages(knex: Knex): Promise<void>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { closeNetworkOutage } from './close-network-outage.js';
|
|
2
|
+
import { computeOutageClampTimestamp } from './compute-outage-clamp-timestamp.js';
|
|
3
|
+
/**
|
|
4
|
+
* Durably close every `network_outages` row still `ended_at = NULL` at the
|
|
5
|
+
* start of a writer session.
|
|
6
|
+
*
|
|
7
|
+
* A row is left open only when the crawl process was killed (Ctrl-C / OOM
|
|
8
|
+
* / SIGKILL) mid-outage, before a recovery probe could close it. Called
|
|
9
|
+
* once from `db-ops/lifecycle/init.ts` — right after `initSchema` (which
|
|
10
|
+
* guarantees the table exists) and before anything else touches
|
|
11
|
+
* `network_outages` — so that by the time `resetFailedPages` /
|
|
12
|
+
* `listDnsBurnedHostCandidates` / any other reader runs, no row can still
|
|
13
|
+
* be open from a PRIOR session. (A row opened by THIS session cannot exist
|
|
14
|
+
* yet at this point in the boot sequence — the sliding-window detector
|
|
15
|
+
* only starts once the crawl loop begins.)
|
|
16
|
+
*
|
|
17
|
+
* This complements, but does not replace, `list-network-outages.ts`'s
|
|
18
|
+
* on-the-fly clamp: that read-side resolution stays in place as a
|
|
19
|
+
* defensive fallback, but after this runs there should be nothing left for
|
|
20
|
+
* it to resolve.
|
|
21
|
+
*
|
|
22
|
+
* Idempotent: a row closed by a previous call (or by a normal
|
|
23
|
+
* recovery-probe success) is simply absent from the `whereNull('ended_at')`
|
|
24
|
+
* scan, so re-running this on every writer open is always safe.
|
|
25
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
26
|
+
*/
|
|
27
|
+
export async function closeStaleOpenNetworkOutages(knex) {
|
|
28
|
+
const hasTable = await knex.schema.hasTable('network_outages');
|
|
29
|
+
if (!hasTable) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const openRows = (await knex('network_outages')
|
|
33
|
+
.whereNull('ended_at')
|
|
34
|
+
.select('id', 'started_at'));
|
|
35
|
+
if (openRows.length === 0) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const clamp = await computeOutageClampTimestamp(knex);
|
|
39
|
+
for (const row of openRows) {
|
|
40
|
+
await closeNetworkOutage(knex, row.id, Math.max(clamp, row.started_at));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Knex } from 'knex';
|
|
2
|
+
/**
|
|
3
|
+
* Compute the timestamp used to resolve an outage row whose `ended_at` is
|
|
4
|
+
* still `NULL` (the crawl process was killed before a recovery probe could
|
|
5
|
+
* close it) into a concrete, bounded window.
|
|
6
|
+
*
|
|
7
|
+
* The archive's own most-recent observations — the latest `crawl_errors`
|
|
8
|
+
* timestamp and the latest `content_items.last_crawled_at` — are the only
|
|
9
|
+
* evidence available for "when did activity in this archive last happen",
|
|
10
|
+
* so the later of the two stands in for "the outage cannot have lasted
|
|
11
|
+
* past this point, because the archive shows activity here". Using the
|
|
12
|
+
* LARGER of the two (not just `crawl_errors`) matters because a session
|
|
13
|
+
* that crashed mid-outage may have its last successful page write be more
|
|
14
|
+
* recent than its last recorded error, or vice versa.
|
|
15
|
+
*
|
|
16
|
+
* Both source tables are guaranteed to exist by the time this runs — this
|
|
17
|
+
* helper is only ever called from writer-context code (see
|
|
18
|
+
* `list-network-outages.ts` / the boot-time stale-open finalizer), and
|
|
19
|
+
* `initSchema` creates both `crawl_errors` and `content_items` before any
|
|
20
|
+
* writer session's first query.
|
|
21
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
22
|
+
* @returns The larger of `MAX(crawl_errors.createdAt)` and
|
|
23
|
+
* `MAX(content_items.last_crawled_at)`, or `0` if the archive has neither
|
|
24
|
+
* (a brand-new archive with no activity yet).
|
|
25
|
+
*/
|
|
26
|
+
export declare function computeOutageClampTimestamp(knex: Knex): Promise<number>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compute the timestamp used to resolve an outage row whose `ended_at` is
|
|
3
|
+
* still `NULL` (the crawl process was killed before a recovery probe could
|
|
4
|
+
* close it) into a concrete, bounded window.
|
|
5
|
+
*
|
|
6
|
+
* The archive's own most-recent observations — the latest `crawl_errors`
|
|
7
|
+
* timestamp and the latest `content_items.last_crawled_at` — are the only
|
|
8
|
+
* evidence available for "when did activity in this archive last happen",
|
|
9
|
+
* so the later of the two stands in for "the outage cannot have lasted
|
|
10
|
+
* past this point, because the archive shows activity here". Using the
|
|
11
|
+
* LARGER of the two (not just `crawl_errors`) matters because a session
|
|
12
|
+
* that crashed mid-outage may have its last successful page write be more
|
|
13
|
+
* recent than its last recorded error, or vice versa.
|
|
14
|
+
*
|
|
15
|
+
* Both source tables are guaranteed to exist by the time this runs — this
|
|
16
|
+
* helper is only ever called from writer-context code (see
|
|
17
|
+
* `list-network-outages.ts` / the boot-time stale-open finalizer), and
|
|
18
|
+
* `initSchema` creates both `crawl_errors` and `content_items` before any
|
|
19
|
+
* writer session's first query.
|
|
20
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
21
|
+
* @returns The larger of `MAX(crawl_errors.createdAt)` and
|
|
22
|
+
* `MAX(content_items.last_crawled_at)`, or `0` if the archive has neither
|
|
23
|
+
* (a brand-new archive with no activity yet).
|
|
24
|
+
*/
|
|
25
|
+
export async function computeOutageClampTimestamp(knex) {
|
|
26
|
+
const [crawlErrorsRow] = (await knex('crawl_errors').max('createdAt as maxCreatedAt'));
|
|
27
|
+
const [contentItemsRow] = (await knex('content_items').max('last_crawled_at as maxLastCrawledAt'));
|
|
28
|
+
const latestError = crawlErrorsRow?.maxCreatedAt ?? 0;
|
|
29
|
+
const latestCrawl = contentItemsRow?.maxLastCrawledAt ?? 0;
|
|
30
|
+
return Math.max(latestError, latestCrawl);
|
|
31
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { InsertNetworkOutageParams } from '../../types.js';
|
|
2
|
+
import type { Knex } from 'knex';
|
|
3
|
+
/**
|
|
4
|
+
* Appends one row to the `network_outages` journal, with `ended_at` left
|
|
5
|
+
* `NULL` — the row starts life as an open outage.
|
|
6
|
+
*
|
|
7
|
+
* Called the moment a recovery probe CONFIRMS a suspect outage (i.e. the
|
|
8
|
+
* probe itself failed, not merely the sliding-window threshold trip). See
|
|
9
|
+
* `close-network-outage.ts` for how the row is later closed.
|
|
10
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
11
|
+
* @param params - The confirmed-outage fields to record.
|
|
12
|
+
* @returns The autoincremented `id` of the newly-inserted row.
|
|
13
|
+
*/
|
|
14
|
+
export declare function insertNetworkOutage(knex: Knex, params: InsertNetworkOutageParams): Promise<number>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Appends one row to the `network_outages` journal, with `ended_at` left
|
|
3
|
+
* `NULL` — the row starts life as an open outage.
|
|
4
|
+
*
|
|
5
|
+
* Called the moment a recovery probe CONFIRMS a suspect outage (i.e. the
|
|
6
|
+
* probe itself failed, not merely the sliding-window threshold trip). See
|
|
7
|
+
* `close-network-outage.ts` for how the row is later closed.
|
|
8
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
9
|
+
* @param params - The confirmed-outage fields to record.
|
|
10
|
+
* @returns The autoincremented `id` of the newly-inserted row.
|
|
11
|
+
*/
|
|
12
|
+
export async function insertNetworkOutage(knex, params) {
|
|
13
|
+
const inserted = await knex
|
|
14
|
+
.from('network_outages')
|
|
15
|
+
.insert({
|
|
16
|
+
started_at: params.startedAt,
|
|
17
|
+
detected_at: params.detectedAt,
|
|
18
|
+
ended_at: null,
|
|
19
|
+
probe_host: params.probeHost,
|
|
20
|
+
trigger_error_count: params.triggerErrorCount,
|
|
21
|
+
trigger_host_count: params.triggerHostCount,
|
|
22
|
+
})
|
|
23
|
+
.returning('id');
|
|
24
|
+
const id = inserted[0]?.id;
|
|
25
|
+
if (typeof id !== 'number') {
|
|
26
|
+
throw new TypeError('insertNetworkOutage: INSERT returned no row id');
|
|
27
|
+
}
|
|
28
|
+
return id;
|
|
29
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { OutageWindow } from '../../../is-within-outage-window.js';
|
|
2
|
+
import type { Knex } from 'knex';
|
|
3
|
+
/**
|
|
4
|
+
* List every recorded outage as a resolved (closed) {@link OutageWindow},
|
|
5
|
+
* suitable for `isWithinOutageWindow`.
|
|
6
|
+
*
|
|
7
|
+
* This is the crawler-internal counterpart to `@nitpicker/query`'s richer
|
|
8
|
+
* `listNetworkOutages` (full row shape, for CLI/MCP display) — this one
|
|
9
|
+
* exists purely to feed the three write-path consumers
|
|
10
|
+
* (`resetFailedPages`, `listDnsBurnedHostCandidates`, the gate itself) that
|
|
11
|
+
* only need "was this timestamp inside an outage", never the row's other
|
|
12
|
+
* columns.
|
|
13
|
+
*
|
|
14
|
+
* Any row whose `ended_at` is still `NULL` (the crawl process was killed
|
|
15
|
+
* before recovery) is resolved on the fly via
|
|
16
|
+
* {@link computeOutageClampTimestamp} rather than ever being returned as an
|
|
17
|
+
* unbounded window — see `is-within-outage-window.ts`'s `OutageWindow`
|
|
18
|
+
* docstring for why an open-ended window would be a correctness bug (every
|
|
19
|
+
* later error would retroactively read as network-caused, forever). This
|
|
20
|
+
* on-the-fly resolution does NOT persist to the row — it is a defensive
|
|
21
|
+
* fallback independent of whichever boot-time finalizer durably closes
|
|
22
|
+
* stale-open rows.
|
|
23
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
24
|
+
* @returns Resolved outage windows. Empty when the archive predates the
|
|
25
|
+
* `network_outages` table (self-healed on next writer open, so this is
|
|
26
|
+
* never a permanent state) or has recorded no outages.
|
|
27
|
+
*/
|
|
28
|
+
export declare function listNetworkOutages(knex: Knex): Promise<OutageWindow[]>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { computeOutageClampTimestamp } from './compute-outage-clamp-timestamp.js';
|
|
2
|
+
/**
|
|
3
|
+
* List every recorded outage as a resolved (closed) {@link OutageWindow},
|
|
4
|
+
* suitable for `isWithinOutageWindow`.
|
|
5
|
+
*
|
|
6
|
+
* This is the crawler-internal counterpart to `@nitpicker/query`'s richer
|
|
7
|
+
* `listNetworkOutages` (full row shape, for CLI/MCP display) — this one
|
|
8
|
+
* exists purely to feed the three write-path consumers
|
|
9
|
+
* (`resetFailedPages`, `listDnsBurnedHostCandidates`, the gate itself) that
|
|
10
|
+
* only need "was this timestamp inside an outage", never the row's other
|
|
11
|
+
* columns.
|
|
12
|
+
*
|
|
13
|
+
* Any row whose `ended_at` is still `NULL` (the crawl process was killed
|
|
14
|
+
* before recovery) is resolved on the fly via
|
|
15
|
+
* {@link computeOutageClampTimestamp} rather than ever being returned as an
|
|
16
|
+
* unbounded window — see `is-within-outage-window.ts`'s `OutageWindow`
|
|
17
|
+
* docstring for why an open-ended window would be a correctness bug (every
|
|
18
|
+
* later error would retroactively read as network-caused, forever). This
|
|
19
|
+
* on-the-fly resolution does NOT persist to the row — it is a defensive
|
|
20
|
+
* fallback independent of whichever boot-time finalizer durably closes
|
|
21
|
+
* stale-open rows.
|
|
22
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
23
|
+
* @returns Resolved outage windows. Empty when the archive predates the
|
|
24
|
+
* `network_outages` table (self-healed on next writer open, so this is
|
|
25
|
+
* never a permanent state) or has recorded no outages.
|
|
26
|
+
*/
|
|
27
|
+
export async function listNetworkOutages(knex) {
|
|
28
|
+
const hasTable = await knex.schema.hasTable('network_outages');
|
|
29
|
+
if (!hasTable) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
const rows = (await knex('network_outages').select('started_at', 'ended_at'));
|
|
33
|
+
if (rows.length === 0) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
const hasOpenRow = rows.some((row) => row.ended_at === null);
|
|
37
|
+
const clamp = hasOpenRow ? await computeOutageClampTimestamp(knex) : 0;
|
|
38
|
+
return rows.map((row) => ({
|
|
39
|
+
startedAt: row.started_at,
|
|
40
|
+
endedAt: row.ended_at ?? Math.max(clamp, row.started_at),
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
@@ -29,6 +29,19 @@ import type { Knex } from 'knex';
|
|
|
29
29
|
* target shrinking across `--retry-failed` passes by leaving deterministic
|
|
30
30
|
* dead-ends alone.
|
|
31
31
|
*
|
|
32
|
+
* **Outage override**: before applying the permanent-kind exclusion, the
|
|
33
|
+
* message's `createdAt` is checked against every recorded
|
|
34
|
+
* `network_outages` window (see `is-within-outage-window.ts`). A `dns` (or
|
|
35
|
+
* any other permanent-kind) failure whose timestamp falls inside a window
|
|
36
|
+
* is treated as retryable regardless — `dns` is only a permanent,
|
|
37
|
+
* site-specific verdict when nothing else explains it; inside a confirmed
|
|
38
|
+
* operator-network outage, the same `getaddrinfo ENOTFOUND` message is
|
|
39
|
+
* evidence about the CRAWLER's connectivity, not the target site, and
|
|
40
|
+
* excluding it from retry would strand a perfectly reachable host as a
|
|
41
|
+
* false permanent failure for the rest of the archive's life. An archive
|
|
42
|
+
* with no recorded outages (`listNetworkOutages` returns `[]`) behaves
|
|
43
|
+
* exactly as before this override existed.
|
|
44
|
+
*
|
|
32
45
|
* Matching rows — internal and external alike — are demoted back to pending
|
|
33
46
|
* (`scraped = 0`) and have their stale scrape metadata cleared (the
|
|
34
47
|
* `page_meta` row is deleted outright rather than nulled column-by-column).
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { classifyErrorKind } from '../../../../classify-error-kind.js';
|
|
2
|
+
import { isWithinOutageWindow } from '../../../../is-within-outage-window.js';
|
|
2
3
|
import { PERMANENT_ERROR_KINDS } from '../../../../permanent-error-kinds.js';
|
|
3
4
|
import { dbLog } from '../../../debug.js';
|
|
4
5
|
import { getFailedPageMessages } from '../../../get-failed-page-messages.js';
|
|
6
|
+
import { listNetworkOutages } from '../../outages/list-network-outages.js';
|
|
5
7
|
/**
|
|
6
8
|
* Reset previously-attempted pages that ended in a recoverable failure so a
|
|
7
9
|
* follow-up crawl can re-fetch them from scratch.
|
|
@@ -32,6 +34,19 @@ import { getFailedPageMessages } from '../../../get-failed-page-messages.js';
|
|
|
32
34
|
* target shrinking across `--retry-failed` passes by leaving deterministic
|
|
33
35
|
* dead-ends alone.
|
|
34
36
|
*
|
|
37
|
+
* **Outage override**: before applying the permanent-kind exclusion, the
|
|
38
|
+
* message's `createdAt` is checked against every recorded
|
|
39
|
+
* `network_outages` window (see `is-within-outage-window.ts`). A `dns` (or
|
|
40
|
+
* any other permanent-kind) failure whose timestamp falls inside a window
|
|
41
|
+
* is treated as retryable regardless — `dns` is only a permanent,
|
|
42
|
+
* site-specific verdict when nothing else explains it; inside a confirmed
|
|
43
|
+
* operator-network outage, the same `getaddrinfo ENOTFOUND` message is
|
|
44
|
+
* evidence about the CRAWLER's connectivity, not the target site, and
|
|
45
|
+
* excluding it from retry would strand a perfectly reachable host as a
|
|
46
|
+
* false permanent failure for the rest of the archive's life. An archive
|
|
47
|
+
* with no recorded outages (`listNetworkOutages` returns `[]`) behaves
|
|
48
|
+
* exactly as before this override existed.
|
|
49
|
+
*
|
|
35
50
|
* Matching rows — internal and external alike — are demoted back to pending
|
|
36
51
|
* (`scraped = 0`) and have their stale scrape metadata cleared (the
|
|
37
52
|
* `page_meta` row is deleted outright rather than nulled column-by-column).
|
|
@@ -69,16 +84,29 @@ export async function resetFailedPages(knex) {
|
|
|
69
84
|
}
|
|
70
85
|
const candidateIds = candidates.map((row) => row.id);
|
|
71
86
|
const candidateUrls = candidates.map((row) => row.url);
|
|
72
|
-
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
87
|
+
// Unrelated tables (page_errors/crawl_errors vs network_outages), no data
|
|
88
|
+
// dependency between them — run concurrently instead of paying two
|
|
89
|
+
// sequential round-trips on every `--retry-failed` pass.
|
|
90
|
+
const [messages, outageWindows] = await Promise.all([
|
|
91
|
+
getFailedPageMessages(knex, candidateIds, candidateUrls),
|
|
92
|
+
listNetworkOutages(knex),
|
|
93
|
+
]);
|
|
94
|
+
// Drop candidates whose latest recorded message classifies as permanent —
|
|
95
|
+
// UNLESS that message's timestamp falls inside a recorded network outage,
|
|
96
|
+
// in which case the permanent-kind verdict is overridden (see the
|
|
97
|
+
// "Outage override" section of this function's docstring). An
|
|
98
|
+
// empty/absent message stays in the retry pool regardless — we keep
|
|
99
|
+
// retrying when we don't know it's permanent, erring on the side of
|
|
100
|
+
// investigation.
|
|
76
101
|
const retryable = candidates.filter((row) => {
|
|
77
|
-
const
|
|
78
|
-
if (message === '') {
|
|
102
|
+
const resolved = messages.get(row.id);
|
|
103
|
+
if (resolved === undefined || resolved.message === '') {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
if (!PERMANENT_ERROR_KINDS.has(classifyErrorKind(resolved.message))) {
|
|
79
107
|
return true;
|
|
80
108
|
}
|
|
81
|
-
return
|
|
109
|
+
return isWithinOutageWindow(resolved.createdAt, outageWindows);
|
|
82
110
|
});
|
|
83
111
|
const excludedCount = candidates.length - retryable.length;
|
|
84
112
|
if (excludedCount > 0) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { tryParseUrl as parseUrl } from '@d-zero/shared/parse-url';
|
|
2
2
|
import { isHtmlContentType } from '../../../../crawler/is-html-content-type.js';
|
|
3
3
|
import { eachSplitted } from '../../../../utils/array/each-splitted.js';
|
|
4
|
+
import { computeBodyHash } from '../../../body-hash/compute-body-hash.js';
|
|
4
5
|
import { dbLog } from '../../../debug.js';
|
|
5
6
|
import { deriveLineageFromParent } from '../../../derive-lineage-from-parent.js';
|
|
6
7
|
import { matchImagesToDomPaths } from '../../../populate-entity-tables/match-images-to-dom-paths.js';
|
|
@@ -141,6 +142,12 @@ async function updatePageInTransaction(trx, knex, caches, page, destUrlObject, s
|
|
|
141
142
|
// content check alone expresses the intent without a redundant term.
|
|
142
143
|
if (writeHtml && page.html.length > 0) {
|
|
143
144
|
await writePageHtmlBlob(pageId, page.html, trx);
|
|
145
|
+
// Computed from the same `page.html` written above, in the same
|
|
146
|
+
// transaction, so `page_meta.body_hash` never observably lags the
|
|
147
|
+
// snapshot it was derived from.
|
|
148
|
+
await trx('page_meta')
|
|
149
|
+
.where('page_id', pageId)
|
|
150
|
+
.update({ body_hash: computeBodyHash(page.html) });
|
|
144
151
|
}
|
|
145
152
|
else if (writeHtml &&
|
|
146
153
|
page.contentType !== null &&
|
|
@@ -155,7 +162,12 @@ async function updatePageInTransaction(trx, knex, caches, page, destUrlObject, s
|
|
|
155
162
|
// stale ref can only have been written by a snapshot-capable call
|
|
156
163
|
// (`setPage`); `setExternalPage` passes `writeHtml = false` and never
|
|
157
164
|
// sets `html`, so it has nothing to clear.
|
|
165
|
+
//
|
|
166
|
+
// `page_meta.body_hash` is cleared alongside it for the same reason —
|
|
167
|
+
// it is derived from the same HTML `page_html_ref` points at, so it
|
|
168
|
+
// must not outlive the ref it was computed from.
|
|
158
169
|
await trx('page_html_ref').where('page_id', pageId).delete();
|
|
170
|
+
await trx('page_meta').where('page_id', pageId).update({ body_hash: null });
|
|
159
171
|
}
|
|
160
172
|
// Re-scrape semantics: the same URL can be scraped more than once
|
|
161
173
|
// (e.g. `crawl --resume`, re-visits, `--append` re-promotion). Edges
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { compressPayload } from '../../_shared/compress-payload.js';
|
|
3
3
|
/**
|
|
4
4
|
* Encodes, dedups, and persists a page's HTML snapshot.
|
|
5
5
|
*
|
|
@@ -20,14 +20,14 @@ import { zstdCompressSync } from 'node:zlib';
|
|
|
20
20
|
export async function writePageHtmlBlob(pageId, html, trx) {
|
|
21
21
|
const rawBytes = Buffer.from(html, 'utf8');
|
|
22
22
|
const hash = createHash('sha256').update(rawBytes).digest();
|
|
23
|
-
const
|
|
23
|
+
const { body, codec, sizeRaw, sizeStored } = compressPayload(rawBytes);
|
|
24
24
|
await trx('page_html_blobs')
|
|
25
25
|
.insert({
|
|
26
26
|
hash,
|
|
27
|
-
body
|
|
28
|
-
codec
|
|
29
|
-
size_raw:
|
|
30
|
-
size_stored:
|
|
27
|
+
body,
|
|
28
|
+
codec,
|
|
29
|
+
size_raw: sizeRaw,
|
|
30
|
+
size_stored: sizeStored,
|
|
31
31
|
})
|
|
32
32
|
.onConflict('hash')
|
|
33
33
|
.ignore();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes raw bytes to a file at the specified path, creating parent
|
|
3
|
+
* directories if needed.
|
|
4
|
+
*
|
|
5
|
+
* Unlike {@link outputText}, the buffer is written verbatim with no UTF-8
|
|
6
|
+
* re-encoding, so callers that need byte-for-byte fidelity (e.g. archiving
|
|
7
|
+
* a source file of unknown or mixed encoding for audit purposes) are not
|
|
8
|
+
* exposed to lossy round-tripping through a JS string.
|
|
9
|
+
* @param filePath - The absolute or relative path to the file to write.
|
|
10
|
+
* @param data - The raw bytes to write.
|
|
11
|
+
*/
|
|
12
|
+
export declare function outputBinary(filePath: string, data: Buffer): Promise<void>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { mkdir } from './mkdir.js';
|
|
3
|
+
/**
|
|
4
|
+
* Writes raw bytes to a file at the specified path, creating parent
|
|
5
|
+
* directories if needed.
|
|
6
|
+
*
|
|
7
|
+
* Unlike {@link outputText}, the buffer is written verbatim with no UTF-8
|
|
8
|
+
* re-encoding, so callers that need byte-for-byte fidelity (e.g. archiving
|
|
9
|
+
* a source file of unknown or mixed encoding for audit purposes) are not
|
|
10
|
+
* exposed to lossy round-tripping through a JS string.
|
|
11
|
+
* @param filePath - The absolute or relative path to the file to write.
|
|
12
|
+
* @param data - The raw bytes to write.
|
|
13
|
+
*/
|
|
14
|
+
export async function outputBinary(filePath, data) {
|
|
15
|
+
mkdir(filePath);
|
|
16
|
+
await fs.writeFile(filePath, data);
|
|
17
|
+
}
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import type { Knex } from 'knex';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* One resolved failure message, with the timestamp it was recorded at.
|
|
4
|
+
* `createdAt` is what lets a caller (`resetFailedPages`) decide whether the
|
|
5
|
+
* failure falls inside a `network_outages` window and should be treated as
|
|
6
|
+
* retryable regardless of its classified `ErrorKind` — see
|
|
7
|
+
* `is-within-outage-window.ts`.
|
|
8
|
+
*/
|
|
9
|
+
export interface FailedPageMessage {
|
|
10
|
+
message: string;
|
|
11
|
+
/** Epoch ms the message was recorded (`page_errors.createdAt` or `crawl_errors.createdAt`). */
|
|
12
|
+
createdAt: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Bulk-resolve a raw error message (plus its timestamp) for each given page
|
|
16
|
+
* id, using only sources reachable from a {@link Knex} handle. Read order:
|
|
17
|
+
* `page_errors` (keyed by `pageId`, the most direct signal a scrape attempt
|
|
18
|
+
* recorded), then `crawl_errors` (keyed by `url`, the crawler-channel
|
|
19
|
+
* record for failures that happened before a page row was scraped).
|
|
8
20
|
*
|
|
9
21
|
* **Known limitation — pre-`crawl_errors` archives**: This helper does NOT
|
|
10
22
|
* read `error.log`. The `crawl_errors` table is created empty (by
|
|
@@ -30,8 +42,8 @@ import type { Knex } from 'knex';
|
|
|
30
42
|
* @param urls - The corresponding `pages.url` values, in the same order as
|
|
31
43
|
* `ids`. Length and indexing MUST match `ids` so the page → url join can be
|
|
32
44
|
* reconstructed without a second `pages` round-trip.
|
|
33
|
-
* @returns `Map<pageId,
|
|
34
|
-
* found in one of the consulted tables.
|
|
45
|
+
* @returns `Map<pageId, FailedPageMessage>` populated only for ids whose
|
|
46
|
+
* message was found in one of the consulted tables.
|
|
35
47
|
* @example
|
|
36
48
|
* ```ts
|
|
37
49
|
* const messages = await getFailedPageMessages(
|
|
@@ -41,4 +53,4 @@ import type { Knex } from 'knex';
|
|
|
41
53
|
* );
|
|
42
54
|
* ```
|
|
43
55
|
*/
|
|
44
|
-
export declare function getFailedPageMessages(instance: Knex, ids: readonly number[], urls: readonly string[]): Promise<Map<number,
|
|
56
|
+
export declare function getFailedPageMessages(instance: Knex, ids: readonly number[], urls: readonly string[]): Promise<Map<number, FailedPageMessage>>;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/** chunk size for `WHERE … IN (?)` SQLite parameter packing. */
|
|
2
2
|
const CHUNK_SIZE = 500;
|
|
3
3
|
/**
|
|
4
|
-
* Bulk-resolve a raw error message for each given page
|
|
5
|
-
* reachable from a {@link Knex} handle. Read order:
|
|
6
|
-
* `pageId`, the most direct signal a scrape attempt
|
|
7
|
-
* `crawl_errors` (keyed by `url`, the crawler-channel
|
|
8
|
-
* that happened before a page row was scraped).
|
|
4
|
+
* Bulk-resolve a raw error message (plus its timestamp) for each given page
|
|
5
|
+
* id, using only sources reachable from a {@link Knex} handle. Read order:
|
|
6
|
+
* `page_errors` (keyed by `pageId`, the most direct signal a scrape attempt
|
|
7
|
+
* recorded), then `crawl_errors` (keyed by `url`, the crawler-channel
|
|
8
|
+
* record for failures that happened before a page row was scraped).
|
|
9
9
|
*
|
|
10
10
|
* **Known limitation — pre-`crawl_errors` archives**: This helper does NOT
|
|
11
11
|
* read `error.log`. The `crawl_errors` table is created empty (by
|
|
@@ -31,8 +31,8 @@ const CHUNK_SIZE = 500;
|
|
|
31
31
|
* @param urls - The corresponding `pages.url` values, in the same order as
|
|
32
32
|
* `ids`. Length and indexing MUST match `ids` so the page → url join can be
|
|
33
33
|
* reconstructed without a second `pages` round-trip.
|
|
34
|
-
* @returns `Map<pageId,
|
|
35
|
-
* found in one of the consulted tables.
|
|
34
|
+
* @returns `Map<pageId, FailedPageMessage>` populated only for ids whose
|
|
35
|
+
* message was found in one of the consulted tables.
|
|
36
36
|
* @example
|
|
37
37
|
* ```ts
|
|
38
38
|
* const messages = await getFailedPageMessages(
|
|
@@ -61,7 +61,7 @@ export async function getFailedPageMessages(instance, ids, urls) {
|
|
|
61
61
|
// it as `parse-error` on the next run when the rows happen to be
|
|
62
62
|
// returned in a different order.
|
|
63
63
|
const rows = (await instance('page_errors')
|
|
64
|
-
.select('pageId', 'message')
|
|
64
|
+
.select('pageId', 'message', 'createdAt')
|
|
65
65
|
.whereIn('pageId', chunk)
|
|
66
66
|
.orderBy('id', 'asc'));
|
|
67
67
|
for (const row of rows) {
|
|
@@ -79,7 +79,10 @@ export async function getFailedPageMessages(instance, ids, urls) {
|
|
|
79
79
|
// `dns` / `tls` / `client-blocked` etc. — defeating
|
|
80
80
|
// `--retry-failed`'s permanent-kind exclusion.
|
|
81
81
|
if (row.message !== '' && !messageByPageId.has(row.pageId)) {
|
|
82
|
-
messageByPageId.set(row.pageId,
|
|
82
|
+
messageByPageId.set(row.pageId, {
|
|
83
|
+
message: row.message,
|
|
84
|
+
createdAt: row.createdAt,
|
|
85
|
+
});
|
|
83
86
|
}
|
|
84
87
|
}
|
|
85
88
|
}
|
|
@@ -110,11 +113,22 @@ export async function getFailedPageMessages(instance, ids, urls) {
|
|
|
110
113
|
for (let i = 0; i < missingUrls.length; i += CHUNK_SIZE) {
|
|
111
114
|
const chunk = missingUrls.slice(i, i + CHUNK_SIZE);
|
|
112
115
|
const rows = (await instance('crawl_errors')
|
|
113
|
-
.select('url', 'message')
|
|
116
|
+
.select('url', 'message', 'createdAt')
|
|
114
117
|
.whereIn('url', chunk));
|
|
115
118
|
for (const row of rows) {
|
|
116
|
-
if (row.url
|
|
117
|
-
|
|
119
|
+
if (row.url === null) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
// Latest-createdAt wins (fixes a previously-undefined
|
|
123
|
+
// selection among duplicate URLs — SQLite's natural scan
|
|
124
|
+
// order is implementation-defined). The most recent message
|
|
125
|
+
// is the most relevant one for both classification and outage
|
|
126
|
+
// attribution: an old NXDOMAIN followed by a network-outage
|
|
127
|
+
// blip should resolve to the outage-era message, not whichever
|
|
128
|
+
// happened to be inserted first.
|
|
129
|
+
const existing = urlToMessage.get(row.url);
|
|
130
|
+
if (existing === undefined || row.createdAt > existing.createdAt) {
|
|
131
|
+
urlToMessage.set(row.url, { message: row.message, createdAt: row.createdAt });
|
|
118
132
|
}
|
|
119
133
|
}
|
|
120
134
|
}
|
|
@@ -123,9 +137,9 @@ export async function getFailedPageMessages(instance, ids, urls) {
|
|
|
123
137
|
if (url === undefined) {
|
|
124
138
|
continue;
|
|
125
139
|
}
|
|
126
|
-
const
|
|
127
|
-
if (
|
|
128
|
-
messageByPageId.set(id,
|
|
140
|
+
const resolved = urlToMessage.get(url);
|
|
141
|
+
if (resolved !== undefined) {
|
|
142
|
+
messageByPageId.set(id, resolved);
|
|
129
143
|
}
|
|
130
144
|
}
|
|
131
145
|
return messageByPageId;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Knex } from 'knex';
|
|
2
|
+
/**
|
|
3
|
+
* Adds the `content_items.alias_of_id` column to archives created before
|
|
4
|
+
* this feature, then ensures its index exists.
|
|
5
|
+
*
|
|
6
|
+
* `content_items` is provisioned via a bare `CREATE TABLE IF NOT EXISTS` in
|
|
7
|
+
* {@link import('./create-entity-tables.js').createEntityTables}, which
|
|
8
|
+
* self-heals a *missing table* on every `initSchema` call but is a no-op
|
|
9
|
+
* against an *existing* table — adding a column to the DDL string never
|
|
10
|
+
* reaches an archive whose `content_items` predates this change. This
|
|
11
|
+
* mirrors {@link import('./migrate-page-meta-body-hash.js').migratePageMetaBodyHash}'s
|
|
12
|
+
* catch-up: a `hasColumn`-guarded `ALTER TABLE` for the one column
|
|
13
|
+
* `CREATE TABLE IF NOT EXISTS` cannot retrofit.
|
|
14
|
+
*
|
|
15
|
+
* Uses a raw `ALTER TABLE` (not the knex schema builder) so the retrofitted
|
|
16
|
+
* column's `REFERENCES content_items(id) DEFERRABLE INITIALLY DEFERRED`
|
|
17
|
+
* constraint matches the fresh-archive DDL bit-for-bit — the same
|
|
18
|
+
* self-referencing shape as `redirect_dest_id` (see
|
|
19
|
+
* `create-entity-tables.ts`'s JSDoc on why that FK must be deferred: a
|
|
20
|
+
* lower-id row can reference a higher-id row within the same write).
|
|
21
|
+
*
|
|
22
|
+
* The index is created here — unconditionally, after the column-add guard,
|
|
23
|
+
* not inside `createEntityTables`'s DDL — for both a fresh archive (where
|
|
24
|
+
* `alias_of_id` already exists from the DDL, so only the index still needs
|
|
25
|
+
* creating) and a legacy archive (where the column is added just above,
|
|
26
|
+
* then the index follows in the same call). `createEntityTables` runs
|
|
27
|
+
* unconditionally on every archive open, including legacy archives that
|
|
28
|
+
* still lack `alias_of_id` at that point; an unconditional
|
|
29
|
+
* `CREATE INDEX ... alias_of_id` there would fail with `no such column`
|
|
30
|
+
* before this migration ever runs. This function is the one place
|
|
31
|
+
* guaranteed to run only after the column is confirmed present, for both
|
|
32
|
+
* archive kinds.
|
|
33
|
+
*
|
|
34
|
+
* Only adds the column and its index — it does not compute values for
|
|
35
|
+
* existing rows (they stay `NULL`). That computation runs separately, from
|
|
36
|
+
* `backfillAliasOfId` during a viewer-read-model build, since it requires
|
|
37
|
+
* comparing every page's title and (for the trailing-slash tier) its
|
|
38
|
+
* `body_hash` against every other page.
|
|
39
|
+
*
|
|
40
|
+
* Idempotent: adding the column is a no-op once it exists (the index
|
|
41
|
+
* creation always runs, but `IF NOT EXISTS` makes repeat runs a no-op too).
|
|
42
|
+
* Guards on `content_items`'s existence defensively, though by the time this
|
|
43
|
+
* runs (after `initSchema`, itself after `assertCompatibleVersion` rejects
|
|
44
|
+
* pre-0.13 archives) the table is always present.
|
|
45
|
+
* @param instance - The Knex query builder instance connected to the database.
|
|
46
|
+
*/
|
|
47
|
+
export declare function migrateContentItemsAliasOfId(instance: Knex): Promise<void>;
|