@wcstack/state 2.1.0 → 2.2.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.ja.md CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  - 変数を取り出す API
16
16
  - 要素ごとに状態を束縛するオブジェクト
17
- - hook
17
+ - hook(`useState` / `useStore` 系。`$connectedCallback` などのライフサイクルコールバックはこれに当たりません)
18
18
  - selector
19
19
  - reactive primitive をコンポーネントへ引き込むための glue code
20
20
 
@@ -22,11 +22,11 @@
22
22
 
23
23
  なぜなら、このライブラリでは UI と状態の結合点を JavaScript の中に置かないからです。状態を「取り出して」コンポーネントへ渡すのではなく、HTML 側がパス文字列によって状態を参照します。要素は状態を所有せず、状態も要素を知りません。両者が共有するのはパスだけです。
24
24
 
25
- ## 既存FWとは比較しません
25
+ ## 位置づけ — 選ぶとき・選ばないとき
26
26
 
27
- これは React / Vue / Solid と同じ問題を別の方法で解いているのではありません。**前提自体が違います。**
27
+ これは React / Vue / Solid の別構文ではありません。あちらは UI と状態の結合点をコンポーネントの中に置き、こちらはパス文字列に置きます。**前提自体が違う**ので、比較は正しい軸で行ったときにだけ意味を持ちます。
28
28
 
29
- | 一般的なFWが前提にするもの | `@wcstack/state` が前提にするもの |
29
+ | コンポーネント型 FW が前提にするもの | `@wcstack/state` が前提にするもの |
30
30
  |---|---|
31
31
  | コンポーネントが UI と状態の結合点 | パス文字列が UI と状態の結合点 |
32
32
  | JavaScript が描画の中心 | HTML と DOM が中心 |
@@ -34,7 +34,16 @@
34
34
  | hook / selector / signal で購読する | 属性とパスで束縛する |
35
35
  | フレームワークの実行モデルにアプリ全体を載せる | ブラウザ標準の上に薄い reactive layer を足す |
36
36
 
