@wcstack/state 2.2.0 → 2.4.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
@@ -114,6 +114,7 @@
114
114
  - **event token** — command token の双対。wc-bindable 要素が dispatch するイベントを `eventToken.<prop>: tokenName` + `$on` マップで state が受信
115
115
  - **stream** — `$streams` 宣言で連続的な非同期フロー(async iterable / `ReadableStream`)を fold して reactive プロパティ化。switchMap 型の依存駆動 restart 付き
116
116
  - **パス getter** — ドットパスキー getter(`get "users.*.fullName"()`)によるデータツリーの任意の深さへのフラットな仮想プロパティ定義、自動依存追跡・キャッシュ
117
+ - **再帰パス** — `$recursion: { "nodes.*": "children.*" }` で木の形が繰り返す場所を宣言し、1 本の `**` getter(`get "nodes.**.total"()`)が全深さを覆う。`$getAll(path, [])` は全深さを合併し、`$setAll(path, [], value)` は全深さへブロードキャストする
117
118
  - **Mustache 構文** — テキストノードでの `{{ path|filter }}`
118
119
  - **複数の状態ソース** — JSON, JS モジュール, インラインスクリプト, API, 属性
119
120
  - **SVG サポート** — `<svg>` 要素内でのフルバインディング対応
@@ -247,7 +248,7 @@
247
248
  <div data-wcs="textContent: cart.total"></div>
248
249
  ```
249
250
 
250
- ボリュームは getter・`$watch`・`$listKeys`・`$updatedCallback`・`$connectedCallback`/`$disconnectedCallback` を宣言できます(すべてマウントパス相対)。`$errorCallback` はルート専用です(バインディングの失敗はツリーの所有者へ 1 回だけ報告されます)。読み込み順は自由です(ルートより先に接続されたボリュームは、ルートの登録時に接ぎ木されます)。マウントパスは静的パスのみです(`*`・`$`・`#`・`@` は不可)。初期化後に `mount` 属性を変更することはできません — 変更は console 警告付きで無視されます。要素を取り除き、望むパスで新しい要素を追加してください。
251
+ ボリュームは getter・`$watch`・`$listKeys`・`$updatedCallback`・`$connectedCallback`/`$disconnectedCallback` を宣言できます(すべてマウントパス相対)。`$errorCallback` はルート専用です(バインディングの失敗はツリーの所有者へ 1 回だけ報告されます)。読み込み順は自由です(ルートより先に接続されたボリュームは、ルートの登録時に接ぎ木されます)。ルートの `<wcs-state>` が初期化に失敗した場合、その時点で待機していたボリュームは永久に待たずに自分の報告を出して決着します。その報告が終点です —— 孤児として報告されたボリュームは後から自分で接ぎ木し直さず、マウントの枠もその rootNode が生きている限り予約されたままなので(枠の台帳は rootNode をキーにした `WeakMap` で、枠が解放されることはありません)、あとから修正版のルートを接続しても復帰しません。ルートの `<wcs-state>` を直してページを読み直してください。マウントパスは静的パスのみです(`*`・`$`・`#`・`@` は不可)。初期化後に `mount` 属性を変更することはできません — 変更は console 警告付きで無視されます。要素を取り除き、望むパスで新しい要素を追加してください。
251
252
 
252
253
  > **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。
253
254
 
@@ -1102,6 +1103,207 @@ export default {
1102
1103
  };
