@wcstack/state 2.3.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
@@ -248,7 +248,7 @@
248
248
  <div data-wcs="textContent: cart.total"></div>
249
249
  ```
250
250
 
251
- ボリュームは 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 警告付きで無視されます。要素を取り除き、望むパスで新しい要素を追加してください。
252
252
 
253
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。
254
254
 
@@ -1252,7 +1252,7 @@ this.$setAll("nodes.**.selected", [], false); // 全深さの全ノード
1252
1252
 
1253
1253
  走査は深さ方向に降りながら、必要な形をその場で検査します。**同じ配列インスタンス**に 2 度到達したら拒否します。その配列が現在のノードの祖先のものなら循環(`wcs/recursion-cycle`)、そうでなければ 2 つのノードが 1 本の子リストを共有しています(`wcs/recursion-shared-list`)。各ノードに自分の `children` 配列を持たせてください —— **空**配列の使い回しは行を持たず別名化のしようがないので、追跡もせず正当です。
1254
1254
 
1255
- **走査は受け付けるのにエンジンがまだ追えない形が 1 つあります —— 行オブジェクトを作り直して `children` 配列を引き継ぐ置換です。** `this.nodes = this.nodes.map(n => ({ ...n }))` の後も子リストの台帳は配列をキーにしたままなので、その行は**旧**行オブジェクトに結び付いたままになります。その行の集計を一度読んだあとに、その下の葉を更新すると、その行の `nodes.*.total` だけが古いまま残ります —— 葉・より深い集計・すべての `[]` 合併は正しいので、何も警告は出ません。行は path 経由で in-place に書く(`$resolve` / `$setAll`)、行オブジェクトを引き継ぐ(`[...this.nodes]`)、部分木ごと置き換える(深いクローン)のいずれかにすると集計が追従します。これは `**` ではなくリストの同一性の制限で、手書きの `nodes.*.total` / `nodes.*.children.*.total` getter でも同じ挙動になります([#256](https://github.com/wcstack/wcstack/issues/256))。
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
1256
 
1257
1257
  上限は展開後のパスの**ワイルドカード 128 段**です。上の集計 getter は評価中のノードより 1 段下を読むので、127 段の鎖までは畳めて、128 段で `wcs/recursion-depth-exceeded` になります(アンカー・到達した深さ・組み立てようとしたパス・上限を名指しします)。この検査は getter 評価スタック自身の 128 段の上限(`wcs/getter-depth-exceeded`)より先に効くので、深い木は「深い」と報告され、循環の疑いを掛けられることはありません。途中で打ち切ることもしません —— 部分的な集計は、誤った値を正しい値として返すことだからです。
1258
1258
 
@@ -1778,7 +1778,7 @@ interface CommandToken {
1778
1778
 
1779
1779
  - 購読者は要素を `WeakRef` で保持するため、token の購読者セットに残っていても、取り外された要素はガベージコレクト可能
1780
1780
  - `emit` 時、WeakRef が回収済みか要素が接続されていない(`isConnected === false`)場合、購読は自動的に破棄される(lazy purge)
1781
- - 所有する `<wcs-state>` が disconnect されると、token レジストリ全体がクリアされる
1781
+ - 所有する `<wcs-state>` が disconnect されても token レジストリは保持されるので、ルート `<wcs-state>` を付け直した後(ホストを DOM 上で移動したときなど)も購読に命令が届く。切断中は state を作れないので、`$command` 経由の emit は起きない
1782
1782
 
1783
1783
  要素のメソッドは `emit` の引数で呼び出されます:
1784
1784
 
@@ -1955,7 +1955,7 @@ $on: {
1955
1955
 
1956
1956
  ### Token API
1957
1957
 
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 はクリアされます。
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 ツリーが見つからず、届きません。
1959
1959
 
1960
1960
  ## Stream(`$streams`)
1961
1961
 
@@ -2070,9 +2070,9 @@ $updatedCallback(paths) {
2070
2070
  }
2071
2071
  ```
2072
2072
 
2073
- **規則:** 描画に依存させたくないロジックは、`$watch`(または `$streams` の `args`)に根を置いてください。`$updatedCallback` は「描かれたものに追随する」用途に限ります。
2073
+ **規則:** 描画に依存させたくないロジックは、`$watch`・`$scan`・`$streams` の `args` のどれかに根を置いてください。`$updatedCallback` は「描かれたものに追随する」用途に限ります。
2074
2074
 
2075
- 上の例は `$watch` に置き換え済みで、`<b>` は表示専用に戻っています。この形(`$updatedCallback` が、どのバインディングにも現れないパスを判定に使っている)は **`wcs/updated-callback-unbound`** として静的に検出されます。
2075
+ 上の例はいまは `$scan` で feed を積み(sentinel の再武装は `$watch`)、`<b>` は表示専用に戻っています。この形(`$updatedCallback` が、どのバインディングにも現れないパスを判定に使っている)は **`wcs/updated-callback-unbound`** として静的に検出されます。
2076
2076
 
2077
2077
  ### 残る制約
2078
2078
 
