@wcstack/state 1.18.0 → 1.19.1
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 +444 -111
- 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
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
let _inSsrCache = null;
|
|
2
1
|
function inSsr() {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
// キャッシュしない: SSR モードはプロセスの属性ではなく「現在の document」の
|
|
3
|
+
// 属性。@wcstack/server はグローバル document を差し替えてサーバーレンダリング
|
|
4
|
+
// した後、同一プロセスでクライアント側ハイドレーションが走る(SSR→hydrate の
|
|
5
|
+
// e2e が該当)。サーバーフェーズの判定をキャッシュするとクライアントフェーズが
|
|
6
|
+
// SSR モード扱いになり、hydrateBindings の代わりに buildBindings が選ばれて
|
|
7
|
+
// connectedCallbackPromise が永久に未解決になる。
|
|
8
|
+
const html = document.documentElement;
|
|
9
|
+
return html ? html.hasAttribute('data-wcs-server') : false;
|
|
8
10
|
}
|
|
9
11
|
const _config = {
|
|
10
12
|
bindAttributeName: 'data-wcs',
|
|
@@ -63,7 +65,7 @@ function setConfig(partialConfig) {
|
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
var version$1 = "1.
|
|
68
|
+
var version$1 = "1.19.1";
|
|
67
69
|
var pkg = {
|
|
68
70
|
version: version$1};
|
|
69
71
|
|
|
@@ -1846,13 +1848,62 @@ const connectedCallbackSymbol = Symbol("$$connectedCallback");
|
|
|
1846
1848
|
const disconnectedCallbackSymbol = Symbol("$$disconnectedCallback");
|
|
1847
1849
|
const updatedCallbackSymbol = Symbol("$$updatedCallback");
|
|
1848
1850
|
|
|
1851
|
+
function createHandlerBindingRegistry() {
|
|
1852
|
+
const attachedByKey = new Map();
|
|
1853
|
+
const countByKey = new Map();
|
|
1854
|
+
return {
|
|
1855
|
+
add(key, binding) {
|
|
1856
|
+
let attached = attachedByKey.get(key);
|
|
1857
|
+
if (typeof attached === "undefined") {
|
|
1858
|
+
attached = new WeakSet();
|
|
1859
|
+
attachedByKey.set(key, attached);
|
|
1860
|
+
}
|
|
1861
|
+
if (attached.has(binding)) {
|
|
1862
|
+
return false;
|
|
1863
|
+
}
|
|
1864
|
+
attached.add(binding);
|
|
1865
|
+
countByKey.set(key, (countByKey.get(key) ?? 0) + 1);
|
|
1866
|
+
return true;
|
|
1867
|
+
},
|
|
1868
|
+
remove(key, binding) {
|
|
1869
|
+
const attached = attachedByKey.get(key);
|
|
1870
|
+
if (typeof attached === "undefined" || !attached.has(binding)) {
|
|
1871
|
+
return false;
|
|
1872
|
+
}
|
|
1873
|
+
attached.delete(binding);
|
|
1874
|
+
const next = (countByKey.get(key) ?? 1) - 1;
|
|
1875
|
+
if (next <= 0) {
|
|
1876
|
+
attachedByKey.delete(key);
|
|
1877
|
+
countByKey.delete(key);
|
|
1878
|
+
return true;
|
|
1879
|
+
}
|
|
1880
|
+
countByKey.set(key, next);
|
|
1881
|
+
return false;
|
|
1882
|
+
},
|
|
1883
|
+
has(key, binding) {
|
|
1884
|
+
return attachedByKey.get(key)?.has(binding) ?? false;
|
|
1885
|
+
},
|
|
1886
|
+
countOf(key) {
|
|
1887
|
+
return countByKey.get(key) ?? 0;
|
|
1888
|
+
},
|
|
1889
|
+
get keyCount() {
|
|
1890
|
+
return countByKey.size;
|
|
1891
|
+
},
|
|
1892
|
+
clear() {
|
|
1893
|
+
attachedByKey.clear();
|
|
1894
|
+
countByKey.clear();
|
|
1895
|
+
},
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1849
1899
|
// onclick: $command.<name> のように、DOM イベントから command token を直接 emit する形式かを判定する。
|
|
1850
1900
|
// 右辺が $command 名前空間配下のパス($command.<token>)のときに true。
|
|
1851
1901
|
function isCommandTokenPath(statePathName) {
|
|
1852
1902
|
return statePathName.startsWith(STATE_COMMAND_NAMESPACE_NAME + ".");
|
|
1853
1903
|
}
|
|
1854
1904
|
const handlerByHandlerKey$3 = new Map();
|
|
1855
|
-
|
|
1905
|
+
// binding を強参照しない台帳(handlerBindingRegistry.ts のリーク解説を参照)
|
|
1906
|
+
const bindingRegistry$3 = createHandlerBindingRegistry();
|
|
1856
1907
|
function getHandlerKey$3(binding) {
|
|
1857
1908
|
const modifierKey = binding.propModifiers.filter(m => m === 'prevent' || m === 'stop').sort().join(',');
|
|
1858
1909
|
return `${binding.stateName}::${binding.statePathName}::${modifierKey}`;
|
|
@@ -1901,14 +1952,7 @@ function attachEventHandler(binding) {
|
|
|
1901
1952
|
}
|
|
1902
1953
|
const eventName = binding.propName.slice(2);
|
|
1903
1954
|
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
|
-
}
|
|
1955
|
+
bindingRegistry$3.add(key, binding);
|
|
1912
1956
|
return true;
|
|
1913
1957
|
}
|
|
1914
1958
|
|
|
@@ -2078,7 +2122,8 @@ function isPossibleTwoWay(node, propName) {
|
|
|
2078
2122
|
}
|
|
2079
2123
|
|
|
2080
2124
|
const handlerByHandlerKey$2 = new Map();
|
|
2081
|
-
|
|
2125
|
+
// binding を強参照しない台帳(handlerBindingRegistry.ts のリーク解説を参照)
|
|
2126
|
+
const bindingRegistry$2 = createHandlerBindingRegistry();
|
|
2082
2127
|
const DEFAULT_GETTER = (e) => e.detail;
|
|
2083
2128
|
function getHandlerKey$2(binding, eventName, hasGetter) {
|
|
2084
2129
|
const filterKey = binding.inFilters.map(f => f.filterName + '(' + f.args.join(',') + ')').join('|');
|
|
@@ -2181,14 +2226,7 @@ function attachTwowayEventHandler(binding) {
|
|
|
2181
2226
|
handlerByHandlerKey$2.set(key, twowayEventHandler);
|
|
2182
2227
|
}
|
|
2183
2228
|
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
|
-
}
|
|
2229
|
+
bindingRegistry$2.add(key, binding);
|
|
2192
2230
|
}
|
|
2193
2231
|
}
|
|
2194
2232
|
|
|
@@ -2752,6 +2790,20 @@ function isSameList(oldList, newList) {
|
|
|
2752
2790
|
}
|
|
2753
2791
|
return true;
|
|
2754
2792
|
}
|
|
2793
|
+
/**
|
|
2794
|
+
* Aligns each list index's .index with its position in the new list.
|
|
2795
|
+
* A diff only becomes the rendered state once the updater applies it: an
|
|
2796
|
+
* earlier diff in the same batch (two replacements in one microtask) may have
|
|
2797
|
+
* moved shared indexes toward a list that never got applied, and a cache hit
|
|
2798
|
+
* skips recomputation entirely — so every createListDiff return re-aligns.
|
|
2799
|
+
*/
|
|
2800
|
+
function syncListIndexes(newIndexes) {
|
|
2801
|
+
for (let i = 0; i < newIndexes.length; i++) {
|
|
2802
|
+
if (newIndexes[i].index !== i) {
|
|
2803
|
+
newIndexes[i].index = i;
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2755
2807
|
/**
|
|
2756
2808
|
* Creates or updates list indexes by comparing old and new lists.
|
|
2757
2809
|
* Optimizes by reusing existing list indexes when values match.
|
|
@@ -2762,6 +2814,11 @@ function isSameList(oldList, newList) {
|
|
|
2762
2814
|
* @returns Array of list indexes for the new list
|
|
2763
2815
|
*/
|
|
2764
2816
|
function createListDiff(parentListIndex, rawOldList, rawNewList) {
|
|
2817
|
+
const diff = computeListDiff(parentListIndex, rawOldList, rawNewList);
|
|
2818
|
+
syncListIndexes(diff.newIndexes);
|
|
2819
|
+
return diff;
|
|
2820
|
+
}
|
|
2821
|
+
function computeListDiff(parentListIndex, rawOldList, rawNewList) {
|
|
2765
2822
|
// Normalize inputs to arrays (handles null/undefined)
|
|
2766
2823
|
const oldList = (Array.isArray(rawOldList) && rawOldList.length > 0) ? rawOldList : EMPTY_LIST;
|
|
2767
2824
|
const newList = (Array.isArray(rawNewList) && rawNewList.length > 0) ? rawNewList : EMPTY_LIST;
|
|
@@ -2810,6 +2867,10 @@ function createListDiff(parentListIndex, rawOldList, rawNewList) {
|
|
|
2810
2867
|
addIndexSet: EMPTY_SET$1,
|
|
2811
2868
|
};
|
|
2812
2869
|
}
|
|
2870
|
+
if (newIndexes !== null) {
|
|
2871
|
+
return calcDiffIndexes(oldIndexes, newIndexes);
|
|
2872
|
+
}
|
|
2873
|
+
newIndexes = [];
|
|
2813
2874
|
// Use index-based map for efficiency
|
|
2814
2875
|
// Supports duplicate values by storing array of indexes
|
|
2815
2876
|
const indexByValue = new Map();
|
|
@@ -2822,10 +2883,6 @@ function createListDiff(parentListIndex, rawOldList, rawNewList) {
|
|
|
2822
2883
|
}
|
|
2823
2884
|
indexes.push(i);
|
|
2824
2885
|
}
|
|
2825
|
-
if (newIndexes !== null) {
|
|
2826
|
-
return calcDiffIndexes(oldList, newList, oldIndexes, newIndexes, indexByValue);
|
|
2827
|
-
}
|
|
2828
|
-
newIndexes = [];
|
|
2829
2886
|
// Build new indexes array by matching values with old list
|
|
2830
2887
|
const changeIndexSet = new Set();
|
|
2831
2888
|
const addIndexSet = new Set();
|
|
@@ -2842,9 +2899,11 @@ function createListDiff(parentListIndex, rawOldList, rawNewList) {
|
|
|
2842
2899
|
else {
|
|
2843
2900
|
// Reuse existing element
|
|
2844
2901
|
const existingListIndex = oldIndexes[oldIndex];
|
|
2845
|
-
//
|
|
2846
|
-
|
|
2847
|
-
|
|
2902
|
+
// Judge position change against the old list's order (oldIndexes array
|
|
2903
|
+
// order), not the mutable .index — an earlier diff in the same batch
|
|
2904
|
+
// may have already moved .index toward a list that was never applied.
|
|
2905
|
+
// The .index itself is re-aligned by syncListIndexes on return.
|
|
2906
|
+
if (oldIndex !== i) {
|
|
2848
2907
|
changeIndexSet.add(existingListIndex);
|
|
2849
2908
|
}
|
|
2850
2909
|
newIndexes.push(existingListIndex);
|
|
@@ -2866,22 +2925,35 @@ function createListDiff(parentListIndex, rawOldList, rawNewList) {
|
|
|
2866
2925
|
}
|
|
2867
2926
|
}
|
|
2868
2927
|
}
|
|
2869
|
-
|
|
2928
|
+
/**
|
|
2929
|
+
* Diff between two lists whose listIndex ledgers both already exist.
|
|
2930
|
+
* Rows are joined by listIndex identity — the same key applyChangeToFor and
|
|
2931
|
+
* walkDependency consume the result sets with. A value-based join could mark
|
|
2932
|
+
* oldIndexes-side objects that are absent from newIndexes (ledgers built along
|
|
2933
|
+
* unconnected diff chains hold different objects for the same value); such
|
|
2934
|
+
* orphan markers never match the consumers' has() lookups and only pollute
|
|
2935
|
+
* the dirty set. Rows without shared identity are represented as add+delete.
|
|
2936
|
+
*/
|
|
2937
|
+
function calcDiffIndexes(oldIndexes, newIndexes) {
|
|
2870
2938
|
const newIndexSet = new Set(newIndexes);
|
|
2871
2939
|
const oldIndexSet = new Set(oldIndexes);
|
|
2872
2940
|
const changeIndexSet = new Set();
|
|
2873
2941
|
const addIndexSet = newIndexSet.difference(oldIndexSet);
|
|
2874
2942
|
const deleteIndexSet = oldIndexSet.difference(newIndexSet);
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2943
|
+
// Old positions come from the oldIndexes array order (.index may have been
|
|
2944
|
+
// mutated by an unapplied diff in the same batch).
|
|
2945
|
+
const oldPosByIndex = new Map();
|
|
2946
|
+
for (let i = 0; i < oldIndexes.length; i++) {
|
|
2947
|
+
oldPosByIndex.set(oldIndexes[i], i);
|
|
2948
|
+
}
|
|
2949
|
+
for (let i = 0; i < newIndexes.length; i++) {
|
|
2950
|
+
const index = newIndexes[i];
|
|
2951
|
+
if (addIndexSet.has(index)) {
|
|
2952
|
+
continue;
|
|
2953
|
+
}
|
|
2954
|
+
if (oldPosByIndex.get(index) !== i) {
|
|
2955
|
+
// 位置が違うことだけを記録
|
|
2956
|
+
changeIndexSet.add(index);
|
|
2885
2957
|
}
|
|
2886
2958
|
}
|
|
2887
2959
|
return {
|
|
@@ -2893,6 +2965,103 @@ function calcDiffIndexes(oldList, newList, oldIndexes, newIndexes, indexByValue)
|
|
|
2893
2965
|
};
|
|
2894
2966
|
}
|
|
2895
2967
|
|
|
2968
|
+
/**
|
|
2969
|
+
* Indices into `seq` whose values form a longest strictly-increasing
|
|
2970
|
+
* subsequence, returned in ascending order. Classic patience-sorting
|
|
2971
|
+
* LIS in O(n log n). `seq` values are assumed distinct (old list
|
|
2972
|
+
* positions are unique).
|
|
2973
|
+
*/
|
|
2974
|
+
function longestIncreasingSubsequence(seq) {
|
|
2975
|
+
const n = seq.length;
|
|
2976
|
+
// tails[k] = index into seq of the smallest tail of an increasing
|
|
2977
|
+
// subsequence of length k+1; prev[i] = predecessor index to rebuild the chain.
|
|
2978
|
+
const tails = [];
|
|
2979
|
+
const prev = new Array(n).fill(-1);
|
|
2980
|
+
for (let i = 0; i < n; i++) {
|
|
2981
|
+
const value = seq[i];
|
|
2982
|
+
let lo = 0;
|
|
2983
|
+
let hi = tails.length;
|
|
2984
|
+
while (lo < hi) {
|
|
2985
|
+
const mid = (lo + hi) >> 1;
|
|
2986
|
+
if (seq[tails[mid]] < value) {
|
|
2987
|
+
lo = mid + 1;
|
|
2988
|
+
}
|
|
2989
|
+
else {
|
|
2990
|
+
hi = mid;
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
if (lo > 0) {
|
|
2994
|
+
prev[i] = tails[lo - 1];
|
|
2995
|
+
}
|
|
2996
|
+
tails[lo] = i;
|
|
2997
|
+
}
|
|
2998
|
+
const result = [];
|
|
2999
|
+
let k = tails.length > 0 ? tails[tails.length - 1] : -1;
|
|
3000
|
+
while (k >= 0) {
|
|
3001
|
+
result.push(k);
|
|
3002
|
+
k = prev[k];
|
|
3003
|
+
}
|
|
3004
|
+
result.reverse();
|
|
3005
|
+
return result;
|
|
3006
|
+
}
|
|
3007
|
+
/**
|
|
3008
|
+
* Determines which reused list indexes can stay where they are when the DOM
|
|
3009
|
+
* is brought into the new list order.
|
|
3010
|
+
*
|
|
3011
|
+
* Returns null when the reused indexes already appear in their old relative
|
|
3012
|
+
* order (no inversions) — the caller's existing position guard then performs
|
|
3013
|
+
* no moves, so nothing extra is needed. When inversions exist, returns the
|
|
3014
|
+
* set of indexes forming a longest increasing subsequence of old positions:
|
|
3015
|
+
* leaving exactly those in place and moving every other content yields the
|
|
3016
|
+
* correct final order with the fewest content moves (the naive forward walk
|
|
3017
|
+
* otherwise cascades: a single swap of rows 2/999 in 1000 rows moves ~997
|
|
3018
|
+
* contents instead of 2).
|
|
3019
|
+
*
|
|
3020
|
+
* Note: IListIndex.index is already mutated to the NEW position by
|
|
3021
|
+
* createListDiff, so old positions must come from the oldIndexes array order.
|
|
3022
|
+
*/
|
|
3023
|
+
function computeStableIndexSet(diff) {
|
|
3024
|
+
// No reused index changed position, or nothing was reused: relative order
|
|
3025
|
+
// is already correct and the walk performs no moves.
|
|
3026
|
+
if (diff.changeIndexSet.size === 0 || diff.addIndexSet.size === diff.newIndexes.length) {
|
|
3027
|
+
return null;
|
|
3028
|
+
}
|
|
3029
|
+
const oldPosByIndex = new Map();
|
|
3030
|
+
for (let i = 0; i < diff.oldIndexes.length; i++) {
|
|
3031
|
+
oldPosByIndex.set(diff.oldIndexes[i], i);
|
|
3032
|
+
}
|
|
3033
|
+
const reused = [];
|
|
3034
|
+
const seq = [];
|
|
3035
|
+
let prevPos = -1;
|
|
3036
|
+
let sorted = true;
|
|
3037
|
+
for (const index of diff.newIndexes) {
|
|
3038
|
+
if (diff.addIndexSet.has(index)) {
|
|
3039
|
+
continue;
|
|
3040
|
+
}
|
|
3041
|
+
const pos = oldPosByIndex.get(index);
|
|
3042
|
+
if (pos === undefined) {
|
|
3043
|
+
// Invariant break (a reused index missing from oldIndexes): fall back
|
|
3044
|
+
// to the settle walk rather than compute a stable set from bad data.
|
|
3045
|
+
return null;
|
|
3046
|
+
}
|
|
3047
|
+
if (pos < prevPos) {
|
|
3048
|
+
sorted = false;
|
|
3049
|
+
}
|
|
3050
|
+
prevPos = pos;
|
|
3051
|
+
reused.push(index);
|
|
3052
|
+
seq.push(pos);
|
|
3053
|
+
}
|
|
3054
|
+
if (sorted) {
|
|
3055
|
+
return null;
|
|
3056
|
+
}
|
|
3057
|
+
const lis = longestIncreasingSubsequence(seq);
|
|
3058
|
+
const stable = new Set();
|
|
3059
|
+
for (const seqIndex of lis) {
|
|
3060
|
+
stable.add(reused[seqIndex]);
|
|
3061
|
+
}
|
|
3062
|
+
return stable;
|
|
3063
|
+
}
|
|
3064
|
+
|
|
2896
3065
|
const bindingSetByAbsoluteStateAddress = new WeakMap();
|
|
2897
3066
|
function getBindingSetByAbsoluteStateAddress(absoluteStateAddress) {
|
|
2898
3067
|
let bindingSet = null;
|
|
@@ -2903,38 +3072,24 @@ function getBindingSetByAbsoluteStateAddress(absoluteStateAddress) {
|
|
|
2903
3072
|
}
|
|
2904
3073
|
return bindingSet;
|
|
2905
3074
|
}
|
|
3075
|
+
/**
|
|
3076
|
+
* 参照専用の取得。get-or-create と違い、未登録アドレスに空 Set を
|
|
3077
|
+
* 生成・キャッシュしない(リスト置換の drain は大量のバインディング無し
|
|
3078
|
+
* アドレスを照会するため、生成すると空 Set が溜まり続ける)。
|
|
3079
|
+
*/
|
|
3080
|
+
function peekBindingSetByAbsoluteStateAddress(absoluteStateAddress) {
|
|
3081
|
+
return bindingSetByAbsoluteStateAddress.get(absoluteStateAddress);
|
|
3082
|
+
}
|
|
2906
3083
|
function addBindingByAbsoluteStateAddress(absoluteStateAddress, binding) {
|
|
2907
3084
|
const bindingSet = getBindingSetByAbsoluteStateAddress(absoluteStateAddress);
|
|
2908
3085
|
bindingSet.add(binding);
|
|
2909
3086
|
}
|
|
2910
3087
|
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;
|
|
3088
|
+
// get-or-create を通すと未登録アドレスに空 Set を生成してしまうため素の get で参照する
|
|
3089
|
+
const bindingSet = bindingSetByAbsoluteStateAddress.get(absoluteStateAddress);
|
|
3090
|
+
if (bindingSet !== undefined) {
|
|
3091
|
+
bindingSet.delete(binding);
|
|
2921
3092
|
}
|
|
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
3093
|
}
|
|
2939
3094
|
|
|
2940
3095
|
const bindingsByContent = new WeakMap();
|
|
@@ -2983,8 +3138,10 @@ function deactivateContent(content) {
|
|
|
2983
3138
|
for (const binding of bindings) {
|
|
2984
3139
|
const absoluteStateAddress = getAbsoluteStateAddressByBinding(binding);
|
|
2985
3140
|
removeBindingByAbsoluteStateAddress(absoluteStateAddress, binding);
|
|
2986
|
-
|
|
2987
|
-
|
|
3141
|
+
// アドレスキャッシュ(absoluteStateAddressByBinding / stateAddressByBindingInfo)
|
|
3142
|
+
// のクリアはここでは行わない。deactivateContent の呼び出し元(for/if)は必ず
|
|
3143
|
+
// 直後に content.unmount() を呼び、unmount が同じ2台帳をネスト content も含めて
|
|
3144
|
+
// クリアする(createContent.ts)。ここで消すと全 binding で二重 delete になる。
|
|
2988
3145
|
}
|
|
2989
3146
|
unbindLoopContextToContent(content);
|
|
2990
3147
|
}
|
|
@@ -3011,6 +3168,40 @@ function getContentSetByNode(node) {
|
|
|
3011
3168
|
}
|
|
3012
3169
|
return EMPTY_SET;
|
|
3013
3170
|
}
|
|
3171
|
+
function deleteContentByNode(node, content) {
|
|
3172
|
+
const contents = contentSetByNode.get(node);
|
|
3173
|
+
if (contents) {
|
|
3174
|
+
contents.delete(content);
|
|
3175
|
+
if (contents.size === 0) {
|
|
3176
|
+
contentSetByNode.delete(node);
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
const stateAddressByBindingInfo = new WeakMap();
|
|
3182
|
+
function getStateAddressByBindingInfo(bindingInfo) {
|
|
3183
|
+
let stateAddress = null;
|
|
3184
|
+
stateAddress = stateAddressByBindingInfo.get(bindingInfo) || null;
|
|
3185
|
+
if (stateAddress !== null) {
|
|
3186
|
+
return stateAddress;
|
|
3187
|
+
}
|
|
3188
|
+
if (bindingInfo.statePathInfo.wildcardCount > 0) {
|
|
3189
|
+
const listIndex = getListIndexByBindingInfo(bindingInfo);
|
|
3190
|
+
if (listIndex === null) {
|
|
3191
|
+
raiseError(`Cannot resolve state address for binding with wildcard statePathName "${bindingInfo.statePathName}" because list index is null.`);
|
|
3192
|
+
}
|
|
3193
|
+
stateAddress = createStateAddress(bindingInfo.statePathInfo, listIndex);
|
|
3194
|
+
}
|
|
3195
|
+
else {
|
|
3196
|
+
stateAddress = createStateAddress(bindingInfo.statePathInfo, null);
|
|
3197
|
+
}
|
|
3198
|
+
stateAddressByBindingInfo.set(bindingInfo, stateAddress);
|
|
3199
|
+
return stateAddress;
|
|
3200
|
+
}
|
|
3201
|
+
// call for change loopContext
|
|
3202
|
+
function clearStateAddressByBindingInfo(bindingInfo) {
|
|
3203
|
+
stateAddressByBindingInfo.delete(bindingInfo);
|
|
3204
|
+
}
|
|
3014
3205
|
|
|
3015
3206
|
const recursiveBindingTypes = new Set(['if', 'elseif', 'else', 'for']);
|
|
3016
3207
|
class Content {
|
|
@@ -3123,14 +3314,27 @@ function hydrateSetLastNode(node, lastNode) {
|
|
|
3123
3314
|
function getPooledContents(bindingInfo) {
|
|
3124
3315
|
return pooledContentsByNode.get(bindingInfo.node) || [];
|
|
3125
3316
|
}
|
|
3317
|
+
// プールの上限(アンカーごと)。プールはアンカー(文書に永続するコメントノード)
|
|
3318
|
+
// から content とその DOM サブツリー・バインディング群を強参照するため、無制限だと
|
|
3319
|
+
// 大きなリストのクリア後もメモリが解放されない(10k 行で 10MB 級)。上限超過分は
|
|
3320
|
+
// contentSetByNode の台帳からも外して GC 可能にする。再追加時は createContent で
|
|
3321
|
+
// 作り直すコストと引き換えになる。
|
|
3322
|
+
const MAX_POOLED_CONTENTS = 1000;
|
|
3323
|
+
let maxPooledContents = MAX_POOLED_CONTENTS;
|
|
3126
3324
|
function setPooledContent(bindingInfo, content) {
|
|
3127
|
-
|
|
3325
|
+
let contents = pooledContentsByNode.get(bindingInfo.node);
|
|
3128
3326
|
if (typeof contents === 'undefined') {
|
|
3129
|
-
|
|
3327
|
+
contents = [];
|
|
3328
|
+
pooledContentsByNode.set(bindingInfo.node, contents);
|
|
3130
3329
|
}
|
|
3131
|
-
|
|
3330
|
+
if (contents.length < maxPooledContents) {
|
|
3132
3331
|
contents.push(content);
|
|
3133
3332
|
}
|
|
3333
|
+
else {
|
|
3334
|
+
// 上限超過: content を完全に手放す。contentSetByNode は createContent 時に
|
|
3335
|
+
// 追加されたきり解放経路が無いため、ここで外さないと GC できない。
|
|
3336
|
+
deleteContentByNode(bindingInfo.node, content);
|
|
3337
|
+
}
|
|
3134
3338
|
}
|
|
3135
3339
|
function isOnlyNodeInParentContent(firstNode, lastNode) {
|
|
3136
3340
|
let prevCheckNode = firstNode.previousSibling;
|
|
@@ -3154,6 +3358,23 @@ function isOnlyNodeInParentContent(firstNode, lastNode) {
|
|
|
3154
3358
|
}
|
|
3155
3359
|
return onlyNode;
|
|
3156
3360
|
}
|
|
3361
|
+
// A stable content may be left in place only when its first node verifiably
|
|
3362
|
+
// follows the settled walk position in the same tree: the listIndexes ledger
|
|
3363
|
+
// can lag the physical DOM (element-write swaps reorder listIndexes without
|
|
3364
|
+
// moving nodes; hidden regions unmount contents that stay registered). Empty
|
|
3365
|
+
// contents (null firstNode) always take the settle walk so their mount
|
|
3366
|
+
// bookkeeping matches the pre-LIS behavior.
|
|
3367
|
+
function isPhysicallyAfter(lastNode, firstNode) {
|
|
3368
|
+
if (firstNode === null) {
|
|
3369
|
+
return false;
|
|
3370
|
+
}
|
|
3371
|
+
if (lastNode.nextSibling === firstNode) {
|
|
3372
|
+
return true;
|
|
3373
|
+
}
|
|
3374
|
+
const position = lastNode.compareDocumentPosition(firstNode);
|
|
3375
|
+
return (position & Node.DOCUMENT_POSITION_FOLLOWING) !== 0
|
|
3376
|
+
&& (position & Node.DOCUMENT_POSITION_DISCONNECTED) === 0;
|
|
3377
|
+
}
|
|
3157
3378
|
function getContent(node, listIndex) {
|
|
3158
3379
|
let contentByListIndex = contentByListIndexByNode.get(node);
|
|
3159
3380
|
if (typeof contentByListIndex === 'undefined') {
|
|
@@ -3213,6 +3434,11 @@ function applyChangeToFor(bindingInfo, context, newValue) {
|
|
|
3213
3434
|
let lastNode = bindingInfo.node;
|
|
3214
3435
|
const elementPathInfo = getPathInfo(listPathInfo.path + '.' + WILDCARD);
|
|
3215
3436
|
const loopContextStack = context.stateElement.loopContextStack;
|
|
3437
|
+
// When the new order contains inversions, contents in the stable set (an LIS
|
|
3438
|
+
// of old positions) keep their relative order and must not be moved; moving
|
|
3439
|
+
// only the rest avoids the cascade where one swap relocates every row in
|
|
3440
|
+
// between. null = no inversions; the position guard below then does no moves.
|
|
3441
|
+
const stableIndexSet = computeStableIndexSet(diff);
|
|
3216
3442
|
let fragment = null;
|
|
3217
3443
|
if (diff.newIndexes.length == diff.addIndexSet.size
|
|
3218
3444
|
&& diff.newIndexes.length > 0
|
|
@@ -3282,7 +3508,13 @@ function applyChangeToFor(bindingInfo, context, newValue) {
|
|
|
3282
3508
|
if (content === null) {
|
|
3283
3509
|
raiseError(`Content not found for ListIndex: ${index.index} at path "${listPathInfo.path}"`);
|
|
3284
3510
|
}
|
|
3285
|
-
|
|
3511
|
+
// Stable contents are already in correct relative order — but only
|
|
3512
|
+
// trust that after physical verification (see isPhysicallyAfter).
|
|
3513
|
+
// Contents out of order (and everything unverifiable) settle via the
|
|
3514
|
+
// self-healing mountAfter walk below.
|
|
3515
|
+
const stable = stableIndexSet !== null && stableIndexSet.has(index)
|
|
3516
|
+
&& isPhysicallyAfter(lastNode, content.firstNode);
|
|
3517
|
+
if (!stable && lastNode.nextSibling !== content.firstNode) {
|
|
3286
3518
|
content.mountAfter(lastNode);
|
|
3287
3519
|
}
|
|
3288
3520
|
}
|
|
@@ -3611,8 +3843,14 @@ function applyChangeToStyle(binding, _context, newValue) {
|
|
|
3611
3843
|
|
|
3612
3844
|
const ssrWrappedNodes = new WeakSet();
|
|
3613
3845
|
function applyChangeToText(binding, _context, newValue) {
|
|
3614
|
-
|
|
3615
|
-
|
|
3846
|
+
// nodeValue は nullable DOMString(実ブラウザでは null / undefined とも空文字に
|
|
3847
|
+
// 正規化される)ため、比較前に同じ規則で文字列化する。生値のまま比較すると
|
|
3848
|
+
// 数値など非文字列値は常に不一致になり、同値でも毎回 DOM 書き込みが走る。
|
|
3849
|
+
// 注: happy-dom は undefined を "undefined" にする非準拠実装なので String() に
|
|
3850
|
+
// 頼らず明示的に "" へ正規化する。
|
|
3851
|
+
const text = newValue === null || newValue === undefined ? "" : String(newValue);
|
|
3852
|
+
if (binding.replaceNode.nodeValue !== text) {
|
|
3853
|
+
binding.replaceNode.nodeValue = text;
|
|
3616
3854
|
}
|
|
3617
3855
|
// SSR モード時: テキストノードの前後にコメントを挿入して境界を明示
|
|
3618
3856
|
if (inSsr() && !ssrWrappedNodes.has(binding.replaceNode)) {
|
|
@@ -3783,15 +4021,20 @@ function applyChange(binding, context) {
|
|
|
3783
4021
|
return;
|
|
3784
4022
|
}
|
|
3785
4023
|
context.appliedBindingSet.add(binding);
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
absAddress
|
|
3794
|
-
|
|
4024
|
+
// $updatedCallback が定義されていない state では、更新アドレスの集計自体が
|
|
4025
|
+
// 不要(drain 終端の呼び出しごと省略される)。大量バインディング適用時の
|
|
4026
|
+
// Set 蓄積を避ける。undefined(テスト用モック等)は従来通り集計する。
|
|
4027
|
+
if (context.stateElement.hasUpdatedCallback !== false) {
|
|
4028
|
+
const absAddress = getAbsoluteStateAddressByBinding(binding);
|
|
4029
|
+
if (context.updatedAbsAddressSetByStateElement.has(context.stateElement)) {
|
|
4030
|
+
const addressSet = context.updatedAbsAddressSetByStateElement.get(context.stateElement);
|
|
4031
|
+
addressSet.add(absAddress);
|
|
4032
|
+
}
|
|
4033
|
+
else {
|
|
4034
|
+
context.updatedAbsAddressSetByStateElement.set(context.stateElement, new Set([
|
|
4035
|
+
absAddress
|
|
4036
|
+
]));
|
|
4037
|
+
}
|
|
3795
4038
|
}
|
|
3796
4039
|
if (binding.bindingType === "event") {
|
|
3797
4040
|
return;
|
|
@@ -3807,6 +4050,14 @@ function applyChange(binding, context) {
|
|
|
3807
4050
|
return;
|
|
3808
4051
|
}
|
|
3809
4052
|
}
|
|
4053
|
+
// applyChangeFromBindings のグループ化ループが解決済みルートの一致を検証済みの
|
|
4054
|
+
// 場合、stateName さえ一致すれば getRootNode の再解決(native 呼び出し)を省略
|
|
4055
|
+
// できる。activateContent 経由(フラグメント内の新規 content)も、フラグメントは
|
|
4056
|
+
// setRootNodeByFragment で context.rootNode に解決されるため同じ不変条件が成り立つ。
|
|
4057
|
+
if (context.sameRootVerified === true && binding.stateName === context.stateName) {
|
|
4058
|
+
_applyChange(binding, context);
|
|
4059
|
+
return;
|
|
4060
|
+
}
|
|
3810
4061
|
let rootNode = binding.replaceNode.getRootNode();
|
|
3811
4062
|
if (rootNode instanceof DocumentFragment && !(rootNode instanceof ShadowRoot)) {
|
|
3812
4063
|
rootNode = getRootNodeByFragment(rootNode);
|
|
@@ -3887,6 +4138,9 @@ function applyChangeFromBindings(bindings) {
|
|
|
3887
4138
|
newListValueByAbsAddress: newListValueByAbsAddress,
|
|
3888
4139
|
updatedAbsAddressSetByStateElement: updatedAbsAddressSetByStateElement,
|
|
3889
4140
|
deferredSelectBindings: deferredSelectBindings,
|
|
4141
|
+
// グループ内の binding は下の do/while が「解決済みルート === rootNode」を
|
|
4142
|
+
// 検証してから applyChange に渡す(applyChange 側の getRootNode 省略の根拠)
|
|
4143
|
+
sameRootVerified: true,
|
|
3890
4144
|
};
|
|
3891
4145
|
do {
|
|
3892
4146
|
applyChange(binding, context);
|
|
@@ -3917,7 +4171,8 @@ function applyChangeFromBindings(bindings) {
|
|
|
3917
4171
|
}
|
|
3918
4172
|
|
|
3919
4173
|
const handlerByHandlerKey$1 = new Map();
|
|
3920
|
-
|
|
4174
|
+
// binding を強参照しない台帳(handlerBindingRegistry.ts のリーク解説を参照)
|
|
4175
|
+
const bindingRegistry$1 = createHandlerBindingRegistry();
|
|
3921
4176
|
function getHandlerKey$1(binding, eventName) {
|
|
3922
4177
|
const filterKey = binding.inFilters.map(f => f.filterName + '(' + f.args.join(',') + ')').join('|');
|
|
3923
4178
|
return `${binding.stateName}::${binding.statePathName}::${eventName}::${filterKey}`;
|
|
@@ -3971,21 +4226,15 @@ function attachRadioEventHandler(binding) {
|
|
|
3971
4226
|
handlerByHandlerKey$1.set(key, radioEventHandler);
|
|
3972
4227
|
}
|
|
3973
4228
|
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
|
-
}
|
|
4229
|
+
bindingRegistry$1.add(key, binding);
|
|
3982
4230
|
return true;
|
|
3983
4231
|
}
|
|
3984
4232
|
return false;
|
|
3985
4233
|
}
|
|
3986
4234
|
|
|
3987
4235
|
const handlerByHandlerKey = new Map();
|
|
3988
|
-
|
|
4236
|
+
// binding を強参照しない台帳(handlerBindingRegistry.ts のリーク解説を参照)
|
|
4237
|
+
const bindingRegistry = createHandlerBindingRegistry();
|
|
3989
4238
|
function getHandlerKey(binding, eventName) {
|
|
3990
4239
|
const filterKey = binding.inFilters.map(f => f.filterName + '(' + f.args.join(',') + ')').join('|');
|
|
3991
4240
|
return `${binding.stateName}::${binding.statePathName}::${eventName}::${filterKey}`;
|
|
@@ -4058,14 +4307,7 @@ function attachCheckboxEventHandler(binding) {
|
|
|
4058
4307
|
handlerByHandlerKey.set(key, checkboxEventHandler);
|
|
4059
4308
|
}
|
|
4060
4309
|
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
|
-
}
|
|
4310
|
+
bindingRegistry.add(key, binding);
|
|
4069
4311
|
return true;
|
|
4070
4312
|
}
|
|
4071
4313
|
return false;
|
|
@@ -5460,8 +5702,14 @@ class LoopContextStack {
|
|
|
5460
5702
|
}
|
|
5461
5703
|
}
|
|
5462
5704
|
else {
|
|
5463
|
-
|
|
5464
|
-
|
|
5705
|
+
// With no active loop context the address must be self-contained: the
|
|
5706
|
+
// listIndex chain supplies one index per wildcard. Top-level lists
|
|
5707
|
+
// (wildcardCount 1) always satisfy this. A nested list re-rendered
|
|
5708
|
+
// directly (e.g. replaced via $resolve from outside the loop) also
|
|
5709
|
+
// satisfies it — the for binding's listIndex carries the full ancestor
|
|
5710
|
+
// chain.
|
|
5711
|
+
if (loopContext.listIndex.length !== loopContext.pathInfo.wildcardCount) {
|
|
5712
|
+
raiseError(`Cannot push loop context when there is no active loop context: the list index chain (length ${loopContext.listIndex.length}) does not cover the wildcard path (wildcard count ${loopContext.pathInfo.wildcardCount}).`);
|
|
5465
5713
|
}
|
|
5466
5714
|
}
|
|
5467
5715
|
this._loopContextStack[this._length] = loopContext;
|
|
@@ -6096,7 +6344,12 @@ class Updater {
|
|
|
6096
6344
|
const absoluteAddressSet = new Set(absoluteAddresses);
|
|
6097
6345
|
const processBindings = [];
|
|
6098
6346
|
for (const absoluteAddress of absoluteAddressSet) {
|
|
6099
|
-
|
|
6347
|
+
// peek: バインディングの無いアドレス(リスト置換で enqueue される中間
|
|
6348
|
+
// アドレス等)に空 Set を生成・蓄積しない
|
|
6349
|
+
const bindings = peekBindingSetByAbsoluteStateAddress(absoluteAddress);
|
|
6350
|
+
if (bindings === undefined) {
|
|
6351
|
+
continue;
|
|
6352
|
+
}
|
|
6100
6353
|
for (const binding of bindings) {
|
|
6101
6354
|
if (binding.replaceNode.isConnected === false) {
|
|
6102
6355
|
// 切断されているバインディングは無視
|
|
@@ -6892,7 +7145,8 @@ function dirtyCacheEntryByAbsoluteStateAddress(address) {
|
|
|
6892
7145
|
function checkDependency(handler, address) {
|
|
6893
7146
|
// 動的依存関係の登録
|
|
6894
7147
|
if (handler.addressStackLength > 0) {
|
|
6895
|
-
const
|
|
7148
|
+
const lastAddress = handler.lastAddressStack;
|
|
7149
|
+
const lastInfo = lastAddress?.pathInfo ?? null;
|
|
6896
7150
|
const stateElement = handler.stateElement;
|
|
6897
7151
|
if (lastInfo !== null) {
|
|
6898
7152
|
if (stateElement.getterPaths.has(lastInfo.path) &&
|
|
@@ -6900,6 +7154,27 @@ function checkDependency(handler, address) {
|
|
|
6900
7154
|
// lastInfo.pathはgetterの名前であり、address.pathInfo.pathは
|
|
6901
7155
|
// そのgetterが参照している値のパスである
|
|
6902
7156
|
stateElement.addDynamicDependency(address.pathInfo.path, lastInfo.path);
|
|
7157
|
+
// 他行読み取りの検出: 評価中の getter と読み取り先が同じワイルドカード親
|
|
7158
|
+
// (リスト)を共有し、その階層の listIndex が異なる場合、この getter は
|
|
7159
|
+
// 自行の外に依存する(隣接項目参照など)。該当リストを crossRowListPaths に
|
|
7160
|
+
// 記録し、walkDependency の diff-filter 展開を全行展開へフォールバックさせる。
|
|
7161
|
+
if (address.pathInfo.wildcardCount > 0 && lastInfo.wildcardCount > 0) {
|
|
7162
|
+
const sharedLen = calcWildcardLen(address.pathInfo, lastInfo);
|
|
7163
|
+
if (sharedLen > 0) {
|
|
7164
|
+
let crossRow = false;
|
|
7165
|
+
for (let level = 0; level < sharedLen; level++) {
|
|
7166
|
+
if (address.listIndex?.at(level) !== lastAddress.listIndex?.at(level)) {
|
|
7167
|
+
crossRow = true;
|
|
7168
|
+
break;
|
|
7169
|
+
}
|
|
7170
|
+
}
|
|
7171
|
+
if (crossRow) {
|
|
7172
|
+
for (let level = 0; level < sharedLen; level++) {
|
|
7173
|
+
stateElement.addCrossRowListPath?.(address.pathInfo.wildcardParentPaths[level]);
|
|
7174
|
+
}
|
|
7175
|
+
}
|
|
7176
|
+
}
|
|
7177
|
+
}
|
|
6903
7178
|
}
|
|
6904
7179
|
}
|
|
6905
7180
|
}
|
|
@@ -7102,6 +7377,37 @@ function _walkExpandWildcard(context, currentWildcardIndex, parentListIndex) {
|
|
|
7102
7377
|
}
|
|
7103
7378
|
}
|
|
7104
7379
|
}
|
|
7380
|
+
/**
|
|
7381
|
+
* 静的子展開で訪問する listIndex 群を選ぶ。"diff" でも次の場合は全行に倒す:
|
|
7382
|
+
* - diff に変化が一切見えない再代入(同一参照および内容同一コピーの再代入。
|
|
7383
|
+
* `arr[0].v = 5; s.items = [...arr]` のような in-place 変異後のリフレッシュ
|
|
7384
|
+
* イディオムは diff に映らないため、全行展開で従来挙動を保つ。
|
|
7385
|
+
* 削除だけの置換は除く — 残存行に変化は無く、集計はコンテナ動的エッジが担う)
|
|
7386
|
+
* - 他行を読む getter が検出されたリスト(隣接項目参照など。未変更行の派生値も変わりうる)
|
|
7387
|
+
*/
|
|
7388
|
+
function selectExpansionIndexes(context, sourcePath, _lastValue, _newValue, listDiff) {
|
|
7389
|
+
if (context.listExpansion === "full") {
|
|
7390
|
+
return listDiff.newIndexes;
|
|
7391
|
+
}
|
|
7392
|
+
if (context.stateElement.crossRowListPaths?.has(sourcePath)) {
|
|
7393
|
+
return listDiff.newIndexes;
|
|
7394
|
+
}
|
|
7395
|
+
if (listDiff.addIndexSet.size === 0 && listDiff.changeIndexSet.size === 0) {
|
|
7396
|
+
// 追加も移動も無い。削除も無ければ「変化が見えない再代入」= リフレッシュ意図
|
|
7397
|
+
if (listDiff.deleteIndexSet.size === 0) {
|
|
7398
|
+
return listDiff.newIndexes;
|
|
7399
|
+
}
|
|
7400
|
+
// 削除のみ: 残存行は位置も値も不変なので展開しない
|
|
7401
|
+
return listDiff.changeIndexSet;
|
|
7402
|
+
}
|
|
7403
|
+
if (listDiff.addIndexSet.size === 0) {
|
|
7404
|
+
return listDiff.changeIndexSet;
|
|
7405
|
+
}
|
|
7406
|
+
if (listDiff.changeIndexSet.size === 0) {
|
|
7407
|
+
return listDiff.addIndexSet;
|
|
7408
|
+
}
|
|
7409
|
+
return [...listDiff.addIndexSet, ...listDiff.changeIndexSet];
|
|
7410
|
+
}
|
|
7105
7411
|
function _walkDependency(context, startAddress, callback) {
|
|
7106
7412
|
const stack = [{ address: startAddress, depth: 0 }];
|
|
7107
7413
|
while (stack.length > 0) {
|
|
@@ -7134,7 +7440,7 @@ function _walkDependency(context, startAddress, callback) {
|
|
|
7134
7440
|
const absAddress = createAbsoluteStateAddress(absPathInfo, address.listIndex);
|
|
7135
7441
|
const lastValue = getLastListValueByAbsoluteStateAddress(absAddress);
|
|
7136
7442
|
const listDiff = createListDiff(address.listIndex, lastValue, newValue);
|
|
7137
|
-
for (const listIndex of listDiff
|
|
7443
|
+
for (const listIndex of selectExpansionIndexes(context, sourcePath, lastValue, newValue, listDiff)) {
|
|
7138
7444
|
const depAddress = createStateAddress(depPathInfo, listIndex);
|
|
7139
7445
|
context.result.add(depAddress);
|
|
7140
7446
|
nextEntries.push({ address: depAddress, depth: nextDepth });
|
|
@@ -7228,7 +7534,7 @@ function _walkDependency(context, startAddress, callback) {
|
|
|
7228
7534
|
}
|
|
7229
7535
|
}
|
|
7230
7536
|
}
|
|
7231
|
-
function walkDependency(stateName, stateElement, startAddress, staticDependency, dynamicDependency, listPathSet, stateProxy, searchType, callback) {
|
|
7537
|
+
function walkDependency(stateName, stateElement, startAddress, staticDependency, dynamicDependency, listPathSet, stateProxy, searchType, callback, options) {
|
|
7232
7538
|
const context = {
|
|
7233
7539
|
stateElement: stateElement,
|
|
7234
7540
|
staticMap: staticDependency,
|
|
@@ -7238,6 +7544,7 @@ function walkDependency(stateName, stateElement, startAddress, staticDependency,
|
|
|
7238
7544
|
visited: new Set(),
|
|
7239
7545
|
stateProxy: stateProxy,
|
|
7240
7546
|
searchType: searchType,
|
|
7547
|
+
listExpansion: options?.listExpansion ?? "full",
|
|
7241
7548
|
};
|
|
7242
7549
|
_walkDependency(context, startAddress, callback);
|
|
7243
7550
|
return Array.from(context.result);
|
|
@@ -7303,7 +7610,10 @@ function _setByAddress(target, address, absAddress, value, receiver, handler) {
|
|
|
7303
7610
|
dirtyCacheEntryByAbsoluteStateAddress(absDepAddress);
|
|
7304
7611
|
// 更新対象として登録
|
|
7305
7612
|
updater.enqueueAbsoluteAddress(absDepAddress);
|
|
7306
|
-
}
|
|
7613
|
+
},
|
|
7614
|
+
// リスト置換時は追加行・位置変更行のみ展開する(未変更行の再訪を省く。
|
|
7615
|
+
// $postUpdate の手動リフレッシュは従来通り全行展開のまま)
|
|
7616
|
+
{ listExpansion: "diff" });
|
|
7307
7617
|
}
|
|
7308
7618
|
}
|
|
7309
7619
|
function _setByAddressWithSwap(target, address, absAddress, value, receiver, handler) {
|
|
@@ -8382,6 +8692,10 @@ class State extends HTMLElement {
|
|
|
8382
8692
|
return getBindingsReady(rootNode);
|
|
8383
8693
|
}
|
|
8384
8694
|
__state;
|
|
8695
|
+
_hasUpdatedCallback = false;
|
|
8696
|
+
// 他行を読む getter が検出されたリストパス(diff-filter 展開の全行フォールバック対象)。
|
|
8697
|
+
// 依存マップ(static/dynamic)と同様に追加のみ・クリアしない(安全側に固定される)。
|
|
8698
|
+
_crossRowListPaths = new Set();
|
|
8385
8699
|
_name = 'default';
|
|
8386
8700
|
_initialized = false;
|
|
8387
8701
|
_initializePromise;
|
|
@@ -8443,6 +8757,13 @@ class State extends HTMLElement {
|
|
|
8443
8757
|
this._commandTokenNames = processCommandTokensDeclaration(value);
|
|
8444
8758
|
this._eventTokenNames = processEventTokensDeclaration(value);
|
|
8445
8759
|
this.__state = value;
|
|
8760
|
+
// $updatedCallback の有無を state セット時に確定しておく(in はプロトタイプ
|
|
8761
|
+
// チェーンも見る・getter を評価しない)。drain 側はこのフラグで更新アドレスの
|
|
8762
|
+
// 集計と writable createState をスキップできる。
|
|
8763
|
+
// 注: state セット後に生オブジェクトへ直接 $updatedCallback を後付けする
|
|
8764
|
+
// パターンは検知できない(bindProperty / _state 再セットは検知する)。
|
|
8765
|
+
// ライフサイクルフックは宣言時に定義するのが規約。
|
|
8766
|
+
this._hasUpdatedCallback = STATE_UPDATED_CALLBACK_NAME in value;
|
|
8446
8767
|
// 再 set 時に二重 subscribe しないよう registry をクリアしてから $on を配線し直す。
|
|
8447
8768
|
clearEventTokenRegistry(this);
|
|
8448
8769
|
processOnDeclaration(this, value, this._eventTokenNames);
|
|
@@ -8865,8 +9186,20 @@ class State extends HTMLElement {
|
|
|
8865
9186
|
this._version++;
|
|
8866
9187
|
return this._version;
|
|
8867
9188
|
}
|
|
9189
|
+
get hasUpdatedCallback() {
|
|
9190
|
+
return this._hasUpdatedCallback;
|
|
9191
|
+
}
|
|
9192
|
+
get crossRowListPaths() {
|
|
9193
|
+
return this._crossRowListPaths;
|
|
9194
|
+
}
|
|
9195
|
+
addCrossRowListPath(path) {
|
|
9196
|
+
this._crossRowListPaths.add(path);
|
|
9197
|
+
}
|
|
8868
9198
|
bindProperty(prop, desc) {
|
|
8869
9199
|
Object.defineProperty(this._state, prop, desc);
|
|
9200
|
+
if (prop === STATE_UPDATED_CALLBACK_NAME) {
|
|
9201
|
+
this._hasUpdatedCallback = true;
|
|
9202
|
+
}
|
|
8870
9203
|
}
|
|
8871
9204
|
setInitialState(state) {
|
|
8872
9205
|
if (!this._initialized) {
|