1103
1104
  ```
1104
1105
 
1106
+ ## 再帰パス(`$recursion`)
1107
+
1108
+ パスは深さを文字列に焼き付けます。`nodes.*.children.*.total` はワイルドカードちょうど 2 段のパスであり、木が 1 段深くなっても 3 段には伸びません。しかし木の深さはコードではなく**データの性質**です。`$recursion` はこの隔たりを埋めます。形が繰り返す場所を宣言し、あとは「いま何段目であれ」を `**` と書きます。
1109
+
1110
+ ```javascript
1111
+ export default {
1112
+ $recursion: { "nodes.*": "children.*" }, // アンカー → 反復サブパス
1113
+
1114
+ nodes: [
1115
+ { value: 1, selected: false, children: [
1116
+ { value: 10, selected: false, children: [
1117
+ { value: 100, selected: false, children: [] }
1118
+ ]},
1119
+ { value: 20, selected: false, children: [] }
1120
+ ]},
1121
+ { value: 2, selected: false, children: [] }
1122
+ ],
1123
+
1124
+ // getter は 1 本で全深さぶん。`**` は評価されている深さに束縛される
1125
+ get "nodes.**.total"() {
1126
+ return this["nodes.**.value"]
1127
+ + this.$getAll("nodes.**.children.*.total").reduce((a, b) => a + b, 0);
1128
+ },
1129
+
1130
+ // 木全体の集計。`[]` は全深さの合併
1131
+ get treeTotal() {
1132
+ return this.$getAll("nodes.**.value", []).reduce((a, b) => a + b, 0);
1133
+ },
1134
+
1135
+ clearSelection() {
1136
+ this.$setAll("nodes.**.selected", [], false);
1137
+ }
1138
+ };
1139
+ ```
1140
+
1141
+ この森の total は `131 / 110 / 100 / 20 / 2`、`treeTotal` は `133` になります。
1142
+
1143
+ **`**` はオーサリング層だけの記号で、エンジンには決して降りません。** 具体パス(`nodes.*.children.*.total`)を読んだ時点で、その深さの getter が遅延実体化されます(実際に触れた深さのぶんだけアクセサが生えます)。その先 —— `PathInfo`、依存グラフ、`$1`…`$n`、`$resolve`、リスト差分 —— が見るのは、いつもどおりワイルドカード本数が固定された普通のパスです。リアクティブの中核は新しい形を覚えていません。
1144
+
1145
+ ### 再帰点を宣言する
1146
+
1147
+ `$recursion` は 1 つの**アンカー**を、1 段深くする**反復サブパス**へ対応づけます。どちらも「固定プロパティ列 + 末尾の `.*`」の形で、リストそのものではなくリストの**要素**を名指します:
1148
+
1149
+ ```javascript
1150
+ $recursion: { "nodes.*": "children.*" } // nodes[i].children[j].children[k]…
1151
+ $recursion: { "data.tree.*": "kids.*" } // 深い位置のアンカーも可
1152
+ $recursion: { "nodes.*": "nodes.*" } // 自己相似な綴りも可
1153
+ ```
1154
+
1155
+ `**` に意味を与えるのはこの宣言だけです。`$recursion` の無い state では `**` はパスの文字ですらなく(`wcs/recursion-unsupported`)、この記法が子孫検索へ黙って滑り落ちることはありません。このバージョンが受け付けるのは **state ごとに単一の自己再帰アンカー**です。アンカーの途中のワイルドカード、2 つ目のエントリ、2 つのアンカー間の相互再帰、1 本のパスに 2 つ目の `**`、`get "nodes.**"`(これはノード自身であって、ノード配下の計算パスではありません)、接尾辞が構造そのものを名指す `**` getter(`get "nodes.**.children"()` / `.children.*` / `.children.length` —— 全深さで実データの子リストを影にしてしまいます)、同じ具体パスへ展開される 2 本の `**` getter、そして再帰 **setter** は、宣言を読んだ時点で拒否します —— 別の意味に解釈することはありません。
1156
+
1157
+ 宣言が定義する族は無限ですが、state に生えるのは実際に要求された深さだけです:
1158
+
1159
+ ```
1160
+ k=0 nodes.*
1161
+ k=1 nodes.*.children.*
1162
+ k=2 nodes.*.children.*.children.*
1163
+ ```
1164
+
1165
+ ### `**` はどこで何を意味するか
1166
+
1167
+ `**` は深さを表す変数で、**束縛**されるか**合併**されるかは文脈が決めます。これは新しい規則ではなく、`*` が既に持っている「現在行」と「全行」の書き分けをそのまま継いだものです:
1168
+
1169
+ | `**` が現れる場所 | 意味 |
1170
+ |---|---|
1171
+ | getter のキー(`get "nodes.**.total"()`) | 評価されている深さに束縛 |
1172
+ | その getter 本体でのパス読み(`this["nodes.**.value"]`) | 同じ深さに束縛 |
1173
+ | `$getAll(path)`(添字**省略**) | その深さに束縛。展開されるのは `**` より**後ろ**のワイルドカードだけ |
1174
+ | `$getAll(path, [])`(**明示**) | **全深さの合併** —— 深さ優先・行きがけ・添字昇順 |
1175
+ | `$getAll(path, [i, …])` | 拒否。接頭辞ではどの深さの話か言えない(`wcs/recursion-getall-form`) |
1176
+ | `$setAll(path, [], value)` | 全深さへのブロードキャスト(合併と同じ走査・同じ順序) |
1177
+ | `$resolve` / `$postUpdate` / `$trackDependency` / `$watch` のキー / `$listKeys` のキー / markup の `data-wcs` / 直接代入 | 拒否(`wcs/recursion-unsupported`) |
1178
+
1179
+ 束縛形は束縛先の深さを必要とするので、**再帰 getter の中**でしか解決できません(アンカー配下の普通の行 getter や、その行に紐づくイベントハンドラも同じく実体の `ListIndex` を持つので使えます)。トップレベルで `this["nodes.**.value"]` を読むと `wcs/recursion-context` になります —— どのノードのつもりだったかを黙って推測することはありません。深さは行の添字と同じく**最も内側の評価フレームだけ**から読みます。再帰 getter が呼ぶ普通の getter(`get "nodes.**.x"() { return this.helper }` と `get helper() { return this["nodes.**.value"] }`)は自分の行を持たないので、これも `wcs/recursion-context` になります —— `**` は再帰 getter の側で読み、値を渡してください。合併形は深さを要求しないので、トップレベルの getter でも普通の行 getter でもメソッドでも読めます。
1180
+
1181
+ ```javascript
1182
+ this.$getAll("nodes.**.value", []); // [1, 10, 100, 20, 2] —— 深さ優先・行きがけ
1183
+ ```
1184
+
1185
+ `**` より**後ろ**の `*` は、各ノードで固定本数のパスと同じ順に展開し、そのノードの分を出し切ってから子へ降ります。上の木のノードに `tags` があるとき(`1` → `[3, 4]`、`10` → `[5]`、`20` → `[7]`、他は空):
1186
+
1187
+ ```javascript
1188
+ this.$getAll("nodes.**.tags.*.v", []); // [3, 4, 5, 7] —— ノード 1 の tags、次にノード 10 の、次にノード 20 の
1189
+ ```
1190
+
1191
+ ### 孫を二重に数えない集計
1192
+
1193
+ 木を畳むのは再帰 getter なので、この書き分けが集計の成否そのものになります:
1194
+
1195
+ ```javascript
1196
+ // ✅ 省略 —— この深さに束縛されるので、直下の子だけを合計する
1197
+ get "nodes.**.total"() {
1198
+ return this["nodes.**.value"]
1199
+ + this.$getAll("nodes.**.children.*.total").reduce((a, b) => a + b, 0);
1200
+ }
1201
+
1202
+ // ❌ `[]` —— 全深さの子 total。各ノードの total が自分の子孫の total を再び含み、
1203
+ // getter が自分自身を要求することになる。実際には誤った値ではなく
1204
+ // `wcs/getter-cycle` になる。
1205
+ get "nodes.**.total"() {
1206
+ return this["nodes.**.value"]
1207
+ + this.$getAll("nodes.**.children.*.total", []).reduce((a, b) => a + b, 0);
1208
+ }
1209
+ ```
1210
+
1211
+ 同じ間違いを再帰の**外側**でやると、こちらは静かです。踏む循環が無く、もっともらしい大きすぎる値が返るだけになります。集計値の合併は、孫を「親の total の内訳」として 1 回、「合併の要素」としてもう 1 回数えます:
1212
+
1213
+ ```javascript
1214
+ // ❌ 363 —— 全ノードの total を合併しているが、total は既に部分木を含んでいる
1215
+ get treeTotalWrong() {
1216
+ return this.$getAll("nodes.**.total", []).reduce((a, b) => a + b, 0);
1217
+ }
1218
+ // ✅ 133 —— 生の値を合併する
1219
+ get treeTotal() {
1220
+ return this.$getAll("nodes.**.value", []).reduce((a, b) => a + b, 0);
1221
+ }
1222
+ // ✅ 133 —— あるいはルートだけを足す(各ルートの total が既に部分木を畳んでいる)
1223
+ get treeTotalFromRoots() {
1224
+ return this.$getAll("nodes.*.total", []).reduce((a, b) => a + b, 0);
1225
+ }
1226
+ ```
1227
+
1228
+ **合併するのは生の値か、さもなくばルートだけを足すこと。自分の部分木を既に集計している値を合併してはいけません。** 二重計上かどうかはパス文字列からは決定できないので、これを捕まえると約束する診断はありません。
1229
+
1230
+ ### 書き込みはブロードキャストのみ
1231
+
1232
+ 再帰 `$setAll` が受け付けるのは `[]` + 素の値という 1 つの形だけで、戻り値は書き込んだアドレスの件数です(上の森なら 5):
1233
+
1234
+ ```javascript
1235
+ this.$setAll("nodes.**.selected", [], false); // 全深さの全ノード
1236
+ ```
1237
+
1238
+ それ以外の形は、走査が 1 件でも書く**前に**拒否します。拒否された呼び出しは木を一切変更しません。この保証は**形**の検査についてのもので、添字綴りの葉(`nodes.**.children.0.value`)は**データ**の条件で途中で止まることがあります —— `children` が空のノードには書き込む子 `0` が無いためで、固定本数の `$setAll("nodes.*.children.0.value", [], v)` と同じ振る舞いです:
1239
+
1240
+ | 形 | 拒否する理由 |
1241
+ |---|---|
1242
+ | 非空の接頭辞 | 接頭辞ではどの深さに適用されるのか言えない(`wcs/recursion-setall-form`) |
1243
+ | 添字の省略 | 書き込み API は文脈を取らないので束縛する深さが無い —— `[]` を渡す |
1244
+ | mapper 関数 | `(current, ...indexes)` の添字の本数が深さごとに変わる |
1245
+ | `{ spread: true }` | 1 次元配列を木へ配るには作者が走査順を知っている必要があり、契約として使えない |
1246
+ | `nodes.**` / `nodes.**.children` / `nodes.**.children.*` / `nodes.**.children.length` —— 反復サブパスが多段(`branch.children.*`)なら、子リストへ至る途中の `nodes.**.branch` も。添字綴りも同じ形に畳まれる(`nodes.**.children.0` は子ノード、`nodes.**.children.0.total` は getter) | 構造そのものへの書き込み(`length` への代入はリストを切り詰める)は、その書き込み自身が確定済みの子アドレスを壊す(`wcs/recursion-structural-write`) |
1247
+ | `nodes.**.total`、およびその値の内側を指すパス | 再帰 getter に setter は無い。導出元を書く(`wcs/recursion-readonly`) |
1248
+
1249
+ 読み取り専用の規則は `**` の綴りに依存しません。再帰 getter の具体的な展開形 —— `nodes.*.total` / `nodes.*.children.*.total` / … —— への書き込みも、固定本数の `$setAll` でも値付きの `$resolve(path, indexes, value)` でも直接代入でも、またその深さが実体化済みかどうかに関わらず、書き込みの入口で拒否します。この検査が無かったときは、未実体化の展開形が「無いキー」に見えてノードのオブジェクトに書き込まれ、代入値が getter のキャッシュ結果として固定されていました。
1250
+
1251
+ ### 入力は木でなければならない
1252
+
1253
+ 走査は深さ方向に降りながら、必要な形をその場で検査します。**同じ配列インスタンス**に 2 度到達したら拒否します。その配列が現在のノードの祖先のものなら循環(`wcs/recursion-cycle`)、そうでなければ 2 つのノードが 1 本の子リストを共有しています(`wcs/recursion-shared-list`)。各ノードに自分の `children` 配列を持たせてください —— **空**配列の使い回しは行を持たず別名化のしようがないので、追跡もせず正当です。
1254
+
1255
+ 行オブジェクトを作り直して `children` 配列を引き継ぐ置換 —— `this.nodes = this.nodes.map(n => ({ ...n }))` —— はふつうの更新で、集計もそのまま追従します。子リストの行オブジェクトはそのまま生き残り(行の identity で持っているもの —— `bind-component` の子スコープが描画した行や、そこにバインドしていない状態 —— は保たれます)、行がぶら下がっていた**退役した親**だけが生きている行に差し替わるので、次の葉の更新は画面に出ている行を dirty にします([#256](https://github.com/wcstack/wcstack/issues/256))。2 つの行が 1 本の `children` 配列を**共有**する形はこれとは別です。2 行とも配列に居る間は従来どおりで、1 本の配列には 1 組の行しかないので 2 行は必ず同じ値で一致し、親を読む行 getter(`this["nodes.*.value"]`)はその行集合の**持ち主**(最初にその配列を展開した行)の文脈で評価されます。変わったのは**持ち主をリストから外したとき**で、行集合は画面に残っている行のどれか 1 行へ移ります —— その行の集計が、外した行の数字で凍る代わりに共有データを追従します(行集合は 1 組しかないので、3 行で共有していれば残った 1 行だけが追従し、他は凍ったままです)。外した行**そのもののオブジェクト**が戻ってくれば持ち主も戻ります —— 同じ配列インスタンスでも、同じ行を並べた新しい配列でも、違う位置に戻しても返ります。一方、**全ての行を作り直す**綴り(`this.nodes = this.nodes.map(n => ({ ...n }))` —— この項の冒頭の更新)ではどの行も一致しないので、行集合は**持ち主が居た位置を占める行**に付きます。2.3.0 でこの通りに戻るのは同じ配列インスタンスの綴りだけで、新しい配列で戻すと 2.3.0 では両方の行が凍ります。子の getter が上を読むなら、ノードごとに自分の配列を持たせてください。
1256
+
1257
+ 上限は展開後のパスの**ワイルドカード 128 段**です。上の集計 getter は評価中のノードより 1 段下を読むので、127 段の鎖までは畳めて、128 段で `wcs/recursion-depth-exceeded` になります(アンカー・到達した深さ・組み立てようとしたパス・上限を名指しします)。この検査は getter 評価スタック自身の 128 段の上限(`wcs/getter-depth-exceeded`)より先に効くので、深い木は「深い」と報告され、循環の疑いを掛けられることはありません。途中で打ち切ることもしません —— 部分的な集計は、誤った値を正しい値として返すことだからです。
1258
+
1259
+ ### 木を描画する
1260
+
1261
+ `**` は markup には書けず、再帰 `<template>` もありません。木は**自己参照コンポーネント**で描画します —— 子ごとに自分自身を shadow の中でマウントするカスタム要素 1 つです。各スコープの中で使うパスは常に 1 段だけ(`node.children.*`)なので markup が深さに依存せず、`node.total` はマウントを通ってルート state の再帰 getter に解決されるので、各ノードが自分の部分木の集計を表示できます。
1262
+
1263
+ ```html
1264
+ <!-- ホスト側 -->
1265
+ <template data-wcs="for: nodes">
1266
+ <tree-node data-wcs="state.node: nodes.*"></tree-node>
1267
+ </template>
1268
+ ```
1269
+
1270
+ ```javascript
1271
+ const markup = `
1272
+ <wcs-state bind-component="state"></wcs-state>
1273
+ <span data-wcs="textContent: node.label"></span>
1274
+ <span data-wcs="textContent: node.total"></span>
1275
+ <template data-wcs="for: node.children">
1276
+ <tree-node data-wcs="state.node: node.children.*"></tree-node>
1277
+ </template>`;
1278
+
1279
+ customElements.define("tree-node", class extends HTMLElement {
1280
+ state = {}; // ← `node` を自分で持たない(マウントから届く)
1281
+ constructor() { super(); this.attachShadow({ mode: "open" }); }
1282
+ connectedCallback() { // ← shadow は constructor ではなくここで組む
1283
+ if (this.shadowRoot.childNodes.length === 0) this.shadowRoot.innerHTML = markup;
1284
+ }
1285
+ });
1286
+ ```
1287
+
1288
+ ここには 2 つの罠があり、どちらも実際に踏んだものです:
1289
+
1290
+ - **コンポーネントの `state` に、マウント先と同名のキーを置かないこと。** 無関係なメソッドや私有キーは構いませんが、自分の `node` を持つとマウントを隠し、子は自分の既定値を表示したまま一段も降りません(実行時に `wcs/mount-own-key-shadow` で名指されます)。
1291
+ - **shadow は constructor ではなく `connectedCallback` で組むこと。** constructor で `innerHTML` を入れると、`<template>` の中身を inert に保たない実装ではその中の要素まで upgrade され、自己参照コンポーネントは自分の constructor の中で無限再帰します。実ブラウザは通ってしまうので、素直なクラッシュではなく環境依存の地雷になります。
1292
+
1293
+ 深さが固定なら、ここまでは要りません。展開後のパスは普通のパスなので、入れ子の `for` テンプレートから `nodes.*.total` や `nodes.*.children.*.total` を他と同じようにバインドできます。
1294
+
1295
+ ### このバージョンに含まれないもの
1296
+
1297
+ 以下はいずれも診断になります。黙って別の意味に解釈されることはありません。
1298
+
1299
+ - 複数アンカー、相互再帰、アンカー途中のワイルドカード、1 本のパスに 2 つ目の `**`
1300
+ - 再帰 setter、接尾辞が構造そのものを名指す `**` getter(`get "nodes.**.children"()`)、`**` getter の展開形と同名の具体 getter、そして代入による `**` 経由の書き込み(`this["nodes.**.x"] = v`、`++` も含む)
1301
+ - 再帰 `$setAll` の mapper・`{ spread: true }`・添字省略・非空の接頭辞・配列でない `indexes`。書き込み API には深さを束縛する評価文脈が無いので、`[]` は必須です
1302
+ - 再帰 `$getAll` の非空の接頭辞・配列でない `indexes`。**添字省略は正当です** —— 再帰 getter の中では束縛形で、評価中の深さを読みます
1303
+ - `data-wcs` / `$watch` や `$listKeys` のキー / `$resolve` / `$postUpdate` / `$trackDependency` への `**`
1304
+ - ボリューム(`mount=`)やマウントされたコンポーネント(`bind-component`)の `$recursion` と `**` getter —— ルートの state に置きます
1305
+ - 再帰 `<template>`、`$depth` 変数、公開の `maxDepth` オプション(3 つとも存在しません)
1306
+
1105
1307
  ## イベントハンドリング
1106
1308
 
1107
1309
  `on*` プロパティでイベントハンドラをバインドします:
@@ -1576,7 +1778,7 @@ interface CommandToken {
1576
1778
 
1577
1779
  - 購読者は要素を `WeakRef` で保持するため、token の購読者セットに残っていても、取り外された要素はガベージコレクト可能
1578
1780
  - `emit` 時、WeakRef が回収済みか要素が接続されていない(`isConnected === false`)場合、購読は自動的に破棄される(lazy purge)
1579
- - 所有する `<wcs-state>` が disconnect されると、token レジストリ全体がクリアされる
1781
+ - 所有する `<wcs-state>` が disconnect されても token レジストリは保持されるので、ルート `<wcs-state>` を付け直した後(ホストを DOM 上で移動したときなど)も購読に命令が届く。切断中は state を作れないので、`$command` 経由の emit は起きない
1580
1782
 
1581
1783
  要素のメソッドは `emit` の引数で呼び出されます:
1582
1784
 
@@ -1753,7 +1955,7 @@ $on: {
1753
1955
 
1754
1956
  ### Token API
1755
1957
 
1756
- event token は command token と同じ `Token` pub/sub プリミティブを共有します —— `name` / `size` / `subscribe` / `unsubscribe` / `emit`、subscribe 順の保持つき([Token API](#token-api) 参照)。token はイベントごとに registry から解決されるため、`setInitialState()` による再構築後も最新の `$on` 購読者に届きます。所有する `<wcs-state>` が disconnect されると、event-token registry はクリアされます。
1958
+ event token は command token と同じ `Token` pub/sub プリミティブを共有します —— `name` / `size` / `subscribe` / `unsubscribe` / `emit`、subscribe 順の保持つき([Token API](#token-api) 参照)。token はイベントごとに registry から解決されるため、`setInitialState()` による再構築後も最新の `$on` 購読者に届きます。所有する `<wcs-state>` が disconnect されても event-token registry は保持されるので、ルート `<wcs-state>` を付け直せば `$on` ハンドラ(と `on` の scan)は再びイベントを受けます。切断中に dispatch されたイベントは state ツリーが見つからず、届きません。
1757
1959
 
1758
1960
  ## Stream(`$streams`)
1759
1961
 
@@ -1868,9 +2070,9 @@ $updatedCallback(paths) {
1868
2070
  }
1869
2071
  ```
