@wcstack/state 1.18.0 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.esm.js +376 -85
- 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
|
@@ -63,7 +63,7 @@ function setConfig(partialConfig) {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
var version$1 = "1.
|
|
66
|
+
var version$1 = "1.19.0";
|
|
67
67
|
var pkg = {
|
|
68
68
|
version: version$1};
|
|
69
69
|
|
|
@@ -1846,13 +1846,62 @@ const connectedCallbackSymbol = Symbol("$$connectedCallback");
|
|
|
1846
1846
|
const disconnectedCallbackSymbol = Symbol("$$disconnectedCallback");
|
|
1847
1847
|
const updatedCallbackSymbol = Symbol("$$updatedCallback");
|
|
1848
1848
|
|
|
1849
|
+
function createHandlerBindingRegistry() {
|
|
1850
|
+
const attachedByKey = new Map();
|
|
1851
|
+
const countByKey = new Map();
|
|
1852
|
+
return {
|
|
1853
|
+
add(key, binding) {
|
|
1854
|
+
let attached = attachedByKey.get(key);
|
|
1855
|
+
if (typeof attached === "undefined") {
|
|
1856
|
+
attached = new WeakSet();
|
|
1857
|
+
attachedByKey.set(key, attached);
|
|
1858
|
+
}
|
|
1859
|
+
if (attached.has(binding)) {
|
|
1860
|
+
return false;
|
|
1861
|
+
}
|
|
1862
|
+
attached.add(binding);
|
|
1863
|
+
countByKey.set(key, (countByKey.get(key) ?? 0) + 1);
|
|
1864
|
+
return true;
|
|
1865
|
+
},
|
|
1866
|
+
remove(key, binding) {
|
|
1867
|
+
const attached = attachedByKey.get(key);
|
|
1868
|
+
if (typeof attached === "undefined" || !attached.has(binding)) {
|
|
1869
|
+
return false;
|
|
1870
|
+
}
|
|
1871
|
+
attached.delete(binding);
|
|
1872
|
+
const next = (countByKey.get(key) ?? 1) - 1;
|
|
1873
|
+
if (next <= 0) {
|
|
1874
|
+
attachedByKey.delete(key);
|
|
1875
|
+
countByKey.delete(key);
|
|
1876
|
+
return true;
|
|
1877
|
+
}
|
|
1878
|
+
countByKey.set(key, next);
|
|
1879
|
+
return false;
|
|
1880
|
+
},
|
|
1881
|
+
has(key, binding) {
|
|
1882
|
+
return attachedByKey.get(key)?.has(binding) ?? false;
|
|
1883
|
+
},
|
|
1884
|
+
countOf(key) {
|
|
1885
|
+
return countByKey.get(key) ?? 0;
|
|
1886
|
+
},
|
|
1887
|
+
get keyCount() {
|
|
1888
|
+
return countByKey.size;
|
|
1889
|
+
},
|
|
1890
|
+
clear() {
|
|
1891
|
+
attachedByKey.clear();
|
|
1892
|
+
countByKey.clear();
|
|
1893
|
+
},
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1849
1897
|
// onclick: $command.<name> のように、DOM イベントから command token を直接 emit する形式かを判定する。
|
|
1850
1898
|
// 右辺が $command 名前空間配下のパス($command.<token>)のときに true。
|
|
1851
1899
|
function isCommandTokenPath(statePathName) {
|
|
1852
1900
|
return statePathName.startsWith(STATE_COMMAND_NAMESPACE_NAME + ".");
|
|
1853
1901
|
}
|
|
1854
1902
|
const handlerByHandlerKey$3 = new Map();
|
|
1855
|
-
|
|
1903
|
+
// binding を強参照しない台帳(handlerBindingRegistry.ts のリーク解説を参照)
|
|
1904
|
+
const bindingRegistry$3 = createHandlerBindingRegistry();
|
|
1856
1905
|
function getHandlerKey$3(binding) {
|
|
1857
1906
|
const modifierKey = binding.propModifiers.filter(m => m === 'prevent' || m === 'stop').sort().join(',');
|
|
1858
1907
|
return `${binding.stateName}::${binding.statePathName}::${modifierKey}`;
|
|
@@ -1901,14 +1950,7 @@ function attachEventHandler(binding) {
|
|
|
1901
1950
|
}
|
|
1902
1951
|
const eventName = binding.propName.slice(2);
|
|
1903
1952
|
binding.node.addEventListener(eventName, stateEventHandler);
|
|
1904
|
-
|
|
1905
|
-
if (typeof bindingSet === "undefined") {
|
|
1906
|
-
bindingSet = new Set([binding]);
|
|
1907
|
-
bindingSetByHandlerKey$3.set(key, bindingSet);
|
|
1908
|
-
}
|
|
1909
|
-
else {
|
|
1910
|
-
bindingSet.add(binding);
|
|
1911
|
-
}
|
|
1953
|
+
bindingRegistry$3.add(key, binding);
|
|
1912
1954
|
return true;
|
|
1913
1955
|
}
|
|
1914
1956
|
|
|
@@ -2078,7 +2120,8 @@ function isPossibleTwoWay(node, propName) {
|
|
|
2078
2120
|
}
|
|
2079
2121
|
|
|
2080
2122
|
const handlerByHandlerKey$2 = new Map();
|
|
2081
|
-
|
|
2123
|
+
// binding を強参照しない台帳(handlerBindingRegistry.ts のリーク解説を参照)
|
|
2124
|
+
const bindingRegistry$2 = createHandlerBindingRegistry();
|
|
2082
2125
|
const DEFAULT_GETTER = (e) => e.detail;
|
|
2083
2126
|
function getHandlerKey$2(binding, eventName, hasGetter) {
|
|
2084
2127
|
const filterKey = binding.inFilters.map(f => f.filterName + '(' + f.args.join(',') + ')').join('|');
|
|
@@ -2181,14 +2224,7 @@ function attachTwowayEventHandler(binding) {
|
|
|
2181
2224
|
handlerByHandlerKey$2.set(key, twowayEventHandler);
|
|
2182
2225
|
}
|
|
2183
2226
|
binding.node.addEventListener(eventName, twowayEventHandler);
|
|
2184
|
-
|
|
2185
|
-
if (typeof bindingSet === "undefined") {
|
|
2186
|
-
bindingSet = new Set([binding]);
|
|
2187
|
-
bindingSetByHandlerKey$2.set(key, bindingSet);
|
|
2188
|
-
}
|
|
2189
|
-
else {
|
|
2190
|
-
bindingSet.add(binding);
|
|
2191
|
-
}
|
|
2227
|
+
bindingRegistry$2.add(key, binding);
|
|
2192
2228
|
}
|
|
2193
2229
|
}
|
|
2194
2230
|
|
|
@@ -2893,6 +2929,103 @@ function calcDiffIndexes(oldList, newList, oldIndexes, newIndexes, indexByValue)
|
|
|
2893
2929
|
};
|
|
2894
2930
|
}
|
|
2895
2931
|
|
|
2932
|
+
/**
|
|
2933
|
+
* Indices into `seq` whose values form a longest strictly-increasing
|
|
2934
|
+
* subsequence, returned in ascending order. Classic patience-sorting
|
|
2935
|
+
* LIS in O(n log n). `seq` values are assumed distinct (old list
|
|
2936
|
+
* positions are unique).
|
|
2937
|
+
*/
|
|
2938
|
+
function longestIncreasingSubsequence(seq) {
|
|
2939
|
+
const n = seq.length;
|
|
2940
|
+
// tails[k] = index into seq of the smallest tail of an increasing
|
|
2941
|
+
// subsequence of length k+1; prev[i] = predecessor index to rebuild the chain.
|
|
2942
|
+
const tails = [];
|
|
2943
|
+
const prev = new Array(n).fill(-1);
|
|
2944
|
+
for (let i = 0; i < n; i++) {
|
|
2945
|
+
const value = seq[i];
|
|
2946
|
+
let lo = 0;
|
|
2947
|
+
let hi = tails.length;
|
|
2948
|
+
while (lo < hi) {
|
|
2949
|
+
const mid = (lo + hi) >> 1;
|
|
2950
|
+
if (seq[tails[mid]] < value) {
|
|
2951
|
+
lo = mid + 1;
|
|
2952
|
+
}
|
|
2953
|
+
else {
|
|
2954
|
+
hi = mid;
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
if (lo > 0) {
|
|
2958
|
+
prev[i] = tails[lo - 1];
|
|
2959
|
+
}
|
|
2960
|
+
tails[lo] = i;
|
|
2961
|
+
}
|
|
2962
|
+
const result = [];
|
|
2963
|
+
let k = tails.length > 0 ? tails[tails.length - 1] : -1;
|
|
2964
|
+
while (k >= 0) {
|
|
2965
|
+
result.push(k);
|
|
2966
|
+
k = prev[k];
|
|
2967
|
+
}
|
|
2968
|
+
result.reverse();
|
|
2969
|
+
return result;
|
|
2970
|
+
}
|
|
2971
|
+
/**
|
|
2972
|
+
* Determines which reused list indexes can stay where they are when the DOM
|
|
2973
|
+
* is brought into the new list order.
|
|
2974
|
+
*
|
|
2975
|
+
* Returns null when the reused indexes already appear in their old relative
|
|
2976
|
+
* order (no inversions) — the caller's existing position guard then performs
|
|
2977
|
+
* no moves, so nothing extra is needed. When inversions exist, returns the
|
|
2978
|
+
* set of indexes forming a longest increasing subsequence of old positions:
|
|
2979
|
+
* leaving exactly those in place and moving every other content yields the
|
|
2980
|
+
* correct final order with the fewest content moves (the naive forward walk
|
|
2981
|
+
* otherwise cascades: a single swap of rows 2/999 in 1000 rows moves ~997
|
|
2982
|
+
* contents instead of 2).
|
|
2983
|
+
*
|
|
2984
|
+
* Note: IListIndex.index is already mutated to the NEW position by
|
|
2985
|
+
* createListDiff, so old positions must come from the oldIndexes array order.
|
|
2986
|
+
*/
|
|
2987
|
+
function computeStableIndexSet(diff) {
|
|
2988
|
+
// No reused index changed position, or nothing was reused: relative order
|
|
2989
|
+
// is already correct and the walk performs no moves.
|
|
2990
|
+
if (diff.changeIndexSet.size === 0 || diff.addIndexSet.size === diff.newIndexes.length) {
|
|
2991
|
+
return null;
|
|
2992
|
+
}
|
|
2993
|
+
const oldPosByIndex = new Map();
|
|
2994
|
+
for (let i = 0; i < diff.oldIndexes.length; i++) {
|
|
2995
|
+
oldPosByIndex.set(diff.oldIndexes[i], i);
|
|
2996
|
+
}
|
|
2997
|
+
const reused = [];
|
|
2998
|
+
const seq = [];
|
|
2999
|
+
let prevPos = -1;
|
|
3000
|
+
let sorted = true;
|
|
3001
|
+
for (const index of diff.newIndexes) {
|
|
3002
|
+
if (diff.addIndexSet.has(index)) {
|
|
3003
|
+
continue;
|
|
3004
|
+
}
|
|
3005
|
+
const pos = oldPosByIndex.get(index);
|
|
3006
|
+
if (pos === undefined) {
|
|
3007
|
+
// Invariant break (a reused index missing from oldIndexes): fall back
|
|
3008
|
+
// to the settle walk rather than compute a stable set from bad data.
|
|
3009
|
+
return null;
|
|
3010
|
+
}
|
|
3011
|
+
if (pos < prevPos) {
|
|
3012
|
+
sorted = false;
|
|
3013
|
+
}
|
|
3014
|
+
prevPos = pos;
|
|
3015
|
+
reused.push(index);
|
|
3016
|
+
seq.push(pos);
|
|
3017
|
+
}
|
|
3018
|
+
if (sorted) {
|
|
3019
|
+
return null;
|
|
3020
|
+
}
|
|
3021
|
+
const lis = longestIncreasingSubsequence(seq);
|
|
3022
|
+
const stable = new Set();
|
|
3023
|
+
for (const seqIndex of lis) {
|
|
3024
|
+
stable.add(reused[seqIndex]);
|
|
3025
|
+
}
|
|
3026
|
+
return stable;
|
|
3027
|
+
}
|
|
3028
|
+
|
|
2896
3029
|
const bindingSetByAbsoluteStateAddress = new WeakMap();
|
|
2897
3030
|
function getBindingSetByAbsoluteStateAddress(absoluteStateAddress) {
|
|
2898
3031
|
let bindingSet = null;
|
|
@@ -2903,38 +3036,24 @@ function getBindingSetByAbsoluteStateAddress(absoluteStateAddress) {
|
|
|
2903
3036
|
}
|
|
2904
3037
|
return bindingSet;
|
|
2905
3038
|
}
|
|
3039
|
+
/**
|
|
3040
|
+
* 参照専用の取得。get-or-create と違い、未登録アドレスに空 Set を
|
|
3041
|
+
* 生成・キャッシュしない(リスト置換の drain は大量のバインディング無し
|
|
3042
|
+
* アドレスを照会するため、生成すると空 Set が溜まり続ける)。
|
|
3043
|
+
*/
|
|
3044
|
+
function peekBindingSetByAbsoluteStateAddress(absoluteStateAddress) {
|
|
3045
|
+
return bindingSetByAbsoluteStateAddress.get(absoluteStateAddress);
|
|
3046
|
+
}
|
|
2906
3047
|
function addBindingByAbsoluteStateAddress(absoluteStateAddress, binding) {
|
|
2907
3048
|
const bindingSet = getBindingSetByAbsoluteStateAddress(absoluteStateAddress);
|
|
2908
3049
|
bindingSet.add(binding);
|
|
2909
3050
|
}
|
|
2910
3051
|
function removeBindingByAbsoluteStateAddress(absoluteStateAddress, binding) {
|
|
2911
|
-
|
|
2912
|
-
bindingSet.
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
const stateAddressByBindingInfo = new WeakMap();
|
|
2916
|
-
function getStateAddressByBindingInfo(bindingInfo) {
|
|
2917
|
-
let stateAddress = null;
|
|
2918
|
-
stateAddress = stateAddressByBindingInfo.get(bindingInfo) || null;
|
|
2919
|
-
if (stateAddress !== null) {
|
|
2920
|
-
return stateAddress;
|
|
3052
|
+
// get-or-create を通すと未登録アドレスに空 Set を生成してしまうため素の get で参照する
|
|
3053
|
+
const bindingSet = bindingSetByAbsoluteStateAddress.get(absoluteStateAddress);
|
|
3054
|
+
if (bindingSet !== undefined) {
|
|
3055
|
+
bindingSet.delete(binding);
|
|
2921
3056
|
}
|
|
2922
|
-
if (bindingInfo.statePathInfo.wildcardCount > 0) {
|
|
2923
|
-
const listIndex = getListIndexByBindingInfo(bindingInfo);
|
|
2924
|
-
if (listIndex === null) {
|
|
2925
|
-
raiseError(`Cannot resolve state address for binding with wildcard statePathName "${bindingInfo.statePathName}" because list index is null.`);
|
|
2926
|
-
}
|
|
2927
|
-
stateAddress = createStateAddress(bindingInfo.statePathInfo, listIndex);
|
|
2928
|
-
}
|
|
2929
|
-
else {
|
|
2930
|
-
stateAddress = createStateAddress(bindingInfo.statePathInfo, null);
|
|
2931
|
-
}
|
|
2932
|
-
stateAddressByBindingInfo.set(bindingInfo, stateAddress);
|
|
2933
|
-
return stateAddress;
|
|
2934
|
-
}
|
|
2935
|
-
// call for change loopContext
|
|
2936
|
-
function clearStateAddressByBindingInfo(bindingInfo) {
|
|
2937
|
-
stateAddressByBindingInfo.delete(bindingInfo);
|
|
2938
3057
|
}
|
|
2939
3058
|
|
|
2940
3059
|
const bindingsByContent = new WeakMap();
|
|
@@ -2983,8 +3102,10 @@ function deactivateContent(content) {
|
|
|
2983
3102
|
for (const binding of bindings) {
|
|
2984
3103
|
const absoluteStateAddress = getAbsoluteStateAddressByBinding(binding);
|
|
2985
3104
|
removeBindingByAbsoluteStateAddress(absoluteStateAddress, binding);
|
|
2986
|
-
|
|
2987
|
-
|
|
3105
|
+
// アドレスキャッシュ(absoluteStateAddressByBinding / stateAddressByBindingInfo)
|
|
3106
|
+
// のクリアはここでは行わない。deactivateContent の呼び出し元(for/if)は必ず
|
|
3107
|
+
// 直後に content.unmount() を呼び、unmount が同じ2台帳をネスト content も含めて
|
|
3108
|
+
// クリアする(createContent.ts)。ここで消すと全 binding で二重 delete になる。
|
|
2988
3109
|
}
|
|
2989
3110
|
unbindLoopContextToContent(content);
|
|
2990
3111
|
}
|
|
@@ -3011,6 +3132,40 @@ function getContentSetByNode(node) {
|
|
|
3011
3132
|
}
|
|
3012
3133
|
return EMPTY_SET;
|
|
3013
3134
|
}
|
|
3135
|
+
function deleteContentByNode(node, content) {
|
|
3136
|
+
const contents = contentSetByNode.get(node);
|
|
3137
|
+
if (contents) {
|
|
3138
|
+
contents.delete(content);
|
|
3139
|
+
if (contents.size === 0) {
|
|
3140
|
+
contentSetByNode.delete(node);
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3145
|
+
const stateAddressByBindingInfo = new WeakMap();
|
|
3146
|
+
function getStateAddressByBindingInfo(bindingInfo) {
|
|
3147
|
+
let stateAddress = null;
|
|
3148
|
+
stateAddress = stateAddressByBindingInfo.get(bindingInfo) || null;
|
|
3149
|
+
if (stateAddress !== null) {
|
|
3150
|
+
return stateAddress;
|
|
3151
|
+
}
|
|
3152
|
+
if (bindingInfo.statePathInfo.wildcardCount > 0) {
|
|
3153
|
+
const listIndex = getListIndexByBindingInfo(bindingInfo);
|
|
3154
|
+
if (listIndex === null) {
|
|
3155
|
+
raiseError(`Cannot resolve state address for binding with wildcard statePathName "${bindingInfo.statePathName}" because list index is null.`);
|
|
3156
|
+
}
|
|
3157
|
+
stateAddress = createStateAddress(bindingInfo.statePathInfo, listIndex);
|
|
3158
|
+
}
|
|
3159
|
+
else {
|
|
3160
|
+
stateAddress = createStateAddress(bindingInfo.statePathInfo, null);
|
|
3161
|
+
}
|
|
3162
|
+
stateAddressByBindingInfo.set(bindingInfo, stateAddress);
|
|
3163
|
+
return stateAddress;
|
|
3164
|
+
}
|
|
3165
|
+
// call for change loopContext
|
|
3166
|
+
function clearStateAddressByBindingInfo(bindingInfo) {
|
|
3167
|
+
stateAddressByBindingInfo.delete(bindingInfo);
|
|
3168
|
+
}
|
|
3014
3169
|
|
|
3015
3170
|
const recursiveBindingTypes = new Set(['if', 'elseif', 'else', 'for']);
|
|
3016
3171
|
class Content {
|
|
@@ -3123,14 +3278,27 @@ function hydrateSetLastNode(node, lastNode) {
|
|
|
3123
3278
|
function getPooledContents(bindingInfo) {
|
|
3124
3279
|
return pooledContentsByNode.get(bindingInfo.node) || [];
|
|
3125
3280
|
}
|
|
3281
|
+
// プールの上限(アンカーごと)。プールはアンカー(文書に永続するコメントノード)
|
|
3282
|
+
// から content とその DOM サブツリー・バインディング群を強参照するため、無制限だと
|
|
3283
|
+
// 大きなリストのクリア後もメモリが解放されない(10k 行で 10MB 級)。上限超過分は
|
|
3284
|
+
// contentSetByNode の台帳からも外して GC 可能にする。再追加時は createContent で
|
|
3285
|
+
// 作り直すコストと引き換えになる。
|
|
3286
|
+
const MAX_POOLED_CONTENTS = 1000;
|
|
3287
|
+
let maxPooledContents = MAX_POOLED_CONTENTS;
|
|
3126
3288
|
function setPooledContent(bindingInfo, content) {
|
|
3127
|
-
|
|
3289
|
+
let contents = pooledContentsByNode.get(bindingInfo.node);
|
|
3128
3290
|
if (typeof contents === 'undefined') {
|
|
3129
|
-
|
|
3291
|
+
contents = [];
|
|
3292
|
+
pooledContentsByNode.set(bindingInfo.node, contents);
|
|
3130
3293
|
}
|
|
3131
|
-
|
|
3294
|
+
if (contents.length < maxPooledContents) {
|
|
3132
3295
|
contents.push(content);
|
|
3133
3296
|
}
|
|
3297
|
+
else {
|
|
3298
|
+
// 上限超過: content を完全に手放す。contentSetByNode は createContent 時に
|
|
3299
|
+
// 追加されたきり解放経路が無いため、ここで外さないと GC できない。
|
|
3300
|
+
deleteContentByNode(bindingInfo.node, content);
|
|
3301
|
+
}
|
|
3134
3302
|
}
|
|
3135
3303
|
function isOnlyNodeInParentContent(firstNode, lastNode) {
|
|
3136
3304
|
let prevCheckNode = firstNode.previousSibling;
|
|
@@ -3154,6 +3322,23 @@ function isOnlyNodeInParentContent(firstNode, lastNode) {
|
|
|
3154
3322
|
}
|
|
3155
3323
|
return onlyNode;
|
|
3156
3324
|
}
|
|
3325
|
+
// A stable content may be left in place only when its first node verifiably
|
|
3326
|
+
// follows the settled walk position in the same tree: the listIndexes ledger
|
|
3327
|
+
// can lag the physical DOM (element-write swaps reorder listIndexes without
|
|
3328
|
+
// moving nodes; hidden regions unmount contents that stay registered). Empty
|
|
3329
|
+
// contents (null firstNode) always take the settle walk so their mount
|
|
3330
|
+
// bookkeeping matches the pre-LIS behavior.
|
|
3331
|
+
function isPhysicallyAfter(lastNode, firstNode) {
|
|
3332
|
+
if (firstNode === null) {
|
|
3333
|
+
return false;
|
|
3334
|
+
}
|
|
3335
|
+
if (lastNode.nextSibling === firstNode) {
|
|
3336
|
+
return true;
|
|
3337
|
+
}
|
|
3338
|
+
const position = lastNode.compareDocumentPosition(firstNode);
|
|
3339
|
+
return (position & Node.DOCUMENT_POSITION_FOLLOWING) !== 0
|
|
3340
|
+
&& (position & Node.DOCUMENT_POSITION_DISCONNECTED) === 0;
|
|
3341
|
+
}
|
|
3157
3342
|
function getContent(node, listIndex) {
|
|
3158
3343
|
let contentByListIndex = contentByListIndexByNode.get(node);
|
|
3159
3344
|
if (typeof contentByListIndex === 'undefined') {
|
|
@@ -3213,6 +3398,11 @@ function applyChangeToFor(bindingInfo, context, newValue) {
|
|
|
3213
3398
|
let lastNode = bindingInfo.node;
|
|
3214
3399
|
const elementPathInfo = getPathInfo(listPathInfo.path + '.' + WILDCARD);
|
|
3215
3400
|
const loopContextStack = context.stateElement.loopContextStack;
|
|
3401
|
+
// When the new order contains inversions, contents in the stable set (an LIS
|
|
3402
|
+
// of old positions) keep their relative order and must not be moved; moving
|
|
3403
|
+
// only the rest avoids the cascade where one swap relocates every row in
|
|
3404
|
+
// between. null = no inversions; the position guard below then does no moves.
|
|
3405
|
+
const stableIndexSet = computeStableIndexSet(diff);
|
|
3216
3406
|
let fragment = null;
|
|
3217
3407
|
if (diff.newIndexes.length == diff.addIndexSet.size
|
|
3218
3408
|
&& diff.newIndexes.length > 0
|
|
@@ -3282,7 +3472,13 @@ function applyChangeToFor(bindingInfo, context, newValue) {
|
|
|
3282
3472
|
if (content === null) {
|
|
3283
3473
|
raiseError(`Content not found for ListIndex: ${index.index} at path "${listPathInfo.path}"`);
|
|
3284
3474
|
}
|
|
3285
|
-
|
|
3475
|
+
// Stable contents are already in correct relative order — but only
|
|
3476
|
+
// trust that after physical verification (see isPhysicallyAfter).
|
|
3477
|
+
// Contents out of order (and everything unverifiable) settle via the
|
|
3478
|
+
// self-healing mountAfter walk below.
|
|
3479
|
+
const stable = stableIndexSet !== null && stableIndexSet.has(index)
|
|
3480
|
+
&& isPhysicallyAfter(lastNode, content.firstNode);
|
|
3481
|
+
if (!stable && lastNode.nextSibling !== content.firstNode) {
|
|
3286
3482
|
content.mountAfter(lastNode);
|
|
3287
3483
|
}
|
|
3288
3484
|
}
|
|
@@ -3611,8 +3807,14 @@ function applyChangeToStyle(binding, _context, newValue) {
|
|
|
3611
3807
|
|
|
3612
3808
|
const ssrWrappedNodes = new WeakSet();
|
|
3613
3809
|
function applyChangeToText(binding, _context, newValue) {
|
|
3614
|
-
|
|
3615
|
-
|
|
3810
|
+
// nodeValue は nullable DOMString(実ブラウザでは null / undefined とも空文字に
|
|
3811
|
+
// 正規化される)ため、比較前に同じ規則で文字列化する。生値のまま比較すると
|
|
3812
|
+
// 数値など非文字列値は常に不一致になり、同値でも毎回 DOM 書き込みが走る。
|
|
3813
|
+
// 注: happy-dom は undefined を "undefined" にする非準拠実装なので String() に
|
|
3814
|
+
// 頼らず明示的に "" へ正規化する。
|
|
3815
|
+
const text = newValue === null || newValue === undefined ? "" : String(newValue);
|
|
3816
|
+
if (binding.replaceNode.nodeValue !== text) {
|
|
3817
|
+
binding.replaceNode.nodeValue = text;
|
|
3616
3818
|
}
|
|
3617
3819
|
// SSR モード時: テキストノードの前後にコメントを挿入して境界を明示
|
|
3618
3820
|
if (inSsr() && !ssrWrappedNodes.has(binding.replaceNode)) {
|
|
@@ -3783,15 +3985,20 @@ function applyChange(binding, context) {
|
|
|
3783
3985
|
return;
|
|
3784
3986
|
}
|
|
3785
3987
|
context.appliedBindingSet.add(binding);
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
absAddress
|
|
3794
|
-
|
|
3988
|
+
// $updatedCallback が定義されていない state では、更新アドレスの集計自体が
|
|
3989
|
+
// 不要(drain 終端の呼び出しごと省略される)。大量バインディング適用時の
|
|
3990
|
+
// Set 蓄積を避ける。undefined(テスト用モック等)は従来通り集計する。
|
|
3991
|
+
if (context.stateElement.hasUpdatedCallback !== false) {
|
|
3992
|
+
const absAddress = getAbsoluteStateAddressByBinding(binding);
|
|
3993
|
+
if (context.updatedAbsAddressSetByStateElement.has(context.stateElement)) {
|
|
3994
|
+
const addressSet = context.updatedAbsAddressSetByStateElement.get(context.stateElement);
|
|
3995
|
+
addressSet.add(absAddress);
|
|
3996
|
+
}
|
|
3997
|
+
else {
|
|
3998
|
+
context.updatedAbsAddressSetByStateElement.set(context.stateElement, new Set([
|
|
3999
|
+
absAddress
|
|
4000
|
+
]));
|
|
4001
|
+
}
|
|
3795
4002
|
}
|
|
3796
4003
|
if (binding.bindingType === "event") {
|
|
3797
4004
|
return;
|
|
@@ -3807,6 +4014,14 @@ function applyChange(binding, context) {
|
|
|
3807
4014
|
return;
|
|
3808
4015
|
}
|
|
3809
4016
|
}
|
|
4017
|
+
// applyChangeFromBindings のグループ化ループが解決済みルートの一致を検証済みの
|
|
4018
|
+
// 場合、stateName さえ一致すれば getRootNode の再解決(native 呼び出し)を省略
|
|
4019
|
+
// できる。activateContent 経由(フラグメント内の新規 content)も、フラグメントは
|
|
4020
|
+
// setRootNodeByFragment で context.rootNode に解決されるため同じ不変条件が成り立つ。
|
|
4021
|
+
if (context.sameRootVerified === true && binding.stateName === context.stateName) {
|
|
4022
|
+
_applyChange(binding, context);
|
|
4023
|
+
return;
|
|
4024
|
+
}
|
|
3810
4025
|
let rootNode = binding.replaceNode.getRootNode();
|
|
3811
4026
|
if (rootNode instanceof DocumentFragment && !(rootNode instanceof ShadowRoot)) {
|
|
3812
4027
|
rootNode = getRootNodeByFragment(rootNode);
|
|
@@ -3887,6 +4102,9 @@ function applyChangeFromBindings(bindings) {
|
|
|
3887
4102
|
newListValueByAbsAddress: newListValueByAbsAddress,
|
|
3888
4103
|
updatedAbsAddressSetByStateElement: updatedAbsAddressSetByStateElement,
|
|
3889
4104
|
deferredSelectBindings: deferredSelectBindings,
|
|
4105
|
+
// グループ内の binding は下の do/while が「解決済みルート === rootNode」を
|
|
4106
|
+
// 検証してから applyChange に渡す(applyChange 側の getRootNode 省略の根拠)
|
|
4107
|
+
sameRootVerified: true,
|
|
3890
4108
|
};
|
|
3891
4109
|
do {
|
|
3892
4110
|
applyChange(binding, context);
|
|
@@ -3917,7 +4135,8 @@ function applyChangeFromBindings(bindings) {
|
|
|
3917
4135
|
}
|
|
3918
4136
|
|
|
3919
4137
|
const handlerByHandlerKey$1 = new Map();
|
|
3920
|
-
|
|
4138
|
+
// binding を強参照しない台帳(handlerBindingRegistry.ts のリーク解説を参照)
|
|
4139
|
+
const bindingRegistry$1 = createHandlerBindingRegistry();
|
|
3921
4140
|
function getHandlerKey$1(binding, eventName) {
|
|
3922
4141
|
const filterKey = binding.inFilters.map(f => f.filterName + '(' + f.args.join(',') + ')').join('|');
|
|
3923
4142
|
return `${binding.stateName}::${binding.statePathName}::${eventName}::${filterKey}`;
|
|
@@ -3971,21 +4190,15 @@ function attachRadioEventHandler(binding) {
|
|
|
3971
4190
|
handlerByHandlerKey$1.set(key, radioEventHandler);
|
|
3972
4191
|
}
|
|
3973
4192
|
binding.node.addEventListener(eventName, radioEventHandler);
|
|
3974
|
-
|
|
3975
|
-
if (typeof bindingSet === "undefined") {
|
|
3976
|
-
bindingSet = new Set([binding]);
|
|
3977
|
-
bindingSetByHandlerKey$1.set(key, bindingSet);
|
|
3978
|
-
}
|
|
3979
|
-
else {
|
|
3980
|
-
bindingSet.add(binding);
|
|
3981
|
-
}
|
|
4193
|
+
bindingRegistry$1.add(key, binding);
|
|
3982
4194
|
return true;
|
|
3983
4195
|
}
|
|
3984
4196
|
return false;
|
|
3985
4197
|
}
|
|
3986
4198
|
|
|
3987
4199
|
const handlerByHandlerKey = new Map();
|
|
3988
|
-
|
|
4200
|
+
// binding を強参照しない台帳(handlerBindingRegistry.ts のリーク解説を参照)
|
|
4201
|
+
const bindingRegistry = createHandlerBindingRegistry();
|
|
3989
4202
|
function getHandlerKey(binding, eventName) {
|
|
3990
4203
|
const filterKey = binding.inFilters.map(f => f.filterName + '(' + f.args.join(',') + ')').join('|');
|
|
3991
4204
|
return `${binding.stateName}::${binding.statePathName}::${eventName}::${filterKey}`;
|
|
@@ -4058,14 +4271,7 @@ function attachCheckboxEventHandler(binding) {
|
|
|
4058
4271
|
handlerByHandlerKey.set(key, checkboxEventHandler);
|
|
4059
4272
|
}
|
|
4060
4273
|
binding.node.addEventListener(eventName, checkboxEventHandler);
|
|
4061
|
-
|
|
4062
|
-
if (typeof bindingSet === "undefined") {
|
|
4063
|
-
bindingSet = new Set([binding]);
|
|
4064
|
-
bindingSetByHandlerKey.set(key, bindingSet);
|
|
4065
|
-
}
|
|
4066
|
-
else {
|
|
4067
|
-
bindingSet.add(binding);
|
|
4068
|
-
}
|
|
4274
|
+
bindingRegistry.add(key, binding);
|
|
4069
4275
|
return true;
|
|
4070
4276
|
}
|
|
4071
4277
|
return false;
|
|
@@ -6096,7 +6302,12 @@ class Updater {
|
|
|
6096
6302
|
const absoluteAddressSet = new Set(absoluteAddresses);
|
|
6097
6303
|
const processBindings = [];
|
|
6098
6304
|
for (const absoluteAddress of absoluteAddressSet) {
|
|
6099
|
-
|
|
6305
|
+
// peek: バインディングの無いアドレス(リスト置換で enqueue される中間
|
|
6306
|
+
// アドレス等)に空 Set を生成・蓄積しない
|
|
6307
|
+
const bindings = peekBindingSetByAbsoluteStateAddress(absoluteAddress);
|
|
6308
|
+
if (bindings === undefined) {
|
|
6309
|
+
continue;
|
|
6310
|
+
}
|
|
6100
6311
|
for (const binding of bindings) {
|
|
6101
6312
|
if (binding.replaceNode.isConnected === false) {
|
|
6102
6313
|
// 切断されているバインディングは無視
|
|
@@ -6892,7 +7103,8 @@ function dirtyCacheEntryByAbsoluteStateAddress(address) {
|
|
|
6892
7103
|
function checkDependency(handler, address) {
|
|
6893
7104
|
// 動的依存関係の登録
|
|
6894
7105
|
if (handler.addressStackLength > 0) {
|
|
6895
|
-
const
|
|
7106
|
+
const lastAddress = handler.lastAddressStack;
|
|
7107
|
+
const lastInfo = lastAddress?.pathInfo ?? null;
|
|
6896
7108
|
const stateElement = handler.stateElement;
|
|
6897
7109
|
if (lastInfo !== null) {
|
|
6898
7110
|
if (stateElement.getterPaths.has(lastInfo.path) &&
|
|
@@ -6900,6 +7112,27 @@ function checkDependency(handler, address) {
|
|
|
6900
7112
|
// lastInfo.pathはgetterの名前であり、address.pathInfo.pathは
|
|
6901
7113
|
// そのgetterが参照している値のパスである
|
|
6902
7114
|
stateElement.addDynamicDependency(address.pathInfo.path, lastInfo.path);
|
|
7115
|
+
// 他行読み取りの検出: 評価中の getter と読み取り先が同じワイルドカード親
|
|
7116
|
+
// (リスト)を共有し、その階層の listIndex が異なる場合、この getter は
|
|
7117
|
+
// 自行の外に依存する(隣接項目参照など)。該当リストを crossRowListPaths に
|
|
7118
|
+
// 記録し、walkDependency の diff-filter 展開を全行展開へフォールバックさせる。
|
|
7119
|
+
if (address.pathInfo.wildcardCount > 0 && lastInfo.wildcardCount > 0) {
|
|
7120
|
+
const sharedLen = calcWildcardLen(address.pathInfo, lastInfo);
|
|
7121
|
+
if (sharedLen > 0) {
|
|
7122
|
+
let crossRow = false;
|
|
7123
|
+
for (let level = 0; level < sharedLen; level++) {
|
|
7124
|
+
if (address.listIndex?.at(level) !== lastAddress.listIndex?.at(level)) {
|
|
7125
|
+
crossRow = true;
|
|
7126
|
+
break;
|
|
7127
|
+
}
|
|
7128
|
+
}
|
|
7129
|
+
if (crossRow) {
|
|
7130
|
+
for (let level = 0; level < sharedLen; level++) {
|
|
7131
|
+
stateElement.addCrossRowListPath?.(address.pathInfo.wildcardParentPaths[level]);
|
|
7132
|
+
}
|
|
7133
|
+
}
|
|
7134
|
+
}
|
|
7135
|
+
}
|
|
6903
7136
|
}
|
|
6904
7137
|
}
|
|
6905
7138
|
}
|
|
@@ -7102,6 +7335,37 @@ function _walkExpandWildcard(context, currentWildcardIndex, parentListIndex) {
|
|
|
7102
7335
|
}
|
|
7103
7336
|
}
|
|
7104
7337
|
}
|
|
7338
|
+
/**
|
|
7339
|
+
* 静的子展開で訪問する listIndex 群を選ぶ。"diff" でも次の場合は全行に倒す:
|
|
7340
|
+
* - diff に変化が一切見えない再代入(同一参照および内容同一コピーの再代入。
|
|
7341
|
+
* `arr[0].v = 5; s.items = [...arr]` のような in-place 変異後のリフレッシュ
|
|
7342
|
+
* イディオムは diff に映らないため、全行展開で従来挙動を保つ。
|
|
7343
|
+
* 削除だけの置換は除く — 残存行に変化は無く、集計はコンテナ動的エッジが担う)
|
|
7344
|
+
* - 他行を読む getter が検出されたリスト(隣接項目参照など。未変更行の派生値も変わりうる)
|
|
7345
|
+
*/
|
|
7346
|
+
function selectExpansionIndexes(context, sourcePath, _lastValue, _newValue, listDiff) {
|
|
7347
|
+
if (context.listExpansion === "full") {
|
|
7348
|
+
return listDiff.newIndexes;
|
|
7349
|
+
}
|
|
7350
|
+
if (context.stateElement.crossRowListPaths?.has(sourcePath)) {
|
|
7351
|
+
return listDiff.newIndexes;
|
|
7352
|
+
}
|
|
7353
|
+
if (listDiff.addIndexSet.size === 0 && listDiff.changeIndexSet.size === 0) {
|
|
7354
|
+
// 追加も移動も無い。削除も無ければ「変化が見えない再代入」= リフレッシュ意図
|
|
7355
|
+
if (listDiff.deleteIndexSet.size === 0) {
|
|
7356
|
+
return listDiff.newIndexes;
|
|
7357
|
+
}
|
|
7358
|
+
// 削除のみ: 残存行は位置も値も不変なので展開しない
|
|
7359
|
+
return listDiff.changeIndexSet;
|
|
7360
|
+
}
|
|
7361
|
+
if (listDiff.addIndexSet.size === 0) {
|
|
7362
|
+
return listDiff.changeIndexSet;
|
|
7363
|
+
}
|
|
7364
|
+
if (listDiff.changeIndexSet.size === 0) {
|
|
7365
|
+
return listDiff.addIndexSet;
|
|
7366
|
+
}
|
|
7367
|
+
return [...listDiff.addIndexSet, ...listDiff.changeIndexSet];
|
|
7368
|
+
}
|
|
7105
7369
|
function _walkDependency(context, startAddress, callback) {
|
|
7106
7370
|
const stack = [{ address: startAddress, depth: 0 }];
|
|
7107
7371
|
while (stack.length > 0) {
|
|
@@ -7134,7 +7398,7 @@ function _walkDependency(context, startAddress, callback) {
|
|
|
7134
7398
|
const absAddress = createAbsoluteStateAddress(absPathInfo, address.listIndex);
|
|
7135
7399
|
const lastValue = getLastListValueByAbsoluteStateAddress(absAddress);
|
|
7136
7400
|
const listDiff = createListDiff(address.listIndex, lastValue, newValue);
|
|
7137
|
-
for (const listIndex of listDiff
|
|
7401
|
+
for (const listIndex of selectExpansionIndexes(context, sourcePath, lastValue, newValue, listDiff)) {
|
|
7138
7402
|
const depAddress = createStateAddress(depPathInfo, listIndex);
|
|
7139
7403
|
context.result.add(depAddress);
|
|
7140
7404
|
nextEntries.push({ address: depAddress, depth: nextDepth });
|
|
@@ -7228,7 +7492,7 @@ function _walkDependency(context, startAddress, callback) {
|
|
|
7228
7492
|
}
|
|
7229
7493
|
}
|
|
7230
7494
|
}
|
|
7231
|
-
function walkDependency(stateName, stateElement, startAddress, staticDependency, dynamicDependency, listPathSet, stateProxy, searchType, callback) {
|
|
7495
|
+
function walkDependency(stateName, stateElement, startAddress, staticDependency, dynamicDependency, listPathSet, stateProxy, searchType, callback, options) {
|
|
7232
7496
|
const context = {
|
|
7233
7497
|
stateElement: stateElement,
|
|
7234
7498
|
staticMap: staticDependency,
|
|
@@ -7238,6 +7502,7 @@ function walkDependency(stateName, stateElement, startAddress, staticDependency,
|
|
|
7238
7502
|
visited: new Set(),
|
|
7239
7503
|
stateProxy: stateProxy,
|
|
7240
7504
|
searchType: searchType,
|
|
7505
|
+
listExpansion: options?.listExpansion ?? "full",
|
|
7241
7506
|
};
|
|
7242
7507
|
_walkDependency(context, startAddress, callback);
|
|
7243
7508
|
return Array.from(context.result);
|
|
@@ -7303,7 +7568,10 @@ function _setByAddress(target, address, absAddress, value, receiver, handler) {
|
|
|
7303
7568
|
dirtyCacheEntryByAbsoluteStateAddress(absDepAddress);
|
|
7304
7569
|
// 更新対象として登録
|
|
7305
7570
|
updater.enqueueAbsoluteAddress(absDepAddress);
|
|
7306
|
-
}
|
|
7571
|
+
},
|
|
7572
|
+
// リスト置換時は追加行・位置変更行のみ展開する(未変更行の再訪を省く。
|
|
7573
|
+
// $postUpdate の手動リフレッシュは従来通り全行展開のまま)
|
|
7574
|
+
{ listExpansion: "diff" });
|
|
7307
7575
|
}
|
|
7308
7576
|
}
|
|
7309
7577
|
function _setByAddressWithSwap(target, address, absAddress, value, receiver, handler) {
|
|
@@ -8382,6 +8650,10 @@ class State extends HTMLElement {
|
|
|
8382
8650
|
return getBindingsReady(rootNode);
|
|
8383
8651
|
}
|
|
8384
8652
|
__state;
|
|
8653
|
+
_hasUpdatedCallback = false;
|
|
8654
|
+
// 他行を読む getter が検出されたリストパス(diff-filter 展開の全行フォールバック対象)。
|
|
8655
|
+
// 依存マップ(static/dynamic)と同様に追加のみ・クリアしない(安全側に固定される)。
|
|
8656
|
+
_crossRowListPaths = new Set();
|
|
8385
8657
|
_name = 'default';
|
|
8386
8658
|
_initialized = false;
|
|
8387
8659
|
_initializePromise;
|
|
@@ -8443,6 +8715,13 @@ class State extends HTMLElement {
|
|
|
8443
8715
|
this._commandTokenNames = processCommandTokensDeclaration(value);
|
|
8444
8716
|
this._eventTokenNames = processEventTokensDeclaration(value);
|
|
8445
8717
|
this.__state = value;
|
|
8718
|
+
// $updatedCallback の有無を state セット時に確定しておく(in はプロトタイプ
|
|
8719
|
+
// チェーンも見る・getter を評価しない)。drain 側はこのフラグで更新アドレスの
|
|
8720
|
+
// 集計と writable createState をスキップできる。
|
|
8721
|
+
// 注: state セット後に生オブジェクトへ直接 $updatedCallback を後付けする
|
|
8722
|
+
// パターンは検知できない(bindProperty / _state 再セットは検知する)。
|
|
8723
|
+
// ライフサイクルフックは宣言時に定義するのが規約。
|
|
8724
|
+
this._hasUpdatedCallback = STATE_UPDATED_CALLBACK_NAME in value;
|
|
8446
8725
|
// 再 set 時に二重 subscribe しないよう registry をクリアしてから $on を配線し直す。
|
|
8447
8726
|
clearEventTokenRegistry(this);
|
|
8448
8727
|
processOnDeclaration(this, value, this._eventTokenNames);
|
|
@@ -8865,8 +9144,20 @@ class State extends HTMLElement {
|
|
|
8865
9144
|
this._version++;
|
|
8866
9145
|
return this._version;
|
|
8867
9146
|
}
|
|
9147
|
+
get hasUpdatedCallback() {
|
|
9148
|
+
return this._hasUpdatedCallback;
|
|
9149
|
+
}
|
|
9150
|
+
get crossRowListPaths() {
|
|
9151
|
+
return this._crossRowListPaths;
|
|
9152
|
+
}
|
|
9153
|
+
addCrossRowListPath(path) {
|
|
9154
|
+
this._crossRowListPaths.add(path);
|
|
9155
|
+
}
|
|
8868
9156
|
bindProperty(prop, desc) {
|
|
8869
9157
|
Object.defineProperty(this._state, prop, desc);
|
|
9158
|
+
if (prop === STATE_UPDATED_CALLBACK_NAME) {
|
|
9159
|
+
this._hasUpdatedCallback = true;
|
|
9160
|
+
}
|
|
8870
9161
|
}
|
|
8871
9162
|
setInitialState(state) {
|
|
8872
9163
|
if (!this._initialized) {
|