@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/README.ja.md +42 -0
- package/README.md +45 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.esm.js +651 -96
- package/dist/index.esm.js.map +1 -1
- package/dist/index.esm.min.js +1 -1
- package/dist/index.esm.min.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
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
|
-
*
|
|
2148
|
-
*
|
|
2149
|
-
*
|
|
2156
|
+
* 絶対アドレス → 登録 binding の台帳。
|
|
2157
|
+
*
|
|
2158
|
+
* リスト行の絶対アドレスは (absolutePathInfo, listIndex) の組ごとに一意で、
|
|
2159
|
+
* 登録される binding は通常 1 本しかない。アドレスごとに Set を確保すると
|
|
2160
|
+
* 行×binding の数だけ Set アロケーションが積み上がるため、単一値で持ち
|
|
2161
|
+
* 2 本目から Set に昇格する(interestedSessionsByNode と同じ前例)。
|
|
2150
2162
|
*/
|
|
2151
|
-
|
|
2152
|
-
return bindingSetByAbsoluteStateAddress.get(absoluteStateAddress);
|
|
2153
|
-
}
|
|
2163
|
+
const bindingsByAbsoluteStateAddress = new WeakMap();
|
|
2154
2164
|
function addBindingByAbsoluteStateAddress(absoluteStateAddress, binding) {
|
|
2155
|
-
const
|
|
2156
|
-
|
|
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
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
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
|
-
|
|
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
|
-
// 初回活性化(
|
|
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
|
|
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
|
|
3732
|
-
* もろとも GC で崩壊する(recordByBinding 以下は全て弱参照)。
|
|
3852
|
+
* content 専用)。イベント listener・loopContext・パターン台帳(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
|
|
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
|
-
|
|
3775
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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
|
|
4177
|
+
addRecordTeardown(record, () => detachEventTokenHandler(binding));
|
|
3891
4178
|
}
|
|
3892
4179
|
});
|
|
3893
4180
|
return;
|
|
3894
4181
|
}
|
|
3895
4182
|
if (attachRadioEventHandler(binding)) {
|
|
3896
|
-
record
|
|
4183
|
+
addRecordTeardown(record, () => detachRadioEventHandler(binding));
|
|
3897
4184
|
}
|
|
3898
4185
|
if (attachCheckboxEventHandler(binding)) {
|
|
3899
|
-
record
|
|
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
|
|
4208
|
+
addRecordTeardown(record, removeObserver);
|
|
3922
4209
|
}
|
|
3923
4210
|
attachTwowayEventHandler(binding);
|
|
3924
|
-
record.
|
|
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
|
|
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
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
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
|
|
4084
|
-
|
|
4085
|
-
|
|
4380
|
+
const binding = record.info;
|
|
4381
|
+
// データ駆動の後始末(従来はクロージャで積んでいた頻出3種)。実行順は従来の
|
|
4382
|
+
// 逆順実行と同じ: アドレス台帳解除(最後に積まれていた)→ 双方向 detach →
|
|
4383
|
+
// 希少クロージャ群(逆順)→ イベント detach。各 detach は互いに独立した資源を
|
|
4384
|
+
// 対象とするため、この順序で意味論は変わらない。
|
|
4385
|
+
if (record.address !== null) {
|
|
4086
4386
|
try {
|
|
4087
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
7295
|
-
|
|
7296
|
-
|
|
7784
|
+
// アドレス等)に空エントリを生成・蓄積しない。エントリは単一 binding
|
|
7785
|
+
// (通常ケース)か Set(同一アドレスに 2 本以上)のどちらか。
|
|
7786
|
+
// 従来台帳 → パターン台帳(リスト行)の順で引く。
|
|
7787
|
+
const entry = peekBindingsForAddress(absoluteAddress);
|
|
7788
|
+
if (entry === undefined) {
|
|
7297
7789
|
continue;
|
|
7298
7790
|
}
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
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
|
-
|
|
7802
|
+
}
|
|
7803
|
+
else if (entry.replaceNode.isConnected !== false) {
|
|
7804
|
+
processBindings.push(entry);
|
|
7305
7805
|
if (context !== null) {
|
|
7306
|
-
propagationContextByBinding.set(
|
|
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
|
}
|