@wcstack/state 1.21.5 → 1.21.6

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) {
@@ -3736,7 +3857,7 @@ class BindingSession {
3736
3857
  destroyRecords() {
3737
3858
  for (const record of this.records) {
3738
3859
  record.phase = "disposed";
3739
- record.teardowns.clear();
3860
+ record.teardowns = null;
3740
3861
  }
3741
3862
  this.records.clear();
3742
3863
  }
@@ -3771,8 +3892,13 @@ class BindingSession {
3771
3892
  handleRemovedNode(node) {
3772
3893
  const known = this.knownBindingsByNode.get(node);
3773
3894
  if (typeof known !== "undefined") {
3774
- for (const binding of known.values())
3775
- this.disposeBinding(binding);
3895
+ if (known instanceof Map) {
3896
+ for (const binding of known.values())
3897
+ this.disposeBinding(binding);
3898
+ }
3899
+ else {
3900
+ this.disposeBinding(known);
3901
+ }
3776
3902
  }
3777
3903
  const tasks = this.deferredByNode.get(node);
3778
3904
  if (typeof tasks !== "undefined") {
@@ -3788,7 +3914,8 @@ class BindingSession {
3788
3914
  const known = this.knownBindingsByNode.get(node);
3789
3915
  if (typeof known === "undefined")
3790
3916
  return;
3791
- for (const binding of known.values()) {
3917
+ const bindings = known instanceof Map ? known.values() : [known];
3918
+ for (const binding of bindings) {
3792
3919
  const record = recordByBinding.get(binding);
3793
3920
  if (record?.phase === "active") {
3794
3921
  this.settleConnectedSnapshot(record);
@@ -3809,16 +3936,33 @@ class BindingSession {
3809
3936
  }
3810
3937
  }
3811
3938
  }
3939
+ /**
3940
+ * anchor の known 台帳を Map 形へ正規化して返す(remember のキー照合用)。
3941
+ * 単一値(プラン行 or 既存単独 binding)は実キーを引いて昇格する。
3942
+ */
3943
+ knownMapFor(anchor) {
3944
+ const current = this.knownBindingsByNode.get(anchor);
3945
+ if (current instanceof Map) {
3946
+ return current;
3947
+ }
3948
+ const map = new Map();
3949
+ if (typeof current !== "undefined") {
3950
+ let key = bindingKeyByBinding.get(current);
3951
+ if (typeof key === "undefined") {
3952
+ key = bindingKey(current);
3953
+ bindingKeyByBinding.set(current, key);
3954
+ }
3955
+ map.set(key, current);
3956
+ }
3957
+ this.knownBindingsByNode.set(anchor, map);
3958
+ return map;
3959
+ }
3812
3960
  remember(binding, options) {
3813
3961
  const anchor = binding.replaceNode;
3814
3962
  // detached fragment 上でも登録しておく(node 単位の台帳なので root 非依存)。
3815
3963
  // fragment 一括マウントで後から接続された行にも mutation 配送が届くようにする。
3816
3964
  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
- }
3965
+ const known = this.knownMapFor(anchor);
3822
3966
  let key = bindingKeyByBinding.get(binding);
3823
3967
  if (typeof key === "undefined") {
3824
3968
  key = bindingKey(binding);
@@ -3838,7 +3982,134 @@ class BindingSession {
3838
3982
  this.optionsByBinding.set(binding, { ...options });
3839
3983
  return binding;
3840
3984
  }
3841
- start(binding, options) {
3985
+ /**
3986
+ * RowPlan 経路の一括初期化(createContent 専用・docs/state-row-instantiation-redesign.md §3-2)。
3987
+ * プラン行の binding はこの呼び出しでのみ生成されるため remember(キー照合・
3988
+ * options マージ)を丸ごと省略し、policy/authority はテンプレート時に解決済みの
3989
+ * 値を焼き込む。options は行内共有の 1 オブジェクト(activate が
3990
+ * registerAddress を昇格するとき行内全 binding が同時に昇格する — 従来も
3991
+ * activate は全 binding を同順で昇格するため観測可能な差はない)。
3992
+ */
3993
+ initializeRow(plan, bindings) {
3994
+ this.rowPlan = plan;
3995
+ const rowOptions = { registerAddress: false, registerPathInfo: false, applyOnReconnect: false };
3996
+ const slots = plan.slots;
3997
+ for (let i = 0; i < bindings.length; i++) {
3998
+ const binding = bindings[i];
3999
+ const slot = slots[i];
4000
+ const anchor = binding.replaceNode;
4001
+ addInterestedSession(anchor, this);
4002
+ this.addKnownRowBinding(anchor, binding, i);
4003
+ this.optionsByBinding.set(binding, rowOptions);
4004
+ const record = {
4005
+ id: ++nextRecordId,
4006
+ info: binding,
4007
+ generation: ++nextGeneration,
4008
+ phase: "active",
4009
+ teardowns: null,
4010
+ session: this,
4011
+ anchor,
4012
+ options: rowOptions,
4013
+ address: null,
4014
+ patternPathInfo: null,
4015
+ patternListIndex: null,
4016
+ pendingDefinitions: 0,
4017
+ initialPolicy: slot.policy,
4018
+ resolvedAuthority: slot.authority,
4019
+ initialSettled: true,
4020
+ observationPending: false,
4021
+ eventSequence: 0,
4022
+ hasProducerValue: false,
4023
+ producerValue: undefined,
4024
+ eventAttached: false,
4025
+ twowayAttached: false,
4026
+ };
4027
+ recordByBinding.set(binding, record);
4028
+ this.records.add(record);
4029
+ if (slot.isEvent) {
4030
+ try {
4031
+ attachEventHandler(binding);
4032
+ }
4033
+ catch (error) {
4034
+ record.phase = "failed";
4035
+ this.runTeardowns(record);
4036
+ this.records.delete(record);
4037
+ throw error;
4038
+ }
4039
+ record.eventAttached = true;
4040
+ }
4041
+ // 非 event スロットはプラン適格性により双方向不能・radio/checkbox 不能・
4042
+ // token 配線不能が確定しているため attach 系を一切呼ばない
4043
+ }
4044
+ }
4045
+ /**
4046
+ * プラン行の活性化(activate の高速経路)。bindings は initializeRow と同一の
4047
+ * スロット整列配列(bindingsByContent がそのまま保持)である前提。
4048
+ * プラン行の record は policy/authority 解決済み・observable なし・
4049
+ * connect-snapshot なしが構造的に保証されているため、settleInitialRecord /
4050
+ * settleConnectedSnapshot の呼び出し自体を省略できる。
4051
+ * プール再利用(disposed/failed)では record オブジェクトを再利用し、
4052
+ * 世代だけ進めて listener attach とアドレス登録をやり直す(record 再割当なし)。
4053
+ */
4054
+ activatePlanRows(plan, bindings, knownRoot) {
4055
+ const slots = plan.slots;
4056
+ for (let i = 0; i < bindings.length; i++) {
4057
+ const binding = bindings[i];
4058
+ const record = recordByBinding.get(binding);
4059
+ if (typeof record === "undefined" || record.session !== this) {
4060
+ // この session の record を持たない binding(防御): 従来経路
4061
+ this.initialize([binding], { registerAddress: true, registerPathInfo: false, applyOnReconnect: false });
4062
+ continue;
4063
+ }
4064
+ record.options.registerAddress = true;
4065
+ if (record.phase === "disposed" || record.phase === "failed") {
4066
+ // pool 再利用: dispose 済み record を initializeRow と同じ内容で再充填
4067
+ const slot = slots[i];
4068
+ record.generation = ++nextGeneration;
4069
+ record.phase = "active";
4070
+ record.initialPolicy = slot.policy;
4071
+ record.resolvedAuthority = slot.authority;
4072
+ record.initialSettled = true;
4073
+ this.records.add(record);
4074
+ if (slot.isEvent) {
4075
+ try {
4076
+ attachEventHandler(binding);
4077
+ }
4078
+ catch (error) {
4079
+ record.phase = "failed";
4080
+ this.runTeardowns(record);
4081
+ this.records.delete(record);
4082
+ throw error;
4083
+ }
4084
+ record.eventAttached = true;
4085
+ }
4086
+ this.registerAddress(record, knownRoot);
4087
+ continue;
4088
+ }
4089
+ if (record.address === null && record.patternListIndex === null) {
4090
+ // 初回活性化
4091
+ this.registerAddress(record, knownRoot);
4092
+ }
4093
+ }
4094
+ }
4095
+ addKnownRowBinding(anchor, binding, slotIndex) {
4096
+ const current = this.knownBindingsByNode.get(anchor);
4097
+ if (typeof current === "undefined") {
4098
+ this.knownBindingsByNode.set(anchor, binding);
4099
+ return;
4100
+ }
4101
+ // 同一 anchor に複数スロット(複数エントリの data-wcs): Map へ昇格。
4102
+ // プラン行はキー照合されないため添字ベースの合成キーで一意性だけ担保する
4103
+ if (current instanceof Map) {
4104
+ current.set("@plan:" + slotIndex, binding);
4105
+ return;
4106
+ }
4107
+ const map = new Map();
4108
+ map.set("@plan:first", current);
4109
+ map.set("@plan:" + slotIndex, binding);
4110
+ this.knownBindingsByNode.set(anchor, map);
4111
+ }
4112
+ start(binding, options, knownRoot) {
3842
4113
  replaceToReplaceNode(binding);
3843
4114
  const recordOptions = this.optionsByBinding.get(binding) ?? { ...options };
3844
4115
  const record = {
@@ -3846,11 +4117,13 @@ class BindingSession {
3846
4117
  info: binding,
3847
4118
  generation: ++nextGeneration,
3848
4119
  phase: "discovered",
3849
- teardowns: new Set(),
4120
+ teardowns: null,
3850
4121
  session: this,
3851
4122
  anchor: binding.replaceNode,
3852
4123
  options: recordOptions,
3853
4124
  address: null,
4125
+ patternPathInfo: null,
4126
+ patternListIndex: null,
3854
4127
  pendingDefinitions: 0,
3855
4128
  initialPolicy: null,
3856
4129
  resolvedAuthority: null,
@@ -3859,15 +4132,20 @@ class BindingSession {
3859
4132
  eventSequence: 0,
3860
4133
  hasProducerValue: false,
3861
4134
  producerValue: undefined,
4135
+ eventAttached: false,
4136
+ twowayAttached: false,
3862
4137
  };
3863
4138
  recordByBinding.set(binding, record);
3864
4139
  this.records.add(record);
3865
- this.observe(record.anchor);
4140
+ // knownRoot が渡されたときは observe を省略する(null = detached fragment 上で
4141
+ // observableRootFor が必ず null、Node = activate 冒頭で owner 保証済み)
4142
+ if (typeof knownRoot === "undefined")
4143
+ this.observe(record.anchor);
3866
4144
  try {
3867
4145
  record.phase = "attaching";
3868
4146
  this.attachListeners(record);
3869
4147
  if (record.options.registerAddress)
3870
- this.registerAddress(record);
4148
+ this.registerAddress(record, knownRoot);
3871
4149
  if (record.pendingDefinitions === 0)
3872
4150
  record.phase = "active";
3873
4151
  }
@@ -3881,22 +4159,22 @@ class BindingSession {
3881
4159
  attachListeners(record) {
3882
4160
  const binding = record.info;
3883
4161
  if (attachEventHandler(binding)) {
3884
- record.teardowns.add(() => detachEventHandler(binding));
4162
+ record.eventAttached = true;
3885
4163
  return;
3886
4164
  }
3887
4165
  if (binding.propSegments[0] === "eventToken") {
3888
4166
  this.attachAfterDefinition(record, () => {
3889
4167
  if (attachEventTokenHandler(binding)) {
3890
- record.teardowns.add(() => detachEventTokenHandler(binding));
4168
+ addRecordTeardown(record, () => detachEventTokenHandler(binding));
3891
4169
  }
3892
4170
  });
3893
4171
  return;
3894
4172
  }
3895
4173
  if (attachRadioEventHandler(binding)) {
3896
- record.teardowns.add(() => detachRadioEventHandler(binding));
4174
+ addRecordTeardown(record, () => detachRadioEventHandler(binding));
3897
4175
  }
3898
4176
  if (attachCheckboxEventHandler(binding)) {
3899
- record.teardowns.add(() => detachCheckboxEventHandler(binding));
4177
+ addRecordTeardown(record, () => detachCheckboxEventHandler(binding));
3900
4178
  }
3901
4179
  this.attachAfterDefinition(record, () => {
3902
4180
  // directional initial sync の producer-value observer は twowayEventHandlerFunction
@@ -3918,10 +4196,10 @@ class BindingSession {
3918
4196
  record.hasProducerValue = true;
3919
4197
  record.producerValue = value;
3920
4198
  });
3921
- record.teardowns.add(removeObserver);
4199
+ addRecordTeardown(record, removeObserver);
3922
4200
  }
3923
4201
  attachTwowayEventHandler(binding);
3924
- record.teardowns.add(() => detachTwowayEventHandler(binding));
4202
+ record.twowayAttached = true;
3925
4203
  });
3926
4204
  }
3927
4205
  attachAfterDefinition(record, attach) {
@@ -3966,7 +4244,7 @@ class BindingSession {
3966
4244
  this.runTeardowns(record);
3967
4245
  this.records.delete(record);
3968
4246
  });
3969
- record.teardowns.add(cancel);
4247
+ addRecordTeardown(record, cancel);
3970
4248
  }
3971
4249
  settleInitialRecord(record) {
3972
4250
  if (!config.enableDirectionalInitialSync || record.initialSettled || !record.options.registerAddress)
@@ -4034,21 +4312,31 @@ class BindingSession {
4034
4312
  this.records.delete(record);
4035
4313
  }
4036
4314
  }
4037
- registerAddress(record) {
4038
- if (record.address !== null)
4315
+ registerAddress(record, knownRoot) {
4316
+ if (record.address !== null || record.patternListIndex !== null)
4039
4317
  return;
4040
4318
  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
- });
4319
+ const listIndex = getListIndexByBindingInfo(binding);
4320
+ if (listIndex !== null) {
4321
+ // リスト行: (absolutePathInfo, listIndex) のパターン台帳に登録し、
4322
+ // AbsoluteStateAddress の intern(アドレス割当 + intern 用 WeakMap)を省略する
4323
+ const rootNode = resolveBindingRootNode(binding, knownRoot);
4324
+ const stateElement = getStateElementByName(rootNode, binding.stateName);
4325
+ if (stateElement === null) {
4326
+ raiseError(`State element with name "${binding.stateName}" not found for binding.`);
4327
+ }
4328
+ const absolutePathInfo = getAbsolutePathInfo(stateElement, binding.statePathInfo);
4329
+ addBindingByPattern(absolutePathInfo, listIndex, binding);
4330
+ record.patternPathInfo = absolutePathInfo;
4331
+ record.patternListIndex = listIndex;
4332
+ }
4333
+ else {
4334
+ const address = getAbsoluteStateAddressByBinding(binding, knownRoot);
4335
+ addBindingByAbsoluteStateAddress(address, binding);
4336
+ record.address = address;
4337
+ }
4338
+ // 台帳解除は runTeardowns が record.address / pattern フィールドから
4339
+ // データ駆動で行う(クロージャ不要)
4052
4340
  if (!record.options.registerPathInfo)
4053
4341
  return;
4054
4342
  const rootNode = binding.replaceNode.getRootNode();
@@ -4080,16 +4368,66 @@ class BindingSession {
4080
4368
  record.observationPending = false;
4081
4369
  decrementPendingObservation();
4082
4370
  }
4083
- const teardowns = Array.from(record.teardowns).reverse();
4084
- record.teardowns.clear();
4085
- for (const teardown of teardowns) {
4371
+ const binding = record.info;
4372
+ // データ駆動の後始末(従来はクロージャで積んでいた頻出3種)。実行順は従来の
4373
+ // 逆順実行と同じ: アドレス台帳解除(最後に積まれていた)→ 双方向 detach
4374
+ // 希少クロージャ群(逆順)→ イベント detach。各 detach は互いに独立した資源を
4375
+ // 対象とするため、この順序で意味論は変わらない。
4376
+ if (record.address !== null) {
4086
4377
  try {
4087
- teardown();
4378
+ removeBindingByAbsoluteStateAddress(record.address, binding);
4379
+ record.address = null;
4380
+ clearStateAddressByBindingInfo(binding);
4381
+ clearAbsoluteStateAddressByBinding(binding);
4088
4382
  }
4089
4383
  catch {
4090
4384
  // Cleanup is best-effort; one faulty resource must not retain the rest.
4091
4385
  }
4092
4386
  }
4387
+ else if (record.patternListIndex !== null) {
4388
+ try {
4389
+ removeBindingByPattern(record.patternPathInfo, record.patternListIndex, binding);
4390
+ record.patternPathInfo = null;
4391
+ record.patternListIndex = null;
4392
+ // 相対アドレス(getValue)と絶対アドレス(applyChangeToFor / updatedCallback 経由の
4393
+ // 遅延 intern)のメモは pattern 登録でも作られうるため対称にクリアする
4394
+ clearStateAddressByBindingInfo(binding);
4395
+ clearAbsoluteStateAddressByBinding(binding);
4396
+ }
4397
+ catch {
4398
+ // Cleanup is best-effort.
4399
+ }
4400
+ }
4401
+ if (record.twowayAttached) {
4402
+ record.twowayAttached = false;
4403
+ try {
4404
+ detachTwowayEventHandler(binding);
4405
+ }
4406
+ catch {
4407
+ // Cleanup is best-effort.
4408
+ }
4409
+ }
4410
+ if (record.teardowns !== null) {
4411
+ const teardowns = Array.from(record.teardowns).reverse();
4412
+ record.teardowns = null;
4413
+ for (const teardown of teardowns) {
4414
+ try {
4415
+ teardown();
4416
+ }
4417
+ catch {
4418
+ // Cleanup is best-effort; one faulty resource must not retain the rest.
4419
+ }
4420
+ }
4421
+ }
4422
+ if (record.eventAttached) {
4423
+ record.eventAttached = false;
4424
+ try {
4425
+ detachEventHandler(binding);
4426
+ }
4427
+ catch {
4428
+ // Cleanup is best-effort.
4429
+ }
4430
+ }
4093
4431
  }
4094
4432
  }
4095
4433
  function getOrCreateBindingSession(root) {
@@ -4770,8 +5108,11 @@ function activateContent(content, loopContext, context) {
4770
5108
  const session = getBindingSessionByContent(content);
4771
5109
  if (session !== null) {
4772
5110
  // createContent 側の initialize で remember 済みの同一 binding 配列なので、
4773
- // remember を再実行しない専用パスで活性化する(リスト行生成のホットパス)
4774
- session.activate(bindings);
5111
+ // remember を再実行しない専用パスで活性化する(リスト行生成のホットパス)。
5112
+ // context.rootNode は applyChangeFromBindings が確定済みの root(fragment
5113
+ // バッファ中は setRootNodeByFragment の対応先と同一)で、binding ごとの
5114
+ // getRootNode を省略できる
5115
+ session.activate(bindings, context.rootNode);
4775
5116
  }
4776
5117
  for (const binding of bindings) {
4777
5118
  if (session === null) {
@@ -4834,6 +5175,81 @@ function deleteContentByNode(node, content) {
4834
5175
  }
4835
5176
  }
4836
5177
 
5178
+ /**
5179
+ * rowPlan.ts — 行実体化プランのコンパイル(docs/state-row-instantiation-redesign.md §3-1)。
5180
+ *
5181
+ * テンプレート(fragmentInfo)を初回行生成時に一度だけ検査し、全スロットが
5182
+ * 「行不変の判定をテンプレート時に確定できる」種別のときだけプランを返す。
5183
+ * 1 スロットでも確定できなければ null(テンプレート丸ごと従来経路 = 部分適用しない。
5184
+ * 経路混在のデバッグ困難を避ける設計判断・同 §5)。
5185
+ *
5186
+ * プラン適格の条件(すべて満たすこと):
5187
+ * - bindingType が text / prop / event のみ(構造 for/if・radio/checkbox・spread は不適格)
5188
+ * - バインディング先ノードがカスタム要素でない(定義待ち・wcBindable 検証が不要)
5189
+ * - prop が command / eventToken 名前空間でない(token 配線 teardown が要るため)
5190
+ * - prop が双方向可能(isPossibleTwoWay)でない(connect-snapshot / observer 配線が要るため)
5191
+ * - initial-sync policy が観測不要(observable=false)かつ authority が "auto" でない
5192
+ * - text スロットは事前正規化済みの Text ノードである
5193
+ */
5194
+ function compileRowPlan(fragmentInfo) {
5195
+ const directional = config.enableDirectionalInitialSync;
5196
+ const slots = [];
5197
+ const nodeInfos = fragmentInfo.nodeInfos;
5198
+ for (let nodeIndex = 0; nodeIndex < nodeInfos.length; nodeIndex++) {
5199
+ const nodeInfo = nodeInfos[nodeIndex];
5200
+ const node = resolveNodePath(fragmentInfo.fragment, nodeInfo.nodePath);
5201
+ if (node === null) {
5202
+ return null;
5203
+ }
5204
+ for (const template of nodeInfo.parseBindTextResults) {
5205
+ const bindingType = template.bindingType;
5206
+ if (bindingType !== "text" && bindingType !== "prop" && bindingType !== "event") {
5207
+ return null;
5208
+ }
5209
+ // command.<name>(prop 扱い)と eventToken.<prop>(event 扱い)は token 配線の
5210
+ // teardown / attach 分岐が要るため不適格
5211
+ const namespace = template.propSegments[0];
5212
+ if (namespace === "command" || namespace === "eventToken") {
5213
+ return null;
5214
+ }
5215
+ if (bindingType === "text") {
5216
+ if (node.nodeType !== Node.TEXT_NODE) {
5217
+ return null;
5218
+ }
5219
+ }
5220
+ else if (getCustomElement(node) !== null) {
5221
+ return null;
5222
+ }
5223
+ if (bindingType === "prop" && isPossibleTwoWay(node, template.propName)) {
5224
+ return null;
5225
+ }
5226
+ let policy;
5227
+ try {
5228
+ // 判定はテンプレートのノードで行う(policy は node の宣言と行不変フィールドの
5229
+ // 純関数)。修飾子エラー等の throw は不適格として従来経路に倒し、従来経路が
5230
+ // 同じエラーを同じタイミング(初回行生成)で報告する。
5231
+ const probe = { ...template, node, replaceNode: node };
5232
+ policy = resolveInitialSyncPolicy(probe);
5233
+ }
5234
+ catch {
5235
+ return null;
5236
+ }
5237
+ if (policy.observable || policy.authority === "auto") {
5238
+ return null;
5239
+ }
5240
+ slots.push({
5241
+ nodeIndex,
5242
+ template,
5243
+ isEvent: bindingType === "event",
5244
+ isIndexBinding: template.statePathName in INDEX_BY_INDEX_NAME,
5245
+ policy,
5246
+ authority: policy.authority,
5247
+ });
5248
+ }
5249
+ }
5250
+ return { directional, slots };
5251
+ }
5252
+
4837
5253
  const recursiveBindingTypes = new Set(['if', 'elseif', 'else', 'for']);
4838
5254
  class Content {
4839
5255
  _content;
@@ -4949,6 +5365,49 @@ function createContentFromNodes(nodes) {
4949
5365
  content._mounted = true; // SSR で既にマウント済み
4950
5366
  return content;
4951
5367
  }
5368
+ /**
5369
+ * RowPlan 経路の実体化: clone → nodePath 解決 → スロットから薄い binding を複製 →
5370
+ * initializeRowBindings。パース再生(spread 展開・remember・キー文字列・options
5371
+ * オブジェクト・policy 再解決)を行ごとに繰り返さない
5372
+ * (docs/state-row-instantiation-redesign.md §3-1/§3-2)。
5373
+ */
5374
+ function createPlanContent(bindingInfo, fragmentInfo, plan) {
5375
+ const cloneFragment = document.importNode(fragmentInfo.fragment, true);
5376
+ const nodeInfos = fragmentInfo.nodeInfos;
5377
+ const nodes = new Array(nodeInfos.length);
5378
+ for (let i = 0; i < nodeInfos.length; i++) {
5379
+ const node = resolveNodePath(cloneFragment, nodeInfos[i].nodePath);
5380
+ if (node === null) {
5381
+ raiseError(`Node not found by path [${nodeInfos[i].nodePath.join(', ')}] in fragment.`);
5382
+ }
5383
+ // 再スキャン防止と初期化完了マークは従来経路と同じ台帳に載せる
5384
+ markNodeRegistered(node);
5385
+ resolveInitializedBinding(node);
5386
+ nodes[i] = node;
5387
+ }
5388
+ const slots = plan.slots;
5389
+ const bindings = new Array(slots.length);
5390
+ const indexBindings = [];
5391
+ for (let k = 0; k < slots.length; k++) {
5392
+ const slot = slots[k];
5393
+ const node = nodes[slot.nodeIndex];
5394
+ // text スロットは事前正規化済みの Text がそのまま replaceNode(従来経路の
5395
+ // getBindingInfos と同じ帰結)。prop/event は node === replaceNode
5396
+ const binding = { ...slot.template, node, replaceNode: node };
5397
+ bindings[k] = binding;
5398
+ if (slot.isIndexBinding) {
5399
+ indexBindings.push(binding);
5400
+ }
5401
+ }
5402
+ const session = initializeRowBindings(plan, bindings);
5403
+ const content = new Content(cloneFragment);
5404
+ setBindingSessionByContent(content, session);
5405
+ setBindingsByContent(content, bindings);
5406
+ setIndexBindingsByContent(content, indexBindings);
5407
+ setNodesByContent(content, nodes);
5408
+ setContentByNode(bindingInfo.node, content);
5409
+ return content;
5410
+ }
4952
5411
  function createContent(bindingInfo) {
4953
5412
  if (typeof bindingInfo.uuid === 'undefined' || bindingInfo.uuid === null) {
4954
5413
  raiseError(`BindingInfo.uuid is null.`);
@@ -4957,6 +5416,15 @@ function createContent(bindingInfo) {
4957
5416
  if (!fragmentInfo) {
4958
5417
  raiseError(`Fragment with UUID "${bindingInfo.uuid}" not found.`);
4959
5418
  }
5419
+ let plan = fragmentInfo.rowPlan;
5420
+ if (typeof plan === 'undefined' || (plan !== null && plan.directional !== config.enableDirectionalInitialSync)) {
5421
+ // 初回 or config(directional)が変わったときだけコンパイル。不適格は null を
5422
+ // キャッシュして以後は従来経路へ直行する
5423
+ plan = fragmentInfo.rowPlan = compileRowPlan(fragmentInfo);
5424
+ }
5425
+ if (plan !== null) {
5426
+ return createPlanContent(bindingInfo, fragmentInfo, plan);
5427
+ }
4960
5428
  const cloneFragment = document.importNode(fragmentInfo.fragment, true);
4961
5429
  const initialInfo = initializeBindingsByFragment(cloneFragment, fragmentInfo.nodeInfos);
4962
5430
  const content = new Content(cloneFragment);
@@ -5960,16 +6428,29 @@ function initializeBindings(root, parentLoopContext) {
5960
6428
  function initializeBindingsByFragment(root, nodeInfos) {
5961
6429
  const [subscriberNodes, allBindings] = collectNodesAndBindingInfosByFragment(root, nodeInfos);
5962
6430
  const session = new BindingSession();
6431
+ // knownRoot=null: detached fragment 上の初期化。observableRootFor が必ず null を
6432
+ // 返す(observe は no-op)ため、binding ごとの getRootNode を省略する
5963
6433
  const initialized = session.initialize(allBindings, {
5964
6434
  registerAddress: false,
5965
6435
  applyOnReconnect: false,
5966
- });
6436
+ }, null);
5967
6437
  return {
5968
6438
  nodes: subscriberNodes,
5969
6439
  bindingInfos: initialized,
5970
6440
  bindingSession: session,
5971
6441
  };
5972
6442
  }
6443
+ /**
6444
+ * RowPlan 経路の行初期化(createContent 専用)。remember / spread 展開 /
6445
+ * shouldApplyState フィルタを経ず、プランのスロットから直接 record を構築する。
6446
+ * 返す session は従来経路と同じ活性化(activate)・破棄(dispose/wholesale)
6447
+ * インターフェースを持つ。
6448
+ */
6449
+ function initializeRowBindings(plan, bindings) {
6450
+ const session = new BindingSession();
6451
+ session.initializeRow(plan, bindings);
6452
+ return session;
6453
+ }
5973
6454
 
5974
6455
  const MUSTACHE_REGEX = /\{\{\s*(.+?)\s*\}\}/g;
5975
6456
  const SKIP_TAGS = new Set(["SCRIPT", "STYLE"]);
@@ -6383,7 +6864,7 @@ async function buildBindings(root) {
6383
6864
  }
6384
6865
  }
6385
6866
 
6386
- var version = "1.21.5";
6867
+ var version = "1.21.6";
6387
6868
  var pkg = {
6388
6869
  version: version};
6389
6870
 
@@ -7291,19 +7772,29 @@ class Updater {
7291
7772
  continue;
7292
7773
  }
7293
7774
  // peek: バインディングの無いアドレス(リスト置換で enqueue される中間
7294
- // アドレス等)に空 Set を生成・蓄積しない
7295
- const bindings = peekBindingSetByAbsoluteStateAddress(absoluteAddress);
7296
- if (bindings === undefined) {
7775
+ // アドレス等)に空エントリを生成・蓄積しない。エントリは単一 binding
7776
+ // (通常ケース)か Set(同一アドレスに 2 本以上)のどちらか。
7777
+ // 従来台帳 パターン台帳(リスト行)の順で引く。
7778
+ const entry = peekBindingsForAddress(absoluteAddress);
7779
+ if (entry === undefined) {
7297
7780
  continue;
7298
7781
  }
7299
- for (const binding of bindings) {
7300
- if (binding.replaceNode.isConnected === false) {
7301
- // 切断されているバインディングは無視
7302
- continue;
7782
+ if (entry instanceof Set) {
7783
+ for (const binding of entry) {
7784
+ if (binding.replaceNode.isConnected === false) {
7785
+ // 切断されているバインディングは無視
7786
+ continue;
7787
+ }
7788
+ processBindings.push(binding);
7789
+ if (context !== null) {
7790
+ propagationContextByBinding.set(binding, context);
7791
+ }
7303
7792
  }
7304
- processBindings.push(binding);
7793
+ }
7794
+ else if (entry.replaceNode.isConnected !== false) {
7795
+ processBindings.push(entry);
7305
7796
  if (context !== null) {
7306
- propagationContextByBinding.set(binding, context);
7797
+ propagationContextByBinding.set(entry, context);
7307
7798
  }
7308
7799
  }
7309
7800
  }
@@ -9010,6 +9501,10 @@ function dirtyCacheEntryByAbsoluteStateAddress(address) {
9010
9501
  }
9011
9502
 
9012
9503
  function checkDependency(handler, address) {
9504
+ // $untrackDependency スコープ中/setter 実行中は依存を張らない
9505
+ if (handler.untracking) {
9506
+ return;
9507
+ }
9013
9508
  // 動的依存関係の登録
9014
9509
  if (handler.addressStackLength > 0) {
9015
9510
  const lastAddress = handler.lastAddressStack;
@@ -9554,12 +10049,18 @@ function _setByAddress(target, address, absAddress, value, receiver, handler) {
9554
10049
  try {
9555
10050
  if (address.pathInfo.path in target) {
9556
10051
  if (handler.stateElement.setterPaths.has(address.pathInfo.path)) {
9557
- // setterの中で参照の可能性があるので、addressをプッシュする
10052
+ // setterの中で参照の可能性があるので、addressをプッシュする。
10053
+ // setter は命令的な代入であって派生(getter)ではないため、実行中の
10054
+ // 読み取り(同値ガードの旧値読み・$1 参照等)で依存を張らない。
10055
+ // アクセサペア(get/set 同名パス)では、抑止しないと setter 内の内部
10056
+ // 書き込みの同値ガード読みが「getter の依存」として誤登録される。
9558
10057
  handler.pushAddress(address);
10058
+ handler.beginUntrack();
9559
10059
  try {
9560
10060
  return Reflect.set(target, address.pathInfo.path, value, receiver);
9561
10061
  }
9562
10062
  finally {
10063
+ handler.endUntrack();
9563
10064
  handler.popAddress();
9564
10065
  }
9565
10066
  }
@@ -9996,6 +10497,35 @@ function trackDependency(_target, _prop, _receiver, handler) {
9996
10497
  };
9997
10498
  }
9998
10499
 
10500
+ /**
10501
+ * untrackDependency.ts
10502
+ *
10503
+ * StateClass の API として、コールバック実行中の依存追跡を抑止する関数
10504
+ * ($untrackDependency)の実装です。$trackDependency(明示的な依存登録)と
10505
+ * 対称の「明示的な依存抑止」API。
10506
+ *
10507
+ * 主な役割:
10508
+ * - fn 実行中、checkDependency の動的依存登録と $1 インデックス依存の記録を抑止
10509
+ * - fn の戻り値をそのまま返す(値の読み取り自体は通常どおり行われる)
10510
+ *
10511
+ * 設計ポイント:
10512
+ * - スコープはハンドラ単位のカウンタ(ネスト可)で管理し、finally で必ず復元する
10513
+ * - 典型例: リスト行 getter が「行の外の単一値」を読みたいが、その値の変更で
10514
+ * 全行を再評価させたくない場合(選択インデックス等)。書き手側が該当行へ
10515
+ * 直接書き込むことで、必要な行だけが更新される
10516
+ */
10517
+ function untrackDependency(_target, _prop, _receiver, handler) {
10518
+ return (fn) => {
10519
+ handler.beginUntrack();
10520
+ try {
10521
+ return fn();
10522
+ }
10523
+ finally {
10524
+ handler.endUntrack();
10525
+ }
10526
+ };
10527
+ }
10528
+
9999
10529
  /**
10000
10530
  * updatedCallback.ts
10001
10531
  *
@@ -10137,8 +10667,9 @@ function get(target, prop, receiver, handler) {
10137
10667
  // getter 評価中のインデックス読み取りを記録する。位置だけが変わった行
10138
10668
  // (listDiff.changeIndexSet)は index 以外の入力が不変なので、walkDependency の
10139
10669
  // 静的子展開を「インデックスを読んだ getter の subtree」に限定できる。
10670
+ // $untrackDependency スコープ中/setter 実行中は記録しない。
10140
10671
  const lastInfo = lastAddress?.pathInfo;
10141
- if (lastInfo && handler.stateElement?.getterPaths.has(lastInfo.path)) {
10672
+ if (lastInfo && !handler.untracking && handler.stateElement?.getterPaths.has(lastInfo.path)) {
10142
10673
  handler.stateElement.addIndexDependentGetterPath?.(lastInfo.path);
10143
10674
  }
10144
10675
  const listIndex = lastAddress?.listIndex;
@@ -10170,6 +10701,11 @@ function get(target, prop, receiver, handler) {
10170
10701
  return trackDependency(target, prop, receiver, handler)(path);
10171
10702
  };
10172
10703
  }
10704
+ case "$untrackDependency": {
10705
+ return (fn) => {
10706
+ return untrackDependency(target, prop, receiver, handler)(fn);
10707
+ };
10708
+ }
10173
10709
  case STATE_COMMAND_NAMESPACE_NAME: {
10174
10710
  return getCommandNamespace(handler.stateElement);
10175
10711
  }
@@ -10294,6 +10830,7 @@ class StateHandler {
10294
10830
  _addressStackIndex = -1;
10295
10831
  _loopContext;
10296
10832
  _mutability;
10833
+ _untrackDepth = 0;
10297
10834
  constructor(rootNode, stateName, mutability) {
10298
10835
  this._stateName = stateName;
10299
10836
  const stateElement = getStateElementByName(rootNode, this._stateName);
@@ -10350,6 +10887,15 @@ class StateHandler {
10350
10887
  clearLoopContext() {
10351
10888
  this._loopContext = undefined;
10352
10889
  }
10890
+ get untracking() {
10891
+ return this._untrackDepth > 0;
10892
+ }
10893
+ beginUntrack() {
10894
+ this._untrackDepth++;
10895
+ }
10896
+ endUntrack() {
10897
+ this._untrackDepth--;
10898
+ }
10353
10899
  get(target, prop, receiver) {
10354
10900
  return get(target, prop, receiver, this);
10355
10901
  }