1870
2072
 
1871
- **規則:** 描画に依存させたくないロジックは、`$watch`(または `$streams` の `args`)に根を置いてください。`$updatedCallback` は「描かれたものに追随する」用途に限ります。
2073
+ **規則:** 描画に依存させたくないロジックは、`$watch`・`$scan`・`$streams` の `args` のどれかに根を置いてください。`$updatedCallback` は「描かれたものに追随する」用途に限ります。
1872
2074
 
1873
- 上の例は `$watch` に置き換え済みで、`<b>` は表示専用に戻っています。この形(`$updatedCallback` が、どのバインディングにも現れないパスを判定に使っている)は **`wcs/updated-callback-unbound`** として静的に検出されます。
2075
+ 上の例はいまは `$scan` で feed を積み(sentinel の再武装は `$watch`)、`<b>` は表示専用に戻っています。この形(`$updatedCallback` が、どのバインディングにも現れないパスを判定に使っている)は **`wcs/updated-callback-unbound`** として静的に検出されます。
1874
2076
 
1875
2077
  ### 残る制約
1876
2078
 
@@ -1912,10 +2114,10 @@ $updatedCallback(paths) {
1912
2114
  | 引数 | 契約 |
1913
2115
  |---|---|
1914
2116
  | `cur` | drain 時点の値(そのバッチの確定値) |
1915
- | `prev` | **バッチ開始時点**の値(first-write-wins)。意味を持つのは**スカラのときだけ**(下記) |
2117
+ | `prev` | **バッチ開始時点**の値(first-write-wins)。記録されるのは**プリミティブを書いたときだけ**(書く前の値はオブジェクトでもよい。下記) |
1916
2118
  | `...indexes` | ワイルドカードパスのときのみ。そのスコープ自身のループ添字(`$1` / `$2` と同じ規約) |
1917
2119
 
1918
- **`prev` はスカラ限定です。** same-value guard が既に読んでいる旧値を再利用するため watch のための追加読みは発生せず、その帰結として参照型(in-place 変異では同じ参照になるため)・`$postUpdate` 経由・`config.sameValueGuard` オフのときは `undefined` になります。
2120
+ **`prev` はプリミティブの書き込みにだけ付きます。** same-value guard がプリミティブを書く前に読む旧値を再利用するため watch のための追加読みは発生せず、その帰結として書く値が参照型(in-place 変異では同じ参照になるため)・`$postUpdate` 経由・`config.sameValueGuard` オフのときは `undefined` になります。オブジェクトの上にプリミティブを書いたときは、そのオブジェクトが `prev` に渡ります。
1919
2121
 
1920
2122
  **`$watch` は独自の発火条件を持ちません。** 更新バッチに載ったものをそのまま発火します。これはうまく噛み合っていて、同値の primitive 書き込みは enqueue 前に落ちている(=実質的に変化時のみ発火)一方、occurrence(`semantics: "event"` の property)は**意図的に**落とされないので `cur === prev` で発火します。エッジ検出が要るならハンドラ内で `cur` と `prev` を比較してください。
1921
2123
 
@@ -1925,16 +2127,17 @@ $updatedCallback(paths) {
1925
2127
 
1926
2128
  | 層 | 順序 | 制御 |
1927
2129
  |---|---|---|
1928
- | 機構間 | `$updatedCallback` → `$watch` → `$streams` restart | 固定 |
2130
+ | 機構間 | `$updatedCallback` → `$scan` → `$watch` → `$streams` restart | 固定 |
1929
2131
  | ハンドラ間 | `$watch` の宣言順 | **宣言を並べ替える** |
1930
2132
  | 同一パスの行間 | `indexes` 昇順 | 固定 |
1931
2133
 
1932
- **機構間の層を動かす唯一のもの**が、`state` 参加者を受け付ける `<wcs-view-transition>` です。バインディング適用 —— したがって `$updatedCallback` —— がフレームで着地する一方、`$watch` と `$streams` restart は state アドレスを消費し DOM を見ないので、drain がキューされた microtask に留まります。タグがある間の順序は `$watch` → `$streams` restart → `$updatedCallback` です。この層を並べ替えるものはページ上でこれ 1 つだけです。[docs/timing-and-firing-contract.ja.md](https://github.com/wcstack/wcstack/blob/main/docs/timing-and-firing-contract.ja.md) §4.3 を参照してください。
2134
+ **機構間の層を動かす唯一のもの**が、`state` 参加者を受け付ける `<wcs-view-transition>` です。バインディング適用 —— したがって `$updatedCallback` —— がフレームで着地する一方、`$scan`・`$watch`・`$streams` restart は state アドレスを消費し DOM を見ないので、drain がキューされた microtask に留まります。タグがある間の順序は `$scan` → `$watch` → `$streams` restart → `$updatedCallback` です。この層を並べ替えるものはページ上でこれ 1 つだけです。[docs/timing-and-firing-contract.ja.md](https://github.com/wcstack/wcstack/blob/main/docs/timing-and-firing-contract.ja.md) §4.3 を参照してください。
1933
2135
 
1934
2136
  主なルール:
1935
2137
 
1936
2138
  - **ツリーのパスのみ** —— パスに `@`(v1 の名前セレクタ)は書けません。含む宣言は loud に拒否されます。
1937
2139
  - **中間値は観測できません** —— 1 バッチ内の `a → b → c` は `cur = c` / `prev = a` で 1 回だけ発火します(binding 更新と同じ契約)。
2140
+ - **行は drain の時点のリストに従います** —— 同じ job で行を書いてから取り除いた・置き換えた・リストを短くした行は発火せず、位置だけが移った行も発火しません。1 つの位置が発火するのは多くても 1 回です。入れ子のリストを置き換えると、新しい配列の行がすべて発火します。
1938
2141
  - **行単位の差分を見たいなら `$listKeys`** —— 未宣言のまま配列全体を代入すると、行 watch は**全行**について `prev === undefined` で発火します(どの行もパス書き込みを通っていないため)。`$listKeys` を宣言すればキー突合が per-field 書き込みに分解するので、変化した行だけが発火し `prev` もスカラで取れます。
1939
2142
  - **headless な行 watch には `$listKeys` が必要** —— `$watch` が単独では headless にならない唯一の箇所です。`items` から `items.*.price` への展開はリストの `for` バインディングが駆動しており、watch を宣言してもそのパスをリストとしては登録しません(意図的)。したがって `for` バインドも `$listKeys` も無い状態で配列を代入すると、行 watch は**一度も**発火しません。`$listKeys` を宣言する(キー突合がフィールドごとにパス書き込みするので展開を経由しない)か、リストを描画してください。スカラーパスは `user.name` のようなネストしたものも含め、この条件なしに headless で発火します。
1940
2143
  - **ハンドラの例外は隔離されます** —— throw はコンソールに報告され、残りの watch(と stream の restart)は続行します。loud fail する `$connectedCallback` / `$updatedCallback` とは異なる扱いです。
@@ -1942,6 +2145,76 @@ $updatedCallback(paths) {
1942
2145
  - **マウントされた `bind-component` スコープでは実行されません** —— マウントされたコンポーネントは宣言面を実行せず、`$watch` の宣言があると 1 回だけ console.warn でルート state(またはボリューム —— `<wcs-state mount>` は `$watch` / `$listKeys` / `$updatedCallback` を持てます)へ誘導します(`$streams` も同様)。plain な(配線なし Shadow の)子は独立ツリーを持つので宣言できます。
1943
2146
  - **SSR では実行されません** —— ハンドラの副作用がサーバーとクライアントで二重に走るためです。
1944
2147
 
2148
+ ## Scan(`$scan`)
2149
+
2150
+ `$streams` が畳むのは 1 回の run の**内側**で、restart のたびに値は `initial` へ戻ります。`$watch` は値を所有しません。**`$scan`** はその両方を跨いで残る値 —— 時間軸方向の累積 —— を、持ち主・発火単位・reset 条件つきで宣言します。
2151
+
2152
+ ```html
2153
+ <wcs-state>
2154
+ <script type="module">
2155
+ export default {
2156
+ page: 1,
2157
+ host: "a",
2158
+ $eventTokens: ["message"],
2159
+ $streams: {
2160
+ pageResult: { args: (s) => s.page, source: loadPage },
2161
+ },
2162
+ $scan: {
2163
+ // from: state パスの着地ごとに畳む(ここでは stream の値)
2164
+ feed: {
2165
+ from: "pageResult",
2166
+ initial: { items: [], pages: [] },
2167
+ fold: (feed, chunk) =>
2168
+ chunk?.kind === "success" && !feed.pages.includes(chunk.page)
2169
+ ? { items: feed.items.concat(chunk.items), pages: [...feed.pages, chunk.page] }
2170
+ : feed,
2171
+ },
2172
+ // on: 宣言済みイベントトークンの出来事ごとに畳む
2173
+ log: {
2174
+ on: "message",
2175
+ initial: [],
2176
+ fold: (log, event) => [...log.slice(-49), event.detail],
2177
+ resetOn: ["host"], // host が変わるたびに [] へ戻す
2178
+ },
2179
+ },
2180
+ };
2181
+ </script>
2182
+ </wcs-state>
2183
+
2184
+ <template data-wcs="for: feed.items">…</template>
2185
+ ```
2186
+
2187
+ | フィールド | 契約 |
2188
+ |---|---|
2189
+ | `from` | state パス。ワイルドカード可。`$` 始まり・getter・getter の配下は不可。`from` と `on` はどちらか 1 つだけ。 |
2190
+ | `on` | `$eventTokens` に宣言したイベントトークン名。 |
2191
+ | `initial` | 必須。累積の種であり、`resetOn` の戻り先。 |
2192
+ | `fold` | 必須。`from` は `(acc, cur, prev, ...indexes) => next`、`on` は `(acc, event, ...indexes) => next`。同期で、`this` 無しで呼ばれ、新しい値を返す。`acc` そのものを返すと書き込まない。 |
2193
+ | `resetOn` | 任意。素の state パスの配列。どれかが書かれたら出力を `initial` に戻す。`from` の scan はそのバッチの fold を行わず、`on` の scan は書き込みより後に来たイベントを `initial` から畳む。`from` の配下は raise、`from` の祖先は可(親の差し替えで作り直す)。オブジェクトのパスはそのオブジェクト自身が書かれたときだけ reset し、子への書き込みでは reset しない(葉のパスを並べるか nonce を使う)。 |
2194
+
2195
+ **出力はランタイムが所有します**(`$streams` の値と同じ)。state にそのプロパティが無ければ `initial` で実体化され(plain なデータは複製するので、出力の plain な部分の子パスへ書いても宣言の `initial` は変わりません。クラスのインスタンスや凍結された値など plain でない値は宣言と共有したままです)、他のパスと同じようにバインドできます。stream の restart・切断と再接続・同じオブジェクトの再セットを跨いで残り、新しい宣言での再セットでは作り直されます。出力名が getter・setter・メソッド・`$streams` のエントリと衝突すると raise します。
2196
+
2197
+ 2 つの source の発火:
2198
+
2199
+ | | `from`(パス) | `on`(イベントトークン) |
2200
+ |---|---|---|
2201
+ | 単位 | 更新バッチに載ったアドレス 1 つにつき 1 回。同じ job 内の複数の書き込みは 1 回に畳まれる。 | イベント 1 回につき 1 回。同じ task の 2 回は 2 回畳む。 |
2202
+ | いつ | drain の終わり、`$watch` より先。 | イベントの中、そのトークンの `$on` ハンドラより先。 |
2203
+ | 出力が見えるのは | 次のバッチから。出力を見る `$watch` はそこで発火し、`prev` はふつう `undefined`(下の注記)。 | すぐ。同じイベントの `$on` ハンドラは畳んだ後の値を見る。 |
2204
+
2205
+ 主なルール:
2206
+
2207
+ - **getter を畳まない。** getter は入力が変わるたびに再評価されるので、畳むと出来事ではなく再評価の回数を数えます。`from` や `resetOn` に getter を書く(`from` に `$recursion` の `**` getter の展開形 `nodes.*.total` を書くのも同じ)と、宣言時に raise します(`wcs/scan-source-computed`)。
2208
+ - **1 回の fold は着地ごとで、ページごとではない。** `done` 後の再試行や、ページの再接続は同じページをもう一度着地させます。問題になるなら fold に冪等キーを持たせてください(上の `pages`)。
2209
+ - **stream の `args` を自分の scan 出力から導出しない。** `feed` から導出した getter(`feed` を畳む別の scan の出力から導出したものを含む)を `pageResult` の `args` が読むと、stream が自分の結果で restart し続けるので、ランタイムは `wcs/scan-feedback-loop` を raise します。カーソルはイベントから進めてください。stream の restart と同じバッチに着地した chunk は abort される run のものなので畳みません。
2210
+ - **要素の出来事は `on` で受ける。** `from` はそのパスへの書き込みをすべて見ます。バインドした要素の初期同期や、親オブジェクトの丸ごと書き(`prev` は `undefined`)も 1 回として畳みます。`prev` は `$watch` と同じ台帳なので、`$scan` / `$watch` のリスナーの中の書き込み(`$watch` ハンドラや、`from` にした別の scan の出力)でも `undefined` です。台帳はそのリスナーの終わりに消えるので、同じ drain でその後に走る `$streams` の restart の書き込みは `prev` を持ちます。
2211
+ - **fold は有界に。** 無限の source は有界な値(直近 N 件・件数)に畳んでください(`$streams` と同じ)。
2212
+ - **例外は隔離される。** throw・Promise の戻り値・読めない値はコンソールと DevTools に報告され、書き込みません(ワイルドカードの `from` で読めない行はその行だけを飛ばし、行の着地はリストの位置 1 つにつき 1 回に絞ります)。他の scan・watch・stream の restart は続行します。
2213
+ - **`$watch` は scan の書き込みの後に走る。** 同じ drain の `$watch` ハンドラは畳んだ後の出力を読み、ハンドラが出力へ書いた値はそのまま残ります。出力の着地が drain される前に `from` の source がもう一度書かれる(その drain の `$watch` ハンドラが書くなど)と、両方が同じバッチに載ります。このとき出力を見る `$watch` は `prev` に着地した値を受け、`cur` に 1 段先の値を見て、次のバッチで同じ値でもう一度発火することがあるので、同じ値の重複に耐える形にしてください。ユーザー操作で累積を消すなら、`resetOn` に nonce を読ませてください。
2214
+ - **ルートのみ。** ボリューム(`mount=`)は `$scan` を拒否し、マウントされた `bind-component` スコープは 1 回の warn で無視します。SSR では `from` は畳みません(出力の実体化は行います)。
2215
+
2216
+ リファレンス: [docs/scan.ja.md](https://github.com/wcstack/wcstack/blob/main/packages/state/docs/scan.ja.md)。設計の決定レコード: [docs/state-scan-design.md](https://github.com/wcstack/wcstack/blob/main/docs/state-scan-design.md)。
2217
+
1945
2218
  ## Inputs と属性ミラー
1946
2219
 
1947
2220
  `wcBindable.inputs` は一方向のプロパティ入力(state → 要素)を宣言します。エントリに `attribute` を設定すると、フレームワークはプロパティを書き込むたびにその値を当該 HTML 属性へも書き込むため、`attributeChangedCallback`・CSS の属性セレクタ・DevTools がすべてプロパティ値と同期し続けます。
@@ -2227,7 +2500,7 @@ li {
2227
2500
  そのタグが `state` 参加者を受け付けている間、知っておくべき帰結が 2 つある。
2228
2501
 
2229
2502
  - drain は microtask ではなくフレームで着地する。state に書いてから `await Promise.resolve()` で DOM を読むコードは遷移を待つ必要がある。`$updatedCallback` はバインディング適用の直後という*位置*こそ変わらないが、その適用ごと 1 フレーム後ろへずれる。
2230
- - `$watch` と `$streams` restart は元の microtask に留まるため、`$updatedCallback` の**前**に走るようになる。
2503
+ - `$scan`・`$watch`・`$streams` restart は元の microtask に留まるため、`$updatedCallback` の**前**に走るようになる。
2231
2504
 
2232
2505
  適用すべきバインディングが実際にあるバッチだけがタグへ渡されるので、headless なパスへの書き込みが遷移を起こすことはない。タグが無ければ drain は従来どおり。[docs/timing-and-firing-contract.ja.md](https://github.com/wcstack/wcstack/blob/main/docs/timing-and-firing-contract.ja.md) §4.3 参照。
2233
2506
 
@@ -2235,7 +2508,7 @@ li {
2235
2508
 
2236
2509
  ### 存在しないパスへの配線は報告されます
2237
2510
 
2238
- 配線したパスが state 上で解決しないことが**確実**なとき、バインド確立時(`$watch` は宣言時)に 1 回だけ警告します。診断 code はコンソール・`@wcstack/lint`・VS Code 拡張で共通です:
2511
+ 配線したパスが state 上で解決しないことが**確実**なとき、バインド確立時(`$watch` と `$scan` は宣言時)に 1 回だけ警告します。診断 code はコンソール・`@wcstack/lint`・VS Code 拡張で共通です:
2239
2512
 
2240
2513
  ```
2241
2514
  [@wcstack/state] [wcs/binding-path-missing] Bound path "user.nmae" does not resolve on the state tree:
@@ -2248,6 +2521,7 @@ dropped. Validate statically: npx @wcstack/lint <file>.
2248
2521
  | ネストしたパスの打ち間違い(`user.nmae`) | `console.warn`(`wcs/binding-path-missing`)。更新は届かないままなので、直すのは書き手 |
2249
2522
  | トップレベルのパスの打ち間違い(`cout`) | 読み取り時に throw。文面は上と同じ語彙(did-you-mean 付き) |
2250
2523
  | `$watch` のキーの打ち間違い | `console.warn`(`wcs/watch-path-missing`)。単一セグメントでも報告する |
2524
+ | `$scan` の `from` / `resetOn` のパスの打ち間違い | `console.warn`(`wcs/scan-path-missing`)。単一セグメントでも報告する。その scan は一度も畳まれない(reset されない) |
2251
2525
 
2252
2526
  判定は**過小近似**です。静的に決められない形では黙ります —— 誤検知でページを騒がせないことを優先しているためで、以下はすべて警告しません:
2253
2527
 
@@ -2261,13 +2535,26 @@ dropped. Validate statically: npx @wcstack/lint <file>.
2261
2535
 
2262
2536
  ### 添字の本数・階数・循環も検査されます
2263
2537
 
2264
- パス文字列から機械的に決まる整合は、実行時にも lint にも同じ診断 code で現れます。
2538
+ パス文字列から機械的に決まる整合は、実行時にも lint にも同じ診断 code で現れます。ただし下表の 6 つ —— `wcs/getter-depth-exceeded` / `wcs/index-param-range` / `wcs/recursion-context` / `wcs/recursion-shared-list` / `wcs/recursion-cycle` / `wcs/recursion-depth-exceeded` —— はこのリリースでは**実行時専用**で、lint は出しません。
2265
2539
 
2266
2540
  | 診断 | 何を見るか | 直し方 |
2267
2541
  |---|---|---|
2268
2542
  | `wcs/index-arity` | `$resolve(path, indexes)` は `*` の本数と**厳密一致**、`$getAll(path, indexes)` / `$setAll(path, indexes, …)` は**上限**(不足は「残りの階層を全展開」という正当な接頭辞) | 本数を合わせる |
2269
2543
  | `wcs/wildcard-rank` | パスの `*` の本数(と `$N` の N)が、囲む `for` の段数を超えていないか | `for` を足すか、`$resolve(path, indexes)` で行を明示する |
2270
- | `wcs/getter-cycle` | パス getter どうしが循環参照していないか | 循環を断つ |
2544
+ | `wcs/getter-cycle` | パス getter どうしが循環参照していないか。実行時は「アドレススタックが既に積んでいるアドレスへ戻る」ことで判定する | 循環を断つ |
2545
+ | `wcs/getter-depth-exceeded` | getter の評価が 1 パスで評価できる深さ(128 段)を超え、かつ同じアドレスを 2 度通っていない = データが単に深い | 集計の段数を減らすか、木を平らにする |
2546
+ | `wcs/index-param-range` | `$N` は実在するワイルドカード段を指すこと(`$1`〜`$128`・先頭ゼロ不可) | 実在する段を使う |
2547
+ | `wcs/recursion-unsupported` | `**` を解釈しない場所へ `**` が渡った —— markup・`$watch` / `$listKeys` のキー・`$resolve` / `$postUpdate` / `$trackDependency`・代入、あるいは state が `$recursion` を宣言していない | 具体パスを使うか、アンカーを宣言する |
2548
+ | `wcs/recursion-declaration-invalid` | `$recursion` 宣言か `**` getter のキーが、このバージョンが受け付けない形 —— 要素を指さない・途中に添字セグメントを持つ(`"nodes.0.items.*"`)アンカー / 反復サブパス、複数アンカー、getter でない・setter を持つ `**` キー、`get "nodes.**"`、構造を名指す getter、同じ具体パスへ展開する 2 本の getter、展開形と同名の具体 getter。lint が先に出し、実行時は宣言を読んだ時点で throw する | 文面のとおり宣言を直す |
2549
+ | `wcs/recursion-anchor` | 宣言済みのアンカーと合致しない `**` パス(このバージョンは state ごとに単一の自己再帰アンカー)、または `**` の後ろが整形されていない —— 空セグメント(`nodes.**.` / `nodes.**..x`)や `**` 直後の素の `*`(`nodes.**.*`) | 宣言どおりに綴り、その後ろに実在するパスを書く |
2550
+ | `wcs/recursion-context` | **束縛**形の `**` を、束縛先の深さが無い場所で読んだ —— トップレベル、またはアンカー外の getter | 再帰 getter か行 getter の中から読むか、`[]` で全深さを合併する |
2551
+ | `wcs/recursion-getall-form` / `wcs/recursion-setall-form` | `**` に対して定義できない `indexes` の形。コードが付くのは非空の接頭辞(両 API)と、`$getAll` の配列でない `indexes`(`null`・文字列など)。`$setAll` の省略・mapper・`{ spread: true }` も同じ誤りで、lint は同じコードで報告するが、実行時は形を名指しした文面で throw するだけでコードは付かない | 現在の深さなら省略、全深さなら `[]` |
2552
+ | `wcs/recursion-structural-write` | 再帰 `$setAll` が構造そのもの(ノード・子リスト・その `length`・子ノード・反復サブパスが多段なら子リストへ至る途中のオブジェクト)を指している | 葉のプロパティへブロードキャストする |
2553
+ | `wcs/recursion-readonly` | 書き込みが再帰 getter、またはその導出値の内側を指している —— 再帰 `$setAll` の `nodes.**.total` でも、`nodes.*.children.*.total` のような具体的な展開形への書き込み(固定本数の `$setAll`・値付き `$resolve`・直接代入)でも | getter の導出元を書く |
2554
+ | `wcs/recursion-shared-list` / `wcs/recursion-cycle` | 走査が同じ配列インスタンスに 2 度到達した —— 2 つのノードが 1 本の子リストを共有、または自分の祖先から到達可能 | 各ノードに自分の子配列を持たせる |
2555
+ | `wcs/recursion-depth-exceeded` | 展開後のパスがワイルドカード 128 段を超える —— 木がエンジンのアドレス可能な深さより深いか、循環している | 木を平らにするか、循環を探す |
2556
+
2557
+ `wcs/recursion-*` の各行がどの形を拒否していて、代わりに何を書けばよいのかは、上の**再帰パス**の節に書いてあります。
2271
2558
 
2272
2559
  `$resolve` / `$getAll` の**添字の超過は以前は黙って捨てられ**、取り違えたまま「もっともらしい値」が返っていました。現在はどちらもエラーです:
2273
2560
 
@@ -2457,7 +2744,7 @@ it("描画・再描画・ハンドラ実行", async () => {
2457
2744
 
2458
2745
  ユーザー操作と同じ経路で動かすなら、state はインライン(メソッド込み)のまま DOM イベントを発火します。`data-wcs="onclick: up"` のハンドラは `button.click()` で走り、`settle()` 1 回の後に DOM へ反映されます。
2459
2746
 
2460
- - `getBindingsReady(root)` は `root`(`document` か shadow root)配下の全バインド構築が終わると resolve し、バインド初期化に失敗すると reject します(v1.26+)。
2747
+ - `getBindingsReady(root)` は `root`(`document` か shadow root)配下の全バインド構築が終わると resolve し、バインド初期化に失敗すると reject します(v1.26+)。その root のルート `<wcs-state>` が初期化に失敗した場合も reject します —— ロードされなかったルートを「ready」と報告しません。
2461
2748
  - 更新はマイクロタスク境界で収束します。書き込み後の `setTimeout(0)` 1 回で十分です。
2462
2749
  - `state.items = [...state.items, "cherry"]` がリアクティブな書き方です — `state.items.push()` は観測されません(ハンドラ内と同じ規則)。
2463
2750
  - happy-dom は `customElements.define` 時に既存ノードを**差し替えて**アップグレードします。「遅れて define された同一ノードに値が届く」はヘッドレスでは検証できません。happy-dom と実ブラウザのイベントタイミング差ももう 1 つの死角なので、そこは実ブラウザ e2e(Playwright)を 1 本残してください。
@@ -2544,7 +2831,7 @@ bootstrapState();
2544
2831
 
2545
2832
  | エクスポート | 説明 |
2546
2833
  |---|---|
2547
- | `getBindingsReady(root)` | `root`(`document` または shadow root)配下の全バインディングが構築されたら解決。バインディング初期化が失敗すれば reject |
2834
+ | `getBindingsReady(root)` | `root`(`document` または shadow root)配下の全バインディングが構築されたら解決。バインディング初期化が失敗した場合、およびその root のルート state 要素が初期化に失敗した場合は reject |
2548
2835
  | `buildBindings(root)` | `document` / `ShadowRoot` 配下のバインディングを明示的に構築する — その root に最初に登録された `<wcs-state>` がスケジュールするもの |
2549
2836
  | `getConfig()` | 現在の設定(読み取り専用ビュー) |
2550
2837
  | `defineState(obj)` | メソッドと getter 内の `this` に型を付けるアイデンティティ関数 — [TypeScript サポート](#typescript-サポート) 参照 |
@@ -2570,14 +2857,14 @@ bootstrapState();
2570
2857
 
2571
2858
  | プロパティ / メソッド | 説明 |
2572
2859
  |---|---|
2573
- | `initializePromise` | 状態の完全な初期化時に解決される Promise |
2574
- | `connectedCallbackPromise` | `connectedCallback` の完了(state のロードと `$connectedCallback` の実行)で解決される Promise — テストのレシピが await するもの |
2860
+ | `initializePromise` | 状態の完全な初期化時に解決される Promise —— **初期化に失敗したときも解決**します(1 要素の失敗がページの他のバインディングを止めないため)。エラーは `connectedCallbackPromise` に届きます |
2861
+ | `connectedCallbackPromise` | `connectedCallback` の完了(state のロードと `$connectedCallback` の実行)で解決される Promise — テストのレシピが await するもの。**ルート**要素が初期化に失敗すると、**元のエラーのまま reject** し、`console.error` にも 1 件報告します(`$` 宣言の不正・ソースのロード失敗・SSR データの merge 失敗・DCC や `bind-component` の設定エラー・同じ root node に 2 本目のルート `<wcs-state>`。2 本目は登録されないまま読み込んだ state を保持するので取り除いてください。健全な要素の DOM 移動は二重登録ではなく、拒否しません)。**ボリューム**(`<wcs-state mount="…">`)はこの Promise を**拒否しません** —— ボリュームの失敗は解決し、種類によっては自分では何も報告しません。その場合エラーはカスタム要素リアクションが捨てる `connectedCallback` の戻り Promise として出ていき、ブラウザのコンソールには "Uncaught (in promise)" と出ますが、promise を待つ側(テストのレシピや `renderToString()`)には届きません。ロード中に切断された要素は reject しません —— その接続が黙って終わるだけで、付け直せば(行プール)通常どおり初期化して解決します。個々の失敗箇所の正確な挙動は `__tests__/integration.initFailureDiagnostics.test.ts` が固定しています |
2575
2862
  | `listPaths` | `for` ループで使用されるパスの Set |
2576
2863
  | `getterPaths` | getter として定義されたパスの Set |
2577
2864
  | `setterPaths` | setter として定義されたパスの Set |
2578
2865
  | `createState(mutability, callback)` | 状態プロキシを作成(`"readonly"` または `"writable"`) |
2579
2866
  | `createStateAsync(mutability, callback)` | `createState` の非同期版 |
2580
- | `setInitialState(state)` | プログラムから状態を設定(初期化前) |
2867
+ | `setInitialState(state)` | プログラムから状態を設定(初期化前)。初期化に失敗した要素では throw します — 再武装はできないので、要素を取り除いて作り直してください |
2581
2868
  | `nextVersion()` | バージョン番号をインクリメントして返す |
2582
2869
 
2583
2870
  ## アーキテクチャ