@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,123 @@
|
|
|
1
|
+
import { upsertTextRefs } from '../../populate-entity-tables/upsert-text-refs.js';
|
|
2
|
+
import { resolveRedirectChain } from '../../resolve-redirect-chain.js';
|
|
3
|
+
import { clearWriteRefCaches } from '../_shared/clear-write-ref-caches.js';
|
|
4
|
+
import { resolveContentItemId } from '../_shared/resolve-content-item-id.js';
|
|
5
|
+
import { upsertJsonRef } from '../_shared/upsert-json-ref.js';
|
|
6
|
+
import { upsertUrlRef } from '../_shared/upsert-url-ref.js';
|
|
7
|
+
import { computeConsoleLogHash } from './compute-console-log-hash.js';
|
|
8
|
+
import { stringifyConsoleLogArgs } from './stringify-console-log-args.js';
|
|
9
|
+
import { upsertConsoleLogItem } from './upsert-console-log-item.js';
|
|
10
|
+
/** Rows sent per `INSERT INTO page_console_logs ... VALUES (...)` statement. */
|
|
11
|
+
const INSERT_CHUNK_SIZE = 500;
|
|
12
|
+
/**
|
|
13
|
+
* Replaces one page's `page_console_logs` rows wholesale with a freshly
|
|
14
|
+
* captured set of console messages / page errors (issue #228).
|
|
15
|
+
*
|
|
16
|
+
* Scoped-Replace, the same pattern as `replaceAnchorEdges` /
|
|
17
|
+
* `replaceImageItems`: the page's existing rows are deleted and the new
|
|
18
|
+
* set is inserted in the same transaction, so a re-scrape (`--append` /
|
|
19
|
+
* `--retry-failed` / re-render) never accumulates duplicate occurrences.
|
|
20
|
+
* Unlike those two, there is no non-empty guard on the DELETE here — the
|
|
21
|
+
* caller (`Crawler#handleConsoleLogs`) already skips emitting the event
|
|
22
|
+
* entirely when `entries` is empty, which is what keeps a degraded
|
|
23
|
+
* re-scrape from wiping out a prior good capture.
|
|
24
|
+
*
|
|
25
|
+
* `pageUrl` is the originally-requested URL (matching `updatePage`'s
|
|
26
|
+
* `page.url.withoutHashAndAuth` contract), not necessarily the page that
|
|
27
|
+
* ends up holding the content: `resolveRedirectChain` derives the same
|
|
28
|
+
* redirect destination `updatePage` writes content under, so console logs
|
|
29
|
+
* attach to the row that actually carries the page's `page_meta`, not to
|
|
30
|
+
* an empty placeholder for the pre-redirect URL.
|
|
31
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
32
|
+
* @param caches - The connection's write-side id caches.
|
|
33
|
+
* @param pageUrl - The originally-requested URL, normalised
|
|
34
|
+
* (`withoutHashAndAuth` form).
|
|
35
|
+
* @param redirectPaths - The redirect chain hops captured during fetch, in
|
|
36
|
+
* order (empty when the page was not redirected).
|
|
37
|
+
* @param entries - The console log entries to persist. Must be non-empty —
|
|
38
|
+
* callers should skip calling this function entirely for an empty list.
|
|
39
|
+
* @example
|
|
40
|
+
* await replaceConsoleLogs(knex, caches, page.url.withoutHashAndAuth, page.redirectPaths, entries);
|
|
41
|
+
*/
|
|
42
|
+
export async function replaceConsoleLogs(knex, caches, pageUrl, redirectPaths, entries) {
|
|
43
|
+
const { destUrl } = resolveRedirectChain(pageUrl, redirectPaths);
|
|
44
|
+
try {
|
|
45
|
+
await knex.transaction(async (trx) => {
|
|
46
|
+
const pageId = await resolveContentItemId(trx, caches, destUrl);
|
|
47
|
+
await trx('page_console_logs').where('pageId', pageId).delete();
|
|
48
|
+
const texts = new Set();
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
// `text_refs` never stores the empty string (its upsert
|
|
51
|
+
// treats `''` as "nothing to dedupe" and skips it) — a
|
|
52
|
+
// `console.log()` call with zero arguments reports `text:
|
|
53
|
+
// ''`, so that entry resolves to `textId: null` below
|
|
54
|
+
// instead of looking it up.
|
|
55
|
+
if (entry.text !== '') {
|
|
56
|
+
texts.add(entry.text);
|
|
57
|
+
}
|
|
58
|
+
if (entry.stack) {
|
|
59
|
+
texts.add(entry.stack);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const textIds = await upsertTextRefs(trx, texts);
|
|
63
|
+
const rows = [];
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
let textId = null;
|
|
66
|
+
if (entry.text !== '') {
|
|
67
|
+
const resolved = textIds.get(entry.text);
|
|
68
|
+
if (resolved === undefined) {
|
|
69
|
+
throw new Error(`replaceConsoleLogs: text_refs id not resolved for "${entry.text}"`);
|
|
70
|
+
}
|
|
71
|
+
textId = resolved;
|
|
72
|
+
}
|
|
73
|
+
const stackTextId = entry.stack ? (textIds.get(entry.stack) ?? null) : null;
|
|
74
|
+
const argsJson = stringifyConsoleLogArgs(entry.args);
|
|
75
|
+
const argsJsonId = argsJson === null ? null : await upsertJsonRef(trx, caches, argsJson);
|
|
76
|
+
const locUrlId = entry.location?.url === undefined
|
|
77
|
+
? null
|
|
78
|
+
: await upsertUrlRef(trx, caches, entry.location.url);
|
|
79
|
+
const consoleLogId = await upsertConsoleLogItem(trx, caches, {
|
|
80
|
+
hash: computeConsoleLogHash({
|
|
81
|
+
type: entry.type,
|
|
82
|
+
text: entry.text,
|
|
83
|
+
argsJson,
|
|
84
|
+
location: entry.location,
|
|
85
|
+
stack: entry.stack,
|
|
86
|
+
}),
|
|
87
|
+
type: entry.type,
|
|
88
|
+
textId,
|
|
89
|
+
argsJsonId,
|
|
90
|
+
locUrlId,
|
|
91
|
+
locLine: entry.location?.lineNumber ?? null,
|
|
92
|
+
locColumn: entry.location?.columnNumber ?? null,
|
|
93
|
+
stackTextId,
|
|
94
|
+
});
|
|
95
|
+
rows.push({ pageId, consoleLogId, ts: entry.ts });
|
|
96
|
+
}
|
|
97
|
+
for (let index = 0; index < rows.length; index += INSERT_CHUNK_SIZE) {
|
|
98
|
+
await trx('page_console_logs').insert(rows.slice(index, index + INSERT_CHUNK_SIZE));
|
|
99
|
+
}
|
|
100
|
+
// Denormalised onto `page_meta` (the same write-once-at-scrape-time
|
|
101
|
+
// pattern as `tag_count` / `jsonld_count`) so the Pages list can
|
|
102
|
+
// display and sort by it without a live JOIN + COUNT per row.
|
|
103
|
+
// Computed from `entries` directly rather than re-querying the rows
|
|
104
|
+
// just inserted above — the data is already in hand. A `page_meta`
|
|
105
|
+
// row may not exist yet for a `'skipped'` / `'error'` scrape; the
|
|
106
|
+
// UPDATE then simply affects zero rows rather than throwing.
|
|
107
|
+
const errorCount = entries.filter((entry) => entry.type === 'error' || entry.type === 'pageerror').length;
|
|
108
|
+
await trx('page_meta').where('page_id', pageId).update({
|
|
109
|
+
console_error_count: errorCount,
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
// A rolled-back transaction can leave ids cached that no longer
|
|
115
|
+
// correspond to any row (AUTOINCREMENT never rewinds) — same
|
|
116
|
+
// cache-poisoning hazard `updatePage` / `recordRedirect` guard
|
|
117
|
+
// against. A full clear (not a partial one) is required because
|
|
118
|
+
// this function shares `caches` with every other write path on
|
|
119
|
+
// the same connection.
|
|
120
|
+
clearWriteRefCaches(caches);
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serializes a console call's argument array to a JSON string, or `null`
|
|
3
|
+
* when there is nothing worth storing.
|
|
4
|
+
*
|
|
5
|
+
* Two cases collapse to `null` rather than an empty/degenerate string:
|
|
6
|
+
* an empty `args` array (most commonly a `"pageerror"` entry, which
|
|
7
|
+
* beholder always reports with `args: []`) and a `JSON.stringify` failure
|
|
8
|
+
* (e.g. a circular reference in a logged object) — both mean "no
|
|
9
|
+
* additional structured payload beyond `text`", not "a malformed one".
|
|
10
|
+
* @param args - The `ConsoleLogEntry.args` array to serialize.
|
|
11
|
+
* @returns The JSON string, or `null` when empty or unserializable.
|
|
12
|
+
* @example
|
|
13
|
+
* stringifyConsoleLogArgs(['a', 1]); // '["a",1]'
|
|
14
|
+
* stringifyConsoleLogArgs([]); // null
|
|
15
|
+
*/
|
|
16
|
+
export declare function stringifyConsoleLogArgs(args: readonly unknown[]): string | null;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serializes a console call's argument array to a JSON string, or `null`
|
|
3
|
+
* when there is nothing worth storing.
|
|
4
|
+
*
|
|
5
|
+
* Two cases collapse to `null` rather than an empty/degenerate string:
|
|
6
|
+
* an empty `args` array (most commonly a `"pageerror"` entry, which
|
|
7
|
+
* beholder always reports with `args: []`) and a `JSON.stringify` failure
|
|
8
|
+
* (e.g. a circular reference in a logged object) — both mean "no
|
|
9
|
+
* additional structured payload beyond `text`", not "a malformed one".
|
|
10
|
+
* @param args - The `ConsoleLogEntry.args` array to serialize.
|
|
11
|
+
* @returns The JSON string, or `null` when empty or unserializable.
|
|
12
|
+
* @example
|
|
13
|
+
* stringifyConsoleLogArgs(['a', 1]); // '["a",1]'
|
|
14
|
+
* stringifyConsoleLogArgs([]); // null
|
|
15
|
+
*/
|
|
16
|
+
export function stringifyConsoleLogArgs(args) {
|
|
17
|
+
if (args.length === 0) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
return JSON.stringify(args);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The content fields of a `ConsoleLogEntry` that determine its identity in
|
|
3
|
+
* `console_log_items` — everything except `pageUrl` / `ts`, which are
|
|
4
|
+
* per-occurrence rather than per-content (see `page_console_logs`).
|
|
5
|
+
*/
|
|
6
|
+
export interface ConsoleLogContent {
|
|
7
|
+
type: string;
|
|
8
|
+
text: string;
|
|
9
|
+
/**
|
|
10
|
+
* Pre-stringified `args` (via `stringifyConsoleLogArgs`), or `null` for
|
|
11
|
+
* an empty/unserializable args array. Callers compute this once and
|
|
12
|
+
* reuse it both for hashing here and for the `json_refs` storage
|
|
13
|
+
* decision, rather than calling `stringifyConsoleLogArgs` on the same
|
|
14
|
+
* `args` array twice.
|
|
15
|
+
*/
|
|
16
|
+
argsJson: string | null;
|
|
17
|
+
location?: {
|
|
18
|
+
url?: string;
|
|
19
|
+
lineNumber?: number;
|
|
20
|
+
columnNumber?: number;
|
|
21
|
+
};
|
|
22
|
+
stack?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The resolved ref ids and scalar fields that make up one
|
|
26
|
+
* `console_log_items` row, computed by the caller before the upsert runs.
|
|
27
|
+
*/
|
|
28
|
+
export interface ConsoleLogItemRow {
|
|
29
|
+
hash: Buffer;
|
|
30
|
+
type: string;
|
|
31
|
+
/** `null` when `text` is the empty string — `text_refs` never stores `''`. */
|
|
32
|
+
textId: number | null;
|
|
33
|
+
argsJsonId: number | null;
|
|
34
|
+
locUrlId: number | null;
|
|
35
|
+
locLine: number | null;
|
|
36
|
+
locColumn: number | null;
|
|
37
|
+
stackTextId: number | null;
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ConsoleLogItemRow } from './types.js';
|
|
2
|
+
import type { WriteRefCaches } from '../_shared/types.js';
|
|
3
|
+
import type { Knex } from 'knex';
|
|
4
|
+
/**
|
|
5
|
+
* Resolves the `console_log_items.id` for one console log entry's content,
|
|
6
|
+
* inserting the dictionary row when the hash is not yet known.
|
|
7
|
+
*
|
|
8
|
+
* Same no-op-update-for-`RETURNING` idiom as `upsertUrlRef` / `upsertJsonRef`:
|
|
9
|
+
* `INSERT ... ON CONFLICT(hash) DO UPDATE SET hash = hash RETURNING id`
|
|
10
|
+
* yields the existing row's id on a hash collision without a separate
|
|
11
|
+
* SELECT round trip, and (unlike `INSERT OR IGNORE ... RETURNING`) still
|
|
12
|
+
* returns a row when the insert itself is the no-op.
|
|
13
|
+
* @param qb - Knex instance or transaction connected to the archive DB.
|
|
14
|
+
* @param caches - The connection's write-side id caches; mutated in place.
|
|
15
|
+
* @param row - The content hash plus every resolved ref id / scalar column.
|
|
16
|
+
* @returns The `console_log_items.id` of the existing or newly inserted row.
|
|
17
|
+
* @example
|
|
18
|
+
* const id = await upsertConsoleLogItem(trx, caches, {
|
|
19
|
+
* hash: computeConsoleLogHash(entry),
|
|
20
|
+
* type: entry.type,
|
|
21
|
+
* textId,
|
|
22
|
+
* argsJsonId: null,
|
|
23
|
+
* locUrlId: null,
|
|
24
|
+
* locLine: null,
|
|
25
|
+
* locColumn: null,
|
|
26
|
+
* stackTextId: null,
|
|
27
|
+
* });
|
|
28
|
+
*/
|
|
29
|
+
export declare function upsertConsoleLogItem(qb: Knex | Knex.Transaction, caches: WriteRefCaches, row: ConsoleLogItemRow): Promise<number>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves the `console_log_items.id` for one console log entry's content,
|
|
3
|
+
* inserting the dictionary row when the hash is not yet known.
|
|
4
|
+
*
|
|
5
|
+
* Same no-op-update-for-`RETURNING` idiom as `upsertUrlRef` / `upsertJsonRef`:
|
|
6
|
+
* `INSERT ... ON CONFLICT(hash) DO UPDATE SET hash = hash RETURNING id`
|
|
7
|
+
* yields the existing row's id on a hash collision without a separate
|
|
8
|
+
* SELECT round trip, and (unlike `INSERT OR IGNORE ... RETURNING`) still
|
|
9
|
+
* returns a row when the insert itself is the no-op.
|
|
10
|
+
* @param qb - Knex instance or transaction connected to the archive DB.
|
|
11
|
+
* @param caches - The connection's write-side id caches; mutated in place.
|
|
12
|
+
* @param row - The content hash plus every resolved ref id / scalar column.
|
|
13
|
+
* @returns The `console_log_items.id` of the existing or newly inserted row.
|
|
14
|
+
* @example
|
|
15
|
+
* const id = await upsertConsoleLogItem(trx, caches, {
|
|
16
|
+
* hash: computeConsoleLogHash(entry),
|
|
17
|
+
* type: entry.type,
|
|
18
|
+
* textId,
|
|
19
|
+
* argsJsonId: null,
|
|
20
|
+
* locUrlId: null,
|
|
21
|
+
* locLine: null,
|
|
22
|
+
* locColumn: null,
|
|
23
|
+
* stackTextId: null,
|
|
24
|
+
* });
|
|
25
|
+
*/
|
|
26
|
+
export async function upsertConsoleLogItem(qb, caches, row) {
|
|
27
|
+
const hex = row.hash.toString('hex');
|
|
28
|
+
const cached = caches.consoleLogIds.get(hex);
|
|
29
|
+
if (cached !== undefined) {
|
|
30
|
+
return cached;
|
|
31
|
+
}
|
|
32
|
+
const rows = await qb.raw(`INSERT INTO console_log_items
|
|
33
|
+
(hash, type, text_id, args_json_id, loc_url_id, loc_line, loc_column, stack_text_id)
|
|
34
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
35
|
+
ON CONFLICT(hash) DO UPDATE SET hash = hash
|
|
36
|
+
RETURNING id`, [
|
|
37
|
+
row.hash,
|
|
38
|
+
row.type,
|
|
39
|
+
row.textId,
|
|
40
|
+
row.argsJsonId,
|
|
41
|
+
row.locUrlId,
|
|
42
|
+
row.locLine,
|
|
43
|
+
row.locColumn,
|
|
44
|
+
row.stackTextId,
|
|
45
|
+
]);
|
|
46
|
+
const first = rows[0];
|
|
47
|
+
if (first === undefined) {
|
|
48
|
+
throw new Error('upsertConsoleLogItem: RETURNING yielded no row');
|
|
49
|
+
}
|
|
50
|
+
caches.consoleLogIds.set(hex, first.id);
|
|
51
|
+
return first.id;
|
|
52
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Knex } from 'knex';
|
|
2
|
+
/**
|
|
3
|
+
* Adds `rejectedCount` onto the `rejected_count` of the `dedupe_cap_events`
|
|
4
|
+
* row for `shapeKey`, treating a still-`NULL` count as `0`. Unlike
|
|
5
|
+
* `finalize-dedupe-cap-event.ts` (which stamps a session's own newly-capped
|
|
6
|
+
* shape exactly once, guarded by `whereNull`), this targets a shape that
|
|
7
|
+
* capped in an EARLIER session and was preloaded into `DedupeCapTracker`'s
|
|
8
|
+
* sticky set (see `DedupeCapTracker`'s constructor JSDoc) — gate rejections
|
|
9
|
+
* for such a shape still occur in the current session, but no `dedupeCap`
|
|
10
|
+
* event (and thus no new row) is ever emitted for it, since the tracker
|
|
11
|
+
* short-circuits on an already-sticky shape before `observe` runs. Matches
|
|
12
|
+
* by `shape_key` rather than `id` because the caller (`CrawlerOrchestrator`)
|
|
13
|
+
* only has the shape key for a preloaded-sticky shape, never its row id.
|
|
14
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
15
|
+
* @param shapeKey - The capped shape whose rejection count to accumulate.
|
|
16
|
+
* @param rejectedCount - Additional anchors rejected for this shape in the current session.
|
|
17
|
+
*/
|
|
18
|
+
export declare function accumulateDedupeCapRejectedCount(knex: Knex, shapeKey: string, rejectedCount: number): Promise<void>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adds `rejectedCount` onto the `rejected_count` of the `dedupe_cap_events`
|
|
3
|
+
* row for `shapeKey`, treating a still-`NULL` count as `0`. Unlike
|
|
4
|
+
* `finalize-dedupe-cap-event.ts` (which stamps a session's own newly-capped
|
|
5
|
+
* shape exactly once, guarded by `whereNull`), this targets a shape that
|
|
6
|
+
* capped in an EARLIER session and was preloaded into `DedupeCapTracker`'s
|
|
7
|
+
* sticky set (see `DedupeCapTracker`'s constructor JSDoc) — gate rejections
|
|
8
|
+
* for such a shape still occur in the current session, but no `dedupeCap`
|
|
9
|
+
* event (and thus no new row) is ever emitted for it, since the tracker
|
|
10
|
+
* short-circuits on an already-sticky shape before `observe` runs. Matches
|
|
11
|
+
* by `shape_key` rather than `id` because the caller (`CrawlerOrchestrator`)
|
|
12
|
+
* only has the shape key for a preloaded-sticky shape, never its row id.
|
|
13
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
14
|
+
* @param shapeKey - The capped shape whose rejection count to accumulate.
|
|
15
|
+
* @param rejectedCount - Additional anchors rejected for this shape in the current session.
|
|
16
|
+
*/
|
|
17
|
+
export async function accumulateDedupeCapRejectedCount(knex, shapeKey, rejectedCount) {
|
|
18
|
+
await knex('dedupe_cap_events')
|
|
19
|
+
.where({ shape_key: shapeKey })
|
|
20
|
+
.update({
|
|
21
|
+
rejected_count: knex.raw('COALESCE(rejected_count, 0) + ?', [rejectedCount]),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Knex } from 'knex';
|
|
2
|
+
/**
|
|
3
|
+
* Finalizes a `dedupe_cap_events` row by stamping `rejected_count` — but
|
|
4
|
+
* ONLY if it is still unset. The `whereNull('rejected_count')` guard makes
|
|
5
|
+
* this idempotent, mirroring `close-network-outage.ts`'s `ended_at` guard: a
|
|
6
|
+
* second call matches zero rows and is a silent no-op rather than
|
|
7
|
+
* overwriting an already-finalized count.
|
|
8
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
9
|
+
* @param id - The `dedupe_cap_events.id` to finalize.
|
|
10
|
+
* @param rejectedCount - Number of anchors rejected for this shape after it capped.
|
|
11
|
+
*/
|
|
12
|
+
export declare function finalizeDedupeCapEvent(knex: Knex, id: number, rejectedCount: number): Promise<void>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finalizes a `dedupe_cap_events` row by stamping `rejected_count` — but
|
|
3
|
+
* ONLY if it is still unset. The `whereNull('rejected_count')` guard makes
|
|
4
|
+
* this idempotent, mirroring `close-network-outage.ts`'s `ended_at` guard: a
|
|
5
|
+
* second call matches zero rows and is a silent no-op rather than
|
|
6
|
+
* overwriting an already-finalized count.
|
|
7
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
8
|
+
* @param id - The `dedupe_cap_events.id` to finalize.
|
|
9
|
+
* @param rejectedCount - Number of anchors rejected for this shape after it capped.
|
|
10
|
+
*/
|
|
11
|
+
export async function finalizeDedupeCapEvent(knex, id, rejectedCount) {
|
|
12
|
+
await knex('dedupe_cap_events').where({ id }).whereNull('rejected_count').update({
|
|
13
|
+
rejected_count: rejectedCount,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { InsertDedupeCapEventParams } from '../../types.js';
|
|
2
|
+
import type { Knex } from 'knex';
|
|
3
|
+
/**
|
|
4
|
+
* Appends one row to the `dedupe_cap_events` journal, with `rejected_count`
|
|
5
|
+
* left `NULL` — the row starts life without a finalized rejection count.
|
|
6
|
+
*
|
|
7
|
+
* Called the instant `DedupeCapTracker#observe` confirms a URL shape as a
|
|
8
|
+
* same-cluster trap (the `dedupeCap` event). See
|
|
9
|
+
* `finalize-dedupe-cap-event.ts` for how `rejected_count` is later set.
|
|
10
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
11
|
+
* @param params - The newly-capped shape's fields to record.
|
|
12
|
+
* @returns The autoincremented `id` of the newly-inserted row.
|
|
13
|
+
*/
|
|
14
|
+
export declare function insertDedupeCapEvent(knex: Knex, params: InsertDedupeCapEventParams): Promise<number>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Appends one row to the `dedupe_cap_events` journal, with `rejected_count`
|
|
3
|
+
* left `NULL` — the row starts life without a finalized rejection count.
|
|
4
|
+
*
|
|
5
|
+
* Called the instant `DedupeCapTracker#observe` confirms a URL shape as a
|
|
6
|
+
* same-cluster trap (the `dedupeCap` event). See
|
|
7
|
+
* `finalize-dedupe-cap-event.ts` for how `rejected_count` is later set.
|
|
8
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
9
|
+
* @param params - The newly-capped shape's fields to record.
|
|
10
|
+
* @returns The autoincremented `id` of the newly-inserted row.
|
|
11
|
+
*/
|
|
12
|
+
export async function insertDedupeCapEvent(knex, params) {
|
|
13
|
+
const inserted = await knex
|
|
14
|
+
.from('dedupe_cap_events')
|
|
15
|
+
.insert({
|
|
16
|
+
shape_key: params.shapeKey,
|
|
17
|
+
sample_url: params.sampleUrl,
|
|
18
|
+
body_hash: params.bodyHash,
|
|
19
|
+
effective_threshold: params.effectiveThreshold,
|
|
20
|
+
observed_count: params.observedCount,
|
|
21
|
+
detected_at: params.detectedAt,
|
|
22
|
+
rejected_count: null,
|
|
23
|
+
})
|
|
24
|
+
.returning('id');
|
|
25
|
+
const id = inserted[0]?.id;
|
|
26
|
+
if (typeof id !== 'number') {
|
|
27
|
+
throw new TypeError('insertDedupeCapEvent: INSERT returned no row id');
|
|
28
|
+
}
|
|
29
|
+
return id;
|
|
30
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Knex } from 'knex';
|
|
2
|
+
/**
|
|
3
|
+
* Every distinct `dedupe_cap_events.shape_key` recorded in this archive —
|
|
4
|
+
* used by `CrawlerOrchestrator` to preload `DedupeCapTracker`'s sticky set
|
|
5
|
+
* on `--resume` / `--append` / `--retry-failed` / `--inventory`, so a trap
|
|
6
|
+
* this crawl already paid the cost of discovering once is not re-admitted
|
|
7
|
+
* in a later session. Fresh (non-resuming) crawls do not call this — there
|
|
8
|
+
* is no archive history to seed from.
|
|
9
|
+
*
|
|
10
|
+
* Unlike `listDnsBurnedHostCandidates`, no additional exclusion logic is
|
|
11
|
+
* needed: once `DedupeCapTracker` confirms a shape as a trap, it stays
|
|
12
|
+
* confirmed — there is no equivalent of "the host might have recovered
|
|
13
|
+
* since".
|
|
14
|
+
*
|
|
15
|
+
* Returns `[]` on legacy archives that pre-date the `dedupe_cap_events`
|
|
16
|
+
* table (self-healed on next writer open, so this is never a permanent
|
|
17
|
+
* state) or that have recorded no capped shapes.
|
|
18
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
19
|
+
* @returns Distinct shape keys already confirmed capped.
|
|
20
|
+
*/
|
|
21
|
+
export declare function listDedupeCapShapeKeys(knex: Knex): Promise<string[]>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every distinct `dedupe_cap_events.shape_key` recorded in this archive —
|
|
3
|
+
* used by `CrawlerOrchestrator` to preload `DedupeCapTracker`'s sticky set
|
|
4
|
+
* on `--resume` / `--append` / `--retry-failed` / `--inventory`, so a trap
|
|
5
|
+
* this crawl already paid the cost of discovering once is not re-admitted
|
|
6
|
+
* in a later session. Fresh (non-resuming) crawls do not call this — there
|
|
7
|
+
* is no archive history to seed from.
|
|
8
|
+
*
|
|
9
|
+
* Unlike `listDnsBurnedHostCandidates`, no additional exclusion logic is
|
|
10
|
+
* needed: once `DedupeCapTracker` confirms a shape as a trap, it stays
|
|
11
|
+
* confirmed — there is no equivalent of "the host might have recovered
|
|
12
|
+
* since".
|
|
13
|
+
*
|
|
14
|
+
* Returns `[]` on legacy archives that pre-date the `dedupe_cap_events`
|
|
15
|
+
* table (self-healed on next writer open, so this is never a permanent
|
|
16
|
+
* state) or that have recorded no capped shapes.
|
|
17
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
18
|
+
* @returns Distinct shape keys already confirmed capped.
|
|
19
|
+
*/
|
|
20
|
+
export async function listDedupeCapShapeKeys(knex) {
|
|
21
|
+
const hasTable = await knex.schema.hasTable('dedupe_cap_events');
|
|
22
|
+
if (!hasTable) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
const rows = (await knex('dedupe_cap_events').distinct('shape_key'));
|
|
26
|
+
return rows.map((row) => row.shape_key);
|
|
27
|
+
}
|
|
@@ -16,6 +16,17 @@ import type { Knex } from 'knex';
|
|
|
16
16
|
*
|
|
17
17
|
* Returns `[]` on legacy archives that pre-date the `crawl_errors`
|
|
18
18
|
* table — the `hasTable` guard keeps the call non-destructive.
|
|
19
|
+
*
|
|
20
|
+
* **Outage exclusion**: a host whose latest DNS error's `createdAt` falls
|
|
21
|
+
* inside a recorded `network_outages` window is dropped from the result
|
|
22
|
+
* regardless of the other checks. A host with no prior success this
|
|
23
|
+
* archive has ever seen (the case this whole exclusion-bag scheme cannot
|
|
24
|
+
* otherwise catch) would, without this check, get preload-seeded into
|
|
25
|
+
* `dnsBurnedHostCache` and short-circuit on EVERY subsequent session
|
|
26
|
+
* forever — the exact "damage 3" this function's caller
|
|
27
|
+
* (`#preloadDnsBurnedHostCache`) exists to prevent for hosts genuinely
|
|
28
|
+
* proven dead, not to inflict on hosts merely unlucky enough to be first
|
|
29
|
+
* contacted during an operator-side network blip.
|
|
19
30
|
* @param knex - Knex query builder connected to the archive DB.
|
|
20
31
|
* @returns Lower-cased hostnames safe to short-circuit.
|
|
21
32
|
*/
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { classifyErrorKind } from '../../../classify-error-kind.js';
|
|
2
|
+
import { isWithinOutageWindow } from '../../../is-within-outage-window.js';
|
|
3
|
+
import { listNetworkOutages } from '../outages/list-network-outages.js';
|
|
2
4
|
/**
|
|
3
5
|
* Hostnames whose `crawl_errors` history is consistently DNS failures and
|
|
4
6
|
* for which no recent 2xx-3xx page or resource is recorded — i.e. hosts
|
|
@@ -16,6 +18,17 @@ import { classifyErrorKind } from '../../../classify-error-kind.js';
|
|
|
16
18
|
*
|
|
17
19
|
* Returns `[]` on legacy archives that pre-date the `crawl_errors`
|
|
18
20
|
* table — the `hasTable` guard keeps the call non-destructive.
|
|
21
|
+
*
|
|
22
|
+
* **Outage exclusion**: a host whose latest DNS error's `createdAt` falls
|
|
23
|
+
* inside a recorded `network_outages` window is dropped from the result
|
|
24
|
+
* regardless of the other checks. A host with no prior success this
|
|
25
|
+
* archive has ever seen (the case this whole exclusion-bag scheme cannot
|
|
26
|
+
* otherwise catch) would, without this check, get preload-seeded into
|
|
27
|
+
* `dnsBurnedHostCache` and short-circuit on EVERY subsequent session
|
|
28
|
+
* forever — the exact "damage 3" this function's caller
|
|
29
|
+
* (`#preloadDnsBurnedHostCache`) exists to prevent for hosts genuinely
|
|
30
|
+
* proven dead, not to inflict on hosts merely unlucky enough to be first
|
|
31
|
+
* contacted during an operator-side network blip.
|
|
19
32
|
* @param knex - Knex query builder connected to the archive DB.
|
|
20
33
|
* @returns Lower-cased hostnames safe to short-circuit.
|
|
21
34
|
*/
|
|
@@ -118,11 +131,12 @@ export async function listDnsBurnedHostCandidates(knex) {
|
|
|
118
131
|
}
|
|
119
132
|
resourceOkHosts.add(host);
|
|
120
133
|
}
|
|
134
|
+
const outageWindows = await listNetworkOutages(knex);
|
|
121
135
|
// A candidate host is burned only if neither pages nor resources hold a
|
|
122
|
-
// 2xx-3xx for it,
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
136
|
+
// 2xx-3xx for it, its latest 2xx page (if any) is not newer than the
|
|
137
|
+
// latest DNS error (guards against re-burning a host that recovered
|
|
138
|
+
// between the last DNS failure and the most recent crawl), AND that
|
|
139
|
+
// latest DNS error did not happen during a recorded network outage.
|
|
126
140
|
const burned = [];
|
|
127
141
|
for (const [host, latestErrorAt] of candidateLatestErrorAt) {
|
|
128
142
|
if (pageOkHosts.has(host)) {
|
|
@@ -135,6 +149,9 @@ export async function listDnsBurnedHostCandidates(knex) {
|
|
|
135
149
|
if (typeof latestOkAt === 'number' && latestOkAt > latestErrorAt) {
|
|
136
150
|
continue;
|
|
137
151
|
}
|
|
152
|
+
if (isWithinOutageWindow(latestErrorAt, outageWindows)) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
138
155
|
burned.push(host);
|
|
139
156
|
}
|
|
140
157
|
return burned;
|
|
@@ -27,6 +27,7 @@ export async function recordInventoryRun(knex, meta) {
|
|
|
27
27
|
new_pages: meta.new_pages ?? null,
|
|
28
28
|
new_resources: meta.new_resources ?? null,
|
|
29
29
|
scope_skipped: meta.scope_skipped ?? null,
|
|
30
|
+
invalid_skipped: meta.invalid_skipped ?? null,
|
|
30
31
|
notes: meta.notes ?? null,
|
|
31
32
|
})
|
|
32
33
|
.returning('id');
|
|
@@ -2,7 +2,7 @@ import type { Knex } from 'knex';
|
|
|
2
2
|
/**
|
|
3
3
|
* Initializes the database schema if tables do not exist, then runs the
|
|
4
4
|
* remaining lightweight migrations (`info.roots`, `info.mainContentSelector`,
|
|
5
|
-
* `page_meta.main_content_
|
|
5
|
+
* `page_meta.main_content_*`, `inventory_runs.invalid_skipped`).
|
|
6
6
|
*
|
|
7
7
|
* There is deliberately no per-table *table-creation* migration chain here:
|
|
8
8
|
* `assertCompatibleVersion` (called below, before any schema work) rejects
|
|
@@ -17,7 +17,16 @@ import type { Knex } from 'knex';
|
|
|
17
17
|
* `CREATE TABLE IF NOT EXISTS` is a no-op once the table is present. Column
|
|
18
18
|
* additions to an existing 0.13 table are therefore the one case that still
|
|
19
19
|
* needs an explicit `hasColumn`-guarded `ALTER TABLE` here (`migrateInfoRoots`,
|
|
20
|
-
* `migrateMainContentsColumns`
|
|
20
|
+
* `migrateMainContentsColumns`, `migratePageMetaBodyHash`,
|
|
21
|
+
* `migratePageMetaConsoleErrorCount`, `migrateContentItemsAliasOfId`,
|
|
22
|
+
* `migrateInventoryRunsInvalidSkipped`) rather than a DDL-string change alone.
|
|
23
|
+
*
|
|
24
|
+
* `closeStaleOpenNetworkOutages` is not a schema migration (no columns
|
|
25
|
+
* change) but belongs at this same boot phase for the same reason the
|
|
26
|
+
* others do: it must run before ANY reader (`resetFailedPages`,
|
|
27
|
+
* `listDnsBurnedHostCandidates`, …) can observe a `network_outages` row
|
|
28
|
+
* left `ended_at = NULL` by a crawl process that was killed mid-outage in a
|
|
29
|
+
* prior session.
|
|
21
30
|
*
|
|
22
31
|
* In read-only mode schema init + migration are SKIPPED so the same DB
|
|
23
32
|
* can be opened safely by a viewer attached to a live (or interrupted)
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { applyConnectionPragmas, initSchema } from '../../init-schema.js';
|
|
2
2
|
import { assertCompatibleVersion } from '../../meta/assert-compatible-version.js';
|
|
3
|
+
import { migrateContentItemsAliasOfId } from '../../migrate-content-items-alias-of-id.js';
|
|
3
4
|
import { migrateInfoMainContentSelector } from '../../migrate-info-main-content-selector.js';
|
|
4
5
|
import { migrateInfoRoots } from '../../migrate-info-roots.js';
|
|
6
|
+
import { migrateInventoryRunsInvalidSkipped } from '../../migrate-inventory-runs-invalid-skipped.js';
|
|
5
7
|
import { migrateMainContentsColumns } from '../../migrate-main-contents-columns.js';
|
|
8
|
+
import { migratePageMetaBodyHash } from '../../migrate-page-meta-body-hash.js';
|
|
9
|
+
import { migratePageMetaConsoleErrorCount } from '../../migrate-page-meta-console-error-count.js';
|
|
10
|
+
import { closeStaleOpenNetworkOutages } from '../outages/close-stale-open-network-outages.js';
|
|
6
11
|
/**
|
|
7
12
|
* Initializes the database schema if tables do not exist, then runs the
|
|
8
13
|
* remaining lightweight migrations (`info.roots`, `info.mainContentSelector`,
|
|
9
|
-
* `page_meta.main_content_
|
|
14
|
+
* `page_meta.main_content_*`, `inventory_runs.invalid_skipped`).
|
|
10
15
|
*
|
|
11
16
|
* There is deliberately no per-table *table-creation* migration chain here:
|
|
12
17
|
* `assertCompatibleVersion` (called below, before any schema work) rejects
|
|
@@ -21,7 +26,16 @@ import { migrateMainContentsColumns } from '../../migrate-main-contents-columns.
|
|
|
21
26
|
* `CREATE TABLE IF NOT EXISTS` is a no-op once the table is present. Column
|
|
22
27
|
* additions to an existing 0.13 table are therefore the one case that still
|
|
23
28
|
* needs an explicit `hasColumn`-guarded `ALTER TABLE` here (`migrateInfoRoots`,
|
|
24
|
-
* `migrateMainContentsColumns`
|
|
29
|
+
* `migrateMainContentsColumns`, `migratePageMetaBodyHash`,
|
|
30
|
+
* `migratePageMetaConsoleErrorCount`, `migrateContentItemsAliasOfId`,
|
|
31
|
+
* `migrateInventoryRunsInvalidSkipped`) rather than a DDL-string change alone.
|
|
32
|
+
*
|
|
33
|
+
* `closeStaleOpenNetworkOutages` is not a schema migration (no columns
|
|
34
|
+
* change) but belongs at this same boot phase for the same reason the
|
|
35
|
+
* others do: it must run before ANY reader (`resetFailedPages`,
|
|
36
|
+
* `listDnsBurnedHostCandidates`, …) can observe a `network_outages` row
|
|
37
|
+
* left `ended_at = NULL` by a crawl process that was killed mid-outage in a
|
|
38
|
+
* prior session.
|
|
25
39
|
*
|
|
26
40
|
* In read-only mode schema init + migration are SKIPPED so the same DB
|
|
27
41
|
* can be opened safely by a viewer attached to a live (or interrupted)
|
|
@@ -49,4 +63,9 @@ export async function init(knex, readOnly) {
|
|
|
49
63
|
await migrateInfoRoots(knex);
|
|
50
64
|
await migrateInfoMainContentSelector(knex);
|
|
51
65
|
await migrateMainContentsColumns(knex);
|
|
66
|
+
await migratePageMetaBodyHash(knex);
|
|
67
|
+
await migratePageMetaConsoleErrorCount(knex);
|
|
68
|
+
await migrateContentItemsAliasOfId(knex);
|
|
69
|
+
await migrateInventoryRunsInvalidSkipped(knex);
|
|
70
|
+
await closeStaleOpenNetworkOutages(knex);
|
|
52
71
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Knex } from 'knex';
|
|
2
|
+
/**
|
|
3
|
+
* Close an outage row by stamping `ended_at` — but ONLY if it is still
|
|
4
|
+
* open. The `whereNull('ended_at')` guard is what makes this idempotent: a
|
|
5
|
+
* second call (e.g. a duplicate recovery-probe success racing the first)
|
|
6
|
+
* matches zero rows and is a silent no-op, rather than overwriting an
|
|
7
|
+
* already-recorded `ended_at` with a later timestamp.
|
|
8
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
9
|
+
* @param id - The `network_outages.id` to close.
|
|
10
|
+
* @param endedAt - Epoch ms the outage is considered to have ended.
|
|
11
|
+
*/
|
|
12
|
+
export declare function closeNetworkOutage(knex: Knex, id: number, endedAt: number): Promise<void>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Close an outage row by stamping `ended_at` — but ONLY if it is still
|
|
3
|
+
* open. The `whereNull('ended_at')` guard is what makes this idempotent: a
|
|
4
|
+
* second call (e.g. a duplicate recovery-probe success racing the first)
|
|
5
|
+
* matches zero rows and is a silent no-op, rather than overwriting an
|
|
6
|
+
* already-recorded `ended_at` with a later timestamp.
|
|
7
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
8
|
+
* @param id - The `network_outages.id` to close.
|
|
9
|
+
* @param endedAt - Epoch ms the outage is considered to have ended.
|
|
10
|
+
*/
|
|
11
|
+
export async function closeNetworkOutage(knex, id, endedAt) {
|
|
12
|
+
await knex('network_outages').where({ id }).whereNull('ended_at').update({
|
|
13
|
+
ended_at: endedAt,
|
|
14
|
+
});
|
|
15
|
+
}
|