37
- 比較表を作るより先に、この前提差を理解してください。同じ棚に置いても、解いている問題の切り取り方が違います。
37
+ より近い親戚は **属性ディレクティブ型・ビルド不要のライブラリ** — Alpine.js や petite-vue の系統です。前提(素の HTML への属性・コンパイラ不要)は共有しつつ、選択を分ける違いが 2 つあります。
38
+
39
+ - **式言語を持たない。** それらは属性に JavaScript 式を書き、実行時に評価します。`data-wcs` に載るのはパスとフィルタチェーンだけで、計算は state 側のパス getter に置きます。バインディングを静的に検査できる(`@wcstack/lint`・VS Code 拡張・`@wcstack/typescript`)のも、`unsafe-eval` なしの厳格な CSP で動く([docs/csp.ja.md](../../docs/csp.ja.md))のも、この選択の帰結です。
40
+ - **Web Components 同士を配線する。** wc-bindable・command-token・event-token の各プロトコルと `bind-component` マウントが、互いを import しない要素同士を接続します。[I/O ノード群](../../README.ja.md#追加パッケージ)はその上に成り立っています。
41
+
42
+ **選ぶとき**: HTML が主役のページ — サーバー描画や静的なマークアップにリアクティブな部分を足す、カスタム要素を組み合わせてページを作る、「HTML を読めばデータ依存が全部わかる」ことに価値があり、ビルド工程が前提ではなくコストであるとき。
43
+
44
+ **選ばないとき**: チームが既にコンポーネント型 FW の中で暮らしている — I/O ノードは[フレームワークアダプタ](../../docs/framework-adapter-integration.ja.md)経由で使ってください。ホットパスが巨大な keyed リスト — [パフォーマンス](#パフォーマンス)節の計測では生成・追加が [`@wcstack/signals`](../signals/) の 2.5〜3.5 倍で、相互運用できる signals のほうが適任です。テンプレートに式を書きたい — 意図的に存在しません。テンプレートの型検査をツールではなくコンパイラに求める — パスは文字列で、`@wcstack/typescript` は差を縮めますが埋めはしません。
45
+
46
+ この軸に乗せれば比較は具体的になります。下の[パフォーマンス](#パフォーマンス)節がその一例で、`e2e/bench/` のドライバで手元のハードウェアでも再現できます。
38
47
 
39
48
  ## 第一原理: パスが唯一の契約
40
49
 
@@ -95,6 +104,9 @@
95
104
  - **宣言的データバインディング** — `data-wcs` 属性によるプロパティ / テキスト / イベント / 構造バインディング
96
105
  - **リアクティブ Proxy** — ES Proxy による依存追跡付き自動 DOM 更新
97
106
  - **構造ディレクティブ** — `<template>` 要素による `for`, `if` / `elseif` / `else`
107
+ - **ボリューム** — `<wcs-state mount="cart">` がモジュールを 1 本の state ツリーに接ぎ木し、バインディングは `cart.…` で読む
108
+ - **行の同一性** — `$listKeys` が再取得した配列でも行の DOM と行オブジェクトを保つ
109
+ - **ワイルドカード集計** — `$getAll` / `$setAll` が配列を作り直さずに `items.*.price` を横断して読み書きする
98
110
  - **組み込みフィルタ** — フォーマット、比較、算術、日付など 46 種類
99
111
  - **双方向バインディング** — `<input>`, `<select>`, `<textarea>` で自動有効
100
112
  - **Web Component バインディング** — Shadow DOM コンポーネントとの双方向状態バインディング
@@ -105,8 +117,10 @@
105
117
  - **Mustache 構文** — テキストノードでの `{{ path|filter }}`
106
118
  - **複数の状態ソース** — JSON, JS モジュール, インラインスクリプト, API, 属性
107
119
  - **SVG サポート** — `<svg>` 要素内でのフルバインディング対応
108
- - **ライフサイクルフック** — `$connectedCallback` / `$disconnectedCallback` / `$updatedCallback`、Web Component 用 `$stateReadyCallback`
109
- - **TypeScript サポート** — `defineState()` によるドットパス自動補完付き型付き状態定義([詳細](docs/define-state.ja.md))
120
+ - **ライフサイクルフック** — `$connectedCallback` / `$disconnectedCallback` / `$updatedCallback` / `$errorCallback`、Web Component 用 `$stateReadyCallback`
121
+ - **headless watch** `$watch` はパスが描画されていてもいなくても state の変化で発火する
122
+ - **診断** — 解決しないパス・添字の本数・getter の循環を `@wcstack/lint`・VS Code 拡張と同じ診断 code で報告する
123
+ - **TypeScript サポート** — `defineState()` によるドットパス自動補完付き型付き状態定義([詳細](docs/define-state.ja.md))。`@wcstack/typescript` は同じ型を HTML の検証器へ運び(`wcs-schema`)、インライン state スクリプトを型検査する(`wcs-tsc`)— [docs/typescript.ja.md](../../docs/typescript.ja.md)
110
124
  - **サーバーサイドレンダリング** — `enable-ssr` 属性 + `@wcstack/server` でフル SSR と自動ハイドレーション
111
125
  - **依存ゼロ** — ランタイム依存なし
112
126
 
@@ -233,7 +247,7 @@
233
247
  <div data-wcs="textContent: cart.total"></div>
234
248
  ```
235
249
 
236
- ボリュームは getter・`$watch`・`$listKeys`・`$updatedCallback`・`$connectedCallback`/`$disconnectedCallback` を宣言できます(すべてマウントパス相対)。読み込み順は自由です(ルートより先に接続されたボリュームは、ルートの登録時に接ぎ木されます)。マウントパスは静的パスのみです(`*`・`$`・`#`・`@` は不可)。
250
+ ボリュームは getter・`$watch`・`$listKeys`・`$updatedCallback`・`$connectedCallback`/`$disconnectedCallback` を宣言できます(すべてマウントパス相対)。`$errorCallback` はルート専用です(バインディングの失敗はツリーの所有者へ 1 回だけ報告されます)。読み込み順は自由です(ルートより先に接続されたボリュームは、ルートの登録時に接ぎ木されます)。マウントパスは静的パスのみです(`*`・`$`・`#`・`@` は不可)。初期化後に `mount` 属性を変更することはできません — 変更は console 警告付きで無視されます。要素を取り除き、望むパスで新しい要素を追加してください。
237
251
 
238
252
  > **v1 の名前付き状態からの移行:** `<wcs-state name="cart">` + `total@cart` は `<wcs-state mount="cart">` + `cart.total` になります。v2 では `name` 属性は fail-fast し、パス中の `@` は parse error です(どちらもこの誘導文付き)。移行の対応表: [docs/state-mount-design.md](../../docs/state-mount-design.md) §9。
239
253
 
@@ -348,6 +362,8 @@ property[#modifier]: path[|filter[|filter(args)...]]
348
362
 
349
363
  ### バインディング authority (`#init=` / `#sync=`)
350
364
 
365
+ **これが解決する問題。** バインディングが attach する時点で既に値を持っている要素 —— 永続値をロード済みの `<wcs-storage>`、時計、自分のスナップショットを復元するウィジェット —— は state の初期値に上書きされます。双方向バインディングの初期同期が state→element に書くからです。そのバインディングにだけ `#init=element` を付けると、初期同期は*要素*が勝ちます。以後の変更は通常どおり両方向に流れます。このケース(load-before-bind)は下で具体的に説明します。この節の残りは、それを一例として含む一般規則です。
366
+
351
367
  `static wcBindable` を宣言したカスタム要素への prop バインディングは、**authority**(バインディング attach 時の**初期同期**をどちら側が勝つか)を解決します。定常時の方向は authority とは別に、メンバの宣言形状で決まります: output-only メンバは state からの書き込みを恒久的に受け付けず(契約)、双方向メンバは初期同期の勝者と無関係に以後は両方向に流れます。既定 authority はメンバの宣言位置から導出されます(`enableDirectionalInitialSync` で既定 ON):
352
368
 
353
369
  | メンバの宣言位置 | 既定 authority | 効果 |
@@ -359,6 +375,34 @@ property[#modifier]: path[|filter[|filter(args)...]]
359
375
 
360
376
  > **作法:** settable なメンバは **`properties` と `inputs` の両方**に宣言してください。`properties` にしか宣言されていないメンバは output-only 扱いになり、state→element 書き込みがバインディングの生存期間ずっと抑止され、要素側の初期値が state 側のシード値を上書きします(`@wcstack` の I/O ノード Shell と DCC の `$bindables` はこの作法に従っています)。
361
377
 
378
+ #### 要素から state に何が書かれるか(`properties[].getter`)
379
+
380
+ 要素が `properties[].event` を dispatch したとき、state に書かれる値は **`getter(event)`** です。`getter` を省略するとプロトコル既定の [`(e) => e.detail`](https://github.com/wc-bindable-protocol/wc-bindable-protocol/blob/main/SPEC.md#default-getter) が適用され、**`detail` 全体がそのまま**書かれます。このとき宣言したプロパティは要素から読まれ*ません*。イベントのペイロードが正です。`wcBindable` を持たない素の HTML 要素は逆で、`input`/`change` 時に `element[propName]` を読みます。
381
+
382
+ したがって `getter` なしで `detail: { value: 7654321 }` を dispatch する要素は、数値ではなく**オブジェクト** `{ value: 7654321 }` を state に書きます。しかもこの失敗はほぼ無言です。書き戻し(`Number({ value: … })` → `NaN`)は例外を投げず、`@wcstack/lint` にも見えません(ペイロードの形は静的に分かりません)。ランタイムは、イベント時点で見分けられる 2 形についてだけ、要素 × プロパティごとに 1 回警告します(`wcs/default-getter-mismatch`): 要素プロパティに値があるのに `detail` が `undefined`(素の `Event`、または `detail` の付け忘れ)、および要素プロパティがオブジェクトでないのに `detail` が `<propName>` キーを持つオブジェクト(上のラッパー)。それ以外の不整合は気づかれずに通り、どちらの場合も書き込み自体はそのまま行われます。次の 2 形のどちらかに揃えてください:
383
+
384
+ ```javascript
385
+ class YenInput extends HTMLElement {
386
+ static wcBindable = {
387
+ protocol: "wc-bindable", version: 1,
388
+ properties: [
389
+ // (a) 値そのものを detail にする — プロトコルの推奨形。getter 不要
390
+ { name: "value", event: "yen-input:value-changed" },
391
+ // (b) detail がオブジェクト、または CustomEvent でないイベントを使う — 読み方を宣言する
392
+ // { name: "value", event: "yen-input:value-changed", getter: (e) => e.detail.value },
393
+ // { name: "value", event: "input", getter: (e) => e.target.value },
394
+ ],
395
+ inputs: [{ name: "value" }],
396
+ };
397
+ #onInput() {
398
+ // (a): ラッパーオブジェクトではなく値を dispatch する
399
+ this.dispatchEvent(new CustomEvent("yen-input:value-changed", { detail: this.value, bubbles: true }));
400
+ }
401
+ }
402
+ ```
403
+
404
+ どちらを選んでも、`element.value` とイベントから取り出す値は同じ論理状態を表していなければなりません(プロトコルの *Producer State Consistency Invariant*)。初期同期はプロパティを読み、以後の更新はイベントを読むからです。wcstack 内部でも両形が使われています —— `<wcs-fetch>` の `loading` は `getter` なしで真偽値を `detail` に載せ、`value` は `getter` で `detail.value` を読みます。DCC の `$bindables` が `getter: (e) => e.target[name]` を宣言するのは、サブパス書き込みには `detail` に載せる単一の値が無いからです。既定そのものを変える予定はありません。`e.detail` はすべての wc-bindable アダプタに対する規範(`@wc-bindable/core` の `bind()` とフレームワークアダプタも同じ既定)で、プロトコル上、既定の変更は新しいプロトコル識別子を要する破壊的変更に分類されています。
405
+
362
406
  authority はバインディング単位で `#init=` により上書きできます:
363
407
 
364
408
  | 値 | 初期同期 | 使える宣言 |
@@ -902,6 +946,20 @@ export default {
902
946
 
903
947
  getter の例外は握り潰されません。評価された場所(バインディングの適用・`$watch` の評価・自分での読み取り)でそのまま表面化します。
904
948
 
949
+ #### 依存追跡の境界
950
+
951
+ 依存グラフに何が載るかは 3 つの規則で決まります。踏み越えるまで意識する必要はありませんが、踏み越えたときの症状は「値が更新されなくなる。エラーは出ない」なので、ここにまとめておきます:
952
+
953
+ | 規則 | 踏み越えたときの見え方 |
954
+ |---|---|
955
+ | **追跡されるのは `this` を通した *パス* の読み取りだけ。** `this.form` は `form` を、`this["form.name"]` は `form.name` を追跡する。`this.form.name` が追跡するのは **`form` だけ** —— `.name` は返ってきたオブジェクトへの素のプロパティアクセスでしかない。`Date.now()`・DOM・モジュール変数・クロージャで掴んだオブジェクトは何も登録しない | その入力に対して getter は二度と再評価されず、最初の値が残り続ける(上の例)。`this.form.name` を読む getter は、`<input data-wcs="value: form.name">` を編集しても再実行されない —— `this["form.name"]` で読む |
956
+ | **setter の中の読み取りは追跡しない。** setter は命令的な代入であって派生ではないので、その中で読んだものは何の依存にもならない | 何を書くかを `this.a` を読んで決める setter は、`a` が変わっても再実行されない。再実行されるのは getter だけ |
957
+ | **同値ガードはプリミティブにだけ効く。** 現在値と `Object.is` で等しいプリミティブの書き込みはキューに入る前に落とされる。オブジェクト・配列の書き込みは同じ参照でも必ず通る | 同じ文字列を再代入しても何も起きない。同じオブジェクトを再代入するとバインディングと `$watch` が再発火する(`config.sameValueGuard`。`semantics: "event"` のプロパティはどちらにせよ対象外) |
958
+
959
+ 規則 1 は静的解析で捕まえられる唯一の規則です。getter が `this.form.name` を読んでいて、ドキュメントのどこかで `form.name` を書いている(`value:` バインド・spread・`this["form.name"] = …`)と、`wcs-validate` と VS Code 拡張が `wcs/getter-untracked-read` を報告します。ルートを丸ごと置換するだけの設計(router の params・`$streams` の fold)には出ません。
960
+
961
+ `$untrackDependency(fn)` は setter の規則を getter に意図的に適用するもので、`fn` の中の読み取りは追跡されません。`$trackDependency(path)` は最初の規則に対する逃げ道です。
962
+
905
963
  ### ループインデックス変数(`$1`, `$2`, ...)
906
964
 
907
965
  getter やイベントハンドラ内で、`this.$1`、`this.$2` などで現在のループイテレーションのインデックスを取得できます(0始まりの値、1始まりの命名):
@@ -1292,6 +1350,23 @@ customElements.define("user-card", UserCard);
1292
1350
  > `$postUpdate` はコンポーネント自身の語彙で書けます — パスはマウント先へ翻訳され、ホスト行の
1293
1351
  > 添字は自動で前置されます。
1294
1352
 
1353
+ #### 公開 getter(コンポーネントの getter を外から読む)
1354
+
1355
+ マウントされたコンポーネントの getter はマウント先に**公開**されます: **ツリーに無いキーの読みは、その位置にマウントされたコンポーネントの getter で答える。ツリーにあるキーはツリーが勝つ。私有キーとメソッドは見せない。** 上の `user-card` なら、ホスト側のマークアップからコンポーネントの派生値をそのまま読めます:
1356
+
1357
+ ```html
1358
+ <user-card data-wcs="state: user"></user-card>
1359
+ <span data-wcs="textContent: user.display"></span> <!-- "Alice <alice@example.com>" — コンポーネントの getter -->
1360
+ ```
1361
+
1362
+ - 行マウントは行ごとに公開されます: `$getAll("users.*.display")` や同じ `for` 内の `text: .display` は各行のコンポーネントの getter を読みます。依存も流れます — `user.name` が変われば `user.display` を読んだものはすべて再描画されます
1363
+ - `get "children.*.label"()` のようにコンポーネント内のパスにワイルドカードを含む accessor は、内部では使えますが**公開されません**。各子行にマウントしたコンポーネントの `get label()` として定義してください。公開対象は、公開パスのワイルドカード数がマウント先のワイルドカード数と一致する accessor だけです
1364
+ - 親は子コンポーネントの登録より先に評価されるので、初回の読みは `undefined` になりえます。コンポーネントがマウントされ次第、値は収束します。派生式は `(x ?? 0)` のように防御的に書いてください
1365
+ - 未存在パスの警告は `getBindingsReady` とは独立に 1 マクロタスク(`setTimeout(0)`)だけ遅延します。autoloader や遅延したカスタム要素定義を使う場合、最終的にバインドが解決しても、登録前の初回警告が出ることがあります
1366
+ - ツリーに同名のキーがある場合(継承したプロパティも含む)はツリーが勝ち、ランタイムが 1 回だけ warn します(`wcs/mount-export-shadowed`)。同一インスタンスに同名キーを公開するコンポーネントが 2 つある形は設定ミスで、候補走査時に検出して throw します(`wcs/mount-export-ambiguous`)。検証付きキャッシュに命中すると他の候補は再走査しないため、初回解決後に追加した競合コンポーネントは検出されない場合があります
1367
+ - 公開キーへの外からの書き込みは accessor の setter を実行し、getter しか無ければ throw します(getter を隠すキーをツリーに作ることはありません)。`in` は公開キーを見ません
1368
+ - **自己再帰コンポーネント**(深さが有界でない木)が書けるようになります: 自分自身を `<template data-wcs="for: children"><tree-node data-wcs="state: ."></tree-node></template>` で入れ子にするコンポーネントは、`get total() { return this.value + this.$getAll("children.*.total").reduce((a, b) => a + (b ?? 0), 0); }` と各段 1 段の式で書け、再帰は台帳が解きます。パス自体は再帰を表現できない(ワイルドカードの個数が固定)ので、再帰は DOM に置き、パスはその展開形になります。設計: [docs/state-overlay-export-design.md](../../docs/state-overlay-export-design.md)
1369
+
1295
1370
  ### 独立した Web Component への状態注入(`__e2e__/single-component`)
1296
1371
 
1297
1372
  ホストの外部状態に依存しないコンポーネントでも、`bind-component` で `state` を注入してリアクティブにできます。
@@ -2078,7 +2153,7 @@ export default {
2078
2153
 
2079
2154
  ## ライフサイクルフック
2080
2155
 
2081
- 状態オブジェクトに `$connectedCallback` / `$disconnectedCallback` / `$updatedCallback` を定義すると、初期化・クリーンアップ・更新時のフックとして利用できます。
2156
+ 状態オブジェクトに `$connectedCallback` / `$disconnectedCallback` / `$updatedCallback` / `$errorCallback` を定義すると、初期化・クリーンアップ・更新時・バインディング失敗時のフックとして利用できます。
2082
2157
 
2083
2158
  ```html
2084
2159
  <wcs-state>
@@ -2108,13 +2183,27 @@ export default {
2108
2183
  | `$connectedCallback` | 初回接続時は状態初期化後、再接続時は毎回呼び出し | 可(await される) |
2109
2184
  | `$disconnectedCallback` | 要素が DOM から削除された時 | 不可(同期のみ) |
2110
2185
  | `$updatedCallback(paths, indexesListByPath)` | live binding に更新が適用された後に呼び出し | 可(await されない) |
2186
+ | `$errorCallback(error, info)` | バインディングの適用に失敗した drain の後 — 失敗した本数ぶん、`$updatedCallback` の後に呼び出し | 可(await されない) |
2111
2187
 
2112
2188
  `$disconnectedCallback` を除くすべてのフックで `async` を使用できます。リアクティブ Proxy はすべてのプロパティへの代入を変更として検知します。そのため、標準の `async/await` による処理とプロパティへの直接代入だけで非同期ロジックが完結します。ローディングフラグの切り替え、取得したデータの格納、エラーメッセージの更新といった処理もすべて単なるプロパティ代入で行えるため、非同期状態を管理するための複雑な抽象化機能は必要ありません。
2113
2189
 
2114
2190
  - フック内の `this` は読み書き可能な状態プロキシです。
2115
2191
  - `$connectedCallback` は要素が接続される**たびに**呼ばれます(一度削除された後の再接続も含みます)。再確立が必要なセットアップ処理に適しています。
2116
2192
  - `$disconnectedCallback` は同期的に呼び出されます。タイマーのクリア、イベントリスナーの削除、リソースの解放といったクリーンアップ処理に使用してください。
2117
- - `$updatedCallback(paths, indexesListByPath)` は、その drain で live binding が適用された path の一覧を受け取ります。binding のない state 書き込みでは呼ばれず、`paths` にも現れません。ワイルドカードをもつパスが更新された場合は、`indexesListByPath` から対象のインデックス情報も取得可能です。`async` を使用できますが、戻り値は await されません。
2193
+ - `$updatedCallback(paths, indexesListByPath)` は、その drain で live binding が適用された path の一覧を受け取ります。binding のない state 書き込みでは呼ばれず、`paths` にも現れません。ワイルドカードをもつパスが更新された場合は、`indexesListByPath` から対象のインデックス情報も取得可能です。マウントされたコンポーネントのマーカーパス(`#m…`)は `paths` に現れません — コンポーネントの私有キーは私有のままです(DevTools の overlays 表示で見えます)。`async` を使用できますが、戻り値は await されません。
2194
+ - `$errorCallback(error, info)` はバインディングの**ページ内エラー境界**です。バインディングの適用が throw したとき(パス getter やフィルタが throw した、構造ディレクティブが失敗した)、その失敗は隔離され(同じバッチの残りは適用され、値も DOM も巻き戻されません)、このフックが無ければ `console.error` で報告されます。フックを宣言すると報告はそこへ届きます: `error` は throw された値、`info` はバインディングを識別する `{ path, bindingType, node }`(`path` は `data-wcs` に書いた形のまま。ワイルドカードもそのまま)。`this` は書き込み可能な state proxy なので、メッセージを state に書いて普通に描画するのが基本形です:
2195
+
2196
+ ```js
2197
+ export default {
2198
+ user: null, loadError: "",
2199
+ get title() { return this.user.profile.name; }, // user が null の間は throw する
2200
+ $errorCallback(error, { path }) {
2201
+ this.loadError = `${path}: ${error.message}`; // <p data-wcs="textContent: loadError">
2202
+ },
2203
+ };
2204
+ ```
2205
+
2206
+ フックはバッチの後(`$updatedCallback` の後)に走り、await されず、フック内で throw しても console に報告されるだけで drain は壊れません。DevTools にはフックの有無に関わらず全失敗が `state:binding-apply-error` として届きます。ルート専用で、ボリューム(`<wcs-state mount>`)に宣言しても無視されます。`$watch` ハンドラの失敗(別途隔離・報告)や、`$connectedCallback` / `$updatedCallback` が投げた例外(loud に失敗する)は対象外です。
2118
2207
  - Web Component を使用している場合は、コンポーネント側に `async $stateReadyCallback(stateProp)` を定義おくことで、`bind-component` でバインドした状態が利用可能になった瞬間にフックとして呼び出されます。
2119
2208
 
2120
2209
  ## 遷移アニメーション
@@ -2149,7 +2238,7 @@ li {
2149
2238
  配線したパスが state 上で解決しないことが**確実**なとき、バインド確立時(`$watch` は宣言時)に 1 回だけ警告します。診断 code はコンソール・`@wcstack/lint`・VS Code 拡張で共通です:
2150
2239
 
2151
2240
  ```
2152
- [@wcstack/state] [wcs/binding-path-missing] Bound path "user.nmae" does not resolve on state "default":
2241
+ [@wcstack/state] [wcs/binding-path-missing] Bound path "user.nmae" does not resolve on the state tree:
2153
2242
  "nmae" is not declared. Did you mean "name"? Updates to this path will be silently
2154
2243
  dropped. Validate statically: npx @wcstack/lint <file>.
2155
2244
  ```
@@ -2233,12 +2322,14 @@ bootstrapState({
2233
2322
  |---|---|---|
2234
2323
  | `bindAttributeName` | `'data-wcs'` | バインディング属性名 |
2235
2324
  | `tagNames.state` | `'wcs-state'` | 状態要素のタグ名 |
2325
+ | `tagNames.ssr` | `'wcs-ssr'` | SSR ハイドレーションデータ要素のタグ名 |
2236
2326
  | `locale` | `<html lang>`、無ければ `'en'` | ロケール依存フィルタ(`locale` / `date` / `time` / `datetime`)のロケール — [ロケール](#ロケール)を参照 |
2237
2327
  | `debug` | `false` | デバッグモード |
2238
2328
  | `enableMustache` | `true` | `{{ }}` 構文の有効化 |
2239
2329
  | `enableDirectionalInitialSync` | `true` | 方向認識のバインディング authority(`#init=` / `#sync=` バインド modifier)— [バインディング authority](#バインディング-authority-init--sync) 参照。既定 on。`false` で opt-out |
2240
2330
  | `enablePropagationContext` | `true` | バインド間の因果伝播トラッキング(echo/diamond のループ防止)。既定 on。`false` で opt-out |
2241
2331
  | `enableContractAnalyzer` | `false` | opt-in の開発時 contract analyzer(`analyzeContract` を公開) |
2332
+ | `sameValueGuard` | `true` | 現在値と `Object.is` で同値なプリミティブ書き込みを enqueue 前に落とす — バインディングと `$watch` は実質「変化時のみ」発火する(参照型は常に通す)。`false` で同値書き込みを通し、`$watch` の `prev` は `undefined` になる |
2242
2333
 
2243
2334
  ### ロケール
2244
2335
 
@@ -2268,6 +2359,21 @@ bootstrapState({
2268
2359
  [docs/i18n-design.md](../../docs/i18n-design.md) を参照。短く言えば、翻訳はフィルタでは
2269
2360
  なくパスに置く。
2270
2361
 
2362
+ **i18n の位置づけと、決めたこと。** i18n パッケージもライブな言語切替も、意図して
2363
+ 持たない。辞書はロケールごとに選ばれる ES モジュールで、ボリューム
2364
+ (`<wcs-state mount="i18n" src="/i18n/state.js">`)としてマウントし、普通のパス
2365
+ (`i18n.checkout.title`)で読む。
2366
+ ロケールはページが描画される**前**に決める — フィルタは `<html lang>` から、router は
2367
+ URL から。router 側のロケールはルートパラメータではなく **basename**(`/ja/…`)に置く。
2368
+ したがって言語切替は state への書き込みではなく、別の basename への実ナビゲーションで
2369
+ ある: router は自分の basename 配下のリンクだけを intercept するので、`/:lang` パラメータ
2370
+ にすると言語が変わらないまま静かに壊れ、ライブ切替はロケール依存の全モジュールの
2371
+ 再評価を要求する。basename を運ぶ `<base href>` には実コストがある(ページ内アンカー・
2372
+ SVG フラグメント参照・CSP 下の相対 `src` が全てそこを基準に解決される)。代替 2 案 —
2373
+ router 自身が `<html lang>` を読む案と、リンク単位の intercept オプトアウト — も検討し、
2374
+ 記録として残した。別の形を選ぶ前に [docs/i18n-design.md](../../docs/i18n-design.md)
2375
+ §9-1 を読むこと。`examples/router-i18n` が参照レイアウト。
2376
+
2271
2377
  > この 3 つは **architecture-hardening** 機能で、規範は `docs/architecture-hardening/` に
2272
2378
  > あります。`enablePropagationContext` は**既定 on** — write-path コストは一方向バインドで
2273
2379
  > ほぼゼロ(echo しうる双方向 wire のみ因果 bookkeeping を行う)で、フラグは恒久的な
@@ -2421,6 +2527,8 @@ export default defineState({
2421
2527
 
2422
2528
  ユーティリティ型 `WcsPaths<T>` と `WcsPathValue<T, P>` もエクスポートされます。詳細は [docs/define-state.ja.md](docs/define-state.ja.md) を参照してください。
2423
2529
 
2530
+ `defineState()` が型を付けるのは state ファイルです。その型を HTML まで運ぶのが [`@wcstack/typescript`](../typescript/README.ja.md) の 2 つの CLI で、`wcs-schema` は `@wcstack/lint` と VS Code 拡張が `data-wcs` パスの検証に使う `stateSchema` sidecar を書き出し、`wcs-tsc` はインラインの `<script type="module">` state に TypeScript コンパイラを掛けます。全体像は [docs/typescript.ja.md](../../docs/typescript.ja.md)。
2531
+
2424
2532
  ## API リファレンス
2425
2533
 
2426
2534
  ### `bootstrapState()`
@@ -2432,6 +2540,21 @@ import { bootstrapState } from '@wcstack/state';
2432
2540
  bootstrapState();
2433
2541
  ```
2434
2542
 
2543
+ ### その他のエクスポート
2544
+
2545
+ | エクスポート | 説明 |
2546
+ |---|---|
2547
+ | `getBindingsReady(root)` | `root`(`document` または shadow root)配下の全バインディングが構築されたら解決。バインディング初期化が失敗すれば reject |
2548
+ | `buildBindings(root)` | `document` / `ShadowRoot` 配下のバインディングを明示的に構築する — その root に最初に登録された `<wcs-state>` がスケジュールするもの |
2549
+ | `getConfig()` | 現在の設定(読み取り専用ビュー) |
2550
+ | `defineState(obj)` | メソッドと getter 内の `this` に型を付けるアイデンティティ関数 — [TypeScript サポート](#typescript-サポート) 参照 |
2551
+ | `VERSION` | パッケージのバージョン。`<wcs-ssr>` に刻印され、ハイドレーション時に照合される |
2552
+ | `getWcsManifest()` / `WCS_MANIFEST_VERSION` | バインディング構文・組み込みフィルタ・予約名の機械可読 manifest — 実装から導出され、`@wcstack/lint` と VS Code 拡張が消費する |
2553
+ | `builtinFilterMeta` | 全組み込みフィルタの引数・戻り値メタデータ |
2554
+ | `analyzeContract()` | 開発時の contract analyzer。`enableContractAnalyzer` が off なら no-op |
2555
+
2556
+ ツール向けのサブパスエントリ: `@wcstack/state/parser`(`data-wcs` パーサを DOM 非依存の純関数として公開)、`@wcstack/state/manifest`、`@wcstack/state/wcs-manifest.json`(manifest をビルド済み JSON として公開)。
2557
+
2435
2558
  ### `<wcs-state>` 要素
2436
2559
 
2437
2560
  | 属性 | 説明 |
@@ -2441,19 +2564,20 @@ bootstrapState();
2441
2564
  | `src` | `.json` または `.js` ファイルの URL |
2442
2565
  | `json` | インライン JSON 文字列 |
2443
2566
  | `bind-component` | Web Component バインディングのプロパティ名 |
2567
+ | `enable-ssr` | SSR を有効化: サーバーはこの state の `<wcs-ssr>` ハイドレーションデータを出力し、クライアントは再描画せずそこから復元する — [サーバーサイドレンダリング](#サーバーサイドレンダリング) 参照 |
2444
2568
 
2445
2569
  ### IStateElement
2446
2570
 
2447
2571
  | プロパティ / メソッド | 説明 |
2448
2572
  |---|---|
2449
2573
  | `initializePromise` | 状態の完全な初期化時に解決される Promise |
2574
+ | `connectedCallbackPromise` | `connectedCallback` の完了(state のロードと `$connectedCallback` の実行)で解決される Promise — テストのレシピが await するもの |
2450
2575
  | `listPaths` | `for` ループで使用されるパスの Set |
2451
2576
  | `getterPaths` | getter として定義されたパスの Set |
2452
2577
  | `setterPaths` | setter として定義されたパスの Set |
2453
2578
  | `createState(mutability, callback)` | 状態プロキシを作成(`"readonly"` または `"writable"`) |
2454
2579
  | `createStateAsync(mutability, callback)` | `createState` の非同期版 |
2455
2580
  | `setInitialState(state)` | プログラムから状態を設定(初期化前) |
2456
- | `bindProperty(prop, descriptor)` | 生の状態オブジェクトにプロパティを定義 |
2457
2581
  | `nextVersion()` | バージョン番号をインクリメントして返す |
2458
2582
 
2459
2583
  ## アーキテクチャ
@@ -2463,11 +2587,14 @@ bootstrapState()
2463
2587
  └── registerComponents() // <wcs-state> カスタム要素を登録
2464
2588
 
2465
2589
  <wcs-state> connectedCallback
2466
- ├── _initializeBindWebComponent() // bind-component: 親コンポーネントから状態を取得
2467
- ├── _initialize() // 状態をロード (state属性 / src / json / script / API)
2468
- └── setStateElementByName() // WeakMap<Node, Map<name, element>> に登録
2469
- └── (rootNode への初回登録時)
2470
- └── queueMicrotask buildBindings()
2590
+ ├── 置かれ方によりいずれか 1 つ:
2591
+ ├── _initializeDCC() // data-wc-definition ホスト配下: DCC クラスを定義
2592
+ ├── _initializeVolume() // mount=: このボリュームをルートツリーへ接ぎ木
2593
+ ├── _initializeBindWebComponent() // bind-component: ホストのツリーをマウント点でエイリアス
2594
+ └── _initialize() // ルート: 状態をロード (state属性 / src / json / script / API)
2595
+ │ └── setStateElement() // WeakMap<Node, IStateElement> に登録 — 1 root 1 ツリー
2596
+ │ └── (rootNode への初回登録時)
2597
+ │ └── queueMicrotask → buildBindings()
2471
2598
  ├── _callStateConnectedCallback() // $connectedCallback が定義されていれば呼び出し
2472
2599
 
2473
2600
  buildBindings(root)
@@ -2491,7 +2618,7 @@ buildBindings(root)
2491
2618
  - **PathInfo** — 静的パスメタデータ(セグメント、ワイルドカード数、親パス)
2492
2619
  - **ListIndex** — ランタイムループインデックスチェーン
2493
2620
  - **StateAddress** — PathInfo + ListIndex の組み合わせ
2494
- - **AbsoluteStateAddress** — 状態名 + StateAddress(クロス状態参照用)
2621
+ - **AbsolutePathInfo / AbsoluteStateAddress** — ツリーを持つ state 要素に固定した PathInfo と、その ListIndex の組。マウントされたコンポーネントとボリュームは相対パスをこの層でホストのツリーへ翻訳する。v2 は 1 root 1 ツリーなので、アドレスに状態名はない
2495
2622
 
2496
2623
  ## パフォーマンス
2497
2624
 
@@ -2531,7 +2658,10 @@ buildBindings(root)
2531
2658
  し、残りのページは宣言的なまま保てます。
2532
2659
  - クリア後に残るヒープは、次のリスト生成を安くする有界の行プールです。
2533
2660
 
2534
- 絶対値は 1 台の開発機での計測です(v1.21.6 + PR#87clear リーク修正)。
2661
+ 絶対値は 1 台の開発機で v1.21.6 + PR#87clear リーク修正)時点に取ったものです。
2662
+ v2.0 のマウント作業は同じドライバの同一セッション A/B でゲートし、実行ごとの
2663
+ ノイズ内に収まった([docs/state-mount-impl-plan.md](../../docs/state-mount-impl-plan.md)
2664
+ §2-2 と slice 27)ため、表は取り直していません。絶対値はマシン状態で ±20% 揺れます。
2535
2665
  `e2e/bench/` のドライバで手元のハードウェアでも再現できます。
2536
2666
 
2537
2667
  ## サーバーサイドレンダリング