@wcstack/state 1.21.5 → 1.21.7

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/dist/index.esm.js CHANGED
@@ -1870,6 +1870,14 @@ function collectNodesAndBindingInfosByFragment(root, nodeInfos) {
1870
1870
  function unregisterNode(node) {
1871
1871
  registeredNodeSet.delete(node);
1872
1872
  }
1873
+ /**
1874
+ * RowPlan 経路(createContent のプラン実体化)用。パース・spread 展開を経ずに
1875
+ * binding を組み立てた subscriber ノードを二重処理防止台帳へ載せる
1876
+ * (後続の collectNodesAndBindingInfos による再スキャンから保護)。
1877
+ */
1878
+ function markNodeRegistered(node) {
1879
+ registeredNodeSet.add(node);
1880
+ }
1873
1881
  /**
1874
1882
  * Re-process a deferred spread entry once the custom element class is
1875
1883
  * registered. Expands the captured parseResults, installs bindings, and
@@ -2082,12 +2090,13 @@ function getListIndexByBindingInfo(bindingInfo) {
2082
2090
  }
2083
2091
 
2084
2092
  const absoluteStateAddressByBinding = new WeakMap();
2085
- function getAbsoluteStateAddressByBinding(binding) {
2086
- // 切断されていても、キャッシュされていれば絶対状態アドレスを返す。
2087
- let absoluteStateAddress = null;
2088
- absoluteStateAddress = absoluteStateAddressByBinding.get(binding) || null;
2089
- if (absoluteStateAddress !== null) {
2090
- return absoluteStateAddress;
2093
+ /**
2094
+ * binding の解決済み root を返す。knownRootNode があれば getRootNode() と
2095
+ * fragment フォールバックを省略する(リスト行活性化のホットパス)。
2096
+ */
2097
+ function resolveBindingRootNode(binding, knownRootNode) {
2098
+ if (knownRootNode != null) {
2099
+ return knownRootNode;
2091
2100
  }
2092
2101
  let rootNode = binding.replaceNode.getRootNode();
2093
2102
  // binding.replaceNodeはisConnected=trueになっていることが前提、切断されている場合はraiseErrorを返す
@@ -2101,6 +2110,16 @@ function getAbsoluteStateAddressByBinding(binding) {
2101
2110
  rootNode = rootNodeByFragment;
2102
2111
  }
2103
2112
  }
2113
+ return rootNode;
2114
+ }
2115
+ function getAbsoluteStateAddressByBinding(binding, knownRootNode) {
2116
+ // 切断されていても、キャッシュされていれば絶対状態アドレスを返す。
2117
+ let absoluteStateAddress = null;
2118
+ absoluteStateAddress = absoluteStateAddressByBinding.get(binding) || null;
2119
+ if (absoluteStateAddress !== null) {
2120
+ return absoluteStateAddress;
2121
+ }
2122
+ const rootNode = resolveBindingRootNode(binding, knownRootNode);
2104
2123
  const listIndex = getListIndexByBindingInfo(binding);
2105
2124
  const stateElement = getStateElementByName(rootNode, binding.stateName);
2106
2125
  if (stateElement === null) {
@@ -2133,41 +2152,114 @@ function setDevtoolsSink(sink) {
2133
2152
  devtoolsSink = sink;
2134
2153
  }
2135
2154
 
2136
- const bindingSetByAbsoluteStateAddress = new WeakMap();
2137
- function getBindingSetByAbsoluteStateAddress(absoluteStateAddress) {
2138
- let bindingSet = null;
2139
- bindingSet = bindingSetByAbsoluteStateAddress.get(absoluteStateAddress) || null;
2140
- if (bindingSet === null) {
2141
- bindingSet = new Set();
2142
- bindingSetByAbsoluteStateAddress.set(absoluteStateAddress, bindingSet);
2143
- }
2144
- return bindingSet;
2145
- }
2146
2155
  /**
2147
- * 参照専用の取得。get-or-create と違い、未登録アドレスに空 Set
2148
- * 生成・キャッシュしない(リスト置換の drain は大量のバインディング無し
2149
- * アドレスを照会するため、生成すると空 Set が溜まり続ける)。
2156
+ * 絶対アドレス 登録 binding の台帳。
2157
+ *
2158
+ * リスト行の絶対アドレスは (absolutePathInfo, listIndex) の組ごとに一意で、
2159
+ * 登録される binding は通常 1 本しかない。アドレスごとに Set を確保すると
2160
+ * 行×binding の数だけ Set アロケーションが積み上がるため、単一値で持ち
2161
+ * 2 本目から Set に昇格する(interestedSessionsByNode と同じ前例)。
2150
2162
  */
2151
- function peekBindingSetByAbsoluteStateAddress(absoluteStateAddress) {
2152
- return bindingSetByAbsoluteStateAddress.get(absoluteStateAddress);
2153
- }
2163
+ const bindingsByAbsoluteStateAddress = new WeakMap();
2154
2164
  function addBindingByAbsoluteStateAddress(absoluteStateAddress, binding) {
2155
- const bindingSet = getBindingSetByAbsoluteStateAddress(absoluteStateAddress);
2156
- bindingSet.add(binding);
2165
+ const current = bindingsByAbsoluteStateAddress.get(absoluteStateAddress);
2166
+ if (typeof current === "undefined") {
2167
+ bindingsByAbsoluteStateAddress.set(absoluteStateAddress, binding);
2168
+ }
2169
+ else if (current instanceof Set) {
2170
+ current.add(binding);
2171
+ }
2172
+ else if (current !== binding) {
2173
+ bindingsByAbsoluteStateAddress.set(absoluteStateAddress, new Set([current, binding]));
2174
+ }
2157
2175
  if (devtoolsSink !== null) {
2158
2176
  devtoolsSink({ type: "state:binding-added", absoluteAddress: absoluteStateAddress, binding });
2159
2177
  }
2160
2178
  }
2161
2179
  function removeBindingByAbsoluteStateAddress(absoluteStateAddress, binding) {
2162
- // get-or-create を通すと未登録アドレスに空 Set を生成してしまうため素の get で参照する
2163
- const bindingSet = bindingSetByAbsoluteStateAddress.get(absoluteStateAddress);
2164
- if (bindingSet !== undefined) {
2165
- bindingSet.delete(binding);
2166
- if (devtoolsSink !== null) {
2167
- devtoolsSink({ type: "state:binding-removed", absoluteAddress: absoluteStateAddress, binding });
2168
- }
2180
+ const current = bindingsByAbsoluteStateAddress.get(absoluteStateAddress);
2181
+ if (typeof current === "undefined") {
2182
+ return;
2183
+ }
2184
+ if (current instanceof Set) {
2185
+ current.delete(binding);
2186
+ }
2187
+ else if (current === binding) {
2188
+ bindingsByAbsoluteStateAddress.delete(absoluteStateAddress);
2189
+ }
2190
+ if (devtoolsSink !== null) {
2191
+ devtoolsSink({ type: "state:binding-removed", absoluteAddress: absoluteStateAddress, binding });
2192
+ }
2193
+ }
2194
+ /**
2195
+ * パターン索引台帳(リスト行バインディング専用・docs/state-row-instantiation-redesign.md §3-3)。
2196
+ *
2197
+ * 行バインディングは (absolutePathInfo, listIndex) の 2 段キーで登録し、登録側では
2198
+ * AbsoluteStateAddress の intern(アドレスオブジェクト割当 + listIndex ごとの
2199
+ * intern 用 WeakMap)を一切行わない。書き込み側(setByAddress → enqueue)は従来
2200
+ * どおり intern 済みアドレスを使うため、drain はアドレスの構成要素
2201
+ * (absolutePathInfo / listIndex — どちらもオブジェクト同一性が保証済み)で
2202
+ * このパターン台帳を引ける。リオーダーは listIndex 同一性キーの帰結として
2203
+ * 従来同様ゼロタッチ。wholesale destroy は従来同様削除ゼロ(listIndex ごと GC 崩壊)。
2204
+ *
2205
+ * devtools 計装(state:binding-added/removed)はプロトコル契約なので、sink 接続時に
2206
+ * 限りアドレスを intern してイベントを流す(フック未接続時のコストは分岐 1 個の規範を維持)。
2207
+ */
2208
+ const patternLedger = new WeakMap();
2209
+ function addBindingByPattern(absolutePathInfo, listIndex, binding) {
2210
+ let rowMap = patternLedger.get(absolutePathInfo);
2211
+ if (typeof rowMap === "undefined") {
2212
+ rowMap = new WeakMap();
2213
+ patternLedger.set(absolutePathInfo, rowMap);
2214
+ }
2215
+ const current = rowMap.get(listIndex);
2216
+ if (typeof current === "undefined") {
2217
+ rowMap.set(listIndex, binding);
2218
+ }
2219
+ else if (current instanceof Set) {
2220
+ current.add(binding);
2221
+ }
2222
+ else if (current !== binding) {
2223
+ rowMap.set(listIndex, new Set([current, binding]));
2224
+ }
2225
+ if (devtoolsSink !== null) {
2226
+ devtoolsSink({ type: "state:binding-added", absoluteAddress: createAbsoluteStateAddress(absolutePathInfo, listIndex), binding });
2169
2227
  }
2170
2228
  }
2229
+ function removeBindingByPattern(absolutePathInfo, listIndex, binding) {
2230
+ const rowMap = patternLedger.get(absolutePathInfo);
2231
+ if (typeof rowMap === "undefined") {
2232
+ return;
2233
+ }
2234
+ const current = rowMap.get(listIndex);
2235
+ if (typeof current === "undefined") {
2236
+ return;
2237
+ }
2238
+ if (current instanceof Set) {
2239
+ current.delete(binding);
2240
+ }
2241
+ else if (current === binding) {
2242
+ rowMap.delete(listIndex);
2243
+ }
2244
+ if (devtoolsSink !== null) {
2245
+ devtoolsSink({ type: "state:binding-removed", absoluteAddress: createAbsoluteStateAddress(absolutePathInfo, listIndex), binding });
2246
+ }
2247
+ }
2248
+ /**
2249
+ * drain(updater)用の統合参照。従来台帳 → パターン台帳の順に引く。
2250
+ * 従来台帳を先に引くのは、listIndex 付きでも旧経路(SSR ハイドレーション等)で
2251
+ * アドレス台帳に登録される可能性を許容するため(取りこぼし防止)。
2252
+ */
2253
+ function peekBindingsForAddress(absoluteStateAddress) {
2254
+ const entry = bindingsByAbsoluteStateAddress.get(absoluteStateAddress);
2255
+ if (typeof entry !== "undefined") {
2256
+ return entry;
2257
+ }
2258
+ if (absoluteStateAddress.listIndex === null) {
2259
+ return undefined;
2260
+ }
2261
+ return patternLedger.get(absoluteStateAddress.absolutePathInfo)?.get(absoluteStateAddress.listIndex);
2262
+ }
2171
2263
 
2172
2264
  const _cache$1 = new WeakMap();
2173
2265
  const _cacheNullListIndex = new WeakMap();
@@ -3552,8 +3644,21 @@ function bindingKey(binding) {
3552
3644
  binding.uuid ?? "",
3553
3645
  ].join("\u0000");
3554
3646
  }
3647
+ function addRecordTeardown(record, teardown) {
3648
+ if (record.teardowns === null) {
3649
+ record.teardowns = new Set();
3650
+ }
3651
+ record.teardowns.add(teardown);
3652
+ }
3555
3653
  class BindingSession {
3556
3654
  records = new Set();
3655
+ /**
3656
+ * initializeRow で設定される行プラン。非 null のとき activate は
3657
+ * スロット整列の高速経路(activatePlanRows)を使う。
3658
+ */
3659
+ rowPlan = null;
3660
+ // anchor ノードが持つ binding は大多数が 1 本なので単一値で持ち、2 本目から
3661
+ // Map(remember 経路のキー照合用)に昇格する(台帳・興味 session と同じ前例)
3557
3662
  knownBindingsByNode = new WeakMap();
3558
3663
  optionsByBinding = new WeakMap();
3559
3664
  deferredByNode = new WeakMap();
@@ -3562,7 +3667,14 @@ class BindingSession {
3562
3667
  if (root !== null)
3563
3668
  this.observe(root);
3564
3669
  }
3565
- initialize(bindings, options = {}) {
3670
+ /**
3671
+ * knownRoot: 呼び出し側が root を確定済みのときの per-binding observe 省略。
3672
+ * - undefined: 従来どおり binding ごとに anchor から root を導出して observe
3673
+ * - null: detached fragment 上(createContent)の初期化。observableRootFor が
3674
+ * 必ず null を返す状況なので observe(= getRootNode)を丸ごと省略する
3675
+ * - Node: 呼び出し側で owner 保証済み(activate 経由のみ。initialize へは未使用)
3676
+ */
3677
+ initialize(bindings, options = {}, knownRoot) {
3566
3678
  const registerAddress = options.registerAddress ?? true;
3567
3679
  const resolvedOptions = {
3568
3680
  registerAddress,
@@ -3575,7 +3687,7 @@ class BindingSession {
3575
3687
  const existing = recordByBinding.get(binding);
3576
3688
  if (typeof existing !== "undefined" && existing.phase !== "disposed" && existing.phase !== "failed") {
3577
3689
  this.observe(existing.anchor);
3578
- if (resolvedOptions.registerAddress && existing.address === null) {
3690
+ if (resolvedOptions.registerAddress && existing.address === null && existing.patternListIndex === null) {
3579
3691
  existing.options.registerAddress = true;
3580
3692
  this.registerAddress(existing);
3581
3693
  }
@@ -3584,7 +3696,7 @@ class BindingSession {
3584
3696
  this.settleConnectedSnapshot(existing);
3585
3697
  continue;
3586
3698
  }
3587
- this.start(binding, resolvedOptions);
3699
+ this.start(binding, resolvedOptions, knownRoot);
3588
3700
  initialized.push(binding);
3589
3701
  }
3590
3702
  return initialized.filter((binding) => this.shouldApplyState(binding));
@@ -3595,18 +3707,27 @@ class BindingSession {
3595
3707
  * にだけ使える前提で、remember の再実行(キー照合・options マージ・興味登録)を省き、
3596
3708
  * 必要な仕事だけ行う: 初回活性化はアドレス登録+初期同期、pool 再利用(disposed)は
3597
3709
  * start による再構築、未知の binding は防御的に従来 initialize へ倒す。
3710
+ *
3711
+ * knownRoot は呼び出し側(applyChangeToFor / applyChangeToIf の apply context)が
3712
+ * 確定済みの root。owner(root ごとの MutationObserver)の保証を呼び出しあたり
3713
+ * 1 回に集約し、binding ごとの observe(= getRootNode)とアドレス解決の
3714
+ * getRootNode を丸ごと省略する。
3598
3715
  */
3599
- activate(bindings) {
3716
+ activate(bindings, knownRoot) {
3717
+ if (isObservableRoot(knownRoot))
3718
+ getBindingOwner(knownRoot);
3719
+ if (this.rowPlan !== null) {
3720
+ this.activatePlanRows(this.rowPlan, bindings, knownRoot);
3721
+ return;
3722
+ }
3600
3723
  for (const binding of bindings) {
3601
3724
  const record = recordByBinding.get(binding);
3602
3725
  if (typeof record !== "undefined" && record.session === this
3603
3726
  && record.phase !== "disposed" && record.phase !== "failed") {
3604
- if (record.address === null) {
3605
- // 初回活性化(mountAfter 経路では anchor が接続済みのことがあるため、
3606
- // 従来 initialize と同様に owner の存在をここで保証する)
3607
- this.observe(record.anchor);
3727
+ if (record.address === null && record.patternListIndex === null) {
3728
+ // 初回活性化(owner は冒頭で保証済み)
3608
3729
  record.options.registerAddress = true;
3609
- this.registerAddress(record);
3730
+ this.registerAddress(record, knownRoot);
3610
3731
  }
3611
3732
  if (record.phase === "active")
3612
3733
  this.settleInitialRecord(record);
@@ -3621,7 +3742,7 @@ class BindingSession {
3621
3742
  }
3622
3743
  // pool 再利用: record は disposed。活性化要件(アドレス登録)を昇格して再構築
3623
3744
  options.registerAddress = true;
3624
- this.start(binding, options);
3745
+ this.start(binding, options, knownRoot);
3625
3746
  }
3626
3747
  }
3627
3748
  shouldApplyState(binding) {
@@ -3648,7 +3769,7 @@ class BindingSession {
3648
3769
  if (typeof record === "undefined" || !this.isAlive(record, record.generation)) {
3649
3770
  return false;
3650
3771
  }
3651
- record.teardowns.add(teardown);
3772
+ addRecordTeardown(record, teardown);
3652
3773
  return true;
3653
3774
  }
3654
3775
  deferUntilDefined(node, tagName, callback, reject = () => undefined) {
@@ -3728,15 +3849,24 @@ class BindingSession {
3728
3849
  }
3729
3850
  /**
3730
3851
  * 全 record を teardown を走らせずに終端化する(canWholesaleDestroy が true の
3731
- * content 専用)。イベント listener・アドレス台帳・loopContext はノード/binding
3732
- * もろとも GC で崩壊する(recordByBinding 以下は全て弱参照)。
3852
+ * content 専用)。イベント listenerloopContext・パターン台帳(listIndex キー)は
3853
+ * ノード/binding もろとも GC で崩壊する(recordByBinding 以下は全て弱参照)。
3854
+ * 例外は null-listIndex の従来台帳(record.address): キーの intern 済み
3855
+ * AbsoluteStateAddress が PathInfo キャッシュ経由で生涯生存するため GC で
3856
+ * 崩壊せず、共有エントリに残った binding が binding.node 経由で行 DOM 全体を
3857
+ * 永久リークする。ここだけ明示除去する(行イベント binding が典型で行あたり
3858
+ * 高々数件・Set.delete のみなので wholesale の速度特性は保たれる)。
3733
3859
  * handlerBindingRegistry のカウンタは減らないが、残るのはキー文字列と数値のみで
3734
3860
  * 実害はない設計(handlerBindingRegistry.ts の弱参照化コメント参照)。
3735
3861
  */
3736
3862
  destroyRecords() {
3737
3863
  for (const record of this.records) {
3864
+ if (record.address !== null) {
3865
+ removeBindingByAbsoluteStateAddress(record.address, record.info);
3866
+ record.address = null;
3867
+ }
3738
3868
  record.phase = "disposed";
3739
- record.teardowns.clear();
3869
+ record.teardowns = null;
3740
3870
  }
3741
3871
  this.records.clear();
3742
3872
  }
@@ -3771,8 +3901,13 @@ class BindingSession {
3771
3901
  handleRemovedNode(node) {
3772
3902
  const known = this.knownBindingsByNode.get(node);
3773
3903
  if (typeof known !== "undefined") {
3774
- for (const binding of known.values())
3775
- this.disposeBinding(binding);
3904
+ if (known instanceof Map) {
3905
+ for (const binding of known.values())
3906
+ this.disposeBinding(binding);
3907
+ }
3908
+ else {
3909
+ this.disposeBinding(known);
3910
+ }
3776
3911
  }
3777
3912
  const tasks = this.deferredByNode.get(node);
3778
3913
  if (typeof tasks !== "undefined") {
@@ -3788,7 +3923,8 @@ class BindingSession {
3788
3923
  const known = this.knownBindingsByNode.get(node);
3789
3924
  if (typeof known === "undefined")
3790
3925
  return;
3791
- for (const binding of known.values()) {
3926
+ const bindings = known instanceof Map ? known.values() : [known];
3927
+ for (const binding of bindings) {
3792
3928
  const record = recordByBinding.get(binding);
3793
3929
  if (record?.phase === "active") {
3794
3930
  this.settleConnectedSnapshot(record);
@@ -3809,16 +3945,33 @@ class BindingSession {
3809
3945
  }
3810
3946
  }
3811
3947
  }
3948
+ /**
3949
+ * anchor の known 台帳を Map 形へ正規化して返す(remember のキー照合用)。
3950
+ * 単一値(プラン行 or 既存単独 binding)は実キーを引いて昇格する。
3951
+ */
3952
+ knownMapFor(anchor) {
3953
+ const current = this.knownBindingsByNode.get(anchor);
3954
+ if (current instanceof Map) {
3955
+ return current;
3956
+ }
3957
+ const map = new Map();
3958
+ if (typeof current !== "undefined") {
3959
+ let key = bindingKeyByBinding.get(current);
3960
+ if (typeof key === "undefined") {
3961
+ key = bindingKey(current);
3962
+ bindingKeyByBinding.set(current, key);
3963
+ }
3964
+ map.set(key, current);
3965
+ }
3966
+ this.knownBindingsByNode.set(anchor, map);
3967
+ return map;
3968
+ }
3812
3969
  remember(binding, options) {
3813
3970
  const anchor = binding.replaceNode;
3814
3971
  // detached fragment 上でも登録しておく(node 単位の台帳なので root 非依存)。
3815
3972
  // fragment 一括マウントで後から接続された行にも mutation 配送が届くようにする。
3816
3973
  addInterestedSession(anchor, this);
3817
- let known = this.knownBindingsByNode.get(anchor);
3818
- if (typeof known === "undefined") {
3819
- known = new Map();
3820
- this.knownBindingsByNode.set(anchor, known);
3821
- }
3974
+ const known = this.knownMapFor(anchor);
3822
3975
  let key = bindingKeyByBinding.get(binding);
3823
3976
  if (typeof key === "undefined") {
3824
3977
  key = bindingKey(binding);
@@ -3838,7 +3991,134 @@ class BindingSession {
3838
3991
  this.optionsByBinding.set(binding, { ...options });
3839
3992
  return binding;
3840
3993
  }
3841
- start(binding, options) {
3994
+ /**
3995
+ * RowPlan 経路の一括初期化(createContent 専用・docs/state-row-instantiation-redesign.md §3-2)。
3996
+ * プラン行の binding はこの呼び出しでのみ生成されるため remember(キー照合・
3997
+ * options マージ)を丸ごと省略し、policy/authority はテンプレート時に解決済みの
3998
+ * 値を焼き込む。options は行内共有の 1 オブジェクト(activate が
3999
+ * registerAddress を昇格するとき行内全 binding が同時に昇格する — 従来も
4000
+ * activate は全 binding を同順で昇格するため観測可能な差はない)。
4001
+ */
4002
+ initializeRow(plan, bindings) {
4003
+ this.rowPlan = plan;
4004
+ const rowOptions = { registerAddress: false, registerPathInfo: false, applyOnReconnect: false };
4005
+ const slots = plan.slots;
4006
+ for (let i = 0; i < bindings.length; i++) {
4007
+ const binding = bindings[i];
4008
+ const slot = slots[i];
4009
+ const anchor = binding.replaceNode;
4010
+ addInterestedSession(anchor, this);
4011
+ this.addKnownRowBinding(anchor, binding, i);
4012
+ this.optionsByBinding.set(binding, rowOptions);
4013
+ const record = {
4014
+ id: ++nextRecordId,
4015
+ info: binding,
4016
+ generation: ++nextGeneration,
4017
+ phase: "active",
4018
+ teardowns: null,
4019
+ session: this,
4020
+ anchor,
4021
+ options: rowOptions,
4022
+ address: null,
4023
+ patternPathInfo: null,
4024
+ patternListIndex: null,
4025
+ pendingDefinitions: 0,
4026
+ initialPolicy: slot.policy,
4027
+ resolvedAuthority: slot.authority,
4028
+ initialSettled: true,
4029
+ observationPending: false,
4030
+ eventSequence: 0,
4031
+ hasProducerValue: false,
4032
+ producerValue: undefined,
4033
+ eventAttached: false,
4034
+ twowayAttached: false,
4035
+ };
4036
+ recordByBinding.set(binding, record);
4037
+ this.records.add(record);
4038
+ if (slot.isEvent) {
4039
+ try {
4040
+ attachEventHandler(binding);
4041
+ }
4042
+ catch (error) {
4043
+ record.phase = "failed";
4044
+ this.runTeardowns(record);
4045
+ this.records.delete(record);
4046
+ throw error;
4047
+ }
4048
+ record.eventAttached = true;
4049
+ }
4050
+ // 非 event スロットはプラン適格性により双方向不能・radio/checkbox 不能・
4051
+ // token 配線不能が確定しているため attach 系を一切呼ばない
4052
+ }
4053
+ }
4054
+ /**
4055
+ * プラン行の活性化(activate の高速経路)。bindings は initializeRow と同一の
4056
+ * スロット整列配列(bindingsByContent がそのまま保持)である前提。
4057
+ * プラン行の record は policy/authority 解決済み・observable なし・
4058
+ * connect-snapshot なしが構造的に保証されているため、settleInitialRecord /
4059
+ * settleConnectedSnapshot の呼び出し自体を省略できる。
4060
+ * プール再利用(disposed/failed)では record オブジェクトを再利用し、
4061
+ * 世代だけ進めて listener attach とアドレス登録をやり直す(record 再割当なし)。
4062
+ */
4063
+ activatePlanRows(plan, bindings, knownRoot) {
4064
+ const slots = plan.slots;
4065
+ for (let i = 0; i < bindings.length; i++) {
4066
+ const binding = bindings[i];
4067
+ const record = recordByBinding.get(binding);
4068
+ if (typeof record === "undefined" || record.session !== this) {
4069
+ // この session の record を持たない binding(防御): 従来経路
4070
+ this.initialize([binding], { registerAddress: true, registerPathInfo: false, applyOnReconnect: false });
4071
+ continue;
4072
+ }
4073
+ record.options.registerAddress = true;
4074
+ if (record.phase === "disposed" || record.phase === "failed") {
4075
+ // pool 再利用: dispose 済み record を initializeRow と同じ内容で再充填
4076
+ const slot = slots[i];
4077
+ record.generation = ++nextGeneration;
4078
+ record.phase = "active";
4079
+ record.initialPolicy = slot.policy;
4080
+ record.resolvedAuthority = slot.authority;
4081
+ record.initialSettled = true;
4082
+ this.records.add(record);
4083
+ if (slot.isEvent) {
4084
+ try {
4085
+ attachEventHandler(binding);
4086
+ }
4087
+ catch (error) {
4088
+ record.phase = "failed";
4089
+ this.runTeardowns(record);
4090
+ this.records.delete(record);
4091
+ throw error;
4092
+ }
4093
+ record.eventAttached = true;
4094
+ }
4095
+ this.registerAddress(record, knownRoot);
4096
+ continue;
4097
+ }
4098
+ if (record.address === null && record.patternListIndex === null) {
4099
+ // 初回活性化
4100
+ this.registerAddress(record, knownRoot);
4101
+ }
4102
+ }
4103
+ }
4104
+ addKnownRowBinding(anchor, binding, slotIndex) {
4105
+ const current = this.knownBindingsByNode.get(anchor);
4106
+ if (typeof current === "undefined") {
4107
+ this.knownBindingsByNode.set(anchor, binding);
4108
+ return;
4109
+ }
4110
+ // 同一 anchor に複数スロット(複数エントリの data-wcs): Map へ昇格。
4111
+ // プラン行はキー照合されないため添字ベースの合成キーで一意性だけ担保する
4112
+ if (current instanceof Map) {
4113
+ current.set("@plan:" + slotIndex, binding);
4114
+ return;
4115
+ }
4116
+ const map = new Map();
4117
+ map.set("@plan:first", current);
4118
+ map.set("@plan:" + slotIndex, binding);
4119
+ this.knownBindingsByNode.set(anchor, map);
4120
+ }
4121
+ start(binding, options, knownRoot) {
3842
4122
  replaceToReplaceNode(binding);
3843
4123
  const recordOptions = this.optionsByBinding.get(binding) ?? { ...options };
3844
4124
  const record = {
@@ -3846,11 +4126,13 @@ class BindingSession {
3846
4126
  info: binding,
3847
4127
  generation: ++nextGeneration,
3848
4128
  phase: "discovered",
3849
- teardowns: new Set(),
4129
+ teardowns: null,
3850
4130
  session: this,
3851
4131
  anchor: binding.replaceNode,
3852
4132
  options: recordOptions,
3853
4133
  address: null,
4134
+ patternPathInfo: null,
4135
+ patternListIndex: null,
3854
4136
  pendingDefinitions: 0,
3855
4137
  initialPolicy: null,
3856
4138
  resolvedAuthority: null,
@@ -3859,15 +4141,20 @@ class BindingSession {
3859
4141
  eventSequence: 0,
3860
4142
  hasProducerValue: false,
3861
4143
  producerValue: undefined,
4144
+ eventAttached: false,
4145
+ twowayAttached: false,
3862
4146
  };
3863
4147
  recordByBinding.set(binding, record);
3864
4148
  this.records.add(record);
3865
- this.observe(record.anchor);
4149
+ // knownRoot が渡されたときは observe を省略する(null = detached fragment 上で
4150
+ // observableRootFor が必ず null、Node = activate 冒頭で owner 保証済み)
4151
+ if (typeof knownRoot === "undefined")
4152
+ this.observe(record.anchor);
3866
4153
  try {
3867
4154
  record.phase = "attaching";
3868
4155
  this.attachListeners(record);
3869
4156
  if (record.options.registerAddress)
3870
- this.registerAddress(record);
4157
+ this.registerAddress(record, knownRoot);
3871
4158
  if (record.pendingDefinitions === 0)
3872
4159
  record.phase = "active";
3873
4160
  }
@@ -3881,22 +4168,22 @@ class BindingSession {
3881
4168
  attachListeners(record) {
3882
4169
  const binding = record.info;
3883
4170
  if (attachEventHandler(binding)) {
3884
- record.teardowns.add(() => detachEventHandler(binding));
4171
+ record.eventAttached = true;
3885
4172
  return;
3886
4173
  }
3887
4174
  if (binding.propSegments[0] === "eventToken") {
3888
4175
  this.attachAfterDefinition(record, () => {
3889
4176
  if (attachEventTokenHandler(binding)) {
3890
- record.teardowns.add(() => detachEventTokenHandler(binding));
4177
+ addRecordTeardown(record, () => detachEventTokenHandler(binding));
3891
4178
  }
3892
4179
  });
3893
4180
  return;
3894
4181
  }
3895
4182
  if (attachRadioEventHandler(binding)) {
3896
- record.teardowns.add(() => detachRadioEventHandler(binding));
4183
+ addRecordTeardown(record, () => detachRadioEventHandler(binding));
3897
4184
  }
3898
4185
  if (attachCheckboxEventHandler(binding)) {
3899
- record.teardowns.add(() => detachCheckboxEventHandler(binding));
4186
+ addRecordTeardown(record, () => detachCheckboxEventHandler(binding));
3900
4187
  }
3901
4188
  this.attachAfterDefinition(record, () => {
3902
4189
  // directional initial sync の producer-value observer は twowayEventHandlerFunction
@@ -3918,10 +4205,10 @@ class BindingSession {
3918
4205
  record.hasProducerValue = true;
3919
4206
  record.producerValue = value;
3920
4207
  });
3921
- record.teardowns.add(removeObserver);
4208
+ addRecordTeardown(record, removeObserver);
3922
4209
  }
3923
4210
  attachTwowayEventHandler(binding);
3924
- record.teardowns.add(() => detachTwowayEventHandler(binding));
4211
+ record.twowayAttached = true;
3925
4212
  });
3926
4213
  }
3927
4214
  attachAfterDefinition(record, attach) {
@@ -3966,7 +4253,7 @@ class BindingSession {
3966
4253
  this.runTeardowns(record);
3967
4254
  this.records.delete(record);
3968
4255
  });
3969
- record.teardowns.add(cancel);
4256
+ addRecordTeardown(record, cancel);
3970
4257
  }
3971
4258
  settleInitialRecord(record) {
3972
4259
  if (!config.enableDirectionalInitialSync || record.initialSettled || !record.options.registerAddress)
@@ -4034,21 +4321,31 @@ class BindingSession {
4034
4321
  this.records.delete(record);
4035
4322
  }
4036
4323
  }
4037
- registerAddress(record) {
4038
- if (record.address !== null)
4324
+ registerAddress(record, knownRoot) {
4325
+ if (record.address !== null || record.patternListIndex !== null)
4039
4326
  return;
4040
4327
  const binding = record.info;
4041
- const address = getAbsoluteStateAddressByBinding(binding);
4042
- addBindingByAbsoluteStateAddress(address, binding);
4043
- record.address = address;
4044
- record.teardowns.add(() => {
4045
- if (record.address === null)
4046
- return;
4047
- removeBindingByAbsoluteStateAddress(record.address, binding);
4048
- record.address = null;
4049
- clearStateAddressByBindingInfo(binding);
4050
- clearAbsoluteStateAddressByBinding(binding);
4051
- });
4328
+ const listIndex = getListIndexByBindingInfo(binding);
4329
+ if (listIndex !== null) {
4330
+ // リスト行: (absolutePathInfo, listIndex) のパターン台帳に登録し、
4331
+ // AbsoluteStateAddress の intern(アドレス割当 + intern 用 WeakMap)を省略する
4332
+ const rootNode = resolveBindingRootNode(binding, knownRoot);
4333
+ const stateElement = getStateElementByName(rootNode, binding.stateName);
4334
+ if (stateElement === null) {
4335
+ raiseError(`State element with name "${binding.stateName}" not found for binding.`);
4336
+ }
4337
+ const absolutePathInfo = getAbsolutePathInfo(stateElement, binding.statePathInfo);
4338
+ addBindingByPattern(absolutePathInfo, listIndex, binding);
4339
+ record.patternPathInfo = absolutePathInfo;
4340
+ record.patternListIndex = listIndex;
4341
+ }
4342
+ else {
4343
+ const address = getAbsoluteStateAddressByBinding(binding, knownRoot);
4344
+ addBindingByAbsoluteStateAddress(address, binding);
4345
+ record.address = address;
4346
+ }
4347
+ // 台帳解除は runTeardowns が record.address / pattern フィールドから
4348
+ // データ駆動で行う(クロージャ不要)
4052
4349
  if (!record.options.registerPathInfo)
4053
4350
  return;
4054
4351
  const rootNode = binding.replaceNode.getRootNode();
@@ -4080,16 +4377,66 @@ class BindingSession {
4080
4377
  record.observationPending = false;
4081
4378
  decrementPendingObservation();
4082
4379
  }
4083
- const teardowns = Array.from(record.teardowns).reverse();
4084
- record.teardowns.clear();
4085
- for (const teardown of teardowns) {
4380
+ const binding = record.info;
4381
+ // データ駆動の後始末(従来はクロージャで積んでいた頻出3種)。実行順は従来の
4382
+ // 逆順実行と同じ: アドレス台帳解除(最後に積まれていた)→ 双方向 detach
4383
+ // 希少クロージャ群(逆順)→ イベント detach。各 detach は互いに独立した資源を
4384
+ // 対象とするため、この順序で意味論は変わらない。
4385
+ if (record.address !== null) {
4086
4386
  try {
4087
- teardown();
4387
+ removeBindingByAbsoluteStateAddress(record.address, binding);
4388
+ record.address = null;
4389
+ clearStateAddressByBindingInfo(binding);
4390
+ clearAbsoluteStateAddressByBinding(binding);
4088
4391
  }
4089
4392
  catch {
4090
4393
  // Cleanup is best-effort; one faulty resource must not retain the rest.
4091
4394
  }
4092
4395
  }
4396
+ else if (record.patternListIndex !== null) {
4397
+ try {
4398
+ removeBindingByPattern(record.patternPathInfo, record.patternListIndex, binding);
4399
+ record.patternPathInfo = null;
4400
+ record.patternListIndex = null;
4401
+ // 相対アドレス(getValue)と絶対アドレス(applyChangeToFor / updatedCallback 経由の
4402
+ // 遅延 intern)のメモは pattern 登録でも作られうるため対称にクリアする
4403
+ clearStateAddressByBindingInfo(binding);
4404
+ clearAbsoluteStateAddressByBinding(binding);
4405
+ }
4406
+ catch {
4407
+ // Cleanup is best-effort.
4408
+ }
4409
+ }
4410
+ if (record.twowayAttached) {
4411
+ record.twowayAttached = false;
4412
+ try {
4413
+ detachTwowayEventHandler(binding);
4414
+ }
4415
+ catch {
4416
+ // Cleanup is best-effort.
4417
+ }
4418
+ }
4419
+ if (record.teardowns !== null) {
4420
+ const teardowns = Array.from(record.teardowns).reverse();
4421
+ record.teardowns = null;
4422
+ for (const teardown of teardowns) {
4423
+ try {
4424
+ teardown();
4425
+ }
4426
+ catch {
4427
+ // Cleanup is best-effort; one faulty resource must not retain the rest.
4428
+ }
4429
+ }
4430
+ }
4431
+ if (record.eventAttached) {
4432
+ record.eventAttached = false;
4433
+ try {
4434
+ detachEventHandler(binding);
4435
+ }
4436
+ catch {
4437
+ // Cleanup is best-effort.
4438
+ }
4439
+ }
4093
4440
  }
4094
4441
  }
4095
4442
  function getOrCreateBindingSession(root) {
@@ -4770,8 +5117,11 @@ function activateContent(content, loopContext, context) {
4770
5117
  const session = getBindingSessionByContent(content);
4771
5118
  if (session !== null) {
4772
5119
  // createContent 側の initialize で remember 済みの同一 binding 配列なので、
4773
- // remember を再実行しない専用パスで活性化する(リスト行生成のホットパス)
4774
- session.activate(bindings);
5120
+ // remember を再実行しない専用パスで活性化する(リスト行生成のホットパス)。
5121
+ // context.rootNode は applyChangeFromBindings が確定済みの root(fragment
5122
+ // バッファ中は setRootNodeByFragment の対応先と同一)で、binding ごとの
5123
+ // getRootNode を省略できる
5124
+ session.activate(bindings, context.rootNode);
4775
5125
  }
4776
5126
  for (const binding of bindings) {
4777
5127
  if (session === null) {
@@ -4834,6 +5184,81 @@ function deleteContentByNode(node, content) {
4834
5184
  }
4835
5185
  }
4836
5186
 
5187
+ /**
5188
+ * rowPlan.ts — 行実体化プランのコンパイル(docs/state-row-instantiation-redesign.md §3-1)。
5189
+ *
5190
+ * テンプレート(fragmentInfo)を初回行生成時に一度だけ検査し、全スロットが
5191
+ * 「行不変の判定をテンプレート時に確定できる」種別のときだけプランを返す。
5192
+ * 1 スロットでも確定できなければ null(テンプレート丸ごと従来経路 = 部分適用しない。
5193
+ * 経路混在のデバッグ困難を避ける設計判断・同 §5)。
5194
+ *
5195
+ * プラン適格の条件(すべて満たすこと):
5196
+ * - bindingType が text / prop / event のみ(構造 for/if・radio/checkbox・spread は不適格)
5197
+ * - バインディング先ノードがカスタム要素でない(定義待ち・wcBindable 検証が不要)
5198
+ * - prop が command / eventToken 名前空間でない(token 配線 teardown が要るため)
5199
+ * - prop が双方向可能(isPossibleTwoWay)でない(connect-snapshot / observer 配線が要るため)
5200
+ * - initial-sync policy が観測不要(observable=false)かつ authority が "auto" でない
5201
+ * - text スロットは事前正規化済みの Text ノードである
5202
+ */
5203
+ function compileRowPlan(fragmentInfo) {
5204
+ const directional = config.enableDirectionalInitialSync;
5205
+ const slots = [];
5206
+ const nodeInfos = fragmentInfo.nodeInfos;
5207
+ for (let nodeIndex = 0; nodeIndex < nodeInfos.length; nodeIndex++) {
5208
+ const nodeInfo = nodeInfos[nodeIndex];
5209
+ const node = resolveNodePath(fragmentInfo.fragment, nodeInfo.nodePath);
5210
+ if (node === null) {
5211
+ return null;
5212
+ }
5213
+ for (const template of nodeInfo.parseBindTextResults) {
5214
+ const bindingType = template.bindingType;
5215
+ if (bindingType !== "text" && bindingType !== "prop" && bindingType !== "event") {
5216
+ return null;
5217
+ }
5218
+ // command.<name>(prop 扱い)と eventToken.<prop>(event 扱い)は token 配線の
5219
+ // teardown / attach 分岐が要るため不適格
5220
+ const namespace = template.propSegments[0];
5221
+ if (namespace === "command" || namespace === "eventToken") {
5222
+ return null;
5223
+ }
5224
+ if (bindingType === "text") {
5225
+ if (node.nodeType !== Node.TEXT_NODE) {
5226
+ return null;
5227
+ }
5228
+ }
5229
+ else if (getCustomElement(node) !== null) {
5230
+ return null;
5231
+ }
5232
+ if (bindingType === "prop" && isPossibleTwoWay(node, template.propName)) {
5233
+ return null;
5234
+ }
5235
+ let policy;
5236
+ try {
5237
+ // 判定はテンプレートのノードで行う(policy は node の宣言と行不変フィールドの
5238
+ // 純関数)。修飾子エラー等の throw は不適格として従来経路に倒し、従来経路が
5239
+ // 同じエラーを同じタイミング(初回行生成)で報告する。
5240
+ const probe = { ...template, node, replaceNode: node };
5241
+ policy = resolveInitialSyncPolicy(probe);
5242
+ }
5243
+ catch {
5244
+ return null;
5245
+ }
5246
+ if (policy.observable || policy.authority === "auto") {
5247
+ return null;
5248
+ }
5249
+ slots.push({
5250
+ nodeIndex,
5251
+ template,
5252
+ isEvent: bindingType === "event",
5253
+ isIndexBinding: template.statePathName in INDEX_BY_INDEX_NAME,
5254
+ policy,
5255
+ authority: policy.authority,
5256
+ });
5257
+ }
5258
+ }
5259
+ return { directional, slots };
5260
+ }
5261
+
4837
5262
  const recursiveBindingTypes = new Set(['if', 'elseif', 'else', 'for']);
4838
5263
  class Content {
4839
5264
  _content;
@@ -4949,6 +5374,49 @@ function createContentFromNodes(nodes) {
4949
5374
  content._mounted = true; // SSR で既にマウント済み
4950
5375
  return content;
4951
5376
  }
5377
+ /**
5378
+ * RowPlan 経路の実体化: clone → nodePath 解決 → スロットから薄い binding を複製 →
5379
+ * initializeRowBindings。パース再生(spread 展開・remember・キー文字列・options
5380
+ * オブジェクト・policy 再解決)を行ごとに繰り返さない
5381
+ * (docs/state-row-instantiation-redesign.md §3-1/§3-2)。
5382
+ */
5383
+ function createPlanContent(bindingInfo, fragmentInfo, plan) {
5384
+ const cloneFragment = document.importNode(fragmentInfo.fragment, true);
5385
+ const nodeInfos = fragmentInfo.nodeInfos;
5386
+ const nodes = new Array(nodeInfos.length);
5387
+ for (let i = 0; i < nodeInfos.length; i++) {
5388
+ const node = resolveNodePath(cloneFragment, nodeInfos[i].nodePath);
5389
+ if (node === null) {
5390
+ raiseError(`Node not found by path [${nodeInfos[i].nodePath.join(', ')}] in fragment.`);
5391
+ }
5392
+ // 再スキャン防止と初期化完了マークは従来経路と同じ台帳に載せる
5393
+ markNodeRegistered(node);
5394
+ resolveInitializedBinding(node);
5395
+ nodes[i] = node;
5396
+ }
5397
+ const slots = plan.slots;
5398
+ const bindings = new Array(slots.length);
5399
+ const indexBindings = [];
5400
+ for (let k = 0; k < slots.length; k++) {
5401
+ const slot = slots[k];
5402
+ const node = nodes[slot.nodeIndex];
5403
+ // text スロットは事前正規化済みの Text がそのまま replaceNode(従来経路の
5404
+ // getBindingInfos と同じ帰結)。prop/event は node === replaceNode
5405
+ const binding = { ...slot.template, node, replaceNode: node };
5406
+ bindings[k] = binding;
5407
+ if (slot.isIndexBinding) {
5408
+ indexBindings.push(binding);
5409
+ }
5410
+ }
5411
+ const session = initializeRowBindings(plan, bindings);
5412
+ const content = new Content(cloneFragment);
5413
+ setBindingSessionByContent(content, session);
5414
+ setBindingsByContent(content, bindings);
5415
+ setIndexBindingsByContent(content, indexBindings);
5416
+ setNodesByContent(content, nodes);
5417
+ setContentByNode(bindingInfo.node, content);
5418
+ return content;
5419
+ }
4952
5420
  function createContent(bindingInfo) {
4953
5421
  if (typeof bindingInfo.uuid === 'undefined' || bindingInfo.uuid === null) {
4954
5422
  raiseError(`BindingInfo.uuid is null.`);
@@ -4957,6 +5425,15 @@ function createContent(bindingInfo) {
4957
5425
  if (!fragmentInfo) {
4958
5426
  raiseError(`Fragment with UUID "${bindingInfo.uuid}" not found.`);
4959
5427
  }
5428
+ let plan = fragmentInfo.rowPlan;
5429
+ if (typeof plan === 'undefined' || (plan !== null && plan.directional !== config.enableDirectionalInitialSync)) {
5430
+ // 初回 or config(directional)が変わったときだけコンパイル。不適格は null を
5431
+ // キャッシュして以後は従来経路へ直行する
5432
+ plan = fragmentInfo.rowPlan = compileRowPlan(fragmentInfo);
5433
+ }
5434
+ if (plan !== null) {
5435
+ return createPlanContent(bindingInfo, fragmentInfo, plan);
5436
+ }
4960
5437
  const cloneFragment = document.importNode(fragmentInfo.fragment, true);
4961
5438
  const initialInfo = initializeBindingsByFragment(cloneFragment, fragmentInfo.nodeInfos);
4962
5439
  const content = new Content(cloneFragment);
@@ -5960,16 +6437,29 @@ function initializeBindings(root, parentLoopContext) {
5960
6437
  function initializeBindingsByFragment(root, nodeInfos) {
5961
6438
  const [subscriberNodes, allBindings] = collectNodesAndBindingInfosByFragment(root, nodeInfos);
5962
6439
  const session = new BindingSession();
6440
+ // knownRoot=null: detached fragment 上の初期化。observableRootFor が必ず null を
6441
+ // 返す(observe は no-op)ため、binding ごとの getRootNode を省略する
5963
6442
  const initialized = session.initialize(allBindings, {
5964
6443
  registerAddress: false,
5965
6444
  applyOnReconnect: false,
5966
- });
6445
+ }, null);
5967
6446
  return {
5968
6447
  nodes: subscriberNodes,
5969
6448
  bindingInfos: initialized,
5970
6449
  bindingSession: session,
5971
6450
  };
5972
6451
  }
6452
+ /**
6453
+ * RowPlan 経路の行初期化(createContent 専用)。remember / spread 展開 /
6454
+ * shouldApplyState フィルタを経ず、プランのスロットから直接 record を構築する。
6455
+ * 返す session は従来経路と同じ活性化(activate)・破棄(dispose/wholesale)
6456
+ * インターフェースを持つ。
6457
+ */
6458
+ function initializeRowBindings(plan, bindings) {
6459
+ const session = new BindingSession();
6460
+ session.initializeRow(plan, bindings);
6461
+ return session;
6462
+ }
5973
6463
 
5974
6464
  const MUSTACHE_REGEX = /\{\{\s*(.+?)\s*\}\}/g;
5975
6465
  const SKIP_TAGS = new Set(["SCRIPT", "STYLE"]);
@@ -6383,7 +6873,7 @@ async function buildBindings(root) {
6383
6873
  }
6384
6874
  }
6385
6875
 
6386
- var version = "1.21.5";
6876
+ var version = "1.21.7";
6387
6877
  var pkg = {
6388
6878
  version: version};
6389
6879
 
@@ -7291,19 +7781,29 @@ class Updater {
7291
7781
  continue;
7292
7782
  }
7293
7783
  // peek: バインディングの無いアドレス(リスト置換で enqueue される中間
7294
- // アドレス等)に空 Set を生成・蓄積しない
7295
- const bindings = peekBindingSetByAbsoluteStateAddress(absoluteAddress);
7296
- if (bindings === undefined) {
7784
+ // アドレス等)に空エントリを生成・蓄積しない。エントリは単一 binding
7785
+ // (通常ケース)か Set(同一アドレスに 2 本以上)のどちらか。
7786
+ // 従来台帳 パターン台帳(リスト行)の順で引く。
7787
+ const entry = peekBindingsForAddress(absoluteAddress);
7788
+ if (entry === undefined) {
7297
7789
  continue;
7298
7790
  }
7299
- for (const binding of bindings) {
7300
- if (binding.replaceNode.isConnected === false) {
7301
- // 切断されているバインディングは無視
7302
- continue;
7791
+ if (entry instanceof Set) {
7792
+ for (const binding of entry) {
7793
+ if (binding.replaceNode.isConnected === false) {
7794
+ // 切断されているバインディングは無視
7795
+ continue;
7796
+ }
7797
+ processBindings.push(binding);
7798
+ if (context !== null) {
7799
+ propagationContextByBinding.set(binding, context);
7800
+ }
7303
7801
  }
7304
- processBindings.push(binding);
7802
+ }
7803
+ else if (entry.replaceNode.isConnected !== false) {
7804
+ processBindings.push(entry);
7305
7805
  if (context !== null) {
7306
- propagationContextByBinding.set(binding, context);
7806
+ propagationContextByBinding.set(entry, context);
7307
7807
  }
7308
7808
  }
7309
7809
  }
@@ -9010,6 +9510,10 @@ function dirtyCacheEntryByAbsoluteStateAddress(address) {
9010
9510
  }
9011
9511
 
9012
9512
  function checkDependency(handler, address) {
9513
+ // $untrackDependency スコープ中/setter 実行中は依存を張らない
9514
+ if (handler.untracking) {
9515
+ return;
9516
+ }
9013
9517
  // 動的依存関係の登録
9014
9518
  if (handler.addressStackLength > 0) {
9015
9519
  const lastAddress = handler.lastAddressStack;
@@ -9554,12 +10058,18 @@ function _setByAddress(target, address, absAddress, value, receiver, handler) {
9554
10058
  try {
9555
10059
  if (address.pathInfo.path in target) {
9556
10060
  if (handler.stateElement.setterPaths.has(address.pathInfo.path)) {
9557
- // setterの中で参照の可能性があるので、addressをプッシュする
10061
+ // setterの中で参照の可能性があるので、addressをプッシュする。
10062
+ // setter は命令的な代入であって派生(getter)ではないため、実行中の
10063
+ // 読み取り(同値ガードの旧値読み・$1 参照等)で依存を張らない。
10064
+ // アクセサペア(get/set 同名パス)では、抑止しないと setter 内の内部
10065
+ // 書き込みの同値ガード読みが「getter の依存」として誤登録される。
9558
10066
  handler.pushAddress(address);
10067
+ handler.beginUntrack();
9559
10068
  try {
9560
10069
  return Reflect.set(target, address.pathInfo.path, value, receiver);
9561
10070
  }
9562
10071
  finally {
10072
+ handler.endUntrack();
9563
10073
  handler.popAddress();
9564
10074
  }
9565
10075
  }
@@ -9996,6 +10506,35 @@ function trackDependency(_target, _prop, _receiver, handler) {
9996
10506
  };
9997
10507
  }
9998
10508
 
10509
+ /**
10510
+ * untrackDependency.ts
10511
+ *
10512
+ * StateClass の API として、コールバック実行中の依存追跡を抑止する関数
10513
+ * ($untrackDependency)の実装です。$trackDependency(明示的な依存登録)と
10514
+ * 対称の「明示的な依存抑止」API。
10515
+ *
10516
+ * 主な役割:
10517
+ * - fn 実行中、checkDependency の動的依存登録と $1 インデックス依存の記録を抑止
10518
+ * - fn の戻り値をそのまま返す(値の読み取り自体は通常どおり行われる)
10519
+ *
10520
+ * 設計ポイント:
10521
+ * - スコープはハンドラ単位のカウンタ(ネスト可)で管理し、finally で必ず復元する
10522
+ * - 典型例: リスト行 getter が「行の外の単一値」を読みたいが、その値の変更で
10523
+ * 全行を再評価させたくない場合(選択インデックス等)。書き手側が該当行へ
10524
+ * 直接書き込むことで、必要な行だけが更新される
10525
+ */
10526
+ function untrackDependency(_target, _prop, _receiver, handler) {
10527
+ return (fn) => {
10528
+ handler.beginUntrack();
10529
+ try {
10530
+ return fn();
10531
+ }
10532
+ finally {
10533
+ handler.endUntrack();
10534
+ }
10535
+ };
10536
+ }
10537
+
9999
10538
  /**
10000
10539
  * updatedCallback.ts
10001
10540
  *
@@ -10137,8 +10676,9 @@ function get(target, prop, receiver, handler) {
10137
10676
  // getter 評価中のインデックス読み取りを記録する。位置だけが変わった行
10138
10677
  // (listDiff.changeIndexSet)は index 以外の入力が不変なので、walkDependency の
10139
10678
  // 静的子展開を「インデックスを読んだ getter の subtree」に限定できる。
10679
+ // $untrackDependency スコープ中/setter 実行中は記録しない。
10140
10680
  const lastInfo = lastAddress?.pathInfo;
10141
- if (lastInfo && handler.stateElement?.getterPaths.has(lastInfo.path)) {
10681
+ if (lastInfo && !handler.untracking && handler.stateElement?.getterPaths.has(lastInfo.path)) {
10142
10682
  handler.stateElement.addIndexDependentGetterPath?.(lastInfo.path);
10143
10683
  }
10144
10684
  const listIndex = lastAddress?.listIndex;
@@ -10170,6 +10710,11 @@ function get(target, prop, receiver, handler) {
10170
10710
  return trackDependency(target, prop, receiver, handler)(path);
10171
10711
  };
10172
10712
  }
10713
+ case "$untrackDependency": {
10714
+ return (fn) => {
10715
+ return untrackDependency(target, prop, receiver, handler)(fn);
10716
+ };
10717
+ }
10173
10718
  case STATE_COMMAND_NAMESPACE_NAME: {
10174
10719
  return getCommandNamespace(handler.stateElement);
10175
10720
  }
@@ -10294,6 +10839,7 @@ class StateHandler {
10294
10839
  _addressStackIndex = -1;
10295
10840
  _loopContext;
10296
10841
  _mutability;
10842
+ _untrackDepth = 0;
10297
10843
  constructor(rootNode, stateName, mutability) {
10298
10844
  this._stateName = stateName;
10299
10845
  const stateElement = getStateElementByName(rootNode, this._stateName);
@@ -10350,6 +10896,15 @@ class StateHandler {
10350
10896
  clearLoopContext() {
10351
10897
  this._loopContext = undefined;
10352
10898
  }
10899
+ get untracking() {
10900
+ return this._untrackDepth > 0;
10901
+ }
10902
+ beginUntrack() {
10903
+ this._untrackDepth++;
10904
+ }
10905
+ endUntrack() {
10906
+ this._untrackDepth--;
10907
+ }
10353
10908
  get(target, prop, receiver) {
10354
10909
  return get(target, prop, receiver, this);
10355
10910
  }