@nitpicker/crawler 0.9.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/archive/archive-accessor.d.ts +87 -14
- package/lib/archive/archive-accessor.js +162 -36
- package/lib/archive/archive.d.ts +262 -24
- package/lib/archive/archive.js +397 -86
- package/lib/archive/cache/compute-archive-cache-key.d.ts +39 -0
- package/lib/archive/cache/compute-archive-cache-key.js +95 -0
- package/lib/archive/cache/extract-archive-to-cache.d.ts +43 -0
- package/lib/archive/cache/extract-archive-to-cache.js +309 -0
- package/lib/archive/cache/get-archive-cache-root.d.ts +20 -0
- package/lib/archive/cache/get-archive-cache-root.js +53 -0
- package/lib/archive/cache/is-archive-cache-disabled.d.ts +24 -0
- package/lib/archive/cache/is-archive-cache-disabled.js +34 -0
- package/lib/archive/cache/resolve-archive-cache-dir.d.ts +26 -0
- package/lib/archive/cache/resolve-archive-cache-dir.js +32 -0
- package/lib/archive/database.d.ts +411 -26
- package/lib/archive/database.js +1881 -540
- package/lib/archive/derive-lineage-from-parent.d.ts +37 -0
- package/lib/archive/derive-lineage-from-parent.js +42 -0
- package/lib/archive/filesystem/peek-tar-top-dir.d.ts +28 -0
- package/lib/archive/filesystem/peek-tar-top-dir.js +65 -0
- package/lib/archive/get-failed-page-messages.d.ts +43 -0
- package/lib/archive/get-failed-page-messages.js +131 -0
- package/lib/archive/init-schema.d.ts +53 -2
- package/lib/archive/init-schema.js +399 -15
- package/lib/archive/is-inventory-source.d.ts +21 -0
- package/lib/archive/is-inventory-source.js +22 -0
- package/lib/archive/meta/assert-compatible-version.d.ts +39 -0
- package/lib/archive/meta/assert-compatible-version.js +72 -0
- package/lib/archive/meta/classify-jsonld-type.d.ts +23 -0
- package/lib/archive/meta/classify-jsonld-type.js +43 -0
- package/lib/archive/meta/compare-semver.d.ts +23 -0
- package/lib/archive/meta/compare-semver.js +51 -0
- package/lib/archive/meta/compute-page-denormalized.d.ts +21 -0
- package/lib/archive/meta/compute-page-denormalized.js +35 -0
- package/lib/archive/meta/derive-flat-from-meta.d.ts +35 -0
- package/lib/archive/meta/derive-flat-from-meta.js +158 -0
- package/lib/archive/meta/derive-meta-extras.d.ts +20 -0
- package/lib/archive/meta/derive-meta-extras.js +23 -0
- package/lib/archive/meta/extract-tags-for-archive.d.ts +18 -0
- package/lib/archive/meta/extract-tags-for-archive.js +36 -0
- package/lib/archive/meta/summarize-jsonld.d.ts +17 -0
- package/lib/archive/meta/summarize-jsonld.js +29 -0
- package/lib/archive/meta/summarize-tags.d.ts +16 -0
- package/lib/archive/meta/summarize-tags.js +33 -0
- package/lib/archive/meta/types.d.ts +207 -0
- package/lib/archive/meta/types.js +33 -0
- package/lib/archive/migrate-crawl-errors.d.ts +20 -0
- package/lib/archive/migrate-crawl-errors.js +38 -0
- package/lib/archive/migrate-html-blob-tables.d.ts +24 -0
- package/lib/archive/migrate-html-blob-tables.js +53 -0
- package/lib/archive/migrate-inventory-runs.d.ts +29 -0
- package/lib/archive/migrate-inventory-runs.js +52 -0
- package/lib/archive/migrate-page-errors.d.ts +16 -0
- package/lib/archive/migrate-page-errors.js +35 -0
- package/lib/archive/migrate-pages-resources-source.d.ts +16 -0
- package/lib/archive/migrate-pages-resources-source.js +46 -0
- package/lib/archive/page.d.ts +187 -49
- package/lib/archive/page.js +258 -63
- package/lib/archive/peek-archive-lock.d.ts +40 -0
- package/lib/archive/peek-archive-lock.js +62 -0
- package/lib/archive/resolve-redirect-chain.d.ts +33 -0
- package/lib/archive/resolve-redirect-chain.js +27 -0
- package/lib/archive/types.d.ts +168 -26
- package/lib/classify-error-kind.d.ts +19 -0
- package/lib/classify-error-kind.js +122 -0
- package/lib/crawler/build-js-redirect-edge.d.ts +68 -0
- package/lib/crawler/build-js-redirect-edge.js +57 -0
- package/lib/crawler/build-redirect-event.d.ts +24 -0
- package/lib/crawler/build-redirect-event.js +28 -0
- package/lib/crawler/clear-dns-burned-host-cache.d.ts +6 -0
- package/lib/crawler/clear-dns-burned-host-cache.js +11 -0
- package/lib/crawler/close-browser-safely.d.ts +64 -0
- package/lib/crawler/close-browser-safely.js +73 -0
- package/lib/crawler/crawler.d.ts +7 -2
- package/lib/crawler/crawler.js +919 -113
- package/lib/crawler/create-change-phase-handler.d.ts +54 -0
- package/lib/crawler/create-change-phase-handler.js +44 -0
- package/lib/crawler/derive-js-redirect-target.d.ts +68 -0
- package/lib/crawler/derive-js-redirect-target.js +129 -0
- package/lib/crawler/derive-page-source.d.ts +23 -0
- package/lib/crawler/derive-page-source.js +28 -0
- package/lib/crawler/derive-resource-source.d.ts +33 -0
- package/lib/crawler/derive-resource-source.js +37 -0
- package/lib/crawler/dns-burned-host-cache.d.ts +26 -0
- package/lib/crawler/dns-burned-host-cache.js +25 -0
- package/lib/crawler/dns-burned-host-short-circuit-counter.d.ts +13 -0
- package/lib/crawler/dns-burned-host-short-circuit-counter.js +11 -0
- package/lib/crawler/drain-phase-errors.d.ts +48 -0
- package/lib/crawler/drain-phase-errors.js +35 -0
- package/lib/crawler/fetch-destination.d.ts +12 -4
- package/lib/crawler/fetch-destination.js +132 -18
- package/lib/crawler/format-crawl-progress.d.ts +12 -3
- package/lib/crawler/format-crawl-progress.js +14 -6
- package/lib/crawler/handle-browser-close.d.ts +29 -0
- package/lib/crawler/handle-browser-close.js +28 -0
- package/lib/crawler/is-html-content-type.d.ts +17 -0
- package/lib/crawler/is-html-content-type.js +19 -0
- package/lib/crawler/is-js-redirect-error-shape.d.ts +40 -0
- package/lib/crawler/is-js-redirect-error-shape.js +53 -0
- package/lib/crawler/is-likely-html-url.d.ts +22 -0
- package/lib/crawler/is-likely-html-url.js +65 -0
- package/lib/crawler/is-puppeteer-fallback-candidate.d.ts +16 -0
- package/lib/crawler/is-puppeteer-fallback-candidate.js +63 -0
- package/lib/crawler/kill-process-tree.d.ts +94 -0
- package/lib/crawler/kill-process-tree.js +178 -0
- package/lib/crawler/link-list.d.ts +21 -1
- package/lib/crawler/link-list.js +25 -4
- package/lib/crawler/link-to-page-data.d.ts +13 -5
- package/lib/crawler/link-to-page-data.js +26 -5
- package/lib/crawler/log-undrained-phase-errors.d.ts +37 -0
- package/lib/crawler/log-undrained-phase-errors.js +34 -0
- package/lib/crawler/normalize-content-type.d.ts +14 -0
- package/lib/crawler/normalize-content-type.js +20 -0
- package/lib/crawler/partition-urls-by-html.d.ts +16 -0
- package/lib/crawler/partition-urls-by-html.js +23 -0
- package/lib/crawler/plan-sub-resource-emits.d.ts +63 -0
- package/lib/crawler/plan-sub-resource-emits.js +44 -0
- package/lib/crawler/preload-short-circuit-error.d.ts +22 -0
- package/lib/crawler/preload-short-circuit-error.js +25 -0
- package/lib/crawler/redirect-dest-key.d.ts +19 -0
- package/lib/crawler/redirect-dest-key.js +27 -0
- package/lib/crawler/resource-to-page-data.d.ts +28 -0
- package/lib/crawler/resource-to-page-data.js +59 -0
- package/lib/crawler/should-burn-host.d.ts +78 -0
- package/lib/crawler/should-burn-host.js +61 -0
- package/lib/crawler/should-get-fallback-on-head-failure.d.ts +38 -0
- package/lib/crawler/should-get-fallback-on-head-failure.js +46 -0
- package/lib/crawler/types.d.ts +229 -1
- package/lib/crawler-orchestrator.d.ts +105 -3
- package/lib/crawler-orchestrator.js +620 -20
- package/lib/crawler.d.ts +8 -2
- package/lib/crawler.js +6 -1
- package/lib/permanent-error-kinds.d.ts +43 -0
- package/lib/permanent-error-kinds.js +48 -0
- package/lib/resource-row-to-lookup-result.d.ts +13 -0
- package/lib/resource-row-to-lookup-result.js +20 -0
- package/lib/types.d.ts +95 -1
- package/lib/utils/compute-file-sha256.d.ts +23 -0
- package/lib/utils/compute-file-sha256.js +55 -0
- package/lib/utils/error/emit-error-with-retry.d.ts +40 -0
- package/lib/utils/error/emit-error-with-retry.js +44 -0
- package/lib/utils/error/emit-error.d.ts +39 -0
- package/lib/utils/error/emit-error.js +41 -0
- package/lib/utils/object/parse-response-headers.d.ts +12 -0
- package/lib/utils/object/parse-response-headers.js +26 -0
- package/package.json +11 -11
- package/lib/utils/error/error-emitter.d.ts +0 -18
- package/lib/utils/error/error-emitter.js +0 -29
|
@@ -4,15 +4,13 @@ import type { ParseURLOptions } from '@d-zero/shared/parse-url';
|
|
|
4
4
|
import { TypedAwaitEventEmitter as EventEmitter } from '@d-zero/shared/typed-await-event-emitter';
|
|
5
5
|
import Page from './page.js';
|
|
6
6
|
import Resource from './resource.js';
|
|
7
|
-
/**
|
|
8
|
-
* Provides read-only access to an archive's database and stored data files.
|
|
9
|
-
*
|
|
10
|
-
* This class is the base for the `Archive` class and is also returned
|
|
11
|
-
* by `Archive.connect` for read-only access to an existing archive.
|
|
12
|
-
* It supports querying pages, anchors, referrers, resources, and custom data.
|
|
13
|
-
*/
|
|
14
7
|
export declare class ArchiveAccessor extends EventEmitter<DatabaseEvent> {
|
|
15
8
|
#private;
|
|
9
|
+
/**
|
|
10
|
+
* Whether this accessor was opened in read-only mode (no filesystem
|
|
11
|
+
* mutation on `tmpDir`).
|
|
12
|
+
*/
|
|
13
|
+
get readOnly(): boolean;
|
|
16
14
|
/**
|
|
17
15
|
* The absolute path to the temporary working directory used by this accessor.
|
|
18
16
|
*/
|
|
@@ -23,8 +21,47 @@ export declare class ArchiveAccessor extends EventEmitter<DatabaseEvent> {
|
|
|
23
21
|
* @param db - The Database instance for querying the SQLite database.
|
|
24
22
|
* @param namespace - An optional namespace for scoping custom data storage.
|
|
25
23
|
* When null, `setData` is not available.
|
|
24
|
+
* @param options - Construction options.
|
|
25
|
+
* @param options.readOnly - When `true`, helpers must not mutate the
|
|
26
|
+
* filesystem under `tmpDir` (used for live-crawl / stub-mode opens
|
|
27
|
+
* where any write would race the crawler).
|
|
26
28
|
*/
|
|
27
|
-
constructor(tmpDir: string, db: Database, namespace?: string | null
|
|
29
|
+
constructor(tmpDir: string, db: Database, namespace?: string | null, options?: {
|
|
30
|
+
readOnly?: boolean;
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* Closes the underlying database connection.
|
|
34
|
+
*
|
|
35
|
+
* This is the **read-only** close path: it releases the SQLite handle and
|
|
36
|
+
* does nothing else. The temporary working directory is left untouched and
|
|
37
|
+
* no `.nitpicker` archive is produced. This makes it safe to call from
|
|
38
|
+
* read-only consumers (e.g. the viewer attached to an in-progress crawl's
|
|
39
|
+
* tmpDir), where touching the filesystem would race with — or destroy —
|
|
40
|
+
* the live crawler's working state.
|
|
41
|
+
*
|
|
42
|
+
* Subclasses that own the archive's lifecycle (notably {@link Archive})
|
|
43
|
+
* override this to add write/cleanup steps.
|
|
44
|
+
*
|
|
45
|
+
* **Idempotent and concurrent-safe**: the first invocation captures the
|
|
46
|
+
* close promise; later invocations (from the same caller, a shutdown
|
|
47
|
+
* signal handler, or a parallel manager teardown) await the same
|
|
48
|
+
* promise and resolve together. If `db.destroy()` rejects, the
|
|
49
|
+
* rejection propagates to *all* awaiters and the accessor stays
|
|
50
|
+
* latched closed — a hung knex pool is not safe to "retry close".
|
|
51
|
+
*
|
|
52
|
+
* **Bounded**: when the optional `timeoutMs` (default {@link
|
|
53
|
+
* DEFAULT_CLOSE_TIMEOUT_MS}) elapses before `db.destroy()` settles, the
|
|
54
|
+
* call resolves with a warning. This prevents a viewer shutdown from
|
|
55
|
+
* being held for the underlying pool's 10-minute `acquireTimeoutMillis`
|
|
56
|
+
* when the live crawler holds the SQLite write lock.
|
|
57
|
+
* @param options - Close options.
|
|
58
|
+
* @param options.timeoutMs - Milliseconds to wait for `db.destroy()`
|
|
59
|
+
* before giving up. Use `Infinity` to wait indefinitely (only
|
|
60
|
+
* advisable in tests and batch jobs that own the DB exclusively).
|
|
61
|
+
*/
|
|
62
|
+
close(options?: {
|
|
63
|
+
timeoutMs?: number;
|
|
64
|
+
}): Promise<void>;
|
|
28
65
|
/**
|
|
29
66
|
* Retrieves anchor (link) data for a specific page by its database ID.
|
|
30
67
|
* @param pageId - The database ID of the page whose anchors to retrieve.
|
|
@@ -51,13 +88,42 @@ export declare class ArchiveAccessor extends EventEmitter<DatabaseEvent> {
|
|
|
51
88
|
*/
|
|
52
89
|
getData(name: string, format?: 'txt' | 'html'): Promise<string>;
|
|
53
90
|
/**
|
|
54
|
-
* Reads the HTML
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
91
|
+
* Reads the HTML snapshot of a page from the archive.
|
|
92
|
+
*
|
|
93
|
+
* HTML is stored as zstd-compressed BLOBs in `page_html_blobs` (keyed by
|
|
94
|
+
* SHA-256 of the raw bytes) with `page_html_ref` linking `page_id → hash`.
|
|
95
|
+
* The read is a straight join + decompress; writer- and read-only (stub)
|
|
96
|
+
* accessors take the same code path because nothing here touches the
|
|
97
|
+
* filesystem.
|
|
98
|
+
*
|
|
99
|
+
* Returns `null` when the page row exists but has no stored body — for
|
|
100
|
+
* example a redirect source, a non-HTML resource (PDF), a page that
|
|
101
|
+
* failed to render, or an external page (whose row is metadata-only).
|
|
102
|
+
* Distinguishing "no body stored" from "empty body" is preserved: an
|
|
103
|
+
* empty HTML string returns `''`, not `null`.
|
|
104
|
+
*
|
|
105
|
+
* Throws if the page's referenced blob is missing or the codec marker
|
|
106
|
+
* is unrecognised — both indicate an archive that was truncated or
|
|
107
|
+
* written by a future tool, neither of which we silently paper over.
|
|
108
|
+
* @param pageId - The database id of the page to read.
|
|
109
|
+
* @returns The HTML content as a UTF-8 string, or `null` when no body
|
|
110
|
+
* is stored for `pageId`.
|
|
111
|
+
* @example
|
|
112
|
+
* const html = await accessor.getHtmlOfPage(pageId);
|
|
113
|
+
* if (html === null) {
|
|
114
|
+
* // page has no stored body — redirect source / non-HTML / failed render
|
|
115
|
+
* } else {
|
|
116
|
+
* processHtml(html);
|
|
117
|
+
* }
|
|
59
118
|
*/
|
|
60
|
-
getHtmlOfPage(
|
|
119
|
+
getHtmlOfPage(pageId: number): Promise<string | null>;
|
|
120
|
+
/**
|
|
121
|
+
* Retrieves the JSON-LD / SpeculationRules entries for the given page,
|
|
122
|
+
* parsed back from the `page_jsonld` table.
|
|
123
|
+
* @param pageId - The database id of the page.
|
|
124
|
+
* @returns Ordered entries with `kind`, `type`, `raw`, `parsed`, `parseError`.
|
|
125
|
+
*/
|
|
126
|
+
getJsonLdOfPage(pageId: number): Promise<import("./meta/types.js").JsonLdRow[]>;
|
|
61
127
|
/**
|
|
62
128
|
* Returns the underlying Knex query builder instance for direct SQL access.
|
|
63
129
|
* Enables advanced queries (GROUP BY, HAVING, JOINs) at the database layer
|
|
@@ -106,6 +172,13 @@ export declare class ArchiveAccessor extends EventEmitter<DatabaseEvent> {
|
|
|
106
172
|
* @returns An array of resource URL strings.
|
|
107
173
|
*/
|
|
108
174
|
getResourceUrlList(): Promise<any[]>;
|
|
175
|
+
/**
|
|
176
|
+
* Retrieves the Wappalyzer tag entries for the given page, parsed back
|
|
177
|
+
* from the `page_tags` table.
|
|
178
|
+
* @param pageId - The database id of the page.
|
|
179
|
+
* @returns Ordered entries with provider, category, externalId, etc.
|
|
180
|
+
*/
|
|
181
|
+
getTagsOfPage(pageId: number): Promise<import("./meta/types.js").TagRow[]>;
|
|
109
182
|
/**
|
|
110
183
|
* Stores custom data in the archive under the configured namespace.
|
|
111
184
|
* Requires a namespace to be set on this accessor; throws if namespace is null.
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { extractZip, unzip } from '@d-zero/fs/zip';
|
|
3
2
|
import { TypedAwaitEventEmitter as EventEmitter } from '@d-zero/shared/typed-await-event-emitter';
|
|
4
3
|
import { log } from './debug.js';
|
|
5
|
-
import { exists } from './filesystem/exists.js';
|
|
6
4
|
import { outputJSON } from './filesystem/output-json.js';
|
|
7
5
|
import { outputText } from './filesystem/output-text.js';
|
|
8
6
|
import { readJSON } from './filesystem/read-json.js';
|
|
@@ -17,13 +15,45 @@ import { safePath } from './safe-path.js';
|
|
|
17
15
|
* by `Archive.connect` for read-only access to an existing archive.
|
|
18
16
|
* It supports querying pages, anchors, referrers, resources, and custom data.
|
|
19
17
|
*/
|
|
18
|
+
/**
|
|
19
|
+
* Default timeout for {@link ArchiveAccessor.close}'s `db.destroy()` step.
|
|
20
|
+
*
|
|
21
|
+
* `knex.destroy()` will otherwise wait the full `acquireTimeoutMillis`
|
|
22
|
+
* (10 minutes in this repo) for in-flight queries to drain. For a viewer
|
|
23
|
+
* shut down by Ctrl-C while the live crawler holds a long write lock that
|
|
24
|
+
* is an unacceptable user experience, so we bound the wait and treat the
|
|
25
|
+
* accessor as closed after the timeout regardless.
|
|
26
|
+
*/
|
|
27
|
+
const DEFAULT_CLOSE_TIMEOUT_MS = 5000;
|
|
20
28
|
export class ArchiveAccessor extends EventEmitter {
|
|
29
|
+
/**
|
|
30
|
+
* Promise tracking an in-progress (or completed) close. `null` means the
|
|
31
|
+
* accessor is open and idle; a settled promise means we are closed (the
|
|
32
|
+
* accessor stays "closed" even if `db.destroy()` rejected, because there
|
|
33
|
+
* is nothing safe to retry — see {@link close}).
|
|
34
|
+
*/
|
|
35
|
+
#closeOnce = null;
|
|
21
36
|
/** The SQLite database instance for querying archived data. */
|
|
22
37
|
#db;
|
|
23
38
|
/** Namespace prefix for custom data storage (e.g. `"analysis/plugin-name"`). `null` disables `setData`. */
|
|
24
39
|
#namespace = null;
|
|
40
|
+
/**
|
|
41
|
+
* Whether this accessor was opened in read-only mode. With HTML stored
|
|
42
|
+
* as a SQLite BLOB, this no longer toggles any code path — the SELECT
|
|
43
|
+
* is identical for writer- and reader-mode accessors. Kept on the
|
|
44
|
+
* accessor so callers like the viewer can still surface "this archive
|
|
45
|
+
* is being read read-only" UI hints without re-deriving it.
|
|
46
|
+
*/
|
|
47
|
+
#readOnly;
|
|
25
48
|
/** Absolute path to the temporary working directory containing the database and files. */
|
|
26
49
|
#tmpDir;
|
|
50
|
+
/**
|
|
51
|
+
* Whether this accessor was opened in read-only mode (no filesystem
|
|
52
|
+
* mutation on `tmpDir`).
|
|
53
|
+
*/
|
|
54
|
+
get readOnly() {
|
|
55
|
+
return this.#readOnly;
|
|
56
|
+
}
|
|
27
57
|
/**
|
|
28
58
|
* The absolute path to the temporary working directory used by this accessor.
|
|
29
59
|
*/
|
|
@@ -36,16 +66,59 @@ export class ArchiveAccessor extends EventEmitter {
|
|
|
36
66
|
* @param db - The Database instance for querying the SQLite database.
|
|
37
67
|
* @param namespace - An optional namespace for scoping custom data storage.
|
|
38
68
|
* When null, `setData` is not available.
|
|
69
|
+
* @param options - Construction options.
|
|
70
|
+
* @param options.readOnly - When `true`, helpers must not mutate the
|
|
71
|
+
* filesystem under `tmpDir` (used for live-crawl / stub-mode opens
|
|
72
|
+
* where any write would race the crawler).
|
|
39
73
|
*/
|
|
40
|
-
constructor(tmpDir, db, namespace = null) {
|
|
74
|
+
constructor(tmpDir, db, namespace = null, options = {}) {
|
|
41
75
|
super();
|
|
42
76
|
this.#tmpDir = tmpDir;
|
|
43
77
|
this.#db = db;
|
|
44
78
|
this.#namespace = namespace;
|
|
79
|
+
this.#readOnly = options.readOnly ?? false;
|
|
45
80
|
this.#db.on('error', (e) => {
|
|
46
81
|
void this.emit('error', e);
|
|
47
82
|
});
|
|
48
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Closes the underlying database connection.
|
|
86
|
+
*
|
|
87
|
+
* This is the **read-only** close path: it releases the SQLite handle and
|
|
88
|
+
* does nothing else. The temporary working directory is left untouched and
|
|
89
|
+
* no `.nitpicker` archive is produced. This makes it safe to call from
|
|
90
|
+
* read-only consumers (e.g. the viewer attached to an in-progress crawl's
|
|
91
|
+
* tmpDir), where touching the filesystem would race with — or destroy —
|
|
92
|
+
* the live crawler's working state.
|
|
93
|
+
*
|
|
94
|
+
* Subclasses that own the archive's lifecycle (notably {@link Archive})
|
|
95
|
+
* override this to add write/cleanup steps.
|
|
96
|
+
*
|
|
97
|
+
* **Idempotent and concurrent-safe**: the first invocation captures the
|
|
98
|
+
* close promise; later invocations (from the same caller, a shutdown
|
|
99
|
+
* signal handler, or a parallel manager teardown) await the same
|
|
100
|
+
* promise and resolve together. If `db.destroy()` rejects, the
|
|
101
|
+
* rejection propagates to *all* awaiters and the accessor stays
|
|
102
|
+
* latched closed — a hung knex pool is not safe to "retry close".
|
|
103
|
+
*
|
|
104
|
+
* **Bounded**: when the optional `timeoutMs` (default {@link
|
|
105
|
+
* DEFAULT_CLOSE_TIMEOUT_MS}) elapses before `db.destroy()` settles, the
|
|
106
|
+
* call resolves with a warning. This prevents a viewer shutdown from
|
|
107
|
+
* being held for the underlying pool's 10-minute `acquireTimeoutMillis`
|
|
108
|
+
* when the live crawler holds the SQLite write lock.
|
|
109
|
+
* @param options - Close options.
|
|
110
|
+
* @param options.timeoutMs - Milliseconds to wait for `db.destroy()`
|
|
111
|
+
* before giving up. Use `Infinity` to wait indefinitely (only
|
|
112
|
+
* advisable in tests and batch jobs that own the DB exclusively).
|
|
113
|
+
*/
|
|
114
|
+
async close(options = {}) {
|
|
115
|
+
if (this.#closeOnce) {
|
|
116
|
+
return this.#closeOnce;
|
|
117
|
+
}
|
|
118
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_CLOSE_TIMEOUT_MS;
|
|
119
|
+
this.#closeOnce = this.#runClose(timeoutMs);
|
|
120
|
+
return this.#closeOnce;
|
|
121
|
+
}
|
|
49
122
|
/**
|
|
50
123
|
* Retrieves anchor (link) data for a specific page by its database ID.
|
|
51
124
|
* @param pageId - The database ID of the page whose anchors to retrieve.
|
|
@@ -71,43 +144,50 @@ export class ArchiveAccessor extends EventEmitter {
|
|
|
71
144
|
return await readText(filePath);
|
|
72
145
|
}
|
|
73
146
|
/**
|
|
74
|
-
* Reads the HTML
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
147
|
+
* Reads the HTML snapshot of a page from the archive.
|
|
148
|
+
*
|
|
149
|
+
* HTML is stored as zstd-compressed BLOBs in `page_html_blobs` (keyed by
|
|
150
|
+
* SHA-256 of the raw bytes) with `page_html_ref` linking `page_id → hash`.
|
|
151
|
+
* The read is a straight join + decompress; writer- and read-only (stub)
|
|
152
|
+
* accessors take the same code path because nothing here touches the
|
|
153
|
+
* filesystem.
|
|
154
|
+
*
|
|
155
|
+
* Returns `null` when the page row exists but has no stored body — for
|
|
156
|
+
* example a redirect source, a non-HTML resource (PDF), a page that
|
|
157
|
+
* failed to render, or an external page (whose row is metadata-only).
|
|
158
|
+
* Distinguishing "no body stored" from "empty body" is preserved: an
|
|
159
|
+
* empty HTML string returns `''`, not `null`.
|
|
160
|
+
*
|
|
161
|
+
* Throws if the page's referenced blob is missing or the codec marker
|
|
162
|
+
* is unrecognised — both indicate an archive that was truncated or
|
|
163
|
+
* written by a future tool, neither of which we silently paper over.
|
|
164
|
+
* @param pageId - The database id of the page to read.
|
|
165
|
+
* @returns The HTML content as a UTF-8 string, or `null` when no body
|
|
166
|
+
* is stored for `pageId`.
|
|
167
|
+
* @example
|
|
168
|
+
* const html = await accessor.getHtmlOfPage(pageId);
|
|
169
|
+
* if (html === null) {
|
|
170
|
+
* // page has no stored body — redirect source / non-HTML / failed render
|
|
171
|
+
* } else {
|
|
172
|
+
* processHtml(html);
|
|
173
|
+
* }
|
|
79
174
|
*/
|
|
80
|
-
async getHtmlOfPage(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const snapshotDir = safePath(this.#tmpDir, path.dirname(filePath));
|
|
85
|
-
const name = path.basename(filePath);
|
|
86
|
-
if (openZipped) {
|
|
87
|
-
await unzip(`${snapshotDir}.zip`, snapshotDir);
|
|
88
|
-
}
|
|
89
|
-
if (exists(snapshotDir)) {
|
|
90
|
-
log('Load %s directly because snapshot dir is unzipped', name);
|
|
91
|
-
const html = await readText(path.resolve(snapshotDir, name)).catch((error) => error);
|
|
92
|
-
if (typeof html === 'string') {
|
|
93
|
-
log('Loaded: %s ...', html.split('\n')[0]);
|
|
94
|
-
return html;
|
|
95
|
-
}
|
|
96
|
-
log('Failed Loading: %O', html);
|
|
97
|
-
return null;
|
|
175
|
+
async getHtmlOfPage(pageId) {
|
|
176
|
+
const html = await this.#db.getHtmlOfPageById(pageId);
|
|
177
|
+
if (html === null) {
|
|
178
|
+
log('No HTML body stored for page id=%d', pageId);
|
|
98
179
|
}
|
|
99
|
-
log('Extracts %s from zipped snapshots', name);
|
|
100
|
-
const zipDir = await extractZip(`${snapshotDir}.zip`);
|
|
101
|
-
const file = zipDir.files.find((f) => f.type === 'File' && f.path === name);
|
|
102
|
-
if (!file) {
|
|
103
|
-
log('Failed: Not found %s from zipped snapshots', name);
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
const buffer = await file.buffer();
|
|
107
|
-
const html = buffer.toString('utf8') || null;
|
|
108
|
-
log('Succeeded: Extracts %s from zipped snapshots', name);
|
|
109
180
|
return html;
|
|
110
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Retrieves the JSON-LD / SpeculationRules entries for the given page,
|
|
184
|
+
* parsed back from the `page_jsonld` table.
|
|
185
|
+
* @param pageId - The database id of the page.
|
|
186
|
+
* @returns Ordered entries with `kind`, `type`, `raw`, `parsed`, `parseError`.
|
|
187
|
+
*/
|
|
188
|
+
async getJsonLdOfPage(pageId) {
|
|
189
|
+
return this.#db.getJsonLdOfPage(pageId);
|
|
190
|
+
}
|
|
111
191
|
/**
|
|
112
192
|
* Returns the underlying Knex query builder instance for direct SQL access.
|
|
113
193
|
* Enables advanced queries (GROUP BY, HAVING, JOINs) at the database layer
|
|
@@ -200,6 +280,15 @@ export class ArchiveAccessor extends EventEmitter {
|
|
|
200
280
|
async getResourceUrlList() {
|
|
201
281
|
return this.#db.getResourceUrlList();
|
|
202
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* Retrieves the Wappalyzer tag entries for the given page, parsed back
|
|
285
|
+
* from the `page_tags` table.
|
|
286
|
+
* @param pageId - The database id of the page.
|
|
287
|
+
* @returns Ordered entries with provider, category, externalId, etc.
|
|
288
|
+
*/
|
|
289
|
+
async getTagsOfPage(pageId) {
|
|
290
|
+
return this.#db.getTagsOfPage(pageId);
|
|
291
|
+
}
|
|
203
292
|
/**
|
|
204
293
|
* Stores custom data in the archive under the configured namespace.
|
|
205
294
|
* Requires a namespace to be set on this accessor; throws if namespace is null.
|
|
@@ -282,4 +371,41 @@ export class ArchiveAccessor extends EventEmitter {
|
|
|
282
371
|
log('Create Page Data: Done');
|
|
283
372
|
return pPages;
|
|
284
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* Actual close worker — invoked exactly once per accessor via
|
|
376
|
+
* {@link close}'s shared promise. Races `db.destroy()` against the
|
|
377
|
+
* caller-supplied timeout; on timeout we log and resolve so the
|
|
378
|
+
* consumer (typically a process shutting down) is not blocked, even
|
|
379
|
+
* though the underlying knex pool may still be draining in the
|
|
380
|
+
* background.
|
|
381
|
+
* @param timeoutMs - Maximum time to wait for `db.destroy()`.
|
|
382
|
+
*/
|
|
383
|
+
async #runClose(timeoutMs) {
|
|
384
|
+
if (!Number.isFinite(timeoutMs)) {
|
|
385
|
+
await this.#db.destroy();
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
let timer = null;
|
|
389
|
+
const timeout = new Promise((resolve) => {
|
|
390
|
+
timer = setTimeout(() => resolve('timeout'), timeoutMs);
|
|
391
|
+
});
|
|
392
|
+
// Track destroy() so we can attach an error-suppressing handler if we
|
|
393
|
+
// give up waiting — otherwise a late rejection becomes an unhandled
|
|
394
|
+
// promise rejection on the process.
|
|
395
|
+
const destroy = this.#db.destroy().then(() => 'done');
|
|
396
|
+
try {
|
|
397
|
+
const result = await Promise.race([destroy, timeout]);
|
|
398
|
+
if (result === 'timeout') {
|
|
399
|
+
log('ArchiveAccessor.close: db.destroy() did not settle within %dms — giving up', timeoutMs);
|
|
400
|
+
destroy.catch((error) => {
|
|
401
|
+
log('ArchiveAccessor.close: late db.destroy() rejection (post-timeout): %O', error);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
finally {
|
|
406
|
+
if (timer) {
|
|
407
|
+
clearTimeout(timer);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
285
411
|
}
|