@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
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { classifyErrorKind } from '../classify-error-kind.js';
|
|
2
|
+
/**
|
|
3
|
+
* Error kinds where a full puppeteer navigation has a realistic chance of
|
|
4
|
+
* succeeding even though the HEAD pre-flight (and its GET fallback) failed.
|
|
5
|
+
*
|
|
6
|
+
* These are the failure modes that a misconfigured WAF / middlebox / slow
|
|
7
|
+
* origin tends to produce against a bare HEAD/GET probe while still letting
|
|
8
|
+
* a real browser through — the browser uses a different request shape (full
|
|
9
|
+
* navigation lifecycle, JS-capable Accept headers, real cookies, optionally
|
|
10
|
+
* client TLS hints), and some hostile middleboxes only inspect the cheap
|
|
11
|
+
* shape. The fallback is one attempt only; if puppeteer also fails the URL
|
|
12
|
+
* is recorded as `status = -1` like before.
|
|
13
|
+
*
|
|
14
|
+
* Excluded kinds:
|
|
15
|
+
* - **dns / dns-transient** — DNS resolution happens at the OS level before
|
|
16
|
+
* any browser request; puppeteer hits the same `getaddrinfo` outcome.
|
|
17
|
+
* - **tls** — Chromium will refuse the same certificate the Node TLS stack
|
|
18
|
+
* refused (expired, wrong SAN, untrusted CA).
|
|
19
|
+
* - **client-blocked** — by definition the browser is the one rejecting.
|
|
20
|
+
* - **connection-refused** — TCP RST from the listener; same answer regardless
|
|
21
|
+
* of client.
|
|
22
|
+
* - **connection-timeout** — `ETIMEDOUT` at the TCP connect stage means the
|
|
23
|
+
* packets never reached the host (no SYN-ACK); puppeteer issues the same
|
|
24
|
+
* `connect()` call and gets the same answer. Reserved for the middlebox
|
|
25
|
+
* case (request reached the server, response timed out), which classifies
|
|
26
|
+
* as `timeout` via the `NetTimeoutError "Timeout: <url>"` shape.
|
|
27
|
+
* - **local-network** — operator-side connectivity loss; nothing on this
|
|
28
|
+
* machine will reach the host.
|
|
29
|
+
* - **protocol** — puppeteer lifecycle race; bouncing back to puppeteer
|
|
30
|
+
* reproduces the same race.
|
|
31
|
+
* - **unknown** — by design. Spinning up a fresh Chromium for every
|
|
32
|
+
* unclassifiable error is too expensive; if a real-world WAF / middlebox
|
|
33
|
+
* pattern lands in `unknown`, add a matcher to {@link classifyErrorKind}
|
|
34
|
+
* so it lands in one of the four included kinds above (where the fallback
|
|
35
|
+
* has a meaningful chance of succeeding) instead of widening this set.
|
|
36
|
+
*
|
|
37
|
+
* `PreloadShortCircuitError`'s synthesised `getaddrinfo ENOTFOUND` message
|
|
38
|
+
* classifies into `dns` and is therefore filtered out automatically — no
|
|
39
|
+
* separate instanceof guard is needed at the call site.
|
|
40
|
+
*/
|
|
41
|
+
const PUPPETEER_FALLBACK_KINDS = new Set([
|
|
42
|
+
'timeout',
|
|
43
|
+
'connection-reset',
|
|
44
|
+
'parse-error',
|
|
45
|
+
]);
|
|
46
|
+
/**
|
|
47
|
+
* Decide whether a failed HEAD/GET pre-flight error message warrants one
|
|
48
|
+
* puppeteer fallback attempt. Pure and deterministic — the same message
|
|
49
|
+
* always gives the same answer, so the decision can be unit-tested without
|
|
50
|
+
* spinning up a browser.
|
|
51
|
+
* @param message - The pre-flight error message (typically the last rejected
|
|
52
|
+
* `retryCall` attempt).
|
|
53
|
+
* @returns `true` when puppeteer should be tried once, `false` to give up.
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* isPuppeteerFallbackCandidate('Timeout: https://slow.example.org/'); // true
|
|
57
|
+
* isPuppeteerFallbackCandidate('getaddrinfo ENOTFOUND host.invalid'); // false
|
|
58
|
+
* isPuppeteerFallbackCandidate('net::ERR_CERT_DATE_INVALID'); // false
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function isPuppeteerFallbackCandidate(message) {
|
|
62
|
+
return PUPPETEER_FALLBACK_KINDS.has(classifyErrorKind(message));
|
|
63
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { ChildProcess, SpawnOptions } from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* Sends a signal to a single process.
|
|
4
|
+
*
|
|
5
|
+
* Abstracted from `process.kill` so tests can verify exactly which PIDs are
|
|
6
|
+
* signalled without actually touching the OS.
|
|
7
|
+
*/
|
|
8
|
+
export interface ProcessKiller {
|
|
9
|
+
/**
|
|
10
|
+
* Sends `signal` to the process with the given PID. Implementations should
|
|
11
|
+
* swallow ESRCH ("no such process") since the target may have already
|
|
12
|
+
* exited between enumeration and the kill call.
|
|
13
|
+
* @param pid - The PID to signal.
|
|
14
|
+
* @param signal - The signal name (e.g. `'SIGKILL'`) or number.
|
|
15
|
+
*/
|
|
16
|
+
kill(pid: number, signal: NodeJS.Signals | number): void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Spawner signature accepted by `KillProcessTreeDeps`. Pins
|
|
20
|
+
* `child_process.spawn` to the `(command, args, options)` overload — the
|
|
21
|
+
* bare import is a union of many overloads (including `(command, options)`)
|
|
22
|
+
* that does not narrow to this shape without a wrapper. Tests can pass a
|
|
23
|
+
* `vi.fn()` returning a tiny EventEmitter cast to `ChildProcess`.
|
|
24
|
+
*/
|
|
25
|
+
export type Spawner = (command: string, args: readonly string[], options: SpawnOptions) => ChildProcess;
|
|
26
|
+
/**
|
|
27
|
+
* Debug logger compatible with the `debug` package's printf-style API.
|
|
28
|
+
*
|
|
29
|
+
* Called only when a best-effort kill path fails (ESRCH, ENOENT for `ps` or
|
|
30
|
+
* `taskkill`, non-zero exit code). Production callers should pass the
|
|
31
|
+
* crawler-namespaced logger so failures show up under
|
|
32
|
+
* `DEBUG=Nitpicker:Crawler`.
|
|
33
|
+
*/
|
|
34
|
+
export type KillProcessTreeLogger = (
|
|
35
|
+
/** printf-style format string. */
|
|
36
|
+
formatter: string,
|
|
37
|
+
/** Arguments interpolated into the format string. */
|
|
38
|
+
...args: readonly unknown[]) => void;
|
|
39
|
+
/**
|
|
40
|
+
* Dependency overrides for {@link killProcessTree}.
|
|
41
|
+
*
|
|
42
|
+
* Default implementations shell out to `ps` (POSIX) or `taskkill` (Windows),
|
|
43
|
+
* which makes them awkward to unit-test directly. Injecting these lets tests
|
|
44
|
+
* verify the orchestration without invoking real processes.
|
|
45
|
+
*/
|
|
46
|
+
export interface KillProcessTreeDeps {
|
|
47
|
+
/**
|
|
48
|
+
* Lists the PIDs of every descendant of `rootPid` in BFS order
|
|
49
|
+
* (parents before children). Used only on POSIX.
|
|
50
|
+
*/
|
|
51
|
+
listDescendants?: (rootPid: number) => Promise<readonly number[]>;
|
|
52
|
+
/** Signals a single PID. Used only on POSIX. */
|
|
53
|
+
killer?: ProcessKiller;
|
|
54
|
+
/**
|
|
55
|
+
* Performs an OS-level tree-kill. Used only on Windows, where `taskkill
|
|
56
|
+
* /T /F` handles the entire tree atomically.
|
|
57
|
+
*/
|
|
58
|
+
runTreeKill?: (rootPid: number) => Promise<void>;
|
|
59
|
+
/** Override for `process.platform` so cross-platform paths can be tested. */
|
|
60
|
+
platform?: NodeJS.Platform;
|
|
61
|
+
/**
|
|
62
|
+
* `child_process.spawn` substitute used when `runTreeKill` is not
|
|
63
|
+
* supplied (i.e. the default Windows path). Lets tests assert the exact
|
|
64
|
+
* command + args without mocking the global module.
|
|
65
|
+
*/
|
|
66
|
+
spawn?: Spawner;
|
|
67
|
+
/**
|
|
68
|
+
* Receives a single line per best-effort failure path (ENOENT,
|
|
69
|
+
* non-zero exit, etc.). Defaults to a no-op so production callers stay
|
|
70
|
+
* silent unless they opt in.
|
|
71
|
+
*/
|
|
72
|
+
log?: KillProcessTreeLogger;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Kills a process and every one of its descendants.
|
|
76
|
+
*
|
|
77
|
+
* WHY: After SIGKILL'ing Chromium's parent process, its renderer / network /
|
|
78
|
+
* zygote subprocesses linger on Linux/macOS because puppeteer spawns Chromium
|
|
79
|
+
* with `detached: false`, so we cannot use a process-group signal (a
|
|
80
|
+
* negative-PID kill would also hit our own Node process). Enumerating
|
|
81
|
+
* descendants via `ps -A -o pid=,ppid=` and signalling each one — leaves
|
|
82
|
+
* first — closes the orphan gap. On Windows `taskkill /T /F /PID <pid>`
|
|
83
|
+
* performs the equivalent OS-level tree kill atomically.
|
|
84
|
+
*
|
|
85
|
+
* Best-effort: ESRCH (process already gone) and `ps`/`taskkill` invocation
|
|
86
|
+
* failures are swallowed, so the function never rejects on a partial-kill
|
|
87
|
+
* outcome. The caller treats this as a hard cleanup that must always
|
|
88
|
+
* resolve.
|
|
89
|
+
* @param rootPid - The PID at the root of the tree.
|
|
90
|
+
* @param signal - The signal to send. Defaults to `'SIGKILL'`. Ignored on
|
|
91
|
+
* Windows (`taskkill /F` is always forceful).
|
|
92
|
+
* @param deps - Test-time overrides.
|
|
93
|
+
*/
|
|
94
|
+
export declare function killProcessTree(rootPid: number, signal?: NodeJS.Signals | number, deps?: KillProcessTreeDeps): Promise<void>;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* Kills a process and every one of its descendants.
|
|
4
|
+
*
|
|
5
|
+
* WHY: After SIGKILL'ing Chromium's parent process, its renderer / network /
|
|
6
|
+
* zygote subprocesses linger on Linux/macOS because puppeteer spawns Chromium
|
|
7
|
+
* with `detached: false`, so we cannot use a process-group signal (a
|
|
8
|
+
* negative-PID kill would also hit our own Node process). Enumerating
|
|
9
|
+
* descendants via `ps -A -o pid=,ppid=` and signalling each one — leaves
|
|
10
|
+
* first — closes the orphan gap. On Windows `taskkill /T /F /PID <pid>`
|
|
11
|
+
* performs the equivalent OS-level tree kill atomically.
|
|
12
|
+
*
|
|
13
|
+
* Best-effort: ESRCH (process already gone) and `ps`/`taskkill` invocation
|
|
14
|
+
* failures are swallowed, so the function never rejects on a partial-kill
|
|
15
|
+
* outcome. The caller treats this as a hard cleanup that must always
|
|
16
|
+
* resolve.
|
|
17
|
+
* @param rootPid - The PID at the root of the tree.
|
|
18
|
+
* @param signal - The signal to send. Defaults to `'SIGKILL'`. Ignored on
|
|
19
|
+
* Windows (`taskkill /F` is always forceful).
|
|
20
|
+
* @param deps - Test-time overrides.
|
|
21
|
+
*/
|
|
22
|
+
export async function killProcessTree(rootPid, signal = 'SIGKILL', deps = {}) {
|
|
23
|
+
const platform = deps.platform ?? process.platform;
|
|
24
|
+
const log = deps.log ?? noopLog;
|
|
25
|
+
const spawner = deps.spawn ?? defaultSpawner;
|
|
26
|
+
if (platform === 'win32') {
|
|
27
|
+
const runTreeKill = deps.runTreeKill ?? ((pid) => runWindowsTaskkill(pid, spawner, log));
|
|
28
|
+
await runTreeKill(rootPid);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const listDescendants = deps.listDescendants ?? ((pid) => listPosixDescendants(pid, spawner, log));
|
|
32
|
+
const killer = deps.killer ?? makePosixDefaultKiller(log);
|
|
33
|
+
const descendants = await listDescendants(rootPid);
|
|
34
|
+
// BFS produces parents before children; reverse so leaves die first and
|
|
35
|
+
// cannot re-spawn anything via their own watchdog before their parent goes.
|
|
36
|
+
for (const pid of descendants.toReversed()) {
|
|
37
|
+
killer.kill(pid, signal);
|
|
38
|
+
}
|
|
39
|
+
killer.kill(rootPid, signal);
|
|
40
|
+
}
|
|
41
|
+
/** No-op `KillProcessTreeLogger` used when the caller does not supply one. */
|
|
42
|
+
const noopLog = () => { };
|
|
43
|
+
/**
|
|
44
|
+
* Default {@link Spawner}: a thin wrapper around `node:child_process.spawn`
|
|
45
|
+
* that pins the call signature to `(command, args, options)`. The bare
|
|
46
|
+
* `spawn` import has many overloads — including `(command, options)` — so it
|
|
47
|
+
* is not assignable to our narrower `Spawner` shape; wrapping it removes the
|
|
48
|
+
* variance without changing runtime behaviour.
|
|
49
|
+
* @param command
|
|
50
|
+
* @param args
|
|
51
|
+
* @param options
|
|
52
|
+
*/
|
|
53
|
+
const defaultSpawner = (command, args, options) => nodeSpawn(command, args, options);
|
|
54
|
+
/**
|
|
55
|
+
* Builds the default POSIX killer: `process.kill` with ESRCH/EPERM swallowed
|
|
56
|
+
* and logged via `log` so production callers can observe how often the tree
|
|
57
|
+
* walk hits already-dead PIDs.
|
|
58
|
+
* @param log - Receives one line per swallowed error.
|
|
59
|
+
* @returns A {@link ProcessKiller}.
|
|
60
|
+
*/
|
|
61
|
+
function makePosixDefaultKiller(log) {
|
|
62
|
+
return {
|
|
63
|
+
kill(pid, signal) {
|
|
64
|
+
try {
|
|
65
|
+
process.kill(pid, signal);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
// Already dead (ESRCH) or denied (EPERM) — best-effort.
|
|
69
|
+
log('process.kill(%d, %s) failed: %O', pid, String(signal), error);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Walks the process table on POSIX and returns every descendant of
|
|
76
|
+
* `rootPid` in BFS order.
|
|
77
|
+
*
|
|
78
|
+
* Shells out to `ps -A -o pid=,ppid=` via `spawn` (no shell), parses each
|
|
79
|
+
* row into `(pid, ppid)`, builds a parent-to-children map, then breadth-first
|
|
80
|
+
* walks from `rootPid`.
|
|
81
|
+
*
|
|
82
|
+
* Failure to invoke `ps` (missing binary, permission denied, non-zero exit)
|
|
83
|
+
* returns an empty array so the caller can still kill the root.
|
|
84
|
+
* @param rootPid - The PID whose descendants to list.
|
|
85
|
+
* @param spawner
|
|
86
|
+
* @param log
|
|
87
|
+
* @returns A promise resolving to descendant PIDs.
|
|
88
|
+
*/
|
|
89
|
+
async function listPosixDescendants(rootPid, spawner, log) {
|
|
90
|
+
const parentToChildren = await readPosixProcessMap(spawner, log);
|
|
91
|
+
const descendants = [];
|
|
92
|
+
const queue = [rootPid];
|
|
93
|
+
while (queue.length > 0) {
|
|
94
|
+
const pid = queue.shift();
|
|
95
|
+
const children = parentToChildren.get(pid);
|
|
96
|
+
if (!children)
|
|
97
|
+
continue;
|
|
98
|
+
for (const child of children) {
|
|
99
|
+
descendants.push(child);
|
|
100
|
+
queue.push(child);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return descendants;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Reads the full POSIX process table by spawning `ps` and parsing its output.
|
|
107
|
+
*
|
|
108
|
+
* Format requested: `pid=,ppid=` (no headers). Each line is `<pid> <ppid>`.
|
|
109
|
+
* @param spawner - Substitute for `child_process.spawn`.
|
|
110
|
+
* @param log - Receives a single line if `ps` cannot be invoked or exits non-zero.
|
|
111
|
+
* @returns A promise resolving to a parent-PID-to-children map. Empty on
|
|
112
|
+
* any invocation failure.
|
|
113
|
+
*/
|
|
114
|
+
async function readPosixProcessMap(spawner, log) {
|
|
115
|
+
return new Promise((resolve) => {
|
|
116
|
+
const proc = spawner('ps', ['-A', '-o', 'pid=,ppid='], {
|
|
117
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
118
|
+
});
|
|
119
|
+
let output = '';
|
|
120
|
+
proc.stdout?.on('data', (chunk) => {
|
|
121
|
+
output += chunk.toString('utf8');
|
|
122
|
+
});
|
|
123
|
+
proc.on('error', (error) => {
|
|
124
|
+
log('ps invocation failed: %O', error);
|
|
125
|
+
resolve(new Map());
|
|
126
|
+
});
|
|
127
|
+
proc.on('close', (code) => {
|
|
128
|
+
if (code !== 0) {
|
|
129
|
+
log('ps exited with non-zero code %s — skipping descendant tree-kill', String(code));
|
|
130
|
+
resolve(new Map());
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const map = new Map();
|
|
134
|
+
for (const line of output.split('\n')) {
|
|
135
|
+
const match = /^\s*(\d+)\s+(\d+)/.exec(line);
|
|
136
|
+
if (!match)
|
|
137
|
+
continue;
|
|
138
|
+
const pid = Number(match[1]);
|
|
139
|
+
const ppid = Number(match[2]);
|
|
140
|
+
const list = map.get(ppid);
|
|
141
|
+
if (list) {
|
|
142
|
+
list.push(pid);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
map.set(ppid, [pid]);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
resolve(map);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Forces a tree-kill on Windows via `taskkill /T /F /PID <pid>`.
|
|
154
|
+
*
|
|
155
|
+
* The `/T` flag walks descendants; `/F` forces termination. Any failure
|
|
156
|
+
* (ENOENT for taskkill, non-zero exit because the PID is already gone) is
|
|
157
|
+
* logged via `log` and swallowed so the caller's cleanup always resolves.
|
|
158
|
+
* @param rootPid - The PID at the root of the tree.
|
|
159
|
+
* @param spawner - Substitute for `child_process.spawn`.
|
|
160
|
+
* @param log - Receives a single line per failure.
|
|
161
|
+
*/
|
|
162
|
+
async function runWindowsTaskkill(rootPid, spawner, log) {
|
|
163
|
+
return new Promise((resolve) => {
|
|
164
|
+
const proc = spawner('taskkill', ['/T', '/F', '/PID', String(rootPid)], {
|
|
165
|
+
stdio: 'ignore',
|
|
166
|
+
});
|
|
167
|
+
proc.on('error', (error) => {
|
|
168
|
+
log('taskkill invocation failed: %O', error);
|
|
169
|
+
resolve();
|
|
170
|
+
});
|
|
171
|
+
proc.on('close', (code) => {
|
|
172
|
+
if (code !== 0) {
|
|
173
|
+
log('taskkill exited with non-zero code %s for PID %d', String(code), rootPid);
|
|
174
|
+
}
|
|
175
|
+
resolve();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
@@ -45,12 +45,32 @@ export default class LinkList {
|
|
|
45
45
|
* @param resource.page - The scraped page data, if the scrape succeeded.
|
|
46
46
|
* @param resource.error - The error object, if the scrape failed.
|
|
47
47
|
* @param options - URL parsing options (e.g., `disableQueries`).
|
|
48
|
+
* @param completion - Behaviour overrides for how the redirect chain is folded into
|
|
49
|
+
* the done-set.
|
|
50
|
+
* @param completion.includeRedirectPaths - When `false`, do NOT mark the URLs in
|
|
51
|
+
* `resource.page.redirectPaths` as done. The default `true` preserves the
|
|
52
|
+
* long-standing behaviour where a redirect chain (`/a → /b → /c`) folds every
|
|
53
|
+
* intermediate URL into the done-set in a single sweep — correct for HTTP-layer
|
|
54
|
+
* chains because the browser actually followed each hop, so reaching `/b` later
|
|
55
|
+
* is a no-op. Pass `false` from the JS-redirect rescue: there `redirectPaths`
|
|
56
|
+
* contains a single URL (the JS target Chromium navigated to after `page.goto()`
|
|
57
|
+
* returned null), which the browser has NOT yet rendered. Folding it into the
|
|
58
|
+
* done-set would make a subsequent `linkList.add(destinationUrl)` no-op (the
|
|
59
|
+
* add() guard at line 51-53 refuses keys already in `#done`), so the dest URL
|
|
60
|
+
* never enters `#pending` and the dealer never sees a push to enqueue. The
|
|
61
|
+
* dealer's own `seen` Set in `#runDeal` (the gate `onPush` consults) is a
|
|
62
|
+
* separate registry from `#done` — they are NOT kept in sync — but here it
|
|
63
|
+
* does not matter: the rescue's `add()` is what feeds the eventual `enqueue()`
|
|
64
|
+
* call, so blocking `add()` alone is enough to silently lose the JS target
|
|
65
|
+
* from the archive.
|
|
48
66
|
* @returns The constructed {@link Link} object, or `null` if the URL was not in the queue.
|
|
49
67
|
*/
|
|
50
68
|
done(url: ExURL, scope: ReadonlyMap<string, readonly ExURL[]>, resource: {
|
|
51
69
|
page?: PageData;
|
|
52
70
|
error?: Error;
|
|
53
|
-
}, options: ParseURLOptions
|
|
71
|
+
}, options: ParseURLOptions, completion?: {
|
|
72
|
+
includeRedirectPaths?: boolean;
|
|
73
|
+
}): Link | null;
|
|
54
74
|
/**
|
|
55
75
|
* Get the current pending and in-progress URL lists.
|
|
56
76
|
* @returns An object containing arrays of pending and in-progress URL strings.
|
package/lib/crawler/link-list.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isError } from '@d-zero/beholder';
|
|
2
2
|
import { isLowerLayer } from '@d-zero/shared/is-lower-layer';
|
|
3
3
|
import { tryParseUrl as parseUrl } from '@d-zero/shared/parse-url';
|
|
4
|
+
import { isHtmlContentType } from './is-html-content-type.js';
|
|
4
5
|
import { protocolAgnosticKey } from './protocol-agnostic-key.js';
|
|
5
6
|
/**
|
|
6
7
|
* Manages the queue of URLs discovered during crawling.
|
|
@@ -63,9 +64,27 @@ export default class LinkList {
|
|
|
63
64
|
* @param resource.page - The scraped page data, if the scrape succeeded.
|
|
64
65
|
* @param resource.error - The error object, if the scrape failed.
|
|
65
66
|
* @param options - URL parsing options (e.g., `disableQueries`).
|
|
67
|
+
* @param completion - Behaviour overrides for how the redirect chain is folded into
|
|
68
|
+
* the done-set.
|
|
69
|
+
* @param completion.includeRedirectPaths - When `false`, do NOT mark the URLs in
|
|
70
|
+
* `resource.page.redirectPaths` as done. The default `true` preserves the
|
|
71
|
+
* long-standing behaviour where a redirect chain (`/a → /b → /c`) folds every
|
|
72
|
+
* intermediate URL into the done-set in a single sweep — correct for HTTP-layer
|
|
73
|
+
* chains because the browser actually followed each hop, so reaching `/b` later
|
|
74
|
+
* is a no-op. Pass `false` from the JS-redirect rescue: there `redirectPaths`
|
|
75
|
+
* contains a single URL (the JS target Chromium navigated to after `page.goto()`
|
|
76
|
+
* returned null), which the browser has NOT yet rendered. Folding it into the
|
|
77
|
+
* done-set would make a subsequent `linkList.add(destinationUrl)` no-op (the
|
|
78
|
+
* add() guard at line 51-53 refuses keys already in `#done`), so the dest URL
|
|
79
|
+
* never enters `#pending` and the dealer never sees a push to enqueue. The
|
|
80
|
+
* dealer's own `seen` Set in `#runDeal` (the gate `onPush` consults) is a
|
|
81
|
+
* separate registry from `#done` — they are NOT kept in sync — but here it
|
|
82
|
+
* does not matter: the rescue's `add()` is what feeds the eventual `enqueue()`
|
|
83
|
+
* call, so blocking `add()` alone is enough to silently lose the JS target
|
|
84
|
+
* from the archive.
|
|
66
85
|
* @returns The constructed {@link Link} object, or `null` if the URL was not in the queue.
|
|
67
86
|
*/
|
|
68
|
-
done(url, scope, resource, options) {
|
|
87
|
+
done(url, scope, resource, options, completion) {
|
|
69
88
|
const key = protocolAgnosticKey(url.withoutHashAndAuth);
|
|
70
89
|
if (!(this.#pending.has(key) || this.#progress.has(key))) {
|
|
71
90
|
return null;
|
|
@@ -95,8 +114,10 @@ export default class LinkList {
|
|
|
95
114
|
responseHeaders: resource.page.responseHeaders,
|
|
96
115
|
title: resource.page.meta.title,
|
|
97
116
|
};
|
|
98
|
-
|
|
99
|
-
|
|
117
|
+
if (completion?.includeRedirectPaths !== false) {
|
|
118
|
+
for (const path of resource.page.redirectPaths) {
|
|
119
|
+
urlList.add(protocolAgnosticKey(path));
|
|
120
|
+
}
|
|
100
121
|
}
|
|
101
122
|
}
|
|
102
123
|
if (resource.error?.message.includes('ERR_NAME_NOT_RESOLVED')) {
|
|
@@ -241,7 +262,7 @@ function isPage(link) {
|
|
|
241
262
|
if (isError(link.dest.status)) {
|
|
242
263
|
return false;
|
|
243
264
|
}
|
|
244
|
-
if (link.dest.contentType
|
|
265
|
+
if (isHtmlContentType(link.dest.contentType)) {
|
|
245
266
|
return true;
|
|
246
267
|
}
|
|
247
268
|
return false;
|
|
@@ -2,12 +2,20 @@ import type { Link, PageData } from '../utils/types/types.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Convert a {@link Link} object into a {@link PageData} structure.
|
|
4
4
|
*
|
|
5
|
-
* Creates a minimal PageData from the link's destination metadata. This is
|
|
6
|
-
* when a full scrape is not performed (e.g., for external pages when
|
|
7
|
-
* `fetchExternal` is disabled, or when a scrape error produces a fallback
|
|
5
|
+
* Creates a minimal PageData from the link's destination metadata. This is
|
|
6
|
+
* used when a full scrape is not performed (e.g., for external pages when
|
|
7
|
+
* `fetchExternal` is disabled, or when a scrape error produces a fallback
|
|
8
|
+
* result).
|
|
8
9
|
*
|
|
9
|
-
* Missing destination fields are filled with sensible defaults (e.g.,
|
|
10
|
-
* for unknown, empty arrays for anchors/images, empty string for
|
|
10
|
+
* Missing destination fields are filled with sensible defaults (e.g.,
|
|
11
|
+
* status -1 for unknown, empty arrays for anchors/images, empty string for
|
|
12
|
+
* HTML).
|
|
13
|
+
*
|
|
14
|
+
* beholder 3.0.0 elevated several Meta sub-fields from "optional" to
|
|
15
|
+
* "required array / object": `jsonLd`, `speculationRules`, `tags`, `others`,
|
|
16
|
+
* `originTrial`. The dummy meta object below populates every required slot
|
|
17
|
+
* so downstream consumers (database `#insertJsonLd` / `#insertTags`,
|
|
18
|
+
* `deriveFlatFromMeta`) never hit `undefined` while iterating.
|
|
11
19
|
* @param link - The link to convert, containing URL and optional destination metadata.
|
|
12
20
|
* @returns A PageData object populated from the link's available data.
|
|
13
21
|
*/
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Convert a {@link Link} object into a {@link PageData} structure.
|
|
3
3
|
*
|
|
4
|
-
* Creates a minimal PageData from the link's destination metadata. This is
|
|
5
|
-
* when a full scrape is not performed (e.g., for external pages when
|
|
6
|
-
* `fetchExternal` is disabled, or when a scrape error produces a fallback
|
|
4
|
+
* Creates a minimal PageData from the link's destination metadata. This is
|
|
5
|
+
* used when a full scrape is not performed (e.g., for external pages when
|
|
6
|
+
* `fetchExternal` is disabled, or when a scrape error produces a fallback
|
|
7
|
+
* result).
|
|
7
8
|
*
|
|
8
|
-
* Missing destination fields are filled with sensible defaults (e.g.,
|
|
9
|
-
* for unknown, empty arrays for anchors/images, empty string for
|
|
9
|
+
* Missing destination fields are filled with sensible defaults (e.g.,
|
|
10
|
+
* status -1 for unknown, empty arrays for anchors/images, empty string for
|
|
11
|
+
* HTML).
|
|
12
|
+
*
|
|
13
|
+
* beholder 3.0.0 elevated several Meta sub-fields from "optional" to
|
|
14
|
+
* "required array / object": `jsonLd`, `speculationRules`, `tags`, `others`,
|
|
15
|
+
* `originTrial`. The dummy meta object below populates every required slot
|
|
16
|
+
* so downstream consumers (database `#insertJsonLd` / `#insertTags`,
|
|
17
|
+
* `deriveFlatFromMeta`) never hit `undefined` while iterating.
|
|
10
18
|
* @param link - The link to convert, containing URL and optional destination metadata.
|
|
11
19
|
* @returns A PageData object populated from the link's available data.
|
|
12
20
|
*/
|
|
@@ -23,6 +31,19 @@ export function linkToPageData(link) {
|
|
|
23
31
|
responseHeaders: link.dest?.responseHeaders || null,
|
|
24
32
|
meta: {
|
|
25
33
|
title: link.dest?.title || '',
|
|
34
|
+
jsonLd: [],
|
|
35
|
+
speculationRules: [],
|
|
36
|
+
tags: { detected: {}, entries: [] },
|
|
37
|
+
others: {
|
|
38
|
+
meta: {},
|
|
39
|
+
property: {},
|
|
40
|
+
httpEquiv: {},
|
|
41
|
+
itemprop: {},
|
|
42
|
+
link: [],
|
|
43
|
+
script: [],
|
|
44
|
+
iframe: [],
|
|
45
|
+
},
|
|
46
|
+
originTrial: [],
|
|
26
47
|
},
|
|
27
48
|
anchorList: [],
|
|
28
49
|
imageList: [],
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { BufferedPhaseError } from './drain-phase-errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Debug logger compatible with the `debug` package's printf-style API.
|
|
4
|
+
*
|
|
5
|
+
* Receives a single line per URL whose buffered phase errors were
|
|
6
|
+
* undrained at finally time.
|
|
7
|
+
*/
|
|
8
|
+
export type PhaseErrorDropLogger = (
|
|
9
|
+
/** printf-style format string. */
|
|
10
|
+
formatter: string,
|
|
11
|
+
/** Arguments interpolated into the format string. */
|
|
12
|
+
...args: readonly unknown[]) => void;
|
|
13
|
+
/**
|
|
14
|
+
* Logs and clears any phase errors still buffered for `urlHref` at
|
|
15
|
+
* worker-finally time.
|
|
16
|
+
*
|
|
17
|
+
* WHY: a Crawler worker's finally clause runs for every code path —
|
|
18
|
+
* success, hard error, and the early `return` taken for predicted URLs
|
|
19
|
+
* that were discarded after probing. The success and catch branches
|
|
20
|
+
* drain via `drainPhaseErrors`, but the predicted-discard branch
|
|
21
|
+
* skips drain entirely, so any `retryExhausted` events captured during
|
|
22
|
+
* its probe would silently leak from the Map.
|
|
23
|
+
*
|
|
24
|
+
* Calling this function as the final cleanup step:
|
|
25
|
+
* - Surfaces the drop via `DEBUG=Nitpicker:Crawler` so production runs
|
|
26
|
+
* are observable instead of silent.
|
|
27
|
+
* - Removes the buffer entry so it does not grow across crawls.
|
|
28
|
+
*
|
|
29
|
+
* Safe to call after a successful drain: the buffer entry is already
|
|
30
|
+
* gone and both the log and the delete become no-ops.
|
|
31
|
+
* @param buffer - The pending-phase-errors map, keyed by URL href.
|
|
32
|
+
* @param urlHref - URL whose buffer entry should be flushed.
|
|
33
|
+
* @param log - Receives one line if any undrained errors are present.
|
|
34
|
+
* @returns The number of phase-error records dropped (0 when nothing
|
|
35
|
+
* was buffered).
|
|
36
|
+
*/
|
|
37
|
+
export declare function logUndrainedPhaseErrors(buffer: Map<string, BufferedPhaseError[]>, urlHref: string, log: PhaseErrorDropLogger): number;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logs and clears any phase errors still buffered for `urlHref` at
|
|
3
|
+
* worker-finally time.
|
|
4
|
+
*
|
|
5
|
+
* WHY: a Crawler worker's finally clause runs for every code path —
|
|
6
|
+
* success, hard error, and the early `return` taken for predicted URLs
|
|
7
|
+
* that were discarded after probing. The success and catch branches
|
|
8
|
+
* drain via `drainPhaseErrors`, but the predicted-discard branch
|
|
9
|
+
* skips drain entirely, so any `retryExhausted` events captured during
|
|
10
|
+
* its probe would silently leak from the Map.
|
|
11
|
+
*
|
|
12
|
+
* Calling this function as the final cleanup step:
|
|
13
|
+
* - Surfaces the drop via `DEBUG=Nitpicker:Crawler` so production runs
|
|
14
|
+
* are observable instead of silent.
|
|
15
|
+
* - Removes the buffer entry so it does not grow across crawls.
|
|
16
|
+
*
|
|
17
|
+
* Safe to call after a successful drain: the buffer entry is already
|
|
18
|
+
* gone and both the log and the delete become no-ops.
|
|
19
|
+
* @param buffer - The pending-phase-errors map, keyed by URL href.
|
|
20
|
+
* @param urlHref - URL whose buffer entry should be flushed.
|
|
21
|
+
* @param log - Receives one line if any undrained errors are present.
|
|
22
|
+
* @returns The number of phase-error records dropped (0 when nothing
|
|
23
|
+
* was buffered).
|
|
24
|
+
*/
|
|
25
|
+
export function logUndrainedPhaseErrors(buffer, urlHref, log) {
|
|
26
|
+
const remaining = buffer.get(urlHref);
|
|
27
|
+
if (!remaining || remaining.length === 0) {
|
|
28
|
+
buffer.delete(urlHref);
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
log('Dropped %d phase error(s) for %s (no archive entry created)', remaining.length, urlHref);
|
|
32
|
+
buffer.delete(urlHref);
|
|
33
|
+
return remaining.length;
|
|
34
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonicalizes a Content-Type media type for storage.
|
|
3
|
+
*
|
|
4
|
+
* MIME types are case-insensitive (RFC 2045) and may arrive with surrounding
|
|
5
|
+
* whitespace (e.g. `text/html ` left after stripping the `; charset=...`
|
|
6
|
+
* parameter). Responses are recorded verbatim (`header.split(';')[0]`) without
|
|
7
|
+
* normalization, so `Text/HTML` or `text/html ` can otherwise reach the
|
|
8
|
+
* database. Storing the canonical (trimmed, lower-cased) form lets the exact
|
|
9
|
+
* SQL page-ness predicate (`WHERE contentType = 'text/html'`) agree with the
|
|
10
|
+
* code-level {@link isHtmlContentType} check, which trims and lower-cases.
|
|
11
|
+
* @param contentType - The raw media type, or `null` when unknown.
|
|
12
|
+
* @returns The trimmed, lower-cased media type, or `null` when unknown/blank.
|
|
13
|
+
*/
|
|
14
|
+
export declare function normalizeContentType(contentType: string | null): string | null;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonicalizes a Content-Type media type for storage.
|
|
3
|
+
*
|
|
4
|
+
* MIME types are case-insensitive (RFC 2045) and may arrive with surrounding
|
|
5
|
+
* whitespace (e.g. `text/html ` left after stripping the `; charset=...`
|
|
6
|
+
* parameter). Responses are recorded verbatim (`header.split(';')[0]`) without
|
|
7
|
+
* normalization, so `Text/HTML` or `text/html ` can otherwise reach the
|
|
8
|
+
* database. Storing the canonical (trimmed, lower-cased) form lets the exact
|
|
9
|
+
* SQL page-ness predicate (`WHERE contentType = 'text/html'`) agree with the
|
|
10
|
+
* code-level {@link isHtmlContentType} check, which trims and lower-cases.
|
|
11
|
+
* @param contentType - The raw media type, or `null` when unknown.
|
|
12
|
+
* @returns The trimmed, lower-cased media type, or `null` when unknown/blank.
|
|
13
|
+
*/
|
|
14
|
+
export function normalizeContentType(contentType) {
|
|
15
|
+
if (contentType === null) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const normalized = contentType.trim().toLowerCase();
|
|
19
|
+
return normalized === '' ? null : normalized;
|
|
20
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ExURL } from '@d-zero/shared/parse-url';
|
|
2
|
+
/**
|
|
3
|
+
* Split URLs into a likely-HTML group and a non-HTML group, preserving input
|
|
4
|
+
* order within each group.
|
|
5
|
+
*
|
|
6
|
+
* Used by the crawler's enqueue path to route discovered URLs: likely-HTML URLs
|
|
7
|
+
* are `unshift`ed to the front of the dealer queue (so page crawling advances
|
|
8
|
+
* ahead of asset/document fetches) while the rest are `push`ed to the tail.
|
|
9
|
+
* Keeping the order stable within each group means a batch (e.g. predicted
|
|
10
|
+
* pagination URLs) stays in ascending order at the front when unshifted as one
|
|
11
|
+
* call. Classification is delegated to {@link isLikelyHtmlUrl}.
|
|
12
|
+
* @param urls - The discovered URLs to partition.
|
|
13
|
+
* @returns A `[html, other]` tuple: likely-HTML URLs and the remainder, each in
|
|
14
|
+
* original input order.
|
|
15
|
+
*/
|
|
16
|
+
export declare function partitionUrlsByHtml(urls: readonly ExURL[]): [html: ExURL[], other: ExURL[]];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { isLikelyHtmlUrl } from './is-likely-html-url.js';
|
|
2
|
+
/**
|
|
3
|
+
* Split URLs into a likely-HTML group and a non-HTML group, preserving input
|
|
4
|
+
* order within each group.
|
|
5
|
+
*
|
|
6
|
+
* Used by the crawler's enqueue path to route discovered URLs: likely-HTML URLs
|
|
7
|
+
* are `unshift`ed to the front of the dealer queue (so page crawling advances
|
|
8
|
+
* ahead of asset/document fetches) while the rest are `push`ed to the tail.
|
|
9
|
+
* Keeping the order stable within each group means a batch (e.g. predicted
|
|
10
|
+
* pagination URLs) stays in ascending order at the front when unshifted as one
|
|
11
|
+
* call. Classification is delegated to {@link isLikelyHtmlUrl}.
|
|
12
|
+
* @param urls - The discovered URLs to partition.
|
|
13
|
+
* @returns A `[html, other]` tuple: likely-HTML URLs and the remainder, each in
|
|
14
|
+
* original input order.
|
|
15
|
+
*/
|
|
16
|
+
export function partitionUrlsByHtml(urls) {
|
|
17
|
+
const html = [];
|
|
18
|
+
const other = [];
|
|
19
|
+
for (const url of urls) {
|
|
20
|
+
(isLikelyHtmlUrl(url) ? html : other).push(url);
|
|
21
|
+
}
|
|
22
|
+
return [html, other];
|
|
23
|
+
}
|