@wcstack/state 1.21.4 → 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/README.ja.md +1 -0
- package/README.md +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.esm.js +676 -118
- 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,40 +2152,113 @@ 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 });
|
|
2227
|
+
}
|
|
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;
|
|
2169
2260
|
}
|
|
2261
|
+
return patternLedger.get(absoluteStateAddress.absolutePathInfo)?.get(absoluteStateAddress.listIndex);
|
|
2170
2262
|
}
|
|
2171
2263
|
|
|
2172
2264
|
const _cache$1 = 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) {
|
|
@@ -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
|
|
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
|
-
|
|
3775
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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
|
|
4168
|
+
addRecordTeardown(record, () => detachEventTokenHandler(binding));
|
|
3891
4169
|
}
|
|
3892
4170
|
});
|
|
3893
4171
|
return;
|
|
3894
4172
|
}
|
|
3895
4173
|
if (attachRadioEventHandler(binding)) {
|
|
3896
|
-
record
|
|
4174
|
+
addRecordTeardown(record, () => detachRadioEventHandler(binding));
|
|
3897
4175
|
}
|
|
3898
4176
|
if (attachCheckboxEventHandler(binding)) {
|
|
3899
|
-
record
|
|
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
|
|
4199
|
+
addRecordTeardown(record, removeObserver);
|
|
3922
4200
|
}
|
|
3923
4201
|
attachTwowayEventHandler(binding);
|
|
3924
|
-
record.
|
|
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
|
|
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
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
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
|
|
4084
|
-
|
|
4085
|
-
|
|
4371
|
+
const binding = record.info;
|
|
4372
|
+
// データ駆動の後始末(従来はクロージャで積んでいた頻出3種)。実行順は従来の
|
|
4373
|
+
// 逆順実行と同じ: アドレス台帳解除(最後に積まれていた)→ 双方向 detach →
|
|
4374
|
+
// 希少クロージャ群(逆順)→ イベント detach。各 detach は互いに独立した資源を
|
|
4375
|
+
// 対象とするため、この順序で意味論は変わらない。
|
|
4376
|
+
if (record.address !== null) {
|
|
4086
4377
|
try {
|
|
4087
|
-
|
|
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
|
-
|
|
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);
|
|
@@ -5049,14 +5517,6 @@ function isPhysicallyAfter(lastNode, firstNode) {
|
|
|
5049
5517
|
return (position & Node.DOCUMENT_POSITION_FOLLOWING) !== 0
|
|
5050
5518
|
&& (position & Node.DOCUMENT_POSITION_DISCONNECTED) === 0;
|
|
5051
5519
|
}
|
|
5052
|
-
function getContent(node, listIndex) {
|
|
5053
|
-
let contentByListIndex = contentByListIndexByNode.get(node);
|
|
5054
|
-
if (typeof contentByListIndex === 'undefined') {
|
|
5055
|
-
return null;
|
|
5056
|
-
}
|
|
5057
|
-
const content = contentByListIndex.get(listIndex);
|
|
5058
|
-
return typeof content === 'undefined' ? null : content;
|
|
5059
|
-
}
|
|
5060
5520
|
function setContent(node, listIndex, content) {
|
|
5061
5521
|
let contentByListIndex = contentByListIndexByNode.get(node);
|
|
5062
5522
|
if (typeof contentByListIndex === 'undefined') {
|
|
@@ -5100,22 +5560,35 @@ function applyChangeToFor(bindingInfo, context, newValue) {
|
|
|
5100
5560
|
// teardown(listener 解除・アドレス台帳・loopContext 掃除)を丸ごと省略して
|
|
5101
5561
|
// ノードごと GC に任せる(tryDestroy)。プール行きの分だけ従来どおり解体する
|
|
5102
5562
|
// (プール行は binding が生存し続けるため address キャッシュのクリアが必須)。
|
|
5563
|
+
// content 台帳の WeakMap ビルトインは V8 プロファイルで本関数の self に計上される
|
|
5564
|
+
// ホットスポット: 外側の node→map 解決はループ外に持ち上げ、fullDelete(旧全行が
|
|
5565
|
+
// deleteIndexSet に載る=台帳の全エントリが消える)では per-index delete を廃して
|
|
5566
|
+
// 台帳ごと 1 回で手放す。
|
|
5567
|
+
let contentMap = contentByListIndexByNode.get(bindingInfo.node);
|
|
5103
5568
|
let poolBudget = fullDelete
|
|
5104
5569
|
? maxPooledContents - getPooledContents(bindingInfo).length
|
|
5105
5570
|
: Number.POSITIVE_INFINITY;
|
|
5106
|
-
|
|
5107
|
-
const
|
|
5108
|
-
|
|
5109
|
-
if (
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5571
|
+
if (typeof contentMap !== 'undefined') {
|
|
5572
|
+
for (const deleteIndex of diff.deleteIndexSet) {
|
|
5573
|
+
const content = contentMap.get(deleteIndex);
|
|
5574
|
+
if (typeof content !== 'undefined') {
|
|
5575
|
+
if (poolBudget <= 0 && content.tryDestroy()) {
|
|
5576
|
+
deleteContentByNode(bindingInfo.node, content);
|
|
5577
|
+
}
|
|
5578
|
+
else {
|
|
5579
|
+
deactivateContent(content);
|
|
5580
|
+
content.unmount();
|
|
5581
|
+
setPooledContent(bindingInfo, content);
|
|
5582
|
+
poolBudget -= 1;
|
|
5583
|
+
}
|
|
5584
|
+
if (!fullDelete) {
|
|
5585
|
+
contentMap.delete(deleteIndex);
|
|
5586
|
+
}
|
|
5117
5587
|
}
|
|
5118
|
-
|
|
5588
|
+
}
|
|
5589
|
+
if (fullDelete) {
|
|
5590
|
+
contentByListIndexByNode.delete(bindingInfo.node);
|
|
5591
|
+
contentMap = undefined;
|
|
5119
5592
|
}
|
|
5120
5593
|
}
|
|
5121
5594
|
let lastNode = bindingInfo.node;
|
|
@@ -5136,14 +5609,16 @@ function applyChangeToFor(bindingInfo, context, newValue) {
|
|
|
5136
5609
|
}
|
|
5137
5610
|
const ssrMode = inSsr();
|
|
5138
5611
|
const uuid = bindingInfo.uuid ?? '';
|
|
5612
|
+
// 追加行ごとの WeakMap 解決を避けるためプール配列も 1 回だけ引く(プールの配列
|
|
5613
|
+
// 実体は setPooledContent が一度作ったら不変なので、delete ループ後の参照で安定)
|
|
5614
|
+
const pooledContents = pooledContentsByNode.get(bindingInfo.node);
|
|
5139
5615
|
for (const index of diff.newIndexes) {
|
|
5140
5616
|
let content;
|
|
5141
5617
|
// add
|
|
5142
5618
|
if (diff.addIndexSet.has(index)) {
|
|
5143
5619
|
const stateAddress = createStateAddress(elementPathInfo, index);
|
|
5144
5620
|
loopContextStack.createLoopContext(stateAddress, (loopContext) => {
|
|
5145
|
-
|
|
5146
|
-
content = pooledContents.pop();
|
|
5621
|
+
content = typeof pooledContents !== 'undefined' ? pooledContents.pop() : undefined;
|
|
5147
5622
|
if (typeof content === 'undefined') {
|
|
5148
5623
|
content = createContent(bindingInfo);
|
|
5149
5624
|
}
|
|
@@ -5182,7 +5657,8 @@ function applyChangeToFor(bindingInfo, context, newValue) {
|
|
|
5182
5657
|
}
|
|
5183
5658
|
}
|
|
5184
5659
|
else {
|
|
5185
|
-
|
|
5660
|
+
// getContent 相当(undefined→null 正規化は後段の raiseError 判定が null 比較のため維持)
|
|
5661
|
+
content = (typeof contentMap !== 'undefined' ? contentMap.get(index) ?? null : null);
|
|
5186
5662
|
if (diff.changeIndexSet.has(index)) {
|
|
5187
5663
|
// change
|
|
5188
5664
|
const indexBindings = getIndexBindingsByContent(content);
|
|
@@ -5206,7 +5682,11 @@ function applyChangeToFor(bindingInfo, context, newValue) {
|
|
|
5206
5682
|
}
|
|
5207
5683
|
}
|
|
5208
5684
|
lastNode = content.lastNode || lastNode;
|
|
5209
|
-
|
|
5685
|
+
if (typeof contentMap === 'undefined') {
|
|
5686
|
+
contentMap = new WeakMap();
|
|
5687
|
+
contentByListIndexByNode.set(bindingInfo.node, contentMap);
|
|
5688
|
+
}
|
|
5689
|
+
contentMap.set(index, content);
|
|
5210
5690
|
}
|
|
5211
5691
|
lastNodeByNode.set(bindingInfo.node, lastNode);
|
|
5212
5692
|
if (fragment !== null) {
|
|
@@ -5948,16 +6428,29 @@ function initializeBindings(root, parentLoopContext) {
|
|
|
5948
6428
|
function initializeBindingsByFragment(root, nodeInfos) {
|
|
5949
6429
|
const [subscriberNodes, allBindings] = collectNodesAndBindingInfosByFragment(root, nodeInfos);
|
|
5950
6430
|
const session = new BindingSession();
|
|
6431
|
+
// knownRoot=null: detached fragment 上の初期化。observableRootFor が必ず null を
|
|
6432
|
+
// 返す(observe は no-op)ため、binding ごとの getRootNode を省略する
|
|
5951
6433
|
const initialized = session.initialize(allBindings, {
|
|
5952
6434
|
registerAddress: false,
|
|
5953
6435
|
applyOnReconnect: false,
|
|
5954
|
-
});
|
|
6436
|
+
}, null);
|
|
5955
6437
|
return {
|
|
5956
6438
|
nodes: subscriberNodes,
|
|
5957
6439
|
bindingInfos: initialized,
|
|
5958
6440
|
bindingSession: session,
|
|
5959
6441
|
};
|
|
5960
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
|
+
}
|
|
5961
6454
|
|
|
5962
6455
|
const MUSTACHE_REGEX = /\{\{\s*(.+?)\s*\}\}/g;
|
|
5963
6456
|
const SKIP_TAGS = new Set(["SCRIPT", "STYLE"]);
|
|
@@ -6371,7 +6864,7 @@ async function buildBindings(root) {
|
|
|
6371
6864
|
}
|
|
6372
6865
|
}
|
|
6373
6866
|
|
|
6374
|
-
var version = "1.21.
|
|
6867
|
+
var version = "1.21.6";
|
|
6375
6868
|
var pkg = {
|
|
6376
6869
|
version: version};
|
|
6377
6870
|
|
|
@@ -7279,19 +7772,29 @@ class Updater {
|
|
|
7279
7772
|
continue;
|
|
7280
7773
|
}
|
|
7281
7774
|
// peek: バインディングの無いアドレス(リスト置換で enqueue される中間
|
|
7282
|
-
//
|
|
7283
|
-
|
|
7284
|
-
|
|
7775
|
+
// アドレス等)に空エントリを生成・蓄積しない。エントリは単一 binding
|
|
7776
|
+
// (通常ケース)か Set(同一アドレスに 2 本以上)のどちらか。
|
|
7777
|
+
// 従来台帳 → パターン台帳(リスト行)の順で引く。
|
|
7778
|
+
const entry = peekBindingsForAddress(absoluteAddress);
|
|
7779
|
+
if (entry === undefined) {
|
|
7285
7780
|
continue;
|
|
7286
7781
|
}
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
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
|
+
}
|
|
7291
7792
|
}
|
|
7292
|
-
|
|
7793
|
+
}
|
|
7794
|
+
else if (entry.replaceNode.isConnected !== false) {
|
|
7795
|
+
processBindings.push(entry);
|
|
7293
7796
|
if (context !== null) {
|
|
7294
|
-
propagationContextByBinding.set(
|
|
7797
|
+
propagationContextByBinding.set(entry, context);
|
|
7295
7798
|
}
|
|
7296
7799
|
}
|
|
7297
7800
|
}
|
|
@@ -8998,6 +9501,10 @@ function dirtyCacheEntryByAbsoluteStateAddress(address) {
|
|
|
8998
9501
|
}
|
|
8999
9502
|
|
|
9000
9503
|
function checkDependency(handler, address) {
|
|
9504
|
+
// $untrackDependency スコープ中/setter 実行中は依存を張らない
|
|
9505
|
+
if (handler.untracking) {
|
|
9506
|
+
return;
|
|
9507
|
+
}
|
|
9001
9508
|
// 動的依存関係の登録
|
|
9002
9509
|
if (handler.addressStackLength > 0) {
|
|
9003
9510
|
const lastAddress = handler.lastAddressStack;
|
|
@@ -9542,12 +10049,18 @@ function _setByAddress(target, address, absAddress, value, receiver, handler) {
|
|
|
9542
10049
|
try {
|
|
9543
10050
|
if (address.pathInfo.path in target) {
|
|
9544
10051
|
if (handler.stateElement.setterPaths.has(address.pathInfo.path)) {
|
|
9545
|
-
// setterの中で参照の可能性があるので、address
|
|
10052
|
+
// setterの中で参照の可能性があるので、addressをプッシュする。
|
|
10053
|
+
// setter は命令的な代入であって派生(getter)ではないため、実行中の
|
|
10054
|
+
// 読み取り(同値ガードの旧値読み・$1 参照等)で依存を張らない。
|
|
10055
|
+
// アクセサペア(get/set 同名パス)では、抑止しないと setter 内の内部
|
|
10056
|
+
// 書き込みの同値ガード読みが「getter の依存」として誤登録される。
|
|
9546
10057
|
handler.pushAddress(address);
|
|
10058
|
+
handler.beginUntrack();
|
|
9547
10059
|
try {
|
|
9548
10060
|
return Reflect.set(target, address.pathInfo.path, value, receiver);
|
|
9549
10061
|
}
|
|
9550
10062
|
finally {
|
|
10063
|
+
handler.endUntrack();
|
|
9551
10064
|
handler.popAddress();
|
|
9552
10065
|
}
|
|
9553
10066
|
}
|
|
@@ -9984,6 +10497,35 @@ function trackDependency(_target, _prop, _receiver, handler) {
|
|
|
9984
10497
|
};
|
|
9985
10498
|
}
|
|
9986
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
|
+
|
|
9987
10529
|
/**
|
|
9988
10530
|
* updatedCallback.ts
|
|
9989
10531
|
*
|
|
@@ -10125,8 +10667,9 @@ function get(target, prop, receiver, handler) {
|
|
|
10125
10667
|
// getter 評価中のインデックス読み取りを記録する。位置だけが変わった行
|
|
10126
10668
|
// (listDiff.changeIndexSet)は index 以外の入力が不変なので、walkDependency の
|
|
10127
10669
|
// 静的子展開を「インデックスを読んだ getter の subtree」に限定できる。
|
|
10670
|
+
// $untrackDependency スコープ中/setter 実行中は記録しない。
|
|
10128
10671
|
const lastInfo = lastAddress?.pathInfo;
|
|
10129
|
-
if (lastInfo && handler.stateElement?.getterPaths.has(lastInfo.path)) {
|
|
10672
|
+
if (lastInfo && !handler.untracking && handler.stateElement?.getterPaths.has(lastInfo.path)) {
|
|
10130
10673
|
handler.stateElement.addIndexDependentGetterPath?.(lastInfo.path);
|
|
10131
10674
|
}
|
|
10132
10675
|
const listIndex = lastAddress?.listIndex;
|
|
@@ -10158,6 +10701,11 @@ function get(target, prop, receiver, handler) {
|
|
|
10158
10701
|
return trackDependency(target, prop, receiver, handler)(path);
|
|
10159
10702
|
};
|
|
10160
10703
|
}
|
|
10704
|
+
case "$untrackDependency": {
|
|
10705
|
+
return (fn) => {
|
|
10706
|
+
return untrackDependency(target, prop, receiver, handler)(fn);
|
|
10707
|
+
};
|
|
10708
|
+
}
|
|
10161
10709
|
case STATE_COMMAND_NAMESPACE_NAME: {
|
|
10162
10710
|
return getCommandNamespace(handler.stateElement);
|
|
10163
10711
|
}
|
|
@@ -10282,6 +10830,7 @@ class StateHandler {
|
|
|
10282
10830
|
_addressStackIndex = -1;
|
|
10283
10831
|
_loopContext;
|
|
10284
10832
|
_mutability;
|
|
10833
|
+
_untrackDepth = 0;
|
|
10285
10834
|
constructor(rootNode, stateName, mutability) {
|
|
10286
10835
|
this._stateName = stateName;
|
|
10287
10836
|
const stateElement = getStateElementByName(rootNode, this._stateName);
|
|
@@ -10338,6 +10887,15 @@ class StateHandler {
|
|
|
10338
10887
|
clearLoopContext() {
|
|
10339
10888
|
this._loopContext = undefined;
|
|
10340
10889
|
}
|
|
10890
|
+
get untracking() {
|
|
10891
|
+
return this._untrackDepth > 0;
|
|
10892
|
+
}
|
|
10893
|
+
beginUntrack() {
|
|
10894
|
+
this._untrackDepth++;
|
|
10895
|
+
}
|
|
10896
|
+
endUntrack() {
|
|
10897
|
+
this._untrackDepth--;
|
|
10898
|
+
}
|
|
10341
10899
|
get(target, prop, receiver) {
|
|
10342
10900
|
return get(target, prop, receiver, this);
|
|
10343
10901
|
}
|