@nitpicker/crawler 0.21.0 → 0.22.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/append-retry-failed-common-setup-phases.d.ts +1 -1
- package/lib/append-retry-failed-common-setup-phases.js +5 -1
- package/lib/append-setup-phases.d.ts +1 -1
- package/lib/archive/archive.d.ts +57 -8
- package/lib/archive/archive.js +83 -12
- package/lib/archive/create-adjunct-tables.d.ts +3 -2
- package/lib/archive/create-adjunct-tables.js +12 -9
- package/lib/archive/create-entity-tables.js +3 -0
- package/lib/archive/database.d.ts +39 -7
- package/lib/archive/database.js +49 -9
- package/lib/archive/db-ops/_shared/keyset-paginate-by-id.d.ts +49 -0
- package/lib/archive/db-ops/_shared/keyset-paginate-by-id.js +69 -0
- package/lib/archive/db-ops/_shared/resolve-content-item-id.d.ts +13 -12
- package/lib/archive/db-ops/_shared/resolve-content-item-id.js +63 -31
- package/lib/archive/db-ops/_shared/types.d.ts +39 -0
- package/lib/archive/db-ops/analysis/replace-analysis-violations.js +7 -4
- package/lib/archive/db-ops/config/get-created-cwd.d.ts +11 -0
- package/lib/archive/db-ops/config/get-created-cwd.js +13 -0
- package/lib/archive/db-ops/config/info-column-allowlist.js +1 -0
- package/lib/archive/db-ops/dedupe-cap/list-dedupe-cap-observations.d.ts +57 -0
- package/lib/archive/db-ops/dedupe-cap/list-dedupe-cap-observations.js +87 -0
- package/lib/archive/db-ops/errors/insert-page-error.js +3 -1
- package/lib/archive/db-ops/lifecycle/init.d.ts +13 -6
- package/lib/archive/db-ops/lifecycle/init.js +30 -10
- package/lib/archive/db-ops/list-reconcile/record-list-reconcile-run.d.ts +22 -0
- package/lib/archive/db-ops/{inventory/record-inventory-run.js → list-reconcile/record-list-reconcile-run.js} +11 -10
- package/lib/archive/db-ops/pages/read/build-page-query.js +1 -1
- package/lib/archive/db-ops/pages/read/get-crawling-state.d.ts +9 -2
- package/lib/archive/db-ops/pages/read/get-crawling-state.js +11 -3
- package/lib/archive/db-ops/pages/reset/clear-page-derived-rows.d.ts +20 -0
- package/lib/archive/db-ops/pages/reset/clear-page-derived-rows.js +37 -0
- package/lib/archive/db-ops/pages/reset/repromote-external-pages.d.ts +3 -2
- package/lib/archive/db-ops/pages/reset/repromote-external-pages.js +17 -37
- package/lib/archive/db-ops/pages/reset/reset-failed-pages.d.ts +22 -1
- package/lib/archive/db-ops/pages/reset/reset-failed-pages.js +79 -33
- package/lib/archive/db-ops/pages/reset/reset-pages-by-urls.d.ts +78 -0
- package/lib/archive/db-ops/pages/reset/reset-pages-by-urls.js +146 -0
- package/lib/archive/db-ops/pages/reset/retryable-image-scan-codes.d.ts +24 -0
- package/lib/archive/db-ops/pages/reset/retryable-image-scan-codes.js +24 -0
- package/lib/archive/db-ops/pages/write/insert-inventory-content-items.js +2 -1
- package/lib/archive/db-ops/pages/write/insert-page.js +4 -2
- package/lib/archive/db-ops/pages/write/link-redirect-sources.js +3 -1
- package/lib/archive/db-ops/pages/write/record-redirect.js +1 -1
- package/lib/archive/db-ops/pages/write/set-skipped-page.js +3 -1
- package/lib/archive/db-ops/pages/write/update-page.d.ts +9 -1
- package/lib/archive/db-ops/pages/write/update-page.js +28 -6
- package/lib/archive/db-ops/resources/get-resource-url-list.d.ts +3 -6
- package/lib/archive/db-ops/resources/get-resource-url-list.js +10 -35
- package/lib/archive/init-schema.d.ts +1 -1
- package/lib/archive/init-schema.js +3 -2
- package/lib/archive/meta/compute-main-contents-denormalized.d.ts +11 -3
- package/lib/archive/meta/compute-main-contents-denormalized.js +14 -2
- package/lib/archive/meta/types.d.ts +24 -0
- package/lib/archive/migrate-content-items-is-metadata-only.d.ts +44 -0
- package/lib/archive/migrate-content-items-is-metadata-only.js +60 -0
- package/lib/archive/migrate-info-created-cwd.d.ts +17 -0
- package/lib/archive/migrate-info-created-cwd.js +36 -0
- package/lib/archive/migrate-inventory-runs-to-list-reconcile-runs.d.ts +34 -0
- package/lib/archive/migrate-inventory-runs-to-list-reconcile-runs.js +51 -0
- package/lib/archive/{migrate-inventory-runs-exclude-skipped.d.ts → migrate-list-reconcile-runs-exclude-skipped.d.ts} +8 -8
- package/lib/archive/{migrate-inventory-runs-exclude-skipped.js → migrate-list-reconcile-runs-exclude-skipped.js} +12 -12
- package/lib/archive/migrate-list-reconcile-runs-invalid-skipped.d.ts +16 -0
- package/lib/archive/migrate-list-reconcile-runs-invalid-skipped.js +35 -0
- package/lib/archive/migrate-page-meta-image-scan.d.ts +27 -0
- package/lib/archive/migrate-page-meta-image-scan.js +52 -0
- package/lib/archive/page.d.ts +12 -0
- package/lib/archive/page.js +16 -0
- package/lib/archive/types.d.ts +97 -10
- package/lib/compute-auto-retry-backoff-delay.d.ts +30 -0
- package/lib/compute-auto-retry-backoff-delay.js +36 -0
- package/lib/crawler/apply-crawl-runtime-options-patch.d.ts +32 -0
- package/lib/crawler/apply-crawl-runtime-options-patch.js +129 -0
- package/lib/crawler/crawler.d.ts +35 -3
- package/lib/crawler/crawler.js +486 -338
- package/lib/crawler/dedupe/build-dedupe-cap-observation.d.ts +74 -0
- package/lib/crawler/dedupe/build-dedupe-cap-observation.js +97 -0
- package/lib/crawler/dedupe/compute-meta-signature.d.ts +7 -5
- package/lib/crawler/dedupe/compute-meta-signature.js +0 -0
- package/lib/crawler/dedupe/compute-shape-key.d.ts +20 -0
- package/lib/crawler/dedupe/compute-shape-key.js +73 -1
- package/lib/crawler/dedupe/dedupe-cap-tracker.d.ts +6 -1
- package/lib/crawler/dedupe/dedupe-cap-tracker.js +6 -1
- package/lib/crawler/dedupe/resolve-og-url-mismatch.d.ts +7 -5
- package/lib/crawler/dedupe/resolve-og-url-mismatch.js +5 -3
- package/lib/crawler/dedupe/types.d.ts +18 -0
- package/lib/crawler/fetch-destination.js +1 -0
- package/lib/crawler/link-list.d.ts +9 -1
- package/lib/crawler/link-list.js +12 -2
- package/lib/crawler/link-to-page-data.js +1 -0
- package/lib/crawler/resource-to-page-data.js +1 -0
- package/lib/crawler/types.d.ts +75 -0
- package/lib/crawler-orchestrator.d.ts +171 -8
- package/lib/crawler-orchestrator.js +1177 -234
- package/lib/crawler.d.ts +3 -0
- package/lib/crawler.js +3 -0
- package/lib/delay-or-abort.d.ts +20 -0
- package/lib/delay-or-abort.js +35 -0
- package/lib/pending-urls-remain-error.d.ts +35 -0
- package/lib/pending-urls-remain-error.js +43 -0
- package/lib/recrawl-setup-phases.d.ts +38 -0
- package/lib/recrawl-setup-phases.js +52 -0
- package/lib/resume-setup-phases.d.ts +1 -1
- package/lib/resume-setup-phases.js +5 -1
- package/lib/retry-failed-setup-phases.d.ts +1 -1
- package/lib/setup-recovery-phase-labels.d.ts +17 -8
- package/lib/setup-recovery-phase-labels.js +17 -8
- package/lib/types.d.ts +62 -12
- package/lib/utils/compute-file-sha256.d.ts +1 -1
- package/lib/utils/compute-file-sha256.js +1 -1
- package/package.json +4 -4
- package/lib/archive/db-ops/inventory/record-inventory-run.d.ts +0 -21
- package/lib/archive/migrate-inventory-runs-invalid-skipped.d.ts +0 -16
- package/lib/archive/migrate-inventory-runs-invalid-skipped.js +0 -35
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ResetPagesByUrlsResult } from '../../../types.js';
|
|
2
|
+
import type { Knex } from 'knex';
|
|
3
|
+
/**
|
|
4
|
+
* Reset pages matching an operator-supplied URL list back to pending, so a
|
|
5
|
+
* follow-up crawl re-fetches them from scratch — the un-scrape half of
|
|
6
|
+
* `crawl --recrawl`.
|
|
7
|
+
*
|
|
8
|
+
* Unlike {@link resetFailedPages}, this function does not filter by prior
|
|
9
|
+
* failure kind: a URL named explicitly by the operator is reset regardless
|
|
10
|
+
* of what its last observation was, including a definitive `4xx`/`5xx` — the
|
|
11
|
+
* whole point of `--recrawl` is to catch a page that used to 404 and is now
|
|
12
|
+
* live. The only requirement on the raw candidate scan is `scraped = 1` (a
|
|
13
|
+
* page never scraped has nothing to reset; it is the caller's "novel URL"
|
|
14
|
+
* path instead).
|
|
15
|
+
*
|
|
16
|
+
* Three conservative guards then apply — matching content follows the same
|
|
17
|
+
* union used to skip `resetFailedPages`'s permanent-failure filter had none:
|
|
18
|
+
* a matched row is excluded (not reset) rather than silently promoted, and
|
|
19
|
+
* the exclusion is reported back to the caller by category:
|
|
20
|
+
*
|
|
21
|
+
* - **Redirect source** (`redirect_dest_id` is set): resetting it would
|
|
22
|
+
* discard a real, previously-observed 3xx. If the operator wants the
|
|
23
|
+
* redirect re-verified, that is `--retry-failed`'s job (which does NOT
|
|
24
|
+
* exclude redirect sources from a permanent-failure reset — a different
|
|
25
|
+
* operation with a different default).
|
|
26
|
+
* - **Intentionally skipped** (`is_skipped = 1`): the row was excluded by
|
|
27
|
+
* `excludes`/`excludeUrls` at ingestion or fetch time. `--recrawl` does not
|
|
28
|
+
* second-guess that configuration; re-running the crawl with a changed
|
|
29
|
+
* exclude list is the supported path to un-skip a page.
|
|
30
|
+
* - **External** (`is_external = 1`): bringing a scope-external URL back
|
|
31
|
+
* into scope is `--append`'s job (`repromoteExternalPages`), which also
|
|
32
|
+
* updates the scope map. `--recrawl` only re-fetches in-scope pages.
|
|
33
|
+
*
|
|
34
|
+
* Deliberately does NOT exclude candidates whose URL shape already has a
|
|
35
|
+
* confirmed same-cluster trap recorded (`dedupe_cap_events.shape_key`) — the
|
|
36
|
+
* exclusion {@link resetFailedPages} applies for exactly that reason (see its
|
|
37
|
+
* own JSDoc). The two functions differ on what "the operator asked for this"
|
|
38
|
+
* means: `resetFailedPages` widens an automatic, unattended SQL scan
|
|
39
|
+
* (`--retry-failed`), so a known trap is safe to silently skip re-admitting.
|
|
40
|
+
* `resetPagesByUrls` resets URLs the operator named one-by-one in a file —
|
|
41
|
+
* the same reasoning that already exempts explicit URLs from the
|
|
42
|
+
* failure-kind filter above applies here too: an operator who names a
|
|
43
|
+
* specific trap-shaped URL is not "re-discovering" it by accident, and
|
|
44
|
+
* silently refusing to reset a row they explicitly listed would be a more
|
|
45
|
+
* surprising failure mode than honouring the request.
|
|
46
|
+
*
|
|
47
|
+
* A URL that matches no `content_items` row at all (not yet known to the
|
|
48
|
+
* archive) is silently absent from every array on the result — the caller
|
|
49
|
+
* treats it as a novel URL, the same "not yet known" path `--inventory`
|
|
50
|
+
* takes. A URL matching a row with `scraped = 0` is likewise absent from
|
|
51
|
+
* every array: it is already pending, so there is nothing to reset.
|
|
52
|
+
*
|
|
53
|
+
* On a match, `content_items` is UPDATEd exactly like
|
|
54
|
+
* {@link resetFailedPages} (`scraped`/`status`/`status_text`/
|
|
55
|
+
* `content_type_id`/`content_length`/`header_set_id` cleared,
|
|
56
|
+
* `first_crawled_at`/`last_crawled_at` preserved), and every derived row is
|
|
57
|
+
* cleared via {@link clearPageDerivedRows} plus `page_errors` and
|
|
58
|
+
* `analysis_violations` — the latter is not part of the shared helper
|
|
59
|
+
* (`repromoteExternalPages` never clears it) but a re-fetched page's old
|
|
60
|
+
* lint findings would otherwise report on HTML that no longer exists until
|
|
61
|
+
* the next `analyze` run overwrites the whole table. `analysis_text_refs` is
|
|
62
|
+
* a content-hash dictionary shared across pages and is not touched; an
|
|
63
|
+
* orphaned entry is a harmless, unreferenced row, the same trade-off already
|
|
64
|
+
* made for `page_html_blobs`.
|
|
65
|
+
*
|
|
66
|
+
* SELECT and UPDATE/DELETE statements are chunked to stay below SQLite's
|
|
67
|
+
* `SQLITE_LIMIT_VARIABLE_NUMBER`.
|
|
68
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
69
|
+
* @param urls - URL strings to match against `content_items`, already in
|
|
70
|
+
* `withoutHashAndAuth` form (mirrors every other URL-list comparison in
|
|
71
|
+
* this package, e.g. `getExistingPageUrls`).
|
|
72
|
+
* @param onProgress - Called after each chunk's DELETE/UPDATE statements
|
|
73
|
+
* complete, with the pages processed so far and the total to reset. Omit
|
|
74
|
+
* for no reporting (the default; e.g. tests).
|
|
75
|
+
* @returns The reset URLs, plus the excluded URLs grouped by exclusion
|
|
76
|
+
* reason — see {@link ResetPagesByUrlsResult}.
|
|
77
|
+
*/
|
|
78
|
+
export declare function resetPagesByUrls(knex: Knex, urls: readonly string[], onProgress?: (processed: number, total: number) => void): Promise<ResetPagesByUrlsResult>;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { eachSplitted } from '../../../../utils/array/each-splitted.js';
|
|
2
|
+
import { dbLog } from '../../../debug.js';
|
|
3
|
+
import { clearPageDerivedRows } from './clear-page-derived-rows.js';
|
|
4
|
+
/**
|
|
5
|
+
* Reset pages matching an operator-supplied URL list back to pending, so a
|
|
6
|
+
* follow-up crawl re-fetches them from scratch — the un-scrape half of
|
|
7
|
+
* `crawl --recrawl`.
|
|
8
|
+
*
|
|
9
|
+
* Unlike {@link resetFailedPages}, this function does not filter by prior
|
|
10
|
+
* failure kind: a URL named explicitly by the operator is reset regardless
|
|
11
|
+
* of what its last observation was, including a definitive `4xx`/`5xx` — the
|
|
12
|
+
* whole point of `--recrawl` is to catch a page that used to 404 and is now
|
|
13
|
+
* live. The only requirement on the raw candidate scan is `scraped = 1` (a
|
|
14
|
+
* page never scraped has nothing to reset; it is the caller's "novel URL"
|
|
15
|
+
* path instead).
|
|
16
|
+
*
|
|
17
|
+
* Three conservative guards then apply — matching content follows the same
|
|
18
|
+
* union used to skip `resetFailedPages`'s permanent-failure filter had none:
|
|
19
|
+
* a matched row is excluded (not reset) rather than silently promoted, and
|
|
20
|
+
* the exclusion is reported back to the caller by category:
|
|
21
|
+
*
|
|
22
|
+
* - **Redirect source** (`redirect_dest_id` is set): resetting it would
|
|
23
|
+
* discard a real, previously-observed 3xx. If the operator wants the
|
|
24
|
+
* redirect re-verified, that is `--retry-failed`'s job (which does NOT
|
|
25
|
+
* exclude redirect sources from a permanent-failure reset — a different
|
|
26
|
+
* operation with a different default).
|
|
27
|
+
* - **Intentionally skipped** (`is_skipped = 1`): the row was excluded by
|
|
28
|
+
* `excludes`/`excludeUrls` at ingestion or fetch time. `--recrawl` does not
|
|
29
|
+
* second-guess that configuration; re-running the crawl with a changed
|
|
30
|
+
* exclude list is the supported path to un-skip a page.
|
|
31
|
+
* - **External** (`is_external = 1`): bringing a scope-external URL back
|
|
32
|
+
* into scope is `--append`'s job (`repromoteExternalPages`), which also
|
|
33
|
+
* updates the scope map. `--recrawl` only re-fetches in-scope pages.
|
|
34
|
+
*
|
|
35
|
+
* Deliberately does NOT exclude candidates whose URL shape already has a
|
|
36
|
+
* confirmed same-cluster trap recorded (`dedupe_cap_events.shape_key`) — the
|
|
37
|
+
* exclusion {@link resetFailedPages} applies for exactly that reason (see its
|
|
38
|
+
* own JSDoc). The two functions differ on what "the operator asked for this"
|
|
39
|
+
* means: `resetFailedPages` widens an automatic, unattended SQL scan
|
|
40
|
+
* (`--retry-failed`), so a known trap is safe to silently skip re-admitting.
|
|
41
|
+
* `resetPagesByUrls` resets URLs the operator named one-by-one in a file —
|
|
42
|
+
* the same reasoning that already exempts explicit URLs from the
|
|
43
|
+
* failure-kind filter above applies here too: an operator who names a
|
|
44
|
+
* specific trap-shaped URL is not "re-discovering" it by accident, and
|
|
45
|
+
* silently refusing to reset a row they explicitly listed would be a more
|
|
46
|
+
* surprising failure mode than honouring the request.
|
|
47
|
+
*
|
|
48
|
+
* A URL that matches no `content_items` row at all (not yet known to the
|
|
49
|
+
* archive) is silently absent from every array on the result — the caller
|
|
50
|
+
* treats it as a novel URL, the same "not yet known" path `--inventory`
|
|
51
|
+
* takes. A URL matching a row with `scraped = 0` is likewise absent from
|
|
52
|
+
* every array: it is already pending, so there is nothing to reset.
|
|
53
|
+
*
|
|
54
|
+
* On a match, `content_items` is UPDATEd exactly like
|
|
55
|
+
* {@link resetFailedPages} (`scraped`/`status`/`status_text`/
|
|
56
|
+
* `content_type_id`/`content_length`/`header_set_id` cleared,
|
|
57
|
+
* `first_crawled_at`/`last_crawled_at` preserved), and every derived row is
|
|
58
|
+
* cleared via {@link clearPageDerivedRows} plus `page_errors` and
|
|
59
|
+
* `analysis_violations` — the latter is not part of the shared helper
|
|
60
|
+
* (`repromoteExternalPages` never clears it) but a re-fetched page's old
|
|
61
|
+
* lint findings would otherwise report on HTML that no longer exists until
|
|
62
|
+
* the next `analyze` run overwrites the whole table. `analysis_text_refs` is
|
|
63
|
+
* a content-hash dictionary shared across pages and is not touched; an
|
|
64
|
+
* orphaned entry is a harmless, unreferenced row, the same trade-off already
|
|
65
|
+
* made for `page_html_blobs`.
|
|
66
|
+
*
|
|
67
|
+
* SELECT and UPDATE/DELETE statements are chunked to stay below SQLite's
|
|
68
|
+
* `SQLITE_LIMIT_VARIABLE_NUMBER`.
|
|
69
|
+
* @param knex - Knex query builder connected to the archive DB.
|
|
70
|
+
* @param urls - URL strings to match against `content_items`, already in
|
|
71
|
+
* `withoutHashAndAuth` form (mirrors every other URL-list comparison in
|
|
72
|
+
* this package, e.g. `getExistingPageUrls`).
|
|
73
|
+
* @param onProgress - Called after each chunk's DELETE/UPDATE statements
|
|
74
|
+
* complete, with the pages processed so far and the total to reset. Omit
|
|
75
|
+
* for no reporting (the default; e.g. tests).
|
|
76
|
+
* @returns The reset URLs, plus the excluded URLs grouped by exclusion
|
|
77
|
+
* reason — see {@link ResetPagesByUrlsResult}.
|
|
78
|
+
*/
|
|
79
|
+
export async function resetPagesByUrls(knex, urls, onProgress) {
|
|
80
|
+
const empty = {
|
|
81
|
+
resetUrls: [],
|
|
82
|
+
excludedRedirects: [],
|
|
83
|
+
excludedSkipped: [],
|
|
84
|
+
excludedExternal: [],
|
|
85
|
+
};
|
|
86
|
+
if (urls.length === 0) {
|
|
87
|
+
return empty;
|
|
88
|
+
}
|
|
89
|
+
const candidates = [];
|
|
90
|
+
await eachSplitted([...urls], 500, async (chunk) => {
|
|
91
|
+
const rows = await knex('content_items')
|
|
92
|
+
.join('url_refs', 'url_refs.id', 'content_items.url_id')
|
|
93
|
+
.select('content_items.id as id', 'url_refs.url as url', 'content_items.redirect_dest_id as redirect_dest_id', 'content_items.is_skipped as is_skipped', 'content_items.is_external as is_external')
|
|
94
|
+
.where('content_items.scraped', 1)
|
|
95
|
+
.whereIn('url_refs.url', chunk);
|
|
96
|
+
candidates.push(...rows);
|
|
97
|
+
});
|
|
98
|
+
if (candidates.length === 0) {
|
|
99
|
+
return empty;
|
|
100
|
+
}
|
|
101
|
+
const resettable = [];
|
|
102
|
+
const excludedRedirects = [];
|
|
103
|
+
const excludedSkipped = [];
|
|
104
|
+
const excludedExternal = [];
|
|
105
|
+
for (const row of candidates) {
|
|
106
|
+
if (row.redirect_dest_id != null) {
|
|
107
|
+
excludedRedirects.push(row.url);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (row.is_skipped) {
|
|
111
|
+
excludedSkipped.push(row.url);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (row.is_external) {
|
|
115
|
+
excludedExternal.push(row.url);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
resettable.push(row);
|
|
119
|
+
}
|
|
120
|
+
if (resettable.length === 0) {
|
|
121
|
+
return { resetUrls: [], excludedRedirects, excludedSkipped, excludedExternal };
|
|
122
|
+
}
|
|
123
|
+
const ids = resettable.map((row) => row.id);
|
|
124
|
+
const resetUrls = resettable.map((row) => row.url);
|
|
125
|
+
const chunkSize = 500;
|
|
126
|
+
for (let i = 0; i < ids.length; i += chunkSize) {
|
|
127
|
+
const chunk = ids.slice(i, i + chunkSize);
|
|
128
|
+
await knex('content_items').whereIn('id', chunk).update({
|
|
129
|
+
scraped: 0,
|
|
130
|
+
status: null,
|
|
131
|
+
status_text: null,
|
|
132
|
+
content_type_id: null,
|
|
133
|
+
content_length: null,
|
|
134
|
+
header_set_id: null,
|
|
135
|
+
// `first_crawled_at` / `last_crawled_at` are deliberately left
|
|
136
|
+
// untouched, matching `resetFailedPages` — the last-success
|
|
137
|
+
// timestamp records survive the demotion.
|
|
138
|
+
});
|
|
139
|
+
await knex('page_errors').whereIn('pageId', chunk).delete();
|
|
140
|
+
await knex('analysis_violations').whereIn('page_id', chunk).delete();
|
|
141
|
+
await clearPageDerivedRows(knex, chunk);
|
|
142
|
+
onProgress?.(Math.min(i + chunkSize, ids.length), ids.length);
|
|
143
|
+
}
|
|
144
|
+
dbLog('Reset %d page(s) matched by URL list back to pending', resetUrls.length);
|
|
145
|
+
return { resetUrls, excludedRedirects, excludedSkipped, excludedExternal };
|
|
146
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `page_meta.image_scan_desktop` / `image_scan_mobile` codes that qualify a
|
|
3
|
+
* page for `--retry-failed`, mirroring `@d-zero/beholder`'s `IMAGE_SCAN_CODE`
|
|
4
|
+
* (kept as a plain numeric literal list here rather than importing that
|
|
5
|
+
* constant, since `@nitpicker/crawler`'s `page_meta` columns persist across
|
|
6
|
+
* a `@d-zero/beholder` version bump and must keep meaning the same numbers
|
|
7
|
+
* even if a future crawler build pins an older beholder).
|
|
8
|
+
*
|
|
9
|
+
* - `2` (`nav-unsettled`) and `3` (`frame-lost`) are transient failures the
|
|
10
|
+
* scan itself could not recover from — a retry is likely to succeed.
|
|
11
|
+
* - `255` (`unknown`) is retried too, erring on the side of investigation,
|
|
12
|
+
* consistent with {@link import('../../../../classify-error-kind.js').classifyErrorKind}'s
|
|
13
|
+
* "unknown" kind never being treated as permanent.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately excluded:
|
|
16
|
+
* - `0` (`ok`) and `1` (`degraded`) — the scan produced data.
|
|
17
|
+
* - `4` (`scroll-height-exceeded`) — a deterministic, page-shape-driven
|
|
18
|
+
* outcome that a retry cannot change.
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* qb.whereIn('page_meta.image_scan_desktop', RETRYABLE_IMAGE_SCAN_CODES);
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare const RETRYABLE_IMAGE_SCAN_CODES: readonly [2, 3, 255];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `page_meta.image_scan_desktop` / `image_scan_mobile` codes that qualify a
|
|
3
|
+
* page for `--retry-failed`, mirroring `@d-zero/beholder`'s `IMAGE_SCAN_CODE`
|
|
4
|
+
* (kept as a plain numeric literal list here rather than importing that
|
|
5
|
+
* constant, since `@nitpicker/crawler`'s `page_meta` columns persist across
|
|
6
|
+
* a `@d-zero/beholder` version bump and must keep meaning the same numbers
|
|
7
|
+
* even if a future crawler build pins an older beholder).
|
|
8
|
+
*
|
|
9
|
+
* - `2` (`nav-unsettled`) and `3` (`frame-lost`) are transient failures the
|
|
10
|
+
* scan itself could not recover from — a retry is likely to succeed.
|
|
11
|
+
* - `255` (`unknown`) is retried too, erring on the side of investigation,
|
|
12
|
+
* consistent with {@link import('../../../../classify-error-kind.js').classifyErrorKind}'s
|
|
13
|
+
* "unknown" kind never being treated as permanent.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately excluded:
|
|
16
|
+
* - `0` (`ok`) and `1` (`degraded`) — the scan produced data.
|
|
17
|
+
* - `4` (`scroll-height-exceeded`) — a deterministic, page-shape-driven
|
|
18
|
+
* outcome that a retry cannot change.
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* qb.whereIn('page_meta.image_scan_desktop', RETRYABLE_IMAGE_SCAN_CODES);
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export const RETRYABLE_IMAGE_SCAN_CODES = [2, 3, 255];
|
|
@@ -45,7 +45,7 @@ export async function insertInventoryContentItems(params) {
|
|
|
45
45
|
});
|
|
46
46
|
await knex('content_items').insert(rows).onConflict('url_id').ignore();
|
|
47
47
|
const inserted = (await knex
|
|
48
|
-
.select('ci.id', 'ci.source', 'ur.url')
|
|
48
|
+
.select('ci.id', 'ci.source', 'ci.is_metadata_only', 'ur.url')
|
|
49
49
|
.from('content_items as ci')
|
|
50
50
|
.join('url_refs as ur', 'ur.id', 'ci.url_id')
|
|
51
51
|
.whereIn('ur.url', chunk));
|
|
@@ -53,6 +53,7 @@ export async function insertInventoryContentItems(params) {
|
|
|
53
53
|
caches.contentItems.set(insertedRow.url, {
|
|
54
54
|
id: insertedRow.id,
|
|
55
55
|
source: insertedRow.source,
|
|
56
|
+
isMetadataOnly: insertedRow.is_metadata_only,
|
|
56
57
|
});
|
|
57
58
|
}
|
|
58
59
|
});
|
|
@@ -65,10 +65,12 @@ import { upsertUrlRef } from '../../_shared/upsert-url-ref.js';
|
|
|
65
65
|
*/
|
|
66
66
|
export async function insertPage(knex, caches, page, isTarget, trx, source) {
|
|
67
67
|
const qb = trx ?? knex;
|
|
68
|
-
const pageId = await resolveContentItemId(qb, caches, page.url.withoutHashAndAuth,
|
|
68
|
+
const pageId = await resolveContentItemId(qb, caches, page.url.withoutHashAndAuth, {
|
|
69
|
+
source,
|
|
70
|
+
});
|
|
69
71
|
const flat = deriveFlatFromMeta(page.meta, page.url.href);
|
|
70
72
|
const denorm = computePageDenormalized(page.meta);
|
|
71
|
-
const mainContentsDenorm = computeMainContentsDenormalized(page.mainContents, page.scrollHeight, page.mainContentCustomElements?.length);
|
|
73
|
+
const mainContentsDenorm = computeMainContentsDenormalized(page.mainContents, page.scrollHeight, page.mainContentCustomElements?.length, page.imageScan);
|
|
72
74
|
const extras = deriveMetaExtras(page.meta);
|
|
73
75
|
const now = Date.now();
|
|
74
76
|
// Canonicalize so the stored dictionary value matches the exact-string
|
|
@@ -53,7 +53,9 @@ export async function linkRedirectSources(trx, caches, sources, destId, destUrlN
|
|
|
53
53
|
// argument is `'crawled'`, matching the anchor-lineage propagation
|
|
54
54
|
// contract — an existing inventory-* intermediate that is later
|
|
55
55
|
// traversed by a `'crawled'` chain gets downgraded.
|
|
56
|
-
const redirectId = await resolveContentItemId(trx, caches, redirect,
|
|
56
|
+
const redirectId = await resolveContentItemId(trx, caches, redirect, {
|
|
57
|
+
source: chainLineageSource,
|
|
58
|
+
});
|
|
57
59
|
await trx('content_items')
|
|
58
60
|
.where('id', redirectId)
|
|
59
61
|
.update({
|
|
@@ -62,7 +62,7 @@ export async function recordRedirect(knex, caches, page, source) {
|
|
|
62
62
|
// inventory chain — without the pass-through, inventory lineage
|
|
63
63
|
// would be laundered to `'crawled'` for js-redirect rescue / #73
|
|
64
64
|
// convergence destinations that have not yet been rendered.
|
|
65
|
-
const destId = await resolveContentItemId(trx, caches, destUrlObject.withoutHashAndAuth,
|
|
65
|
+
const destId = await resolveContentItemId(trx, caches, destUrlObject.withoutHashAndAuth, { source });
|
|
66
66
|
// Chain lineage propagates FROM the originating URL (`page.url`),
|
|
67
67
|
// NOT from the destination. The originating URL is what initiated
|
|
68
68
|
// the redirect chain, so its lineage is what every intermediate hop
|
|
@@ -10,7 +10,9 @@ import { resolveContentItemId } from '../../_shared/resolve-content-item-id.js';
|
|
|
10
10
|
* @param isExternal - Whether the page is on an external domain. Defaults to `false`.
|
|
11
11
|
*/
|
|
12
12
|
export async function setSkippedPage(knex, caches, url, reason, isExternal = false) {
|
|
13
|
-
const pageId = await resolveContentItemId(knex, caches, url,
|
|
13
|
+
const pageId = await resolveContentItemId(knex, caches, url, {
|
|
14
|
+
isExternal: isExternal ? 1 : 0,
|
|
15
|
+
});
|
|
14
16
|
await knex('content_items')
|
|
15
17
|
.where('id', pageId)
|
|
16
18
|
.update({
|
|
@@ -27,6 +27,14 @@ import type { Knex } from 'knex';
|
|
|
27
27
|
* @param bodyHash - Precomputed body hash for the page's HTML (see
|
|
28
28
|
* `CrawlerEventTypes.page.bodyHash`). `undefined`/`null` falls back to
|
|
29
29
|
* computing it from the HTML instead.
|
|
30
|
+
* @param recursive - The crawl session's `recursive` option, forwarded to
|
|
31
|
+
* `replaceAnchorEdges` so it can compute each discovered anchor's
|
|
32
|
+
* `is_metadata_only` value the same way `processAnchors`
|
|
33
|
+
* (`handle-scrape-end.ts`) decides whether to queue it for a full or
|
|
34
|
+
* metadata-only scrape (#369). Defaults to `true` (the historical,
|
|
35
|
+
* pre-#369 behaviour: never persist an anchor as metadata-only) for
|
|
36
|
+
* callers that do not track the option, such as `setExternalPage`, whose
|
|
37
|
+
* anchor list is always empty anyway.
|
|
30
38
|
* @returns The database `pageId` (`content_items.id`) of the inserted or updated row.
|
|
31
39
|
*/
|
|
32
|
-
export declare function updatePage(knex: Knex, caches: WriteRefCaches, page: PageDataWithDomPaths, writeHtml: boolean, isTarget: boolean, source?: PageSource, bodyHash?: Buffer | null): Promise<number>;
|
|
40
|
+
export declare function updatePage(knex: Knex, caches: WriteRefCaches, page: PageDataWithDomPaths, writeHtml: boolean, isTarget: boolean, source?: PageSource, bodyHash?: Buffer | null, recursive?: boolean): Promise<number>;
|
|
@@ -50,9 +50,17 @@ import { writePageHtmlBlob } from './write-page-html-blob.js';
|
|
|
50
50
|
* @param bodyHash - Precomputed body hash for the page's HTML (see
|
|
51
51
|
* `CrawlerEventTypes.page.bodyHash`). `undefined`/`null` falls back to
|
|
52
52
|
* computing it from the HTML instead.
|
|
53
|
+
* @param recursive - The crawl session's `recursive` option, forwarded to
|
|
54
|
+
* `replaceAnchorEdges` so it can compute each discovered anchor's
|
|
55
|
+
* `is_metadata_only` value the same way `processAnchors`
|
|
56
|
+
* (`handle-scrape-end.ts`) decides whether to queue it for a full or
|
|
57
|
+
* metadata-only scrape (#369). Defaults to `true` (the historical,
|
|
58
|
+
* pre-#369 behaviour: never persist an anchor as metadata-only) for
|
|
59
|
+
* callers that do not track the option, such as `setExternalPage`, whose
|
|
60
|
+
* anchor list is always empty anyway.
|
|
53
61
|
* @returns The database `pageId` (`content_items.id`) of the inserted or updated row.
|
|
54
62
|
*/
|
|
55
|
-
export async function updatePage(knex, caches, page, writeHtml, isTarget, source, bodyHash) {
|
|
63
|
+
export async function updatePage(knex, caches, page, writeHtml, isTarget, source, bodyHash, recursive = true) {
|
|
56
64
|
const { destUrl, sources } = resolveRedirectChain(page.url.withoutHashAndAuth, page.redirectPaths);
|
|
57
65
|
const destUrlObject = parseUrl(destUrl);
|
|
58
66
|
if (!destUrlObject) {
|
|
@@ -60,7 +68,7 @@ export async function updatePage(knex, caches, page, writeHtml, isTarget, source
|
|
|
60
68
|
}
|
|
61
69
|
try {
|
|
62
70
|
return await knex.transaction(async (trx) => {
|
|
63
|
-
return await updatePageInTransaction(trx, knex, caches, page, destUrlObject, sources, writeHtml, isTarget, source, bodyHash);
|
|
71
|
+
return await updatePageInTransaction(trx, knex, caches, page, destUrlObject, sources, writeHtml, isTarget, source, bodyHash, recursive);
|
|
64
72
|
});
|
|
65
73
|
}
|
|
66
74
|
catch (error) {
|
|
@@ -88,9 +96,10 @@ export async function updatePage(knex, caches, page, writeHtml, isTarget, source
|
|
|
88
96
|
* @param isTarget - See {@link updatePage}.
|
|
89
97
|
* @param source - See {@link updatePage}.
|
|
90
98
|
* @param bodyHash - See {@link updatePage}.
|
|
99
|
+
* @param recursive - See {@link updatePage}.
|
|
91
100
|
* @returns The `content_items.id` of the inserted or updated row.
|
|
92
101
|
*/
|
|
93
|
-
async function updatePageInTransaction(trx, knex, caches, page, destUrlObject, sources, writeHtml, isTarget, source, bodyHash) {
|
|
102
|
+
async function updatePageInTransaction(trx, knex, caches, page, destUrlObject, sources, writeHtml, isTarget, source, bodyHash, recursive) {
|
|
94
103
|
const pageId = await insertPage(knex, caches, {
|
|
95
104
|
...page,
|
|
96
105
|
url: destUrlObject,
|
|
@@ -218,7 +227,7 @@ async function updatePageInTransaction(trx, knex, caches, page, destUrlObject, s
|
|
|
218
227
|
.from('content_items')
|
|
219
228
|
.where('id', pageId));
|
|
220
229
|
const anchorLineageSource = deriveLineageFromParent(parentRow?.source, 'crawled');
|
|
221
|
-
await replaceAnchorEdges(trx, caches, pageId, page, anchorLineageSource);
|
|
230
|
+
await replaceAnchorEdges(trx, caches, pageId, page, anchorLineageSource, recursive);
|
|
222
231
|
await replaceImageItems(trx, caches, pageId, page);
|
|
223
232
|
// Clear this page's resource_ref_edges unconditionally (no non-empty
|
|
224
233
|
// guard, unlike anchors/images above): the crawler always emits this
|
|
@@ -279,14 +288,27 @@ async function readSourceByUrl(trx, caches, url) {
|
|
|
279
288
|
* @param page - The scraped page payload.
|
|
280
289
|
* @param anchorLineageSource - Lineage label for anchor-target
|
|
281
290
|
* placeholder rows (see the caller's lineage comment).
|
|
291
|
+
* @param recursive - The crawl session's `recursive` option. Combined with
|
|
292
|
+
* each anchor's `isExternal` (already resolved by `processAnchors` before
|
|
293
|
+
* this ever runs) to compute `is_metadata_only` the same way
|
|
294
|
+
* `processAnchors` (`handle-scrape-end.ts`) decides whether to queue the
|
|
295
|
+
* anchor for a full or metadata-only scrape: `!recursive || isExternal`.
|
|
296
|
+
* Both `recursive` and a given URL's `isExternal` are constant for the
|
|
297
|
+
* whole crawl session, so this is a pure recomputation, not a second
|
|
298
|
+
* source of truth (#369).
|
|
282
299
|
*/
|
|
283
|
-
async function replaceAnchorEdges(trx, caches, pageId, page, anchorLineageSource) {
|
|
300
|
+
async function replaceAnchorEdges(trx, caches, pageId, page, anchorLineageSource, recursive) {
|
|
284
301
|
if (page.anchorList.length === 0) {
|
|
285
302
|
return;
|
|
286
303
|
}
|
|
287
304
|
const edges = new Map();
|
|
288
305
|
for (const anchor of page.anchorList) {
|
|
289
|
-
const
|
|
306
|
+
const isExternal = anchor.isExternal ?? false;
|
|
307
|
+
const hrefId = await resolveContentItemId(trx, caches, anchor.href.withoutHashAndAuth, {
|
|
308
|
+
isExternal: isExternal ? 1 : 0,
|
|
309
|
+
source: anchorLineageSource,
|
|
310
|
+
isMetadataOnly: !recursive || isExternal ? 1 : 0,
|
|
311
|
+
});
|
|
290
312
|
const existing = edges.get(hrefId);
|
|
291
313
|
if (existing !== undefined) {
|
|
292
314
|
existing.count += 1;
|
|
@@ -5,12 +5,9 @@ import type { Knex } from 'knex';
|
|
|
5
5
|
* two tables and returns the resolved strings.
|
|
6
6
|
*
|
|
7
7
|
* Read in `resource_items.id`-keyset chunks rather than a single SELECT
|
|
8
|
-
* (issue #294)
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* identical to the previous single-SELECT read — chunking exists purely to
|
|
12
|
-
* make the scan observable, not to bound memory (the flat URL list is
|
|
13
|
-
* already fully materialised for the caller either way).
|
|
8
|
+
* (issue #294) via `keysetPaginateById` — see that function's JSDoc for
|
|
9
|
+
* why (progress observability on a resource-heavy archive, not memory
|
|
10
|
+
* bounding).
|
|
14
11
|
* @param knex - Knex query builder connected to the archive DB.
|
|
15
12
|
* @param onProgress - Called after each chunk with the `resource_items.id`
|
|
16
13
|
* scanned up to so far and the max id. Omit for no reporting (the
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { keysetPaginateById } from '../_shared/keyset-paginate-by-id.js';
|
|
1
2
|
/** Rows read per `resource_items.id` keyset chunk. */
|
|
2
3
|
const READ_CHUNK_SIZE = 2000;
|
|
3
4
|
/**
|
|
@@ -6,12 +7,9 @@ const READ_CHUNK_SIZE = 2000;
|
|
|
6
7
|
* two tables and returns the resolved strings.
|
|
7
8
|
*
|
|
8
9
|
* Read in `resource_items.id`-keyset chunks rather than a single SELECT
|
|
9
|
-
* (issue #294)
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* identical to the previous single-SELECT read — chunking exists purely to
|
|
13
|
-
* make the scan observable, not to bound memory (the flat URL list is
|
|
14
|
-
* already fully materialised for the caller either way).
|
|
10
|
+
* (issue #294) via `keysetPaginateById` — see that function's JSDoc for
|
|
11
|
+
* why (progress observability on a resource-heavy archive, not memory
|
|
12
|
+
* bounding).
|
|
15
13
|
* @param knex - Knex query builder connected to the archive DB.
|
|
16
14
|
* @param onProgress - Called after each chunk with the `resource_items.id`
|
|
17
15
|
* scanned up to so far and the max id. Omit for no reporting (the
|
|
@@ -19,33 +17,10 @@ const READ_CHUNK_SIZE = 2000;
|
|
|
19
17
|
* @returns An array of resource URL strings.
|
|
20
18
|
*/
|
|
21
19
|
export async function getResourceUrlList(knex, onProgress) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
});
|
|
29
|
-
maxId = maxRow?.max ?? 0;
|
|
30
|
-
}
|
|
31
|
-
const urls = [];
|
|
32
|
-
let lastId = 0;
|
|
33
|
-
for (;;) {
|
|
34
|
-
const rows = (await knex('resource_items')
|
|
35
|
-
.join('url_refs', 'url_refs.id', 'resource_items.url_id')
|
|
36
|
-
.where('resource_items.id', '>', lastId)
|
|
37
|
-
.orderBy('resource_items.id', 'asc')
|
|
38
|
-
.limit(READ_CHUNK_SIZE)
|
|
39
|
-
.select('resource_items.id as id', 'url_refs.url as url'));
|
|
40
|
-
if (rows.length === 0) {
|
|
41
|
-
onProgress?.(maxId, maxId);
|
|
42
|
-
break;
|
|
43
|
-
}
|
|
44
|
-
lastId = rows.at(-1).id;
|
|
45
|
-
for (const row of rows) {
|
|
46
|
-
urls.push(row.url);
|
|
47
|
-
}
|
|
48
|
-
onProgress?.(Math.min(lastId, maxId), maxId);
|
|
49
|
-
}
|
|
50
|
-
return urls;
|
|
20
|
+
return keysetPaginateById(knex, 'resource_items', (lastId) => knex('resource_items')
|
|
21
|
+
.join('url_refs', 'url_refs.id', 'resource_items.url_id')
|
|
22
|
+
.where('resource_items.id', '>', lastId)
|
|
23
|
+
.orderBy('resource_items.id', 'asc')
|
|
24
|
+
.limit(READ_CHUNK_SIZE)
|
|
25
|
+
.select('resource_items.id as id', 'url_refs.url as url'), (row) => row.url, onProgress);
|
|
51
26
|
}
|
|
@@ -30,7 +30,7 @@ export declare function applyConnectionPragmas(instance: Knex): Promise<void>;
|
|
|
30
30
|
* writes during a crawl and every reader queries. Must run AFTER
|
|
31
31
|
* `createRefTables` because most entity tables reference ref-table PKs.
|
|
32
32
|
* - **Adjunct tables** ({@link createAdjunctTables}): `page_errors`,
|
|
33
|
-
* `crawl_errors`, `technology_signals`, `page_technologies`, `page_jsonld`, `
|
|
33
|
+
* `crawl_errors`, `technology_signals`, `page_technologies`, `page_jsonld`, `list_reconcile_runs`,
|
|
34
34
|
* `analysis_text_refs` + `analysis_violations`, `page_html_blobs` +
|
|
35
35
|
* `page_html_ref`. Must run AFTER `createEntityTables` because the
|
|
36
36
|
* page-scoped tables FK into `content_items(id)`.
|
|
@@ -55,7 +55,7 @@ export async function applyConnectionPragmas(instance) {
|
|
|
55
55
|
* writes during a crawl and every reader queries. Must run AFTER
|
|
56
56
|
* `createRefTables` because most entity tables reference ref-table PKs.
|
|
57
57
|
* - **Adjunct tables** ({@link createAdjunctTables}): `page_errors`,
|
|
58
|
-
* `crawl_errors`, `technology_signals`, `page_technologies`, `page_jsonld`, `
|
|
58
|
+
* `crawl_errors`, `technology_signals`, `page_technologies`, `page_jsonld`, `list_reconcile_runs`,
|
|
59
59
|
* `analysis_text_refs` + `analysis_violations`, `page_html_blobs` +
|
|
60
60
|
* `page_html_ref`. Must run AFTER `createEntityTables` because the
|
|
61
61
|
* page-scoped tables FK into `content_items(id)`.
|
|
@@ -113,6 +113,7 @@ export async function initSchema(instance) {
|
|
|
113
113
|
t.string('userAgent');
|
|
114
114
|
t.boolean('ignoreRobots');
|
|
115
115
|
t.string('mainContentSelector');
|
|
116
|
+
t.string('createdCwd');
|
|
116
117
|
});
|
|
117
118
|
}
|
|
118
119
|
// 0.13 ref / header dictionary tables. DDL + column-level rationale
|
|
@@ -135,7 +136,7 @@ export async function initSchema(instance) {
|
|
|
135
136
|
// Adjunct tables that FK into `content_items` (page_errors /
|
|
136
137
|
// technology_signals / page_technologies / page_jsonld / analysis_* /
|
|
137
138
|
// page_html_*) plus the standalone log tables
|
|
138
|
-
// (crawl_errors /
|
|
139
|
+
// (crawl_errors / list_reconcile_runs). MUST run after
|
|
139
140
|
// {@link createEntityTables} so the FK targets exist. DDL +
|
|
140
141
|
// column-level rationale lives in {@link createAdjunctTables}, which is
|
|
141
142
|
// shared with `scripts/migrate-to-0.13.mjs` — a divergence between the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MainContentsDenormalizedColumns } from './types.js';
|
|
1
|
+
import type { ImageScanColumns, MainContentsDenormalizedColumns } from './types.js';
|
|
2
2
|
import type { MainContentsData, ScrollHeightData } from '@d-zero/beholder';
|
|
3
3
|
/**
|
|
4
4
|
* Computes the `page_meta.main_content_*` / `scroll_height_*` denormalised
|
|
@@ -23,11 +23,19 @@ import type { MainContentsData, ScrollHeightData } from '@d-zero/beholder';
|
|
|
23
23
|
* `undefined`/capture-not-attempted and capture-failure both collapse to
|
|
24
24
|
* `null` (unknown — NOT the same as "captured, zero found"), while any
|
|
25
25
|
* number (including `0`) means capture succeeded.
|
|
26
|
+
* `imageScan` follows the same independent-of-`mainContents` shape as
|
|
27
|
+
* `scrollHeight` (both are per-viewport beholder scan outcomes, not derived
|
|
28
|
+
* from the main-content region) — it is typed as a plain `number | null`
|
|
29
|
+
* pair here rather than beholder's `ImageScanCode` so this function does not
|
|
30
|
+
* require a `@d-zero/beholder` version that exports it; the caller narrows.
|
|
26
31
|
* @param mainContents - Beholder's per-page main-content metrics, or `null`/`undefined`.
|
|
27
32
|
* @param scrollHeight - Beholder's per-page scroll-height measurements, or `null`/`undefined`.
|
|
28
33
|
* @param customElementCount - Count of Web Components nitpicker captured in
|
|
29
34
|
* the main-content region, or `null`/`undefined` when capture was not
|
|
30
35
|
* attempted or failed.
|
|
31
|
-
* @
|
|
36
|
+
* @param imageScan - Beholder's per-viewport `<img>` scan outcome codes, or
|
|
37
|
+
* `null`/`undefined` when not attempted (page not fully rendered, or the
|
|
38
|
+
* scraper version does not report it yet).
|
|
39
|
+
* @returns The twenty denormalised columns.
|
|
32
40
|
*/
|
|
33
|
-
export declare function computeMainContentsDenormalized(mainContents: MainContentsData | null | undefined, scrollHeight: ScrollHeightData | null | undefined, customElementCount?: number | null): MainContentsDenormalizedColumns;
|
|
41
|
+
export declare function computeMainContentsDenormalized(mainContents: MainContentsData | null | undefined, scrollHeight: ScrollHeightData | null | undefined, customElementCount?: number | null, imageScan?: ImageScanColumns | null): MainContentsDenormalizedColumns;
|
|
@@ -21,14 +21,22 @@
|
|
|
21
21
|
* `undefined`/capture-not-attempted and capture-failure both collapse to
|
|
22
22
|
* `null` (unknown — NOT the same as "captured, zero found"), while any
|
|
23
23
|
* number (including `0`) means capture succeeded.
|
|
24
|
+
* `imageScan` follows the same independent-of-`mainContents` shape as
|
|
25
|
+
* `scrollHeight` (both are per-viewport beholder scan outcomes, not derived
|
|
26
|
+
* from the main-content region) — it is typed as a plain `number | null`
|
|
27
|
+
* pair here rather than beholder's `ImageScanCode` so this function does not
|
|
28
|
+
* require a `@d-zero/beholder` version that exports it; the caller narrows.
|
|
24
29
|
* @param mainContents - Beholder's per-page main-content metrics, or `null`/`undefined`.
|
|
25
30
|
* @param scrollHeight - Beholder's per-page scroll-height measurements, or `null`/`undefined`.
|
|
26
31
|
* @param customElementCount - Count of Web Components nitpicker captured in
|
|
27
32
|
* the main-content region, or `null`/`undefined` when capture was not
|
|
28
33
|
* attempted or failed.
|
|
29
|
-
* @
|
|
34
|
+
* @param imageScan - Beholder's per-viewport `<img>` scan outcome codes, or
|
|
35
|
+
* `null`/`undefined` when not attempted (page not fully rendered, or the
|
|
36
|
+
* scraper version does not report it yet).
|
|
37
|
+
* @returns The twenty denormalised columns.
|
|
30
38
|
*/
|
|
31
|
-
export function computeMainContentsDenormalized(mainContents, scrollHeight, customElementCount) {
|
|
39
|
+
export function computeMainContentsDenormalized(mainContents, scrollHeight, customElementCount, imageScan) {
|
|
32
40
|
if (mainContents == null) {
|
|
33
41
|
return {
|
|
34
42
|
main_content_node_name: null,
|
|
@@ -49,6 +57,8 @@ export function computeMainContentsDenormalized(mainContents, scrollHeight, cust
|
|
|
49
57
|
main_content_custom_element_count: null,
|
|
50
58
|
scroll_height_desktop: null,
|
|
51
59
|
scroll_height_mobile: null,
|
|
60
|
+
image_scan_desktop: null,
|
|
61
|
+
image_scan_mobile: null,
|
|
52
62
|
};
|
|
53
63
|
}
|
|
54
64
|
return {
|
|
@@ -72,5 +82,7 @@ export function computeMainContentsDenormalized(mainContents, scrollHeight, cust
|
|
|
72
82
|
main_content_custom_element_count: customElementCount ?? null,
|
|
73
83
|
scroll_height_desktop: scrollHeight?.desktop ?? null,
|
|
74
84
|
scroll_height_mobile: scrollHeight?.mobile ?? null,
|
|
85
|
+
image_scan_desktop: imageScan?.desktop ?? null,
|
|
86
|
+
image_scan_mobile: imageScan?.mobile ?? null,
|
|
75
87
|
};
|
|
76
88
|
}
|