@@ -2114,10 +2114,10 @@ $updatedCallback(paths) {
2114
2114
  | 引数 | 契約 |
2115
2115
  |---|---|
2116
2116
  | `cur` | drain 時点の値(そのバッチの確定値) |
2117
- | `prev` | **バッチ開始時点**の値(first-write-wins)。意味を持つのは**スカラのときだけ**(下記) |
2117
+ | `prev` | **バッチ開始時点**の値(first-write-wins)。記録されるのは**プリミティブを書いたときだけ**(書く前の値はオブジェクトでもよい。下記) |
2118
2118
  | `...indexes` | ワイルドカードパスのときのみ。そのスコープ自身のループ添字(`$1` / `$2` と同じ規約) |
2119
2119
 
2120
- **`prev` はスカラ限定です。** same-value guard が既に読んでいる旧値を再利用するため watch のための追加読みは発生せず、その帰結として参照型(in-place 変異では同じ参照になるため)・`$postUpdate` 経由・`config.sameValueGuard` オフのときは `undefined` になります。
2120
+ **`prev` はプリミティブの書き込みにだけ付きます。** same-value guard がプリミティブを書く前に読む旧値を再利用するため watch のための追加読みは発生せず、その帰結として書く値が参照型(in-place 変異では同じ参照になるため)・`$postUpdate` 経由・`config.sameValueGuard` オフのときは `undefined` になります。オブジェクトの上にプリミティブを書いたときは、そのオブジェクトが `prev` に渡ります。
2121
2121
 
2122
2122
  **`$watch` は独自の発火条件を持ちません。** 更新バッチに載ったものをそのまま発火します。これはうまく噛み合っていて、同値の primitive 書き込みは enqueue 前に落ちている(=実質的に変化時のみ発火)一方、occurrence(`semantics: "event"` の property)は**意図的に**落とされないので `cur === prev` で発火します。エッジ検出が要るならハンドラ内で `cur` と `prev` を比較してください。
2123
2123
 
@@ -2127,16 +2127,17 @@ $updatedCallback(paths) {
2127
2127
 
2128
2128
  | 層 | 順序 | 制御 |
2129
2129
  |---|---|---|
2130
- | 機構間 | `$updatedCallback` → `$watch` → `$streams` restart | 固定 |
2130
+ | 機構間 | `$updatedCallback` → `$scan` → `$watch` → `$streams` restart | 固定 |
2131
2131
  | ハンドラ間 | `$watch` の宣言順 | **宣言を並べ替える** |
2132
2132
  | 同一パスの行間 | `indexes` 昇順 | 固定 |
2133
2133
 
2134
- **機構間の層を動かす唯一のもの**が、`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 を参照してください。
2135
2135
 
2136
2136
  主なルール:
2137
2137
 
2138
2138
  - **ツリーのパスのみ** —— パスに `@`(v1 の名前セレクタ)は書けません。含む宣言は loud に拒否されます。
2139
2139
  - **中間値は観測できません** —— 1 バッチ内の `a → b → c` は `cur = c` / `prev = a` で 1 回だけ発火します(binding 更新と同じ契約)。
2140
+ - **行は drain の時点のリストに従います** —— 同じ job で行を書いてから取り除いた・置き換えた・リストを短くした行は発火せず、位置だけが移った行も発火しません。1 つの位置が発火するのは多くても 1 回です。入れ子のリストを置き換えると、新しい配列の行がすべて発火します。
2140
2141
  - **行単位の差分を見たいなら `$listKeys`** —— 未宣言のまま配列全体を代入すると、行 watch は**全行**について `prev === undefined` で発火します(どの行もパス書き込みを通っていないため)。`$listKeys` を宣言すればキー突合が per-field 書き込みに分解するので、変化した行だけが発火し `prev` もスカラで取れます。
2141
2142
  - **headless な行 watch には `$listKeys` が必要** —— `$watch` が単独では headless にならない唯一の箇所です。`items` から `items.*.price` への展開はリストの `for` バインディングが駆動しており、watch を宣言してもそのパスをリストとしては登録しません(意図的)。したがって `for` バインドも `$listKeys` も無い状態で配列を代入すると、行 watch は**一度も**発火しません。`$listKeys` を宣言する(キー突合がフィールドごとにパス書き込みするので展開を経由しない)か、リストを描画してください。スカラーパスは `user.name` のようなネストしたものも含め、この条件なしに headless で発火します。
2142
2143
  - **ハンドラの例外は隔離されます** —— throw はコンソールに報告され、残りの watch(と stream の restart)は続行します。loud fail する `$connectedCallback` / `$updatedCallback` とは異なる扱いです。
@@ -2144,6 +2145,76 @@ $updatedCallback(paths) {
2144
2145
  - **マウントされた `bind-component` スコープでは実行されません** —— マウントされたコンポーネントは宣言面を実行せず、`$watch` の宣言があると 1 回だけ console.warn でルート state(またはボリューム —— `<wcs-state mount>` は `$watch` / `$listKeys` / `$updatedCallback` を持てます)へ誘導します(`$streams` も同様)。plain な(配線なし Shadow の)子は独立ツリーを持つので宣言できます。
2145
2146
  - **SSR では実行されません** —— ハンドラの副作用がサーバーとクライアントで二重に走るためです。
2146
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
+
2147
2218
  ## Inputs と属性ミラー
2148
2219
 
2149
2220
  `wcBindable.inputs` は一方向のプロパティ入力(state → 要素)を宣言します。エントリに `attribute` を設定すると、フレームワークはプロパティを書き込むたびにその値を当該 HTML 属性へも書き込むため、`attributeChangedCallback`・CSS の属性セレクタ・DevTools がすべてプロパティ値と同期し続けます。
@@ -2429,7 +2500,7 @@ li {
2429
2500
  そのタグが `state` 参加者を受け付けている間、知っておくべき帰結が 2 つある。
2430
2501
 
2431
2502
  - drain は microtask ではなくフレームで着地する。state に書いてから `await Promise.resolve()` で DOM を読むコードは遷移を待つ必要がある。`$updatedCallback` はバインディング適用の直後という*位置*こそ変わらないが、その適用ごと 1 フレーム後ろへずれる。
2432
- - `$watch` と `$streams` restart は元の microtask に留まるため、`$updatedCallback` の**前**に走るようになる。
2503
+ - `$scan`・`$watch`・`$streams` restart は元の microtask に留まるため、`$updatedCallback` の**前**に走るようになる。
2433
2504
 
2434
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 参照。
2435
2506
 
@@ -2437,7 +2508,7 @@ li {
2437
2508
 
2438
2509
  ### 存在しないパスへの配線は報告されます
2439
2510
 
2440
- 配線したパスが state 上で解決しないことが**確実**なとき、バインド確立時(`$watch` は宣言時)に 1 回だけ警告します。診断 code はコンソール・`@wcstack/lint`・VS Code 拡張で共通です:
2511
+ 配線したパスが state 上で解決しないことが**確実**なとき、バインド確立時(`$watch` と `$scan` は宣言時)に 1 回だけ警告します。診断 code はコンソール・`@wcstack/lint`・VS Code 拡張で共通です:
2441
2512
 
2442
2513
  ```
2443
2514
  [@wcstack/state] [wcs/binding-path-missing] Bound path "user.nmae" does not resolve on the state tree:
@@ -2450,6 +2521,7 @@ dropped. Validate statically: npx @wcstack/lint <file>.
2450
2521
  | ネストしたパスの打ち間違い(`user.nmae`) | `console.warn`(`wcs/binding-path-missing`)。更新は届かないままなので、直すのは書き手 |
2451
2522
  | トップレベルのパスの打ち間違い(`cout`) | 読み取り時に throw。文面は上と同じ語彙(did-you-mean 付き) |
2452
2523
  | `$watch` のキーの打ち間違い | `console.warn`(`wcs/watch-path-missing`)。単一セグメントでも報告する |
2524
+ | `$scan` の `from` / `resetOn` のパスの打ち間違い | `console.warn`(`wcs/scan-path-missing`)。単一セグメントでも報告する。その scan は一度も畳まれない(reset されない) |
2453
2525
 
2454
2526
  判定は**過小近似**です。静的に決められない形では黙ります —— 誤検知でページを騒がせないことを優先しているためで、以下はすべて警告しません:
2455
2527
 
@@ -2672,7 +2744,7 @@ it("描画・再描画・ハンドラ実行", async () => {
2672
2744
 
2673
2745
  ユーザー操作と同じ経路で動かすなら、state はインライン(メソッド込み)のまま DOM イベントを発火します。`data-wcs="onclick: up"` のハンドラは `button.click()` で走り、`settle()` 1 回の後に DOM へ反映されます。
2674
2746
 
2675
- - `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」と報告しません。
2676
2748
  - 更新はマイクロタスク境界で収束します。書き込み後の `setTimeout(0)` 1 回で十分です。
2677
2749
  - `state.items = [...state.items, "cherry"]` がリアクティブな書き方です — `state.items.push()` は観測されません(ハンドラ内と同じ規則)。
2678
2750
  - happy-dom は `customElements.define` 時に既存ノードを**差し替えて**アップグレードします。「遅れて define された同一ノードに値が届く」はヘッドレスでは検証できません。happy-dom と実ブラウザのイベントタイミング差ももう 1 つの死角なので、そこは実ブラウザ e2e(Playwright)を 1 本残してください。
@@ -2759,7 +2831,7 @@ bootstrapState();
2759
2831
 
2760
2832
  | エクスポート | 説明 |
2761
2833
  |---|---|
2762
- | `getBindingsReady(root)` | `root`(`document` または shadow root)配下の全バインディングが構築されたら解決。バインディング初期化が失敗すれば reject |
2834
+ | `getBindingsReady(root)` | `root`(`document` または shadow root)配下の全バインディングが構築されたら解決。バインディング初期化が失敗した場合、およびその root のルート state 要素が初期化に失敗した場合は reject |
2763
2835
  | `buildBindings(root)` | `document` / `ShadowRoot` 配下のバインディングを明示的に構築する — その root に最初に登録された `<wcs-state>` がスケジュールするもの |
2764
2836
  | `getConfig()` | 現在の設定(読み取り専用ビュー) |
2765
2837
  | `defineState(obj)` | メソッドと getter 内の `this` に型を付けるアイデンティティ関数 — [TypeScript サポート](#typescript-サポート) 参照 |
@@ -2785,14 +2857,14 @@ bootstrapState();
2785
2857
 
2786
2858
  | プロパティ / メソッド | 説明 |
2787
2859
  |---|---|
2788
- | `initializePromise` | 状態の完全な初期化時に解決される Promise |
2789
- | `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` が固定しています |
2790
2862
  | `listPaths` | `for` ループで使用されるパスの Set |
2791
2863
  | `getterPaths` | getter として定義されたパスの Set |
2792
2864
  | `setterPaths` | setter として定義されたパスの Set |
2793
2865
  | `createState(mutability, callback)` | 状態プロキシを作成(`"readonly"` または `"writable"`) |
2794
2866
  | `createStateAsync(mutability, callback)` | `createState` の非同期版 |
2795
- | `setInitialState(state)` | プログラムから状態を設定(初期化前) |
2867
+ | `setInitialState(state)` | プログラムから状態を設定(初期化前)。初期化に失敗した要素では throw します — 再武装はできないので、要素を取り除いて作り直してください |
2796
2868
  | `nextVersion()` | バージョン番号をインクリメントして返す |
2797
2869
 
2798
2870
  ## アーキテクチャ
package/README.md CHANGED
@@ -248,7 +248,7 @@ There is **one state tree per root**. To split state across modules, mount a vol
248
248
  <div data-wcs="textContent: cart.total"></div>
249
249
  ```
250
250
 
251
- A volume may declare getters, `$watch`, `$listKeys`, `$updatedCallback`, and `$connectedCallback`/`$disconnectedCallback` — all relative to its mount path. `$errorCallback` is root-only (a binding failure is reported once, to the tree's owner). Load order does not matter (a volume connected before the root is grafted when the root registers). Mount paths must be static (`*`, `$`, `#`, `@` are rejected). Changing `mount` after the element has initialized is not supported: the change is ignored with a console warning — remove the element and add a new one with the desired path.
251
+ A volume may declare getters, `$watch`, `$listKeys`, `$updatedCallback`, and `$connectedCallback`/`$disconnectedCallback` — all relative to its mount path. `$errorCallback` is root-only (a binding failure is reported once, to the tree's owner). Load order does not matter (a volume connected before the root is grafted when the root registers). If the root `<wcs-state>` fails to initialize, the volumes already waiting for it settle with a report of their own instead of waiting forever. That report is the end of the line for those volumes: a volume reported as an orphan does not graft itself later, and its mount slot stays reserved for as long as that root node is alive — the slot ledger is a `WeakMap` keyed by the root node and a slot is never released — so connecting a corrected root afterwards does not bring it back. Fix the root `<wcs-state>` and reload the page. Mount paths must be static (`*`, `$`, `#`, `@` are rejected). Changing `mount` after the element has initialized is not supported: the change is ignored with a console warning — remove the element and add a new one with the desired path.
252
252
 
253
253
  > **Migrating from v1's named states:** `<wcs-state name="cart">` + `total@cart` becomes `<wcs-state mount="cart">` + `cart.total`. In v2 the `name` attribute fails fast and `@` in a path is a parse error, each with this exact guidance. Migration table: [docs/state-mount-design.md](../../docs/state-mount-design.md) §9.
254
254
 
@@ -1253,7 +1253,7 @@ The read-only rule does not depend on spelling `**`. A recursive getter's concre
1253
1253
 
1254
1254
  The walk descends by depth and checks the shape it needs as it goes: reaching the **same array instance** twice is refused. If that array belongs to one of the current node's ancestors it is a cycle (`wcs/recursion-cycle`); otherwise two nodes share one child list (`wcs/recursion-shared-list`). Give every node its own `children` array — sharing an *empty* one is fine and untracked, because it has no rows to alias.
1255
1255
 
1256
- **One shape the walk accepts but the engine cannot follow yet: replacing a row object while keeping its `children` array.** After `this.nodes = this.nodes.map(n => ({ ...n }))` the child list's ledger is still keyed by the array, so its rows stay attached to the *old* row object. Once that row's aggregate has been read, the next leaf update below it leaves that row's `nodes.*.total` stale the leaf, the deeper totals and every `[]` union are still right, so nothing complains. Write rows in place through paths (`$resolve`, `$setAll`), keep the row objects (`[...this.nodes]`), or replace the whole subtree (a deep clone), and the aggregates follow. This is a limit of list identity, not of `**`: hand-written `nodes.*.total` / `nodes.*.children.*.total` getters behave the same way ([#256](https://github.com/wcstack/wcstack/issues/256)).
1256
+ Replacing a row object while keeping its `children` array `this.nodes = this.nodes.map(n => ({ ...n }))` — is an ordinary update, and the aggregates follow it. The child list keeps its existing row objects — so anything keyed by row identity, such as a `bind-component` child scope's rendered rows and any state you have not bound there, survives — and only the retired row they hung under is swapped for the live one, so the next leaf update dirties the row that is actually on screen ([#256](https://github.com/wcstack/wcstack/issues/256)). Two rows *sharing* one `children` array is a different thing. While both rows are in the list it is unchanged: an array has one set of rows, so both rows always agree on every value, and a row getter that reads its parent (`this["nodes.*.value"]`) is evaluated in the context of the row that owns those rows — the one that first expanded the array. What changed is what happens when that owner is removed from the list: the rows follow one of the rows still on screen, so that row's aggregate tracks the shared data instead of freezing at the removed row's numbers — an array still has one set of rows, so with three rows sharing one array a single survivor follows and the others stay frozen. Putting the removed row back hands them straight back to it when that row's own object comes back — whether you reassign the same array instance, build a new array holding the same rows, or put the row back at a different position. When every row is rebuilt instead (`this.nodes = this.nodes.map(n => ({ ...n }))`), no row object matches and the rows end up under whichever row now occupies the owner's old position. Only the same-array-instance restore behaves this way on 2.3.0; restoring with a new array leaves both rows frozen there. Give every node its own array when a child getter reads upward.
1257
1257
 
1258
1258
  The ceiling is **128 wildcard levels** on the expanded path. The aggregate above reads one level below the node it is evaluating, so it folds a chain 127 deep and stops at 128 with `wcs/recursion-depth-exceeded`, naming the anchor, the depth reached, the path it was building, and the limit. That check trips before the getter stack's own 128-frame limit (`wcs/getter-depth-exceeded`), so a deep tree is reported as deep instead of being accused of a cycle. Nothing is truncated on the way: a partial aggregate would be a wrong number reported as a right one.
1259
1259
 
@@ -1782,7 +1782,7 @@ interface CommandToken {
1782
1782
 
1783
1783
  - The subscriber holds the element via `WeakRef`, so a removed element can still be garbage collected even while it remains in the token's subscriber set
1784
1784
  - On `emit`, if the WeakRef has been collected or the element is no longer connected (`isConnected === false`), the subscription is purged automatically (lazy purge)
1785
- - When the owning `<wcs-state>` is disconnected, the entire token registry is cleared
1785
+ - Disconnecting the owning `<wcs-state>` keeps the token registry, so the subscriptions still receive commands after the root `<wcs-state>` is re-attached (for example when its host moves in the DOM). While it is disconnected, the state cannot be created, so nothing emits through `$command`
1786
1786
 
1787
1787
  The element's method is invoked with the arguments from `emit`:
1788
1788
 
@@ -1959,7 +1959,7 @@ $on: {
1959
1959
 
1960
1960
  ### Token API
1961
1961
 
1962
- Event tokens share the same `Token` pub/sub primitive as command tokens — `name` / `size` / `subscribe` / `unsubscribe` / `emit`, with subscribe-order preservation (see [Token API](#token-api)). The token is resolved from the registry on every event so a re-`setInitialState()` rebuild still reaches the latest `$on` subscribers. When the owning `<wcs-state>` is disconnected, the event-token registry is cleared.
1962
+ Event tokens share the same `Token` pub/sub primitive as command tokens — `name` / `size` / `subscribe` / `unsubscribe` / `emit`, with subscribe-order preservation (see [Token API](#token-api)). The token is resolved from the registry on every event so a re-`setInitialState()` rebuild still reaches the latest `$on` subscribers. Disconnecting the owning `<wcs-state>` keeps the event-token registry, so `$on` handlers (and `on` scans) receive events again once the root `<wcs-state>` is re-attached; an event dispatched while it is disconnected finds no state tree and is not delivered.
1963
1963
 
1964
1964
  ## Streams (`$streams`)
1965
1965
 
@@ -2074,9 +2074,9 @@ $updatedCallback(paths) {
2074
2074
  }
2075
2075
  ```
2076
2076
 
2077
- **The rule:** logic that must not depend on what is rendered belongs on a `$watch` (or a `$streams` `args`). Keep `$updatedCallback` for "follow what was drawn".
2077
+ **The rule:** logic that must not depend on what is rendered belongs on a `$watch`, a `$scan`, or a `$streams` `args`. Keep `$updatedCallback` for "follow what was drawn".
2078
2078
 
2079
- That example now uses `$watch`, and the `<b>` is display-only again. This shape — `$updatedCallback` testing a path that is not bound anywhere — is detected statically as **`wcs/updated-callback-unbound`**.
2079
+ That example now accumulates its feed with `$scan` (and re-arms the sentinel from a `$watch`), and the `<b>` is display-only again. This shape — `$updatedCallback` testing a path that is not bound anywhere — is detected statically as **`wcs/updated-callback-unbound`**.
2080
2080
 
2081
2081
  ### The limitation that remains
2082
2082
 
@@ -2118,10 +2118,10 @@ The handler runs with `this` bound to a **writable** state proxy, so it can writ
2118
2118
  | Argument | Contract |
2119
2119
  |---|---|
2120
2120
  | `cur` | The value at drain time (the settled value for the batch) |
2121
- | `prev` | The value at the **start of the batch** (first-write-wins). Meaningful **for scalars only** — see below |
2121
+ | `prev` | The value at the **start of the batch** (first-write-wins). Recorded **only when a primitive is written** (the value before may be an object) — see below |
2122
2122
  | `...indexes` | Only for wildcard paths: this scope's own loop indexes, same convention as `$1`, `$2` |
2123
2123
 
2124
- **`prev` is scalar-only.** It reuses the old value the same-value guard already reads, so watch costs no extra read — and it is `undefined` for reference types (an in-place mutation would give you the same reference anyway), for `$postUpdate`, and when `config.sameValueGuard` is off.
2124
+ **`prev` comes only with primitive writes.** It reuses the old value the same-value guard reads before writing a primitive, so watch costs no extra read — and it is `undefined` when the new value is a reference type (an in-place mutation would give you the same reference anyway), for `$postUpdate`, and when `config.sameValueGuard` is off. A primitive written over an object passes that object as `prev`.
2125
2125
 
2126
2126
  **Watch adds no firing condition of its own.** It fires for whatever landed in the update batch. That falls out well: an equal primitive write is already dropped before it is enqueued (so you effectively get change-only firing), while an occurrence write — a `semantics: "event"` property — is deliberately *not* dropped, and still fires with `cur === prev`. If you need edge detection, compare `cur` and `prev` in the handler.
2127
2127
 
@@ -2131,16 +2131,17 @@ Firing order is defined in three layers, and only the middle one is yours to ste
2131
2131
 
2132
2132
  | Layer | Order | Your control |
2133
2133
  |---|---|---|
2134
- | Mechanisms | `$updatedCallback` → `$watch` → `$streams` restart | fixed |
2134
+ | Mechanisms | `$updatedCallback` → `$scan` → `$watch` → `$streams` restart | fixed |
2135
2135
  | Between handlers | declaration order in `$watch` | **reorder the declarations** |
2136
2136
  | Between rows of one path | ascending `indexes` | fixed |
2137
2137
 
2138
- **The one thing that moves the mechanism layer** is a `<wcs-view-transition>` that accepts the `state` participant. Binding application — and with it `$updatedCallback` — then lands on a frame, while `$watch` and the `$streams` restart stay on the microtask the drain was queued on, because they consume state addresses and not the DOM. For as long as the tag is present the order is `$watch` → `$streams` restart → `$updatedCallback`. Nothing else on the page reorders this layer; see [docs/timing-and-firing-contract.md](https://github.com/wcstack/wcstack/blob/main/docs/timing-and-firing-contract.md) §4.3.
2138
+ **The one thing that moves the mechanism layer** is a `<wcs-view-transition>` that accepts the `state` participant. Binding application — and with it `$updatedCallback` — then lands on a frame, while `$scan`, `$watch` and the `$streams` restart stay on the microtask the drain was queued on, because they consume state addresses and not the DOM. For as long as the tag is present the order is `$scan` → `$watch` → `$streams` restart → `$updatedCallback`. Nothing else on the page reorders this layer; see [docs/timing-and-firing-contract.md](https://github.com/wcstack/wcstack/blob/main/docs/timing-and-firing-contract.md) §4.3.
2139
2139
 
2140
2140
  Key rules:
2141
2141
 
2142
2142
  - **Paths of the tree only** — a path may not contain `@` (the v1 name selector); such a declaration is rejected loudly.
2143
2143
  - **Intermediate values are not observable** — a batch that goes `a → b → c` fires once with `cur = c`, `prev = a`, the same contract as binding updates.
2144
+ - **Rows follow the list as it stands at the drain** — a row written and then removed, replaced or cut off in the same job does not fire, a row that only moved into another position does not fire, and each position fires at most once. Replacing a nested list fires for every row of the new array.
2144
2145
  - **Row-level diffs want `$listKeys`** — without it, assigning a whole array fires the row watch for *every* row with `prev === undefined`, because no row went through a path write. With `$listKeys` declared, the key match decomposes the assignment into per-field writes, so only changed rows fire and `prev` is a real scalar.
2145
2146
  - **A headless row watch requires `$listKeys`** — this is the one place `$watch` is *not* headless on its own. Expanding `items` into `items.*.price` is driven by the list's `for` binding, and declaring a watch deliberately does not register the path as a list. So with neither a `for` binding nor `$listKeys`, assigning the array fires the row watch **zero** times. Add `$listKeys` (the key match writes each field by path, bypassing the expansion) or render the list. Scalar paths — including nested ones like `user.name` — are headless with no such condition.
2146
2147
  - **Handler exceptions are isolated** — a throw is reported to the console and the remaining watches (and stream restarts) still run. This differs from `$connectedCallback` / `$updatedCallback`, which fail loudly.
@@ -2148,6 +2149,76 @@ Key rules:
2148
2149
  - **Not run on a mounted `bind-component` scope** — mounted components do not execute declaration surfaces: the `$watch` declaration is ignored with a one-time console warning that points to the root state (or a volume — `<wcs-state mount>` hosts `$watch` / `$listKeys` / `$updatedCallback`). This applies to `$streams` too. A plain (unwired Shadow) child owns an independent tree and can declare it.
2149
2150
  - **SSR does not run watches** — handler side effects would otherwise execute on both server and client.
2150
2151
 
2152
+ ## Scan (`$scan`)
2153
+
2154
+ `$streams` folds *within* one run — every restart resets the value to `initial` — and `$watch` owns no value. **`$scan`** declares the value that has to outlive both: an accumulation over time, with an owner, a firing unit and a reset condition.
2155
+
2156
+ ```html
2157
+ <wcs-state>
2158
+ <script type="module">
2159
+ export default {
2160
+ page: 1,
2161
+ host: "a",
2162
+ $eventTokens: ["message"],
2163
+ $streams: {
2164
+ pageResult: { args: (s) => s.page, source: loadPage },
2165
+ },
2166
+ $scan: {
2167
+ // from: fold each landing of a state path — here, the stream's value
2168
+ feed: {
2169
+ from: "pageResult",
2170
+ initial: { items: [], pages: [] },
2171
+ fold: (feed, chunk) =>
2172
+ chunk?.kind === "success" && !feed.pages.includes(chunk.page)
2173
+ ? { items: feed.items.concat(chunk.items), pages: [...feed.pages, chunk.page] }
2174
+ : feed,
2175
+ },
2176
+ // on: fold each event of a declared event token
2177
+ log: {
2178
+ on: "message",
2179
+ initial: [],
2180
+ fold: (log, event) => [...log.slice(-49), event.detail],
2181
+ resetOn: ["host"], // back to [] whenever host changes
2182
+ },
2183
+ },
2184
+ };
2185
+ </script>
2186
+ </wcs-state>
2187
+
2188
+ <template data-wcs="for: feed.items">…</template>
2189
+ ```
2190
+
2191
+ | Field | Contract |
2192
+ |---|---|
2193
+ | `from` | A state path. Wildcards are allowed; it may not start with `$`, and may not be a getter or sit under one. Declare exactly one of `from` / `on`. |
2194
+ | `on` | An event-token name declared in `$eventTokens`. |
2195
+ | `initial` | Required. The seed of the accumulator, and what `resetOn` returns to. |
2196
+ | `fold` | Required. `from`: `(acc, cur, prev, ...indexes) => next`. `on`: `(acc, event, ...indexes) => next`. Synchronous, called without `this`, returns a new value. Returning `acc` itself writes nothing. |
2197
+ | `resetOn` | Optional array of plain state paths. When one of them is written, the output returns to `initial`: a `from` scan skips that batch's fold, and an `on` scan folds any event that comes after the write into `initial`. A path under `from` raises; an ancestor of `from` is allowed (start over when the parent is replaced). An object path resets only when that object itself is written, not on writes to its children — list the leaf paths or use a nonce. |
2198
+
2199
+ **The runtime owns the output**, like a `$streams` value. It is materialized from `initial` when the state does not already have that property (plain data is copied, so writing a child path in the plain part of the output never changes the declared `initial`; class instances, frozen values and other non-plain values stay shared with it), and you bind it like any other path. It survives stream restarts, disconnect and reconnect, and a re-set of the same object; a re-set with a new declaration rebuilds the scan. An output name that collides with a getter, a setter, a method or a `$streams` entry raises.
2200
+
2201
+ How the two sources fire:
2202
+
2203
+ | | `from` (a path) | `on` (an event token) |
2204
+ |---|---|---|
2205
+ | Unit | One fold per address that landed in an update batch. Writes made in one job are coalesced. | One fold per event. Two events in one task fold twice. |
2206
+ | When | At the end of the drain, before `$watch`. | Inside the event, before that token's `$on` handlers. |
2207
+ | Output visible | From the next batch. A `$watch` on the output fires then, normally with `prev === undefined` (see below). | Immediately. The `$on` handlers of the same event already see it. |
2208
+
2209
+ Key rules:
2210
+
2211
+ - **Never fold a getter.** A getter re-evaluates whenever its inputs change, so a fold over it would count re-evaluations, not events. A getter as `from` or `resetOn` — or an expansion of a `$recursion` `**` getter such as `nodes.*.total` as `from` — raises at declaration (`wcs/scan-source-computed`).
2212
+ - **One fold per landing, not per page.** A retry after the page is `done`, or reconnecting the page, lands the same page again. When that matters, keep an idempotency key in the fold — the `pages` list above.
2213
+ - **Do not derive a stream's `args` from its own scan output.** A getter over `feed` — or over another scan that folds `feed` — read by `pageResult`'s `args` would restart the stream on its own result, so the runtime raises `wcs/scan-feedback-loop`. Advance the cursor from an event instead. A chunk that lands in the same batch as its stream's restart belongs to the aborted run and is not folded.
2214
+ - **Receive element events through `on`.** A `from` path sees every write to that path, including a bound element's initial sync and a whole-parent write (which arrives with `prev === undefined`). `prev` follows `$watch`'s ledger, so it is also `undefined` for a write made inside the `$scan` / `$watch` listener — by a `$watch` handler, or by another scan whose output is the `from`. The ledger is cleared at the end of that listener, so the `$streams` restart that runs after it in the same drain keeps `prev`.
2215
+ - **Keep folds bounded.** An infinite source must fold into a bounded value (the last N, a count), exactly as with `$streams`.
2216
+ - **Errors are isolated.** A throw, a returned Promise or a value that cannot be read is reported to the console and DevTools and writes nothing (an unreadable row of a wildcard `from` is skipped alone, and row landings are narrowed to one per list position); the other scans, watches and stream restarts still run.
2217
+ - **`$watch` runs after the scan write.** A `$watch` handler in the same drain reads the output as folded, and a value it writes to the output stays. When the `from` source is written again before the output's landing drains — by a `$watch` handler in that drain, say — both land in one batch: a `$watch` on the output then gets the landed value in `prev`, sees `cur` one step ahead, and can fire again with the same value in the next batch, so make it tolerant of a repeated value. Clear an accumulation from a user action with a nonce read by `resetOn`.
2218
+ - **Root only.** A volume (`mount=`) refuses `$scan`, and a mounted `bind-component` scope ignores it with a one-time warning. Under SSR, `from` does not fold; the output is still materialized.
2219
+
2220
+ Reference: [docs/scan.md](https://github.com/wcstack/wcstack/blob/main/packages/state/docs/scan.md). Design record: [docs/state-scan-design.md](https://github.com/wcstack/wcstack/blob/main/docs/state-scan-design.md).
2221
+
2151
2222
  ## Inputs and Attribute Mirror
2152
2223
 
2153
2224
  `wcBindable.inputs` declares one-way property inputs (state → element). When an entry sets `attribute`, the framework writes the value to that HTML attribute every time it writes the property, so `attributeChangedCallback`, CSS attribute selectors, and DevTools all stay in sync with the property value.
@@ -2433,7 +2504,7 @@ li {
2433
2504
  Two consequences to know while that tag accepts the `state` participant:
2434
2505
 
2435
2506
  - The drain lands on a frame instead of a microtask, so code that writes state and then reads the DOM after `await Promise.resolve()` must wait for the transition. `$updatedCallback` still fires immediately after the bindings are applied — its *position* is unchanged, but it moves a frame later along with them.
2436
- - Because `$watch` and the `$streams` restart stay on the original microtask, they now run **before** `$updatedCallback` instead of after it.
2507
+ - Because `$scan`, `$watch` and the `$streams` restart stay on the original microtask, they now run **before** `$updatedCallback` instead of after it.
2437
2508
 
2438
2509
  Only a batch that actually has bindings to apply is handed to the tag, so a write to a headless path never starts a transition. Without the tag the drain is exactly what it was. See [docs/timing-and-firing-contract.md](https://github.com/wcstack/wcstack/blob/main/docs/timing-and-firing-contract.md) §4.3.
2439
2510
 
@@ -2441,7 +2512,7 @@ Only a batch that actually has bindings to apply is handed to the tag, so a writ
2441
2512
 
2442
2513
  ### Wiring to a path that does not exist is reported
2443
2514
 
2444
- When a wired path provably does not resolve against the state, you get one warning at binding time (at declaration time for `$watch`). The diagnostic codes are shared by the console, `@wcstack/lint`, and the VS Code extension:
2515
+ When a wired path provably does not resolve against the state, you get one warning at binding time (at declaration time for `$watch` and `$scan`). The diagnostic codes are shared by the console, `@wcstack/lint`, and the VS Code extension:
2445
2516
 
2446
2517
  ```
2447
2518
  [@wcstack/state] [wcs/binding-path-missing] Bound path "user.nmae" does not resolve on the state tree:
@@ -2454,6 +2525,7 @@ dropped. Validate statically: npx @wcstack/lint <file>.
2454
2525
  | Typo in a nested path (`user.nmae`) | `console.warn` (`wcs/binding-path-missing`). Updates still never arrive — you fix it |
2455
2526
  | Typo in a top-level path (`cout`) | Throws on read, with the same wording and did-you-mean |
2456
2527
  | Typo in a `$watch` key | `console.warn` (`wcs/watch-path-missing`), reported even for a single segment |
2528
+ | Typo in a `$scan` `from` / `resetOn` path | `console.warn` (`wcs/scan-path-missing`), reported even for a single segment. The scan never folds (or never resets) |
2457
2529
 
2458
2530
  The check **under-approximates**: it stays silent for anything it cannot decide statically, because a false alarm costs more than a missed one. None of these warn:
2459
2531
 
@@ -2680,7 +2752,7 @@ it("renders, re-renders, and runs handlers", async () => {
2680
2752
 
2681
2753
  To drive the page the way a user does, keep the state inline (methods included) and dispatch DOM events; a `data-wcs="onclick: up"` handler runs on `button.click()`, and the DOM reflects the write after one `settle()`.
2682
2754
 
2683
- - `getBindingsReady(root)` resolves once every binding under `root` (a `document` or a shadow root) is built, and rejects if binding initialization fails (v1.26+).
2755
+ - `getBindingsReady(root)` resolves once every binding under `root` (a `document` or a shadow root) is built, and rejects if binding initialization fails (v1.26+) or if the root's `<wcs-state>` failed to initialize — a root that never loaded reports the failure instead of "ready".
2684
2756
  - Updates settle on the microtask queue; a single `setTimeout(0)` after a write is enough.
2685
2757
  - `state.items = [...state.items, "cherry"]` is the reactive form — `state.items.push()` is not observed (same rule as in handlers).
2686
2758
  - Under happy-dom, `customElements.define` upgrades existing nodes by **replacing** them; "a value reaches the same node after a late define" cannot be asserted headlessly. Event timing differences between happy-dom and real browsers are the other blind spot — keep one browser e2e (Playwright) for those.
@@ -2767,7 +2839,7 @@ bootstrapState();
2767
2839
 
2768
2840
  | Export | Description |
2769
2841
  |---|---|
2770
- | `getBindingsReady(root)` | Resolves once every binding under `root` (a `document` or a shadow root) is built; rejects if binding initialization fails |
2842
+ | `getBindingsReady(root)` | Resolves once every binding under `root` (a `document` or a shadow root) is built; rejects if binding initialization fails, or if the root's state element failed to initialize |
2771
2843
  | `buildBindings(root)` | Build the bindings under a `document` or `ShadowRoot` explicitly — what the first `<wcs-state>` registered on a root schedules for it |
2772
2844
  | `getConfig()` | The current configuration (read-only view) |
2773
2845
  | `defineState(obj)` | Identity function that types `this` inside methods and getters — see [TypeScript Support](#typescript-support) |
@@ -2793,14 +2865,14 @@ Subpath entries for tooling: `@wcstack/state/parser` (the `data-wcs` parser as a
2793
2865
 
2794
2866
  | Property / Method | Description |
2795
2867
  |---|---|
2796
- | `initializePromise` | Resolves when state is fully initialized |
2797
- | `connectedCallbackPromise` | Resolves once `connectedCallback` has completed (state loaded, `$connectedCallback` run) — what the testing recipes await |
2868
+ | `initializePromise` | Resolves when state is fully initialized — and also **when initialization fails**, so one element's failure never blocks the rest of the page's bindings; the error is delivered on `connectedCallbackPromise` |
2869
+ | `connectedCallbackPromise` | Resolves once `connectedCallback` has completed (state loaded, `$connectedCallback` run) — what the testing recipes await. A **root** element that fails to initialize **rejects** it with the original error, unwrapped, and reports the failure once with `console.error`: an invalid `$` declaration, a source it cannot load, the SSR data merge, a DCC or `bind-component` setup error, or a second root `<wcs-state>` on the same root node (that second element stays unregistered but keeps the state it loaded, so remove it; moving a healthy element in the DOM is not a duplicate and is never refused). A **volume** (`<wcs-state mount="…">`) never rejects it — a volume failure resolves it instead, and some volume failures report nothing of their own: the error leaves as the `connectedCallback` promise that custom-element reactions discard, which a browser console shows as "Uncaught (in promise)" but nothing awaiting these promises (a test recipe, `renderToString()`) ever sees. Detaching an element while its source is still loading rejects nothing — that connection just ends, and re-appending the element (row pooling) initializes it and resolves normally. For the exact behaviour of any single failure site, read `__tests__/integration.initFailureDiagnostics.test.ts`: it pins every case |
2798
2870
  | `listPaths` | Set of paths used in `for` loops |
2799
2871
  | `getterPaths` | Set of paths defined as getters |
2800
2872
  | `setterPaths` | Set of paths defined as setters |
2801
2873
  | `createState(mutability, callback)` | Create a state proxy (`"readonly"` or `"writable"`) |
2802
2874
  | `createStateAsync(mutability, callback)` | Async version of `createState` |
2803
- | `setInitialState(state)` | Set state programmatically (before initialization) |
2875
+ | `setInitialState(state)` | Set state programmatically (before initialization). Throws if the element already failed to initialize — such an element cannot be re-armed; remove it and create a new one |
2804
2876
  | `nextVersion()` | Increment and return version number |
2805
2877
 
2806
2878
  ## Architecture