@nitpicker/core 0.24.0 → 0.25.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/README.md +165 -35
- package/lib/assert-u32-id.d.ts +15 -0
- package/lib/assert-u32-id.js +22 -0
- package/lib/check-native-executor-config.d.ts +21 -0
- package/lib/check-native-executor-config.js +24 -0
- package/lib/compute-alias-keys.d.ts +28 -0
- package/lib/compute-alias-keys.js +30 -0
- package/lib/compute-body-hash.d.ts +22 -0
- package/lib/compute-body-hash.js +25 -0
- package/lib/compute-url-sort-ranks.d.ts +28 -0
- package/lib/compute-url-sort-ranks.js +49 -0
- package/lib/create-isolated-cluster-run.d.ts +35 -0
- package/lib/create-isolated-cluster-run.js +52 -0
- package/lib/create-template-classification-run.d.ts +49 -0
- package/lib/create-template-classification-run.js +386 -0
- package/lib/group-alias-candidates.d.ts +40 -0
- package/lib/group-alias-candidates.js +104 -0
- package/lib/load-native-binding.d.ts +26 -0
- package/lib/load-native-binding.js +152 -0
- package/lib/match-js-license-comment-signal.d.ts +19 -0
- package/lib/match-js-license-comment-signal.js +21 -0
- package/lib/match-selector-html.d.ts +41 -0
- package/lib/match-selector-html.js +42 -0
- package/lib/match-selector-snapshots.d.ts +40 -0
- package/lib/match-selector-snapshots.js +57 -0
- package/lib/match-structural-technology-signals.d.ts +23 -0
- package/lib/match-structural-technology-signals.js +25 -0
- package/lib/max-decoded-snapshot-bytes.d.ts +22 -0
- package/lib/max-decoded-snapshot-bytes.js +35 -0
- package/lib/native-binding.d.ts +9 -0
- package/lib/native-binding.js +9 -0
- package/lib/pack-stored-snapshots.d.ts +23 -0
- package/lib/pack-stored-snapshots.js +53 -0
- package/lib/run-with-signal.d.ts +27 -0
- package/lib/run-with-signal.js +54 -0
- package/lib/search-html-document.d.ts +32 -0
- package/lib/search-html-document.js +31 -0
- package/lib/search-html-snapshots.d.ts +46 -0
- package/lib/search-html-snapshots.js +54 -0
- package/lib/template-block-sample-size.d.ts +13 -0
- package/lib/template-block-sample-size.js +14 -0
- package/lib/template-exact-page-limit.d.ts +18 -0
- package/lib/template-exact-page-limit.js +19 -0
- package/lib/template-memory-budget.d.ts +16 -0
- package/lib/template-memory-budget.js +17 -0
- package/lib/to-native-selector-plan.d.ts +16 -0
- package/lib/to-native-selector-plan.js +40 -0
- package/lib/to-u32-ids.d.ts +14 -0
- package/lib/to-u32-ids.js +23 -0
- package/lib/types.d.ts +736 -248
- package/lib/validate-search-pattern.d.ts +25 -0
- package/lib/validate-search-pattern.js +27 -0
- package/package.json +83 -16
- package/lib/core.d.ts +0 -4
- package/lib/core.js +0 -4
- package/lib/discover-analyze-plugins.d.ts +0 -14
- package/lib/discover-analyze-plugins.js +0 -32
- package/lib/get-table-cache-root.d.ts +0 -18
- package/lib/get-table-cache-root.js +0 -22
- package/lib/hooks/define-plugin.d.ts +0 -68
- package/lib/hooks/define-plugin.js +0 -69
- package/lib/import-modules.d.ts +0 -24
- package/lib/import-modules.js +0 -38
- package/lib/load-plugin-settings.d.ts +0 -44
- package/lib/load-plugin-settings.js +0 -125
- package/lib/nitpicker.d.ts +0 -150
- package/lib/nitpicker.js +0 -437
- package/lib/page-analysis-worker.d.ts +0 -58
- package/lib/page-analysis-worker.js +0 -68
- package/lib/read-plugin-labels.d.ts +0 -15
- package/lib/read-plugin-labels.js +0 -30
- package/lib/table.d.ts +0 -75
- package/lib/table.js +0 -132
- package/lib/url-event-bus.d.ts +0 -33
- package/lib/url-event-bus.js +0 -21
- package/lib/worker/runner.d.ts +0 -25
- package/lib/worker/runner.js +0 -31
- package/lib/worker/types.d.ts +0 -23
- package/lib/worker/types.js +0 -1
- package/lib/worker/worker-pool.d.ts +0 -82
- package/lib/worker/worker-pool.js +0 -278
- package/lib/worker/worker.d.ts +0 -34
- package/lib/worker/worker.js +0 -108
package/README.md
CHANGED
|
@@ -1,47 +1,177 @@
|
|
|
1
1
|
# @nitpicker/core
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
3
|
+
Nitpicker の計算コアを Rust(Node-API、[napi-rs](https://napi.rs/))で実装したネイティブアドオンです。重い計算をここに集め、[@nitpicker/archive](../archive/README.md) などの TypeScript パッケージから呼び出します。現在は、ページ本文の内容ハッシュ(`page_meta.body_hash`)、HTML の構造シグナルによる技術検出、URL の自然順の順位付け、alias の判定、リンクの連結成分、テンプレート分類(ページのシグナルと HTML から、ページごとのテンプレートキーとクラスタごとの reason まで)、保存済み HTML スナップショットに対する CSS セレクタの照合(`query match-selector`)と文字列・正規表現の検索(`query search-html`)を提供しています。
|
|
4
|
+
|
|
5
|
+
## 対応プラットフォーム
|
|
6
|
+
|
|
7
|
+
ビルド済みバイナリだけを配布し、JavaScript のフォールバックはありません。対応しているのは次の 2 つです。
|
|
8
|
+
|
|
9
|
+
| プラットフォーム | バイナリを含むパッケージ |
|
|
10
|
+
| ------------------------------------- | ---------------------------------------------------------------- |
|
|
11
|
+
| macOS(Apple silicon) | [@nitpicker/core-darwin-arm64](../core-darwin-arm64/README.md) |
|
|
12
|
+
| Linux x64・glibc 2.28 以上(WSL2 可) | [@nitpicker/core-linux-x64-gnu](../core-linux-x64-gnu/README.md) |
|
|
13
|
+
|
|
14
|
+
glibc 2.28 は Node.js 24 自体の Linux バイナリと同じ下限です。上記以外(Intel Mac、linux-arm64、musl、Windows ネイティブ)では、読み込んだ時点で対応表を含むエラーを出して終了します。プラットフォーム別パッケージは `optionalDependencies` で入るため、`--omit=optional` / `--no-optional` を付けてインストールすると読み込めません。
|
|
15
|
+
|
|
16
|
+
## 使い方
|
|
17
|
+
|
|
18
|
+
公開 API は 1 関数 1 サブパスで export しています。
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { computeBodyHash } from '@nitpicker/core/compute-body-hash';
|
|
22
|
+
|
|
23
|
+
const a = computeBodyHash(Buffer.from('<body><a href="/p/a1b2c3d4">x</a></body>'));
|
|
24
|
+
const b = computeBodyHash(Buffer.from('<body><a href="/p/z9y8x7w6">x</a></body>'));
|
|
25
|
+
a.equals(b); // true — 動的 ID はマスクしてからハッシュする
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
フロントエンドフレームワークの構造シグナル(Next.js の `__NEXT_DATA__`、Vue の `data-v-*` など)は、`matchStructuralTechnologySignals` がページの HTML を 1 回走査して検出します。JS リソースの先頭にあるライセンスコメントは `matchJsLicenseCommentSignal` で照合します。フィンガープリントの一覧は `crates/nitpicker_tech_detect` が正です。
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { matchStructuralTechnologySignals } from '@nitpicker/core/match-structural-technology-signals';
|
|
32
|
+
|
|
33
|
+
matchStructuralTechnologySignals(Buffer.from('<div data-v-1a2b3c4d></div>'));
|
|
34
|
+
// [{ technology: 'Vue', signalType: 'scoped-attr', evidence: ' data-v-1a2b3c4d', weight: 40, category: 'JavaScript frameworks' }]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
URL の自然順(`page2` が `page10` の前、大文字小文字・パーセントエンコードで順序が割れない、ホストが先でパスが後)は、`computeUrlSortRanks` が全件まとめて順位にします。URL は 1 回だけパースし、結果は入力順に依存しない全順序です(同じ URL は入力順)。順序の定義は `crates/nitpicker_url_key` の doc が正です。
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { computeUrlSortRanks } from '@nitpicker/core/compute-url-sort-ranks';
|
|
41
|
+
|
|
42
|
+
computeUrlSortRanks([
|
|
43
|
+
'https://example.com/page10',
|
|
44
|
+
'https://example.com/page2',
|
|
45
|
+
'https://example.com/',
|
|
46
|
+
]); // Uint32Array [2, 1, 0]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
同じページが別の URL で現れている組(`content_items.alias_of_id`)の判定には、URL からスキーム・ホストの大文字小文字・`/index.{ext}` をたたんだ Tier A キーと、さらに末尾スラッシュを落とした Tier B キーを使います。`computeAliasKeys` が 1 回のパースで両方を返し、`groupAliasCandidates` がキー・タイトル・本文ハッシュで連結成分にまとめて代表を選びます(グループ内を指す canonical が最多 → 最短 URL → UTF-16 の昇順)。
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { computeAliasKeys } from '@nitpicker/core/compute-alias-keys';
|
|
53
|
+
|
|
54
|
+
computeAliasKeys('http://Example.com/about/index.html');
|
|
55
|
+
// { tierA: 'example.com/about/', tierB: 'example.com/about' }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
リンクでつながったページの連結成分は、`createIsolatedClusterRun` で求めます。辺は SQLite から読んだ分ずつ `unionEdges` に渡して捨てるので、メモリはページ数に比例し、辺の数には比例しません。リダイレクト先・alias の先に解決してから結び、終わりのない連鎖(ループ、ループに合流する尾)の辺は捨てます。成分の番号は候補の並び順で振られ、辺の順序や分け方に依存しません。
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { createIsolatedClusterRun } from '@nitpicker/core/create-isolated-cluster-run';
|
|
62
|
+
|
|
63
|
+
// 候補 10, 20, 30。ページ 40 は 30 へのリダイレクト元で候補ではない
|
|
64
|
+
const run = createIsolatedClusterRun([10, 20, 30], [40, 30]);
|
|
65
|
+
run.unionEdges([10, 20, 20, 40]);
|
|
66
|
+
run.labels(); // Uint32Array [0, 0, 0]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
保存済み HTML スナップショットのセレクタ照合は、`matchSelectorSnapshots` がバッチ(`page_html_blobs` の行)を受け取り、zstd の展開から判定までをスレッドプールで並列に行って、スナップショットごとの判定を返します。同時に展開済みの文書を持つのは、スレッド数ぶんだけです。zstd のスナップショット 1 件の展開後は `MAX_DECODED_SNAPSHOT_BYTES`(`@nitpicker/core/max-decoded-snapshot-bytes`、約 1.5 GiB)までで、超えると「不一致」にせず、`snapshot <n> could not be decoded` で呼び出し全体を reject します。zstd のフレームヘッダが述べる展開後サイズは省略も偽りもできるので、展開しながら出てきたバイト数を数えて止めます。上限はクローラーが保存できる最大(V8 の文字列の最大長 2^29 − 24 code unit × UTF-8 の最大 3 バイト)なので、クローラーが保存したスナップショットが上限で弾かれることはありません。圧縮せずに保存されたスナップショットには掛けません(`searchHtmlSnapshots` も同じ)。1 文書だけを同期で判定する `matchSelectorHtml` は、テストとベンチの段階比較のために出しています。判定は、必要なリテラルが順に現れるかの事前判定、開始タグだけで決まるセレクタの開始タグ走査、結合子と `:nth-*` のための開タグスタックの順で、最も安い厳密な手段を使います。タグ名と属性名は ASCII の大文字小文字だけを無視します。計画(`SelectorPlan`)の作り方と解釈規則は `@nitpicker/cli` の `docs/query.md` が正です。
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { matchSelectorSnapshots } from '@nitpicker/core/match-selector-snapshots';
|
|
73
|
+
|
|
74
|
+
const verdicts = await matchSelectorSnapshots({
|
|
75
|
+
plan,
|
|
76
|
+
snapshots: rows.map((row) => ({ body: row.body, codec: row.codec })),
|
|
77
|
+
});
|
|
78
|
+
verdicts[0]; // { matched: true, prefiltered: false, tokenized: false }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
保存済み HTML スナップショットの文字列・正規表現検索は、`searchHtmlSnapshots` がバッチを受け取り、展開から照合までを同じようにスレッドプールで並列に行って、スナップショットごとの `matchCount`(0 なら不一致)とスニペットを返します。パターンはスナップショットごとではなく 1 回だけコンパイルし、アドオンは直前にコンパイルしたパターンを保持して次のバッチでも使い回します(保持は 1 件だけなので、別のパターンの検索が並行すると入れ替わり、そのぶん再コンパイルします)。1 文書だけを同期で検索する `searchHtmlDocument` はテスト用、アーカイブを開く前にパターンを検証する `validateSearchPattern` は空文字列に一致するかを返し、構文エラーは `InvalidArg` で投げます。パターンは Rust の `regex` crate の構文(後方参照・先読み・後読みなし。照合時間は文書長に比例)で、マッチの単位は Unicode スカラー値、`\d` `\w` `\s` `\b` は Unicode の定義、`.` は `\r` と `\n` に一致せず、`m` の `^` `$` は CRLF を 1 つの行末として扱います。スニペットは、文書全体の空白を畳んでから窓を切ります。バッチの `bodies` は JS のバッファをコピーせず読むので、呼び出し側は promise が決着するまで書き換えないでください。永続化される値ではないので `(?-u)` は使わず、契約は `@nitpicker/cli` の `docs/query.md`(パターンの契約)が正です。
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { searchHtmlSnapshots } from '@nitpicker/core/search-html-snapshots';
|
|
85
|
+
|
|
86
|
+
const outcomes = await searchHtmlSnapshots({
|
|
87
|
+
pattern: {
|
|
88
|
+
source: 'fonts.example.org',
|
|
89
|
+
literal: true,
|
|
90
|
+
ignoreCase: false,
|
|
91
|
+
multiLine: false,
|
|
92
|
+
dotAll: false,
|
|
93
|
+
},
|
|
94
|
+
snippetLength: 160,
|
|
95
|
+
snapshots: rows.map((row) => ({ body: row.body, codec: row.codec })),
|
|
35
96
|
});
|
|
97
|
+
outcomes[0]; // { matchCount: 2, snippet: '…fonts.example.org…' }
|
|
36
98
|
```
|
|
37
99
|
|
|
38
|
-
`
|
|
100
|
+
テンプレート分類は、呼び出し側(`@nitpicker/archive` の `resolvePageClusterKeys`)がコーパスを読んで順に渡して実行します。全ページのシグナル(URL パス・スタイルシート・ホスト)を渡してブロック分け(Pass 0)し、続けて全ページの HTML をコーパス順に渡します。計算する呼び出しはアドオン内のスレッドプールで動き、Promise を返します。呼び出しは重ねず、1 つずつ await します。
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { createTemplateClassificationRun } from '@nitpicker/core/create-template-classification-run';
|
|
104
|
+
|
|
105
|
+
const run = createTemplateClassificationRun();
|
|
106
|
+
run.pushSignals([
|
|
107
|
+
{ paths: ['news', '1'], stylesheetHrefs: [] },
|
|
108
|
+
{ paths: ['news', '2'], stylesheetHrefs: [] },
|
|
109
|
+
]);
|
|
110
|
+
await run.block();
|
|
111
|
+
await run.pushPages([
|
|
112
|
+
{ html: '<body><main><p>a</p></main></body>' },
|
|
113
|
+
{ html: '<body><main><p>b</p></main></body>' },
|
|
114
|
+
]);
|
|
115
|
+
await run.finishPages();
|
|
116
|
+
const { keys, keyOfPage, reasons } = await run.takeOutput();
|
|
117
|
+
keys[keyOfPage[0]!]; // '["path:news","cluster:0"]'
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
ブロックは既定で全ページをクラスタリングします。次の 2 つの場合だけ、そのブロックを `TEMPLATE_BLOCK_SAMPLE_SIZE`(`@nitpicker/core/template-block-sample-size`)ページのサンプルでクラスタリングします。
|
|
121
|
+
|
|
122
|
+
- Stage A の類似度表(`n * (n - 1) / 2 * 8` バイト、`n` はブロックのページ数)が `memoryBudget` を超える。`memoryBudget` は `createTemplateClassificationRun` のオプションで、既定は `TEMPLATE_MEMORY_BUDGET`(`@nitpicker/core/template-memory-budget`、512 MiB)、0 なら全ブロックが対象になる。
|
|
123
|
+
- 全ページで計算するブロックの合計が `TEMPLATE_EXACT_PAGE_LIMIT`(`@nitpicker/core/template-exact-page-limit`、20,000 ページ)を超える。この場合は、標本より大きい(100 ページを超える)ブロックを、ページ数が多い順(同数なら出現順)に、収まるまで対象にする。100 ページ以下のブロックはサンプルにしても保持ページが減らないので対象にならず、それだけで上限を超えるときは超えたまま保持する。上限は固定で、利用者は変えられない。
|
|
124
|
+
|
|
125
|
+
既定の 512 MiB は、約 11,500 ページのブロックまでの類似度表に当たります。1 ページの解析結果は最大で約 60 KB です。同じ切り取りの深さはトークンを共有し、本文は圧縮して持つので、通常はこれより小さくなります。20,000 ページなら最大で約 1.2 GB を、ブロックの計算が終わるまで抱えます。どのブロックが落ちるかで保存される `templateKey` が決まるので、メモリ上限を変えると結果が変わります。既存のアーカイブは、`viewer-build` で再分類するまで以前のキーのままです。
|
|
126
|
+
|
|
127
|
+
サンプルに落としたブロックの残りのページは、2 回目の読み込み(`needsAssignment()` が true のときの `pushAssignmentPages`)で最も近いクラスタに割り当てます。落としたブロックは、`block()` の間に `block-sampled` の進捗イベントで 1 つずつ通知され、`takeOutput()` の `sampledBlocks` にも、理由(`table-over-budget` か `pages-over-limit`)つきで残ります。どちらの読み込みでも全ページをコーパス順に渡しますが、`pageMask()` が 0 のページは HTML を空文字にして構いません(HTML を読み出す必要もありません)。
|
|
128
|
+
|
|
129
|
+
### スレッドプール・同時実行数・キャンセル
|
|
130
|
+
|
|
131
|
+
非同期のネイティブ呼び出し(`matchSelectorSnapshots`・`searchHtmlSnapshots`・テンプレート分類の各呼び出し)は、アドオンが持つ専用のスレッドプールで動きます。次の 2 つの環境変数で調整できます。値は正の整数で、プロセスで最初にプールを使うときに 1 回だけ読みます。正の整数でない値は、既定値に置き換えずにエラーにします。
|
|
132
|
+
|
|
133
|
+
| 環境変数 | 既定 | 内容 |
|
|
134
|
+
| ------------------------------ | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
135
|
+
| `NITPICKER_NATIVE_THREADS` | rayon の既定(`RAYON_NUM_THREADS`、なければ論理コア数) | プールのスレッド数。同期の `computeUrlSortRanks` もこのプールで動く |
|
|
136
|
+
| `NITPICKER_NATIVE_CONCURRENCY` | 1 | 同時に走らせる非同期呼び出しの数。1 つの呼び出しがすでに全スレッドを使うので、増やしても合計の処理量は増えない。同期の `computeUrlSortRanks` はこの上限に数えず、待たずに専用プールを使う |
|
|
137
|
+
|
|
138
|
+
上限を超えた呼び出しは、拒否せず、受け付けた順(FIFO)に待ちます。待っている間はスレッドを占有しません。`checkNativeExecutorConfig` は、環境変数の誤りを最初のネイティブ呼び出しではなく作業の開始前に検出します。
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { checkNativeExecutorConfig } from '@nitpicker/core/check-native-executor-config';
|
|
142
|
+
|
|
143
|
+
checkNativeExecutorConfig(); // NITPICKER_NATIVE_THREADS="lots" なら InvalidArg で投げる
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`matchSelectorSnapshots` と `searchHtmlSnapshots` は `signal`(`AbortSignal`)を受け取ります。abort すると、実行中の呼び出しは次のスナップショットの前か展開中のスナップショットの次の 1 MiB を読む前で止まり、待機中の呼び出しは順番が来た時点で終わります。どちらも `signal.reason` で reject します。すでに abort 済みの signal は、アドオンを呼ばずに reject します。同じバッチに展開に失敗したスナップショットがあっても、abort を見たスナップショットが 1 つでもあれば abort(`signal.reason`)を返します。展開のエラーを返すのは、どのスナップショットも abort を見る前に失敗したときだけです。テンプレート分類は、これまでどおり `run.cancel()` で止めます。
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
const controller = new AbortController();
|
|
150
|
+
const pending = matchSelectorSnapshots({ plan, snapshots, signal: controller.signal });
|
|
151
|
+
controller.abort(new Error('client went away'));
|
|
152
|
+
await pending; // rejects with the Error above
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## 開発
|
|
156
|
+
|
|
157
|
+
Rust ツールチェーンが必要です(バージョンはリポジトリルートの `rust-toolchain.toml` で固定。セットアップは [CONTRIBUTING.md](../../../CONTRIBUTING.md) を参照)。`yarn build` を実行すると、`scripts/build-native.mjs` がホスト向けのアドオンをビルドしてパッケージ直下に `core.<platform>.node` として置きます。ローダーはこのローカルビルドを先に探し、なければプラットフォーム別パッケージを読み込みます。
|
|
158
|
+
|
|
159
|
+
- Rust のコードは `crates/` 配下の Cargo workspace にあります。napi に依存するのは `nitpicker_napi`(公開関数だけの薄い層)だけで、計算本体の crate は `cargo test` だけでテストできます
|
|
160
|
+
- `yarn test:rust` で Rust のテスト、`yarn lint` で rustfmt と clippy も実行します
|
|
161
|
+
- body hash は既存アーカイブに保存済みの値と byte 単位で一致させています(例外は、本物の body より前に `<bodyguard>` のような `<body` で始まるカスタム要素があるページで、body の開始位置が変わるため値が変わります。#434)。`crates/nitpicker_html_scan/tests/fixtures/body-hash-golden.json` は `@nitpicker/archive` の `src/body-hash/` にある JavaScript 段階実装(parity テストの oracle)から生成した golden fixture です。Rust 側は正規化とマスクを 1 パスにまとめていて、段階版の Rust 関数と一致することを `tests/fused_body_hash_properties.rs` で確かめています
|
|
162
|
+
- 構造シグナルの技術検出(`crates/nitpicker_tech_detect`)は、既存アーカイブの `technology_signals` を書いた JavaScript 正規表現と同じシグナル・同じ順序・同じ evidence を返します。正規表現は JS の `u` フラグなしの意味論に合わせ、`\s` は JS が含める非 ASCII の空白(U+00A0・U+FEFF など)まで並べています。走査は、定義ごとの必須リテラルを Aho-Corasick で 1 回探し、見つかった定義だけを正規表現で確かめます。`tests/fixtures/technology-signals-golden.json` は `@nitpicker/archive` の `src/meta/technologies/structural-signal-definitions.ts`(parity テストの oracle)から `scripts/generate-technology-signals-golden.mjs` で生成した golden fixture です
|
|
163
|
+
- テンプレート分類(`crates/nitpicker_template_cluster`)も、既存アーカイブの `templateKey` と reason に一致させています。HTML は htmlparser2 12.0.0 の挙動を移植した `crates/nitpicker_html_parser` で読み、スタイルシートのホストは呼び出し側の `new URL(href).host` を使います。一致の確認は `@nitpicker/archive` の golden(`src/template-classification/golden/`)で行います
|
|
164
|
+
- URL の順序(`crates/nitpicker_url_key`)は、文字列から数値を取り出して 2 件ずつ比べる方式を採りません。その方式は推移律が崩れる(`a9` は `a10` より前、`a10` は `a1x` より前、`a1x` は `a9` より前になる)ためで、順序はバイト列キーの全順序として定義します。基準は、`tests/order_spec.rs`(期待順序の表)と `tests/order_properties.rs`(入力順不変・キーが同じ URL は生の URL 文字列の順・数値順などのプロパティテスト)です。順位は viewer の read model に保存されるので、順序の定義を変えるときは read model の schema version を上げます
|
|
165
|
+
- alias キー(`crates/nitpicker_url_key` の `alias_keys`)は、既存アーカイブの `content_items.alias_of_id` を作った JavaScript 実装のキーと一致させています。`tests/fixtures/alias-keys-golden.json` は `@nitpicker/archive` の `src/url-alias/` にある JavaScript 実装(parity テストの oracle)から `scripts/generate-alias-keys-golden.mjs` で生成した golden fixture で、キーは URL を書かれたまま(WHATWG URL の `pathname` と `search`)読むので、並べ替え用のキーとは別の読み方です。グルーピングと代表選定は、JavaScript の参照実装と突き合わせる parity テスト(`@nitpicker/archive` の `group-alias-candidates.parity.spec.ts`)で固めています
|
|
166
|
+
- 連結成分(`crates/nitpicker_union_find`)の成分は、辺の順序とバッチ分割に依存しません(`tests/components_properties.rs`)。成分の番号は候補の並び順だけで決まり、read model の `component_id` になるので、候補は page id の昇順で渡します
|
|
167
|
+
- セレクタ照合(`crates/nitpicker_html_scan` の `html_matches_selector` と `scan_snapshots`)の結果は永続化されないので、`docs/query.md` の解釈規則を正解にして書いています。`tag_tokenizer.rs` がタグを読み、direct 段(`run_direct_match.rs`)と開タグスタック(`run_selector_state_machine.rs`)が同じトークナイザを使います。開始タグの属性は、ブラウザのシリアライザの出力を読み戻せるよう HTML のトークナイザと同じ規則で読みます(`start_tag_attributes.rs`。ソースの引用符が欠けたページは属性名に `"` を含んで直列化されるため、引用符の対応だけでは開始タグの終わりを見失います)。正解は `@nitpicker/query` の oracle spec(直列化前の DOM の `querySelector`)です
|
|
168
|
+
- `crates/nitpicker_html_parser` には生成した固定データが 2 つあります。どちらも、リポジトリが依存していない固定版のパッケージを scratch ディレクトリに入れ、そのディレクトリを引数に渡して生成します(手順と理由は各スクリプトの冒頭)
|
|
169
|
+
- エンティティの表 `src/html_decode_tree.rs`: `node scripts/generate-html-decode-tree.mjs <entities@8.0.0 のディレクトリ>`。デコードを意図して変えるときだけ再生成します
|
|
170
|
+
- パーサーの比較基準 `tests/htmlparser2-fixture-events.tsv`: `node scripts/record-htmlparser2-events.mjs <htmlparser2@12.0.0 のディレクトリ>`。`@nitpicker/archive` の `page-cluster/__fixtures__/` を変えたときに取り直します
|
|
39
171
|
|
|
40
172
|
## 関連リンク
|
|
41
173
|
|
|
42
174
|
- [Nitpicker README](../../../README.md)
|
|
43
|
-
- [CLI analyze docs](../cli/docs/analyze.md)
|
|
44
|
-
- [CONTRIBUTING.md](../../../CONTRIBUTING.md)
|
|
45
175
|
- [ARCHITECTURE.md](../../../ARCHITECTURE.md)
|
|
46
176
|
|
|
47
177
|
## ライセンス
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks that a row id survives being stored in a `u32` array: a typed array
|
|
3
|
+
* wraps `2**32` to `0` and truncates `1.5` to `1`, which here would join the
|
|
4
|
+
* wrong pages without any error.
|
|
5
|
+
* @param value - The id (a database row id).
|
|
6
|
+
* @param what - What the id is, for the error message.
|
|
7
|
+
* @returns `value`, unchanged.
|
|
8
|
+
* @throws {RangeError} When `value` is not an integer in `0..=4294967294`.
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* assertU32Id(7, 'candidate id'); // 7
|
|
12
|
+
* assertU32Id(2 ** 32, 'candidate id'); // throws RangeError
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare function assertU32Id(value: number, what: string): number;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** The addon reads `4294967295` as "none", so no id may take that value. */
|
|
2
|
+
const MAX_ID = 4_294_967_294;
|
|
3
|
+
/**
|
|
4
|
+
* Checks that a row id survives being stored in a `u32` array: a typed array
|
|
5
|
+
* wraps `2**32` to `0` and truncates `1.5` to `1`, which here would join the
|
|
6
|
+
* wrong pages without any error.
|
|
7
|
+
* @param value - The id (a database row id).
|
|
8
|
+
* @param what - What the id is, for the error message.
|
|
9
|
+
* @returns `value`, unchanged.
|
|
10
|
+
* @throws {RangeError} When `value` is not an integer in `0..=4294967294`.
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* assertU32Id(7, 'candidate id'); // 7
|
|
14
|
+
* assertU32Id(2 ** 32, 'candidate id'); // throws RangeError
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export function assertU32Id(value, what) {
|
|
18
|
+
if (!Number.isInteger(value) || value < 0 || value > MAX_ID) {
|
|
19
|
+
throw new RangeError(`${what}: ${value} is not an integer in 0..=${MAX_ID}`);
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the addon's thread pool from its environment settings now, so an
|
|
3
|
+
* invalid value fails before any work starts instead of at the first native
|
|
4
|
+
* call that needs the pool.
|
|
5
|
+
*
|
|
6
|
+
* - `NITPICKER_NATIVE_THREADS`: threads of the pool. Unset: rayon's default
|
|
7
|
+
* (`RAYON_NUM_THREADS`, else the number of logical cores).
|
|
8
|
+
* - `NITPICKER_NATIVE_CONCURRENCY`: asynchronous native calls (match-selector,
|
|
9
|
+
* search-html, template classification) that run at once. Unset: 1. A call
|
|
10
|
+
* over the limit waits its turn; it is never refused.
|
|
11
|
+
*
|
|
12
|
+
* Both are read once, when the pool is first built; changing them afterwards
|
|
13
|
+
* has no effect in that process.
|
|
14
|
+
* @throws {Error} With code `InvalidArg`, naming the variable and its value,
|
|
15
|
+
* when a setting is not a positive integer.
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* checkNativeExecutorConfig(); // throws before the crawl starts if the env is wrong
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function checkNativeExecutorConfig(): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { nativeBinding } from './native-binding.js';
|
|
2
|
+
/**
|
|
3
|
+
* Builds the addon's thread pool from its environment settings now, so an
|
|
4
|
+
* invalid value fails before any work starts instead of at the first native
|
|
5
|
+
* call that needs the pool.
|
|
6
|
+
*
|
|
7
|
+
* - `NITPICKER_NATIVE_THREADS`: threads of the pool. Unset: rayon's default
|
|
8
|
+
* (`RAYON_NUM_THREADS`, else the number of logical cores).
|
|
9
|
+
* - `NITPICKER_NATIVE_CONCURRENCY`: asynchronous native calls (match-selector,
|
|
10
|
+
* search-html, template classification) that run at once. Unset: 1. A call
|
|
11
|
+
* over the limit waits its turn; it is never refused.
|
|
12
|
+
*
|
|
13
|
+
* Both are read once, when the pool is first built; changing them afterwards
|
|
14
|
+
* has no effect in that process.
|
|
15
|
+
* @throws {Error} With code `InvalidArg`, naming the variable and its value,
|
|
16
|
+
* when a setting is not a positive integer.
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* checkNativeExecutorConfig(); // throws before the crawl starts if the env is wrong
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function checkNativeExecutorConfig() {
|
|
23
|
+
nativeBinding.checkNativeExecutorConfig();
|
|
24
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AliasKeys } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Computes the two keys under which pages are grouped as the same page under
|
|
4
|
+
* another URL, parsing `url` once.
|
|
5
|
+
*
|
|
6
|
+
* Both keys read the URL as written (`pathname` and `search` as the WHATWG
|
|
7
|
+
* URL serializes them) and are `host[:port]path[?query]`:
|
|
8
|
+
*
|
|
9
|
+
* - **Tier A** folds what is safe to treat as one resource from the URL
|
|
10
|
+
* alone: the scheme (a default port is left out, so `http` and `https`
|
|
11
|
+
* share a key), the host's case, and a trailing `/index.{ext}`.
|
|
12
|
+
* - **Tier B** is Tier A with one trailing `/` of the path dropped (the path
|
|
13
|
+
* `/` stays): a weaker signal, so a caller confirms it by comparing the
|
|
14
|
+
* page content.
|
|
15
|
+
*
|
|
16
|
+
* The fragment is never part of a key, and an empty query is no query.
|
|
17
|
+
* @param url - The URL string.
|
|
18
|
+
* @returns Both keys, or `null` for a string that does not parse and for a
|
|
19
|
+
* URL that is not `http` or `https` (such a URL is left out of alias
|
|
20
|
+
* grouping).
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* computeAliasKeys('http://Example.com/about/index.html');
|
|
24
|
+
* // { tierA: 'example.com/about/', tierB: 'example.com/about' }
|
|
25
|
+
* computeAliasKeys('mailto:a@example.com'); // null
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function computeAliasKeys(url: string): AliasKeys | null;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { nativeBinding } from './native-binding.js';
|
|
2
|
+
/**
|
|
3
|
+
* Computes the two keys under which pages are grouped as the same page under
|
|
4
|
+
* another URL, parsing `url` once.
|
|
5
|
+
*
|
|
6
|
+
* Both keys read the URL as written (`pathname` and `search` as the WHATWG
|
|
7
|
+
* URL serializes them) and are `host[:port]path[?query]`:
|
|
8
|
+
*
|
|
9
|
+
* - **Tier A** folds what is safe to treat as one resource from the URL
|
|
10
|
+
* alone: the scheme (a default port is left out, so `http` and `https`
|
|
11
|
+
* share a key), the host's case, and a trailing `/index.{ext}`.
|
|
12
|
+
* - **Tier B** is Tier A with one trailing `/` of the path dropped (the path
|
|
13
|
+
* `/` stays): a weaker signal, so a caller confirms it by comparing the
|
|
14
|
+
* page content.
|
|
15
|
+
*
|
|
16
|
+
* The fragment is never part of a key, and an empty query is no query.
|
|
17
|
+
* @param url - The URL string.
|
|
18
|
+
* @returns Both keys, or `null` for a string that does not parse and for a
|
|
19
|
+
* URL that is not `http` or `https` (such a URL is left out of alias
|
|
20
|
+
* grouping).
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* computeAliasKeys('http://Example.com/about/index.html');
|
|
24
|
+
* // { tierA: 'example.com/about/', tierB: 'example.com/about' }
|
|
25
|
+
* computeAliasKeys('mailto:a@example.com'); // null
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function computeAliasKeys(url) {
|
|
29
|
+
return nativeBinding.computeAliasKeys(url);
|
|
30
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Computes the content hash stored as `page_meta.body_hash`: SHA-256 of the
|
|
3
|
+
* page's `<body>` content after collapsing `/index.{ext}` URL forms to `/`
|
|
4
|
+
* and masking mixed letter-and-digit tokens of 8+ characters (cache-busting
|
|
5
|
+
* hashes, session ids, CSS-module suffixes).
|
|
6
|
+
*
|
|
7
|
+
* Takes UTF-8 bytes rather than a string so the native side never builds a
|
|
8
|
+
* UTF-16 copy. The result is byte-identical to the hashes already stored in
|
|
9
|
+
* existing archives, which JavaScript computed (golden fixture in
|
|
10
|
+
* `crates/nitpicker_html_scan/tests/fixtures/`), so old and new crawls stay
|
|
11
|
+
* comparable.
|
|
12
|
+
* @param html - The full HTML document (or a fragment) as UTF-8 bytes —
|
|
13
|
+
* `Buffer.from(html, 'utf8')`.
|
|
14
|
+
* @returns The 32-byte hash, ready to insert into a `BLOB` column.
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const a = computeBodyHash(Buffer.from('<body><a href="/p/a1b2c3d4">x</a></body>'));
|
|
18
|
+
* const b = computeBodyHash(Buffer.from('<body><a href="/p/z9y8x7w6">x</a></body>'));
|
|
19
|
+
* a.equals(b); // true — the differing token is masked before hashing
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function computeBodyHash(html: Uint8Array): Buffer;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { nativeBinding } from './native-binding.js';
|
|
2
|
+
/**
|
|
3
|
+
* Computes the content hash stored as `page_meta.body_hash`: SHA-256 of the
|
|
4
|
+
* page's `<body>` content after collapsing `/index.{ext}` URL forms to `/`
|
|
5
|
+
* and masking mixed letter-and-digit tokens of 8+ characters (cache-busting
|
|
6
|
+
* hashes, session ids, CSS-module suffixes).
|
|
7
|
+
*
|
|
8
|
+
* Takes UTF-8 bytes rather than a string so the native side never builds a
|
|
9
|
+
* UTF-16 copy. The result is byte-identical to the hashes already stored in
|
|
10
|
+
* existing archives, which JavaScript computed (golden fixture in
|
|
11
|
+
* `crates/nitpicker_html_scan/tests/fixtures/`), so old and new crawls stay
|
|
12
|
+
* comparable.
|
|
13
|
+
* @param html - The full HTML document (or a fragment) as UTF-8 bytes —
|
|
14
|
+
* `Buffer.from(html, 'utf8')`.
|
|
15
|
+
* @returns The 32-byte hash, ready to insert into a `BLOB` column.
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* const a = computeBodyHash(Buffer.from('<body><a href="/p/a1b2c3d4">x</a></body>'));
|
|
19
|
+
* const b = computeBodyHash(Buffer.from('<body><a href="/p/z9y8x7w6">x</a></body>'));
|
|
20
|
+
* a.equals(b); // true — the differing token is masked before hashing
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function computeBodyHash(html) {
|
|
24
|
+
return nativeBinding.computeBodyHash(html);
|
|
25
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ranks URLs in the natural order the viewer lists them in: numbers inside
|
|
3
|
+
* a path compare by value (`page2` before `page10`), letter case and
|
|
4
|
+
* percent-escapes do not split the order, a directory's own page comes
|
|
5
|
+
* before its index pages and its children, and the host decides before the
|
|
6
|
+
* path. The full definition is the `nitpicker_url_key` crate's docs.
|
|
7
|
+
*
|
|
8
|
+
* The order is a total order: the same URLs in any input order get the same
|
|
9
|
+
* ranking, and identical URLs rank in input order. It is persisted (the
|
|
10
|
+
* viewer read model stores each page's rank), so a change to it needs a read
|
|
11
|
+
* model schema version bump.
|
|
12
|
+
*
|
|
13
|
+
* Parses each URL once, natively, and sorts on the native thread pool; only
|
|
14
|
+
* two buffers cross the boundary however many URLs there are.
|
|
15
|
+
* @param urls - The URLs to rank. Each must be a well-formed string (a lone
|
|
16
|
+
* surrogate is written as U+FFFD).
|
|
17
|
+
* @returns For each input position, the rank (zero-based position in the
|
|
18
|
+
* sorted list) of that URL — a permutation of `0..urls.length`.
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* computeUrlSortRanks([
|
|
22
|
+
* 'https://example.com/page10',
|
|
23
|
+
* 'https://example.com/page2',
|
|
24
|
+
* 'https://example.com/',
|
|
25
|
+
* ]); // Uint32Array [2, 1, 0]
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function computeUrlSortRanks(urls: readonly string[]): Uint32Array;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { nativeBinding } from './native-binding.js';
|
|
2
|
+
/** The native call addresses the URL bytes with `u32` offsets. */
|
|
3
|
+
const MAX_URL_BYTES = 4_294_967_295;
|
|
4
|
+
/**
|
|
5
|
+
* Ranks URLs in the natural order the viewer lists them in: numbers inside
|
|
6
|
+
* a path compare by value (`page2` before `page10`), letter case and
|
|
7
|
+
* percent-escapes do not split the order, a directory's own page comes
|
|
8
|
+
* before its index pages and its children, and the host decides before the
|
|
9
|
+
* path. The full definition is the `nitpicker_url_key` crate's docs.
|
|
10
|
+
*
|
|
11
|
+
* The order is a total order: the same URLs in any input order get the same
|
|
12
|
+
* ranking, and identical URLs rank in input order. It is persisted (the
|
|
13
|
+
* viewer read model stores each page's rank), so a change to it needs a read
|
|
14
|
+
* model schema version bump.
|
|
15
|
+
*
|
|
16
|
+
* Parses each URL once, natively, and sorts on the native thread pool; only
|
|
17
|
+
* two buffers cross the boundary however many URLs there are.
|
|
18
|
+
* @param urls - The URLs to rank. Each must be a well-formed string (a lone
|
|
19
|
+
* surrogate is written as U+FFFD).
|
|
20
|
+
* @returns For each input position, the rank (zero-based position in the
|
|
21
|
+
* sorted list) of that URL — a permutation of `0..urls.length`.
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* computeUrlSortRanks([
|
|
25
|
+
* 'https://example.com/page10',
|
|
26
|
+
* 'https://example.com/page2',
|
|
27
|
+
* 'https://example.com/',
|
|
28
|
+
* ]); // Uint32Array [2, 1, 0]
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function computeUrlSortRanks(urls) {
|
|
32
|
+
// One encoding pass into a buffer sized for the worst case (3 UTF-8 bytes
|
|
33
|
+
// per UTF-16 unit), rather than measuring every URL and encoding it again.
|
|
34
|
+
let capacity = 0;
|
|
35
|
+
for (const url of urls) {
|
|
36
|
+
capacity += url.length * 3;
|
|
37
|
+
}
|
|
38
|
+
const bytes = Buffer.allocUnsafe(capacity);
|
|
39
|
+
const offsets = new Uint32Array(urls.length + 1);
|
|
40
|
+
let total = 0;
|
|
41
|
+
for (const [index, url] of urls.entries()) {
|
|
42
|
+
total += bytes.write(url, total, 'utf8');
|
|
43
|
+
if (total > MAX_URL_BYTES) {
|
|
44
|
+
throw new RangeError(`The URLs take more than ${MAX_URL_BYTES} bytes (4 GiB), which the native call cannot address.`);
|
|
45
|
+
}
|
|
46
|
+
offsets[index + 1] = total;
|
|
47
|
+
}
|
|
48
|
+
return nativeBinding.computeUrlSortRanks(bytes.subarray(0, total), offsets);
|
|
49
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { IsolatedClusterRun } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Starts the computation of the connected components of the isolated pages:
|
|
4
|
+
* the inventory pages that are linked to only from other inventory pages.
|
|
5
|
+
*
|
|
6
|
+
* The caller reads the link edges (`anchor_edges`) from SQLite batch by batch
|
|
7
|
+
* and hands each batch to {@link IsolatedClusterRun.unionEdges} as it
|
|
8
|
+
* arrives, so the edges are never held: the memory follows the number of pages,
|
|
9
|
+
* not the number of links. An edge to a redirect source or an alias member
|
|
10
|
+
* counts as an edge to the page it finally lands on; a chain that never ends
|
|
11
|
+
* (it loops, or leads into a loop) drops the edge. The components do not
|
|
12
|
+
* depend on the order of the edges or how they were batched.
|
|
13
|
+
*
|
|
14
|
+
* The components are numbered in the order of the candidates: the first
|
|
15
|
+
* candidate's component is `0`, and each later component takes the next
|
|
16
|
+
* number the first time one of its candidates appears. Hand the candidates
|
|
17
|
+
* over in ascending page id, which is the numbering the viewer read model
|
|
18
|
+
* stores as `component_id`.
|
|
19
|
+
* @param candidateIds - The candidate page ids, in the order the components
|
|
20
|
+
* are numbered in; each an integer in `0..=4294967294`.
|
|
21
|
+
* @param redirects - The redirect and alias hops of the whole archive,
|
|
22
|
+
* `[source, destination, source, destination, ...]` by page id. A source
|
|
23
|
+
* listed twice keeps its last hop.
|
|
24
|
+
* @returns The run.
|
|
25
|
+
* @throws {RangeError} When an id is not an integer in `0..=4294967294`
|
|
26
|
+
* (`Uint32Array` would otherwise wrap it silently).
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* // Candidates 10, 20, 30; page 40 redirects to 30 and is not a candidate.
|
|
30
|
+
* const run = createIsolatedClusterRun([10, 20, 30], [40, 30]);
|
|
31
|
+
* run.unionEdges([10, 20, 20, 40]);
|
|
32
|
+
* run.labels(); // Uint32Array [0, 0, 0]
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function createIsolatedClusterRun(candidateIds: ArrayLike<number>, redirects: ArrayLike<number>): IsolatedClusterRun;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { nativeBinding } from './native-binding.js';
|
|
2
|
+
import { toU32Ids } from './to-u32-ids.js';
|
|
3
|
+
/**
|
|
4
|
+
* Starts the computation of the connected components of the isolated pages:
|
|
5
|
+
* the inventory pages that are linked to only from other inventory pages.
|
|
6
|
+
*
|
|
7
|
+
* The caller reads the link edges (`anchor_edges`) from SQLite batch by batch
|
|
8
|
+
* and hands each batch to {@link IsolatedClusterRun.unionEdges} as it
|
|
9
|
+
* arrives, so the edges are never held: the memory follows the number of pages,
|
|
10
|
+
* not the number of links. An edge to a redirect source or an alias member
|
|
11
|
+
* counts as an edge to the page it finally lands on; a chain that never ends
|
|
12
|
+
* (it loops, or leads into a loop) drops the edge. The components do not
|
|
13
|
+
* depend on the order of the edges or how they were batched.
|
|
14
|
+
*
|
|
15
|
+
* The components are numbered in the order of the candidates: the first
|
|
16
|
+
* candidate's component is `0`, and each later component takes the next
|
|
17
|
+
* number the first time one of its candidates appears. Hand the candidates
|
|
18
|
+
* over in ascending page id, which is the numbering the viewer read model
|
|
19
|
+
* stores as `component_id`.
|
|
20
|
+
* @param candidateIds - The candidate page ids, in the order the components
|
|
21
|
+
* are numbered in; each an integer in `0..=4294967294`.
|
|
22
|
+
* @param redirects - The redirect and alias hops of the whole archive,
|
|
23
|
+
* `[source, destination, source, destination, ...]` by page id. A source
|
|
24
|
+
* listed twice keeps its last hop.
|
|
25
|
+
* @returns The run.
|
|
26
|
+
* @throws {RangeError} When an id is not an integer in `0..=4294967294`
|
|
27
|
+
* (`Uint32Array` would otherwise wrap it silently).
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* // Candidates 10, 20, 30; page 40 redirects to 30 and is not a candidate.
|
|
31
|
+
* const run = createIsolatedClusterRun([10, 20, 30], [40, 30]);
|
|
32
|
+
* run.unionEdges([10, 20, 20, 40]);
|
|
33
|
+
* run.labels(); // Uint32Array [0, 0, 0]
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function createIsolatedClusterRun(candidateIds, redirects) {
|
|
37
|
+
if (redirects.length % 2 !== 0) {
|
|
38
|
+
throw new RangeError('redirects must hold an even number of ids');
|
|
39
|
+
}
|
|
40
|
+
const native = new nativeBinding.IsolatedClusterRun(toU32Ids(candidateIds, 'candidate ids'), toU32Ids(redirects, 'redirects'));
|
|
41
|
+
return {
|
|
42
|
+
unionEdges(edges) {
|
|
43
|
+
if (edges.length % 2 !== 0) {
|
|
44
|
+
throw new RangeError('edges must hold an even number of ids');
|
|
45
|
+
}
|
|
46
|
+
native.unionEdges(toU32Ids(edges, 'edges'));
|
|
47
|
+
},
|
|
48
|
+
labels() {
|
|
49
|
+
return native.labels();
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|